first pass of merging in warp (doesn't build)
This commit is contained in:
@@ -8,18 +8,23 @@ use galaxyui::elements::{
|
||||
MouseStateHandle, Radius, Text,
|
||||
};
|
||||
use galaxyui::fonts::{Properties, Style, Weight};
|
||||
use galaxyui::platform::Cursor;
|
||||
use galaxyui::keymap::Keystroke;
|
||||
use galaxyui::platform::{Cursor, OperatingSystem};
|
||||
use galaxyui::text_layout::ClipConfig;
|
||||
use galaxyui::ui_components::button::ButtonVariant;
|
||||
use galaxyui::ui_components::components::{Coords, UiComponent, UiComponentStyles};
|
||||
use galaxyui::{AppContext, Element, Entity, EntityId, SingletonEntity as _};
|
||||
use itertools::Itertools;
|
||||
use markdown_parser::{FormattedText, FormattedTextFragment, FormattedTextLine};
|
||||
use ordered_float::OrderedFloat;
|
||||
use galaxyui::{AppContext, Element, Entity, EntityId, ModelHandle, SingletonEntity as _};
|
||||
|
||||
use super::model_spec_scores::{
|
||||
render_model_spec_header, render_model_spec_scores, CostRow, CostRowTooltip,
|
||||
ModelSpecScoresLayout, CUSTOM_MODEL_ROUTER_DESCRIPTION, CUSTOM_MODEL_ROUTER_TITLE,
|
||||
MODEL_SPECS_DESCRIPTION, MODEL_SPECS_TITLE, REASONING_LEVEL_DESCRIPTION, REASONING_LEVEL_TITLE,
|
||||
};
|
||||
use crate::ai::custom_model_routers::is_custom_router_id;
|
||||
use crate::ai::execution_profiles::model_menu_items::is_auto;
|
||||
use crate::ai::llms::{
|
||||
is_using_api_key_for_provider, DisableReason, LLMId, LLMInfo, LLMPreferences, LLMProvider,
|
||||
LLMSpec,
|
||||
is_using_api_key_for_provider, should_show_bedrock_icon_for_model, DisableReason, LLMId,
|
||||
LLMInfo, LLMPreferences, LLMProvider, LLMSpec,
|
||||
};
|
||||
use crate::auth::AuthStateProvider;
|
||||
use crate::features::FeatureFlag;
|
||||
@@ -29,19 +34,15 @@ use crate::search::result_renderer::ItemHighlightState;
|
||||
use crate::search::{SearchItem, SyncDataSource};
|
||||
use crate::settings_view::SettingsSection;
|
||||
use crate::terminal::input::inline_menu::{
|
||||
default_navigation_message_items, InlineMenuAction, InlineMenuMessageArgs, InlineMenuType,
|
||||
default_navigation_message_items, styles as inline_styles, DetailsRenderConfig,
|
||||
InlineMenuAction, InlineMenuMessageArgs, InlineMenuType,
|
||||
};
|
||||
use crate::terminal::input::inline_menu::{styles as inline_styles, DetailsRenderConfig};
|
||||
use crate::terminal::input::message_bar::{Message, MessageItem};
|
||||
use crate::terminal::view::ambient_agent::AmbientAgentViewModel;
|
||||
use crate::workspace::WorkspaceAction;
|
||||
use crate::workspaces::user_workspaces::UserWorkspaces;
|
||||
use galaxyui::keymap::Keystroke;
|
||||
use galaxyui::platform::OperatingSystem;
|
||||
|
||||
use super::model_spec_scores::{
|
||||
render_model_spec_header, render_model_spec_scores, CostRow, ModelSpecScoresLayout,
|
||||
MODEL_SPECS_DESCRIPTION, MODEL_SPECS_TITLE, REASONING_LEVEL_DESCRIPTION, REASONING_LEVEL_TITLE,
|
||||
};
|
||||
const AUTO_BEDROCK_TOOLTIP: &str = "Warp uses Bedrock when the model Auto selects supports it; otherwise it may use Warp-hosted inference.";
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct AcceptModel {
|
||||
@@ -130,11 +131,56 @@ fn model_specs_width(app: &AppContext) -> f32 {
|
||||
|
||||
pub struct ModelSelectorDataSource {
|
||||
terminal_view_id: EntityId,
|
||||
ambient_agent_view_model: Option<ModelHandle<AmbientAgentViewModel>>,
|
||||
}
|
||||
|
||||
impl ModelSelectorDataSource {
|
||||
pub fn new(terminal_view_id: EntityId) -> Self {
|
||||
Self { terminal_view_id }
|
||||
pub fn new(
|
||||
terminal_view_id: EntityId,
|
||||
ambient_agent_view_model: Option<ModelHandle<AmbientAgentViewModel>>,
|
||||
) -> Self {
|
||||
Self {
|
||||
terminal_view_id,
|
||||
ambient_agent_view_model,
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns whether a model should appear in the inline picker.
|
||||
/// Custom-endpoint models are suppressed in Oz cloud agent panes because
|
||||
/// they cannot route through Warp's cloud inference infrastructure.
|
||||
pub(crate) fn include_model_in_picker(is_cloud_pane: bool, is_custom_endpoint: bool) -> bool {
|
||||
!is_cloud_pane || !is_custom_endpoint
|
||||
}
|
||||
|
||||
fn order_model_choices<'a>(
|
||||
llm_preferences: &LLMPreferences,
|
||||
choices: Vec<&'a LLMInfo>,
|
||||
) -> Vec<&'a LLMInfo> {
|
||||
let mut auto_choices = Vec::new();
|
||||
let mut custom_router_choices = Vec::new();
|
||||
let mut custom_choices = Vec::new();
|
||||
let mut other_choices = Vec::new();
|
||||
|
||||
for llm in choices {
|
||||
// Check custom router before is_auto because custom router ids contain
|
||||
// "auto" and would otherwise land in auto_choices.
|
||||
if is_custom_router_id(llm.id.as_str()) {
|
||||
custom_router_choices.push(llm);
|
||||
} else if is_auto(llm) {
|
||||
auto_choices.push(llm);
|
||||
} else if llm_preferences.custom_llm_info_for_id(&llm.id).is_some() {
|
||||
custom_choices.push(llm);
|
||||
} else {
|
||||
other_choices.push(llm);
|
||||
}
|
||||
}
|
||||
|
||||
auto_choices
|
||||
.into_iter()
|
||||
.chain(custom_router_choices)
|
||||
.chain(custom_choices)
|
||||
.chain(other_choices)
|
||||
.collect()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -161,13 +207,25 @@ impl SyncDataSource for ModelSelectorDataSource {
|
||||
.clone()
|
||||
};
|
||||
|
||||
let choices: Vec<&LLMInfo> = if is_full_terminal {
|
||||
llm_preferences.get_cli_agent_llm_choices().collect_vec()
|
||||
let is_cloud_pane = self.ambient_agent_view_model.is_some();
|
||||
let choices = if is_full_terminal {
|
||||
llm_preferences
|
||||
.get_cli_agent_llm_choices(app)
|
||||
.filter(|llm| {
|
||||
let is_custom = llm_preferences.custom_llm_info_for_id(&llm.id).is_some();
|
||||
Self::include_model_in_picker(is_cloud_pane, is_custom)
|
||||
})
|
||||
.collect_vec()
|
||||
} else {
|
||||
llm_preferences
|
||||
.get_base_llm_choices_for_agent_mode()
|
||||
.get_base_llm_choices_for_agent_mode(app)
|
||||
.filter(|llm| {
|
||||
let is_custom = llm_preferences.custom_llm_info_for_id(&llm.id).is_some();
|
||||
Self::include_model_in_picker(is_cloud_pane, is_custom)
|
||||
})
|
||||
.collect_vec()
|
||||
};
|
||||
let choices = Self::order_model_choices(llm_preferences, choices);
|
||||
|
||||
let query_text = query.text.trim().to_lowercase();
|
||||
|
||||
@@ -210,13 +268,21 @@ struct ModelSearchItem {
|
||||
id: LLMId,
|
||||
provider: LLMProvider,
|
||||
spec: Option<LLMSpec>,
|
||||
provider_icon: Option<Icon>,
|
||||
leading_icon: Icon,
|
||||
credential_icon: Option<Icon>,
|
||||
display_text: String,
|
||||
is_selected: bool,
|
||||
is_custom_endpoint: bool,
|
||||
is_custom_router: bool,
|
||||
/// Source/routing description for custom model routers (from `LLMInfo.description`).
|
||||
description: Option<String>,
|
||||
disable_reason: Option<DisableReason>,
|
||||
is_auto: bool,
|
||||
is_using_bedrock: bool,
|
||||
name_match_result: Option<FuzzyMatchResult>,
|
||||
score: OrderedFloat<f64>,
|
||||
manage_api_key_mouse_state: MouseStateHandle,
|
||||
cost_row_tooltip_mouse_state: MouseStateHandle,
|
||||
reasoning_level: Option<String>,
|
||||
discount_percentage: Option<f32>,
|
||||
}
|
||||
@@ -232,17 +298,44 @@ impl ModelSearchItem {
|
||||
} else {
|
||||
llm.disable_reason.clone()
|
||||
};
|
||||
let is_custom_endpoint = LLMPreferences::as_ref(app)
|
||||
.custom_llm_info_for_id(&llm.id)
|
||||
.is_some();
|
||||
let is_custom_router = is_custom_router_id(llm.id.as_str());
|
||||
let is_auto = is_auto(llm);
|
||||
let is_using_bedrock = should_show_bedrock_icon_for_model(llm, app);
|
||||
let is_using_api_key =
|
||||
is_custom_endpoint || is_using_api_key_for_provider(&llm.provider, app);
|
||||
let leading_icon = if is_using_bedrock {
|
||||
Icon::Aws
|
||||
} else if is_custom_router {
|
||||
Icon::Dataflow
|
||||
} else {
|
||||
llm.provider.icon().unwrap_or(Icon::Oz)
|
||||
};
|
||||
let credential_icon = if !is_using_bedrock && is_using_api_key {
|
||||
Some(Icon::Key)
|
||||
} else {
|
||||
None
|
||||
};
|
||||
Self {
|
||||
id: llm.id.clone(),
|
||||
provider: llm.provider.clone(),
|
||||
spec: llm.spec.clone(),
|
||||
provider_icon: llm.provider.icon(),
|
||||
leading_icon,
|
||||
credential_icon,
|
||||
display_text: llm.display_name.clone(),
|
||||
is_selected: &llm.id == active_llm_id,
|
||||
is_custom_endpoint,
|
||||
is_custom_router,
|
||||
description: llm.description.clone(),
|
||||
disable_reason,
|
||||
is_auto,
|
||||
is_using_bedrock,
|
||||
name_match_result: None,
|
||||
score: OrderedFloat(f64::MIN),
|
||||
manage_api_key_mouse_state: Default::default(),
|
||||
cost_row_tooltip_mouse_state: Default::default(),
|
||||
reasoning_level: llm.reasoning_level(),
|
||||
discount_percentage: llm.discount_percentage,
|
||||
}
|
||||
@@ -270,11 +363,7 @@ impl SearchItem for ModelSearchItem {
|
||||
let icon_size = inline_styles::font_size(appearance);
|
||||
let icon_color = inline_styles::icon_color(appearance);
|
||||
|
||||
let icon = self
|
||||
.provider_icon
|
||||
.unwrap_or(Icon::Oz)
|
||||
.to_galaxyui_icon(icon_color)
|
||||
.finish();
|
||||
let icon = self.leading_icon.to_galaxyui_icon(icon_color).finish();
|
||||
|
||||
Container::new(
|
||||
ConstrainedBox::new(icon)
|
||||
@@ -329,14 +418,17 @@ impl SearchItem for ModelSearchItem {
|
||||
let mut row = Flex::row()
|
||||
.with_cross_axis_alignment(CrossAxisAlignment::Center)
|
||||
.with_child(text.finish());
|
||||
|
||||
if is_using_api_key_for_provider(&self.provider, app) {
|
||||
let key_icon =
|
||||
ConstrainedBox::new(Icon::Key.to_galaxyui_icon(secondary_text_color).finish())
|
||||
if let Some(icon) = self.credential_icon {
|
||||
let credential_icon =
|
||||
ConstrainedBox::new(icon.to_galaxyui_icon(secondary_text_color).finish())
|
||||
.with_width(font_size)
|
||||
.with_height(font_size)
|
||||
.finish();
|
||||
row = row.with_child(Container::new(key_icon).with_margin_left(6.).finish());
|
||||
row = row.with_child(
|
||||
Container::new(credential_icon)
|
||||
.with_margin_left(6.)
|
||||
.finish(),
|
||||
);
|
||||
}
|
||||
|
||||
if self.is_selected {
|
||||
@@ -379,7 +471,7 @@ impl SearchItem for ModelSearchItem {
|
||||
|
||||
if should_show_discount_chip(
|
||||
self.discount_percentage,
|
||||
is_using_api_key_for_provider(&self.provider, app),
|
||||
is_using_api_key_for_provider(&self.provider, app) || self.is_using_bedrock,
|
||||
) {
|
||||
let discount_percentage = self.discount_percentage.unwrap_or(0.);
|
||||
let chip = Container::new(
|
||||
@@ -412,11 +504,35 @@ impl SearchItem for ModelSearchItem {
|
||||
}
|
||||
|
||||
fn render_details(&self, app: &AppContext) -> Option<Box<dyn Element>> {
|
||||
use galaxyui::elements::{Flex, ParentElement as _};
|
||||
|
||||
let appearance = crate::appearance::Appearance::as_ref(app);
|
||||
let theme = appearance.theme();
|
||||
|
||||
// Custom auto models get an informational blurb instead of spec bars.
|
||||
if self.is_custom_router {
|
||||
let header = render_model_spec_header(
|
||||
CUSTOM_MODEL_ROUTER_TITLE,
|
||||
CUSTOM_MODEL_ROUTER_DESCRIPTION,
|
||||
app,
|
||||
);
|
||||
let source_text = Text::new(
|
||||
self.description.as_deref().unwrap_or("").to_string(),
|
||||
appearance.ui_font_family(),
|
||||
inline_styles::font_size(appearance),
|
||||
)
|
||||
.with_color(theme.disabled_ui_text_color().into())
|
||||
.finish();
|
||||
let column = Flex::column()
|
||||
.with_child(Container::new(header).with_margin_bottom(12.).finish())
|
||||
.with_child(source_text)
|
||||
.finish();
|
||||
return Some(
|
||||
ConstrainedBox::new(column)
|
||||
.with_width(model_specs_width(app))
|
||||
.finish(),
|
||||
);
|
||||
}
|
||||
|
||||
let (title, description) = if self.reasoning_level.is_some() {
|
||||
(REASONING_LEVEL_TITLE, REASONING_LEVEL_DESCRIPTION)
|
||||
} else {
|
||||
@@ -424,8 +540,15 @@ impl SearchItem for ModelSearchItem {
|
||||
};
|
||||
let header = render_model_spec_header(title, description, app);
|
||||
|
||||
let is_using_api_key = is_using_api_key_for_provider(&self.provider, app);
|
||||
let cost_row = if is_using_api_key {
|
||||
let is_using_api_key =
|
||||
self.is_custom_endpoint || is_using_api_key_for_provider(&self.provider, app);
|
||||
let cost_row = if self.is_using_bedrock || is_using_api_key {
|
||||
let search_query = if self.is_using_bedrock {
|
||||
"bedrock"
|
||||
} else {
|
||||
"api"
|
||||
}
|
||||
.to_string();
|
||||
let manage_button = appearance
|
||||
.ui_builder()
|
||||
.button(
|
||||
@@ -445,15 +568,29 @@ impl SearchItem for ModelSearchItem {
|
||||
})
|
||||
.with_cursor(Some(Cursor::PointingHand))
|
||||
.build()
|
||||
.on_click(|ctx, _, _| {
|
||||
.on_click(move |ctx, _, _| {
|
||||
ctx.dispatch_typed_action(WorkspaceAction::ShowSettingsPageWithSearch {
|
||||
search_query: "api".to_string(),
|
||||
search_query: search_query.clone(),
|
||||
section: Some(SettingsSection::WarpAgent),
|
||||
});
|
||||
})
|
||||
.finish();
|
||||
|
||||
CostRow::BilledToApi {
|
||||
CostRow::BilledToProvider {
|
||||
label: if self.is_using_bedrock && self.is_auto {
|
||||
"Inference may use Bedrock"
|
||||
} else if self.is_using_bedrock {
|
||||
"Inference via Bedrock"
|
||||
} else {
|
||||
"Inference via API key"
|
||||
},
|
||||
tooltip: if self.is_using_bedrock && self.is_auto {
|
||||
Some(CostRowTooltip {
|
||||
text: AUTO_BEDROCK_TOOLTIP,
|
||||
mouse_state: self.cost_row_tooltip_mouse_state.clone(),
|
||||
})
|
||||
} else {
|
||||
None
|
||||
},
|
||||
manage_button: Container::new(manage_button).finish(),
|
||||
}
|
||||
} else {
|
||||
@@ -493,7 +630,7 @@ impl SearchItem for ModelSearchItem {
|
||||
|
||||
// Show a BYOK option when the user's tier supports it and the provider
|
||||
// is one that accepts user-supplied API keys.
|
||||
let byok_available = UserWorkspaces::as_ref(app).is_byo_api_key_enabled()
|
||||
let byok_available = UserWorkspaces::as_ref(app).is_byo_api_key_enabled(app)
|
||||
&& matches!(
|
||||
self.provider,
|
||||
LLMProvider::OpenAI | LLMProvider::Anthropic | LLMProvider::Google
|
||||
|
||||
Reference in New Issue
Block a user