Simplify Galaxy permissions and preserve explicit user authorization

This commit is contained in:
2026-10-02 01:58:37 -05:00
parent 5a70995502
commit 2796fdd114
37 changed files with 1818 additions and 3530 deletions
+49 -734
View File
@@ -1,5 +1,3 @@
use std::path::{Path, PathBuf};
use ai::api_keys::{ApiKeyManager, ApiKeyManagerEvent};
use galaxy_core::ui::theme::color::internal_colors;
use itertools::Itertools;
@@ -25,13 +23,12 @@ use crate::ai::execution_profiles::profiles::{
AIExecutionProfilesModel, AIExecutionProfilesModelEvent, ClientProfileId,
};
use crate::ai::execution_profiles::{
AIExecutionProfile, AIExecutionProfileAppExt as _, ActionPermission, RunAgentsPermission,
WriteToPtyPermission,
AIExecutionProfile, AIExecutionProfileAppExt as _, PermissionMode,
};
use crate::ai::llms::{
DisableReason, LLMContextWindow, LLMId, LLMInfo, LLMPreferences, LLMPreferencesEvent,
};
use crate::ai::paths::host_native_absolute_path;
use crate::cloud_object::model::generic_string_model::StringModel;
use crate::editor::{
EditorView, Event as EditorEvent, InteractionState, SingleLineEditorOptions, TextOptions,
};
@@ -46,8 +43,8 @@ use crate::view_components::{
Dropdown, DropdownItem, FilterableDropdown, SubmittableTextInput, SubmittableTextInputEvent,
};
use crate::workspace::WorkspaceAction;
use crate::workspaces::user_workspaces::UserWorkspacesEvent;
use crate::{Appearance, TemplatableMCPServerManager, UserWorkspaces};
use crate::workspaces::user_workspaces::{UserWorkspaces, UserWorkspacesEvent};
use crate::{Appearance, TemplatableMCPServerManager};
const MODEL_MENU_WIDTH: f32 = 480.;
@@ -119,19 +116,7 @@ fn render_upgrade_footer(
#[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,
permission_mode_tooltip_mouse_state: MouseStateHandle,
mcp_denylist_editor_tooltip_mouse_state: MouseStateHandle,
}
@@ -171,36 +156,8 @@ pub enum ExecutionProfileEditorViewAction {
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,
SetPermissionMode {
mode: PermissionMode,
},
AddToCommandDenylist {
predicate: AgentModeCommandExecutionPredicate,
@@ -208,18 +165,6 @@ pub enum ExecutionProfileEditorViewAction {
RemoveFromCommandDenylist {
predicate: AgentModeCommandExecutionPredicate,
},
AddToDirectoryAllowlist {
path: PathBuf,
},
RemoveFromDirectoryAllowlist {
path: PathBuf,
},
AddToMCPAllowlist {
id: uuid::Uuid,
},
RemoveFromMCPAllowlist {
id: uuid::Uuid,
},
AddToMCPDenylist {
id: uuid::Uuid,
},
@@ -249,23 +194,10 @@ pub struct ExecutionProfileEditorView {
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>,
permission_mode_dropdown: ViewHandle<Dropdown<ExecutionProfileEditorViewAction>>,
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>,
@@ -280,254 +212,26 @@ 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 permission_mode_dropdown = ctx.add_typed_action_view(|ctx| {
let mut dropdown = Dropdown::new(ctx);
dropdown.set_items(
vec![
[
PermissionMode::ReadOnly,
PermissionMode::AskFirst,
PermissionMode::GalaxyDecides,
]
.into_iter()
.map(|mode| {
DropdownItem::new(
"Always allow",
ExecutionProfileEditorViewAction::SetApplyCodeDiffs {
permission: ActionPermission::AlwaysAllow,
},
),
DropdownItem::new(
"Always ask",
ExecutionProfileEditorViewAction::SetApplyCodeDiffs {
permission: ActionPermission::AlwaysAsk,
},
),
DropdownItem::new(
"Galaxy Decides",
ExecutionProfileEditorViewAction::SetApplyCodeDiffs {
permission: ActionPermission::GalaxyDecides,
},
),
],
mode.label(),
ExecutionProfileEditorViewAction::SetPermissionMode { mode },
)
})
.collect(),
ctx,
);
dropdown
});
let read_files_dropdown = ctx.add_typed_action_view(|ctx| {
let mut dropdown = Dropdown::new(ctx);
dropdown.set_items(
vec![
DropdownItem::new(
"Always allow",
ExecutionProfileEditorViewAction::SetReadFiles {
permission: ActionPermission::AlwaysAllow,
},
),
DropdownItem::new(
"Always ask",
ExecutionProfileEditorViewAction::SetReadFiles {
permission: ActionPermission::AlwaysAsk,
},
),
DropdownItem::new(
"Galaxy Decides",
ExecutionProfileEditorViewAction::SetReadFiles {
permission: ActionPermission::GalaxyDecides,
},
),
],
ctx,
);
dropdown
});
let execute_commands_dropdown = ctx.add_typed_action_view(|ctx| {
let mut dropdown = Dropdown::new(ctx);
dropdown.set_items(
vec![
DropdownItem::new(
"Always allow",
ExecutionProfileEditorViewAction::SetExecuteCommands {
permission: ActionPermission::AlwaysAllow,
},
),
DropdownItem::new(
"Always ask",
ExecutionProfileEditorViewAction::SetExecuteCommands {
permission: ActionPermission::AlwaysAsk,
},
),
DropdownItem::new(
"Galaxy Decides",
ExecutionProfileEditorViewAction::SetExecuteCommands {
permission: ActionPermission::GalaxyDecides,
},
),
],
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,
},
),
DropdownItem::new(
"Galaxy Decides",
ExecutionProfileEditorViewAction::SetWriteToPty {
permission: WriteToPtyPermission::GalaxyDecides,
},
),
],
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(
"Always allow",
ExecutionProfileEditorViewAction::SetCallMcpServers {
permission: ActionPermission::AlwaysAllow,
},
),
DropdownItem::new(
"Always ask",
ExecutionProfileEditorViewAction::SetCallMcpServers {
permission: ActionPermission::AlwaysAsk,
},
),
DropdownItem::new(
"Galaxy Decides",
ExecutionProfileEditorViewAction::SetCallMcpServers {
permission: ActionPermission::GalaxyDecides,
},
),
],
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,
},
),
DropdownItem::new(
"Galaxy Decides",
ExecutionProfileEditorViewAction::SetComputerUse {
permission: super::ComputerUsePermission::GalaxyDecides,
},
),
],
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,
},
),
DropdownItem::new(
"Galaxy Decides",
ExecutionProfileEditorViewAction::SetRunAgents {
permission: RunAgentsPermission::GalaxyDecides,
},
),
],
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");
@@ -537,12 +241,6 @@ impl ExecutionProfileEditorView {
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()
@@ -594,19 +292,6 @@ impl ExecutionProfileEditorView {
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());
@@ -620,21 +305,6 @@ impl ExecutionProfileEditorView {
.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 {
@@ -677,27 +347,14 @@ impl ExecutionProfileEditorView {
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,
permission_mode_dropdown,
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,
@@ -718,24 +375,6 @@ impl ExecutionProfileEditorView {
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) {
@@ -754,20 +393,6 @@ impl ExecutionProfileEditorView {
}
});
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);
@@ -867,23 +492,22 @@ impl ExecutionProfileEditorView {
);
let workspace = UserWorkspaces::handle(ctx);
ctx.subscribe_to_model(&workspace, |me, workspace, event, ctx| {
ctx.subscribe_to_model(&workspace, |me, _, event, ctx| {
if let UserWorkspacesEvent::TeamsChanged = event {
Self::update_all_editor_interaction_states(me, workspace, ctx);
Self::update_all_editor_interaction_states(me, 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);
Self::update_all_editor_interaction_states(me, ctx);
me.sync_context_window_editor(ctx, true);
ctx.notify();
}
});
Self::update_all_editor_interaction_states(&view, workspace, ctx);
Self::update_all_editor_interaction_states(&view, ctx);
view.refresh_profile_state(ctx);
@@ -901,12 +525,6 @@ impl ExecutionProfileEditorView {
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()
@@ -919,18 +537,6 @@ impl ExecutionProfileEditorView {
.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()
@@ -941,18 +547,6 @@ impl ExecutionProfileEditorView {
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(),
@@ -986,65 +580,22 @@ impl ExecutionProfileEditorView {
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 },
&current_permissions.mcp_allowlist,
&current_permissions.mcp_denylist,
ctx,
);
self.permission_mode_dropdown.update(ctx, |dropdown, ctx| {
dropdown.set_selected_by_action(
ExecutionProfileEditorViewAction::SetPermissionMode {
mode: current_permissions.permission_mode,
},
ctx,
);
if AISettings::as_ref(ctx).is_any_ai_enabled(ctx) {
dropdown.set_enabled(ctx);
} else {
dropdown.set_disabled(ctx);
}
});
Self::refresh_mcp_dropdown(
&self.mcp_denylist_dropdown,
|uuid| ExecutionProfileEditorViewAction::AddToMCPDenylist { id: uuid },
&current_permissions.mcp_allowlist,
&current_permissions.mcp_denylist,
ctx,
);
@@ -1053,135 +604,6 @@ impl ExecutionProfileEditorView {
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::AlwaysAllow => 0,
ActionPermission::AlwaysAsk | ActionPermission::Unknown => 1,
ActionPermission::GalaxyDecides => 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,
WriteToPtyPermission::GalaxyDecides => 3,
};
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,
super::ComputerUsePermission::GalaxyDecides => 3,
};
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,
RunAgentsPermission::GalaxyDecides => 3,
};
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>,
@@ -1295,7 +717,6 @@ impl ExecutionProfileEditorView {
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
@@ -1305,9 +726,7 @@ impl ExecutionProfileEditorView {
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)
})
.filter(|(uuid, _server_name)| !profile_mcp_denylist.contains(uuid))
.collect();
dropdown.set_items(
@@ -1358,11 +777,7 @@ impl ExecutionProfileEditorView {
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()
};
let display_name = profile_data.display_name();
// 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
@@ -1370,37 +785,19 @@ impl ExecutionProfileEditorView {
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>,
) {
fn update_all_editor_interaction_states(view: &Self, 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(),
view.profile_name_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(),
view.command_denylist_editor.as_ref(ctx).editor().clone(),
is_any_ai_enabled,
ctx,
);
}
@@ -1537,11 +934,7 @@ impl View for ExecutionProfileEditorView {
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_header_section(appearance, &self.profile_name_editor))
.with_child(render_models_section(appearance, self, app))
.with_child(render_permissions_section(
appearance,
@@ -1649,63 +1042,9 @@ impl TypedActionView for ExecutionProfileEditorView {
});
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);
ExecutionProfileEditorViewAction::SetPermissionMode { mode } => {
AIExecutionProfilesModel::handle(ctx).update(ctx, |profiles, ctx| {
profiles.set_permission_mode(self.profile_id, *mode, ctx);
});
ctx.notify();
}
@@ -1721,30 +1060,6 @@ impl TypedActionView for ExecutionProfileEditorView {
});
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);
@@ -15,7 +15,6 @@ 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;
@@ -72,9 +71,8 @@ pub const WORKSPACE_OVERRIDE_TOOLTIP_MESSAGE: &str =
pub fn render_header_section(
appearance: &Appearance,
profile_name_editor: &ViewHandle<EditorView>,
is_default_profile: bool,
) -> Box<dyn Element> {
let mut column = Flex::column()
let column = Flex::column()
.with_child(render_header_title(appearance))
.with_child(render_header_name_label(appearance))
.with_child(
@@ -90,14 +88,6 @@ pub fn render_header_section(
.finish(),
);
if is_default_profile {
column.add_child(render_info_section(
"Default profile name cannot be changed.",
None,
appearance,
));
}
Container::new(column.finish())
.with_margin_bottom(24.)
.finish()
@@ -446,166 +436,23 @@ pub fn render_permissions_section(
profile_data: &AIExecutionProfile,
app: &galaxyui::AppContext,
) -> Box<dyn Element> {
let ai_settings = AISettings::as_ref(app);
let mut column = Flex::column().with_children([
render_separator(appearance),
render_section_label("PERMISSIONS", appearance),
render_permission_row(
appearance,
Icon::Code2,
"Apply code diffs",
&view.apply_code_diffs_dropdown,
profile_data.apply_code_diffs.description(),
!ai_settings.is_code_diffs_permissions_editable(app),
Icon::Terminal,
"Permission mode",
&view.permission_mode_dropdown,
profile_data.permission_mode.description(),
false,
view.tooltip_mouse_state_handles
.apply_code_diffs_tooltip_mouse_state
.clone(),
),
render_permission_row(
appearance,
Icon::Notebook,
"Read files",
&view.read_files_dropdown,
profile_data.read_files.description(),
!ai_settings.is_read_files_permissions_editable(app),
view.tooltip_mouse_state_handles
.read_files_tooltip_mouse_state
.permission_mode_tooltip_mouse_state
.clone(),
),
render_command_denylist_section(view, profile_data, appearance, app),
render_mcp_denylist_section(view, profile_data, app, appearance),
]);
if profile_data.read_files == ActionPermission::AlwaysAsk {
column.add_child(render_directory_allowlist_section(
view,
profile_data,
appearance,
app,
));
}
column.add_child(render_permission_row(
appearance,
Icon::Terminal,
"Execute commands",
&view.execute_commands_dropdown,
profile_data.execute_commands.description(),
!ai_settings.is_execute_commands_permissions_editable(app),
view.tooltip_mouse_state_handles
.execute_commands_tooltip_mouse_state
.clone(),
));
match profile_data.execute_commands {
ActionPermission::AlwaysAllow | ActionPermission::GalaxyDecides => {
column.add_child(render_command_denylist_section(
view,
profile_data,
appearance,
app,
));
}
ActionPermission::AlwaysAsk => {
column.add_child(render_command_allowlist_section(
view,
profile_data,
appearance,
app,
));
}
ActionPermission::Unknown => {
column.add_children([
render_command_allowlist_section(view, profile_data, appearance, app),
render_command_denylist_section(view, profile_data, appearance, app),
]);
}
}
column.add_child(render_permission_row(
appearance,
Icon::Workflow,
"Interact with running commands",
&view.write_to_pty_dropdown,
profile_data.write_to_pty.description(),
!ai_settings.is_write_to_pty_permissions_editable(app),
view.tooltip_mouse_state_handles
.write_to_pty_tooltip_mouse_state
.clone(),
));
if FeatureFlag::LocalComputerUse.is_enabled() {
column.add_child(render_permission_row(
appearance,
Icon::Laptop,
"Computer use",
&view.computer_use_dropdown,
profile_data.computer_use.description(),
!ai_settings.is_computer_use_permissions_editable(app),
view.tooltip_mouse_state_handles
.computer_use_tooltip_mouse_state
.clone(),
));
}
column.add_child(render_permission_row(
appearance,
Icon::MessageText,
"Ask questions",
&view.ask_user_question_dropdown,
profile_data.ask_user_question.description(),
!ai_settings.is_ask_user_question_permissions_editable(app),
view.tooltip_mouse_state_handles
.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,
Icon::Dataflow,
"Call MCP servers",
&view.call_mcp_servers_dropdown,
profile_data.mcp_permissions.description(),
!ai_settings.is_mcp_permission_editable(app), // Use MCP override for this permission
view.tooltip_mouse_state_handles
.call_mcp_servers_tooltip_mouse_state
.clone(),
));
match profile_data.mcp_permissions {
ActionPermission::AlwaysAllow | ActionPermission::GalaxyDecides => {
column.add_child(render_mcp_denylist_section(
view,
profile_data,
app,
appearance,
));
}
ActionPermission::AlwaysAsk => {
column.add_child(render_mcp_allowlist_section(
view,
profile_data,
app,
appearance,
));
}
ActionPermission::Unknown => {
column.add_children([
render_mcp_allowlist_section(view, profile_data, app, appearance),
render_mcp_denylist_section(view, profile_data, app, appearance),
]);
}
}
if FeatureFlag::WebSearchUI.is_enabled() {
column.add_child(
Container::new(render_web_search_toggle(appearance, view, profile_data))
@@ -709,57 +556,6 @@ where
.finish()
}
fn render_directory_allowlist_section(
view: &ExecutionProfileEditorView,
profile_data: &AIExecutionProfile,
appearance: &Appearance,
app: &galaxyui::AppContext,
) -> Box<dyn Element> {
let ai_settings = AISettings::as_ref(app);
let is_editable = ai_settings.is_directory_allowlist_editable(app);
render_list_section(
"Directory allowlist",
"Give the agent file access to certain directories.",
&profile_data.directory_allowlist,
&view.directory_allowlist_mouse_state_handles,
Some(&view.directory_allowlist_editor),
None,
|path| ExecutionProfileEditorViewAction::RemoveFromDirectoryAllowlist { path },
|path| path.display().to_string(),
appearance,
is_editable,
view.tooltip_mouse_state_handles
.directory_allowlist_editor_tooltip_mouse_state
.clone(),
)
}
fn render_command_allowlist_section(
view: &ExecutionProfileEditorView,
profile_data: &AIExecutionProfile,
appearance: &Appearance,
app: &galaxyui::AppContext,
) -> Box<dyn Element> {
let ai_settings = AISettings::as_ref(app);
let is_editable = ai_settings.is_command_allowlist_editable(app);
render_list_section(
"Command allowlist",
"Regular expressions to match commands that can be automatically executed by Oz.",
&profile_data.command_allowlist,
&view.command_allowlist_mouse_state_handles,
Some(&view.command_allowlist_editor),
None,
|predicate| ExecutionProfileEditorViewAction::RemoveFromCommandAllowlist { predicate },
|item| item.to_string(),
appearance,
is_editable,
view.tooltip_mouse_state_handles
.command_allowlist_editor_tooltip_mouse_state
.clone(),
)
}
fn render_command_denylist_section(
view: &ExecutionProfileEditorView,
profile_data: &AIExecutionProfile,
@@ -809,7 +605,7 @@ fn render_command_denylist_section(
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.",
"Regular expressions for restricted commands. Ask First requires confirmation; Galaxy Decides denies matching commands.",
appearance,
));
column = column.with_child(list);
@@ -826,32 +622,6 @@ fn display_mcp_name(uuid: &Uuid, app: &AppContext) -> String {
})
}
fn render_mcp_allowlist_section(
view: &ExecutionProfileEditorView,
profile_data: &AIExecutionProfile,
app: &galaxyui::AppContext,
appearance: &Appearance,
) -> Box<dyn Element> {
let ai_settings = AISettings::as_ref(app);
let is_editable = ai_settings.is_mcp_permission_editable(app);
render_list_section(
"MCP allowlist",
"MCP servers that are allowed to be called by Oz.",
&profile_data.mcp_allowlist,
&view.mcp_allowlist_mouse_state_handles,
None,
Some(&view.mcp_allowlist_dropdown),
|id| ExecutionProfileEditorViewAction::RemoveFromMCPAllowlist { id },
|uuid| display_mcp_name(uuid, app),
appearance,
is_editable,
view.tooltip_mouse_state_handles
.mcp_allowlist_editor_tooltip_mouse_state
.clone(),
)
}
fn render_mcp_denylist_section(
view: &ExecutionProfileEditorView,
profile_data: &AIExecutionProfile,
@@ -863,7 +633,7 @@ fn render_mcp_denylist_section(
render_list_section(
"MCP denylist",
"MCP servers that are not allowed to be called by Oz.",
"MCP servers restricted by this profile.",
&profile_data.mcp_denylist,
&view.mcp_denylist_mouse_state_handles,
None,
+9 -7
View File
@@ -1,7 +1,7 @@
pub use cloud_object_models::{
AIExecutionProfile, ActionPermission, AskUserQuestionPermission, CloudAIExecutionProfile,
CloudAIExecutionProfileModel, ComputerUsePermission, RunAgentsPermission, WriteToPtyPermission,
PROFILE_NAME_MAX_LENGTH,
CloudAIExecutionProfileModel, ComputerUsePermission, PermissionMode, RunAgentsPermission,
WriteToPtyPermission, PROFILE_NAME_MAX_LENGTH,
};
use galaxy_core::features::FeatureFlag;
use galaxyui::{AppContext, SingletonEntity};
@@ -262,11 +262,13 @@ impl StringModel for AIExecutionProfile {
}
fn display_name(&self) -> String {
// Handles case where default profile was previously created and named "Untitled"
if self.is_default_profile {
"Default".to_string()
} else if self.name.trim().is_empty() {
"Untitled".to_string()
if self.name.trim().is_empty() {
if self.is_default_profile {
"Default"
} else {
"Untitled"
}
.to_string()
} else {
self.name.clone()
}
+101
View File
@@ -648,6 +648,51 @@ impl AIExecutionProfilesModel {
);
}
pub fn set_permission_mode(
&mut self,
profile_id: ClientProfileId,
mode: super::PermissionMode,
ctx: &mut ModelContext<Self>,
) {
self.edit_profile_internal(
profile_id,
|profile| {
if profile.permission_mode == mode {
return false;
}
profile.permission_mode = mode;
profile.approved_command_hashes.clear();
profile.project_permission_mode();
true
},
ctx,
);
}
pub fn remember_approved_command(
&mut self,
profile_id: ClientProfileId,
fingerprint: String,
ctx: &mut ModelContext<Self>,
) {
self.edit_profile_internal(
profile_id,
|profile| {
if profile.permission_mode != super::PermissionMode::AskFirst
|| profile.approved_command_hashes.contains(&fingerprint)
{
return false;
}
if profile.approved_command_hashes.len() >= 256 {
profile.approved_command_hashes.remove(0);
}
profile.approved_command_hashes.push(fingerprint);
true
},
ctx,
);
}
pub fn set_execute_commands(
&mut self,
profile_id: ClientProfileId,
@@ -887,6 +932,62 @@ impl AIExecutionProfilesModel {
);
}
/// Select exactly one persisted default, retaining the former default as a
/// normal profile. Explicit per-terminal selections remain unchanged.
pub fn set_default_profile(
&mut self,
profile_id: ClientProfileId,
ctx: &mut ModelContext<Self>,
) {
if profile_id == self.default_profile_state.id()
|| matches!(self.default_profile_state, DefaultProfileState::Cli { .. })
{
return;
}
let Some(target) = self.get_profile_by_id(profile_id, ctx) else {
return;
};
let Some(sync_id) = target.sync_id() else {
return;
};
if LocalObjectRepository::as_ref(ctx)
.execution_profile(&sync_id, ctx)
.is_none()
{
return;
}
let previous_id = self.default_profile_state.id();
// Materialize an unsaved default before replacing its in-memory slot.
self.edit_profile_internal(
previous_id,
|profile| {
if profile.name.trim().is_empty() {
profile.name = "Default".to_owned();
}
profile.is_default_profile = false;
true
},
ctx,
);
for id in self.get_all_profile_ids() {
self.edit_profile_internal(
id,
|profile| {
let is_default = id == profile_id;
if profile.is_default_profile == is_default {
return false;
}
profile.is_default_profile = is_default;
true
},
ctx,
);
}
self.default_profile_state = DefaultProfileState::Synced { id: profile_id };
ctx.emit(AIExecutionProfilesModelEvent::ProfileUpdated(previous_id));
ctx.emit(AIExecutionProfilesModelEvent::ProfileUpdated(profile_id));
}
pub fn set_profile_name(
&mut self,
profile_id: ClientProfileId,
+73 -11
View File
@@ -5,15 +5,16 @@ use galaxyui::{App, SingletonEntity};
use crate::ai::execution_profiles::profiles::AIExecutionProfilesModel;
use crate::ai::execution_profiles::{
AIExecutionProfile, ActionPermission, CloudAIExecutionProfileModel, WriteToPtyPermission,
AIExecutionProfile, ActionPermission, CloudAIExecutionProfileModel, PermissionMode,
WriteToPtyPermission,
};
use crate::ai::mcp::TemplatableMCPServerManager;
use crate::auth::user::TEST_USER_UID;
use crate::auth::{AuthStateProvider, UserUid};
use crate::cloud_object::model::persistence::{CloudModel, CloudModelEvent};
use crate::cloud_object::{
Owner, Revision, ServerAIExecutionProfile, ServerGuestSubject, ServerMetadata,
ServerObjectGuest, ServerPermissions,
CloudObject as _, Owner, Revision, ServerAIExecutionProfile, ServerGuestSubject,
ServerMetadata, ServerObjectGuest, ServerPermissions,
};
use crate::network::NetworkStatus;
use crate::server::cloud_objects::update_manager::UpdateManager;
@@ -107,27 +108,26 @@ fn edits_persist_default_profile_locally_when_logged_out() {
let default_profile_id = profile_model.read(&app, |model, _ctx| model.default_profile_id());
// Sanity-check the precondition: the baseline `apply_code_diffs`
// on a fresh default profile is `GalaxyDecides`.
// Fresh profiles start in Ask First mode.
profile_model.read(&app, |model, ctx| {
assert!(
matches!(
model.default_profile(ctx).data().apply_code_diffs,
ActionPermission::GalaxyDecides
model.default_profile(ctx).data().permission_mode,
PermissionMode::AskFirst
),
"unexpected baseline apply_code_diffs"
"unexpected baseline permission mode"
);
});
profile_model.update(&mut app, |model, ctx| {
model.set_apply_code_diffs(default_profile_id, &ActionPermission::AlwaysAllow, ctx);
model.set_permission_mode(default_profile_id, PermissionMode::GalaxyDecides, ctx);
});
profile_model.read(&app, |model, ctx| {
let default_profile = model.default_profile(ctx);
assert_eq!(
default_profile.data().apply_code_diffs,
ActionPermission::AlwaysAllow,
default_profile.data().permission_mode,
PermissionMode::GalaxyDecides,
"the local default profile should retain the edit",
);
let persisted_id = default_profile
@@ -412,3 +412,65 @@ fn legacy_agent_decides_profiles_use_galaxy_decides() {
"\"GalaxyDecides\""
);
}
#[test]
fn default_can_be_renamed_and_transferred_without_losing_the_previous_profile() {
App::test((), |mut app| async move {
install_singletons(&mut app, AuthStateProvider::new_logged_out_for_test());
let profiles = app.add_singleton_model(|ctx| {
AIExecutionProfilesModel::new(&LaunchMode::new_for_unit_test(), ctx)
});
profiles.update(&mut app, |profiles, ctx| {
let original = profiles.default_profile_id();
let replacement = profiles.create_profile(ctx).unwrap();
profiles.set_profile_name(original, "Personal", ctx);
profiles.set_profile_name(replacement, "Work", ctx);
profiles.set_default_profile(replacement, ctx);
assert_eq!(profiles.default_profile_id(), replacement);
assert_eq!(profiles.default_profile(ctx).data().name, "Work");
assert_eq!(
profiles
.get_profile_by_id(original, ctx)
.unwrap()
.data()
.name,
"Personal"
);
assert!(
!profiles
.get_profile_by_id(original, ctx)
.unwrap()
.data()
.is_default_profile
);
// Selecting the checked default must never leave zero defaults.
profiles.set_default_profile(replacement, ctx);
assert_eq!(
profiles
.get_all_profile_ids()
.iter()
.filter(|id| {
profiles
.get_profile_by_id(**id, ctx)
.unwrap()
.data()
.is_default_profile
})
.count(),
1
);
profiles.set_default_profile(original, ctx);
assert_eq!(profiles.default_profile_id(), original);
let stored = crate::local_object_repository::LocalObjectRepository::as_ref(ctx)
.execution_profiles(ctx);
assert_eq!(
stored
.iter()
.filter(|p| p.model().string_model.is_default_profile)
.count(),
1
);
assert_eq!(stored.len(), 2);
});
});
}