fix: restore goal entry and manual resume

This commit is contained in:
James Peter
2025-10-24 16:10:17 +10:00
parent 28cdc19222
commit c8563bb591
5 changed files with 107 additions and 9 deletions
+22
View File
@@ -0,0 +1,22 @@
## Overview
- Stabilize Auto Drive state transitions so manual overrides no longer resume automation unexpectedly.
- Ensure Esc reliably cancels running agents before exiting Auto Drive.
## Key changes
- Add `AutoRunPhase` to the controller with helpers (`set_phase`, `is_auto_active`, `current_phase`, `set/clear/should_bypass_coordinator_next_submit`) and a one-shot bypass flag.
- Update ChatWidget: PausedManual sets bypass; manual submit clears bypass and leaves Auto Idle unless `/auto` explicitly restarts it; all coordinator/resume paths are gated on `!should_bypass`; `/auto` with no goal resets to Idle.
- Esc routing now cancels agents on the first press and stops Auto Drive on the second.
- Test harness uses `auto_spawn_countdown` / `auto_handle_countdown`; the core crate re‑exports `TurnComplexity`.
- Added regression tests for PausedManual manual submit, post-`auto_stop` submit, and Esc cancel→exit flows.
## Tests
- `./build-fast.sh`
- `cargo test -p code-tui` (warnings only)
## Risk / Rollback
- Low risk; changes are scoped to Auto Drive plumbing and are test covered. Roll back via `git revert` if manual control regresses.
## Verification
- Manual: start Auto Drive in the TUI, press Esc once (agents cancel) then Esc again (Auto Drive exits).
- Manual: pause Auto Drive for manual edit, submit the edited prompt — automation remains Idle until `/auto` is issued.
- Automated: `cargo test -p code-tui -- auto_drive`
@@ -79,6 +79,12 @@ impl AutoRunPhase {
}
}
impl Default for AutoRunPhase {
fn default() -> Self {
Self::Idle
}
}
#[derive(Debug, Clone)]
pub struct AutoRunSummary {
pub duration: Duration,
@@ -209,6 +215,7 @@ pub struct AutoDriveController {
pub intro_pending: bool,
pub elapsed_override: Option<Duration>,
pub phase: AutoRunPhase,
pub bypass_coordinator_next_submit: bool,
}
impl AutoDriveController {
@@ -238,6 +245,7 @@ impl AutoDriveController {
self.goal = Some(goal);
self.awaiting_goal_input = false;
self.phase = AutoRunPhase::Launching;
self.bypass_coordinator_next_submit = false;
}
pub fn launch_succeeded(
@@ -269,6 +277,7 @@ impl AutoDriveController {
self.coordinator_waiting = true;
self.reset_countdown();
self.phase = AutoRunPhase::Active;
self.bypass_coordinator_next_submit = false;
vec![
AutoControllerEffect::LaunchStarted { goal },
@@ -283,6 +292,7 @@ impl AutoDriveController {
self.mark_intro_pending();
self.reset_countdown();
self.phase = AutoRunPhase::Idle;
self.bypass_coordinator_next_submit = false;
vec![
AutoControllerEffect::LaunchFailed { goal, error },
@@ -311,6 +321,7 @@ impl AutoDriveController {
self.last_run_summary = Some(summary.clone());
self.awaiting_goal_input = true;
self.phase = AutoRunPhase::Idle;
self.bypass_coordinator_next_submit = false;
vec![
AutoControllerEffect::CancelCoordinator,
@@ -343,6 +354,7 @@ impl AutoDriveController {
self.pending_agent_actions.clear();
self.pending_agent_timing = None;
self.phase = AutoRunPhase::TransientRecovery;
self.bypass_coordinator_next_submit = false;
if pending_attempt > AUTO_RESTART_MAX_ATTEMPTS {
self.transient_restart_attempts = pending_attempt;
@@ -506,6 +518,9 @@ impl AutoDriveController {
} else {
AutoRunPhase::Idle
};
if self.phase != AutoRunPhase::PausedManual {
self.bypass_coordinator_next_submit = false;
}
}
pub fn reset_intro_timing(&mut self) {
@@ -550,6 +565,33 @@ impl AutoDriveController {
self.seconds_remaining = self.countdown_seconds().unwrap_or(0);
}
pub fn set_phase(&mut self, phase: AutoRunPhase) {
self.phase = phase;
if phase != AutoRunPhase::PausedManual {
self.bypass_coordinator_next_submit = false;
}
}
pub fn is_auto_active(&self) -> bool {
self.phase.is_active()
}
pub fn current_phase(&self) -> AutoRunPhase {
self.phase
}
pub fn set_bypass_coordinator_next_submit(&mut self) {
self.bypass_coordinator_next_submit = true;
}
pub fn should_bypass_coordinator_next_submit(&self) -> bool {
self.bypass_coordinator_next_submit
}
pub fn clear_bypass_coordinator_flag(&mut self) {
self.bypass_coordinator_next_submit = false;
}
fn auto_restart_delay(attempt: u32) -> Duration {
if attempt == 0 {
return AUTO_RESTART_BASE_DELAY.min(AUTO_RESTART_MAX_DELAY);
+2
View File
@@ -18,6 +18,7 @@ pub use auto_coordinator::{
AutoTurnAgentsAction,
AutoTurnAgentsTiming,
AutoTurnCliAction,
TurnComplexity,
TurnConfig,
TurnDescriptor,
TurnMode,
@@ -28,6 +29,7 @@ pub use controller::{
AutoContinueMode,
AutoControllerEffect,
AutoDriveController,
AutoRunPhase,
AutoResolvePhase,
AutoResolveState,
AutoRestartState,
+36 -8
View File
@@ -96,6 +96,7 @@ use code_auto_drive_core::{
AutoCoordinatorStatus,
AutoDriveHistory,
AutoDriveController,
AutoRunPhase,
AutoControllerEffect,
AutoTurnAgentsAction,
AutoTurnAgentsTiming,
@@ -4759,7 +4760,9 @@ impl ChatWidget<'_> {
match key_event.code {
crossterm::event::KeyCode::Enter
| crossterm::event::KeyCode::Char(' ') if key_event.modifiers.is_empty() => {
self.auto_submit_prompt();
if !self.auto_state.should_bypass_coordinator_next_submit() {
self.auto_submit_prompt();
}
return;
}
crossterm::event::KeyCode::Char('e') | crossterm::event::KeyCode::Char('E')
@@ -7391,7 +7394,12 @@ impl ChatWidget<'_> {
if trimmed.is_empty() {
return None;
}
if !self.auto_state.active {
if !self.auto_state.is_auto_active() {
return None;
}
if self.auto_state.current_phase() == AutoRunPhase::PausedManual
&& self.auto_state.should_bypass_coordinator_next_submit()
{
return None;
}
if !self.config.auto_drive.coordinator_routing {
@@ -7477,7 +7485,8 @@ impl ChatWidget<'_> {
let mut submitted_cli = false;
if !message.suppress_persistence
&& !original_text.trim().starts_with('/')
&& self.auto_state.active
&& self.auto_state.is_auto_active()
&& !self.auto_state.should_bypass_coordinator_next_submit()
&& self.config.auto_drive.coordinator_routing
{
let mut conversation = self.current_auto_history();
@@ -7492,7 +7501,9 @@ impl ChatWidget<'_> {
}
}
if !message.suppress_persistence {
if !message.suppress_persistence
&& !self.auto_state.should_bypass_coordinator_next_submit()
{
if let Some(mut routed) = self.try_coordinator_route(&original_text) {
self.finalize_sent_user_message(message);
self.consume_pending_prompt_for_ui_only_turn();
@@ -9192,9 +9203,11 @@ impl ChatWidget<'_> {
self.auto_state.paused_for_manual_edit = false;
self.auto_state.awaiting_submission = false;
self.auto_state.seconds_remaining = 0;
self.auto_state.clear_bypass_coordinator_flag();
self.auto_rebuild_live_ring();
self.bottom_pane.update_status_text(String::new());
self.bottom_pane.set_task_running(false);
self.auto_state.set_phase(AutoRunPhase::Active);
}
self.request_redraw();
@@ -12959,6 +12972,8 @@ fi\n\
self.bottom_pane.update_status_text("Auto Drive".to_string());
self.auto_update_terminal_hint();
self.bottom_pane.ensure_input_focus();
self.auto_state.awaiting_goal_input = true;
self.clear_composer();
self.request_redraw();
}
@@ -13073,7 +13088,8 @@ fi\n\
if self.auto_state.active {
self.auto_stop(None);
}
self.reset_auto_state_to_defaults();
self.auto_state.reset();
self.auto_state.set_phase(AutoRunPhase::Idle);
self.auto_show_goal_entry_panel();
self.request_redraw();
return;
@@ -13186,6 +13202,7 @@ fi\n\
return;
}
self.auto_state.waiting_for_review = false;
self.auto_state.clear_bypass_coordinator_flag();
let conversation = self.current_auto_history();
let Some(handle) = self.auto_handle.as_ref() else {
return;
@@ -13220,6 +13237,7 @@ fi\n\
if !self.auto_state.active {
return;
}
self.auto_state.clear_bypass_coordinator_flag();
let conversation = self.current_auto_history();
let Some(handle) = self.auto_handle.as_ref() else {
return;
@@ -13531,7 +13549,9 @@ fi\n\
}
}
AutoControllerEffect::SubmitPrompt => {
self.auto_submit_prompt();
if !self.auto_state.should_bypass_coordinator_next_submit() {
self.auto_submit_prompt();
}
}
AutoControllerEffect::LaunchStarted { goal } => {
self.bottom_pane.set_task_running(false);
@@ -13919,6 +13939,8 @@ fi\n\
self.auto_state.coordinator_waiting = false;
self.auto_state.paused_for_manual_edit = false;
self.auto_state.resume_after_manual_submit = false;
self.auto_state.clear_bypass_coordinator_flag();
self.auto_state.set_phase(AutoRunPhase::Active);
self.auto_state.seconds_remaining = 0;
let post_submit_display = self.auto_state.last_decision_display.clone();
self.auto_state.current_summary = None;
@@ -13974,6 +13996,8 @@ use crate::chatwidget::message::UserMessage;
self.auto_state.paused_for_manual_edit = true;
self.auto_state.resume_after_manual_submit = true;
self.auto_state.set_phase(AutoRunPhase::PausedManual);
self.auto_state.set_bypass_coordinator_next_submit();
self.auto_state.countdown_id = self.auto_state.countdown_id.wrapping_add(1);
self.auto_state.reset_countdown();
self.clear_composer();
@@ -14115,7 +14139,9 @@ use crate::chatwidget::message::UserMessage;
return;
}
self.auto_send_conversation();
if !self.auto_state.should_bypass_coordinator_next_submit() {
self.auto_send_conversation();
}
}
#[cfg(any(test, feature = "test-helpers"))]
@@ -23249,7 +23275,9 @@ impl ChatWidget<'_> {
return;
}
self.auto_state.waiting_for_review = false;
self.auto_send_conversation();
if !self.auto_state.should_bypass_coordinator_next_submit() {
self.auto_send_conversation();
}
self.request_redraw();
}
+5 -1
View File
@@ -457,7 +457,11 @@ impl ChatWidgetHarness {
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_submission && !chat.auto_state.paused_for_manual_edit {
chat.auto_start_countdown(chat.auto_state.countdown_id, countdown);
if let Some(seconds) = countdown {
chat.auto_spawn_countdown(chat.auto_state.countdown_id, seconds);
} else {
let _ = chat.auto_handle_countdown(chat.auto_state.countdown_id, 0);
}
if countdown == Some(0) {
chat.auto_state.awaiting_submission = false;
chat.auto_state.waiting_for_response = true;