From 54af7210f4a00d1255aa620eaff48f2e929add23 Mon Sep 17 00:00:00 2001 From: Yan Lobau Date: Mon, 19 Jan 2026 17:08:57 +0200 Subject: [PATCH 1/2] docs(readme): note cargo requirement for build-fast --- README.md | 1 + 1 file changed, 1 insertion(+) diff --git a/README.md b/README.md index 9b0aaab18ad..380731d33fd 100644 --- a/README.md +++ b/README.md @@ -299,6 +299,7 @@ cd code npm install # Build (use fast build for development) +# Requires the Rust toolchain (cargo) installed on your machine ./build-fast.sh # Run locally From 9c3627d1c7f9d432a5272b59295be0af90724fdc Mon Sep 17 00:00:00 2001 From: Yan Lobau Date: Tue, 20 Jan 2026 11:04:05 +0200 Subject: [PATCH 2/2] feat(auto-drive): pause when input is required --- .../src/auto_coordinator.rs | 27 +++++++++ .../code-auto-drive-core/src/controller.rs | 42 +++++++++---- code-rs/core/prompt_coordinator.md | 1 + code-rs/exec/src/lib.rs | 1 + code-rs/tui/src/app/events.rs | 2 + code-rs/tui/src/app_event.rs | 1 + .../src/bottom_pane/auto_coordinator_view.rs | 13 +++- code-rs/tui/src/chatwidget.rs | 60 ++++++++++++++----- code-rs/tui/src/chatwidget/smoke_helpers.rs | 16 ++++- ...shot__auto_drive_input_required_waits.snap | 21 +++++++ .../tui/tests/vt100_chatwidget_snapshot.rs | 22 +++++++ 11 files changed, 180 insertions(+), 26 deletions(-) create mode 100644 code-rs/tui/tests/snapshots/vt100_chatwidget_snapshot__auto_drive_input_required_waits.snap diff --git a/code-rs/code-auto-drive-core/src/auto_coordinator.rs b/code-rs/code-auto-drive-core/src/auto_coordinator.rs index c79e3bb14e4..de5f5372dd8 100644 --- a/code-rs/code-auto-drive-core/src/auto_coordinator.rs +++ b/code-rs/code-auto-drive-core/src/auto_coordinator.rs @@ -214,6 +214,7 @@ pub enum AutoCoordinatorEvent { Decision { seq: u64, status: AutoCoordinatorStatus, + input_required: bool, status_title: Option, status_sent_to_user: Option, goal: Option, @@ -295,6 +296,7 @@ pub enum AutoCoordinatorCommand { struct PendingDecision { seq: u64, status: AutoCoordinatorStatus, + input_required: bool, status_title: Option, status_sent_to_user: Option, goal: Option, @@ -309,6 +311,7 @@ impl PendingDecision { AutoCoordinatorEvent::Decision { seq: self.seq, status: self.status, + input_required: self.input_required, status_title: self.status_title, status_sent_to_user: self.status_sent_to_user, goal: self.goal, @@ -471,6 +474,10 @@ mod tests { props.contains_key("prompt_sent_to_cli"), "prompt_sent_to_cli property missing" ); + assert!( + props.contains_key("input_required"), + "input_required property missing" + ); assert!(props.contains_key("agents"), "agents property missing"); assert!(!props.contains_key("code_review")); assert!(!props.contains_key("cross_check")); @@ -890,6 +897,8 @@ mod tests { struct CoordinatorDecisionNew { finish_status: String, #[serde(default)] + input_required: bool, + #[serde(default)] status_title: Option, #[serde(default)] status_sent_to_user: Option, @@ -978,6 +987,7 @@ struct CoordinatorDecisionLegacy { struct ParsedCoordinatorDecision { status: AutoCoordinatorStatus, + input_required: bool, status_title: Option, status_sent_to_user: Option, cli: Option, @@ -1209,6 +1219,7 @@ fn run_auto_loop( let event = AutoCoordinatorEvent::Decision { seq: decision_seq, status: AutoCoordinatorStatus::Continue, + input_required: false, status_title: Some(seed.status_title.clone()), status_sent_to_user: Some(seed.status_sent_to_user.clone()), goal: Some(goal_text.clone()), @@ -1305,6 +1316,7 @@ fn run_auto_loop( ) { Ok(ParsedCoordinatorDecision { status, + input_required, status_title, status_sent_to_user, goal, @@ -1340,6 +1352,7 @@ fn run_auto_loop( let event = AutoCoordinatorEvent::Decision { seq: current_seq, status: AutoCoordinatorStatus::Failed, + input_required: false, status_title: Some("Turn limit reached".to_string()), status_sent_to_user: Some(format!( "Stopped after {coordinator_turns_seen} coordinator turns (cap={coordinator_turn_cap}) to prevent a runaway session." @@ -1373,6 +1386,7 @@ fn run_auto_loop( let event = AutoCoordinatorEvent::Decision { seq: current_seq, status, + input_required, status_title: status_title.clone(), status_sent_to_user: status_sent_to_user.clone(), goal: goal.clone(), @@ -1397,6 +1411,7 @@ fn run_auto_loop( let decision_event = PendingDecision { seq: current_seq, status, + input_required, status_title, status_sent_to_user, goal: goal.clone(), @@ -1520,6 +1535,7 @@ fn run_auto_loop( let event = AutoCoordinatorEvent::Decision { seq: current_seq, status: AutoCoordinatorStatus::Failed, + input_required: false, status_title: Some("Coordinator error".to_string()), status_sent_to_user: Some(format!("Encountered an error: {error}")), goal: None, @@ -1857,6 +1873,14 @@ fn build_schema(active_agents: &[String], features: SchemaFeatures) -> Value { ); required.push(Value::String("status_sent_to_user".to_string())); + properties.insert( + "input_required".to_string(), + json!({ + "type": "boolean", + "description": "Set true only when the CLI must pause for mandatory user input (credentials, permissions, missing info). When true, Auto Drive will wait indefinitely for the user response instead of auto-continuing." + }), + ); + // NOTE: We intentionally omit `maxLength` here. Some providers truncate // responses to satisfy schema length caps, which would hide overlong // prompts. We validate length after parsing and treat >600 as recoverable @@ -2899,6 +2923,7 @@ fn convert_decision_new( ) -> Result { let CoordinatorDecisionNew { finish_status: _, + input_required, status_title, status_sent_to_user, progress, @@ -2988,6 +3013,7 @@ fn convert_decision_new( Ok(ParsedCoordinatorDecision { status, + input_required, status_title, status_sent_to_user, cli, @@ -3037,6 +3063,7 @@ fn convert_decision_legacy( Ok(ParsedCoordinatorDecision { status, + input_required: false, status_title, status_sent_to_user, cli, diff --git a/code-rs/code-auto-drive-core/src/controller.rs b/code-rs/code-auto-drive-core/src/controller.rs index 90a7e391404..b66be23f2d0 100644 --- a/code-rs/code-auto-drive-core/src/controller.rs +++ b/code-rs/code-auto-drive-core/src/controller.rs @@ -301,6 +301,7 @@ pub struct AutoDriveController { pub countdown_decision_seq: u64, pub seconds_remaining: u8, pub countdown_override: Option, + pub input_required: bool, pub last_broadcast_summary: Option, pub last_decision_summary: Option, pub last_decision_status_sent_to_user: Option, @@ -369,6 +370,7 @@ impl AutoDriveController { pub fn on_prompt_submitted(&mut self) { self.countdown_override = None; + self.input_required = false; self.apply_phase(AutoRunPhase::AwaitingDiagnostics { coordinator_waiting: true }); } @@ -660,10 +662,12 @@ impl AutoDriveController { decision_seq: u64, prompt_text: String, countdown_override: Option, + input_required: bool, ) -> Vec { self.current_cli_prompt = Some(prompt_text); self.apply_phase(AutoRunPhase::AwaitingCoordinator { prompt_ready: true }); self.countdown_override = countdown_override; + self.input_required = input_required; self.reset_countdown(); self.countdown_id = self.countdown_id.wrapping_add(1); self.countdown_decision_seq = decision_seq; @@ -672,12 +676,14 @@ impl AutoDriveController { self.seconds_remaining = countdown.unwrap_or(0); let mut effects = vec![AutoControllerEffect::RefreshUi]; - if let Some(seconds) = countdown { - effects.push(AutoControllerEffect::StartCountdown { - countdown_id, - decision_seq, - seconds, - }); + if !self.input_required { + if let Some(seconds) = countdown { + effects.push(AutoControllerEffect::StartCountdown { + countdown_id, + decision_seq, + seconds, + }); + } } effects } @@ -689,7 +695,10 @@ impl AutoDriveController { self.seconds_remaining = self.countdown_seconds().unwrap_or(0); let mut effects = vec![AutoControllerEffect::RefreshUi]; - if self.phase.awaiting_coordinator_submit() && !self.phase.is_paused_manual() { + if self.phase.awaiting_coordinator_submit() + && !self.phase.is_paused_manual() + && !self.input_required + { self.countdown_id = self.countdown_id.wrapping_add(1); let countdown_id = self.countdown_id; let decision_seq = self.countdown_decision_seq; @@ -791,6 +800,7 @@ impl AutoDriveController { pub fn countdown_active(&self) -> bool { self.phase.awaiting_coordinator_submit() && !self.phase.is_paused_manual() + && !self.input_required && self .countdown_seconds() .map(|seconds| seconds > 0) @@ -988,7 +998,8 @@ mod tests { fn countdown_tick_respects_decision_seq() { let mut controller = AutoDriveController::default(); - let _effects = controller.schedule_cli_prompt(1, "Test prompt".to_string(), None); + let _effects = + controller.schedule_cli_prompt(1, "Test prompt".to_string(), None, false); let countdown_id = controller.countdown_id; let effects = controller.handle_countdown_tick(countdown_id, 1, 5); @@ -1002,7 +1013,7 @@ mod tests { fn countdown_tick_final_emits_submit() { let mut controller = AutoDriveController::default(); - let _effects = controller.schedule_cli_prompt(7, "Prompt".to_string(), None); + let _effects = controller.schedule_cli_prompt(7, "Prompt".to_string(), None, false); let countdown_id = controller.countdown_id; let effects = controller.handle_countdown_tick(countdown_id, 7, 0); @@ -1013,11 +1024,22 @@ mod tests { #[test] fn countdown_tick_ignores_stopped_phase() { let mut controller = AutoDriveController::default(); - let _effects = controller.schedule_cli_prompt(3, "Prompt".to_string(), None); + let _effects = controller.schedule_cli_prompt(3, "Prompt".to_string(), None, false); let countdown_id = controller.countdown_id; controller.set_phase(AutoRunPhase::Idle); let effects = controller.handle_countdown_tick(countdown_id, 3, 5); assert!(effects.is_empty()); } + + #[test] + fn input_required_skips_countdown_start() { + let mut controller = AutoDriveController::default(); + let effects = controller.schedule_cli_prompt(1, "Prompt".to_string(), None, true); + + assert!(!controller.countdown_active()); + assert!(!effects + .iter() + .any(|effect| matches!(effect, AutoControllerEffect::StartCountdown { .. }))); + } } diff --git a/code-rs/core/prompt_coordinator.md b/code-rs/core/prompt_coordinator.md index 1f5e039f2b5..90a497d6d8b 100644 --- a/code-rs/core/prompt_coordinator.md +++ b/code-rs/core/prompt_coordinator.md @@ -32,6 +32,7 @@ Every turn you must reply with a single JSON object matching the coordinator sch | `finish_status` | Required string: `"continue"`, `"finish_success"`, or `"finish_failed"`. Should almost always be `"continue"`. | | `status_title` | Required string (1–4 words). Present-tense headline describing what you asked the CLI to work on. | | `status_sent_to_user` | Required string (1–2 sentences). Present-tense message shown to the user explaining what you've asked the CLI to do. | +| `input_required` | Optional boolean. Set to `true` only when the CLI must pause for mandatory user input (credentials, permissions, missing info). When true, Auto Drive waits indefinitely for the user response. | | `prompt_sent_to_cli` | Required string (4–600 chars). The single atomic instruction for the CLI when `finish_status` is `"continue"`. Set to `null` only when finishing. | | `agents` | Optional object with `timing` (`"parallel"` or `"blocking"`) and `list` (≤4 agent entries). Each entry requires `prompt` (8–400 chars), optional `context` (≤1500 chars), `write` (bool), and optional `models` (array of preferred models). | | `goal` | Optional (≤200 chars). Used only if bootstrapping a derived mission goal is required. | diff --git a/code-rs/exec/src/lib.rs b/code-rs/exec/src/lib.rs index 4a958cd5b8c..ccb7e536538 100644 --- a/code-rs/exec/src/lib.rs +++ b/code-rs/exec/src/lib.rs @@ -1449,6 +1449,7 @@ async fn run_auto_drive_session( AutoCoordinatorEvent::Decision { seq, status, + input_required: _, status_title, status_sent_to_user, goal: maybe_goal, diff --git a/code-rs/tui/src/app/events.rs b/code-rs/tui/src/app/events.rs index d3532626b8c..b1b1431357c 100644 --- a/code-rs/tui/src/app/events.rs +++ b/code-rs/tui/src/app/events.rs @@ -899,6 +899,7 @@ impl App<'_> { AppEvent::AutoCoordinatorDecision { seq, status, + input_required, status_title, status_sent_to_user, goal, @@ -911,6 +912,7 @@ impl App<'_> { widget.auto_handle_decision( seq, status, + input_required, status_title, status_sent_to_user, goal, diff --git a/code-rs/tui/src/app_event.rs b/code-rs/tui/src/app_event.rs index 9acce8a6004..697544aa2cf 100644 --- a/code-rs/tui/src/app_event.rs +++ b/code-rs/tui/src/app_event.rs @@ -178,6 +178,7 @@ pub(crate) enum AppEvent { AutoCoordinatorDecision { seq: u64, status: AutoCoordinatorStatus, + input_required: bool, status_title: Option, status_sent_to_user: Option, goal: Option, diff --git a/code-rs/tui/src/bottom_pane/auto_coordinator_view.rs b/code-rs/tui/src/bottom_pane/auto_coordinator_view.rs index 9d10776cec1..29880a15cc7 100644 --- a/code-rs/tui/src/bottom_pane/auto_coordinator_view.rs +++ b/code-rs/tui/src/bottom_pane/auto_coordinator_view.rs @@ -43,6 +43,7 @@ pub(crate) struct AutoActiveViewModel { pub show_composer: bool, pub editing_prompt: bool, pub awaiting_submission: bool, + pub input_required: bool, pub waiting_for_response: bool, pub coordinator_waiting: bool, pub waiting_for_review: bool, @@ -311,7 +312,11 @@ impl AutoCoordinatorView { let mut style = self.style.frame.clone(); if self.style.variant == AutoDriveVariant::Beacon { if let Some(accent) = style.accent.as_mut() { - accent.style = if model.awaiting_submission { + accent.style = if model.input_required { + Style::default() + .fg(colors::warning()) + .add_modifier(Modifier::BOLD) + } else if model.awaiting_submission { Style::default() .fg(colors::warning()) .add_modifier(Modifier::BOLD) @@ -346,6 +351,8 @@ impl AutoCoordinatorView { fn status_label(model: &AutoActiveViewModel) -> &'static str { if model.waiting_for_review { "Awaiting review" + } else if model.input_required { + "Input required" } else if model.awaiting_submission { "Waiting" } else if model.coordinator_waiting { @@ -385,6 +392,10 @@ impl AutoCoordinatorView { } } + if model.input_required { + return "Input required".to_string(); + } + if model.awaiting_submission { if let Some(countdown) = &model.countdown { return format!("Awaiting confirmation ({}s)", countdown.remaining); diff --git a/code-rs/tui/src/chatwidget.rs b/code-rs/tui/src/chatwidget.rs index e30242a7c32..94c48bbe265 100644 --- a/code-rs/tui/src/chatwidget.rs +++ b/code-rs/tui/src/chatwidget.rs @@ -17731,6 +17731,7 @@ fi\n\ cli_context: None, show_composer: true, awaiting_submission: false, + input_required: false, waiting_for_response: false, coordinator_waiting: false, waiting_for_review: false, @@ -17821,6 +17822,7 @@ fi\n\ AutoCoordinatorEvent::Decision { seq, status, + input_required, status_title, status_sent_to_user, goal, @@ -17832,6 +17834,7 @@ fi\n\ app_event_tx.send(AppEvent::AutoCoordinatorDecision { seq, status, + input_required, status_title, status_sent_to_user, goal, @@ -18240,6 +18243,7 @@ fi\n\ &mut self, seq: u64, status: AutoCoordinatorStatus, + input_required: bool, status_title: Option, status_sent_to_user: Option, goal: Option, @@ -18267,6 +18271,8 @@ fi\n\ let status_title = Self::normalize_status_field(status_title); let status_sent_to_user = Self::normalize_status_field(status_sent_to_user); + self.auto_state.input_required = input_required && matches!(status, AutoCoordinatorStatus::Continue); + self.auto_state.turns_completed = self.auto_state.turns_completed.saturating_add(1); if !transcript.is_empty() { @@ -18390,7 +18396,7 @@ fi\n\ )); } } else { - self.schedule_auto_cli_prompt(seq, prompt_text); + self.schedule_auto_cli_prompt(seq, prompt_text, input_required); } } AutoCoordinatorStatus::Success => { @@ -18432,7 +18438,7 @@ Have we met every part of this goal and is there no further work to do?"# "Auto Drive Diagnostics: Validating progress".to_string(), AutoDriveActionKind::Info, ); - self.schedule_auto_cli_prompt(seq, prompt_text); + self.schedule_auto_cli_prompt(seq, prompt_text, false); self.auto_submit_prompt(); return; } @@ -18536,8 +18542,18 @@ Have we met every part of this goal and is there no further work to do?"# self.request_redraw(); } - fn schedule_auto_cli_prompt(&mut self, decision_seq: u64, prompt_text: String) { - self.schedule_auto_cli_prompt_with_override(decision_seq, prompt_text, None); + fn schedule_auto_cli_prompt( + &mut self, + decision_seq: u64, + prompt_text: String, + input_required: bool, + ) { + self.schedule_auto_cli_prompt_with_override( + decision_seq, + prompt_text, + None, + input_required, + ); } fn schedule_auto_cli_prompt_with_override( @@ -18545,11 +18561,12 @@ Have we met every part of this goal and is there no further work to do?"# decision_seq: u64, prompt_text: String, countdown_override: Option, + input_required: bool, ) { self.auto_state.suppress_next_cli_display = false; let effects = self .auto_state - .schedule_cli_prompt(decision_seq, prompt_text, countdown_override); + .schedule_cli_prompt(decision_seq, prompt_text, countdown_override, input_required); self.auto_apply_controller_effects(effects); } @@ -19173,7 +19190,7 @@ Have we met every part of this goal and is there no further work to do?"# } else { None }; - self.schedule_auto_cli_prompt_with_override(0, String::new(), override_seconds); + self.schedule_auto_cli_prompt_with_override(0, String::new(), override_seconds, false); true } @@ -19597,8 +19614,9 @@ Have we met every part of this goal and is there no further work to do?"# status_lines, cli_prompt: None, cli_context: None, - show_composer: true, + show_composer: true, awaiting_submission: false, + input_required: false, waiting_for_response: false, coordinator_waiting: false, waiting_for_review: false, @@ -19647,8 +19665,11 @@ Have we met every part of this goal and is there no further work to do?"# self.bottom_pane.clear_live_ring(); + let input_required = self.auto_state.input_required; let status_text = if self.auto_state.awaiting_review() { "waiting for code review...".to_string() + } else if input_required { + "Input required".to_string() } else if let Some(line) = self .auto_state .current_display_line @@ -19746,7 +19767,7 @@ Have we met every part of this goal and is there no further work to do?"# let countdown_limit = self.auto_state.countdown_seconds(); let countdown_active = self.auto_state.countdown_active(); - let countdown = if self.auto_state.awaiting_coordinator_submit() { + let countdown = if self.auto_state.awaiting_coordinator_submit() && !input_required { match countdown_limit { Some(limit) if limit > 0 => Some(CountdownState { remaining: self.auto_state.seconds_remaining.min(limit), @@ -19757,7 +19778,7 @@ Have we met every part of this goal and is there no further work to do?"# None }; - let button = if self.auto_state.awaiting_coordinator_submit() { + let button = if self.auto_state.awaiting_coordinator_submit() && !input_required { let has_cli_prompt = cli_prompt.is_some(); let base_label = if bootstrap_pending { "Complete Current Task" @@ -19782,7 +19803,9 @@ Have we met every part of this goal and is there no further work to do?"# }; let manual_hint = if self.auto_state.awaiting_coordinator_submit() { - if self.auto_state.is_paused_manual() { + if input_required { + Some("Type your response and press Enter.".to_string()) + } else if self.auto_state.is_paused_manual() { Some("Edit the prompt, then press Enter to continue.".to_string()) } else if bootstrap_pending { None @@ -19803,7 +19826,9 @@ Have we met every part of this goal and is there no further work to do?"# let ctrl_switch_hint = if self.auto_state.awaiting_coordinator_submit() { let has_cli_prompt = cli_prompt.is_some(); - if self.auto_state.is_paused_manual() { + if input_required { + "Esc to stop".to_string() + } else if self.auto_state.is_paused_manual() { "Esc to cancel".to_string() } else if bootstrap_pending { "Esc enter new goal".to_string() @@ -19820,14 +19845,16 @@ Have we met every part of this goal and is there no further work to do?"# String::new() }; - let show_composer = - !self.auto_state.awaiting_coordinator_submit() || self.auto_state.is_paused_manual(); + let show_composer = input_required + || !self.auto_state.awaiting_coordinator_submit() + || self.auto_state.is_paused_manual(); let model = AutoCoordinatorViewModel::Active(AutoActiveViewModel { goal: self.auto_state.goal.clone(), status_lines, cli_prompt, awaiting_submission: self.auto_state.awaiting_coordinator_submit(), + input_required, waiting_for_response: self.auto_state.is_waiting_for_response(), coordinator_waiting: self.auto_state.is_coordinator_waiting(), waiting_for_review: self.auto_state.awaiting_review(), @@ -19865,6 +19892,7 @@ Have we met every part of this goal and is there no further work to do?"# self.auto_state.is_active() && self.auto_state.awaiting_coordinator_submit() && !self.auto_state.is_paused_manual() + && !self.auto_state.input_required && self.config.auto_drive.coordinator_routing && self.auto_state.continue_mode != AutoContinueMode::Manual } @@ -30403,7 +30431,7 @@ use code_core::protocol::OrderMeta; chat.auto_state.continue_mode = AutoContinueMode::Immediate; chat.auto_state.goal = Some("Ship feature".to_string()); chat.auto_state.set_phase(AutoRunPhase::Active); - chat.schedule_auto_cli_prompt(0, "echo ready".to_string()); + chat.schedule_auto_cli_prompt(0, "echo ready".to_string(), false); }); let (button_label, countdown_override, ctrl_switch_hint, manual_hint_present) = @@ -30566,6 +30594,7 @@ use code_core::protocol::OrderMeta; chat.auto_handle_decision( 1, AutoCoordinatorStatus::Continue, + false, None, None, Some("Finish migrations".to_string()), @@ -30602,6 +30631,7 @@ use code_core::protocol::OrderMeta; chat.auto_handle_decision( 2, AutoCoordinatorStatus::Continue, + false, None, None, Some("Document release tasks".to_string()), @@ -30689,6 +30719,7 @@ use code_core::protocol::OrderMeta; chat.auto_handle_decision( 3, AutoCoordinatorStatus::Continue, + false, Some("Drafting fix".to_string()), Some("Past work".to_string()), None, @@ -31401,6 +31432,7 @@ use code_core::protocol::OrderMeta; chat.auto_handle_decision( 4, AutoCoordinatorStatus::Continue, + false, Some("Running unit tests".to_string()), Some("Finished setup".to_string()), Some("Refine goal".to_string()), diff --git a/code-rs/tui/src/chatwidget/smoke_helpers.rs b/code-rs/tui/src/chatwidget/smoke_helpers.rs index 9d34c51a295..e7dbd5df6a4 100644 --- a/code-rs/tui/src/chatwidget/smoke_helpers.rs +++ b/code-rs/tui/src/chatwidget/smoke_helpers.rs @@ -613,6 +613,7 @@ impl ChatWidgetHarness { chat.auto_state.set_waiting_for_response(false); chat.auto_state.set_coordinator_waiting(false); chat.auto_state.on_complete_review(); + chat.auto_state.input_required = false; chat.auto_state.current_cli_prompt = Some(cli_prompt.into()); chat.auto_state.current_display_line = Some(headline.into()); chat.auto_state.current_display_is_summary = summary.is_some(); @@ -653,7 +654,10 @@ impl ChatWidgetHarness { let countdown = chat.auto_state.countdown_seconds(); chat.auto_state.countdown_id = chat.auto_state.countdown_id.wrapping_add(1); chat.auto_state.seconds_remaining = countdown.unwrap_or(0); - if chat.auto_state.awaiting_coordinator_submit() && !chat.auto_state.is_paused_manual() { + if chat.auto_state.awaiting_coordinator_submit() + && !chat.auto_state.is_paused_manual() + && !chat.auto_state.input_required + { if let Some(seconds) = countdown { chat.auto_spawn_countdown( chat.auto_state.countdown_id, @@ -686,6 +690,16 @@ impl ChatWidgetHarness { self.flush_into_widget(); } + pub fn auto_drive_set_input_required(&mut self, input_required: bool) { + { + let chat = self.chat(); + chat.auto_state.input_required = input_required; + chat.auto_rebuild_live_ring(); + chat.request_redraw(); + } + self.flush_into_widget(); + } + pub fn auto_drive_set_waiting_for_review(&mut self, summary: Option) { { let chat = self.chat(); diff --git a/code-rs/tui/tests/snapshots/vt100_chatwidget_snapshot__auto_drive_input_required_waits.snap b/code-rs/tui/tests/snapshots/vt100_chatwidget_snapshot__auto_drive_input_required_waits.snap new file mode 100644 index 00000000000..eb1e7e74f9a --- /dev/null +++ b/code-rs/tui/tests/snapshots/vt100_chatwidget_snapshot__auto_drive_input_required_waits.snap @@ -0,0 +1,21 @@ +--- +source: tui/tests/vt100_chatwidget_snapshot.rs +expression: frame +--- + +----------------------------------------------------------------------------+ + | Every Code | + +----------------------------------------------------------------------------+ + + • I can help with various tasks including: + + - Writing code + - Reading files + - Running commands + + + + Auto Drive > Input required ✶ Input required (Xs) + +----------------------------------------------------------------------------+ + | What can I code for you today? | + +----------------------------------------------------------------------------+ + • Agents Enabled • Diagnostics Enabled API key diff --git a/code-rs/tui/tests/vt100_chatwidget_snapshot.rs b/code-rs/tui/tests/vt100_chatwidget_snapshot.rs index cb039d24288..2eaffa1ffdd 100644 --- a/code-rs/tui/tests/vt100_chatwidget_snapshot.rs +++ b/code-rs/tui/tests/vt100_chatwidget_snapshot.rs @@ -938,6 +938,28 @@ fn auto_drive_manual_mode_waits() { insta::assert_snapshot!("auto_drive_manual_mode_waits", frame); } +#[test] +fn auto_drive_input_required_waits() { + let mut harness = ChatWidgetHarness::new(); + + harness.auto_drive_activate( + "Input required for auth", + true, + true, + AutoContinueModeFixture::TenSeconds, + ); + harness.auto_drive_set_awaiting_submission( + "request credentials", + "Auto Drive needs credentials to continue", + Some("Waiting for required user input.".to_string()), + ); + harness.auto_drive_set_input_required(true); + + let frame = normalize_output(render_chat_widget_to_vt100(&mut harness, 80, 18)); + + insta::assert_snapshot!("auto_drive_input_required_waits", frame); +} + #[test] fn auto_drive_review_resume_returns_to_running() { let mut harness = ChatWidgetHarness::new();