first pass of merging in warp (doesn't build)
This commit is contained in:
@@ -1,11 +1,10 @@
|
||||
use galaxyui::{Entity, EntityId, ModelContext, SingletonEntity, WindowId};
|
||||
use std::collections::HashMap;
|
||||
|
||||
use crate::{
|
||||
ai::execution_profiles::profiles::ClientProfileId,
|
||||
pane_group::{ExecutionProfileEditorPane, PaneContent},
|
||||
PaneViewLocator,
|
||||
};
|
||||
|
||||
use crate::ai::execution_profiles::profiles::ClientProfileId;
|
||||
use crate::pane_group::{ExecutionProfileEditorPane, PaneContent};
|
||||
use crate::PaneViewLocator;
|
||||
|
||||
/// Manages execution profile editor panes across different windows and profiles.
|
||||
///
|
||||
|
||||
@@ -1,46 +1,53 @@
|
||||
use std::path::{Path, PathBuf};
|
||||
|
||||
use ai::api_keys::{ApiKeyManager, ApiKeyManagerEvent};
|
||||
use itertools::Itertools;
|
||||
use regex::Regex;
|
||||
use thousands::Separable;
|
||||
use galaxy_core::ui::theme::color::internal_colors;
|
||||
use warpui::elements::{
|
||||
Align, Border, ChildView, ClippedScrollStateHandle, ClippedScrollable, ConstrainedBox,
|
||||
Container, CrossAxisAlignment, Expanded, Flex, Highlight, MouseStateHandle, ParentElement,
|
||||
PartialClickableElement, ScrollbarWidth, Text,
|
||||
};
|
||||
use warpui::fonts::Properties;
|
||||
use warpui::platform::Cursor;
|
||||
use warpui::ui_components::slider::SliderStateHandle;
|
||||
use warpui::ui_components::switch::SwitchStateHandle;
|
||||
use warpui::{
|
||||
AppContext, Element, Entity, ModelHandle, SingletonEntity, TypedActionView, View, ViewContext,
|
||||
ViewHandle,
|
||||
};
|
||||
|
||||
use crate::ai::blocklist::BlocklistAIPermissions;
|
||||
use crate::ai::execution_profiles::model_menu_items::available_model_menu_items;
|
||||
use crate::ai::execution_profiles::profiles::{
|
||||
AIExecutionProfilesModel, AIExecutionProfilesModelEvent, ClientProfileId,
|
||||
};
|
||||
use crate::ai::execution_profiles::{
|
||||
profiles::{AIExecutionProfilesModel, AIExecutionProfilesModelEvent, ClientProfileId},
|
||||
AIExecutionProfile, ActionPermission, WriteToPtyPermission,
|
||||
AIExecutionProfile, AIExecutionProfileAppExt as _, ActionPermission, RunAgentsPermission,
|
||||
WriteToPtyPermission,
|
||||
};
|
||||
use crate::ai::llms::{
|
||||
DisableReason, LLMContextWindow, LLMId, LLMInfo, LLMPreferences, LLMPreferencesEvent,
|
||||
};
|
||||
use crate::ai::llms::{DisableReason, LLMId, LLMInfo, LLMPreferences, LLMPreferencesEvent};
|
||||
use crate::ai::paths::host_native_absolute_path;
|
||||
use crate::editor::InteractionState;
|
||||
use crate::editor::{EditorView, Event as EditorEvent, SingleLineEditorOptions};
|
||||
use crate::editor::{
|
||||
EditorView, Event as EditorEvent, InteractionState, SingleLineEditorOptions, TextOptions,
|
||||
};
|
||||
use crate::pane_group::focus_state::PaneFocusHandle;
|
||||
use crate::settings::{AISettings, AgentModeCommandExecutionPredicate};
|
||||
use crate::pane_group::pane::view;
|
||||
use crate::pane_group::{BackingView, PaneConfiguration, PaneEvent};
|
||||
use crate::settings::{AISettings, AISettingsChangedEvent, AgentModeCommandExecutionPredicate};
|
||||
use crate::ui_components::icons::Icon;
|
||||
use crate::view_components::action_button::{ActionButton, DangerSecondaryTheme};
|
||||
use crate::view_components::dropdown::DropdownAction;
|
||||
use crate::view_components::{
|
||||
action_button::{ActionButton, DangerSecondaryTheme},
|
||||
Dropdown, DropdownItem, FilterableDropdown, SubmittableTextInput, SubmittableTextInputEvent,
|
||||
};
|
||||
use crate::workspace::WorkspaceAction;
|
||||
use crate::workspaces::user_workspaces::UserWorkspacesEvent;
|
||||
use crate::TemplatableMCPServerManager;
|
||||
use crate::UserWorkspaces;
|
||||
use crate::{
|
||||
pane_group::{pane::view, BackingView, PaneConfiguration, PaneEvent},
|
||||
Appearance,
|
||||
};
|
||||
use ai::api_keys::{ApiKeyManager, ApiKeyManagerEvent};
|
||||
use galaxy_core::ui::theme::color::internal_colors;
|
||||
use galaxyui::fonts::Properties;
|
||||
use galaxyui::platform::Cursor;
|
||||
use galaxyui::ui_components::switch::SwitchStateHandle;
|
||||
use itertools::Itertools;
|
||||
use regex::Regex;
|
||||
|
||||
use galaxyui::{
|
||||
elements::{
|
||||
Align, Border, ChildView, ClippedScrollStateHandle, ClippedScrollable, ConstrainedBox,
|
||||
Container, CrossAxisAlignment, Expanded, Flex, Highlight, MouseStateHandle, ParentElement,
|
||||
PartialClickableElement, ScrollbarWidth, Text,
|
||||
},
|
||||
AppContext, Element, Entity, ModelHandle, SingletonEntity, TypedActionView, View, ViewContext,
|
||||
ViewHandle,
|
||||
};
|
||||
use std::path::{Path, PathBuf};
|
||||
use crate::{Appearance, TemplatableMCPServerManager, UserWorkspaces};
|
||||
|
||||
const MODEL_MENU_WIDTH: f32 = 480.;
|
||||
|
||||
@@ -119,10 +126,10 @@ struct TooltipMouseStateHandles {
|
||||
write_to_pty_tooltip_mouse_state: MouseStateHandle,
|
||||
computer_use_tooltip_mouse_state: MouseStateHandle,
|
||||
ask_user_question_tooltip_mouse_state: MouseStateHandle,
|
||||
run_agents_tooltip_mouse_state: MouseStateHandle,
|
||||
call_mcp_servers_tooltip_mouse_state: MouseStateHandle,
|
||||
// Separate mouse state handles for text input editors (for workspace override tooltips)
|
||||
command_allowlist_editor_tooltip_mouse_state: MouseStateHandle,
|
||||
command_denylist_editor_tooltip_mouse_state: MouseStateHandle,
|
||||
directory_allowlist_editor_tooltip_mouse_state: MouseStateHandle,
|
||||
mcp_allowlist_editor_tooltip_mouse_state: MouseStateHandle,
|
||||
mcp_denylist_editor_tooltip_mouse_state: MouseStateHandle,
|
||||
@@ -145,6 +152,15 @@ pub enum ExecutionProfileEditorViewAction {
|
||||
SetBaseModel {
|
||||
id: LLMId,
|
||||
},
|
||||
/// Fired continuously while the user drags the context window slider.
|
||||
ContextWindowSliderDragged {
|
||||
value: u32,
|
||||
},
|
||||
/// Fired when the user commits a new context window value (slider drop,
|
||||
/// track click, or input box commit).
|
||||
SetContextWindowSize {
|
||||
value: u32,
|
||||
},
|
||||
SetCodingModel {
|
||||
id: LLMId,
|
||||
},
|
||||
@@ -177,6 +193,9 @@ pub enum ExecutionProfileEditorViewAction {
|
||||
SetAskUserQuestion {
|
||||
permission: super::AskUserQuestionPermission,
|
||||
},
|
||||
SetRunAgents {
|
||||
permission: RunAgentsPermission,
|
||||
},
|
||||
AddToCommandAllowlist {
|
||||
predicate: AgentModeCommandExecutionPredicate,
|
||||
},
|
||||
@@ -222,6 +241,10 @@ pub struct ExecutionProfileEditorView {
|
||||
focus_handle: Option<PaneFocusHandle>,
|
||||
clipped_scroll_state: ClippedScrollStateHandle,
|
||||
base_model_dropdown: ViewHandle<FilterableDropdown<ExecutionProfileEditorViewAction>>,
|
||||
context_window_slider_state: SliderStateHandle,
|
||||
context_window_editor: ViewHandle<EditorView>,
|
||||
last_synced_context_window_editor_value: Option<u32>,
|
||||
dragged_context_window_value: Option<u32>,
|
||||
coding_model_dropdown: ViewHandle<Dropdown<ExecutionProfileEditorViewAction>>,
|
||||
full_terminal_use_model_dropdown:
|
||||
ViewHandle<FilterableDropdown<ExecutionProfileEditorViewAction>>,
|
||||
@@ -233,11 +256,13 @@ pub struct ExecutionProfileEditorView {
|
||||
call_mcp_servers_dropdown: ViewHandle<Dropdown<ExecutionProfileEditorViewAction>>,
|
||||
computer_use_dropdown: ViewHandle<Dropdown<ExecutionProfileEditorViewAction>>,
|
||||
ask_user_question_dropdown: ViewHandle<Dropdown<ExecutionProfileEditorViewAction>>,
|
||||
run_agents_dropdown: ViewHandle<Dropdown<ExecutionProfileEditorViewAction>>,
|
||||
command_allowlist_editor: ViewHandle<SubmittableTextInput>,
|
||||
command_denylist_editor: ViewHandle<SubmittableTextInput>,
|
||||
directory_allowlist_editor: ViewHandle<SubmittableTextInput>,
|
||||
command_allowlist_mouse_state_handles: Vec<MouseStateHandle>,
|
||||
command_denylist_mouse_state_handles: Vec<MouseStateHandle>,
|
||||
command_denylist_tooltip_mouse_state_handles: Vec<MouseStateHandle>,
|
||||
directory_allowlist_mouse_state_handles: Vec<MouseStateHandle>,
|
||||
mcp_allowlist_dropdown: ViewHandle<FilterableDropdown<ExecutionProfileEditorViewAction>>,
|
||||
mcp_allowlist_mouse_state_handles: Vec<MouseStateHandle>,
|
||||
@@ -451,6 +476,34 @@ impl ExecutionProfileEditorView {
|
||||
dropdown
|
||||
});
|
||||
|
||||
let run_agents_dropdown = ctx.add_typed_action_view(|ctx| {
|
||||
let mut dropdown = Dropdown::new(ctx);
|
||||
dropdown.set_items(
|
||||
vec![
|
||||
DropdownItem::new(
|
||||
"Never",
|
||||
ExecutionProfileEditorViewAction::SetRunAgents {
|
||||
permission: RunAgentsPermission::NeverAllow,
|
||||
},
|
||||
),
|
||||
DropdownItem::new(
|
||||
"Always allow",
|
||||
ExecutionProfileEditorViewAction::SetRunAgents {
|
||||
permission: RunAgentsPermission::AlwaysAllow,
|
||||
},
|
||||
),
|
||||
DropdownItem::new(
|
||||
"Always ask",
|
||||
ExecutionProfileEditorViewAction::SetRunAgents {
|
||||
permission: RunAgentsPermission::AlwaysAsk,
|
||||
},
|
||||
),
|
||||
],
|
||||
ctx,
|
||||
);
|
||||
dropdown
|
||||
});
|
||||
|
||||
let mcp_allowlist_dropdown = ctx.add_typed_action_view(|ctx| {
|
||||
let mut dropdown = FilterableDropdown::new(ctx);
|
||||
dropdown.set_menu_header_to_static("Select MCP servers");
|
||||
@@ -484,6 +537,27 @@ impl ExecutionProfileEditorView {
|
||||
dropdown.set_menu_width(MODEL_MENU_WIDTH, ctx);
|
||||
dropdown
|
||||
});
|
||||
|
||||
// Initialize the context window editor buffer with the profile's
|
||||
// persisted limit (or the active model's max as a sensible default).
|
||||
// The slider's current position is derived from the profile on each
|
||||
// render, so no local Cell is needed.
|
||||
let initial_context_window_value = initial_context_window_display_value(&profile_data, ctx);
|
||||
let context_window_slider_state = SliderStateHandle::default();
|
||||
let context_window_editor = ctx.add_typed_action_view(|ctx| {
|
||||
let options = SingleLineEditorOptions {
|
||||
text: TextOptions {
|
||||
font_size_override: Some(Appearance::as_ref(ctx).ui_font_size()),
|
||||
..Default::default()
|
||||
},
|
||||
..Default::default()
|
||||
};
|
||||
let mut editor = EditorView::single_line(options, ctx);
|
||||
editor.set_buffer_text(&initial_context_window_value.separate_with_commas(), ctx);
|
||||
editor
|
||||
});
|
||||
let last_synced_context_window_editor_value = Some(initial_context_window_value);
|
||||
|
||||
let coding_model_dropdown = ctx.add_typed_action_view(|ctx| {
|
||||
let mut dropdown = Dropdown::new(ctx);
|
||||
dropdown.set_top_bar_max_width(MODEL_MENU_WIDTH);
|
||||
@@ -578,6 +652,10 @@ impl ExecutionProfileEditorView {
|
||||
focus_handle: None,
|
||||
clipped_scroll_state: Default::default(),
|
||||
base_model_dropdown,
|
||||
context_window_slider_state,
|
||||
context_window_editor,
|
||||
last_synced_context_window_editor_value,
|
||||
dragged_context_window_value: None,
|
||||
coding_model_dropdown,
|
||||
full_terminal_use_model_dropdown,
|
||||
computer_use_model_dropdown,
|
||||
@@ -588,11 +666,17 @@ impl ExecutionProfileEditorView {
|
||||
call_mcp_servers_dropdown,
|
||||
computer_use_dropdown,
|
||||
ask_user_question_dropdown,
|
||||
run_agents_dropdown,
|
||||
command_allowlist_editor,
|
||||
command_denylist_editor,
|
||||
directory_allowlist_editor,
|
||||
command_allowlist_mouse_state_handles,
|
||||
command_denylist_mouse_state_handles,
|
||||
command_denylist_tooltip_mouse_state_handles: profile_data
|
||||
.command_denylist
|
||||
.iter()
|
||||
.map(|_| Default::default())
|
||||
.collect(),
|
||||
directory_allowlist_mouse_state_handles,
|
||||
mcp_allowlist_dropdown,
|
||||
mcp_allowlist_mouse_state_handles,
|
||||
@@ -612,6 +696,10 @@ impl ExecutionProfileEditorView {
|
||||
}
|
||||
});
|
||||
|
||||
ctx.subscribe_to_view(&view.context_window_editor, |view, _, event, ctx| {
|
||||
view.handle_context_window_editor_event(event, ctx);
|
||||
});
|
||||
|
||||
ctx.subscribe_to_view(&view.command_allowlist_editor, |view, _, event, ctx| {
|
||||
if let SubmittableTextInputEvent::Submit(s) = event {
|
||||
let predicate = match AgentModeCommandExecutionPredicate::new_regex(s) {
|
||||
@@ -671,7 +759,7 @@ impl ExecutionProfileEditorView {
|
||||
Self::refresh_filterable_model_dropdown(
|
||||
&me.base_model_dropdown,
|
||||
current_permissions.base_model.clone(),
|
||||
|prefs| prefs.get_base_llm_choices_for_agent_mode().collect_vec(),
|
||||
|prefs, app| prefs.get_base_llm_choices_for_agent_mode(app).collect_vec(),
|
||||
|id| ExecutionProfileEditorViewAction::SetBaseModel { id },
|
||||
|prefs| prefs.get_default_base_model().id.clone(),
|
||||
&me.upgrade_footer_mouse_state,
|
||||
@@ -685,7 +773,7 @@ impl ExecutionProfileEditorView {
|
||||
Self::refresh_filterable_model_dropdown(
|
||||
&me.full_terminal_use_model_dropdown,
|
||||
current_permissions.cli_agent_model.clone(),
|
||||
|prefs| prefs.get_cli_agent_llm_choices().collect_vec(),
|
||||
|prefs, app| prefs.get_cli_agent_llm_choices(app).collect_vec(),
|
||||
|id| ExecutionProfileEditorViewAction::SetFullTerminalUseModel { id },
|
||||
|prefs| prefs.get_default_cli_agent_model().id.clone(),
|
||||
&me.upgrade_footer_mouse_state,
|
||||
@@ -694,23 +782,25 @@ impl ExecutionProfileEditorView {
|
||||
Self::refresh_filterable_model_dropdown(
|
||||
&me.computer_use_model_dropdown,
|
||||
current_permissions.computer_use_model.clone(),
|
||||
|prefs| prefs.get_computer_use_llm_choices().collect_vec(),
|
||||
|prefs, _| prefs.get_computer_use_llm_choices().collect_vec(),
|
||||
|id| ExecutionProfileEditorViewAction::SetComputerUseModel { id },
|
||||
|prefs| prefs.get_default_computer_use_model().id.clone(),
|
||||
&me.upgrade_footer_mouse_state,
|
||||
ctx,
|
||||
);
|
||||
me.sync_context_window_editor(ctx, false);
|
||||
}
|
||||
LLMPreferencesEvent::UpdatedActiveAgentModeLLM => {
|
||||
Self::refresh_filterable_model_dropdown(
|
||||
&me.base_model_dropdown,
|
||||
current_permissions.base_model.clone(),
|
||||
|prefs| prefs.get_base_llm_choices_for_agent_mode().collect_vec(),
|
||||
|prefs, app| prefs.get_base_llm_choices_for_agent_mode(app).collect_vec(),
|
||||
|id| ExecutionProfileEditorViewAction::SetBaseModel { id },
|
||||
|prefs| prefs.get_default_base_model().id.clone(),
|
||||
&me.upgrade_footer_mouse_state,
|
||||
ctx,
|
||||
);
|
||||
me.sync_context_window_editor(ctx, false);
|
||||
}
|
||||
LLMPreferencesEvent::UpdatedActiveCodingLLM => {
|
||||
Self::refresh_coding_model_dropdown(
|
||||
@@ -732,7 +822,7 @@ impl ExecutionProfileEditorView {
|
||||
Self::refresh_filterable_model_dropdown(
|
||||
&me.base_model_dropdown,
|
||||
current_permissions.base_model.clone(),
|
||||
|prefs| prefs.get_base_llm_choices_for_agent_mode().collect_vec(),
|
||||
|prefs, app| prefs.get_base_llm_choices_for_agent_mode(app).collect_vec(),
|
||||
|id| ExecutionProfileEditorViewAction::SetBaseModel { id },
|
||||
|prefs| prefs.get_default_base_model().id.clone(),
|
||||
&me.upgrade_footer_mouse_state,
|
||||
@@ -743,6 +833,7 @@ impl ExecutionProfileEditorView {
|
||||
current_permissions.coding_model.clone(),
|
||||
ctx,
|
||||
);
|
||||
me.sync_context_window_editor(ctx, false);
|
||||
ctx.notify();
|
||||
},
|
||||
);
|
||||
@@ -761,6 +852,15 @@ impl ExecutionProfileEditorView {
|
||||
ctx.subscribe_to_model(&workspace, |me, workspace, event, ctx| {
|
||||
if let UserWorkspacesEvent::TeamsChanged = event {
|
||||
Self::update_all_editor_interaction_states(me, workspace, ctx);
|
||||
me.update_mouse_state_handles(ctx);
|
||||
ctx.notify();
|
||||
}
|
||||
});
|
||||
ctx.subscribe_to_model(&AISettings::handle(ctx), |me, _, event, ctx| {
|
||||
if let AISettingsChangedEvent::IsAnyAIEnabled { .. } = event {
|
||||
let workspace = UserWorkspaces::handle(ctx);
|
||||
Self::update_all_editor_interaction_states(me, workspace, ctx);
|
||||
me.sync_context_window_editor(ctx, true);
|
||||
ctx.notify();
|
||||
}
|
||||
});
|
||||
@@ -795,6 +895,12 @@ impl ExecutionProfileEditorView {
|
||||
.map(|_| Default::default())
|
||||
.collect();
|
||||
|
||||
self.command_denylist_tooltip_mouse_state_handles = current_permissions
|
||||
.command_denylist
|
||||
.iter()
|
||||
.map(|_| Default::default())
|
||||
.collect();
|
||||
|
||||
self.directory_allowlist_mouse_state_handles = current_permissions
|
||||
.directory_allowlist
|
||||
.iter()
|
||||
@@ -826,12 +932,13 @@ impl ExecutionProfileEditorView {
|
||||
let computer_use_disabled = !ai_settings.is_computer_use_permissions_editable(ctx);
|
||||
let ask_user_question_disabled =
|
||||
!ai_settings.is_ask_user_question_permissions_editable(ctx);
|
||||
let run_agents_disabled = !ai_settings.is_run_agents_permissions_editable(ctx);
|
||||
let mcp_disabled = !ai_settings.is_mcp_permission_editable(ctx);
|
||||
|
||||
Self::refresh_filterable_model_dropdown(
|
||||
&self.base_model_dropdown,
|
||||
current_permissions.base_model.clone(),
|
||||
|prefs| prefs.get_base_llm_choices_for_agent_mode().collect_vec(),
|
||||
|prefs, app| prefs.get_base_llm_choices_for_agent_mode(app).collect_vec(),
|
||||
|id| ExecutionProfileEditorViewAction::SetBaseModel { id },
|
||||
|prefs| prefs.get_default_base_model().id.clone(),
|
||||
&self.upgrade_footer_mouse_state,
|
||||
@@ -845,7 +952,7 @@ impl ExecutionProfileEditorView {
|
||||
Self::refresh_filterable_model_dropdown(
|
||||
&self.full_terminal_use_model_dropdown,
|
||||
current_permissions.cli_agent_model.clone(),
|
||||
|prefs| prefs.get_cli_agent_llm_choices().collect_vec(),
|
||||
|prefs, app| prefs.get_cli_agent_llm_choices(app).collect_vec(),
|
||||
|id| ExecutionProfileEditorViewAction::SetFullTerminalUseModel { id },
|
||||
|prefs| prefs.get_default_cli_agent_model().id.clone(),
|
||||
&self.upgrade_footer_mouse_state,
|
||||
@@ -854,7 +961,7 @@ impl ExecutionProfileEditorView {
|
||||
Self::refresh_filterable_model_dropdown(
|
||||
&self.computer_use_model_dropdown,
|
||||
current_permissions.computer_use_model.clone(),
|
||||
|prefs| prefs.get_computer_use_llm_choices().collect_vec(),
|
||||
|prefs, _| prefs.get_computer_use_llm_choices().collect_vec(),
|
||||
|id| ExecutionProfileEditorViewAction::SetComputerUseModel { id },
|
||||
|prefs| prefs.get_default_computer_use_model().id.clone(),
|
||||
&self.upgrade_footer_mouse_state,
|
||||
@@ -903,6 +1010,12 @@ impl ExecutionProfileEditorView {
|
||||
ask_user_question_disabled,
|
||||
ctx,
|
||||
);
|
||||
Self::refresh_run_agents_dropdown_menu(
|
||||
&self.run_agents_dropdown,
|
||||
current_permissions.run_agents,
|
||||
run_agents_disabled,
|
||||
ctx,
|
||||
);
|
||||
Self::refresh_mcp_dropdown(
|
||||
&self.mcp_allowlist_dropdown,
|
||||
|uuid| ExecutionProfileEditorViewAction::AddToMCPAllowlist { id: uuid },
|
||||
@@ -919,6 +1032,7 @@ impl ExecutionProfileEditorView {
|
||||
);
|
||||
|
||||
Self::update_profile_name_editor(&self.profile_name_editor, ¤t_permissions, ctx);
|
||||
self.sync_context_window_editor(ctx, false);
|
||||
}
|
||||
|
||||
fn refresh_execution_profile_dropdown_menu(
|
||||
@@ -1022,6 +1136,31 @@ impl ExecutionProfileEditorView {
|
||||
ctx.notify();
|
||||
}
|
||||
|
||||
fn refresh_run_agents_dropdown_menu(
|
||||
menu: &ViewHandle<Dropdown<ExecutionProfileEditorViewAction>>,
|
||||
current_permission: RunAgentsPermission,
|
||||
disabled: bool,
|
||||
ctx: &mut ViewContext<Self>,
|
||||
) {
|
||||
menu.update(ctx, |menu, ctx| {
|
||||
if !disabled {
|
||||
menu.set_enabled(ctx);
|
||||
} else {
|
||||
menu.set_disabled(ctx);
|
||||
}
|
||||
|
||||
let active = match current_permission {
|
||||
RunAgentsPermission::NeverAllow | RunAgentsPermission::Unknown => 0,
|
||||
RunAgentsPermission::AlwaysAllow => 1,
|
||||
RunAgentsPermission::AlwaysAsk => 2,
|
||||
};
|
||||
|
||||
menu.set_selected_by_index(active, ctx);
|
||||
ctx.notify();
|
||||
});
|
||||
ctx.notify();
|
||||
}
|
||||
|
||||
fn refresh_filterable_model_dropdown<G, A, D>(
|
||||
menu: &ViewHandle<FilterableDropdown<ExecutionProfileEditorViewAction>>,
|
||||
profile_model: Option<LLMId>,
|
||||
@@ -1031,7 +1170,7 @@ impl ExecutionProfileEditorView {
|
||||
upgrade_mouse_state: &MouseStateHandle,
|
||||
ctx: &mut ViewContext<Self>,
|
||||
) where
|
||||
G: FnOnce(&LLMPreferences) -> Vec<&LLMInfo>,
|
||||
G: for<'a> FnOnce(&'a LLMPreferences, &AppContext) -> Vec<&'a LLMInfo>,
|
||||
A: Fn(LLMId) -> ExecutionProfileEditorViewAction,
|
||||
D: FnOnce(&LLMPreferences) -> LLMId,
|
||||
{
|
||||
@@ -1046,7 +1185,7 @@ impl ExecutionProfileEditorView {
|
||||
|
||||
let llm_prefs = LLMPreferences::handle(ctx);
|
||||
let llm_prefs = llm_prefs.as_ref(ctx);
|
||||
let choices = get_choices(llm_prefs);
|
||||
let choices = get_choices(llm_prefs, ctx);
|
||||
|
||||
let has_upgrade_gated_models = choices
|
||||
.iter()
|
||||
@@ -1054,7 +1193,7 @@ impl ExecutionProfileEditorView {
|
||||
|
||||
let items = available_model_menu_items(
|
||||
choices,
|
||||
|llm| create_action(llm.id.clone()).into(),
|
||||
|llm| DropdownAction::select_action_and_close(create_action(llm.id.clone())),
|
||||
None,
|
||||
None,
|
||||
false,
|
||||
@@ -1097,13 +1236,15 @@ impl ExecutionProfileEditorView {
|
||||
}
|
||||
|
||||
let choices = LLMPreferences::as_ref(ctx)
|
||||
.get_coding_llm_choices()
|
||||
.get_coding_llm_choices(ctx)
|
||||
.collect_vec();
|
||||
|
||||
let items = available_model_menu_items(
|
||||
choices,
|
||||
|llm| {
|
||||
ExecutionProfileEditorViewAction::SetCodingModel { id: llm.id.clone() }.into()
|
||||
DropdownAction::select_action_and_close(
|
||||
ExecutionProfileEditorViewAction::SetCodingModel { id: llm.id.clone() },
|
||||
)
|
||||
},
|
||||
None,
|
||||
None,
|
||||
@@ -1225,7 +1366,7 @@ impl ExecutionProfileEditorView {
|
||||
|
||||
Self::update_editor_interaction_state(
|
||||
view.command_denylist_editor.as_ref(ctx).editor().clone(),
|
||||
is_any_ai_enabled && !ai_autonomy_settings.has_override_for_execute_commands_denylist(),
|
||||
is_any_ai_enabled,
|
||||
ctx,
|
||||
);
|
||||
|
||||
@@ -1256,10 +1397,112 @@ impl ExecutionProfileEditorView {
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
fn configurable_context_window(&self, app: &AppContext) -> Option<LLMContextWindow> {
|
||||
let profile =
|
||||
BlocklistAIPermissions::as_ref(app).permissions_profile_for_id(app, self.profile_id);
|
||||
profile.configurable_context_window(app)
|
||||
}
|
||||
|
||||
fn current_context_window_display_value(&self, app: &AppContext) -> Option<u32> {
|
||||
let profile =
|
||||
BlocklistAIPermissions::as_ref(app).permissions_profile_for_id(app, self.profile_id);
|
||||
profile.context_window_display_value(app)
|
||||
}
|
||||
|
||||
fn handle_context_window_editor_event(
|
||||
&mut self,
|
||||
event: &EditorEvent,
|
||||
ctx: &mut ViewContext<Self>,
|
||||
) {
|
||||
match event {
|
||||
EditorEvent::Blurred | EditorEvent::Enter => {
|
||||
if !AISettings::as_ref(ctx).is_any_ai_enabled(ctx) {
|
||||
self.sync_context_window_editor(ctx, true);
|
||||
return;
|
||||
}
|
||||
let Some(cw) = self.configurable_context_window(ctx) else {
|
||||
return;
|
||||
};
|
||||
let buffer_text = self.context_window_editor.as_ref(ctx).buffer_text(ctx);
|
||||
let cleaned: String = buffer_text
|
||||
.chars()
|
||||
.filter(|c| !c.is_whitespace() && *c != ',')
|
||||
.collect();
|
||||
if let Ok(parsed) = cleaned.parse::<u32>() {
|
||||
let clamped = parsed.clamp(cw.min, cw.max);
|
||||
if Some(clamped) != self.current_context_window_display_value(ctx) {
|
||||
AIExecutionProfilesModel::handle(ctx).update(ctx, |profiles_model, ctx| {
|
||||
profiles_model.set_context_window_limit(
|
||||
self.profile_id,
|
||||
Some(clamped),
|
||||
ctx,
|
||||
);
|
||||
});
|
||||
}
|
||||
}
|
||||
self.sync_context_window_editor(ctx, true);
|
||||
ctx.notify();
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
|
||||
fn sync_context_window_editor(&mut self, ctx: &mut ViewContext<Self>, force: bool) {
|
||||
self.dragged_context_window_value = None;
|
||||
let Some(value) = self.current_context_window_display_value(ctx) else {
|
||||
self.last_synced_context_window_editor_value = None;
|
||||
self.context_window_slider_state.reset_offset();
|
||||
ctx.notify();
|
||||
return;
|
||||
};
|
||||
|
||||
let formatted = value.separate_with_commas();
|
||||
let should_update = if force {
|
||||
true
|
||||
} else {
|
||||
match self.last_synced_context_window_editor_value {
|
||||
Some(last_value) => {
|
||||
self.context_window_editor.as_ref(ctx).buffer_text(ctx)
|
||||
== last_value.separate_with_commas()
|
||||
}
|
||||
None => true,
|
||||
}
|
||||
};
|
||||
|
||||
if should_update {
|
||||
self.context_window_editor.update(ctx, |editor, ctx| {
|
||||
if editor.buffer_text(ctx) != formatted {
|
||||
editor.system_reset_buffer_text(&formatted, ctx);
|
||||
}
|
||||
});
|
||||
self.last_synced_context_window_editor_value = Some(value);
|
||||
self.context_window_slider_state.reset_offset();
|
||||
ctx.notify();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn initial_context_window_display_value(
|
||||
profile_data: &AIExecutionProfile,
|
||||
app: &AppContext,
|
||||
) -> u32 {
|
||||
profile_data
|
||||
.context_window_display_value(app)
|
||||
.unwrap_or_else(|| {
|
||||
LLMPreferences::as_ref(app)
|
||||
.get_default_base_model()
|
||||
.context_window
|
||||
.default_max
|
||||
})
|
||||
}
|
||||
|
||||
mod ui_helpers;
|
||||
|
||||
#[cfg(test)]
|
||||
#[path = "mod_tests.rs"]
|
||||
mod tests;
|
||||
|
||||
impl View for ExecutionProfileEditorView {
|
||||
fn ui_name() -> &'static str {
|
||||
"ExecutionProfileEditorView"
|
||||
@@ -1278,7 +1521,7 @@ impl View for ExecutionProfileEditorView {
|
||||
&self.profile_name_editor,
|
||||
profile_data.is_default_profile,
|
||||
))
|
||||
.with_child(render_models_section(appearance, self))
|
||||
.with_child(render_models_section(appearance, self, app))
|
||||
.with_child(render_permissions_section(
|
||||
appearance,
|
||||
self,
|
||||
@@ -1323,9 +1566,48 @@ impl TypedActionView for ExecutionProfileEditorView {
|
||||
ctx.emit(ExecutionProfileEditorViewEvent::Pane(PaneEvent::Close));
|
||||
}
|
||||
ExecutionProfileEditorViewAction::SetBaseModel { id } => {
|
||||
// Changing the base model resets any persisted context window
|
||||
// override — the new model may have a different range (or not
|
||||
// be configurable at all). The user can pick a new value for
|
||||
// the new model if they want one.
|
||||
AIExecutionProfilesModel::handle(ctx).update(ctx, |profiles_model, ctx| {
|
||||
profiles_model.set_base_model(self.profile_id, Some(id.clone()), ctx);
|
||||
profiles_model.set_context_window_limit(self.profile_id, None, ctx);
|
||||
});
|
||||
self.sync_context_window_editor(ctx, true);
|
||||
ctx.notify();
|
||||
}
|
||||
ExecutionProfileEditorViewAction::ContextWindowSliderDragged { value } => {
|
||||
if !AISettings::as_ref(ctx).is_any_ai_enabled(ctx) {
|
||||
self.sync_context_window_editor(ctx, true);
|
||||
return;
|
||||
}
|
||||
// Transient drag update: reflect the current slider position
|
||||
// in the input box without persisting to the profile yet.
|
||||
// Persistence happens on SetContextWindowSize (drop / commit).
|
||||
if self.configurable_context_window(ctx).is_some() {
|
||||
self.dragged_context_window_value = Some(*value);
|
||||
let formatted = value.separate_with_commas();
|
||||
self.context_window_editor.update(ctx, |editor, ctx| {
|
||||
editor.system_reset_buffer_text(&formatted, ctx);
|
||||
});
|
||||
ctx.notify();
|
||||
}
|
||||
}
|
||||
ExecutionProfileEditorViewAction::SetContextWindowSize { value } => {
|
||||
self.dragged_context_window_value = None;
|
||||
if !AISettings::as_ref(ctx).is_any_ai_enabled(ctx) {
|
||||
self.sync_context_window_editor(ctx, true);
|
||||
return;
|
||||
}
|
||||
let Some(cw) = self.configurable_context_window(ctx) else {
|
||||
return;
|
||||
};
|
||||
let clamped = (*value).clamp(cw.min, cw.max);
|
||||
AIExecutionProfilesModel::handle(ctx).update(ctx, |profiles_model, ctx| {
|
||||
profiles_model.set_context_window_limit(self.profile_id, Some(clamped), ctx);
|
||||
});
|
||||
self.sync_context_window_editor(ctx, true);
|
||||
ctx.notify();
|
||||
}
|
||||
ExecutionProfileEditorViewAction::SetCodingModel { id } => {
|
||||
@@ -1388,6 +1670,12 @@ impl TypedActionView for ExecutionProfileEditorView {
|
||||
});
|
||||
ctx.notify();
|
||||
}
|
||||
ExecutionProfileEditorViewAction::SetRunAgents { permission } => {
|
||||
AIExecutionProfilesModel::handle(ctx).update(ctx, |profiles_model, ctx| {
|
||||
profiles_model.set_run_agents(self.profile_id, *permission, ctx);
|
||||
});
|
||||
ctx.notify();
|
||||
}
|
||||
ExecutionProfileEditorViewAction::AddToCommandAllowlist { predicate } => {
|
||||
AIExecutionProfilesModel::handle(ctx).update(ctx, |profiles_model, ctx| {
|
||||
profiles_model.add_to_command_allowlist(self.profile_id, predicate, ctx);
|
||||
|
||||
@@ -0,0 +1,315 @@
|
||||
use std::collections::HashMap;
|
||||
|
||||
use galaxy_core::features::FeatureFlag;
|
||||
use warpui::App;
|
||||
|
||||
use super::ui_helpers::context_window_snap_values;
|
||||
use crate::ai::execution_profiles::profiles::AIExecutionProfilesModel;
|
||||
use crate::ai::execution_profiles::{
|
||||
has_configurable_context_window, should_show_long_context_pricing_warning, AIExecutionProfile,
|
||||
AIExecutionProfileAppExt as _,
|
||||
};
|
||||
use crate::ai::llms::{
|
||||
AvailableLLMs, LLMContextWindow, LLMInfo, LLMPreferences, LLMProvider, LLMUsageMetadata,
|
||||
ModelsByFeature,
|
||||
};
|
||||
use crate::ai::mcp::TemplatableMCPServerManager;
|
||||
use crate::auth::auth_manager::AuthManager;
|
||||
use crate::auth::AuthStateProvider;
|
||||
use crate::cloud_object::model::persistence::CloudModel;
|
||||
use crate::network::NetworkStatus;
|
||||
use crate::server::cloud_objects::update_manager::UpdateManager;
|
||||
use crate::server::server_api::ServerApiProvider;
|
||||
use crate::server::sync_queue::SyncQueue;
|
||||
use crate::test_util::settings::initialize_settings_for_tests;
|
||||
use crate::workspaces::team_tester::TeamTesterStatus;
|
||||
use crate::workspaces::user_workspaces::UserWorkspaces;
|
||||
use crate::LaunchMode;
|
||||
fn configurable_model(provider: LLMProvider) -> LLMInfo {
|
||||
LLMInfo {
|
||||
display_name: "test model".to_string(),
|
||||
base_model_name: "test model".to_string(),
|
||||
id: "test-model".into(),
|
||||
reasoning_level: None,
|
||||
usage_metadata: LLMUsageMetadata {
|
||||
request_multiplier: 1,
|
||||
credit_multiplier: None,
|
||||
},
|
||||
description: None,
|
||||
disable_reason: None,
|
||||
vision_supported: false,
|
||||
spec: None,
|
||||
provider,
|
||||
host_configs: HashMap::new(),
|
||||
discount_percentage: None,
|
||||
context_window: LLMContextWindow {
|
||||
is_configurable: true,
|
||||
min: 200_000,
|
||||
max: 1_000_000,
|
||||
default_max: 272_000,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
fn assert_context_window_limit_for_request(
|
||||
model: &LLMInfo,
|
||||
selected_limit: Option<u32>,
|
||||
gpt_configurable_context_window_enabled: bool,
|
||||
expected: Option<u32>,
|
||||
) {
|
||||
let model = model.clone();
|
||||
App::test((), move |mut app| async move {
|
||||
let _flag = FeatureFlag::GPTConfigurableContextWindow
|
||||
.override_enabled(gpt_configurable_context_window_enabled);
|
||||
|
||||
initialize_settings_for_tests(&mut app);
|
||||
app.add_singleton_model(|_| ServerApiProvider::new_for_test());
|
||||
app.add_singleton_model(|_| AuthStateProvider::new_for_test());
|
||||
app.add_singleton_model(AuthManager::new_for_test);
|
||||
app.add_singleton_model(|_| NetworkStatus::new());
|
||||
app.add_singleton_model(UserWorkspaces::default_mock);
|
||||
app.add_singleton_model(CloudModel::mock);
|
||||
app.add_singleton_model(TeamTesterStatus::mock);
|
||||
app.add_singleton_model(SyncQueue::mock);
|
||||
app.add_singleton_model(UpdateManager::mock);
|
||||
app.add_singleton_model(|_| TemplatableMCPServerManager::default());
|
||||
app.add_singleton_model(|ctx| {
|
||||
AIExecutionProfilesModel::new(&LaunchMode::new_for_unit_test(), ctx)
|
||||
});
|
||||
let llm_preferences = app.add_singleton_model(LLMPreferences::new);
|
||||
|
||||
let profile_model_id = model.id.clone();
|
||||
let available_model_id = profile_model_id.clone();
|
||||
llm_preferences.update(&mut app, move |preferences, ctx| {
|
||||
preferences.update_feature_model_choices(
|
||||
Ok(ModelsByFeature {
|
||||
agent_mode: AvailableLLMs::new(available_model_id, [model], None)
|
||||
.expect("test model should create available LLMs"),
|
||||
..Default::default()
|
||||
}),
|
||||
ctx,
|
||||
);
|
||||
});
|
||||
|
||||
let profile = AIExecutionProfile {
|
||||
base_model: Some(profile_model_id),
|
||||
context_window_limit: selected_limit,
|
||||
..Default::default()
|
||||
};
|
||||
app.read(|ctx| {
|
||||
assert_eq!(profile.context_window_limit_for_request(ctx), expected);
|
||||
});
|
||||
});
|
||||
}
|
||||
/// Helper: round-trip f32 → u32 for readable assertions and absorb the
|
||||
/// negligible f64→f32 drift the snap helper picks up on large ranges.
|
||||
fn rounded(values: &[f32]) -> Vec<u32> {
|
||||
values.iter().map(|v| v.round() as u32).collect()
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn snap_values_for_min_eq_max_returns_single_point() {
|
||||
assert_eq!(
|
||||
rounded(&context_window_snap_values(50_000, 50_000)),
|
||||
vec![50_000]
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn snap_values_for_min_gt_max_collapses_to_min() {
|
||||
// Defensive: invalid bounds shouldn't panic, just degrade gracefully.
|
||||
assert_eq!(rounded(&context_window_snap_values(100, 50)), vec![100]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn snap_values_always_include_endpoints() {
|
||||
let values = rounded(&context_window_snap_values(1_000, 200_000));
|
||||
assert_eq!(values.first(), Some(&1_000));
|
||||
assert_eq!(values.last(), Some(&200_000));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn snap_values_for_classic_200k_range_match_legacy_layout() {
|
||||
// Mirrors the old hardcoded list, except `1_000` replaces the missing
|
||||
// round multiple at the start.
|
||||
let values = rounded(&context_window_snap_values(1_000, 200_000));
|
||||
assert_eq!(
|
||||
values,
|
||||
vec![1_000, 25_000, 50_000, 75_000, 100_000, 125_000, 150_000, 175_000, 200_000]
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn snap_values_for_claude_1m_range_pick_100k_steps() {
|
||||
let values = rounded(&context_window_snap_values(200_000, 1_000_000));
|
||||
assert_eq!(
|
||||
values,
|
||||
vec![200_000, 300_000, 400_000, 500_000, 600_000, 700_000, 800_000, 900_000, 1_000_000]
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn snap_values_for_min_zero_skips_duplicate_zero() {
|
||||
let values = rounded(&context_window_snap_values(0, 100));
|
||||
// First entry is min (0), then nice multiples up to and including max.
|
||||
assert_eq!(values.first(), Some(&0));
|
||||
assert_eq!(values.last(), Some(&100));
|
||||
assert!(values.iter().filter(|&&v| v == 0).count() == 1);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn snap_values_for_offset_min_align_to_nice_grid() {
|
||||
// min=26_000 doesn't sit on a 25k boundary; first nice value is 50_000.
|
||||
let values = rounded(&context_window_snap_values(26_000, 200_000));
|
||||
assert_eq!(values.first(), Some(&26_000));
|
||||
assert_eq!(values.last(), Some(&200_000));
|
||||
// Ensure the second point lands on a nice multiple, not on min+step.
|
||||
assert_eq!(values.get(1), Some(&50_000));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn snap_values_keep_count_reasonable_for_huge_range() {
|
||||
// 1B span should still produce a small (~9) snap-point list, not
|
||||
// millions of entries.
|
||||
let values = context_window_snap_values(0, 1_000_000_000);
|
||||
assert!(
|
||||
values.len() <= 12,
|
||||
"expected at most 12 snap points, got {}",
|
||||
values.len()
|
||||
);
|
||||
assert!(
|
||||
values.len() >= 5,
|
||||
"expected at least 5 snap points, got {}",
|
||||
values.len()
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn openai_long_context_warning_starts_above_threshold() {
|
||||
let model = configurable_model(LLMProvider::OpenAI);
|
||||
|
||||
assert!(!should_show_long_context_pricing_warning(
|
||||
&model,
|
||||
Some(200_000),
|
||||
true
|
||||
));
|
||||
assert!(!should_show_long_context_pricing_warning(
|
||||
&model,
|
||||
Some(272_000),
|
||||
true
|
||||
));
|
||||
assert!(should_show_long_context_pricing_warning(
|
||||
&model,
|
||||
Some(272_001),
|
||||
true
|
||||
));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn openai_long_context_warning_clamps_stale_override_to_lowered_model_max() {
|
||||
let mut model = configurable_model(LLMProvider::OpenAI);
|
||||
model.context_window.max = 272_000;
|
||||
|
||||
assert!(!should_show_long_context_pricing_warning(
|
||||
&model,
|
||||
Some(1_000_000),
|
||||
true
|
||||
));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn openai_request_limit_is_clamped_when_configurable_context_is_available() {
|
||||
let model = configurable_model(LLMProvider::OpenAI);
|
||||
assert_context_window_limit_for_request(&model, Some(1_500_000), true, Some(1_000_000));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn openai_request_limit_remains_unset_without_a_selected_override() {
|
||||
let model = configurable_model(LLMProvider::OpenAI);
|
||||
|
||||
assert_context_window_limit_for_request(&model, None, true, None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn custom_endpoint_fixed_context_does_not_expose_control_or_warning() {
|
||||
let mut model = configurable_model(LLMProvider::Unknown);
|
||||
model.context_window.is_configurable = false;
|
||||
model.context_window.max = 200_000;
|
||||
assert!(!has_configurable_context_window(&model, false));
|
||||
assert_context_window_limit_for_request(&model, Some(1_000_000), false, None);
|
||||
assert!(!should_show_long_context_pricing_warning(
|
||||
&model,
|
||||
Some(1_000_000),
|
||||
false
|
||||
));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn openai_configurable_context_uses_server_metadata_without_model_or_host_allowlist() {
|
||||
let mut model = configurable_model(LLMProvider::OpenAI);
|
||||
model.base_model_name = "new-server-configurable-model".to_string();
|
||||
assert!(has_configurable_context_window(&model, true));
|
||||
assert_context_window_limit_for_request(&model, Some(1_000_000), true, Some(1_000_000));
|
||||
assert!(should_show_long_context_pricing_warning(
|
||||
&model,
|
||||
Some(1_000_000),
|
||||
true
|
||||
));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn openai_fixed_context_metadata_does_not_expose_control_or_warning() {
|
||||
let mut model = configurable_model(LLMProvider::OpenAI);
|
||||
model.context_window = LLMContextWindow {
|
||||
is_configurable: false,
|
||||
min: 272_000,
|
||||
max: 272_000,
|
||||
default_max: 272_000,
|
||||
};
|
||||
assert!(!has_configurable_context_window(&model, true));
|
||||
assert_context_window_limit_for_request(&model, Some(1_000_000), true, None);
|
||||
assert!(!should_show_long_context_pricing_warning(
|
||||
&model,
|
||||
Some(1_000_000),
|
||||
true
|
||||
));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn openai_configurable_context_does_not_require_direct_host_metadata() {
|
||||
let model = configurable_model(LLMProvider::OpenAI);
|
||||
|
||||
assert!(has_configurable_context_window(&model, true));
|
||||
assert_context_window_limit_for_request(&model, Some(1_000_000), true, Some(1_000_000));
|
||||
assert!(should_show_long_context_pricing_warning(
|
||||
&model,
|
||||
Some(1_000_000),
|
||||
true
|
||||
));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn openai_expanded_context_is_hidden_while_feature_flag_is_off() {
|
||||
let model = configurable_model(LLMProvider::OpenAI);
|
||||
|
||||
assert!(!has_configurable_context_window(&model, false));
|
||||
assert_context_window_limit_for_request(&model, Some(1_000_000), false, None);
|
||||
assert!(!should_show_long_context_pricing_warning(
|
||||
&model,
|
||||
Some(1_000_000),
|
||||
false
|
||||
));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn non_openai_configurable_context_ignores_gpt_flag_and_does_not_show_openai_warning() {
|
||||
let model = configurable_model(LLMProvider::Anthropic);
|
||||
|
||||
assert!(has_configurable_context_window(&model, false));
|
||||
assert_context_window_limit_for_request(&model, Some(1_000_000), false, Some(1_000_000));
|
||||
assert!(!should_show_long_context_pricing_warning(
|
||||
&model,
|
||||
Some(1_000_000),
|
||||
false
|
||||
));
|
||||
}
|
||||
@@ -1,28 +1,69 @@
|
||||
use crate::ai::execution_profiles::{AIExecutionProfile, ActionPermission};
|
||||
use pathfinder_geometry::vector::vec2f;
|
||||
use thousands::Separable;
|
||||
use uuid::Uuid;
|
||||
use galaxy_core::features::FeatureFlag;
|
||||
use warpui::elements::{
|
||||
ChildAnchor, ChildView, ConstrainedBox, Container, CrossAxisAlignment, Dismiss, Flex,
|
||||
Hoverable, MainAxisAlignment, MainAxisSize, MouseStateHandle, OffsetPositioning, ParentAnchor,
|
||||
ParentElement, ParentOffsetBounds, Shrinkable, Stack, Text,
|
||||
};
|
||||
use warpui::fonts::{Properties, Weight};
|
||||
use warpui::ui_components::components::{Coords, UiComponent, UiComponentStyles};
|
||||
use warpui::{AppContext, Element, SingletonEntity, ViewHandle};
|
||||
|
||||
use super::{ExecutionProfileEditorView, ExecutionProfileEditorViewAction};
|
||||
use crate::ai::blocklist::BlocklistAIPermissions;
|
||||
use crate::ai::execution_profiles::{
|
||||
long_context_pricing_warning_title, AIExecutionProfile, AIExecutionProfileAppExt as _,
|
||||
ActionPermission,
|
||||
};
|
||||
use crate::editor::EditorView;
|
||||
use crate::settings::AISettings;
|
||||
use crate::ui_components::icons::Icon;
|
||||
use crate::view_components::FilterableDropdown;
|
||||
use crate::view_components::{Dropdown, SubmittableTextInput};
|
||||
use crate::Appearance;
|
||||
use crate::TemplatableMCPServerManager;
|
||||
use galaxy_core::features::FeatureFlag;
|
||||
use galaxyui::elements::Hoverable;
|
||||
use galaxyui::elements::MouseStateHandle;
|
||||
use galaxyui::elements::{
|
||||
ChildAnchor, ChildView, ConstrainedBox, Container, CrossAxisAlignment, Flex, MainAxisAlignment,
|
||||
MainAxisSize, OffsetPositioning, ParentAnchor, ParentElement, ParentOffsetBounds, Shrinkable,
|
||||
Stack, Text,
|
||||
use crate::view_components::{
|
||||
render_warning_box, Dropdown, DropdownItemAction, FilterableDropdown, SubmittableTextInput,
|
||||
WarningBoxConfig,
|
||||
};
|
||||
use galaxyui::fonts::{Properties, Weight};
|
||||
use galaxyui::ui_components::components::UiComponent;
|
||||
use galaxyui::AppContext;
|
||||
use galaxyui::{Element, SingletonEntity, ViewHandle};
|
||||
use pathfinder_geometry::vector::vec2f;
|
||||
use uuid::Uuid;
|
||||
use crate::{Appearance, TemplatableMCPServerManager};
|
||||
|
||||
use super::ExecutionProfileEditorView;
|
||||
use super::ExecutionProfileEditorViewAction;
|
||||
const CONTEXT_WINDOW_SLIDER_WIDTH: f32 = 220.;
|
||||
const CONTEXT_WINDOW_INPUT_BOX_WIDTH: f32 = 120.;
|
||||
|
||||
pub(super) fn context_window_snap_values(min: u32, max: u32) -> Vec<f32> {
|
||||
if min >= max {
|
||||
return vec![min as f32];
|
||||
}
|
||||
let range = (max - min) as f64;
|
||||
let step = nice_step(range / 8.0);
|
||||
|
||||
let mut values = vec![min as f32];
|
||||
let mut v = (min as f64 / step).ceil() * step;
|
||||
while v < max as f64 {
|
||||
if v > min as f64 {
|
||||
values.push(v as f32);
|
||||
}
|
||||
v += step;
|
||||
}
|
||||
if values.last().copied() != Some(max as f32) {
|
||||
values.push(max as f32);
|
||||
}
|
||||
values
|
||||
}
|
||||
|
||||
fn nice_step(raw: f64) -> f64 {
|
||||
let magnitude = 10f64.powf(raw.log10().floor());
|
||||
let normalized = raw / magnitude;
|
||||
let nice = if normalized < 1.5 {
|
||||
1.0
|
||||
} else if normalized < 3.5 {
|
||||
2.5
|
||||
} else if normalized < 7.5 {
|
||||
5.0
|
||||
} else {
|
||||
10.0
|
||||
};
|
||||
nice * magnitude
|
||||
}
|
||||
|
||||
use crate::settings_view::{render_input_list, render_separator, InputListItem};
|
||||
|
||||
@@ -90,7 +131,7 @@ pub fn render_section_label(label: &str, appearance: &Appearance) -> Box<dyn Ele
|
||||
.finish()
|
||||
}
|
||||
|
||||
fn render_filterable_dropdown_row<T: Clone + 'static + std::fmt::Debug + Send + Sync>(
|
||||
fn render_filterable_dropdown_row<T: DropdownItemAction>(
|
||||
appearance: &Appearance,
|
||||
label: &str,
|
||||
desc: &str,
|
||||
@@ -161,8 +202,14 @@ fn render_info_section(
|
||||
.finish();
|
||||
Container::new(description).with_margin_bottom(12.).finish()
|
||||
}
|
||||
fn render_long_context_pricing_warning(appearance: &Appearance) -> Box<dyn Element> {
|
||||
render_warning_box(
|
||||
WarningBoxConfig::formatted_title(long_context_pricing_warning_title()),
|
||||
appearance,
|
||||
)
|
||||
}
|
||||
|
||||
fn render_permission_row<T: Clone + 'static + std::fmt::Debug + Send + Sync>(
|
||||
fn render_permission_row<T: DropdownItemAction>(
|
||||
appearance: &Appearance,
|
||||
icon: Icon,
|
||||
label: &str,
|
||||
@@ -212,6 +259,7 @@ fn render_permission_row<T: Clone + 'static + std::fmt::Debug + Send + Sync>(
|
||||
pub fn render_models_section(
|
||||
appearance: &Appearance,
|
||||
view: &ExecutionProfileEditorView,
|
||||
app: &AppContext,
|
||||
) -> Box<dyn Element> {
|
||||
let mut column = Flex::column()
|
||||
.with_child(render_separator(appearance))
|
||||
@@ -221,14 +269,19 @@ pub fn render_models_section(
|
||||
"Base model",
|
||||
"This model serves as the primary engine behind the agent. It powers most interactions and invokes other models for tasks like planning or code generation when necessary. Warp may automatically switch to alternate models based on model availability or for auxiliary tasks such as conversation summarization.",
|
||||
&view.base_model_dropdown,
|
||||
))
|
||||
.with_child(render_filterable_dropdown_row(
|
||||
appearance,
|
||||
"Full terminal use model",
|
||||
"The model used when the agent operates inside interactive terminal applications like database shells, debuggers, REPLs, or dev servers—reading live output and writing commands to the PTY.",
|
||||
&view.full_terminal_use_model_dropdown,
|
||||
));
|
||||
|
||||
if let Some(row) = render_context_window_row(appearance, view, app) {
|
||||
column.add_child(row);
|
||||
}
|
||||
|
||||
column = column.with_child(render_filterable_dropdown_row(
|
||||
appearance,
|
||||
"Full terminal use model",
|
||||
"The model used when the agent operates inside interactive terminal applications like database shells, debuggers, REPLs, or dev servers—reading live output and writing commands to the PTY.",
|
||||
&view.full_terminal_use_model_dropdown,
|
||||
));
|
||||
|
||||
if FeatureFlag::LocalComputerUse.is_enabled() {
|
||||
column.add_child(render_filterable_dropdown_row(
|
||||
appearance,
|
||||
@@ -243,6 +296,150 @@ pub fn render_models_section(
|
||||
.finish()
|
||||
}
|
||||
|
||||
/// Renders a `[min — slider — max] [input]` row beneath the base model
|
||||
/// dropdown. Returns `None` if the active base model doesn't advertise a
|
||||
/// configurable context window or global AI is disabled.
|
||||
fn render_context_window_row(
|
||||
appearance: &Appearance,
|
||||
view: &ExecutionProfileEditorView,
|
||||
app: &AppContext,
|
||||
) -> Option<Box<dyn Element>> {
|
||||
if !AISettings::as_ref(app).is_any_ai_enabled(app) {
|
||||
return None;
|
||||
}
|
||||
let cw = view.configurable_context_window(app)?;
|
||||
let min = cw.min;
|
||||
let max = cw.max;
|
||||
|
||||
let label = Text::new(
|
||||
"Context window".to_string(),
|
||||
appearance.ui_font_family(),
|
||||
13.,
|
||||
)
|
||||
.with_color(appearance.theme().active_ui_text_color().into())
|
||||
.finish();
|
||||
let min_label_text = min.separate_with_commas();
|
||||
let max_label_text = max.separate_with_commas();
|
||||
let desc = Text::new(
|
||||
"The base model's working memory — how many tokens of your conversation, code, and documents it can consider at once. Larger windows enable longer conversations and more coherent responses over bigger codebases, at the cost of higher latency and compute usage.".to_string(),
|
||||
appearance.ui_font_family(),
|
||||
11.,
|
||||
)
|
||||
.with_color(
|
||||
appearance
|
||||
.theme()
|
||||
.sub_text_color(appearance.theme().surface_1())
|
||||
.into(),
|
||||
)
|
||||
.finish();
|
||||
let label_desc = Flex::column().with_child(label).with_child(desc).finish();
|
||||
|
||||
let min_label = Text::new(min_label_text.clone(), appearance.ui_font_family(), 11.)
|
||||
.with_color(
|
||||
appearance
|
||||
.theme()
|
||||
.sub_text_color(appearance.theme().surface_1())
|
||||
.into(),
|
||||
)
|
||||
.finish();
|
||||
let max_label = Text::new(max_label_text.clone(), appearance.ui_font_family(), 11.)
|
||||
.with_color(
|
||||
appearance
|
||||
.theme()
|
||||
.sub_text_color(appearance.theme().surface_1())
|
||||
.into(),
|
||||
)
|
||||
.finish();
|
||||
|
||||
let current_value = view
|
||||
.current_context_window_display_value(app)
|
||||
.unwrap_or(cw.default_max)
|
||||
.clamp(min, max);
|
||||
let slider = appearance
|
||||
.ui_builder()
|
||||
.slider(view.context_window_slider_state.clone())
|
||||
.with_range(min as f32..max as f32)
|
||||
.with_snap_values(context_window_snap_values(min, max))
|
||||
.with_default_value(current_value as f32)
|
||||
.with_style(UiComponentStyles {
|
||||
width: Some(CONTEXT_WINDOW_SLIDER_WIDTH),
|
||||
margin: Some(Coords::default().left(8.).right(8.)),
|
||||
..Default::default()
|
||||
})
|
||||
.on_drag(|ctx, _, val| {
|
||||
ctx.dispatch_typed_action(
|
||||
ExecutionProfileEditorViewAction::ContextWindowSliderDragged {
|
||||
value: val.round() as u32,
|
||||
},
|
||||
);
|
||||
})
|
||||
.on_change(|ctx, _, val| {
|
||||
ctx.dispatch_typed_action(ExecutionProfileEditorViewAction::SetContextWindowSize {
|
||||
value: val.round() as u32,
|
||||
});
|
||||
})
|
||||
.build()
|
||||
.finish();
|
||||
|
||||
let context_window_editor = view.context_window_editor.clone();
|
||||
let input_box = Dismiss::new(
|
||||
appearance
|
||||
.ui_builder()
|
||||
.text_input(view.context_window_editor.clone())
|
||||
.with_style(UiComponentStyles {
|
||||
width: Some(CONTEXT_WINDOW_INPUT_BOX_WIDTH),
|
||||
padding: Some(Coords {
|
||||
top: 6.,
|
||||
bottom: 6.,
|
||||
left: 10.,
|
||||
right: 10.,
|
||||
}),
|
||||
margin: Some(Coords::default().left(12.)),
|
||||
background: Some(appearance.theme().surface_2().into()),
|
||||
..Default::default()
|
||||
})
|
||||
.build()
|
||||
.finish(),
|
||||
)
|
||||
.on_dismiss(move |ctx, app| {
|
||||
let buffer_text = context_window_editor.as_ref(app).buffer_text(app);
|
||||
let cleaned: String = buffer_text
|
||||
.chars()
|
||||
.filter(|c| !c.is_whitespace() && *c != ',')
|
||||
.collect();
|
||||
if let Ok(parsed) = cleaned.parse::<u32>() {
|
||||
ctx.dispatch_typed_action(ExecutionProfileEditorViewAction::SetContextWindowSize {
|
||||
value: parsed,
|
||||
});
|
||||
}
|
||||
})
|
||||
.finish();
|
||||
|
||||
let slider_row = Flex::row()
|
||||
.with_cross_axis_alignment(CrossAxisAlignment::Center)
|
||||
.with_child(min_label)
|
||||
.with_child(Shrinkable::new(1., slider).finish())
|
||||
.with_child(max_label)
|
||||
.with_child(input_box)
|
||||
.finish();
|
||||
|
||||
let mut column = Flex::column()
|
||||
.with_child(Container::new(label_desc).with_margin_bottom(4.).finish())
|
||||
.with_child(slider_row);
|
||||
if BlocklistAIPermissions::as_ref(app)
|
||||
.permissions_profile_for_id(app, view.profile_id())
|
||||
.should_show_long_context_pricing_warning(view.dragged_context_window_value, app)
|
||||
{
|
||||
column.add_child(render_long_context_pricing_warning(appearance));
|
||||
}
|
||||
|
||||
Some(
|
||||
Container::new(column.finish())
|
||||
.with_margin_bottom(12.)
|
||||
.finish(),
|
||||
)
|
||||
}
|
||||
|
||||
pub fn render_permissions_section(
|
||||
appearance: &Appearance,
|
||||
view: &ExecutionProfileEditorView,
|
||||
@@ -362,6 +559,17 @@ pub fn render_permissions_section(
|
||||
.ask_user_question_tooltip_mouse_state
|
||||
.clone(),
|
||||
));
|
||||
column.add_child(render_permission_row(
|
||||
appearance,
|
||||
Icon::Atom,
|
||||
"Run orchestrated agents",
|
||||
&view.run_agents_dropdown,
|
||||
profile_data.run_agents.description(),
|
||||
!ai_settings.is_run_agents_permissions_editable(app),
|
||||
view.tooltip_mouse_state_handles
|
||||
.run_agents_tooltip_mouse_state
|
||||
.clone(),
|
||||
));
|
||||
|
||||
column.add_child(render_permission_row(
|
||||
appearance,
|
||||
@@ -475,10 +683,12 @@ where
|
||||
item: display_fn(&item),
|
||||
mouse_state_handle,
|
||||
on_remove_action: on_remove_action(item),
|
||||
is_disabled: !is_editable,
|
||||
tooltip_mouse_state: None,
|
||||
})
|
||||
.collect();
|
||||
|
||||
let list = render_input_list(None, input_items, editor, !is_editable, appearance);
|
||||
let list = render_input_list(None, input_items, editor, appearance);
|
||||
let list_element = if !is_editable {
|
||||
wrap_disabled_with_workspace_override_tooltip(list, tooltip_mouse_state, appearance)
|
||||
} else {
|
||||
@@ -558,24 +768,58 @@ fn render_command_denylist_section(
|
||||
appearance: &Appearance,
|
||||
app: &galaxyui::AppContext,
|
||||
) -> Box<dyn Element> {
|
||||
let ai_settings = AISettings::as_ref(app);
|
||||
let is_editable = ai_settings.is_command_denylist_editable(app);
|
||||
|
||||
render_list_section(
|
||||
let ai_disabled = !AISettings::as_ref(app).is_any_ai_enabled(app);
|
||||
let org_denylist = BlocklistAIPermissions::get_org_execute_commands_denylist(app);
|
||||
let mut tooltip_idx = 0usize;
|
||||
|
||||
let input_items: Vec<InputListItem<ExecutionProfileEditorViewAction>> = profile_data
|
||||
.command_denylist
|
||||
.iter()
|
||||
.cloned()
|
||||
.zip(view.command_denylist_mouse_state_handles.iter().cloned())
|
||||
.rev()
|
||||
.map(|(predicate, mouse_state_handle)| {
|
||||
let is_org = org_denylist.contains(&predicate);
|
||||
let tooltip_mouse_state = if is_org {
|
||||
let handle = view
|
||||
.command_denylist_tooltip_mouse_state_handles
|
||||
.get(tooltip_idx)
|
||||
.cloned();
|
||||
tooltip_idx += 1;
|
||||
handle
|
||||
} else {
|
||||
None
|
||||
};
|
||||
InputListItem {
|
||||
item: predicate.to_string(),
|
||||
mouse_state_handle,
|
||||
on_remove_action: ExecutionProfileEditorViewAction::RemoveFromCommandDenylist {
|
||||
predicate,
|
||||
},
|
||||
is_disabled: is_org || ai_disabled,
|
||||
tooltip_mouse_state,
|
||||
}
|
||||
})
|
||||
.collect();
|
||||
|
||||
let list = render_input_list(
|
||||
None,
|
||||
input_items,
|
||||
Some(&view.command_denylist_editor),
|
||||
appearance,
|
||||
);
|
||||
|
||||
let mut column = Flex::column().with_child(create_section_header(
|
||||
"Command denylist",
|
||||
"Regular expressions to match commands that Oz should always ask permission to execute.",
|
||||
&profile_data.command_denylist,
|
||||
&view.command_denylist_mouse_state_handles,
|
||||
Some(&view.command_denylist_editor),
|
||||
None,
|
||||
|predicate| ExecutionProfileEditorViewAction::RemoveFromCommandDenylist { predicate },
|
||||
|item| item.to_string(),
|
||||
appearance,
|
||||
is_editable,
|
||||
view.tooltip_mouse_state_handles
|
||||
.command_denylist_editor_tooltip_mouse_state
|
||||
.clone(),
|
||||
)
|
||||
));
|
||||
column = column.with_child(list);
|
||||
|
||||
Container::new(column.finish())
|
||||
.with_margin_bottom(16.)
|
||||
.finish()
|
||||
}
|
||||
|
||||
fn display_mcp_name(uuid: &Uuid, app: &AppContext) -> String {
|
||||
|
||||
Reference in New Issue
Block a user