Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
27 changes: 27 additions & 0 deletions code-rs/code-auto-drive-core/src/auto_coordinator.rs
Original file line number Diff line number Diff line change
Expand Up @@ -214,6 +214,7 @@ pub enum AutoCoordinatorEvent {
Decision {
seq: u64,
status: AutoCoordinatorStatus,
input_required: bool,
status_title: Option<String>,
status_sent_to_user: Option<String>,
goal: Option<String>,
Expand Down Expand Up @@ -295,6 +296,7 @@ pub enum AutoCoordinatorCommand {
struct PendingDecision {
seq: u64,
status: AutoCoordinatorStatus,
input_required: bool,
status_title: Option<String>,
status_sent_to_user: Option<String>,
goal: Option<String>,
Expand All @@ -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,
Expand Down Expand Up @@ -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"));
Expand Down Expand Up @@ -890,6 +897,8 @@ mod tests {
struct CoordinatorDecisionNew {
finish_status: String,
#[serde(default)]
input_required: bool,
#[serde(default)]
status_title: Option<String>,
#[serde(default)]
status_sent_to_user: Option<String>,
Expand Down Expand Up @@ -978,6 +987,7 @@ struct CoordinatorDecisionLegacy {

struct ParsedCoordinatorDecision {
status: AutoCoordinatorStatus,
input_required: bool,
status_title: Option<String>,
status_sent_to_user: Option<String>,
cli: Option<CliAction>,
Expand Down Expand Up @@ -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()),
Expand Down Expand Up @@ -1305,6 +1316,7 @@ fn run_auto_loop(
) {
Ok(ParsedCoordinatorDecision {
status,
input_required,
status_title,
status_sent_to_user,
goal,
Expand Down Expand Up @@ -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."
Expand Down Expand Up @@ -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(),
Expand All @@ -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(),
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -2899,6 +2923,7 @@ fn convert_decision_new(
) -> Result<ParsedCoordinatorDecision> {
let CoordinatorDecisionNew {
finish_status: _,
input_required,
status_title,
status_sent_to_user,
progress,
Expand Down Expand Up @@ -2988,6 +3013,7 @@ fn convert_decision_new(

Ok(ParsedCoordinatorDecision {
status,
input_required,
status_title,
status_sent_to_user,
cli,
Expand Down Expand Up @@ -3037,6 +3063,7 @@ fn convert_decision_legacy(

Ok(ParsedCoordinatorDecision {
status,
input_required: false,
status_title,
status_sent_to_user,
cli,
Expand Down
42 changes: 32 additions & 10 deletions code-rs/code-auto-drive-core/src/controller.rs
Original file line number Diff line number Diff line change
Expand Up @@ -301,6 +301,7 @@ pub struct AutoDriveController {
pub countdown_decision_seq: u64,
pub seconds_remaining: u8,
pub countdown_override: Option<u8>,
pub input_required: bool,
pub last_broadcast_summary: Option<String>,
pub last_decision_summary: Option<String>,
pub last_decision_status_sent_to_user: Option<String>,
Expand Down Expand Up @@ -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 });
}

Expand Down Expand Up @@ -660,10 +662,12 @@ impl AutoDriveController {
decision_seq: u64,
prompt_text: String,
countdown_override: Option<u8>,
input_required: bool,
) -> Vec<AutoControllerEffect> {
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;
Expand All @@ -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
}
Expand All @@ -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;
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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);
Expand All @@ -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);
Expand All @@ -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 { .. })));
}
}
1 change: 1 addition & 0 deletions code-rs/core/prompt_coordinator.md
Original file line number Diff line number Diff line change
Expand Up @@ -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. |
Expand Down
1 change: 1 addition & 0 deletions code-rs/exec/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
2 changes: 2 additions & 0 deletions code-rs/tui/src/app/events.rs
Original file line number Diff line number Diff line change
Expand Up @@ -899,6 +899,7 @@ impl App<'_> {
AppEvent::AutoCoordinatorDecision {
seq,
status,
input_required,
status_title,
status_sent_to_user,
goal,
Expand All @@ -911,6 +912,7 @@ impl App<'_> {
widget.auto_handle_decision(
seq,
status,
input_required,
status_title,
status_sent_to_user,
goal,
Expand Down
1 change: 1 addition & 0 deletions code-rs/tui/src/app_event.rs
Original file line number Diff line number Diff line change
Expand Up @@ -178,6 +178,7 @@ pub(crate) enum AppEvent {
AutoCoordinatorDecision {
seq: u64,
status: AutoCoordinatorStatus,
input_required: bool,
status_title: Option<String>,
status_sent_to_user: Option<String>,
goal: Option<String>,
Expand Down
13 changes: 12 additions & 1 deletion code-rs/tui/src/bottom_pane/auto_coordinator_view.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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 {
Expand Down Expand Up @@ -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);
Expand Down
Loading