mirror of
https://github.com/hanzoai/dev.git
synced 2026-10-25 03:32:32 +00:00
feat(tui/auto): add setup toggles for Auto Drive
This commit is contained in:
@@ -1672,6 +1672,31 @@ impl App<'_> {
|
||||
);
|
||||
}
|
||||
}
|
||||
AppEvent::AutoSetupToggleReview => {
|
||||
if let AppState::Chat { widget } = &mut self.app_state {
|
||||
widget.auto_setup_toggle_review();
|
||||
}
|
||||
}
|
||||
AppEvent::AutoSetupToggleSubagents => {
|
||||
if let AppState::Chat { widget } = &mut self.app_state {
|
||||
widget.auto_setup_toggle_subagents();
|
||||
}
|
||||
}
|
||||
AppEvent::AutoSetupSelectCountdown(mode) => {
|
||||
if let AppState::Chat { widget } = &mut self.app_state {
|
||||
widget.auto_setup_select_countdown(mode);
|
||||
}
|
||||
}
|
||||
AppEvent::AutoSetupConfirm => {
|
||||
if let AppState::Chat { widget } = &mut self.app_state {
|
||||
widget.auto_setup_confirm();
|
||||
}
|
||||
}
|
||||
AppEvent::AutoSetupCancel => {
|
||||
if let AppState::Chat { widget } = &mut self.app_state {
|
||||
widget.auto_setup_cancel();
|
||||
}
|
||||
}
|
||||
AppEvent::PerformUndoRestore {
|
||||
commit,
|
||||
restore_files,
|
||||
|
||||
@@ -104,6 +104,58 @@ pub(crate) struct AutoObserverTelemetry {
|
||||
pub last_intervention: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
||||
pub(crate) enum AutoContinueMode {
|
||||
Immediate,
|
||||
TenSeconds,
|
||||
SixtySeconds,
|
||||
Manual,
|
||||
}
|
||||
|
||||
impl Default for AutoContinueMode {
|
||||
fn default() -> Self {
|
||||
Self::TenSeconds
|
||||
}
|
||||
}
|
||||
|
||||
impl AutoContinueMode {
|
||||
pub fn seconds(self) -> Option<u8> {
|
||||
match self {
|
||||
Self::Immediate => Some(0),
|
||||
Self::TenSeconds => Some(10),
|
||||
Self::SixtySeconds => Some(60),
|
||||
Self::Manual => None,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn label(self) -> &'static str {
|
||||
match self {
|
||||
Self::Immediate => "Immediate",
|
||||
Self::TenSeconds => "10 seconds",
|
||||
Self::SixtySeconds => "60 seconds",
|
||||
Self::Manual => "Manual approval",
|
||||
}
|
||||
}
|
||||
|
||||
pub fn cycle_forward(self) -> Self {
|
||||
match self {
|
||||
Self::Immediate => Self::TenSeconds,
|
||||
Self::TenSeconds => Self::SixtySeconds,
|
||||
Self::SixtySeconds => Self::Manual,
|
||||
Self::Manual => Self::Immediate,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn cycle_backward(self) -> Self {
|
||||
match self {
|
||||
Self::Immediate => Self::Manual,
|
||||
Self::TenSeconds => Self::Immediate,
|
||||
Self::SixtySeconds => Self::TenSeconds,
|
||||
Self::Manual => Self::SixtySeconds,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[allow(clippy::large_enum_variant)]
|
||||
#[derive(Debug)]
|
||||
pub(crate) enum AppEvent {
|
||||
@@ -174,6 +226,11 @@ pub(crate) enum AppEvent {
|
||||
replace_message: Option<String>,
|
||||
additional_instructions: Option<String>,
|
||||
},
|
||||
AutoSetupToggleReview,
|
||||
AutoSetupToggleSubagents,
|
||||
AutoSetupSelectCountdown(AutoContinueMode),
|
||||
AutoSetupConfirm,
|
||||
AutoSetupCancel,
|
||||
|
||||
/// Dispatch a recognized slash command from the UI (composer) to the app
|
||||
/// layer so it can be handled centrally. Includes the full command text.
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
use crate::app_event::AppEvent;
|
||||
use crate::app_event::{AppEvent, AutoContinueMode};
|
||||
use crate::app_event_sender::AppEventSender;
|
||||
use crate::auto_drive_strings;
|
||||
use crate::colors;
|
||||
use crate::header_wave::{HeaderBorderWeaveEffect, HeaderWaveEffect};
|
||||
use crate::spinner;
|
||||
use crossterm::event::{KeyCode, KeyEvent, KeyModifiers};
|
||||
use ratatui::buffer::Buffer;
|
||||
use ratatui::layout::Rect;
|
||||
use ratatui::style::{Modifier, Style};
|
||||
@@ -15,6 +16,7 @@ use std::time::{Duration, Instant, SystemTime, UNIX_EPOCH};
|
||||
use super::{
|
||||
bottom_pane_view::{BottomPaneView, ConditionalUpdate},
|
||||
chat_composer::ChatComposer,
|
||||
BottomPane,
|
||||
};
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
@@ -29,7 +31,7 @@ pub(crate) struct AutoCoordinatorButton {
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
pub(crate) struct AutoCoordinatorViewModel {
|
||||
pub(crate) struct AutoActiveViewModel {
|
||||
#[allow(dead_code)]
|
||||
pub goal: Option<String>,
|
||||
pub status_lines: Vec<String>,
|
||||
@@ -43,6 +45,20 @@ pub(crate) struct AutoCoordinatorViewModel {
|
||||
pub cli_running: bool,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
pub(crate) struct AutoSetupViewModel {
|
||||
pub goal: String,
|
||||
pub auto_review_enabled: bool,
|
||||
pub subagents_enabled: bool,
|
||||
pub continue_mode: AutoContinueMode,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
pub(crate) enum AutoCoordinatorViewModel {
|
||||
Setup(AutoSetupViewModel),
|
||||
Active(AutoActiveViewModel),
|
||||
}
|
||||
|
||||
struct ButtonContext {
|
||||
label: String,
|
||||
enabled: bool,
|
||||
@@ -88,19 +104,15 @@ impl AutoCoordinatorView {
|
||||
self.model = model;
|
||||
}
|
||||
|
||||
fn build_context(&self) -> VariantContext {
|
||||
let button = self
|
||||
.model
|
||||
.button
|
||||
.as_ref()
|
||||
.map(|btn| ButtonContext {
|
||||
label: btn.label.clone(),
|
||||
enabled: btn.enabled,
|
||||
});
|
||||
fn build_context(model: &AutoActiveViewModel) -> VariantContext {
|
||||
let button = model.button.as_ref().map(|btn| ButtonContext {
|
||||
label: btn.label.clone(),
|
||||
enabled: btn.enabled,
|
||||
});
|
||||
VariantContext {
|
||||
button,
|
||||
ctrl_hint: self.model.ctrl_switch_hint.clone(),
|
||||
manual_hint: self.model.manual_hint.clone(),
|
||||
ctrl_hint: model.ctrl_switch_hint.clone(),
|
||||
manual_hint: model.manual_hint.clone(),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -134,8 +146,8 @@ impl AutoCoordinatorView {
|
||||
}
|
||||
}
|
||||
|
||||
fn spinner_should_run(&self) -> bool {
|
||||
self.model.cli_running
|
||||
fn spinner_should_run(model: &AutoActiveViewModel) -> bool {
|
||||
model.cli_running
|
||||
}
|
||||
|
||||
fn overlay_text(&self, spinner_symbol: &str) -> Option<String> {
|
||||
@@ -265,11 +277,11 @@ impl AutoCoordinatorView {
|
||||
}
|
||||
}
|
||||
|
||||
fn derived_status_entries(&self) -> Vec<(String, Style)> {
|
||||
fn derived_status_entries(&self, model: &AutoActiveViewModel) -> Vec<(String, Style)> {
|
||||
let mut entries: Vec<(String, Style)> = Vec::new();
|
||||
|
||||
if self.model.awaiting_submission {
|
||||
let text = if let Some(countdown) = &self.model.countdown {
|
||||
if model.awaiting_submission {
|
||||
let text = if let Some(countdown) = &model.countdown {
|
||||
format!("Auto continue in {}s", countdown.remaining)
|
||||
} else {
|
||||
"Awaiting confirmation".to_string()
|
||||
@@ -277,7 +289,7 @@ impl AutoCoordinatorView {
|
||||
entries.push((text, Style::default().fg(colors::text_dim())));
|
||||
}
|
||||
|
||||
for status in &self.model.status_lines {
|
||||
for status in &model.status_lines {
|
||||
let trimmed = status.trim();
|
||||
if trimmed.is_empty() {
|
||||
continue;
|
||||
@@ -313,8 +325,8 @@ impl AutoCoordinatorView {
|
||||
lines
|
||||
}
|
||||
|
||||
fn cli_prompt_lines(&self, style: Style) -> Option<Vec<Line<'static>>> {
|
||||
self.model.cli_prompt.as_ref().map(|prompt| {
|
||||
fn cli_prompt_lines(&self, model: &AutoActiveViewModel, style: Style) -> Option<Vec<Line<'static>>> {
|
||||
model.cli_prompt.as_ref().map(|prompt| {
|
||||
prompt
|
||||
.lines()
|
||||
.map(|line| Line::from(Span::styled(line.trim_end().to_string(), style)))
|
||||
@@ -436,7 +448,12 @@ impl AutoCoordinatorView {
|
||||
.sum()
|
||||
}
|
||||
|
||||
fn estimated_height(&self, width: u16, ctx: &VariantContext) -> u16 {
|
||||
fn estimated_height_active(
|
||||
&self,
|
||||
width: u16,
|
||||
ctx: &VariantContext,
|
||||
model: &AutoActiveViewModel,
|
||||
) -> u16 {
|
||||
let inner_width = self.inner_width(width);
|
||||
let button_height = if ctx.button.is_some() { 3 } else { 0 };
|
||||
let hint_height = ctx
|
||||
@@ -453,12 +470,10 @@ impl AutoCoordinatorView {
|
||||
Self::wrap_count(ctrl_hint, inner_width)
|
||||
};
|
||||
|
||||
let awaiting = self.model.awaiting_submission;
|
||||
|
||||
let mut total = 0;
|
||||
|
||||
if awaiting {
|
||||
if let Some(prompt) = &self.model.cli_prompt {
|
||||
if model.awaiting_submission {
|
||||
if let Some(prompt) = &model.cli_prompt {
|
||||
total += Self::wrap_count(prompt, inner_width).max(1);
|
||||
}
|
||||
if ctx.button.is_some() {
|
||||
@@ -469,7 +484,7 @@ impl AutoCoordinatorView {
|
||||
total += ctrl_height.max(1);
|
||||
}
|
||||
} else {
|
||||
let status_entries = self.derived_status_entries();
|
||||
let status_entries = self.derived_status_entries(model);
|
||||
for (_, (text, _)) in status_entries.iter().enumerate() {
|
||||
total += Self::wrap_count(text, inner_width);
|
||||
}
|
||||
@@ -490,16 +505,16 @@ impl AutoCoordinatorView {
|
||||
.min(u16::MAX as usize) as u16
|
||||
}
|
||||
|
||||
fn render_internal(&self, area: Rect, buf: &mut Buffer, ctx: &VariantContext) {
|
||||
self.render_classic(area, buf, ctx);
|
||||
}
|
||||
|
||||
fn render_classic(&self, area: Rect, buf: &mut Buffer, ctx: &VariantContext) {
|
||||
fn render_active(
|
||||
&self,
|
||||
area: Rect,
|
||||
buf: &mut Buffer,
|
||||
model: &AutoActiveViewModel,
|
||||
) {
|
||||
let now = Instant::now();
|
||||
let waiting = self.model.waiting_for_response;
|
||||
let mut frame_needed = false;
|
||||
|
||||
let should_enable_border = waiting && !self.model.cli_running;
|
||||
let should_enable_border = model.waiting_for_response && !model.cli_running;
|
||||
let mut border_enabled = self.header_border.is_enabled();
|
||||
if should_enable_border && !border_enabled {
|
||||
self.header_border.set_enabled(true, now);
|
||||
@@ -513,7 +528,7 @@ impl AutoCoordinatorView {
|
||||
frame_needed = true;
|
||||
}
|
||||
|
||||
let spinner_active = self.spinner_should_run();
|
||||
let spinner_active = Self::spinner_should_run(model);
|
||||
let spinner_def = spinner::current_spinner();
|
||||
let spinner_symbol = if spinner_active {
|
||||
let now_ms = SystemTime::now()
|
||||
@@ -536,38 +551,40 @@ impl AutoCoordinatorView {
|
||||
return;
|
||||
}
|
||||
|
||||
let status_entries = self.derived_status_entries();
|
||||
let status_entries = self.derived_status_entries(model);
|
||||
let mut content_lines: Vec<Line<'static>> = Vec::new();
|
||||
let mut footer_lines: Vec<Line<'static>> = Vec::new();
|
||||
let mut has_button_block = false;
|
||||
|
||||
if self.model.awaiting_submission {
|
||||
if let Some(prompt_lines) =
|
||||
self.cli_prompt_lines(Style::default().fg(colors::text_dim()))
|
||||
{
|
||||
content_lines.extend(prompt_lines);
|
||||
}
|
||||
let ctx = Self::build_context(model);
|
||||
|
||||
if let Some(button_block) = self.button_block_lines(ctx) {
|
||||
if model.awaiting_submission {
|
||||
if let Some(prompt_lines) =
|
||||
self.cli_prompt_lines(model, Style::default().fg(colors::text_dim()))
|
||||
{
|
||||
content_lines.extend(prompt_lines);
|
||||
}
|
||||
|
||||
if let Some(button_block) = self.button_block_lines(&ctx) {
|
||||
has_button_block = true;
|
||||
footer_lines.extend(button_block);
|
||||
} else if let Some(ctrl_hint_line) = self.ctrl_hint_line(ctx) {
|
||||
} else if let Some(ctrl_hint_line) = self.ctrl_hint_line(&ctx) {
|
||||
footer_lines.push(ctrl_hint_line);
|
||||
}
|
||||
} else {
|
||||
let status_lines = self.status_lines_with_entries(&status_entries);
|
||||
content_lines.extend(status_lines);
|
||||
|
||||
if let Some(button_block) = self.button_block_lines(ctx) {
|
||||
if let Some(button_block) = self.button_block_lines(&ctx) {
|
||||
has_button_block = true;
|
||||
footer_lines.extend(button_block);
|
||||
}
|
||||
|
||||
if let Some(hint_line) = self.manual_hint_line(ctx) {
|
||||
if let Some(hint_line) = self.manual_hint_line(&ctx) {
|
||||
footer_lines.push(hint_line);
|
||||
}
|
||||
|
||||
if let Some(ctrl_hint_line) = self.ctrl_hint_line(ctx) {
|
||||
if let Some(ctrl_hint_line) = self.ctrl_hint_line(&ctx) {
|
||||
footer_lines.push(Line::default());
|
||||
footer_lines.push(ctrl_hint_line);
|
||||
}
|
||||
@@ -640,6 +657,155 @@ impl AutoCoordinatorView {
|
||||
}
|
||||
}
|
||||
|
||||
fn estimated_height_setup(&self, width: u16, model: &AutoSetupViewModel) -> u16 {
|
||||
let inner_width = self.inner_width(width);
|
||||
if inner_width == 0 {
|
||||
return 2;
|
||||
}
|
||||
|
||||
let lines = Self::setup_text_lines(model);
|
||||
let mut total = 0usize;
|
||||
for line in lines {
|
||||
if line.is_empty() {
|
||||
total += 1;
|
||||
} else {
|
||||
total += Self::wrap_count(&line, inner_width).max(1);
|
||||
}
|
||||
}
|
||||
|
||||
total
|
||||
.saturating_add(2)
|
||||
.min(u16::MAX as usize) as u16
|
||||
}
|
||||
|
||||
fn render_setup(&self, area: Rect, buf: &mut Buffer, model: &AutoSetupViewModel) {
|
||||
let now = Instant::now();
|
||||
if self.header_wave.is_enabled() {
|
||||
self.header_wave.set_enabled(false, now);
|
||||
}
|
||||
if self.header_border.is_enabled() {
|
||||
self.header_border.set_enabled(false, now);
|
||||
}
|
||||
|
||||
let Some(inner) = self.render_frame(area, buf, now, None) else {
|
||||
return;
|
||||
};
|
||||
let inner = self.apply_left_padding(inner, buf);
|
||||
if inner.height == 0 {
|
||||
return;
|
||||
}
|
||||
|
||||
let lines = Self::setup_render_lines(model);
|
||||
Paragraph::new(lines)
|
||||
.wrap(Wrap { trim: true })
|
||||
.render(inner, buf);
|
||||
}
|
||||
|
||||
fn setup_text_lines(model: &AutoSetupViewModel) -> Vec<String> {
|
||||
let goal_text = model.goal.trim();
|
||||
let goal_display = if goal_text.is_empty() {
|
||||
"Goal: (none)".to_string()
|
||||
} else {
|
||||
format!("Goal: {goal_text}")
|
||||
};
|
||||
|
||||
vec![
|
||||
goal_display,
|
||||
String::new(),
|
||||
Self::setup_toggle_text("Automatic review", "R", model.auto_review_enabled),
|
||||
Self::setup_toggle_text("Sub agents", "S", model.subagents_enabled),
|
||||
Self::setup_countdown_text(model),
|
||||
String::new(),
|
||||
"Enter to launch Auto Drive | Esc to cancel".to_string(),
|
||||
]
|
||||
}
|
||||
|
||||
fn setup_toggle_text(label: &str, key_hint: &str, enabled: bool) -> String {
|
||||
let checkbox = if enabled { "[x]" } else { "[ ]" };
|
||||
format!("{checkbox} {label} ({key_hint} to toggle)")
|
||||
}
|
||||
|
||||
fn setup_countdown_text(model: &AutoSetupViewModel) -> String {
|
||||
format!(
|
||||
"Auto-continue: {} (C cycle | 0=now | 1=10s | 6=60s | M=manual)",
|
||||
model.continue_mode.label()
|
||||
)
|
||||
}
|
||||
|
||||
fn setup_render_lines(model: &AutoSetupViewModel) -> Vec<Line<'static>> {
|
||||
let mut lines: Vec<Line<'static>> = Vec::new();
|
||||
let goal_text = model.goal.trim();
|
||||
let goal_display = if goal_text.is_empty() {
|
||||
"(none)"
|
||||
} else {
|
||||
goal_text
|
||||
};
|
||||
lines.push(Line::from(vec![
|
||||
Span::styled("Goal: ", Style::default().fg(colors::text())),
|
||||
Span::styled(
|
||||
goal_display.to_string(),
|
||||
Style::default().fg(colors::text()).add_modifier(Modifier::BOLD),
|
||||
),
|
||||
]));
|
||||
lines.push(Line::default());
|
||||
lines.push(Self::setup_toggle_line(
|
||||
"Automatic review",
|
||||
"R",
|
||||
model.auto_review_enabled,
|
||||
));
|
||||
lines.push(Self::setup_toggle_line("Sub agents", "S", model.subagents_enabled));
|
||||
lines.push(Self::setup_countdown_line(model));
|
||||
lines.push(Line::default());
|
||||
lines.push(Line::from(Span::styled(
|
||||
"Enter to launch Auto Drive | Esc to cancel".to_string(),
|
||||
Style::default()
|
||||
.fg(colors::text_dim())
|
||||
.add_modifier(Modifier::BOLD),
|
||||
)));
|
||||
lines
|
||||
}
|
||||
|
||||
fn setup_toggle_line(label: &str, key_hint: &str, enabled: bool) -> Line<'static> {
|
||||
let checkbox = if enabled { "[x]" } else { "[ ]" };
|
||||
let state_style = if enabled {
|
||||
Style::default()
|
||||
.fg(colors::success())
|
||||
.add_modifier(Modifier::BOLD)
|
||||
} else {
|
||||
Style::default().fg(colors::text_dim())
|
||||
};
|
||||
let mut spans: Vec<Span<'static>> = Vec::new();
|
||||
spans.push(Span::styled(checkbox.to_string(), state_style));
|
||||
spans.push(Span::raw(" "));
|
||||
spans.push(Span::styled(label.to_string(), Style::default().fg(colors::text())));
|
||||
spans.push(Span::raw(" "));
|
||||
spans.push(Span::styled(
|
||||
format!("({key_hint} to toggle)"),
|
||||
Style::default().fg(colors::text_dim()),
|
||||
));
|
||||
Line::from(spans)
|
||||
}
|
||||
|
||||
fn setup_countdown_line(model: &AutoSetupViewModel) -> Line<'static> {
|
||||
let mut spans: Vec<Span<'static>> = Vec::new();
|
||||
spans.push(Span::styled(
|
||||
"Auto-continue: ".to_string(),
|
||||
Style::default().fg(colors::text()),
|
||||
));
|
||||
spans.push(Span::styled(
|
||||
model.continue_mode.label().to_string(),
|
||||
Style::default()
|
||||
.fg(colors::primary())
|
||||
.add_modifier(Modifier::BOLD),
|
||||
));
|
||||
spans.push(Span::raw(" "));
|
||||
spans.push(Span::styled(
|
||||
"C cycle | 0=now | 1=10s | 6=60s | M=manual".to_string(),
|
||||
Style::default().fg(colors::text_dim()),
|
||||
));
|
||||
Line::from(spans)
|
||||
}
|
||||
|
||||
fn apply_left_padding(&self, area: Rect, buf: &mut Buffer) -> Rect {
|
||||
if area.width <= 1 {
|
||||
return area;
|
||||
@@ -662,18 +828,95 @@ impl AutoCoordinatorView {
|
||||
}
|
||||
|
||||
impl<'a> BottomPaneView<'a> for AutoCoordinatorView {
|
||||
fn handle_key_event(&mut self, _pane: &mut BottomPane<'a>, key_event: KeyEvent) {
|
||||
let AutoCoordinatorViewModel::Setup(model) = &self.model else {
|
||||
return;
|
||||
};
|
||||
|
||||
let has_ctrl_like = key_event.modifiers.contains(KeyModifiers::CONTROL)
|
||||
|| key_event.modifiers.contains(KeyModifiers::ALT)
|
||||
|| key_event.modifiers.contains(KeyModifiers::SUPER);
|
||||
if has_ctrl_like {
|
||||
return;
|
||||
}
|
||||
|
||||
match key_event.code {
|
||||
KeyCode::Enter => {
|
||||
self.app_event_tx.send(AppEvent::AutoSetupConfirm);
|
||||
}
|
||||
KeyCode::Esc => {
|
||||
self.app_event_tx.send(AppEvent::AutoSetupCancel);
|
||||
}
|
||||
KeyCode::Left => {
|
||||
self
|
||||
.app_event_tx
|
||||
.send(AppEvent::AutoSetupSelectCountdown(model.continue_mode.cycle_backward()));
|
||||
}
|
||||
KeyCode::Right => {
|
||||
self
|
||||
.app_event_tx
|
||||
.send(AppEvent::AutoSetupSelectCountdown(model.continue_mode.cycle_forward()));
|
||||
}
|
||||
KeyCode::Char(ch) => {
|
||||
let lowered = ch.to_ascii_lowercase();
|
||||
match lowered {
|
||||
'r' => {
|
||||
self.app_event_tx.send(AppEvent::AutoSetupToggleReview);
|
||||
}
|
||||
's' => {
|
||||
self.app_event_tx.send(AppEvent::AutoSetupToggleSubagents);
|
||||
}
|
||||
'c' => {
|
||||
self
|
||||
.app_event_tx
|
||||
.send(AppEvent::AutoSetupSelectCountdown(model.continue_mode.cycle_forward()));
|
||||
}
|
||||
'0' => {
|
||||
self
|
||||
.app_event_tx
|
||||
.send(AppEvent::AutoSetupSelectCountdown(AutoContinueMode::Immediate));
|
||||
}
|
||||
'1' => {
|
||||
self
|
||||
.app_event_tx
|
||||
.send(AppEvent::AutoSetupSelectCountdown(AutoContinueMode::TenSeconds));
|
||||
}
|
||||
'6' => {
|
||||
self
|
||||
.app_event_tx
|
||||
.send(AppEvent::AutoSetupSelectCountdown(AutoContinueMode::SixtySeconds));
|
||||
}
|
||||
'm' => {
|
||||
self
|
||||
.app_event_tx
|
||||
.send(AppEvent::AutoSetupSelectCountdown(AutoContinueMode::Manual));
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
|
||||
fn desired_height(&self, width: u16) -> u16 {
|
||||
let ctx = self.build_context();
|
||||
self.estimated_height(width, &ctx)
|
||||
match &self.model {
|
||||
AutoCoordinatorViewModel::Active(model) => {
|
||||
let ctx = Self::build_context(model);
|
||||
self.estimated_height_active(width, &ctx, model)
|
||||
}
|
||||
AutoCoordinatorViewModel::Setup(model) => self.estimated_height_setup(width, model),
|
||||
}
|
||||
}
|
||||
|
||||
fn render(&self, area: Rect, buf: &mut Buffer) {
|
||||
let ctx = self.build_context();
|
||||
if area.height == 0 {
|
||||
return;
|
||||
}
|
||||
|
||||
self.render_internal(area, buf, &ctx);
|
||||
match &self.model {
|
||||
AutoCoordinatorViewModel::Active(model) => self.render_active(area, buf, model),
|
||||
AutoCoordinatorViewModel::Setup(model) => self.render_setup(area, buf, model),
|
||||
}
|
||||
}
|
||||
|
||||
fn update_status_text(&mut self, text: String) -> ConditionalUpdate {
|
||||
|
||||
@@ -61,9 +61,11 @@ pub(crate) enum CancellationEvent {
|
||||
pub(crate) use chat_composer::ChatComposer;
|
||||
pub(crate) use chat_composer::InputResult;
|
||||
pub(crate) use auto_coordinator_view::{
|
||||
AutoActiveViewModel,
|
||||
AutoCoordinatorButton,
|
||||
AutoCoordinatorView,
|
||||
AutoCoordinatorViewModel,
|
||||
AutoSetupViewModel,
|
||||
CountdownState,
|
||||
};
|
||||
pub(crate) use login_accounts_view::{
|
||||
|
||||
+321
-100
@@ -128,8 +128,10 @@ use codex_core::protocol::TaskCompleteEvent;
|
||||
use codex_core::protocol::TokenUsage;
|
||||
use codex_core::protocol::TurnDiffEvent;
|
||||
use crate::bottom_pane::{
|
||||
AutoActiveViewModel,
|
||||
AutoCoordinatorButton,
|
||||
AutoCoordinatorViewModel,
|
||||
AutoSetupViewModel,
|
||||
CountdownState,
|
||||
};
|
||||
use crate::exec_command::strip_bash_lc_and_escape;
|
||||
@@ -191,7 +193,6 @@ const STATUS_TOKENS_PREFIX: &str = " ";
|
||||
const RESUME_PLACEHOLDER_MESSAGE: &str = "Resuming previous session...";
|
||||
const RESUME_NO_HISTORY_NOTICE: &str =
|
||||
"No saved messages for this session. Start typing to continue.";
|
||||
const AUTO_COUNTDOWN_SECONDS: u8 = 10;
|
||||
const ENABLE_WARP_STRIPES: bool = false;
|
||||
|
||||
fn status_field_prefix(label: &str) -> String {
|
||||
@@ -226,6 +227,7 @@ use crate::app_event::{
|
||||
AutoCoordinatorStatus,
|
||||
AutoObserverStatus,
|
||||
AutoObserverTelemetry,
|
||||
AutoContinueMode,
|
||||
BackgroundPlacement,
|
||||
TerminalAfter,
|
||||
TerminalCommandGate,
|
||||
@@ -501,7 +503,6 @@ pub(crate) struct GhostState {
|
||||
queued_user_messages: VecDeque<UserMessage>,
|
||||
}
|
||||
|
||||
#[derive(Default)]
|
||||
struct AutoCoordinatorUiState {
|
||||
active: bool,
|
||||
goal: Option<String>,
|
||||
@@ -533,6 +534,10 @@ struct AutoCoordinatorUiState {
|
||||
observer_telemetry: Option<AutoObserverTelemetry>,
|
||||
observer_status: AutoObserverStatus,
|
||||
coordinator_waiting: bool,
|
||||
review_enabled: bool,
|
||||
subagents_enabled: bool,
|
||||
continue_mode: AutoContinueMode,
|
||||
setup: Option<AutoSetupState>,
|
||||
}
|
||||
|
||||
impl AutoCoordinatorUiState {
|
||||
@@ -541,8 +546,72 @@ impl AutoCoordinatorUiState {
|
||||
}
|
||||
|
||||
fn countdown_active(&self) -> bool {
|
||||
self.awaiting_submission && !self.paused_for_manual_edit
|
||||
self.awaiting_submission
|
||||
&& !self.paused_for_manual_edit
|
||||
&& self
|
||||
.countdown_seconds()
|
||||
.map(|seconds| seconds > 0)
|
||||
.unwrap_or(false)
|
||||
}
|
||||
|
||||
fn countdown_seconds(&self) -> Option<u8> {
|
||||
self.continue_mode.seconds()
|
||||
}
|
||||
|
||||
fn reset_countdown(&mut self) {
|
||||
self.seconds_remaining = self.countdown_seconds().unwrap_or(0);
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for AutoCoordinatorUiState {
|
||||
fn default() -> Self {
|
||||
let continue_mode = AutoContinueMode::default();
|
||||
let seconds = continue_mode.seconds().unwrap_or(0);
|
||||
Self {
|
||||
active: false,
|
||||
goal: None,
|
||||
current_summary: None,
|
||||
current_progress_past: None,
|
||||
current_progress_current: None,
|
||||
current_cli_prompt: None,
|
||||
current_cli_context: None,
|
||||
current_display_line: None,
|
||||
current_display_is_summary: false,
|
||||
current_reasoning_title: None,
|
||||
placeholder_phrase: None,
|
||||
thinking_prefix_stripped: false,
|
||||
current_summary_index: None,
|
||||
awaiting_submission: false,
|
||||
waiting_for_response: false,
|
||||
paused_for_manual_edit: false,
|
||||
resume_after_manual_submit: false,
|
||||
waiting_for_review: false,
|
||||
countdown_id: 0,
|
||||
seconds_remaining: seconds,
|
||||
awaiting_goal_input: false,
|
||||
last_broadcast_summary: None,
|
||||
last_decision_summary: None,
|
||||
last_decision_progress_past: None,
|
||||
last_decision_progress_current: None,
|
||||
last_decision_display: None,
|
||||
last_decision_display_is_summary: false,
|
||||
observer_telemetry: None,
|
||||
observer_status: AutoObserverStatus::default(),
|
||||
coordinator_waiting: false,
|
||||
review_enabled: false,
|
||||
subagents_enabled: false,
|
||||
continue_mode,
|
||||
setup: None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
struct AutoSetupState {
|
||||
goal: String,
|
||||
auto_review_enabled: bool,
|
||||
subagents_enabled: bool,
|
||||
continue_mode: AutoContinueMode,
|
||||
}
|
||||
|
||||
const AUTO_RESOLVE_MAX_REVIEW_ATTEMPTS: u32 = 3;
|
||||
@@ -8628,11 +8697,12 @@ impl ChatWidget<'_> {
|
||||
self.auto_resolve_on_task_complete(last_agent_message.clone());
|
||||
}
|
||||
// Auto-review: if the coordinator marked this as a write turn, kick off a review pass
|
||||
if self.auto_state.active {
|
||||
if let Some(cfg) = self.pending_auto_turn_config.take() {
|
||||
if !self.is_review_flow_active() {
|
||||
self.auto_handle_post_turn_review(cfg);
|
||||
}
|
||||
if let Some(cfg) = self.pending_auto_turn_config.take() {
|
||||
if self.auto_state.active
|
||||
&& self.auto_state.review_enabled
|
||||
&& !self.is_review_flow_active()
|
||||
{
|
||||
self.auto_handle_post_turn_review(cfg);
|
||||
}
|
||||
}
|
||||
// Defensive: clear transient agents-preparing state
|
||||
@@ -11472,6 +11542,100 @@ fi\n\
|
||||
}
|
||||
}
|
||||
|
||||
fn auto_present_setup(&mut self) {
|
||||
let Some(setup) = self.auto_state.setup.as_ref() else {
|
||||
return;
|
||||
};
|
||||
let goal = setup.goal.clone();
|
||||
let auto_review_enabled = setup.auto_review_enabled;
|
||||
let subagents_enabled = setup.subagents_enabled;
|
||||
let continue_mode = setup.continue_mode;
|
||||
self.auto_state.review_enabled = auto_review_enabled;
|
||||
self.auto_state.subagents_enabled = subagents_enabled;
|
||||
self.auto_state.continue_mode = continue_mode;
|
||||
self.auto_state.reset_countdown();
|
||||
let model = AutoCoordinatorViewModel::Setup(AutoSetupViewModel {
|
||||
goal,
|
||||
auto_review_enabled,
|
||||
subagents_enabled,
|
||||
continue_mode,
|
||||
});
|
||||
self.bottom_pane.show_auto_coordinator_view(model);
|
||||
self.bottom_pane.set_task_running(false);
|
||||
self.bottom_pane.clear_live_ring();
|
||||
self.bottom_pane
|
||||
.update_status_text("Auto Drive Setup".to_string());
|
||||
self.bottom_pane
|
||||
.set_standard_terminal_hint(Some("Enter to launch | Esc to cancel".to_string()));
|
||||
self.bottom_pane.ensure_input_focus();
|
||||
self.update_header_border_activation();
|
||||
self.request_redraw();
|
||||
}
|
||||
|
||||
fn auto_launch_with_setup(&mut self, setup: AutoSetupState) {
|
||||
let AutoSetupState {
|
||||
goal,
|
||||
auto_review_enabled,
|
||||
subagents_enabled,
|
||||
continue_mode,
|
||||
} = setup;
|
||||
|
||||
let conversation = self.rebuild_auto_history();
|
||||
match start_auto_coordinator(
|
||||
self.app_event_tx.clone(),
|
||||
goal.clone(),
|
||||
conversation,
|
||||
self.config.clone(),
|
||||
self.config.debug,
|
||||
self.config.auto_drive_observer_cadence,
|
||||
) {
|
||||
Ok(handle) => {
|
||||
self.auto_handle = Some(handle);
|
||||
self.auto_state.reset();
|
||||
self.auto_state.setup = None;
|
||||
self.auto_state.review_enabled = auto_review_enabled;
|
||||
self.auto_state.subagents_enabled = subagents_enabled;
|
||||
self.auto_state.continue_mode = continue_mode;
|
||||
self.auto_state.reset_countdown();
|
||||
self.auto_state.active = true;
|
||||
self.auto_state.goal = Some(goal.clone());
|
||||
self.auto_state.current_summary = None;
|
||||
self.auto_state.current_progress_past = None;
|
||||
self.auto_state.current_progress_current = None;
|
||||
self.auto_state.current_cli_prompt = None;
|
||||
self.auto_state.current_cli_context = None;
|
||||
self.auto_state.current_display_line = None;
|
||||
self.auto_state.current_display_is_summary = false;
|
||||
self.auto_state.current_reasoning_title = None;
|
||||
self.auto_state.current_summary_index = None;
|
||||
self.auto_state.placeholder_phrase =
|
||||
Some(auto_drive_strings::next_auto_drive_phrase().to_string());
|
||||
self.auto_state.thinking_prefix_stripped = false;
|
||||
self.auto_state.last_broadcast_summary = None;
|
||||
self.auto_state.last_decision_progress_past = None;
|
||||
self.auto_state.last_decision_progress_current = None;
|
||||
self.auto_state.waiting_for_response = true;
|
||||
self.auto_state.coordinator_waiting = true;
|
||||
self.bottom_pane
|
||||
.set_standard_terminal_hint(Some("Esc to stop Auto Drive".to_string()));
|
||||
self.update_header_border_activation();
|
||||
self.auto_rebuild_live_ring();
|
||||
self.push_background_tail(format!("Auto Drive started: {goal}"));
|
||||
self.request_redraw();
|
||||
}
|
||||
Err(err) => {
|
||||
self.push_background_tail(format!("Coordinator failed to start: {err}"));
|
||||
self.auto_state.setup = Some(AutoSetupState {
|
||||
goal,
|
||||
auto_review_enabled,
|
||||
subagents_enabled,
|
||||
continue_mode,
|
||||
});
|
||||
self.auto_present_setup();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn handle_auto_command(&mut self, goal: Option<String>) {
|
||||
let provided = goal.unwrap_or_default();
|
||||
let trimmed = provided.trim();
|
||||
@@ -11514,49 +11678,72 @@ fi\n\
|
||||
self.auto_stop(None);
|
||||
}
|
||||
|
||||
let conversation = self.rebuild_auto_history();
|
||||
match start_auto_coordinator(
|
||||
self.app_event_tx.clone(),
|
||||
goal_text.clone(),
|
||||
conversation,
|
||||
self.config.clone(),
|
||||
self.config.debug,
|
||||
self.config.auto_drive_observer_cadence,
|
||||
) {
|
||||
Ok(handle) => {
|
||||
self.auto_handle = Some(handle);
|
||||
self.auto_state.reset();
|
||||
self.auto_state.active = true;
|
||||
self.auto_state.goal = Some(goal_text.clone());
|
||||
self.auto_state.current_summary = None;
|
||||
self.auto_state.current_progress_past = None;
|
||||
self.auto_state.current_progress_current = None;
|
||||
self.auto_state.current_cli_prompt = None;
|
||||
self.auto_state.current_cli_context = None;
|
||||
self.auto_state.current_display_line = None;
|
||||
self.auto_state.current_display_is_summary = false;
|
||||
self.auto_state.current_reasoning_title = None;
|
||||
self.auto_state.current_summary_index = None;
|
||||
self.auto_state.placeholder_phrase =
|
||||
Some(auto_drive_strings::next_auto_drive_phrase().to_string());
|
||||
self.auto_state.thinking_prefix_stripped = false;
|
||||
self.auto_state.last_broadcast_summary = None;
|
||||
self.auto_state.last_decision_progress_past = None;
|
||||
self.auto_state.last_decision_progress_current = None;
|
||||
self.auto_state.seconds_remaining = AUTO_COUNTDOWN_SECONDS;
|
||||
self.auto_state.waiting_for_response = true;
|
||||
self.auto_state.coordinator_waiting = true;
|
||||
self.update_header_border_activation();
|
||||
self.auto_rebuild_live_ring();
|
||||
self.push_background_tail(format!("Auto Drive started: {goal_text}"));
|
||||
self.request_redraw();
|
||||
}
|
||||
Err(err) => {
|
||||
self.push_background_tail(format!("Coordinator failed to start: {err}"));
|
||||
self.auto_state.reset();
|
||||
self.auto_state.setup = Some(AutoSetupState {
|
||||
goal: goal_text.clone(),
|
||||
auto_review_enabled: false,
|
||||
subagents_enabled: false,
|
||||
continue_mode: AutoContinueMode::default(),
|
||||
});
|
||||
self.auto_state.review_enabled = false;
|
||||
self.auto_state.subagents_enabled = false;
|
||||
self.auto_state.continue_mode = AutoContinueMode::default();
|
||||
self.auto_state.reset_countdown();
|
||||
|
||||
self.push_background_tail(format!(
|
||||
"Auto Drive setup ready. Review defaults before launching goal: {goal_text}"
|
||||
));
|
||||
self.auto_present_setup();
|
||||
}
|
||||
|
||||
pub(crate) fn auto_setup_toggle_review(&mut self) {
|
||||
if let Some(setup) = self.auto_state.setup.as_mut() {
|
||||
setup.auto_review_enabled = !setup.auto_review_enabled;
|
||||
self.auto_state.review_enabled = setup.auto_review_enabled;
|
||||
self.auto_present_setup();
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn auto_setup_toggle_subagents(&mut self) {
|
||||
if let Some(setup) = self.auto_state.setup.as_mut() {
|
||||
setup.subagents_enabled = !setup.subagents_enabled;
|
||||
self.auto_state.subagents_enabled = setup.subagents_enabled;
|
||||
self.auto_present_setup();
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn auto_setup_select_countdown(&mut self, mode: AutoContinueMode) {
|
||||
if let Some(setup) = self.auto_state.setup.as_mut() {
|
||||
if setup.continue_mode != mode {
|
||||
setup.continue_mode = mode;
|
||||
self.auto_state.continue_mode = mode;
|
||||
self.auto_state.reset_countdown();
|
||||
self.auto_present_setup();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn auto_setup_confirm(&mut self) {
|
||||
if let Some(setup) = self.auto_state.setup.clone() {
|
||||
self.auto_launch_with_setup(setup);
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn auto_setup_cancel(&mut self) {
|
||||
if self.auto_state.setup.is_none() {
|
||||
return;
|
||||
}
|
||||
self.auto_state.reset();
|
||||
self.bottom_pane.clear_auto_coordinator_view();
|
||||
self.bottom_pane.set_standard_terminal_hint(None);
|
||||
self.bottom_pane.update_status_text(String::new());
|
||||
self.bottom_pane.set_task_running(false);
|
||||
self.bottom_pane.ensure_input_focus();
|
||||
self.update_header_border_activation();
|
||||
self.request_redraw();
|
||||
self.push_background_tail("Auto Drive setup canceled.".to_string());
|
||||
}
|
||||
|
||||
fn auto_send_conversation(&mut self) {
|
||||
if !self.auto_state.active || self.auto_state.waiting_for_response {
|
||||
return;
|
||||
@@ -11644,12 +11831,13 @@ fi\n\
|
||||
};
|
||||
self.auto_state.current_cli_prompt = Some(prompt_text.clone());
|
||||
self.auto_state.awaiting_submission = true;
|
||||
self.auto_state.seconds_remaining = AUTO_COUNTDOWN_SECONDS;
|
||||
self.auto_state.reset_countdown();
|
||||
self.auto_state.countdown_id = self.auto_state.countdown_id.wrapping_add(1);
|
||||
let countdown_id = self.auto_state.countdown_id;
|
||||
let countdown_seconds = self.auto_state.countdown_seconds();
|
||||
self.auto_rebuild_live_ring();
|
||||
self.request_redraw();
|
||||
self.auto_start_countdown(countdown_id);
|
||||
self.auto_start_countdown(countdown_id, countdown_seconds);
|
||||
}
|
||||
AutoCoordinatorStatus::Success => {
|
||||
let normalized = summary_text.trim();
|
||||
@@ -11684,29 +11872,34 @@ fi\n\
|
||||
}
|
||||
}
|
||||
|
||||
fn auto_start_countdown(&self, countdown_id: u64) {
|
||||
if AUTO_COUNTDOWN_SECONDS == 0 {
|
||||
let _ = self.app_event_tx.send(AppEvent::AutoCoordinatorCountdown {
|
||||
countdown_id,
|
||||
seconds_left: 0,
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
let tx = self.app_event_tx.clone();
|
||||
std::thread::spawn(move || {
|
||||
let mut remaining = AUTO_COUNTDOWN_SECONDS;
|
||||
while remaining > 0 {
|
||||
std::thread::sleep(std::time::Duration::from_secs(1));
|
||||
remaining -= 1;
|
||||
if !tx.send_with_result(AppEvent::AutoCoordinatorCountdown {
|
||||
fn auto_start_countdown(&self, countdown_id: u64, countdown_seconds: Option<u8>) {
|
||||
match countdown_seconds {
|
||||
Some(0) => {
|
||||
let _ = self.app_event_tx.send(AppEvent::AutoCoordinatorCountdown {
|
||||
countdown_id,
|
||||
seconds_left: remaining,
|
||||
}) {
|
||||
break;
|
||||
}
|
||||
seconds_left: 0,
|
||||
});
|
||||
}
|
||||
});
|
||||
Some(seconds) => {
|
||||
let tx = self.app_event_tx.clone();
|
||||
std::thread::spawn(move || {
|
||||
let mut remaining = seconds;
|
||||
while remaining > 0 {
|
||||
std::thread::sleep(std::time::Duration::from_secs(1));
|
||||
remaining -= 1;
|
||||
if !tx.send_with_result(AppEvent::AutoCoordinatorCountdown {
|
||||
countdown_id,
|
||||
seconds_left: remaining,
|
||||
}) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
None => {
|
||||
// Manual approval mode: do not start countdown automatically.
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn auto_handle_countdown(&mut self, countdown_id: u64, seconds_left: u8) {
|
||||
@@ -11759,8 +11952,9 @@ fi\n\
|
||||
self.auto_state.current_cli_prompt = Some(message.to_string());
|
||||
if self.auto_state.awaiting_submission {
|
||||
self.auto_state.countdown_id = self.auto_state.countdown_id.wrapping_add(1);
|
||||
self.auto_state.seconds_remaining = AUTO_COUNTDOWN_SECONDS;
|
||||
self.auto_start_countdown(self.auto_state.countdown_id);
|
||||
self.auto_state.reset_countdown();
|
||||
let countdown_seconds = self.auto_state.countdown_seconds();
|
||||
self.auto_start_countdown(self.auto_state.countdown_id, countdown_seconds);
|
||||
}
|
||||
let summary = Self::truncate_with_ellipsis(message, 160);
|
||||
self.push_background_tail(format!("Observer replaced prompt with: {summary}"));
|
||||
@@ -11785,6 +11979,10 @@ fi\n\
|
||||
}
|
||||
|
||||
fn auto_handle_post_turn_review(&mut self, cfg: TurnConfig) {
|
||||
if !self.auto_state.review_enabled {
|
||||
self.auto_turn_review_state = None;
|
||||
return;
|
||||
}
|
||||
if cfg.read_only {
|
||||
self.auto_turn_review_state = None;
|
||||
return;
|
||||
@@ -11842,7 +12040,7 @@ fi\n\
|
||||
}
|
||||
|
||||
fn prepare_auto_turn_review_state(&mut self) {
|
||||
if !self.auto_state.active {
|
||||
if !self.auto_state.active || !self.auto_state.review_enabled {
|
||||
self.auto_turn_review_state = None;
|
||||
return;
|
||||
}
|
||||
@@ -11938,11 +12136,12 @@ fi\n\
|
||||
self.auto_state.current_reasoning_title = None;
|
||||
self.auto_state.thinking_prefix_stripped = false;
|
||||
// If coordinator hinted medium/high complexity, prime the Agents HUD
|
||||
let should_prepare_agents = self
|
||||
.pending_auto_turn_config
|
||||
.as_ref()
|
||||
.and_then(|cfg| cfg.complexity)
|
||||
.is_some_and(|c| matches!(c, TurnComplexity::Medium | TurnComplexity::High));
|
||||
let should_prepare_agents = self.auto_state.subagents_enabled
|
||||
&& self
|
||||
.pending_auto_turn_config
|
||||
.as_ref()
|
||||
.and_then(|cfg| cfg.complexity)
|
||||
.is_some_and(|c| matches!(c, TurnComplexity::Medium | TurnComplexity::High));
|
||||
if should_prepare_agents {
|
||||
self.prepare_agents();
|
||||
}
|
||||
@@ -11950,7 +12149,11 @@ fi\n\
|
||||
// Build hidden preface with explicit agent_run guidance
|
||||
let preface = self.build_auto_turn_preface(&prompt);
|
||||
|
||||
self.prepare_auto_turn_review_state();
|
||||
if self.auto_state.review_enabled {
|
||||
self.prepare_auto_turn_review_state();
|
||||
} else {
|
||||
self.auto_turn_review_state = None;
|
||||
}
|
||||
self.bottom_pane.update_status_text(String::new());
|
||||
self.bottom_pane.set_task_running(false);
|
||||
if preface.trim().is_empty() {
|
||||
@@ -11994,7 +12197,7 @@ fi\n\
|
||||
self.auto_state.paused_for_manual_edit = true;
|
||||
self.auto_state.resume_after_manual_submit = true;
|
||||
self.auto_state.countdown_id = self.auto_state.countdown_id.wrapping_add(1);
|
||||
self.auto_state.seconds_remaining = AUTO_COUNTDOWN_SECONDS;
|
||||
self.auto_state.reset_countdown();
|
||||
self.clear_composer();
|
||||
self.insert_str(&full_prompt);
|
||||
self.bottom_pane.ensure_input_focus();
|
||||
@@ -12042,20 +12245,30 @@ fi\n\
|
||||
parts.push("This turn is likely low complexity, so you should focus on addressing it directly without using agents.".to_string());
|
||||
}
|
||||
TurnComplexity::Medium => {
|
||||
parts.push(format!(
|
||||
"This turn is likely medium complexity, so you should use at least 2 agents using `agent_run` and read_only: {}.",
|
||||
ro_str
|
||||
));
|
||||
if self.auto_state.subagents_enabled {
|
||||
parts.push(format!(
|
||||
"This turn is likely medium complexity, so you should use at least 2 agents using `agent_run` and read_only: {}.",
|
||||
ro_str
|
||||
));
|
||||
} else {
|
||||
parts.push("This turn is likely medium complexity; break the work into clear steps and handle them directly without launching sub agents.".to_string());
|
||||
}
|
||||
}
|
||||
TurnComplexity::High => {
|
||||
parts.push(format!(
|
||||
"This turn is likely high complexity, you should use `agent_run` with all models [\"claude\",\"gemini\",\"qwen\",\"code\",\"cloud\"] and read_only: {}.",
|
||||
ro_str
|
||||
));
|
||||
if self.auto_state.subagents_enabled {
|
||||
parts.push(format!(
|
||||
"This turn is likely high complexity, you should use `agent_run` with all models [\"claude\",\"gemini\",\"qwen\",\"code\",\"cloud\"] and read_only: {}.",
|
||||
ro_str
|
||||
));
|
||||
} else {
|
||||
parts.push("This turn is likely high complexity; plan carefully and execute step by step without invoking sub agents.".to_string());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if matches!(complexity, TurnComplexity::Medium | TurnComplexity::High) {
|
||||
if self.auto_state.subagents_enabled
|
||||
&& matches!(complexity, TurnComplexity::Medium | TurnComplexity::High)
|
||||
{
|
||||
if cfg.read_only {
|
||||
parts.push("Use `agent_wait` with `batch_id` and `return_all: true` to wait for all agents to complete automatically. Once they complete you should collate their output into your final output.".to_string());
|
||||
} else {
|
||||
@@ -12111,7 +12324,7 @@ fi\n\
|
||||
self.auto_state.awaiting_submission = false;
|
||||
self.auto_state.paused_for_manual_edit = false;
|
||||
self.auto_state.resume_after_manual_submit = false;
|
||||
self.auto_state.seconds_remaining = AUTO_COUNTDOWN_SECONDS;
|
||||
self.auto_state.reset_countdown();
|
||||
self.auto_state.current_summary = Some(String::new());
|
||||
self.auto_state.current_progress_past = None;
|
||||
self.auto_state.current_progress_current = None;
|
||||
@@ -12140,6 +12353,9 @@ fi\n\
|
||||
}
|
||||
|
||||
fn auto_start_post_turn_review(&mut self, scope: Option<AutoReviewCommitScope>) {
|
||||
if !self.auto_state.review_enabled {
|
||||
return;
|
||||
}
|
||||
let (prompt, hint, auto_metadata, review_metadata, preparation) = match scope {
|
||||
Some(scope) => {
|
||||
let commit_id = scope.commit;
|
||||
@@ -12289,13 +12505,14 @@ fi\n\
|
||||
.clone()
|
||||
.filter(|p| !p.trim().is_empty());
|
||||
|
||||
let countdown = if self.auto_state.awaiting_submission && AUTO_COUNTDOWN_SECONDS > 0 {
|
||||
Some(CountdownState {
|
||||
remaining: self
|
||||
.auto_state
|
||||
.seconds_remaining
|
||||
.min(AUTO_COUNTDOWN_SECONDS),
|
||||
})
|
||||
let countdown_limit = self.auto_state.countdown_seconds();
|
||||
let countdown = if self.auto_state.awaiting_submission {
|
||||
match countdown_limit {
|
||||
Some(limit) if limit > 0 => Some(CountdownState {
|
||||
remaining: self.auto_state.seconds_remaining.min(limit),
|
||||
}),
|
||||
_ => None,
|
||||
}
|
||||
} else {
|
||||
None
|
||||
};
|
||||
@@ -12304,7 +12521,11 @@ fi\n\
|
||||
let label = if self.auto_state.countdown_active() {
|
||||
format!("Continue ({}s)", self.auto_state.seconds_remaining)
|
||||
} else {
|
||||
"Continue now".to_string()
|
||||
match countdown_limit {
|
||||
Some(0) => "Continue now".to_string(),
|
||||
Some(_) => "Continue now".to_string(),
|
||||
None => "Continue".to_string(),
|
||||
}
|
||||
};
|
||||
Some(AutoCoordinatorButton {
|
||||
label,
|
||||
@@ -12326,7 +12547,7 @@ fi\n\
|
||||
String::new()
|
||||
};
|
||||
|
||||
let model = AutoCoordinatorViewModel {
|
||||
let model = AutoCoordinatorViewModel::Active(AutoActiveViewModel {
|
||||
goal: self.auto_state.goal.clone(),
|
||||
status_lines,
|
||||
cli_prompt,
|
||||
@@ -12337,7 +12558,7 @@ fi\n\
|
||||
manual_hint,
|
||||
ctrl_switch_hint,
|
||||
cli_running,
|
||||
};
|
||||
});
|
||||
|
||||
self
|
||||
.bottom_pane
|
||||
|
||||
@@ -1192,7 +1192,7 @@ fn observer_report_replaces_prompt_and_resets_countdown() {
|
||||
|
||||
chat.auto_state.active = true;
|
||||
chat.auto_state.awaiting_submission = true;
|
||||
chat.auto_state.current_prompt = Some("old prompt".to_string());
|
||||
chat.auto_state.current_cli_prompt = Some("old prompt".to_string());
|
||||
chat.auto_state.countdown_id = 7;
|
||||
chat.auto_state.seconds_remaining = 3;
|
||||
|
||||
@@ -1209,7 +1209,7 @@ fn observer_report_replaces_prompt_and_resets_countdown() {
|
||||
Some("double-check the failing command".to_string()),
|
||||
);
|
||||
|
||||
assert_eq!(chat.auto_state.current_prompt.as_deref(), Some("new prompt"));
|
||||
assert_eq!(chat.auto_state.current_cli_prompt.as_deref(), Some("new prompt"));
|
||||
assert_eq!(chat.auto_state.observer_status, AutoObserverStatus::Failing);
|
||||
assert_eq!(
|
||||
chat
|
||||
@@ -1221,7 +1221,8 @@ fn observer_report_replaces_prompt_and_resets_countdown() {
|
||||
telemetry.trigger_count
|
||||
);
|
||||
assert_eq!(chat.auto_state.countdown_id, 8);
|
||||
assert_eq!(chat.auto_state.seconds_remaining, AUTO_COUNTDOWN_SECONDS);
|
||||
let default_seconds = chat.auto_state.countdown_seconds().unwrap_or(0);
|
||||
assert_eq!(chat.auto_state.seconds_remaining, default_seconds);
|
||||
|
||||
let texts = cell_texts(&chat);
|
||||
assert!(texts.iter().any(|line| line.contains("Observer guidance")));
|
||||
@@ -2247,10 +2248,11 @@ fn pump_app_events(
|
||||
fn prepare_auto_drive_for_manual_pause(chat: &mut ChatWidget<'_>) {
|
||||
chat.auto_state.reset();
|
||||
chat.auto_state.active = true;
|
||||
chat.auto_state.review_enabled = true;
|
||||
chat.auto_state.awaiting_submission = true;
|
||||
chat.auto_state.current_cli_prompt = Some("echo hi".to_string());
|
||||
chat.auto_state.goal = Some("demo goal".to_string());
|
||||
chat.auto_state.seconds_remaining = AUTO_COUNTDOWN_SECONDS;
|
||||
chat.auto_state.seconds_remaining = chat.auto_state.countdown_seconds().unwrap_or(0);
|
||||
}
|
||||
|
||||
fn esc_key() -> KeyEvent {
|
||||
@@ -2282,6 +2284,7 @@ fn auto_waits_for_review_before_continuing() {
|
||||
chat.auto_state.waiting_for_response = true;
|
||||
chat.auto_state.coordinator_waiting = true;
|
||||
chat.auto_state.goal = Some("demo goal".to_string());
|
||||
chat.auto_state.review_enabled = true;
|
||||
|
||||
let (tx, auto_rx) = channel();
|
||||
chat.auto_handle = Some(AutoCoordinatorHandle::for_tests(tx));
|
||||
@@ -2383,6 +2386,7 @@ fn auto_resume_clears_review_flag_even_when_waiting_for_response() {
|
||||
chat.auto_state.waiting_for_response = true;
|
||||
chat.auto_state.coordinator_waiting = true;
|
||||
chat.auto_state.goal = Some("demo goal".to_string());
|
||||
chat.auto_state.review_enabled = true;
|
||||
|
||||
chat.auto_resolve_state = Some(AutoResolveState::new(
|
||||
"Review workspace".to_string(),
|
||||
@@ -2409,6 +2413,7 @@ fn auto_blocks_until_auto_resolve_completes() {
|
||||
chat.auto_state.waiting_for_response = true;
|
||||
chat.auto_state.coordinator_waiting = true;
|
||||
chat.auto_state.goal = Some("demo goal".to_string());
|
||||
chat.auto_state.review_enabled = true;
|
||||
chat.config.tui.review_auto_resolve = true;
|
||||
|
||||
let (tx, auto_rx) = channel();
|
||||
|
||||
Reference in New Issue
Block a user