use crate::ai::blocklist::BlocklistAIPermissions; use crate::ai::execution_profiles::profiles::{ AIExecutionProfilesModel, AIExecutionProfilesModelEvent, ClientProfileId, }; use crate::ai::execution_profiles::{ ActionPermission, AskUserQuestionPermission, WriteToPtyPermission, }; use crate::ai::llms::LLMPreferences; use crate::appearance::Appearance; use crate::cloud_object::model::generic_string_model::StringModel; use crate::settings::AISettings; use crate::ui_components::icons::Icon; use crate::view_components::action_button::{ActionButton, ButtonSize, SecondaryTheme}; use crate::TemplatableMCPServerManager; use std::path::PathBuf; use uuid::Uuid; use galaxy_core::features::FeatureFlag; use galaxyui::elements::ParentElement; use galaxyui::SingletonEntity; use galaxyui::{ elements::{ ConstrainedBox, Container, CrossAxisAlignment, Flex, MainAxisAlignment, MainAxisSize, Shrinkable, Text, Wrap, }, fonts::{Properties, Weight}, AppContext, Element, Entity, TypedActionView, View, ViewContext, ViewHandle, }; #[derive(Debug, Clone)] pub enum ExecutionProfileViewAction { EditProfile, } pub enum ExecutionProfileViewEvent { EditProfile, } pub struct ExecutionProfileView { profile_id: ClientProfileId, edit_button: ViewHandle, } impl ExecutionProfileView { pub fn new(profile_id: ClientProfileId, ctx: &mut ViewContext) -> Self { ctx.subscribe_to_model(&AIExecutionProfilesModel::handle(ctx), |me, _, event, ctx| { if matches!(event, AIExecutionProfilesModelEvent::ProfileUpdated(profile_id) if *profile_id == me.profile_id) { ctx.notify(); } }); ctx.subscribe_to_model(&LLMPreferences::handle(ctx), |_me, _, _, ctx| { ctx.notify(); }); let edit_button = ctx.add_typed_action_view(|_ctx| { ActionButton::new("Edit", SecondaryTheme) .with_icon(Icon::Pencil) .with_size(ButtonSize::Small) .on_click(|ctx| { ctx.dispatch_typed_action(ExecutionProfileViewAction::EditProfile); }) }); let is_any_ai_enabled = AISettings::as_ref(ctx).is_any_ai_enabled(ctx); edit_button.update(ctx, |button, ctx| { button.set_disabled(!is_any_ai_enabled, ctx); }); ctx.subscribe_to_model(&AISettings::handle(ctx), |me, _, _, ctx| { let is_any_ai_enabled = AISettings::as_ref(ctx).is_any_ai_enabled(ctx); me.edit_button.update(ctx, |button, ctx| { button.set_disabled(!is_any_ai_enabled, ctx); }); ctx.notify(); }); Self { profile_id, edit_button, } } } impl Entity for ExecutionProfileView { type Event = ExecutionProfileViewEvent; } impl View for ExecutionProfileView { fn ui_name() -> &'static str { "ExecutionProfileView" } fn render(&self, app: &AppContext) -> Box { let appearance = Appearance::as_ref(app); let is_any_ai_enabled = AISettings::as_ref(app).is_any_ai_enabled(app); let permissions = BlocklistAIPermissions::as_ref(app); let profile = permissions.permissions_profile_for_id(app, self.profile_id); let llm_preferences = LLMPreferences::as_ref(app); let base_model = profile .base_model .as_ref() .and_then(|id| llm_preferences.get_llm_info(id)) .map(|info| info.display_name.clone()) .unwrap_or_else(|| { llm_preferences .get_default_base_model() .display_name .clone() }); let cli_agent_model = profile .cli_agent_model .as_ref() .and_then(|id| llm_preferences.get_llm_info(id)) .map(|info| info.display_name.clone()) .unwrap_or_else(|| "Auto".to_string()); let computer_use_model = profile .computer_use_model .as_ref() .and_then(|id| llm_preferences.get_llm_info(id)) .map(|info| info.display_name.clone()) .unwrap_or_else(|| "Auto".to_string()); Container::new( Flex::column() .with_child( Flex::row() .with_main_axis_size(MainAxisSize::Max) .with_main_axis_alignment(MainAxisAlignment::SpaceBetween) .with_cross_axis_alignment(CrossAxisAlignment::Center) .with_child( Text::new(profile.display_name(), appearance.ui_font_family(), 14.) .with_style(Properties::default().weight(Weight::Medium)) .with_color(if is_any_ai_enabled { appearance.theme().active_ui_text_color().into() } else { appearance.theme().disabled_ui_text_color().into() }) .finish(), ) .with_child(self.edit_button.as_ref(app).render(app)) .finish(), ) .with_child({ let mut model_flex = Flex::column(); model_flex.add_child( Container::new( Text::new("MODELS", appearance.ui_font_family(), 10.) .with_color(appearance.theme().disabled_ui_text_color().into()) .finish(), ) .with_margin_bottom(8.) .finish(), ); model_flex.add_child(with_standard_vertical_margin( render_model_line_with_icon( Icon::Lightning, "Base model:", base_model, appearance, is_any_ai_enabled, ), )); model_flex.add_child(with_standard_vertical_margin( render_model_line_with_icon( Icon::Terminal, "Full terminal use:", cli_agent_model, appearance, is_any_ai_enabled, ), )); if FeatureFlag::LocalComputerUse.is_enabled() { model_flex.add_child(with_standard_vertical_margin( render_model_line_with_icon( Icon::Laptop, "Computer use:", computer_use_model, appearance, is_any_ai_enabled, ), )); } Container::new(model_flex.finish()) .with_margin_top(16.) .with_margin_bottom(8.) .finish() }) .with_child( Container::new({ let mut permissions_column = Flex::column() .with_child( Container::new( Text::new("PERMISSIONS", appearance.ui_font_family(), 10.) .with_color( appearance.theme().disabled_ui_text_color().into(), ) .finish(), ) .with_margin_bottom(8.) .finish(), ) .with_child(with_standard_vertical_margin( render_action_permission_line_with_icon( Icon::Code2, "Apply code diffs:", &profile.apply_code_diffs, appearance, is_any_ai_enabled, ), )) .with_child(with_standard_vertical_margin( render_action_permission_line_with_icon( Icon::Notebook, "Read files:", &profile.read_files, appearance, is_any_ai_enabled, ), )); if profile.read_files == ActionPermission::AlwaysAsk || profile.read_files == ActionPermission::AgentDecides { permissions_column.add_child(render_directory_allowlist( &profile, appearance, is_any_ai_enabled, )); } permissions_column.add_child(with_standard_vertical_margin( render_action_permission_line_with_icon( Icon::Terminal, "Execute commands:", &profile.execute_commands, appearance, is_any_ai_enabled, ), )); match profile.execute_commands { ActionPermission::AlwaysAllow => { permissions_column.add_child(render_command_denylist( &profile, appearance, is_any_ai_enabled, )); } ActionPermission::AlwaysAsk => { permissions_column.add_child(render_command_allowlist( &profile, appearance, is_any_ai_enabled, )); } ActionPermission::AgentDecides | ActionPermission::Unknown => { permissions_column.add_child(render_command_allowlist( &profile, appearance, is_any_ai_enabled, )); permissions_column.add_child(render_command_denylist( &profile, appearance, is_any_ai_enabled, )); } } permissions_column.add_child(with_standard_vertical_margin( render_write_to_pty_permission_line_with_icon( Icon::Workflow, "Interact with running commands:", &profile.write_to_pty, appearance, is_any_ai_enabled, ), )); if FeatureFlag::LocalComputerUse.is_enabled() { permissions_column.add_child(with_standard_vertical_margin( render_computer_use_permission_line_with_icon( Icon::Laptop, "Computer use:", &profile.computer_use, appearance, is_any_ai_enabled, ), )); } permissions_column.add_child(with_standard_vertical_margin( render_ask_user_question_permission_line_with_icon( Icon::MessageText, "Ask questions:", &profile.ask_user_question, appearance, is_any_ai_enabled, ), )); permissions_column.add_child(with_standard_vertical_margin( render_action_permission_line_with_icon( Icon::Dataflow, "Call MCP servers:", &profile.mcp_permissions, appearance, is_any_ai_enabled, ), )); match profile.mcp_permissions { ActionPermission::AlwaysAllow => { permissions_column.add_child(render_mcp_denylist( &profile, appearance, app, is_any_ai_enabled, )); } ActionPermission::AlwaysAsk => { permissions_column.add_child(render_mcp_allowlist( &profile, appearance, app, is_any_ai_enabled, )); } ActionPermission::AgentDecides | ActionPermission::Unknown => { permissions_column.add_child(render_mcp_allowlist( &profile, appearance, app, is_any_ai_enabled, )); permissions_column.add_child(render_mcp_denylist( &profile, appearance, app, is_any_ai_enabled, )); } } if FeatureFlag::WebSearchUI.is_enabled() { permissions_column.add_child(with_standard_vertical_margin( render_bool_permission_line_with_icon( Icon::Globe, "Call web tools:", profile.web_search_enabled, appearance, is_any_ai_enabled, ), )); } permissions_column.add_child(with_standard_vertical_margin( render_bool_permission_line_with_icon( Icon::Compass, "Auto-sync plans to Warp Drive:", profile.autosync_plans_to_warp_drive, appearance, is_any_ai_enabled, ), )); permissions_column.finish() }) .with_margin_top(16.) .with_margin_bottom(8.) .finish(), ) .finish(), ) .with_background(appearance.theme().surface_2()) .with_border( galaxyui::elements::Border::new(1.).with_border_fill(appearance.theme().outline()), ) .with_corner_radius(galaxyui::elements::CornerRadius::with_all( galaxyui::elements::Radius::Pixels(4.), )) .with_horizontal_padding(16.) .with_vertical_padding(12.) .finish() } } impl TypedActionView for ExecutionProfileView { type Action = ExecutionProfileViewAction; fn handle_action(&mut self, action: &Self::Action, ctx: &mut ViewContext) { match action { ExecutionProfileViewAction::EditProfile => { ctx.emit(ExecutionProfileViewEvent::EditProfile); } } } } fn render_chips_row( items: I, appearance: &Appearance, is_ai_enabled: bool, ) -> Box where I: IntoIterator, S: ToString, { let items_vec: Vec = items.into_iter().map(|item| item.to_string()).collect(); if items_vec.is_empty() { return Container::new( Text::new("None", appearance.ui_font_family(), 12.) .with_color(appearance.theme().disabled_ui_text_color().into()) .finish(), ) .finish(); } Wrap::row() .with_run_spacing(4.) .with_children( items_vec .into_iter() .map(|item| { Container::new( Container::new( Text::new(item, appearance.ui_font_family(), 11.) .with_color(if is_ai_enabled { appearance.theme().active_ui_text_color().into() } else { appearance.theme().disabled_ui_text_color().into() }) .finish(), ) .with_background(appearance.theme().surface_2()) .with_border( galaxyui::elements::Border::all(1.) .with_border_fill(appearance.theme().outline()), ) .with_corner_radius(galaxyui::elements::CornerRadius::with_all( galaxyui::elements::Radius::Pixels(3.), )) .with_horizontal_padding(6.) .with_vertical_padding(2.) .finish(), ) .with_margin_right(4.) .finish() }) .collect::>(), ) .finish() } fn render_allowlist_denylist_row( icon: Icon, label: String, items: &[String], appearance: &Appearance, is_ai_enabled: bool, ) -> Box { Container::new( Flex::row() .with_cross_axis_alignment(CrossAxisAlignment::Start) .with_child( Container::new( ConstrainedBox::new( icon.to_galaxyui_icon(if is_ai_enabled { appearance .theme() .sub_text_color(appearance.theme().surface_1()) } else { appearance.theme().disabled_ui_text_color() }) .finish(), ) .with_width(12.) .with_height(12.) .finish(), ) .with_margin_right(6.) .finish(), ) .with_child( Container::new( Text::new(label, appearance.ui_font_family(), 12.) .with_color(if is_ai_enabled { appearance .theme() .sub_text_color(appearance.theme().surface_1()) .into() } else { appearance.theme().disabled_ui_text_color().into() }) .finish(), ) .with_margin_right(8.) .finish(), ) .with_child( Shrinkable::new(1., render_chips_row(items, appearance, is_ai_enabled)).finish(), ) .finish(), ) .with_margin_left(8.) .with_border(galaxyui::elements::Border::left(1.).with_border_fill(appearance.theme().outline())) .with_padding_left(8.) .finish() } fn render_pathbuf_allowlist_row( icon: Icon, label: String, items: &[PathBuf], appearance: &Appearance, is_ai_enabled: bool, ) -> Box { let items_str: Vec = items.iter().map(|p| p.display().to_string()).collect(); render_allowlist_denylist_row(icon, label, &items_str, appearance, is_ai_enabled) } fn render_command_predicate_row( icon: Icon, label: String, items: &[crate::settings::AgentModeCommandExecutionPredicate], appearance: &Appearance, is_ai_enabled: bool, ) -> Box { let items_str: Vec = items.iter().map(|c| c.to_string()).collect(); render_allowlist_denylist_row(icon, label, &items_str, appearance, is_ai_enabled) } fn render_mcp_uuid_row( icon: Icon, label: String, uuids: &[Uuid], appearance: &Appearance, app: &AppContext, is_ai_enabled: bool, ) -> Box { let items_str: Vec = uuids .iter() .filter_map(|uuid| TemplatableMCPServerManager::get_mcp_name(uuid, app)) .collect(); render_allowlist_denylist_row(icon, label, &items_str, appearance, is_ai_enabled) } fn with_standard_vertical_margin(element: Box) -> Box { Container::new(element) .with_margin_top(4.) .with_margin_bottom(4.) .finish() } fn render_model_line_with_icon( icon: Icon, label: impl Into, model_name: impl Into, appearance: &Appearance, is_ai_enabled: bool, ) -> Box { let label = label.into(); let model_name = model_name.into(); Container::new( Flex::row() .with_cross_axis_alignment(CrossAxisAlignment::Center) .with_child( Container::new( ConstrainedBox::new( icon.to_galaxyui_icon(if is_ai_enabled { appearance .theme() .sub_text_color(appearance.theme().surface_1()) } else { appearance.theme().disabled_ui_text_color() }) .finish(), ) .with_width(12.) .with_height(12.) .finish(), ) .with_margin_right(6.) .finish(), ) .with_child( Container::new( Text::new(label, appearance.ui_font_family(), 12.) .with_color(if is_ai_enabled { appearance .theme() .sub_text_color(appearance.theme().surface_1()) .into() } else { appearance.theme().disabled_ui_text_color().into() }) .finish(), ) .with_margin_right(8.) .finish(), ) .with_child( Text::new(model_name, appearance.ui_font_family(), 12.) .with_color(if is_ai_enabled { appearance.theme().active_ui_text_color().into() } else { appearance.theme().disabled_ui_text_color().into() }) .finish(), ) .finish(), ) .finish() } fn render_permission_line_with_icon( icon: Icon, label: impl Into, permission_text: impl Into, appearance: &Appearance, is_ai_enabled: bool, ) -> Box { let label = label.into(); let permission_text = permission_text.into(); Flex::row() .with_cross_axis_alignment(CrossAxisAlignment::Center) .with_child( Container::new( ConstrainedBox::new( icon.to_galaxyui_icon(if is_ai_enabled { appearance .theme() .sub_text_color(appearance.theme().surface_1()) } else { appearance.theme().disabled_ui_text_color() }) .finish(), ) .with_width(12.) .with_height(12.) .finish(), ) .with_margin_right(6.) .finish(), ) .with_child( Container::new( Text::new(label, appearance.ui_font_family(), 12.) .with_color(if is_ai_enabled { appearance .theme() .sub_text_color(appearance.theme().surface_1()) .into() } else { appearance.theme().disabled_ui_text_color().into() }) .finish(), ) .with_margin_right(8.) .finish(), ) .with_child( Text::new(permission_text, appearance.ui_font_family(), 12.) .with_color(if is_ai_enabled { appearance.theme().active_ui_text_color().into() } else { appearance.theme().disabled_ui_text_color().into() }) .finish(), ) .finish() } fn render_action_permission_line_with_icon( icon: Icon, label: impl Into, permission: &ActionPermission, appearance: &Appearance, is_ai_enabled: bool, ) -> Box { let permission_text = match permission { ActionPermission::AgentDecides => "Agent decides", ActionPermission::AlwaysAllow => "Always allow", ActionPermission::AlwaysAsk => "Always ask", ActionPermission::Unknown => "Unknown", }; render_permission_line_with_icon(icon, label, permission_text, appearance, is_ai_enabled) } fn render_write_to_pty_permission_line_with_icon( icon: Icon, label: impl Into, permission: &WriteToPtyPermission, appearance: &Appearance, is_ai_enabled: bool, ) -> Box { let permission_text = match permission { WriteToPtyPermission::AlwaysAllow => "Always allow", WriteToPtyPermission::AlwaysAsk => "Always ask", WriteToPtyPermission::AskOnFirstWrite => "Ask on first write", WriteToPtyPermission::Unknown => "Unknown", }; render_permission_line_with_icon(icon, label, permission_text, appearance, is_ai_enabled) } fn render_computer_use_permission_line_with_icon( icon: Icon, label: impl Into, permission: &crate::ai::execution_profiles::ComputerUsePermission, appearance: &Appearance, is_ai_enabled: bool, ) -> Box { let permission_text = match permission { crate::ai::execution_profiles::ComputerUsePermission::Never | crate::ai::execution_profiles::ComputerUsePermission::Unknown => "Never", crate::ai::execution_profiles::ComputerUsePermission::AlwaysAsk => "Always ask", crate::ai::execution_profiles::ComputerUsePermission::AlwaysAllow => "Always allow", }; render_permission_line_with_icon(icon, label, permission_text, appearance, is_ai_enabled) } fn render_ask_user_question_permission_line_with_icon( icon: Icon, label: impl Into, permission: &AskUserQuestionPermission, appearance: &Appearance, is_ai_enabled: bool, ) -> Box { let permission_text = match permission { AskUserQuestionPermission::Never => "Never ask", AskUserQuestionPermission::AskExceptInAutoApprove | AskUserQuestionPermission::Unknown => { "Ask unless auto-approve" } AskUserQuestionPermission::AlwaysAsk => "Always ask", }; render_permission_line_with_icon(icon, label, permission_text, appearance, is_ai_enabled) } fn render_bool_permission_line_with_icon( icon: Icon, label: impl Into, enabled: bool, appearance: &Appearance, is_ai_enabled: bool, ) -> Box { let permission_text = if enabled { "On" } else { "Off" }; render_permission_line_with_icon(icon, label, permission_text, appearance, is_ai_enabled) } fn render_directory_allowlist( profile: &crate::ai::execution_profiles::AIExecutionProfile, appearance: &Appearance, is_ai_enabled: bool, ) -> Box { with_standard_vertical_margin(render_pathbuf_allowlist_row( Icon::Check, "Directory allowlist:".to_string(), &profile.directory_allowlist, appearance, is_ai_enabled, )) } fn render_command_allowlist( profile: &crate::ai::execution_profiles::AIExecutionProfile, appearance: &Appearance, is_ai_enabled: bool, ) -> Box { with_standard_vertical_margin(render_command_predicate_row( Icon::Check, "Command allowlist:".to_string(), &profile.command_allowlist, appearance, is_ai_enabled, )) } fn render_command_denylist( profile: &crate::ai::execution_profiles::AIExecutionProfile, appearance: &Appearance, is_ai_enabled: bool, ) -> Box { with_standard_vertical_margin(render_command_predicate_row( Icon::SlashCircle, "Command denylist:".to_string(), &profile.command_denylist, appearance, is_ai_enabled, )) } fn render_mcp_allowlist( profile: &crate::ai::execution_profiles::AIExecutionProfile, appearance: &Appearance, app: &AppContext, is_ai_enabled: bool, ) -> Box { with_standard_vertical_margin(render_mcp_uuid_row( Icon::Check, "MCP allowlist:".to_string(), &profile.mcp_allowlist, appearance, app, is_ai_enabled, )) } fn render_mcp_denylist( profile: &crate::ai::execution_profiles::AIExecutionProfile, appearance: &Appearance, app: &AppContext, is_ai_enabled: bool, ) -> Box { with_standard_vertical_margin(render_mcp_uuid_row( Icon::SlashCircle, "MCP denylist:".to_string(), &profile.mcp_denylist, appearance, app, is_ai_enabled, )) }