diff --git a/crates/maestro-cli/src/sessions.rs b/crates/maestro-cli/src/sessions.rs index f98d9eb..31ecff1 100644 --- a/crates/maestro-cli/src/sessions.rs +++ b/crates/maestro-cli/src/sessions.rs @@ -57,6 +57,7 @@ pub async fn run_spawn(client: &DaemonClient, args: SpawnArgs) -> Result Vec { id: "claude".into(), label: "Claude".into(), command: "claude".into(), + auto_approve: false, }, SpawnPreset { id: "opencode".into(), label: "opencode".into(), command: "opencode".into(), + auto_approve: false, }, SpawnPreset { id: "zsh".into(), label: "Shell".into(), command: "zsh".into(), + auto_approve: false, }, ] } @@ -334,6 +337,7 @@ pub fn custom_preset() -> SpawnPreset { id: CUSTOM_PRESET_ID.into(), label: "Custom…".into(), command: String::new(), + auto_approve: false, } } @@ -515,6 +519,7 @@ auto_install = false id: "fish".into(), label: "fish".into(), command: "fish".into(), + auto_approve: false, }]), ..Default::default() }; @@ -551,6 +556,7 @@ auto_install = false id: " ".into(), label: "x".into(), command: "x".into(), + auto_approve: false, }]), ..Default::default() }; @@ -561,6 +567,7 @@ auto_install = false id: "x".into(), label: "x".into(), command: "".into(), + auto_approve: false, }]), ..Default::default() }; @@ -578,11 +585,13 @@ auto_install = false id: "a".into(), label: "a".into(), command: "a".into(), + auto_approve: false, }, SpawnPreset { id: "a".into(), label: "a2".into(), command: "a2".into(), + auto_approve: false, }, ]), ..Default::default() @@ -597,6 +606,7 @@ auto_install = false id: "custom".into(), label: "Custom".into(), command: "whatever".into(), + auto_approve: false, }]), ..Default::default() }; diff --git a/crates/maestro-daemon/src/session.rs b/crates/maestro-daemon/src/session.rs index d377af3..fb5b716 100644 --- a/crates/maestro-daemon/src/session.rs +++ b/crates/maestro-daemon/src/session.rs @@ -122,6 +122,9 @@ pub struct SessionRecord { /// Completion signal for the task draining ACP notifications into the /// event log. Shutdown waits for it before returning. acp_event_pump: Option>, + /// When `true`, the permission pump auto-responds `AllowAlways` to every + /// ACP permission request without surfacing it to clients. + pub auto_approve: bool, } impl SessionRecord { @@ -1414,7 +1417,7 @@ impl SessionManager { let mut sessions = self.inner.sessions.lock().await; let event_pump = event_pump.map(|(acp, events)| { let done = start_acp_event_pump(&self.inner, id, &acp, events.clone()); - start_acp_permission_pump(&self.inner, id, &acp, events); + start_acp_permission_pump(&self.inner, id, &acp, events, req.auto_approve); done }); sessions.insert( @@ -1435,6 +1438,7 @@ impl SessionManager { acp_events, structured_activity_watermark: None, acp_event_pump: event_pump, + auto_approve: req.auto_approve, }, ); } @@ -1647,6 +1651,7 @@ impl SessionManager { acp_events: None, structured_activity_watermark: None, acp_event_pump: None, + auto_approve: false, }, ); let _ = self @@ -1824,11 +1829,14 @@ impl SessionManager { acp_events, structured_activity_watermark: None, acp_event_pump: None, + // Recovery cannot know the original preset's flag; the + // operator re-enables it by restarting with the preset. + auto_approve: false, }, ); if let Some((acp, events)) = event_pump { let done = start_acp_event_pump(&self.inner, id, &acp, events.clone()); - start_acp_permission_pump(&self.inner, id, &acp, events); + start_acp_permission_pump(&self.inner, id, &acp, events, false); if let Some(record) = self.inner.sessions.lock().await.get_mut(&id) { record.acp_event_pump = Some(done); } @@ -2327,6 +2335,7 @@ fn start_acp_permission_pump( id: Uuid, acp: &Arc, events: Arc>, + auto_approve: bool, ) { let Some(mut requests) = acp.take_permission_requests() else { return; @@ -2340,6 +2349,12 @@ fn start_acp_permission_pump( response, }) = requests.recv().await { + if auto_approve { + let outcome = + permission_outcome(ApprovalDecision::AllowForSession, &request.options); + let _ = response.send(outcome); + continue; + } let mut events = events.lock().await; if events.projection.active_turn() != Some(turn) { let _ = response.send(maestro_acp::schema::RequestPermissionOutcome::Cancelled); @@ -3060,6 +3075,7 @@ mod tests { env: std::collections::HashMap::new(), metadata: std::collections::HashMap::new(), worktree: None, + auto_approve: false, } } diff --git a/crates/maestro-daemon/tests/acp.rs b/crates/maestro-daemon/tests/acp.rs index 552a6a1..ab4fcec 100644 --- a/crates/maestro-daemon/tests/acp.rs +++ b/crates/maestro-daemon/tests/acp.rs @@ -112,6 +112,7 @@ fn structured_command( env, metadata: HashMap::new(), worktree: None, + auto_approve: false, } } @@ -705,6 +706,7 @@ async fn shutdown_preserves_a_pristine_structured_worktree_for_recovery() { branch: branch.into(), create_branch: true, }), + auto_approve: false, }) .await .expect("create"); @@ -1187,6 +1189,7 @@ async fn cancelling_a_structured_handshake_rolls_back_and_settles_admission() { branch: "cancelled-create".into(), create_branch: true, }), + auto_approve: false, }; let create = { diff --git a/crates/maestro-daemon/tests/activity_report.rs b/crates/maestro-daemon/tests/activity_report.rs index dda80fa..1c99d6e 100644 --- a/crates/maestro-daemon/tests/activity_report.rs +++ b/crates/maestro-daemon/tests/activity_report.rs @@ -48,6 +48,7 @@ async fn spawn_sleeping(manager: &SessionManager) -> Uuid { env: Default::default(), metadata: Default::default(), worktree: None, + auto_approve: false, }) .await .unwrap() diff --git a/crates/maestro-daemon/tests/attach.rs b/crates/maestro-daemon/tests/attach.rs index 1648613..000abfc 100644 --- a/crates/maestro-daemon/tests/attach.rs +++ b/crates/maestro-daemon/tests/attach.rs @@ -81,6 +81,7 @@ async fn create_bash_session(h: &Harness) -> Uuid { env: HashMap::new(), metadata: HashMap::new(), worktree: None, + auto_approve: false, }) .await .unwrap(); diff --git a/crates/maestro-daemon/tests/autoname.rs b/crates/maestro-daemon/tests/autoname.rs index 491ff5b..d49c89d 100644 --- a/crates/maestro-daemon/tests/autoname.rs +++ b/crates/maestro-daemon/tests/autoname.rs @@ -56,6 +56,7 @@ async fn spawn(manager: &SessionManager, name: Option<&str>) -> Uuid { env: Default::default(), metadata, worktree: None, + auto_approve: false, }) .await .unwrap() diff --git a/crates/maestro-daemon/tests/blocked_work_cost.rs b/crates/maestro-daemon/tests/blocked_work_cost.rs index 35ce151..05bee0a 100644 --- a/crates/maestro-daemon/tests/blocked_work_cost.rs +++ b/crates/maestro-daemon/tests/blocked_work_cost.rs @@ -29,6 +29,7 @@ fn request(cwd: &std::path::Path, repo: &std::path::Path, branch: &str) -> Creat branch: branch.into(), create_branch: true, }), + auto_approve: false, } } diff --git a/crates/maestro-daemon/tests/local_api.rs b/crates/maestro-daemon/tests/local_api.rs index d2aa08f..aaad617 100644 --- a/crates/maestro-daemon/tests/local_api.rs +++ b/crates/maestro-daemon/tests/local_api.rs @@ -162,6 +162,7 @@ async fn structured_session_with_env(h: &Harness, env: HashMap) env, metadata: HashMap::new(), worktree: None, + auto_approve: false, }) .await .unwrap() @@ -493,6 +494,7 @@ async fn structured_prompt_rejects_unknown_and_unavailable_sessions() { env: HashMap::new(), metadata: HashMap::new(), worktree: None, + auto_approve: false, }) .await .unwrap(); @@ -626,6 +628,7 @@ async fn structured_replay_rejects_unknown_terminal_and_dead_sessions() { env: HashMap::new(), metadata: HashMap::new(), worktree: None, + auto_approve: false, }) .await .unwrap(); @@ -768,6 +771,7 @@ async fn list_sessions_starts_empty_and_reflects_creates() { env: HashMap::new(), metadata: HashMap::new(), worktree: None, + auto_approve: false, }; let created: Session = reqwest::Client::new() .post(url(h.addr, "/api/sessions")) @@ -985,6 +989,7 @@ async fn patch_session_renames_it() { env: HashMap::new(), metadata: HashMap::new(), worktree: None, + auto_approve: false, }) .await .unwrap(); @@ -1028,6 +1033,7 @@ async fn post_focus_stamps_last_focused_at() { env: HashMap::new(), metadata: HashMap::new(), worktree: None, + auto_approve: false, }) .await .unwrap(); @@ -1062,6 +1068,7 @@ async fn delete_owned_session_kills_it() { env: HashMap::new(), metadata: HashMap::new(), worktree: None, + auto_approve: false, }) .await .unwrap(); @@ -1084,6 +1091,7 @@ async fn spawn_via_manager(h: &Harness) -> Uuid { env: HashMap::new(), metadata: HashMap::new(), worktree: None, + auto_approve: false, }) .await .unwrap() @@ -1198,6 +1206,7 @@ async fn delete_a_session_whose_worktree_holds_unsaved_work_returns_409_with_the branch: "feat/guarded".into(), create_branch: true, }), + auto_approve: false, }) .await .unwrap(); @@ -1251,6 +1260,7 @@ async fn delete_with_force_ends_the_session_and_keeps_the_worktree() { branch: "feat/guarded".into(), create_branch: true, }), + auto_approve: false, }) .await .unwrap(); @@ -1281,6 +1291,7 @@ async fn attach_ws_streams_pty_bytes() { env: HashMap::new(), metadata: HashMap::new(), worktree: None, + auto_approve: false, }) .await .unwrap(); @@ -1383,6 +1394,7 @@ async fn events_ws_sends_a_snapshot_then_streams_deltas() { env: HashMap::new(), metadata: HashMap::new(), worktree: None, + auto_approve: false, }) .await .expect("create"); @@ -1495,6 +1507,7 @@ async fn every_events_subscriber_gets_its_own_snapshot() { env: HashMap::new(), metadata: HashMap::new(), worktree: None, + auto_approve: false, }) .await .expect("create"); diff --git a/crates/maestro-daemon/tests/rename.rs b/crates/maestro-daemon/tests/rename.rs index 6a6cae1..8f6e129 100644 --- a/crates/maestro-daemon/tests/rename.rs +++ b/crates/maestro-daemon/tests/rename.rs @@ -57,6 +57,7 @@ async fn spawn_named(manager: &SessionManager, name: Option<&str>) -> Uuid { env: Default::default(), metadata, worktree: None, + auto_approve: false, }) .await .unwrap() diff --git a/crates/maestro-daemon/tests/session_decisions.rs b/crates/maestro-daemon/tests/session_decisions.rs index d7e2d20..8d699fe 100644 --- a/crates/maestro-daemon/tests/session_decisions.rs +++ b/crates/maestro-daemon/tests/session_decisions.rs @@ -70,6 +70,7 @@ impl Fx { env: HashMap::new(), metadata: HashMap::new(), worktree: None, + auto_approve: false, } } } diff --git a/crates/maestro-daemon/tests/session_manager.rs b/crates/maestro-daemon/tests/session_manager.rs index 51cf0fc..28a81b0 100644 --- a/crates/maestro-daemon/tests/session_manager.rs +++ b/crates/maestro-daemon/tests/session_manager.rs @@ -28,6 +28,7 @@ async fn create_then_kill_round_trip() { env: HashMap::new(), metadata: HashMap::new(), worktree: None, + auto_approve: false, }) .await .expect("create"); @@ -70,6 +71,7 @@ async fn create_appends_to_spawn_history() { env: HashMap::new(), metadata: HashMap::from([("name".into(), serde_json::json!("neon-drifter"))]), worktree: None, + auto_approve: false, }) .await .expect("create"); @@ -100,6 +102,7 @@ async fn create_without_history_logs_nothing() { env: HashMap::new(), metadata: HashMap::new(), worktree: None, + auto_approve: false, }) .await .expect("create"); @@ -123,6 +126,7 @@ async fn recover_adopts_existing_tmux_sessions() { env: HashMap::new(), metadata: HashMap::new(), worktree: None, + auto_approve: false, }) .await .expect("create"); @@ -166,6 +170,7 @@ async fn create_captures_initial_output_in_replay_buffer() { env: HashMap::new(), metadata: HashMap::new(), worktree: None, + auto_approve: false, }) .await .expect("create"); @@ -215,6 +220,7 @@ async fn attach_replay_is_the_current_screen_not_the_byte_tail() { env: HashMap::new(), metadata: HashMap::new(), worktree: None, + auto_approve: false, }) .await .expect("create"); @@ -273,6 +279,7 @@ async fn attach_replay_carries_the_panes_mouse_modes() { env: HashMap::new(), metadata: HashMap::new(), worktree: None, + auto_approve: false, }) .await .expect("create"); @@ -324,6 +331,7 @@ async fn recover_re_pipes_pane_so_output_reaches_subscribers() { env: HashMap::new(), metadata: HashMap::new(), worktree: None, + auto_approve: false, }) .await .expect("create"); @@ -387,6 +395,7 @@ async fn pty_broadcast_delivers_bytes_to_subscribers() { env: HashMap::new(), metadata: HashMap::new(), worktree: None, + auto_approve: false, }) .await .unwrap(); @@ -438,6 +447,7 @@ async fn liveness_watcher_marks_dead_sessions_and_keeps_them_until_dismissed() { env: HashMap::new(), metadata: HashMap::new(), worktree: None, + auto_approve: false, }) .await .expect("create"); @@ -508,6 +518,7 @@ async fn watcher_refreshes_the_blocked_work_indicator() { branch: "feat/indicator".into(), create_branch: true, }), + auto_approve: false, }) .await .expect("create"); @@ -601,6 +612,7 @@ async fn kill_removes_worktree_even_when_spec_says_keep() { branch: "feat/keep".into(), create_branch: true, }), + auto_approve: false, }) .await .expect("create"); @@ -639,6 +651,7 @@ async fn killing_refuses_a_worktree_with_unsaved_work_and_keeps_the_session() { branch: "feat/guarded".into(), create_branch: true, }), + auto_approve: false, }) .await .expect("create"); @@ -703,6 +716,7 @@ async fn killing_refuses_until_the_branch_is_pushed() { branch: "feat/pushed".into(), create_branch: true, }), + auto_approve: false, }) .await .expect("create"); @@ -753,6 +767,7 @@ async fn force_killing_a_guarded_worktree_leaves_it_on_disk() { branch: "feat/forced".into(), create_branch: true, }), + auto_approve: false, }) .await .expect("create"); @@ -799,6 +814,7 @@ async fn force_on_a_clear_worktree_removes_it_as_usual() { branch: "feat/clear".into(), create_branch: true, }), + auto_approve: false, }) .await .expect("create"); @@ -840,6 +856,7 @@ async fn killing_an_ignored_only_worktree_succeeds() { branch: "feat/rusty".into(), create_branch: true, }), + auto_approve: false, }) .await .expect("create"); @@ -882,6 +899,7 @@ async fn natural_exit_of_a_guarded_worktree_leaves_it_on_disk() { branch: "feat/exited".into(), create_branch: true, }), + auto_approve: false, }) .await .expect("create"); @@ -944,6 +962,7 @@ async fn natural_exit_of_a_pristine_worktree_removes_it() { branch: "feat/clean".into(), create_branch: true, }), + auto_approve: false, }) .await .expect("create"); @@ -1001,6 +1020,7 @@ async fn liveness_watcher_leaves_running_sessions_alone() { env: HashMap::new(), metadata: HashMap::new(), worktree: None, + auto_approve: false, }) .await .expect("create"); @@ -1040,6 +1060,7 @@ async fn worktree_base_repo_records_the_main_checkout_not_the_request() { branch: "feat/one".into(), create_branch: true, }), + auto_approve: false, }) .await .expect("create first"); @@ -1058,6 +1079,7 @@ async fn worktree_base_repo_records_the_main_checkout_not_the_request() { branch: "feat/two".into(), create_branch: true, }), + auto_approve: false, }) .await .expect("create second"); @@ -1093,6 +1115,7 @@ async fn spawned_sessions_export_maestro_session_id_and_url() { env: HashMap::new(), metadata: HashMap::new(), worktree: None, + auto_approve: false, }) .await .expect("create"); @@ -1132,6 +1155,7 @@ async fn a_freshly_created_session_has_never_been_focused() { env: HashMap::new(), metadata: HashMap::new(), worktree: None, + auto_approve: false, }) .await .expect("create"); @@ -1158,6 +1182,7 @@ async fn set_focused_true_stamps_last_focused_at() { env: HashMap::new(), metadata: HashMap::new(), worktree: None, + auto_approve: false, }) .await .expect("create"); @@ -1207,6 +1232,7 @@ async fn attaching_stamps_last_focused_at() { env: HashMap::new(), metadata: HashMap::new(), worktree: None, + auto_approve: false, }) .await .expect("create"); diff --git a/crates/maestro-e2e/tests/e2e.rs b/crates/maestro-e2e/tests/e2e.rs index d1406f5..755eeba 100644 --- a/crates/maestro-e2e/tests/e2e.rs +++ b/crates/maestro-e2e/tests/e2e.rs @@ -33,6 +33,7 @@ fn create_req(cwd: &std::path::Path, command: &str) -> CreateSessionRequest { env: HashMap::new(), metadata: HashMap::new(), worktree: None, + auto_approve: false, } } @@ -111,6 +112,7 @@ fn worktree_req(base: &std::path::Path, branch: &str, create_branch: bool) -> Cr env: HashMap::new(), metadata: HashMap::new(), worktree: Some(worktree_spec(base, branch, create_branch)), + auto_approve: false, } } @@ -206,6 +208,7 @@ async fn kill_removes_clean_worktree() { env: HashMap::new(), metadata: HashMap::new(), worktree: Some(worktree_spec(base.path(), "feature/e2e", true)), + auto_approve: false, }; let s = client.spawn_session(&req).await.expect("spawn worktree"); @@ -260,6 +263,7 @@ async fn kill_refuses_worktree_with_uncommitted_changes() { env: HashMap::new(), metadata: HashMap::new(), worktree: Some(worktree_spec(base.path(), "feature/unsaved", true)), + auto_approve: false, }; let s = client.spawn_session(&req).await.expect("spawn worktree"); @@ -319,6 +323,7 @@ async fn spawn_on_existing_branch_checks_it_out() { env: HashMap::new(), metadata: HashMap::new(), worktree: Some(worktree_spec(base.path(), "feature/existing", false)), + auto_approve: false, }; let s = client @@ -827,6 +832,7 @@ async fn natural_exit_settles_dirty_and_pristine_worktrees_differently() { env: HashMap::new(), metadata: HashMap::new(), worktree: Some(worktree_spec(dirty_repo.path(), "feature/messy", true)), + auto_approve: false, }) .await .expect("spawn dirty"); @@ -876,6 +882,7 @@ fn create_req_cmd(cwd: &std::path::Path, command: &str, branch: &str) -> CreateS env: HashMap::new(), metadata: HashMap::new(), worktree: Some(worktree_spec(cwd, branch, true)), + auto_approve: false, } } @@ -1068,6 +1075,7 @@ async fn sigterm_stops_structured_child_before_events_ws_drains() { env: fake.env.clone(), metadata: HashMap::new(), worktree: None, + auto_approve: false, }) .await .expect("spawn structured session"); diff --git a/crates/maestro-protocol/src/lib.rs b/crates/maestro-protocol/src/lib.rs index 03cd125..e64dda5 100644 --- a/crates/maestro-protocol/src/lib.rs +++ b/crates/maestro-protocol/src/lib.rs @@ -383,6 +383,10 @@ pub struct CreateSessionRequest { /// untouched. #[serde(default)] pub view: SessionView, + /// When `true`, the daemon auto-approves every ACP permission request + /// for this structured session. + #[serde(default)] + pub auto_approve: bool, } /// Result of `GET /api/dirs`: the immediate subdirectories of one directory. @@ -522,6 +526,11 @@ pub struct SpawnPreset { pub id: String, pub label: String, pub command: String, + /// When `true`, the daemon auto-approves every ACP permission request + /// for structured sessions spawned from this preset instead of surfacing + /// them to the client. + #[serde(default)] + pub auto_approve: bool, } /// Build provenance of the running daemon: the commit it was compiled @@ -1251,6 +1260,7 @@ mod tests { create_branch: true, }), view: SessionView::Terminal, + auto_approve: false, }; assert_eq!(req, round_trip(&req)); } diff --git a/crates/maestro-tui/src/app.rs b/crates/maestro-tui/src/app.rs index 09ac938..4825a65 100644 --- a/crates/maestro-tui/src/app.rs +++ b/crates/maestro-tui/src/app.rs @@ -3009,6 +3009,7 @@ mod tests { env: Default::default(), metadata: Default::default(), worktree: None, + auto_approve: false, }; app.submit_spawn(req).await; @@ -3057,6 +3058,7 @@ mod tests { env: Default::default(), metadata: Default::default(), worktree: None, + auto_approve: false, }; let structured_id = Uuid::from_u128(188); diff --git a/crates/maestro-tui/src/draft.rs b/crates/maestro-tui/src/draft.rs index b78b275..7ace0ff 100644 --- a/crates/maestro-tui/src/draft.rs +++ b/crates/maestro-tui/src/draft.rs @@ -718,6 +718,10 @@ impl DraftPane { ); } let worktree = self.build_worktree()?; + let auto_approve = self + .presets + .get(self.selected_preset) + .is_some_and(|p| p.auto_approve); Ok(CreateSessionRequest { view: if self.structured_view && self.supports_structured_view() { SessionView::Structured @@ -729,6 +733,7 @@ impl DraftPane { env: Default::default(), metadata, worktree, + auto_approve, }) } @@ -1776,6 +1781,7 @@ mod tests { id: "aider".into(), label: "Aider".into(), command: "aider".into(), + auto_approve: false, }]); assert_eq!( draft.command(), @@ -1798,11 +1804,13 @@ mod tests { id: "claude".into(), label: "Claude".into(), command: "claude".into(), + auto_approve: false, }, SpawnPreset { id: "aider".into(), label: "Aider".into(), command: "aider".into(), + auto_approve: false, }, ]); assert_eq!(draft.command(), "aider"); @@ -1822,6 +1830,7 @@ mod tests { id: "claude".into(), label: "Claude".into(), command: "claude".into(), + auto_approve: false, }]); assert!(draft.is_custom()); assert_eq!(draft.command(), "bash -l"); @@ -1898,16 +1907,19 @@ mod tests { id: "claude".into(), label: "Claude".into(), command: "claude".into(), + auto_approve: false, }, SpawnPreset { id: "codex".into(), label: "Codex".into(), command: "codex".into(), + auto_approve: false, }, SpawnPreset { id: "opencode".into(), label: "OpenCode".into(), command: "opencode".into(), + auto_approve: false, }, ]); assert!(!draft.structured_view); @@ -2046,16 +2058,19 @@ mod tests { id: "claude-wrapped".into(), label: "Claude (npx)".into(), command: "npx claude-code --resume".into(), + auto_approve: false, }, SpawnPreset { id: "codex-wrapped".into(), label: "Codex (npx)".into(), command: "npx codex exec".into(), + auto_approve: false, }, SpawnPreset { id: "aider".into(), label: "Aider".into(), command: "aider".into(), + auto_approve: false, }, ]); diff --git a/crates/maestro-tui/src/fast_forward.rs b/crates/maestro-tui/src/fast_forward.rs index cd87800..21e5916 100644 --- a/crates/maestro-tui/src/fast_forward.rs +++ b/crates/maestro-tui/src/fast_forward.rs @@ -121,6 +121,7 @@ impl FastForwardDefaults { branch: handle.to_string(), create_branch: true, }), + auto_approve: false, }) } @@ -184,6 +185,7 @@ mod tests { id: id.into(), label: id.into(), command: command.into(), + auto_approve: false, } } diff --git a/web/src/spawnForm.test.ts b/web/src/spawnForm.test.ts index 412a4c0..cd4974e 100644 --- a/web/src/spawnForm.test.ts +++ b/web/src/spawnForm.test.ts @@ -7,6 +7,7 @@ const base: SpawnFormValues = { cwd: "/home/user/project", name: "amber-fox", structuredView: false, + autoApprove: false, worktreeEnabled: false, worktreeBranch: "", worktreeBranchMode: "new", diff --git a/web/src/spawnForm.ts b/web/src/spawnForm.ts index 24dd35f..2db0698 100644 --- a/web/src/spawnForm.ts +++ b/web/src/spawnForm.ts @@ -49,6 +49,7 @@ export interface SpawnFormValues { cwd: string; name: string; structuredView: boolean; + autoApprove: boolean; worktreeEnabled: boolean; worktreeBranch: string; worktreeBranchEdited: boolean; @@ -79,6 +80,10 @@ export function buildSpawnRequest( req.view = "Structured"; } + if (values.autoApprove) { + req.auto_approve = true; + } + if (values.worktreeEnabled) { const mode = values.worktreeBranchMode ?? "new"; const branch = values.worktreeBranch.trim(); @@ -261,6 +266,8 @@ export function createSpawnForm(opts: SpawnFormOptions): SpawnFormHandle { let ready = false; let destroyed = false; const branchValue = () => (branchMode === "existing" ? branchSelect.value : branchInput.value); + const selectedAutoApprove = () => + presets.find((p) => p.id === selectedPresetId)?.auto_approve ?? false; const emit = () => { if (!ready || destroyed) return; opts.onChange?.({ @@ -268,6 +275,7 @@ export function createSpawnForm(opts: SpawnFormOptions): SpawnFormHandle { cwd, name: nameInput.value, structuredView: structuredView.checked, + autoApprove: selectedAutoApprove(), worktreeEnabled: wtEnabled.checked, worktreeBranch: branchValue(), worktreeBranchEdited: branchEdited, @@ -917,6 +925,7 @@ export function createSpawnForm(opts: SpawnFormOptions): SpawnFormHandle { cwd, name: nameInput.value, structuredView: structuredView.checked, + autoApprove: selectedAutoApprove(), worktreeEnabled: wtEnabled.checked, worktreeBranch: branchValue(), worktreeBranchEdited: branchEdited, diff --git a/web/src/types.ts b/web/src/types.ts index 2a10938..eb944ba 100644 --- a/web/src/types.ts +++ b/web/src/types.ts @@ -91,6 +91,7 @@ export interface SpawnPreset { id: string; label: string; command: string; + auto_approve?: boolean; } /** `GET /api/info` — the daemon's identity plus its spawn presets (#26). @@ -277,6 +278,7 @@ export interface CreateSessionRequest { metadata?: Record; worktree?: WorktreeSpec; view?: SessionView; + auto_approve?: boolean; } export type CatalogEvent =