Improve Galaxy permissions, computer-use routing, and agent UX

This commit is contained in:
2026-09-30 09:55:24 -05:00
parent cc064d4050
commit a2a2633199
30 changed files with 623 additions and 180 deletions
+14 -1
View File
@@ -15,7 +15,6 @@
mod execute;
mod preprocess;
mod risk_review;
use std::collections::{HashMap, HashSet, VecDeque};
use std::path::PathBuf;
use std::sync::Arc;
@@ -43,6 +42,7 @@ use galaxyui::{AppContext, Entity, EntityId, ModelContext, ModelHandle, Singleto
use itertools::Itertools;
use parking_lot::FairMutex;
use preprocess::{PendingPreprocessedActions, PreprocessId};
pub(crate) use risk_review::{ReviewDisplay, ReviewDisplayState};
use self::execute::ask_user_question::AskUserQuestionExecutor;
use self::execute::search_codebase::SearchCodebaseExecutor;
@@ -784,6 +784,7 @@ pub struct BlocklistAIActionModel {
denied_permissions: HashSet<(AIConversationId, AIAgentActionId)>,
pending_permissions: HashSet<(AIConversationId, AIAgentActionId)>,
action_reviews: HashMap<(AIConversationId, AIAgentActionId), risk_review::ActionReview>,
review_outcomes: HashMap<(AIConversationId, AIAgentActionId), risk_review::ReviewOutcome>,
permission_denial_reasons: HashMap<(AIConversationId, AIAgentActionId), String>,
/// Actions parked because their executor-specific UI or state was not ready yet.
@@ -911,6 +912,7 @@ impl BlocklistAIActionModel {
denied_permissions: Default::default(),
pending_permissions: Default::default(),
action_reviews: Default::default(),
review_outcomes: Default::default(),
permission_denial_reasons: Default::default(),
not_ready_actions: Default::default(),
deferred_provider_preprocessing: Default::default(),
@@ -2214,6 +2216,15 @@ impl BlocklistAIActionModel {
pending_action: &AIAgentAction,
ctx: &mut ModelContext<Self>,
) {
if self
.review_reason(conversation_id, &pending_action.id)
.is_some()
{
self.review_outcomes.insert(
(conversation_id, pending_action.id.clone()),
risk_review::ReviewOutcome::denied_by_user(pending_action),
);
}
self.denied_permissions
.insert((conversation_id, pending_action.id.clone()));
#[cfg(not(target_family = "wasm"))]
@@ -2363,6 +2374,8 @@ impl BlocklistAIActionModel {
.retain(|(conv_id, _)| *conv_id != conversation_id);
self.action_reviews
.retain(|(conv_id, _), _| *conv_id != conversation_id);
self.review_outcomes
.retain(|(conv_id, _), _| *conv_id != conversation_id);
self.permission_denial_reasons
.retain(|(conv_id, _), _| *conv_id != conversation_id);
self.pending_actions.remove(&conversation_id);
@@ -84,6 +84,57 @@ enum ReviewDecision {
Ask { reason: String, deadline: Instant },
}
#[derive(Clone, Copy)]
pub(crate) enum ReviewDisplayState {
Assessing,
AwaitingApproval,
Approved,
Denied,
}
pub(crate) struct ReviewDisplay {
pub(crate) state: ReviewDisplayState,
pub(crate) summary: String,
pub(crate) detail: String,
}
pub(super) struct ReviewOutcome {
summary: String,
detail: String,
approved: bool,
}
impl ReviewOutcome {
pub(super) fn denied_by_user(action: &AIAgentAction) -> Self {
Self {
summary: review_summary(action),
detail: "Denied by you.".to_string(),
approved: false,
}
}
}
fn review_summary(action: &AIAgentAction) -> String {
let description = match &action.action {
AIAgentActionType::RequestCommandOutput { command, .. } => {
format!("Run: {command}")
}
_ => action.action.user_friendly_name(),
};
let summary = description.split_whitespace().collect::<Vec<_>>().join(" ");
shorten_for_review(&summary, 160)
}
fn shorten_for_review(text: &str, limit: usize) -> String {
let mut characters = text.chars();
let shortened = characters.by_ref().take(limit).collect::<String>();
if characters.next().is_some() {
format!("{shortened}…")
} else {
shortened
}
}
impl BlocklistAIActionModel {
fn review_environment(&self, ctx: &AppContext) -> ReviewEnvironment {
let session = self.active_session.as_ref(ctx);
@@ -267,6 +318,7 @@ impl BlocklistAIActionModel {
&& self.review_requires_human(action, conversation_id, ctx))
}) {
self.action_reviews.remove(&key);
self.review_outcomes.remove(&key);
}
if let Some(ActionReview {
decision: ReviewDecision::Ask { deadline, .. },
@@ -279,6 +331,20 @@ impl BlocklistAIActionModel {
}
}
if user_initiated || !self.uses_galaxy_decides(action, ctx) {
if user_initiated {
if let Some(review) = self.action_reviews.get(&key) {
if let ReviewDecision::Ask { reason, .. } = &review.decision {
self.review_outcomes.insert(
key.clone(),
ReviewOutcome {
summary: review_summary(action),
detail: format!("Approved by you. {reason}"),
approved: true,
},
);
}
}
}
self.action_reviews.remove(&key);
return Some(false);
}
@@ -289,6 +355,7 @@ impl BlocklistAIActionModel {
};
}
let token = uuid::Uuid::new_v4();
self.review_outcomes.remove(&key);
let (cancel, control) = turn_control();
let execution_ref = self.provider_tool_execution_ref(conversation_id, &action.id);
self.action_reviews.insert(
@@ -408,6 +475,18 @@ impl BlocklistAIActionModel {
.as_ref()
.is_ok_and(|assessment| assessment.allows_automatic_execution(policy_requires_human))
{
let assessment = result.as_ref().unwrap();
self.review_outcomes.insert(
key.clone(),
ReviewOutcome {
summary: review_summary(&action),
detail: format!(
"Approved by Galaxy · risk {}/5. {}",
assessment.risk, assessment.reason
),
approved: true,
},
);
self.action_reviews.get_mut(&key).unwrap().decision = ReviewDecision::Allow;
self.try_to_execute_available_actions(conversation_id, ctx);
return;
@@ -486,6 +565,43 @@ impl BlocklistAIActionModel {
}
}
pub(crate) fn review_display(
&self,
conversation_id: AIConversationId,
action_id: &AIAgentActionId,
) -> Option<ReviewDisplay> {
let key = (conversation_id, action_id.clone());
if let Some(review) = self.action_reviews.get(&key) {
let summary = review_summary(&review.action);
return Some(match &review.decision {
ReviewDecision::Assessing => ReviewDisplay {
state: ReviewDisplayState::Assessing,
summary,
detail: "Galaxy is assessing the permission risk…".to_string(),
},
ReviewDecision::Ask { reason, .. } => ReviewDisplay {
state: ReviewDisplayState::AwaitingApproval,
summary,
detail: shorten_for_review(reason, 320),
},
ReviewDecision::Allow => ReviewDisplay {
state: ReviewDisplayState::Approved,
summary,
detail: shorten_for_review(&self.review_outcomes.get(&key)?.detail, 320),
},
});
}
self.review_outcomes.get(&key).map(|outcome| ReviewDisplay {
state: if outcome.approved {
ReviewDisplayState::Approved
} else {
ReviewDisplayState::Denied
},
summary: outcome.summary.clone(),
detail: shorten_for_review(&outcome.detail, 320),
})
}
fn reject_reviewed_action(
&mut self,
action: &AIAgentAction,
@@ -507,6 +623,14 @@ impl BlocklistAIActionModel {
return;
};
let action = queue.remove(index).unwrap();
self.review_outcomes.insert(
key.clone(),
ReviewOutcome {
summary: review_summary(&action),
detail: "Denied automatically after 60 seconds without a response.".to_string(),
approved: false,
},
);
self.pending_permissions.remove(&key);
#[cfg(not(target_family = "wasm"))]
log_tool_event(
+1
View File
@@ -4918,6 +4918,7 @@ impl AIBlock {
}
BlocklistAIActionEvent::ActionBlockedOnUserConfirmation { .. } => {
ctx.emit(AIBlockEvent::ActionBlockedOnUserConfirmation);
ctx.notify();
}
BlocklistAIActionEvent::FinishedAction { action_id, .. } => {
me.abort_auto_expand_requested_command_timer();
+79 -18
View File
@@ -70,7 +70,7 @@ use crate::ai::agent::{
};
use crate::ai::agent_conversations_model::AgentConversationsModel;
use crate::ai::ambient_agents::AmbientAgentTaskId;
use crate::ai::blocklist::action_model::AIActionStatus;
use crate::ai::blocklist::action_model::{AIActionStatus, ReviewDisplay, ReviewDisplayState};
use crate::ai::blocklist::block::model::{AIBlockModel, AIBlockModelHelper, AIBlockOutputStatus};
use crate::ai::blocklist::block::view_impl::common::{
MaybeShimmeringText, BLOCKED_ACTION_MESSAGE_FOR_GREP_OR_FILE_GLOB,
@@ -319,23 +319,12 @@ pub(super) fn render(props: Props, app: &AppContext) -> Box<dyn Element> {
for output_message in output.messages.iter() {
if let AIAgentOutputMessageType::Action(action) = &output_message.message {
if let Some(reason) = props
if let Some(review) = props
.action_model
.as_ref(app)
.review_reason(props.conversation_id, &action.id)
.review_display(props.conversation_id, &action.id)
{
output_items.add_child(
Text::new(
reason,
appearance.ai_font_family(),
appearance.monospace_font_size(),
)
.with_color(blended_colors::text_main(
appearance.theme(),
appearance.theme().surface_1(),
))
.finish(),
);
output_items.add_child(render_review_status(review, app));
}
}
match &output_message.message {
@@ -1331,6 +1320,55 @@ fn render_streaming_tool_call(
render_tool_pane_shell(Clipped::new(header).finish(), false, false, false, app)
}
fn render_review_status(review: ReviewDisplay, app: &AppContext) -> Box<dyn Element> {
let appearance = Appearance::as_ref(app);
let icon = match review.state {
ReviewDisplayState::Assessing => Icon::Loading.to_galaxyui_icon(
galaxy_core::ui::theme::Fill::Solid(internal_colors::neutral_6(appearance.theme())),
),
ReviewDisplayState::AwaitingApproval => icons::yellow_stop_icon(appearance),
ReviewDisplayState::Approved => inline_action_icons::green_check_icon(appearance),
ReviewDisplayState::Denied => inline_action_icons::red_x_icon(appearance),
};
let title = match review.state {
ReviewDisplayState::Assessing => "Checking permission",
ReviewDisplayState::AwaitingApproval => "Approval required · 60 seconds",
ReviewDisplayState::Approved => "Approved",
ReviewDisplayState::Denied => "Denied",
};
let header = HeaderConfig::new(title, app)
.with_icon(icon)
.with_corner_radius_override(CornerRadius::with_all(Radius::Pixels(8.)))
.render(app);
let detail = format!("{}\n{}", review.summary, review.detail);
let body = Container::new(
Text::new(
detail,
appearance.ai_font_family(),
appearance.monospace_font_size(),
)
.with_color(blended_colors::text_main(
appearance.theme(),
appearance.theme().surface_1(),
))
.finish(),
)
.with_margin_left(16.)
.with_margin_right(16.)
.with_margin_bottom(12.)
.finish();
render_tool_pane_shell(
Flex::column()
.with_child(Clipped::new(header).finish())
.with_child(body)
.finish(),
matches!(review.state, ReviewDisplayState::AwaitingApproval),
false,
false,
app,
)
}
fn render_runtime_activity(
output_message: &AIAgentOutputMessage,
activity: &RuntimeActivity,
@@ -3084,9 +3122,32 @@ fn render_use_computer(
app: &AppContext,
) -> Box<dyn Element> {
let appearance = Appearance::handle(app).as_ref(app);
let mut renderable_action = RenderableAction::new(&request.action_summary, app)
.with_icon(action_icon(action_id, props.action_model, props.model, app).finish());
let status = props
.action_model
.as_ref(app)
.get_action_status(props.conversation_id, action_id);
let mut renderable_action = RenderableAction::new(&request.action_summary, app);
if status.as_ref().is_some_and(|status| status.is_blocked()) {
let buttons = props
.action_buttons
.get(action_id)
.expect("Button states must exist for each requested action.");
renderable_action = renderable_action
.with_header(blocked_action_header(
action_id.clone(),
"OK if I use computer control for this action?",
buttons.run_button.clone(),
buttons.cancel_button.clone(),
props.action_model,
props.model,
app,
))
.with_highlighted_border()
.with_background_color(appearance.theme().background().into_solid());
} else {
renderable_action = renderable_action
.with_icon(action_icon(action_id, props.action_model, props.model, app).finish());
}
// Add a "View screenshot" button if the action result contains a screenshot.
let has_screenshot = props
+98 -10
View File
@@ -87,6 +87,7 @@ use crate::ai::runtime::{
ProviderRunCoordinator, ProviderRunProfile, ProviderRunProjection,
ProviderRunResponseProjector, ProviderToolExecutionRef, ProviderToolLifecycleOutcome,
RuntimeResponseConfig, BASE_PROVIDER_PROFILE, CLI_MONITOR_PROVIDER_PROFILE,
COMPUTER_USE_PROVIDER_PROFILE,
};
use crate::ai::AIRequestUsageModel;
use crate::cloud_object::model::persistence::CloudModel;
@@ -1206,6 +1207,7 @@ struct ActiveProviderRunCheckpoint {
run: ProviderRun,
base_request: TurnRequest,
cli_monitor_request: Option<TurnRequest>,
computer_use_request: Option<TurnRequest>,
response_config: RuntimeResponseConfig,
action_context: ProviderActionContext,
persistence_offset: usize,
@@ -1229,10 +1231,15 @@ impl ActiveProviderRunCheckpoint {
.coordinator
.profile_request(CLI_MONITOR_PROVIDER_PROFILE)
.cloned();
let computer_use_request = run
.coordinator
.profile_request(COMPUTER_USE_PROVIDER_PROFILE)
.cloned();
Ok(Self {
run: run.coordinator.run().clone(),
base_request,
cli_monitor_request,
computer_use_request,
response_config: run.response_config.clone(),
action_context: run.action_context.clone(),
persistence_offset: run.persistence_offset,
@@ -1255,6 +1262,8 @@ struct ActiveProviderRunSnapshot {
run: ProviderRun,
base_request: TurnRequest,
cli_monitor_request: Option<TurnRequest>,
#[serde(default)]
computer_use_request: Option<TurnRequest>,
response_config: RuntimeResponseConfig,
action_context: ProviderActionContext,
projection_target: ProviderProjectionTarget,
@@ -1303,6 +1312,7 @@ impl ActiveProviderRunSnapshot {
run: checkpoint.run,
base_request: checkpoint.base_request,
cli_monitor_request: checkpoint.cli_monitor_request,
computer_use_request: checkpoint.computer_use_request,
response_config: checkpoint.response_config,
action_context: checkpoint.action_context,
projection_target: slot.projection_target.clone(),
@@ -1372,6 +1382,13 @@ impl ActiveProviderRunSnapshot {
"provider run uses the CLI profile without a persisted request".to_string(),
);
}
COMPUTER_USE_PROVIDER_PROFILE if self.computer_use_request.is_some() => {}
COMPUTER_USE_PROVIDER_PROFILE => {
return Err(
"provider run uses the computer-use profile without a persisted request"
.to_string(),
);
}
profile => {
return Err(format!(
"provider run uses unknown request profile '{profile}'"
@@ -2212,18 +2229,18 @@ fn provider_run_terminal_remote_log_record(
enum ProviderDriveMessage {
Projection {
lifecycle: Option<ProviderLlmLifecycle>,
lifecycle: Box<Option<ProviderLlmLifecycle>>,
latest_usage: Option<Usage>,
tool_call_progress: ProviderToolCallProgressUpdate,
events: Vec<warp_multi_agent_api::ResponseEvent>,
acknowledgement: oneshot::Sender<Result<(), String>>,
},
Checkpoint {
checkpoint: ActiveProviderRunCheckpoint,
checkpoint: Box<ActiveProviderRunCheckpoint>,
acknowledgement: oneshot::Sender<Result<(), String>>,
},
Blocked {
run: ActiveProviderRun,
run: Box<ActiveProviderRun>,
result: Result<ProviderRunBlock, String>,
},
}
@@ -5881,6 +5898,10 @@ impl BlocklistAIController {
.cli_monitor_request
.as_ref()
.map(|request| ResponseStream::resolve_provider_config(request.model.as_str(), ctx));
let computer_use_provider_config = snapshot
.computer_use_request
.as_ref()
.map(|request| ResponseStream::resolve_provider_config(request.model.as_str(), ctx));
let _ = ctx.spawn(
async move {
let base_runtime =
@@ -5910,6 +5931,23 @@ impl BlocklistAIController {
.with_max_context_tokens(cli_context_limit),
);
}
if let Some(computer_use_request) = snapshot.computer_use_request.as_ref() {
let provider_config = computer_use_provider_config.ok_or_else(|| {
anyhow!("restored computer-use request is missing its provider config")
})?;
let context_limit = provider_context_window_tokens(
&provider_config,
computer_use_request.model.as_str(),
base_context_limit,
);
let runtime =
provider_runtime_for_request(provider_config, computer_use_request).await?;
profiles.insert(
COMPUTER_USE_PROVIDER_PROFILE.to_string(),
ProviderRunProfile::new(runtime, computer_use_request.clone())
.with_max_context_tokens(context_limit),
);
}
Ok::<_, anyhow::Error>(PreparedRestoredProviderRun {
snapshot,
profiles,
@@ -6071,6 +6109,7 @@ impl BlocklistAIController {
run: provider_run,
base_request: _,
cli_monitor_request: _,
computer_use_request: _,
response_config,
action_context,
projection_target,
@@ -6533,11 +6572,16 @@ impl BlocklistAIController {
credit_multiplier: model.usage_metadata.credit_multiplier,
})
.collect();
let computer_use_provider_config = ResponseStream::resolve_provider_config(
request_params.computer_use_model.as_str(),
ctx,
);
let _ = ctx.spawn(
async move {
prepare_provider_run(
base_provider_config,
cli_provider_config,
computer_use_provider_config,
request_params,
orchestration_models,
)
@@ -6587,6 +6631,7 @@ impl BlocklistAIController {
let PreparedProviderRun {
base_profile,
cli_monitor_profile,
computer_use_profile,
tool_result_archive,
messages_sent,
persistence_offset,
@@ -6612,6 +6657,12 @@ impl BlocklistAIController {
return;
}
}
if let Some(profile) = computer_use_profile {
if let Err(error) = coordinator.insert_profile(COMPUTER_USE_PROVIDER_PROFILE, profile) {
self.fail_provider_startup(conversation_id, stream_id, error.to_string(), ctx);
return;
}
}
let cancellation_reason = self
.active_provider_runs
.get(&conversation_id)
@@ -7481,7 +7532,7 @@ impl BlocklistAIController {
let (acknowledgement, receiver) = oneshot::channel();
projection_sender
.send(ProviderDriveMessage::Projection {
lifecycle,
lifecycle: Box::new(lifecycle),
latest_usage,
tool_call_progress,
events,
@@ -7503,7 +7554,7 @@ impl BlocklistAIController {
let (acknowledgement, receiver) = oneshot::channel();
checkpoint_sender
.send(ProviderDriveMessage::Checkpoint {
checkpoint,
checkpoint: Box::new(checkpoint),
acknowledgement,
})
.await
@@ -7519,7 +7570,10 @@ impl BlocklistAIController {
.await
.map_err(|error| error.to_string());
let _ = sender
.send(ProviderDriveMessage::Blocked { run, result })
.send(ProviderDriveMessage::Blocked {
run: Box::new(run),
result,
})
.await;
},
|_, _, _| {},
@@ -7598,7 +7652,7 @@ impl BlocklistAIController {
}
};
let mut should_refresh_status = progress_changed;
if let Some(lifecycle) = lifecycle.as_ref() {
if let Some(lifecycle) = lifecycle.as_ref().as_ref() {
should_refresh_status |= {
let slot = self
.active_provider_runs
@@ -7648,7 +7702,7 @@ impl BlocklistAIController {
);
});
}
let model_turn_requested = lifecycle.as_ref().is_some_and(|lifecycle| {
let model_turn_requested = lifecycle.as_ref().as_ref().is_some_and(|lifecycle| {
matches!(lifecycle.phase, ProviderLlmLifecyclePhase::Requested)
});
let projection_succeeded = result.is_ok();
@@ -7681,7 +7735,7 @@ impl BlocklistAIController {
} => {
let result = match self.active_provider_runs.get_mut(&conversation_id) {
Some(slot) if checkpoint.run.id() == &slot.run_id => {
slot.checkpoint = Some(checkpoint);
slot.checkpoint = Some(*checkpoint);
self.persist_active_provider_run(conversation_id, ctx)
}
Some(_) => Err("provider checkpoint run identity did not match".to_string()),
@@ -7690,7 +7744,7 @@ impl BlocklistAIController {
let _ = acknowledgement.send(result);
}
ProviderDriveMessage::Blocked { run, result } => {
self.handle_provider_run_blocked(conversation_id, run, result, ctx);
self.handle_provider_run_blocked(conversation_id, *run, result, ctx);
}
}
}
@@ -8263,10 +8317,44 @@ impl BlocklistAIController {
) {
return;
}
// Screenshot results belong to the next model turn. Select its runtime only after
// the complete tool batch is committed, so a restart sees the same decision.
let next_tool_profile = match run.coordinator.run().state() {
ProviderRunState::AwaitingTools { batch } => {
let used_computer = batch.calls.iter().any(|pending| {
matches!(
pending.call.name.as_str(),
"request_computer_use" | "use_computer"
)
});
used_computer.then_some(COMPUTER_USE_PROVIDER_PROFILE)
}
_ => None,
};
let mut should_resume = false;
let should_drive = match run.coordinator.apply_tool_lifecycle(execution_ref, event) {
Ok(ProviderToolLifecycleOutcome::Pending) => false,
Ok(ProviderToolLifecycleOutcome::BatchCommitted) => {
if let Some(profile) = next_tool_profile.filter(|_| {
run.coordinator
.profile_request(COMPUTER_USE_PROVIDER_PROFILE)
.is_some()
}) {
if let Err(error) = run.coordinator.run_mut().set_ready_profile(profile) {
self.fail_active_provider_run(conversation_id, error.to_string(), ctx);
return;
}
} else if run.coordinator.run().profile().as_str() == COMPUTER_USE_PROVIDER_PROFILE
{
if let Err(error) = run
.coordinator
.run_mut()
.set_ready_profile(BASE_PROVIDER_PROFILE)
{
self.fail_active_provider_run(conversation_id, error.to_string(), ctx);
return;
}
}
let work_id = execution_ref.work_id();
should_resume = record_provider_batch_signal(
&mut slot.committed_provider_batch,
+3
View File
@@ -260,6 +260,7 @@ fn provider_snapshot(conversation_id: AIConversationId) -> super::ActiveProvider
),
base_request: TurnRequest::new("provider-model", messages),
cli_monitor_request: None,
computer_use_request: None,
response_config: crate::ai::runtime::RuntimeResponseConfig {
task_id: task_id.to_string(),
conversation_id: conversation_id.to_string(),
@@ -1042,6 +1043,7 @@ fn cancelling_provider_startup_keeps_slot_and_durable_cancellation() {
run: snapshot.run.clone(),
base_request: snapshot.base_request.clone(),
cli_monitor_request: snapshot.cli_monitor_request.clone(),
computer_use_request: snapshot.computer_use_request.clone(),
response_config: snapshot.response_config.clone(),
action_context: snapshot.action_context.clone(),
persistence_offset: snapshot.persistence_offset,
@@ -1157,6 +1159,7 @@ fn same_conversation_follow_up_waits_for_cancelled_provider_generation_cleanup()
run: old_snapshot.run.clone(),
base_request: old_snapshot.base_request.clone(),
cli_monitor_request: old_snapshot.cli_monitor_request.clone(),
computer_use_request: old_snapshot.computer_use_request.clone(),
response_config: old_snapshot.response_config.clone(),
action_context: old_snapshot.action_context.clone(),
persistence_offset: old_snapshot.persistence_offset,
@@ -1250,13 +1250,20 @@ impl RequestedCommandView {
self.block_model.as_ref().conversation(app).is_some_and(
|conversation| conversation.status().is_error(),
);
if is_errored {
AGENT_ERRORED_COMMAND_MESSAGE.into()
let (status, short_status) = if is_errored {
(AGENT_ERRORED_COMMAND_MESSAGE, "Agent needs help")
} else if *is_blocked {
AGENT_NEEDS_INPUT_MESSAGE.into()
(AGENT_NEEDS_INPUT_MESSAGE, "Needs input")
} else {
MONITORING_COMMAND_MESSAGE.into()
(MONITORING_COMMAND_MESSAGE, "Agent monitoring")
};
let command = self.get_header_title_text();
if command.trim().is_empty() {
status.into()
} else {
font_override = Some(appearance.monospace_font_family());
badge = Some(short_status.to_string());
command.into()
}
}
LongRunningCommandControlState::User { reason } => {
+4 -6
View File
@@ -705,7 +705,7 @@ impl BlocklistAIPermissions {
}
match self.get_read_files_setting(ctx, terminal_view_id) {
ActionPermission::AgentDecides | ActionPermission::Unknown => {
ActionPermission::Unknown => {
// For now, we always read files. We don't ask the user for permission.
FileReadPermission::Allowed(FileReadPermissionAllowedReason::AgentDecided)
}
@@ -838,9 +838,7 @@ impl BlocklistAIPermissions {
.is_some_and(|uid| self.get_mcp_denylist(ctx, terminal_view_id).contains(&uid));
match self.get_mcp_permissions_setting(ctx, terminal_view_id) {
ActionPermission::AgentDecides | ActionPermission::Unknown => {
allowlisted && !denylisted
}
ActionPermission::Unknown => allowlisted && !denylisted,
ActionPermission::AlwaysAllow => !denylisted,
ActionPermission::AlwaysAsk | ActionPermission::GalaxyDecides => {
allowlisted && !denylisted
@@ -855,7 +853,7 @@ impl BlocklistAIPermissions {
ctx: &AppContext,
) -> FileWritePermission {
match self.get_apply_code_diffs_setting(ctx, terminal_view_id) {
ActionPermission::AgentDecides | ActionPermission::Unknown => {
ActionPermission::Unknown => {
FileWritePermission::Denied(FileWritePermissionDeniedReason::AgentDecided)
}
ActionPermission::AlwaysAllow => FileWritePermission::Allowed(
@@ -915,7 +913,7 @@ impl BlocklistAIPermissions {
}
match self.get_execute_commands_setting(ctx, terminal_view_id) {
ActionPermission::AgentDecides | ActionPermission::Unknown => {
ActionPermission::Unknown => {
if FeatureFlag::AgentDecidesCommandExecution.is_enabled() && is_risky == Some(false)
{
return CommandExecutionPermission::Allowed(
+8 -8
View File
@@ -337,11 +337,11 @@ fn test_can_write_files() {
..
} = initialize_permissions_test(&mut app);
// Test AgentDecides setting
// Test GalaxyDecides setting before the independent review gate.
profile_model.update(&mut app, |model, ctx| {
model.set_apply_code_diffs(
*model.active_profile(Some(terminal_view_id), ctx).id(),
&ActionPermission::AgentDecides,
&ActionPermission::GalaxyDecides,
ctx,
);
});
@@ -352,9 +352,9 @@ fn test_can_write_files() {
assert!(
matches!(
result,
FileWritePermission::Denied(FileWritePermissionDeniedReason::AgentDecided)
FileWritePermission::Denied(FileWritePermissionDeniedReason::AlwaysAskEnabled)
),
"not allowed because AgentDecides right now just means ask"
"GalaxyDecides requires the independent action review"
);
});
@@ -1223,7 +1223,7 @@ fn test_can_use_mcp_server_always_ask_denylist_overrides_allowlist() {
}
#[test]
fn test_can_use_mcp_server_agent_decides() {
fn test_can_use_mcp_server_galaxy_decides() {
App::test((), |mut app| async move {
let PermissionsTestState {
convo_id,
@@ -1239,7 +1239,7 @@ fn test_can_use_mcp_server_agent_decides() {
profile_model.update(&mut app, |model, ctx| {
model.set_mcp_permissions(
*model.active_profile(Some(terminal_view_id), ctx).id(),
&ActionPermission::AgentDecides,
&ActionPermission::GalaxyDecides,
ctx,
);
model.add_to_mcp_allowlist(
@@ -1269,7 +1269,7 @@ fn test_can_use_mcp_server_agent_decides() {
}
#[test]
fn test_can_use_mcp_server_agent_decides_denylist_overrides_allowlist() {
fn test_can_use_mcp_server_galaxy_decides_denylist_overrides_allowlist() {
App::test((), |mut app| async move {
let PermissionsTestState {
convo_id,
@@ -1284,7 +1284,7 @@ fn test_can_use_mcp_server_agent_decides_denylist_overrides_allowlist() {
profile_model.update(&mut app, |model, ctx| {
model.set_mcp_permissions(
*model.active_profile(Some(terminal_view_id), ctx).id(),
&ActionPermission::AgentDecides,
&ActionPermission::GalaxyDecides,
ctx,
);
model.add_to_mcp_allowlist(