diff --git a/crates/ide-agent/src/config.rs b/crates/ide-agent/src/config.rs index 9b0b52dcf..14b1d02ff 100644 --- a/crates/ide-agent/src/config.rs +++ b/crates/ide-agent/src/config.rs @@ -37,6 +37,16 @@ pub(crate) fn bypass_approvals(provider: &str, config: &[AgentConfigOption]) -> provider == "cursor" && config.iter().any(|o| o.id == CURSOR_APPROVAL && o.current.as_deref() == Some("bypass")) } +/// The mode that approves everything without asking (`bypassPermissions`, `yolo`, `full-access`…), if the agent has one. +pub(crate) fn bypass_mode(options: &[AgentConfigOption]) -> Option<(&AgentConfigOption, &AgentConfigValue)> { + let option = options.iter().find(|o| o.category.as_deref() == Some("mode"))?; + let value = option.values.iter().find(|v| { + let m: String = v.value.to_lowercase().chars().filter(char::is_ascii_lowercase).collect(); + m.contains("bypass") || m == "yolo" || m.contains("fullaccess") || m == "dangerous" + })?; + Some((option, value)) +} + /// The settings in a `session/new` or `session/load` result: its select `configOptions`, /// plus the older `modes` / `models` fields for what those don't cover. pub(crate) fn options_of(result: &Value) -> Vec { diff --git a/crates/ide-agent/src/lib.rs b/crates/ide-agent/src/lib.rs index 233c461ba..84f9c48e8 100644 --- a/crates/ide-agent/src/lib.rs +++ b/crates/ide-agent/src/lib.rs @@ -117,9 +117,13 @@ impl Shared { fn known_config(&self, provider: &str) -> Vec { let mut config = self.known.lock().get(provider).map(|k| k.config.clone()).unwrap_or_default(); config::add_client_options(provider, &mut config); - // Approval bypass belongs to the conversation, never to another new task. + if let Some((o, v)) = config::bypass_mode(&config).map(|(o, v)| (o.id.clone(), v.value.clone())) + && let Some(o) = config.iter_mut().find(|c| c.id == o) + { + o.current = Some(v); + } if let Some(o) = config.iter_mut().find(|o| o.id == config::CURSOR_APPROVAL) { - o.current = Some("ask".into()); + o.current = Some("bypass".into()); } config } diff --git a/crates/ide-agent/src/tests.rs b/crates/ide-agent/src/tests.rs index c769e42c0..af27b307b 100644 --- a/crates/ide-agent/src/tests.rs +++ b/crates/ide-agent/src/tests.rs @@ -793,13 +793,13 @@ async fn cursor_approval_switch_resolves_pending_and_survives_restart() { mgr.prompt(&task.id, text("Fix again"), false).await.unwrap(); wait_for(&mgr, &task.id, AgentStatus::Ready).await; assert!(!agent.calls.lock().iter().any(|c| c.contains("bypass"))); - assert!(!config::bypass_approvals("cursor", &mgr.shared.known_config("cursor"))); + assert!(config::bypass_approvals("cursor", &mgr.shared.known_config("cursor"))); mgr.set_config(&task.id, config::CURSOR_APPROVAL, "ask".into()).await.unwrap(); mgr.prompt(&task.id, text("Fix once more"), false).await.unwrap(); wait_for(&mgr, &task.id, AgentStatus::NeedsInput).await; mgr.cancel(&task.id, None).unwrap(); wait_for(&mgr, &task.id, AgentStatus::Ready).await; - // An unrelated new conversation still asks, even after the cache was updated. + // A task created without choosing approvals still asks; the New Task page sends the default. let other = mgr.create(new()).await.unwrap(); wait_for(&mgr, &other.id, AgentStatus::NeedsInput).await; mgr.cancel(&other.id, None).unwrap(); diff --git a/src/features/agents/composerModel.ts b/src/features/agents/composerModel.ts index c88f70dde..885644344 100644 --- a/src/features/agents/composerModel.ts +++ b/src/features/agents/composerModel.ts @@ -149,6 +149,17 @@ export function modeIcon(mode: string): string { return "lock"; } +/** The settings that approve everything without asking (a bypass mode, or approvals), by option id. */ +export function bypassDefaults(config: readonly AgentConfigOption[]): Record { + const out: Record = {}; + const mode = modeOption(config); + const unlock = mode?.values.find((v) => modeIcon(v.value) === "unlock"); + if (mode && unlock) out[mode.id] = unlock.value; + const approval = config.find((o) => o.category === "approval"); + if (approval?.values.some((v) => v.value === "bypass")) out[approval.id] = "bypass"; + return out; +} + /** Shift+Tab: the mode after the current one. */ export function nextValue(option: AgentConfigOption, current = option.current): string | null { if (!option.values.length) return null; diff --git a/src/features/agents/store.ts b/src/features/agents/store.ts index 6ceba2b87..ef33d1d2c 100644 --- a/src/features/agents/store.ts +++ b/src/features/agents/store.ts @@ -13,7 +13,7 @@ import type { } from "@/bindings"; import { call, commands, isTauri } from "@/platform/ipc"; import { editorService, notifications } from "@/workbench/contributions"; -import { canSend, emptyDraft, toPrompt, type ComposerDraft, type FileRef } from "./composerModel"; +import { bypassDefaults, canSend, emptyDraft, toPrompt, type ComposerDraft, type FileRef } from "./composerModel"; import { withQuote } from "./conversation"; import { HOME_KEY, @@ -348,14 +348,15 @@ export function openHome(prompt?: string) { /** `isolation` overrides the draft's (a project without git can only use the local checkout). */ export async function startTask(isolation: Isolation = useAgents.getState().draft.isolation): Promise { - const { draft } = useAgents.getState(); + const { draft, providers } = useAgents.getState(); const composer = composerDraft(HOME); if (!canSend(composer)) return; + const config = { ...bypassDefaults(providers.find((p) => p.id === draft.provider)?.config ?? []), ...draft.config }; const task = await call( commands.agentCreate({ prompt: toPrompt(composer), provider: draft.provider, - config: Object.entries(draft.config).map(([id, value]) => ({ id, value })), + config: Object.entries(config).map(([id, value]) => ({ id, value })), isolation, base: isolation === "worktree" ? draft.base : null, }),