1808 lines
70 KiB
Rust
1808 lines
70 KiB
Rust
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::{
|
|
AIExecutionProfile, AIExecutionProfileAppExt as _, ActionPermission, RunAgentsPermission,
|
|
WriteToPtyPermission,
|
|
};
|
|
use crate::ai::llms::{
|
|
DisableReason, LLMContextWindow, LLMId, LLMInfo, LLMPreferences, LLMPreferencesEvent,
|
|
};
|
|
use crate::ai::paths::host_native_absolute_path;
|
|
use crate::editor::{
|
|
EditorView, Event as EditorEvent, InteractionState, SingleLineEditorOptions, TextOptions,
|
|
};
|
|
use crate::pane_group::focus_state::PaneFocusHandle;
|
|
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::{
|
|
Dropdown, DropdownItem, FilterableDropdown, SubmittableTextInput, SubmittableTextInputEvent,
|
|
};
|
|
use crate::workspace::WorkspaceAction;
|
|
use crate::workspaces::user_workspaces::UserWorkspacesEvent;
|
|
use crate::{Appearance, TemplatableMCPServerManager, UserWorkspaces};
|
|
|
|
const MODEL_MENU_WIDTH: f32 = 480.;
|
|
|
|
/// Renders a footer banner for model dropdowns informing free-plan users that
|
|
/// frontier models require an upgrade, with a clickable "Upgrade" link.
|
|
fn render_upgrade_footer(
|
|
upgrade_mouse_state: MouseStateHandle,
|
|
app: &AppContext,
|
|
) -> Box<dyn Element> {
|
|
let appearance = Appearance::as_ref(app);
|
|
let theme = appearance.theme();
|
|
let surface = theme.surface_2();
|
|
let text_color = theme.main_text_color(surface);
|
|
|
|
let info_icon = ConstrainedBox::new(
|
|
galaxy_core::ui::Icon::Info
|
|
.to_galaxyui_icon(text_color)
|
|
.finish(),
|
|
)
|
|
.with_width(16.)
|
|
.with_height(16.)
|
|
.finish();
|
|
|
|
let label = "Frontier models are unavailable on free plans. Upgrade";
|
|
let upgrade_start = label.len() - "Upgrade".len();
|
|
let info_text = Text::new(
|
|
label,
|
|
appearance.ui_font_family(),
|
|
appearance.ui_font_size(),
|
|
)
|
|
.with_color(text_color.into())
|
|
.with_single_highlight(
|
|
Highlight::new()
|
|
.with_properties(Properties::default())
|
|
.with_foreground_color(internal_colors::accent_fg(theme).into()),
|
|
(upgrade_start..label.len()).collect(),
|
|
)
|
|
.with_hoverable_char_range(
|
|
upgrade_start..label.len(),
|
|
upgrade_mouse_state,
|
|
Some(Cursor::PointingHand),
|
|
|_is_hovered, _ctx, _app| {},
|
|
)
|
|
.with_clickable_char_range(upgrade_start..label.len(), move |_modifiers, ctx, _app| {
|
|
ctx.dispatch_typed_action(WorkspaceAction::ShowUpgrade);
|
|
})
|
|
.finish();
|
|
|
|
let inner = Container::new(
|
|
Flex::row()
|
|
.with_cross_axis_alignment(CrossAxisAlignment::Start)
|
|
.with_child(
|
|
Container::new(info_icon)
|
|
.with_margin_right(6.)
|
|
.with_margin_top(2.)
|
|
.finish(),
|
|
)
|
|
.with_child(Expanded::new(1., info_text).finish())
|
|
.finish(),
|
|
)
|
|
.with_horizontal_padding(16.)
|
|
.with_vertical_padding(6.)
|
|
.with_background(internal_colors::fg_overlay_1(theme))
|
|
.with_border(Border::top(1.).with_border_color(internal_colors::neutral_3(theme)))
|
|
.finish();
|
|
|
|
Container::new(inner).with_background(surface).finish()
|
|
}
|
|
|
|
#[derive(Default)]
|
|
struct TooltipMouseStateHandles {
|
|
// Separate mouse state handles for each permission dropdown (for workspace override tooltips)
|
|
apply_code_diffs_tooltip_mouse_state: MouseStateHandle,
|
|
read_files_tooltip_mouse_state: MouseStateHandle,
|
|
execute_commands_tooltip_mouse_state: MouseStateHandle,
|
|
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,
|
|
directory_allowlist_editor_tooltip_mouse_state: MouseStateHandle,
|
|
mcp_allowlist_editor_tooltip_mouse_state: MouseStateHandle,
|
|
mcp_denylist_editor_tooltip_mouse_state: MouseStateHandle,
|
|
}
|
|
|
|
pub mod manager;
|
|
pub use manager::*;
|
|
|
|
pub const HEADER_TEXT: &str = "Profile Editor";
|
|
|
|
#[derive(Debug, Clone)]
|
|
pub enum ExecutionProfileEditorViewEvent {
|
|
Pane(PaneEvent),
|
|
}
|
|
|
|
#[derive(Debug, Clone, PartialEq)]
|
|
pub enum ExecutionProfileEditorViewAction {
|
|
Save,
|
|
Close,
|
|
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,
|
|
},
|
|
SetFullTerminalUseModel {
|
|
id: LLMId,
|
|
},
|
|
SetComputerUseModel {
|
|
id: LLMId,
|
|
},
|
|
|
|
SetApplyCodeDiffs {
|
|
permission: ActionPermission,
|
|
},
|
|
SetReadFiles {
|
|
permission: ActionPermission,
|
|
},
|
|
|
|
SetExecuteCommands {
|
|
permission: ActionPermission,
|
|
},
|
|
SetWriteToPty {
|
|
permission: WriteToPtyPermission,
|
|
},
|
|
SetCallMcpServers {
|
|
permission: ActionPermission,
|
|
},
|
|
SetComputerUse {
|
|
permission: super::ComputerUsePermission,
|
|
},
|
|
SetAskUserQuestion {
|
|
permission: super::AskUserQuestionPermission,
|
|
},
|
|
SetRunAgents {
|
|
permission: RunAgentsPermission,
|
|
},
|
|
AddToCommandAllowlist {
|
|
predicate: AgentModeCommandExecutionPredicate,
|
|
},
|
|
RemoveFromCommandAllowlist {
|
|
predicate: AgentModeCommandExecutionPredicate,
|
|
},
|
|
AddToCommandDenylist {
|
|
predicate: AgentModeCommandExecutionPredicate,
|
|
},
|
|
RemoveFromCommandDenylist {
|
|
predicate: AgentModeCommandExecutionPredicate,
|
|
},
|
|
AddToDirectoryAllowlist {
|
|
path: PathBuf,
|
|
},
|
|
RemoveFromDirectoryAllowlist {
|
|
path: PathBuf,
|
|
},
|
|
AddToMCPAllowlist {
|
|
id: uuid::Uuid,
|
|
},
|
|
RemoveFromMCPAllowlist {
|
|
id: uuid::Uuid,
|
|
},
|
|
AddToMCPDenylist {
|
|
id: uuid::Uuid,
|
|
},
|
|
RemoveFromMCPDenylist {
|
|
id: uuid::Uuid,
|
|
},
|
|
DeleteProfile,
|
|
SetPlanAutoSync {
|
|
enabled: bool,
|
|
},
|
|
SetWebSearchEnabled {
|
|
enabled: bool,
|
|
},
|
|
}
|
|
|
|
pub struct ExecutionProfileEditorView {
|
|
profile_id: ClientProfileId,
|
|
pane_configuration: ModelHandle<PaneConfiguration>,
|
|
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>>,
|
|
computer_use_model_dropdown: ViewHandle<FilterableDropdown<ExecutionProfileEditorViewAction>>,
|
|
apply_code_diffs_dropdown: ViewHandle<Dropdown<ExecutionProfileEditorViewAction>>,
|
|
read_files_dropdown: ViewHandle<Dropdown<ExecutionProfileEditorViewAction>>,
|
|
execute_commands_dropdown: ViewHandle<Dropdown<ExecutionProfileEditorViewAction>>,
|
|
write_to_pty_dropdown: ViewHandle<Dropdown<ExecutionProfileEditorViewAction>>,
|
|
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>,
|
|
mcp_denylist_dropdown: ViewHandle<FilterableDropdown<ExecutionProfileEditorViewAction>>,
|
|
mcp_denylist_mouse_state_handles: Vec<MouseStateHandle>,
|
|
profile_name_editor: ViewHandle<EditorView>,
|
|
delete_button: ViewHandle<ActionButton>,
|
|
tooltip_mouse_state_handles: TooltipMouseStateHandles,
|
|
plan_auto_sync_switch: SwitchStateHandle,
|
|
web_search_switch: SwitchStateHandle,
|
|
upgrade_footer_mouse_state: MouseStateHandle,
|
|
}
|
|
|
|
impl ExecutionProfileEditorView {
|
|
pub fn new(profile_id: ClientProfileId, ctx: &mut ViewContext<Self>) -> Self {
|
|
let pane_configuration = ctx.add_model(|_ctx| PaneConfiguration::new(HEADER_TEXT));
|
|
|
|
let apply_code_diffs_dropdown = ctx.add_typed_action_view(|ctx| {
|
|
let mut dropdown = Dropdown::new(ctx);
|
|
dropdown.set_items(
|
|
vec![
|
|
DropdownItem::new(
|
|
"Agent decides",
|
|
ExecutionProfileEditorViewAction::SetApplyCodeDiffs {
|
|
permission: ActionPermission::AgentDecides,
|
|
},
|
|
),
|
|
DropdownItem::new(
|
|
"Always allow",
|
|
ExecutionProfileEditorViewAction::SetApplyCodeDiffs {
|
|
permission: ActionPermission::AlwaysAllow,
|
|
},
|
|
),
|
|
DropdownItem::new(
|
|
"Always ask",
|
|
ExecutionProfileEditorViewAction::SetApplyCodeDiffs {
|
|
permission: ActionPermission::AlwaysAsk,
|
|
},
|
|
),
|
|
],
|
|
ctx,
|
|
);
|
|
dropdown
|
|
});
|
|
|
|
let read_files_dropdown = ctx.add_typed_action_view(|ctx| {
|
|
let mut dropdown = Dropdown::new(ctx);
|
|
dropdown.set_items(
|
|
vec![
|
|
DropdownItem::new(
|
|
"Agent decides",
|
|
ExecutionProfileEditorViewAction::SetReadFiles {
|
|
permission: ActionPermission::AgentDecides,
|
|
},
|
|
),
|
|
DropdownItem::new(
|
|
"Always allow",
|
|
ExecutionProfileEditorViewAction::SetReadFiles {
|
|
permission: ActionPermission::AlwaysAllow,
|
|
},
|
|
),
|
|
DropdownItem::new(
|
|
"Always ask",
|
|
ExecutionProfileEditorViewAction::SetReadFiles {
|
|
permission: ActionPermission::AlwaysAsk,
|
|
},
|
|
),
|
|
],
|
|
ctx,
|
|
);
|
|
dropdown
|
|
});
|
|
|
|
let execute_commands_dropdown = ctx.add_typed_action_view(|ctx| {
|
|
let mut dropdown = Dropdown::new(ctx);
|
|
dropdown.set_items(
|
|
vec![
|
|
DropdownItem::new(
|
|
"Agent decides",
|
|
ExecutionProfileEditorViewAction::SetExecuteCommands {
|
|
permission: ActionPermission::AgentDecides,
|
|
},
|
|
),
|
|
DropdownItem::new(
|
|
"Always allow",
|
|
ExecutionProfileEditorViewAction::SetExecuteCommands {
|
|
permission: ActionPermission::AlwaysAllow,
|
|
},
|
|
),
|
|
DropdownItem::new(
|
|
"Always ask",
|
|
ExecutionProfileEditorViewAction::SetExecuteCommands {
|
|
permission: ActionPermission::AlwaysAsk,
|
|
},
|
|
),
|
|
],
|
|
ctx,
|
|
);
|
|
dropdown
|
|
});
|
|
|
|
let write_to_pty_dropdown = ctx.add_typed_action_view(|ctx| {
|
|
let mut dropdown = Dropdown::new(ctx);
|
|
dropdown.set_items(
|
|
vec![
|
|
DropdownItem::new(
|
|
"Always allow",
|
|
ExecutionProfileEditorViewAction::SetWriteToPty {
|
|
permission: WriteToPtyPermission::AlwaysAllow,
|
|
},
|
|
),
|
|
DropdownItem::new(
|
|
"Always ask",
|
|
ExecutionProfileEditorViewAction::SetWriteToPty {
|
|
permission: WriteToPtyPermission::AlwaysAsk,
|
|
},
|
|
),
|
|
DropdownItem::new(
|
|
"Ask on first write",
|
|
ExecutionProfileEditorViewAction::SetWriteToPty {
|
|
permission: WriteToPtyPermission::AskOnFirstWrite,
|
|
},
|
|
),
|
|
],
|
|
ctx,
|
|
);
|
|
dropdown
|
|
});
|
|
|
|
let call_mcp_servers_dropdown = ctx.add_typed_action_view(|ctx| {
|
|
let mut dropdown = Dropdown::new(ctx);
|
|
dropdown.set_items(
|
|
vec![
|
|
DropdownItem::new(
|
|
"Agent decides",
|
|
ExecutionProfileEditorViewAction::SetCallMcpServers {
|
|
permission: ActionPermission::AgentDecides,
|
|
},
|
|
),
|
|
DropdownItem::new(
|
|
"Always allow",
|
|
ExecutionProfileEditorViewAction::SetCallMcpServers {
|
|
permission: ActionPermission::AlwaysAllow,
|
|
},
|
|
),
|
|
DropdownItem::new(
|
|
"Always ask",
|
|
ExecutionProfileEditorViewAction::SetCallMcpServers {
|
|
permission: ActionPermission::AlwaysAsk,
|
|
},
|
|
),
|
|
],
|
|
ctx,
|
|
);
|
|
dropdown
|
|
});
|
|
|
|
let computer_use_dropdown = ctx.add_typed_action_view(|ctx| {
|
|
let mut dropdown = Dropdown::new(ctx);
|
|
dropdown.set_items(
|
|
vec![
|
|
DropdownItem::new(
|
|
"Never",
|
|
ExecutionProfileEditorViewAction::SetComputerUse {
|
|
permission: super::ComputerUsePermission::Never,
|
|
},
|
|
),
|
|
DropdownItem::new(
|
|
"Always ask",
|
|
ExecutionProfileEditorViewAction::SetComputerUse {
|
|
permission: super::ComputerUsePermission::AlwaysAsk,
|
|
},
|
|
),
|
|
DropdownItem::new(
|
|
"Always allow",
|
|
ExecutionProfileEditorViewAction::SetComputerUse {
|
|
permission: super::ComputerUsePermission::AlwaysAllow,
|
|
},
|
|
),
|
|
],
|
|
ctx,
|
|
);
|
|
dropdown
|
|
});
|
|
|
|
let ask_user_question_dropdown = ctx.add_typed_action_view(|ctx| {
|
|
let mut dropdown = Dropdown::new(ctx);
|
|
dropdown.set_items(
|
|
vec![
|
|
DropdownItem::new(
|
|
"Never ask",
|
|
ExecutionProfileEditorViewAction::SetAskUserQuestion {
|
|
permission: super::AskUserQuestionPermission::Never,
|
|
},
|
|
),
|
|
DropdownItem::new(
|
|
"Ask unless auto-approve",
|
|
ExecutionProfileEditorViewAction::SetAskUserQuestion {
|
|
permission: super::AskUserQuestionPermission::AskExceptInAutoApprove,
|
|
},
|
|
),
|
|
DropdownItem::new(
|
|
"Always ask",
|
|
ExecutionProfileEditorViewAction::SetAskUserQuestion {
|
|
permission: super::AskUserQuestionPermission::AlwaysAsk,
|
|
},
|
|
),
|
|
],
|
|
ctx,
|
|
);
|
|
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");
|
|
dropdown
|
|
});
|
|
|
|
let mcp_denylist_dropdown = ctx.add_typed_action_view(|ctx| {
|
|
let mut dropdown = FilterableDropdown::new(ctx);
|
|
dropdown.set_menu_header_to_static("Select MCP servers");
|
|
dropdown
|
|
});
|
|
|
|
let permissions = BlocklistAIPermissions::as_ref(ctx);
|
|
let profile_data = permissions.permissions_profile_for_id(ctx, profile_id);
|
|
|
|
let mcp_allowlist_mouse_state_handles = profile_data
|
|
.mcp_allowlist
|
|
.iter()
|
|
.map(|_| Default::default())
|
|
.collect();
|
|
|
|
let mcp_denylist_mouse_state_handles = profile_data
|
|
.mcp_denylist
|
|
.iter()
|
|
.map(|_| Default::default())
|
|
.collect();
|
|
|
|
let base_model_dropdown = ctx.add_typed_action_view(|ctx| {
|
|
let mut dropdown = FilterableDropdown::new(ctx);
|
|
dropdown.set_top_bar_max_width(MODEL_MENU_WIDTH);
|
|
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);
|
|
dropdown.set_menu_width(MODEL_MENU_WIDTH, ctx);
|
|
dropdown
|
|
});
|
|
let full_terminal_use_model_dropdown = ctx.add_typed_action_view(|ctx| {
|
|
let mut dropdown = FilterableDropdown::new(ctx);
|
|
dropdown.set_top_bar_max_width(MODEL_MENU_WIDTH);
|
|
dropdown.set_menu_width(MODEL_MENU_WIDTH, ctx);
|
|
dropdown
|
|
});
|
|
let computer_use_model_dropdown = ctx.add_typed_action_view(|ctx| {
|
|
let mut dropdown = FilterableDropdown::new(ctx);
|
|
dropdown.set_top_bar_max_width(MODEL_MENU_WIDTH);
|
|
dropdown.set_menu_width(MODEL_MENU_WIDTH, ctx);
|
|
dropdown
|
|
});
|
|
let command_allowlist_editor = ctx.add_typed_action_view(|ctx| {
|
|
let mut input =
|
|
SubmittableTextInput::new(ctx).validate_on_edit(|s| Regex::new(s).is_ok());
|
|
input.set_placeholder_text("e.g. ls .*", ctx);
|
|
input
|
|
});
|
|
|
|
let command_allowlist_mouse_state_handles = profile_data
|
|
.command_allowlist
|
|
.iter()
|
|
.map(|_| Default::default())
|
|
.collect();
|
|
|
|
let command_denylist_editor = ctx.add_typed_action_view(|ctx| {
|
|
let mut input =
|
|
SubmittableTextInput::new(ctx).validate_on_edit(|s| Regex::new(s).is_ok());
|
|
input.set_placeholder_text("e.g. rm .*", ctx);
|
|
input
|
|
});
|
|
|
|
let command_denylist_mouse_state_handles = profile_data
|
|
.command_denylist
|
|
.iter()
|
|
.map(|_| Default::default())
|
|
.collect();
|
|
|
|
let directory_allowlist_editor = ctx.add_typed_action_view(|ctx| {
|
|
let mut input = SubmittableTextInput::new(ctx).validate_on_submit(|s| {
|
|
let expanded = host_native_absolute_path(s, &None, &None);
|
|
Path::new(&expanded).is_dir()
|
|
});
|
|
input.set_placeholder_text("e.g. ~/code-repos/repo", ctx);
|
|
input
|
|
});
|
|
|
|
let directory_allowlist_mouse_state_handles = profile_data
|
|
.directory_allowlist
|
|
.iter()
|
|
.map(|_| Default::default())
|
|
.collect();
|
|
|
|
let profile_name_editor = ctx.add_view(|ctx| {
|
|
let mut editor = EditorView::single_line(
|
|
SingleLineEditorOptions {
|
|
max_buffer_len: Some(super::PROFILE_NAME_MAX_LENGTH),
|
|
..Default::default()
|
|
},
|
|
ctx,
|
|
);
|
|
editor.set_placeholder_text("e.g. \"YOLO code\"", ctx);
|
|
editor
|
|
});
|
|
|
|
let font_family = Appearance::as_ref(ctx).ui_font_family();
|
|
|
|
profile_name_editor.update(ctx, |editor, ctx| {
|
|
editor.set_font_size(12., ctx);
|
|
editor.set_font_family(font_family, ctx);
|
|
});
|
|
|
|
Self::update_profile_name_editor(&profile_name_editor, &profile_data, ctx);
|
|
|
|
let delete_button = ctx.add_typed_action_view(|_| {
|
|
ActionButton::new("Delete profile", DangerSecondaryTheme)
|
|
.with_icon(Icon::Trash)
|
|
.on_click(|ctx| {
|
|
ctx.dispatch_typed_action(ExecutionProfileEditorViewAction::DeleteProfile);
|
|
})
|
|
});
|
|
|
|
let mut view = Self {
|
|
profile_id,
|
|
pane_configuration,
|
|
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,
|
|
apply_code_diffs_dropdown,
|
|
read_files_dropdown,
|
|
execute_commands_dropdown,
|
|
write_to_pty_dropdown,
|
|
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,
|
|
mcp_denylist_dropdown,
|
|
mcp_denylist_mouse_state_handles,
|
|
profile_name_editor,
|
|
delete_button,
|
|
tooltip_mouse_state_handles: Default::default(),
|
|
plan_auto_sync_switch: Default::default(),
|
|
web_search_switch: Default::default(),
|
|
upgrade_footer_mouse_state: Default::default(),
|
|
};
|
|
|
|
ctx.subscribe_to_view(&view.profile_name_editor, |view, _, event, ctx| {
|
|
if let EditorEvent::Edited(_) = event {
|
|
view.save_profile_name_if_valid(ctx);
|
|
}
|
|
});
|
|
|
|
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) {
|
|
Ok(regex) => regex,
|
|
Err(e) => {
|
|
log::warn!(
|
|
"Failed to convert string to regex for cmd execution allowlist: {e}"
|
|
);
|
|
return;
|
|
}
|
|
};
|
|
AIExecutionProfilesModel::handle(ctx).update(ctx, |profiles_model, ctx| {
|
|
profiles_model.add_to_command_allowlist(view.profile_id, &predicate, ctx);
|
|
});
|
|
ctx.notify();
|
|
}
|
|
});
|
|
|
|
ctx.subscribe_to_view(&view.command_denylist_editor, |view, _, event, ctx| {
|
|
if let SubmittableTextInputEvent::Submit(s) = event {
|
|
let predicate = match AgentModeCommandExecutionPredicate::new_regex(s) {
|
|
Ok(regex) => regex,
|
|
Err(e) => {
|
|
log::warn!(
|
|
"Failed to convert string to regex for cmd execution denylist: {e}"
|
|
);
|
|
return;
|
|
}
|
|
};
|
|
AIExecutionProfilesModel::handle(ctx).update(ctx, |profiles_model, ctx| {
|
|
profiles_model.add_to_command_denylist(view.profile_id, &predicate, ctx);
|
|
});
|
|
ctx.notify();
|
|
}
|
|
});
|
|
|
|
ctx.subscribe_to_view(&view.directory_allowlist_editor, |view, _, event, ctx| {
|
|
if let SubmittableTextInputEvent::Submit(s) = event {
|
|
let expanded = host_native_absolute_path(s, &None, &None);
|
|
AIExecutionProfilesModel::handle(ctx).update(ctx, |profiles_model, ctx| {
|
|
profiles_model.add_to_directory_allowlist(
|
|
view.profile_id,
|
|
&PathBuf::from(expanded),
|
|
ctx,
|
|
);
|
|
});
|
|
ctx.notify();
|
|
}
|
|
});
|
|
|
|
ctx.subscribe_to_model(&LLMPreferences::handle(ctx), |me, _, event, ctx| {
|
|
let permissions = BlocklistAIPermissions::as_ref(ctx);
|
|
let current_permissions = permissions.permissions_profile_for_id(ctx, me.profile_id);
|
|
|
|
match event {
|
|
LLMPreferencesEvent::UpdatedAvailableLLMs => {
|
|
Self::refresh_filterable_model_dropdown(
|
|
&me.base_model_dropdown,
|
|
current_permissions.base_model.clone(),
|
|
|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,
|
|
);
|
|
Self::refresh_coding_model_dropdown(
|
|
&me.coding_model_dropdown,
|
|
current_permissions.coding_model.clone(),
|
|
ctx,
|
|
);
|
|
Self::refresh_filterable_model_dropdown(
|
|
&me.full_terminal_use_model_dropdown,
|
|
current_permissions.cli_agent_model.clone(),
|
|
|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,
|
|
ctx,
|
|
);
|
|
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(),
|
|
|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, 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(
|
|
&me.coding_model_dropdown,
|
|
current_permissions.coding_model.clone(),
|
|
ctx,
|
|
);
|
|
}
|
|
}
|
|
});
|
|
|
|
// Refresh model dropdowns when BYO API keys update so key icons reflect current state.
|
|
ctx.subscribe_to_model(
|
|
&ApiKeyManager::handle(ctx),
|
|
|me, _model, _event: &ApiKeyManagerEvent, ctx| {
|
|
let permissions = BlocklistAIPermissions::as_ref(ctx);
|
|
let current_permissions =
|
|
permissions.permissions_profile_for_id(ctx, me.profile_id);
|
|
Self::refresh_filterable_model_dropdown(
|
|
&me.base_model_dropdown,
|
|
current_permissions.base_model.clone(),
|
|
|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,
|
|
);
|
|
Self::refresh_coding_model_dropdown(
|
|
&me.coding_model_dropdown,
|
|
current_permissions.coding_model.clone(),
|
|
ctx,
|
|
);
|
|
me.sync_context_window_editor(ctx, false);
|
|
ctx.notify();
|
|
},
|
|
);
|
|
|
|
ctx.subscribe_to_model(
|
|
&AIExecutionProfilesModel::handle(ctx),
|
|
|me, _, event, ctx| {
|
|
if matches!(event, AIExecutionProfilesModelEvent::ProfileUpdated(profile_id) if *profile_id == me.profile_id) {
|
|
me.refresh_profile_state(ctx);
|
|
me.update_mouse_state_handles(ctx);
|
|
}
|
|
},
|
|
);
|
|
|
|
let workspace = UserWorkspaces::handle(ctx);
|
|
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();
|
|
}
|
|
});
|
|
|
|
Self::update_all_editor_interaction_states(&view, workspace, ctx);
|
|
|
|
view.refresh_profile_state(ctx);
|
|
|
|
view.update_mouse_state_handles(ctx);
|
|
|
|
view
|
|
}
|
|
|
|
pub fn profile_id(&self) -> ClientProfileId {
|
|
self.profile_id
|
|
}
|
|
|
|
fn update_mouse_state_handles(&mut self, ctx: &mut ViewContext<Self>) {
|
|
let app = ctx;
|
|
let permissions = BlocklistAIPermissions::as_ref(app);
|
|
let current_permissions = permissions.permissions_profile_for_id(app, self.profile_id);
|
|
|
|
self.command_allowlist_mouse_state_handles = current_permissions
|
|
.command_allowlist
|
|
.iter()
|
|
.map(|_| Default::default())
|
|
.collect();
|
|
|
|
self.command_denylist_mouse_state_handles = current_permissions
|
|
.command_denylist
|
|
.iter()
|
|
.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()
|
|
.map(|_| Default::default())
|
|
.collect();
|
|
|
|
self.mcp_allowlist_mouse_state_handles = current_permissions
|
|
.mcp_allowlist
|
|
.iter()
|
|
.map(|_| Default::default())
|
|
.collect();
|
|
|
|
self.mcp_denylist_mouse_state_handles = current_permissions
|
|
.mcp_denylist
|
|
.iter()
|
|
.map(|_| Default::default())
|
|
.collect();
|
|
}
|
|
|
|
fn refresh_profile_state(&mut self, ctx: &mut ViewContext<Self>) {
|
|
let permissions = BlocklistAIPermissions::as_ref(ctx);
|
|
let current_permissions = permissions.permissions_profile_for_id(ctx, self.profile_id);
|
|
let ai_settings = AISettings::as_ref(ctx);
|
|
|
|
let apply_code_diffs_disabled = !ai_settings.is_code_diffs_permissions_editable(ctx);
|
|
let read_files_disabled = !ai_settings.is_read_files_permissions_editable(ctx);
|
|
let execute_commands_disabled = !ai_settings.is_execute_commands_permissions_editable(ctx);
|
|
let write_to_pty_disabled = !ai_settings.is_write_to_pty_permissions_editable(ctx);
|
|
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, 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,
|
|
ctx,
|
|
);
|
|
Self::refresh_coding_model_dropdown(
|
|
&self.coding_model_dropdown,
|
|
current_permissions.coding_model.clone(),
|
|
ctx,
|
|
);
|
|
Self::refresh_filterable_model_dropdown(
|
|
&self.full_terminal_use_model_dropdown,
|
|
current_permissions.cli_agent_model.clone(),
|
|
|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,
|
|
ctx,
|
|
);
|
|
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(),
|
|
|id| ExecutionProfileEditorViewAction::SetComputerUseModel { id },
|
|
|prefs| prefs.get_default_computer_use_model().id.clone(),
|
|
&self.upgrade_footer_mouse_state,
|
|
ctx,
|
|
);
|
|
|
|
Self::refresh_execution_profile_dropdown_menu(
|
|
&self.apply_code_diffs_dropdown,
|
|
current_permissions.apply_code_diffs,
|
|
apply_code_diffs_disabled,
|
|
ctx,
|
|
);
|
|
Self::refresh_execution_profile_dropdown_menu(
|
|
&self.read_files_dropdown,
|
|
current_permissions.read_files,
|
|
read_files_disabled,
|
|
ctx,
|
|
);
|
|
Self::refresh_execution_profile_dropdown_menu(
|
|
&self.execute_commands_dropdown,
|
|
current_permissions.execute_commands,
|
|
execute_commands_disabled,
|
|
ctx,
|
|
);
|
|
Self::refresh_write_to_pty_dropdown_menu(
|
|
&self.write_to_pty_dropdown,
|
|
current_permissions.write_to_pty,
|
|
write_to_pty_disabled,
|
|
ctx,
|
|
);
|
|
Self::refresh_execution_profile_dropdown_menu(
|
|
&self.call_mcp_servers_dropdown,
|
|
current_permissions.mcp_permissions,
|
|
mcp_disabled,
|
|
ctx,
|
|
);
|
|
Self::refresh_computer_use_dropdown_menu(
|
|
&self.computer_use_dropdown,
|
|
current_permissions.computer_use,
|
|
computer_use_disabled,
|
|
ctx,
|
|
);
|
|
Self::refresh_ask_user_question_dropdown_menu(
|
|
&self.ask_user_question_dropdown,
|
|
current_permissions.ask_user_question,
|
|
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 },
|
|
¤t_permissions.mcp_allowlist,
|
|
¤t_permissions.mcp_denylist,
|
|
ctx,
|
|
);
|
|
Self::refresh_mcp_dropdown(
|
|
&self.mcp_denylist_dropdown,
|
|
|uuid| ExecutionProfileEditorViewAction::AddToMCPDenylist { id: uuid },
|
|
¤t_permissions.mcp_allowlist,
|
|
¤t_permissions.mcp_denylist,
|
|
ctx,
|
|
);
|
|
|
|
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(
|
|
menu: &ViewHandle<Dropdown<ExecutionProfileEditorViewAction>>,
|
|
current_permission: ActionPermission,
|
|
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 {
|
|
ActionPermission::AgentDecides | ActionPermission::Unknown => 0,
|
|
ActionPermission::AlwaysAllow => 1,
|
|
ActionPermission::AlwaysAsk => 2,
|
|
};
|
|
|
|
menu.set_selected_by_index(active, ctx);
|
|
ctx.notify();
|
|
});
|
|
ctx.notify();
|
|
}
|
|
|
|
fn refresh_write_to_pty_dropdown_menu(
|
|
menu: &ViewHandle<Dropdown<ExecutionProfileEditorViewAction>>,
|
|
current_permission: WriteToPtyPermission,
|
|
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 {
|
|
WriteToPtyPermission::AlwaysAllow => 0,
|
|
WriteToPtyPermission::AlwaysAsk | WriteToPtyPermission::Unknown => 1,
|
|
WriteToPtyPermission::AskOnFirstWrite => 2,
|
|
};
|
|
|
|
menu.set_selected_by_index(active, ctx);
|
|
ctx.notify();
|
|
});
|
|
ctx.notify();
|
|
}
|
|
|
|
fn refresh_computer_use_dropdown_menu(
|
|
menu: &ViewHandle<Dropdown<ExecutionProfileEditorViewAction>>,
|
|
current_permission: super::ComputerUsePermission,
|
|
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 {
|
|
super::ComputerUsePermission::Never | super::ComputerUsePermission::Unknown => 0,
|
|
super::ComputerUsePermission::AlwaysAsk => 1,
|
|
super::ComputerUsePermission::AlwaysAllow => 2,
|
|
};
|
|
|
|
menu.set_selected_by_index(active, ctx);
|
|
ctx.notify();
|
|
});
|
|
ctx.notify();
|
|
}
|
|
|
|
fn refresh_ask_user_question_dropdown_menu(
|
|
menu: &ViewHandle<Dropdown<ExecutionProfileEditorViewAction>>,
|
|
current_permission: super::AskUserQuestionPermission,
|
|
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 {
|
|
super::AskUserQuestionPermission::Never => 0,
|
|
super::AskUserQuestionPermission::AskExceptInAutoApprove
|
|
| super::AskUserQuestionPermission::Unknown => 1,
|
|
super::AskUserQuestionPermission::AlwaysAsk => 2,
|
|
};
|
|
|
|
menu.set_selected_by_index(active, ctx);
|
|
ctx.notify();
|
|
});
|
|
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>,
|
|
get_choices: G,
|
|
create_action: A,
|
|
get_default_id: D,
|
|
upgrade_mouse_state: &MouseStateHandle,
|
|
ctx: &mut ViewContext<Self>,
|
|
) where
|
|
G: for<'a> FnOnce(&'a LLMPreferences, &AppContext) -> Vec<&'a LLMInfo>,
|
|
A: Fn(LLMId) -> ExecutionProfileEditorViewAction,
|
|
D: FnOnce(&LLMPreferences) -> LLMId,
|
|
{
|
|
menu.update(ctx, |dropdown, ctx| {
|
|
let disabled_by_ai_toggle = !AISettings::as_ref(ctx).is_any_ai_enabled(ctx);
|
|
|
|
if disabled_by_ai_toggle {
|
|
dropdown.set_disabled(ctx);
|
|
} else {
|
|
dropdown.set_enabled(ctx);
|
|
}
|
|
|
|
let llm_prefs = LLMPreferences::handle(ctx);
|
|
let llm_prefs = llm_prefs.as_ref(ctx);
|
|
let choices = get_choices(llm_prefs, ctx);
|
|
|
|
let has_upgrade_gated_models = choices
|
|
.iter()
|
|
.any(|llm| matches!(llm.disable_reason, Some(DisableReason::RequiresUpgrade)));
|
|
|
|
let items = available_model_menu_items(
|
|
choices,
|
|
|llm| DropdownAction::select_action_and_close(create_action(llm.id.clone())),
|
|
None,
|
|
None,
|
|
false,
|
|
false,
|
|
ctx,
|
|
);
|
|
dropdown.set_rich_items(items, ctx);
|
|
|
|
if has_upgrade_gated_models {
|
|
let mouse_state = upgrade_mouse_state.clone();
|
|
dropdown.set_footer(
|
|
move |app| render_upgrade_footer(mouse_state.clone(), app),
|
|
ctx,
|
|
);
|
|
} else {
|
|
dropdown.clear_footer(ctx);
|
|
}
|
|
|
|
let llm_prefs = LLMPreferences::handle(ctx);
|
|
let llm_prefs = llm_prefs.as_ref(ctx);
|
|
let model_to_select = profile_model.unwrap_or_else(|| get_default_id(llm_prefs));
|
|
dropdown.set_selected_by_action(create_action(model_to_select), ctx);
|
|
ctx.notify();
|
|
});
|
|
ctx.notify();
|
|
}
|
|
|
|
fn refresh_coding_model_dropdown(
|
|
menu: &ViewHandle<Dropdown<ExecutionProfileEditorViewAction>>,
|
|
profile_coding_model: Option<LLMId>,
|
|
ctx: &mut ViewContext<Self>,
|
|
) {
|
|
menu.update(ctx, |dropdown, ctx| {
|
|
let disabled_by_ai_toggle = !AISettings::as_ref(ctx).is_any_ai_enabled(ctx);
|
|
|
|
if disabled_by_ai_toggle {
|
|
dropdown.set_disabled(ctx);
|
|
} else {
|
|
dropdown.set_enabled(ctx);
|
|
}
|
|
|
|
let choices = LLMPreferences::as_ref(ctx)
|
|
.get_coding_llm_choices(ctx)
|
|
.collect_vec();
|
|
|
|
let items = available_model_menu_items(
|
|
choices,
|
|
|llm| {
|
|
DropdownAction::select_action_and_close(
|
|
ExecutionProfileEditorViewAction::SetCodingModel { id: llm.id.clone() },
|
|
)
|
|
},
|
|
None,
|
|
None,
|
|
false,
|
|
false,
|
|
ctx,
|
|
);
|
|
dropdown.set_rich_items(items, ctx);
|
|
|
|
let model_to_select = profile_coding_model.unwrap_or_else(|| {
|
|
LLMPreferences::as_ref(ctx)
|
|
.get_default_coding_model()
|
|
.id
|
|
.clone()
|
|
});
|
|
dropdown.set_selected_by_action(
|
|
ExecutionProfileEditorViewAction::SetCodingModel {
|
|
id: model_to_select,
|
|
},
|
|
ctx,
|
|
);
|
|
ctx.notify();
|
|
});
|
|
ctx.notify();
|
|
}
|
|
|
|
fn refresh_mcp_dropdown<F>(
|
|
dropdown: &ViewHandle<FilterableDropdown<ExecutionProfileEditorViewAction>>,
|
|
action_creator: F,
|
|
profile_mcp_allowlist: &[uuid::Uuid],
|
|
profile_mcp_denylist: &[uuid::Uuid],
|
|
ctx: &mut ViewContext<Self>,
|
|
) where
|
|
F: Fn(uuid::Uuid) -> ExecutionProfileEditorViewAction,
|
|
{
|
|
let all_mcp_servers = TemplatableMCPServerManager::get_all_cloud_synced_mcp_servers(ctx);
|
|
dropdown.update(ctx, |dropdown, ctx| {
|
|
let mcps_in_dropdown: Vec<(uuid::Uuid, String)> = all_mcp_servers
|
|
.into_iter()
|
|
.filter(|(uuid, _server_name)| {
|
|
!profile_mcp_allowlist.contains(uuid) && !profile_mcp_denylist.contains(uuid)
|
|
})
|
|
.collect();
|
|
|
|
dropdown.set_items(
|
|
mcps_in_dropdown
|
|
.iter()
|
|
.map(|(uuid, server_name)| {
|
|
DropdownItem::new(server_name, action_creator(*uuid))
|
|
})
|
|
.collect(),
|
|
ctx,
|
|
);
|
|
ctx.notify()
|
|
});
|
|
ctx.notify();
|
|
}
|
|
|
|
pub fn pane_configuration(&self) -> ModelHandle<PaneConfiguration> {
|
|
self.pane_configuration.clone()
|
|
}
|
|
|
|
pub fn focus(&mut self, ctx: &mut ViewContext<Self>) {
|
|
ctx.focus_self();
|
|
}
|
|
|
|
fn save_profile_name_if_valid(&self, ctx: &mut ViewContext<Self>) {
|
|
let new_name = self.profile_name_editor.read(ctx, |editor, ctx| {
|
|
editor.buffer_text(ctx).trim().to_string()
|
|
});
|
|
|
|
if new_name.is_empty() {
|
|
return;
|
|
}
|
|
|
|
let current_name = BlocklistAIPermissions::as_ref(ctx)
|
|
.permissions_profile_for_id(ctx, self.profile_id)
|
|
.name;
|
|
|
|
if current_name != new_name {
|
|
AIExecutionProfilesModel::handle(ctx).update(ctx, |profiles_model, ctx| {
|
|
profiles_model.set_profile_name(self.profile_id, &new_name, ctx);
|
|
});
|
|
}
|
|
}
|
|
|
|
fn update_profile_name_editor(
|
|
profile_name_editor: &ViewHandle<EditorView>,
|
|
profile_data: &AIExecutionProfile,
|
|
ctx: &mut ViewContext<Self>,
|
|
) {
|
|
profile_name_editor.update(ctx, |editor, ctx| {
|
|
let display_name = if profile_data.is_default_profile {
|
|
"Default".to_string()
|
|
} else {
|
|
profile_data.name.clone()
|
|
};
|
|
|
|
// Only update the buffer text if it's different from what's currently displayed
|
|
// This preserves the cursor position when the text hasn't changed
|
|
let current_text = editor.buffer_text(ctx);
|
|
if current_text != display_name {
|
|
editor.set_buffer_text(&display_name, ctx);
|
|
}
|
|
|
|
if profile_data.is_default_profile {
|
|
editor.set_interaction_state(InteractionState::Disabled, ctx);
|
|
}
|
|
});
|
|
}
|
|
|
|
fn update_all_editor_interaction_states(
|
|
view: &Self,
|
|
workspace: ModelHandle<UserWorkspaces>,
|
|
ctx: &mut ViewContext<Self>,
|
|
) {
|
|
let is_any_ai_enabled = AISettings::as_ref(ctx).is_any_ai_enabled(ctx);
|
|
let ai_autonomy_settings = workspace.as_ref(ctx).ai_autonomy_settings();
|
|
|
|
Self::update_editor_interaction_state(
|
|
view.command_denylist_editor.as_ref(ctx).editor().clone(),
|
|
is_any_ai_enabled,
|
|
ctx,
|
|
);
|
|
|
|
Self::update_editor_interaction_state(
|
|
view.command_allowlist_editor.as_ref(ctx).editor().clone(),
|
|
is_any_ai_enabled
|
|
&& !ai_autonomy_settings.has_override_for_execute_commands_allowlist(),
|
|
ctx,
|
|
);
|
|
|
|
Self::update_editor_interaction_state(
|
|
view.directory_allowlist_editor.as_ref(ctx).editor().clone(),
|
|
is_any_ai_enabled && !ai_autonomy_settings.has_override_for_read_files_allowlist(),
|
|
ctx,
|
|
);
|
|
}
|
|
|
|
fn update_editor_interaction_state(
|
|
editor: ViewHandle<EditorView>,
|
|
is_editable: bool,
|
|
ctx: &mut ViewContext<Self>,
|
|
) {
|
|
editor.update(ctx, |editor, ctx| {
|
|
if !is_editable {
|
|
editor.set_interaction_state(InteractionState::Disabled, ctx);
|
|
} else {
|
|
editor.set_interaction_state(InteractionState::Editable, ctx);
|
|
}
|
|
});
|
|
}
|
|
|
|
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"
|
|
}
|
|
|
|
fn render(&self, app: &AppContext) -> Box<dyn Element> {
|
|
let appearance = Appearance::as_ref(app);
|
|
use ui_helpers::*;
|
|
|
|
let permissions = BlocklistAIPermissions::as_ref(app);
|
|
let profile_data = permissions.permissions_profile_for_id(app, self.profile_id);
|
|
|
|
let mut column = Flex::column()
|
|
.with_child(render_header_section(
|
|
appearance,
|
|
&self.profile_name_editor,
|
|
profile_data.is_default_profile,
|
|
))
|
|
.with_child(render_models_section(appearance, self, app))
|
|
.with_child(render_permissions_section(
|
|
appearance,
|
|
self,
|
|
&profile_data,
|
|
app,
|
|
));
|
|
|
|
if !profile_data.is_default_profile {
|
|
column.add_child(ChildView::new(&self.delete_button).finish());
|
|
}
|
|
|
|
let content = Container::new(column.finish())
|
|
.with_uniform_padding(16.)
|
|
.finish();
|
|
|
|
ClippedScrollable::vertical(
|
|
self.clipped_scroll_state.clone(),
|
|
Align::new(content).top_center().finish(),
|
|
ScrollbarWidth::Auto,
|
|
appearance.theme().nonactive_ui_detail().into(),
|
|
appearance.theme().active_ui_detail().into(),
|
|
galaxyui::elements::Fill::None,
|
|
)
|
|
.finish()
|
|
}
|
|
}
|
|
|
|
impl Entity for ExecutionProfileEditorView {
|
|
type Event = ExecutionProfileEditorViewEvent;
|
|
}
|
|
|
|
impl TypedActionView for ExecutionProfileEditorView {
|
|
type Action = ExecutionProfileEditorViewAction;
|
|
|
|
fn handle_action(&mut self, action: &Self::Action, ctx: &mut ViewContext<Self>) {
|
|
match action {
|
|
ExecutionProfileEditorViewAction::Save => {
|
|
// TODO: Implement save logic
|
|
log::info!("Save profile");
|
|
}
|
|
ExecutionProfileEditorViewAction::Close => {
|
|
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 } => {
|
|
AIExecutionProfilesModel::handle(ctx).update(ctx, |profiles_model, ctx| {
|
|
profiles_model.set_coding_model(self.profile_id, Some(id.clone()), ctx);
|
|
});
|
|
ctx.notify();
|
|
}
|
|
ExecutionProfileEditorViewAction::SetFullTerminalUseModel { id } => {
|
|
AIExecutionProfilesModel::handle(ctx).update(ctx, |profiles_model, ctx| {
|
|
profiles_model.set_cli_agent_model(self.profile_id, Some(id.clone()), ctx);
|
|
});
|
|
ctx.notify();
|
|
}
|
|
ExecutionProfileEditorViewAction::SetComputerUseModel { id } => {
|
|
AIExecutionProfilesModel::handle(ctx).update(ctx, |profiles_model, ctx| {
|
|
profiles_model.set_computer_use_model(self.profile_id, Some(id.clone()), ctx);
|
|
});
|
|
ctx.notify();
|
|
}
|
|
ExecutionProfileEditorViewAction::SetApplyCodeDiffs { permission } => {
|
|
AIExecutionProfilesModel::handle(ctx).update(ctx, |profiles_model, ctx| {
|
|
profiles_model.set_apply_code_diffs(self.profile_id, permission, ctx);
|
|
});
|
|
ctx.notify();
|
|
}
|
|
ExecutionProfileEditorViewAction::SetReadFiles { permission } => {
|
|
AIExecutionProfilesModel::handle(ctx).update(ctx, |profiles_model, ctx| {
|
|
profiles_model.set_read_files(self.profile_id, permission, ctx);
|
|
});
|
|
ctx.notify();
|
|
}
|
|
ExecutionProfileEditorViewAction::SetExecuteCommands { permission } => {
|
|
AIExecutionProfilesModel::handle(ctx).update(ctx, |profiles_model, ctx| {
|
|
profiles_model.set_execute_commands(self.profile_id, permission, ctx);
|
|
});
|
|
ctx.notify();
|
|
}
|
|
ExecutionProfileEditorViewAction::SetWriteToPty { permission } => {
|
|
AIExecutionProfilesModel::handle(ctx).update(ctx, |profiles_model, ctx| {
|
|
profiles_model.set_write_to_pty(self.profile_id, permission, ctx);
|
|
});
|
|
ctx.notify();
|
|
}
|
|
ExecutionProfileEditorViewAction::SetCallMcpServers { permission } => {
|
|
AIExecutionProfilesModel::handle(ctx).update(ctx, |profiles_model, ctx| {
|
|
profiles_model.set_mcp_permissions(self.profile_id, permission, ctx);
|
|
});
|
|
ctx.notify();
|
|
}
|
|
ExecutionProfileEditorViewAction::SetComputerUse { permission } => {
|
|
AIExecutionProfilesModel::handle(ctx).update(ctx, |profiles_model, ctx| {
|
|
profiles_model.set_computer_use(self.profile_id, permission, ctx);
|
|
});
|
|
ctx.notify();
|
|
}
|
|
ExecutionProfileEditorViewAction::SetAskUserQuestion { permission } => {
|
|
AIExecutionProfilesModel::handle(ctx).update(ctx, |profiles_model, ctx| {
|
|
profiles_model.set_ask_user_question(self.profile_id, *permission, ctx);
|
|
});
|
|
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);
|
|
});
|
|
ctx.notify();
|
|
}
|
|
ExecutionProfileEditorViewAction::RemoveFromCommandAllowlist { predicate } => {
|
|
AIExecutionProfilesModel::handle(ctx).update(ctx, |profiles_model, ctx| {
|
|
profiles_model.remove_from_command_allowlist(self.profile_id, predicate, ctx);
|
|
});
|
|
ctx.notify();
|
|
}
|
|
ExecutionProfileEditorViewAction::AddToCommandDenylist { predicate } => {
|
|
AIExecutionProfilesModel::handle(ctx).update(ctx, |profiles_model, ctx| {
|
|
profiles_model.add_to_command_denylist(self.profile_id, predicate, ctx);
|
|
});
|
|
ctx.notify();
|
|
}
|
|
ExecutionProfileEditorViewAction::RemoveFromCommandDenylist { predicate } => {
|
|
AIExecutionProfilesModel::handle(ctx).update(ctx, |profiles_model, ctx| {
|
|
profiles_model.remove_from_command_denylist(self.profile_id, predicate, ctx);
|
|
});
|
|
ctx.notify();
|
|
}
|
|
ExecutionProfileEditorViewAction::AddToDirectoryAllowlist { path } => {
|
|
AIExecutionProfilesModel::handle(ctx).update(ctx, |profiles_model, ctx| {
|
|
profiles_model.add_to_directory_allowlist(self.profile_id, path, ctx);
|
|
});
|
|
ctx.notify();
|
|
}
|
|
ExecutionProfileEditorViewAction::RemoveFromDirectoryAllowlist { path } => {
|
|
AIExecutionProfilesModel::handle(ctx).update(ctx, |profiles_model, ctx| {
|
|
profiles_model.remove_from_directory_allowlist(self.profile_id, path, ctx);
|
|
});
|
|
ctx.notify();
|
|
}
|
|
ExecutionProfileEditorViewAction::AddToMCPAllowlist { id } => {
|
|
AIExecutionProfilesModel::handle(ctx).update(ctx, |profiles_model, ctx| {
|
|
profiles_model.add_to_mcp_allowlist(self.profile_id, id, ctx);
|
|
});
|
|
ctx.notify();
|
|
}
|
|
ExecutionProfileEditorViewAction::RemoveFromMCPAllowlist { id } => {
|
|
AIExecutionProfilesModel::handle(ctx).update(ctx, |profiles_model, ctx| {
|
|
profiles_model.remove_from_mcp_allowlist(self.profile_id, id, ctx);
|
|
});
|
|
ctx.notify();
|
|
}
|
|
ExecutionProfileEditorViewAction::AddToMCPDenylist { id } => {
|
|
AIExecutionProfilesModel::handle(ctx).update(ctx, |profiles_model, ctx| {
|
|
profiles_model.add_to_mcp_denylist(self.profile_id, id, ctx);
|
|
});
|
|
ctx.notify();
|
|
}
|
|
ExecutionProfileEditorViewAction::RemoveFromMCPDenylist { id } => {
|
|
AIExecutionProfilesModel::handle(ctx).update(ctx, |profiles_model, ctx| {
|
|
profiles_model.remove_from_mcp_denylist(self.profile_id, id, ctx);
|
|
});
|
|
ctx.notify();
|
|
}
|
|
ExecutionProfileEditorViewAction::DeleteProfile => {
|
|
AIExecutionProfilesModel::handle(ctx).update(ctx, |profiles_model, ctx| {
|
|
profiles_model.delete_profile(self.profile_id, ctx);
|
|
});
|
|
ctx.emit(ExecutionProfileEditorViewEvent::Pane(PaneEvent::Close));
|
|
}
|
|
ExecutionProfileEditorViewAction::SetPlanAutoSync { enabled } => {
|
|
AIExecutionProfilesModel::handle(ctx).update(ctx, |profiles_model, ctx| {
|
|
profiles_model.set_autosync_plans_to_warp_drive(self.profile_id, *enabled, ctx);
|
|
});
|
|
ctx.notify();
|
|
}
|
|
ExecutionProfileEditorViewAction::SetWebSearchEnabled { enabled } => {
|
|
AIExecutionProfilesModel::handle(ctx).update(ctx, |profiles_model, ctx| {
|
|
profiles_model.set_web_search_enabled(self.profile_id, *enabled, ctx);
|
|
});
|
|
ctx.notify();
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
impl BackingView for ExecutionProfileEditorView {
|
|
type PaneHeaderOverflowMenuAction = ExecutionProfileEditorViewAction;
|
|
type CustomAction = ();
|
|
type AssociatedData = ();
|
|
|
|
fn handle_pane_header_overflow_menu_action(
|
|
&mut self,
|
|
_action: &Self::PaneHeaderOverflowMenuAction,
|
|
_ctx: &mut galaxyui::ViewContext<Self>,
|
|
) {
|
|
self.handle_action(_action, _ctx)
|
|
}
|
|
|
|
fn close(&mut self, ctx: &mut galaxyui::ViewContext<Self>) {
|
|
self.save_profile_name_if_valid(ctx);
|
|
ctx.emit(ExecutionProfileEditorViewEvent::Pane(PaneEvent::Close));
|
|
}
|
|
|
|
fn focus_contents(&mut self, ctx: &mut galaxyui::ViewContext<Self>) {
|
|
self.focus(ctx);
|
|
}
|
|
|
|
fn render_header_content(
|
|
&self,
|
|
_ctx: &view::HeaderRenderContext<'_>,
|
|
_app: &AppContext,
|
|
) -> view::HeaderContent {
|
|
view::HeaderContent::Standard(view::StandardHeader {
|
|
title: HEADER_TEXT.into(),
|
|
title_secondary: None,
|
|
title_style: None,
|
|
title_clip_config: galaxyui::text_layout::ClipConfig::start(),
|
|
title_max_width: None,
|
|
left_of_title: None,
|
|
right_of_title: None,
|
|
left_of_overflow: None,
|
|
options: view::StandardHeaderOptions {
|
|
always_show_icons: true,
|
|
..Default::default()
|
|
},
|
|
})
|
|
}
|
|
|
|
fn set_focus_handle(&mut self, focus_handle: PaneFocusHandle, _ctx: &mut ViewContext<Self>) {
|
|
self.focus_handle = Some(focus_handle);
|
|
}
|
|
}
|