diff --git a/app/src/ai/blocklist/block/view_impl/common.rs b/app/src/ai/blocklist/block/view_impl/common.rs index 5bc0408d..ec1de633 100644 --- a/app/src/ai/blocklist/block/view_impl/common.rs +++ b/app/src/ai/blocklist/block/view_impl/common.rs @@ -970,11 +970,12 @@ fn render_permission_mode_button( ) -> Box { use crate::ai::execution_profiles::PermissionMode; let is_active = matches!(props.mode, PermissionMode::GalaxyDecides | PermissionMode::AllowAll); - let is_locked = props.is_locked || props.mode == PermissionMode::ReadOnly; - let icon = if is_active { - Icon::FastForwardFilled - } else { - Icon::FastForward + let is_locked = props.is_locked; + let (icon, next_mode) = match props.mode { + PermissionMode::ReadOnly => (Icon::Eye, PermissionMode::AskFirst), + PermissionMode::AskFirst => (Icon::MessageChatSquare, PermissionMode::GalaxyDecides), + PermissionMode::GalaxyDecides => (Icon::GalaxyLogo, PermissionMode::AllowAll), + PermissionMode::AllowAll => (Icon::CheckCircleBroken, PermissionMode::ReadOnly), }; let icon_size = get_icon_size(appearance); let icon = Container::new( @@ -988,20 +989,28 @@ fn render_permission_mode_button( ) .finish(); + let content = Flex::row() + .with_cross_axis_alignment(CrossAxisAlignment::Center) + .with_spacing(6.) + .with_child(icon) + .with_child( + Text::new(props.mode.label(), appearance.ui_font_family(), get_keybinding_font_size(appearance)) + .with_color(appearance.theme().active_ui_text_color().into()) + .finish(), + ) + .finish(); let tooltip_text = if is_locked { - "Permission mode is managed by the profile" - } else if is_active { - "Galaxy Decides · switch to Ask First" + format!("{} · permission mode is managed by the profile", props.mode.label()) } else { - "Ask First · switch to Galaxy Decides" + format!("{} · switch to {}", props.mode.label(), next_mode.label()) }; render_warping_indicator_button( props.button_handle.clone(), appearance, - icon, + content, props.keystroke, - tooltip_text.to_string(), + tooltip_text, is_active, is_locked, move |ctx| { diff --git a/app/src/ai/execution_profiles/editor/mod.rs b/app/src/ai/execution_profiles/editor/mod.rs index d8ac313b..7b67f4a2 100644 --- a/app/src/ai/execution_profiles/editor/mod.rs +++ b/app/src/ai/execution_profiles/editor/mod.rs @@ -201,8 +201,8 @@ pub struct ExecutionProfileEditorView { command_denylist_editor: ViewHandle, command_allowlist_editor: ViewHandle, command_allowlist_mouse_state_handles: Vec, - orchestration_provider_switches: Vec<(String, SwitchStateHandle)>, - all_providers_switch: SwitchStateHandle, + orchestration_provider_checkboxes: Vec<(String, MouseStateHandle)>, + all_providers_checkbox: MouseStateHandle, command_denylist_mouse_state_handles: Vec, command_denylist_tooltip_mouse_state_handles: Vec, mcp_denylist_dropdown: ViewHandle>, @@ -364,8 +364,8 @@ impl ExecutionProfileEditorView { command_denylist_editor, command_allowlist_editor, command_allowlist_mouse_state_handles: Vec::new(), - orchestration_provider_switches: Vec::new(), - all_providers_switch: Default::default(), + orchestration_provider_checkboxes: Vec::new(), + all_providers_checkbox: Default::default(), command_denylist_mouse_state_handles, command_denylist_tooltip_mouse_state_handles: profile_data .command_denylist @@ -555,16 +555,16 @@ impl ExecutionProfileEditorView { self.command_allowlist_mouse_state_handles = current_permissions.command_allowlist.iter().map(|_| Default::default()).collect(); let mut names: Vec<_> = LLMPreferences::as_ref(app).get_base_llm_choices_for_agent_mode(app) - .filter(|model| !crate::ai::custom_model_routers::is_custom_router_id(model.id.as_str())) + .filter(|model| !matches!(model.id.as_str(), "none" | "placeholder") && !crate::ai::custom_model_routers::is_custom_router_id(model.id.as_str())) .filter_map(|model| model.description.clone()).collect(); names.extend(AISettings::as_ref(app).openai_providers.value().iter().map(|provider| provider.name.clone())); - if let Some(included) = ¤t_permissions.orchestration_providers { names.extend(included.iter().cloned()); } + names.retain(|name| !name.trim().is_empty()); names.sort(); names.dedup(); - self.orchestration_provider_switches.retain(|(name, _)| names.contains(name)); - for name in names { if !self.orchestration_provider_switches.iter().any(|(existing, _)| existing == &name) { - self.orchestration_provider_switches.push((name, Default::default())); + self.orchestration_provider_checkboxes.retain(|(name, _)| names.contains(name)); + for name in names { if !self.orchestration_provider_checkboxes.iter().any(|(existing, _)| existing == &name) { + self.orchestration_provider_checkboxes.push((name, Default::default())); }} - self.orchestration_provider_switches.sort_by(|a, b| a.0.cmp(&b.0)); + self.orchestration_provider_checkboxes.sort_by(|a, b| a.0.cmp(&b.0)); self.command_denylist_mouse_state_handles = current_permissions .command_denylist diff --git a/app/src/ai/execution_profiles/editor/ui_helpers.rs b/app/src/ai/execution_profiles/editor/ui_helpers.rs index d9d5517c..0bf1bbc9 100644 --- a/app/src/ai/execution_profiles/editor/ui_helpers.rs +++ b/app/src/ai/execution_profiles/editor/ui_helpers.rs @@ -9,6 +9,7 @@ use warpui::elements::{ }; use warpui::fonts::{Properties, Weight}; use warpui::ui_components::components::{Coords, UiComponent, UiComponentStyles}; +use warpui::ui_components::text::Span; use warpui::{AppContext, Element, SingletonEntity, ViewHandle}; use super::{ExecutionProfileEditorView, ExecutionProfileEditorViewAction}; @@ -627,23 +628,61 @@ fn render_command_allowlist_section(view: &ExecutionProfileEditorView, profile: .with_child(render_input_list(None, items, Some(&view.command_allowlist_editor), appearance)).finish() } -fn render_orchestration_providers(view: &ExecutionProfileEditorView, profile: &AIExecutionProfile, appearance: &Appearance) -> Box { - let mut column = Flex::column().with_child(create_section_header("Orchestration providers", "Only included providers can supply models for sub-agents and code reviews. All providers includes providers added later.", appearance)); +fn render_orchestration_providers( + view: &ExecutionProfileEditorView, + profile: &AIExecutionProfile, + appearance: &Appearance, +) -> Box { + let mut column = Flex::column().with_child(create_section_header( + "Orchestration providers", + "Choose which providers can supply models for sub-agents and code reviews.", + appearance, + )); let all = profile.orchestration_providers.is_none(); - let names: Vec<_> = view.orchestration_provider_switches.iter().map(|(name, _)| name.clone()).collect(); - let selected = if all { Some(names.clone()) } else { None }; - let switch = appearance.ui_builder().switch(view.all_providers_switch.clone()).check(all).build() - .on_click(move |ctx, _, _| ctx.dispatch_typed_action(ExecutionProfileEditorViewAction::SetOrchestrationProviders { providers: selected.clone() })).finish(); - column.add_child(Flex::row().with_main_axis_alignment(MainAxisAlignment::SpaceBetween).with_child(Text::new("All providers", appearance.ui_font_family(), 13.).with_color(appearance.theme().active_ui_text_color().into()).finish()).with_child(switch).finish()); - for (name, state) in &view.orchestration_provider_switches { - let included = profile.includes_orchestration_provider(name); - let mut next = profile.orchestration_providers.clone().unwrap_or_else(|| names.clone()); - if included { next.retain(|provider| provider != name); } else { next.push(name.clone()); } - let switch = appearance.ui_builder().switch(state.clone()).check(included).build() - .on_click(move |ctx, _, _| ctx.dispatch_typed_action(ExecutionProfileEditorViewAction::SetOrchestrationProviders { providers: Some(next.clone()) })).finish(); - column.add_child(Container::new(Flex::row().with_main_axis_alignment(MainAxisAlignment::SpaceBetween).with_child(Text::new(name.clone(), appearance.ui_font_family(), 13.).with_color(appearance.theme().active_ui_text_color().into()).finish()).with_child(switch).finish()).with_margin_top(8.).finish()); + let names: Vec<_> = view.orchestration_provider_checkboxes + .iter() + .map(|(name, _)| name.clone()) + .collect(); + let selected = if all { Some(names) } else { None }; + let checkbox = appearance.ui_builder() + .checkbox(view.all_providers_checkbox.clone(), Some(14.)) + .with_label(Span::new("All providers", Default::default())) + .check(all) + .build() + .on_click(move |ctx, _, _| { + ctx.dispatch_typed_action(ExecutionProfileEditorViewAction::SetOrchestrationProviders { + providers: selected.clone(), + }); + }) + .finish(); + column.add_child(Container::new(checkbox).with_margin_top(12.).finish()); + + if !all { + let mut providers = Flex::column(); + for (name, state) in &view.orchestration_provider_checkboxes { + let included = profile.includes_orchestration_provider(name); + let mut next = profile.orchestration_providers.clone().unwrap_or_default(); + if included { + next.retain(|provider| provider != name); + } else { + next.push(name.clone()); + } + let checkbox = appearance.ui_builder() + .checkbox(state.clone(), Some(14.)) + .with_label(Span::new(name.clone(), Default::default())) + .check(included) + .build() + .on_click(move |ctx, _, _| { + ctx.dispatch_typed_action(ExecutionProfileEditorViewAction::SetOrchestrationProviders { + providers: Some(next.clone()), + }); + }) + .finish(); + providers.add_child(Container::new(checkbox).with_margin_top(12.).finish()); + } + column.add_child(Container::new(providers.finish()).with_margin_left(24.).finish()); } - Container::new(column.finish()).with_margin_top(16.).finish() + Container::new(column.finish()).with_margin_top(16.).with_margin_bottom(12.).finish() } fn display_mcp_name(uuid: &Uuid, app: &AppContext) -> String { diff --git a/app/src/terminal/view.rs b/app/src/terminal/view.rs index 1abae1ea..a513b994 100644 --- a/app/src/terminal/view.rs +++ b/app/src/terminal/view.rs @@ -27437,15 +27437,12 @@ impl TypedActionView for TerminalView { ToggleAutoexecuteMode => { AIExecutionProfilesModel::handle(ctx).update(ctx, |profiles, ctx| { let profile = profiles.active_profile(Some(self.view_id), ctx); + use crate::ai::execution_profiles::PermissionMode; let next = match profile.data().permission_mode { - crate::ai::execution_profiles::PermissionMode::ReadOnly => return, - crate::ai::execution_profiles::PermissionMode::AskFirst => { - crate::ai::execution_profiles::PermissionMode::GalaxyDecides - } - crate::ai::execution_profiles::PermissionMode::GalaxyDecides - | crate::ai::execution_profiles::PermissionMode::AllowAll => { - crate::ai::execution_profiles::PermissionMode::AskFirst - } + PermissionMode::ReadOnly => PermissionMode::AskFirst, + PermissionMode::AskFirst => PermissionMode::GalaxyDecides, + PermissionMode::GalaxyDecides => PermissionMode::AllowAll, + PermissionMode::AllowAll => PermissionMode::ReadOnly, }; profiles.set_permission_mode(*profile.id(), next, ctx); });