mirror of
https://github.com/hanzoai/dev.git
synced 2026-10-25 03:32:32 +00:00
fix: restore goal entry and manual resume
This commit is contained in:
+22
@@ -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);
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user