feat(auto-drive): stream coordinator reasoning

This commit is contained in:
James Peter
2025-09-28 08:52:29 +10:00
parent 529c85f740
commit fc8da8cf54
7 changed files with 99 additions and 57 deletions
+5
View File
@@ -1619,6 +1619,11 @@ impl App<'_> {
widget.auto_handle_decision(status, thoughts, prompt); widget.auto_handle_decision(status, thoughts, prompt);
} }
} }
AppEvent::AutoCoordinatorThinking { delta } => {
if let AppState::Chat { widget } = &mut self.app_state {
widget.auto_handle_thinking(delta);
}
}
AppEvent::AutoCoordinatorCountdown { countdown_id, seconds_left } => { AppEvent::AutoCoordinatorCountdown { countdown_id, seconds_left } => {
if let AppState::Chat { widget } = &mut self.app_state { if let AppState::Chat { widget } = &mut self.app_state {
widget.auto_handle_countdown(countdown_id, seconds_left); widget.auto_handle_countdown(countdown_id, seconds_left);
+3
View File
@@ -132,6 +132,9 @@ pub(crate) enum AppEvent {
thoughts: String, thoughts: String,
prompt: Option<String>, prompt: Option<String>,
}, },
AutoCoordinatorThinking {
delta: String,
},
AutoCoordinatorCountdown { AutoCoordinatorCountdown {
countdown_id: u64, countdown_id: u64,
seconds_left: u8, seconds_left: u8,
+5
View File
@@ -70,3 +70,8 @@ pub fn next_auto_drive_phrase() -> &'static str {
% AUTO_DRIVE_PHRASES.len(); % AUTO_DRIVE_PHRASES.len();
AUTO_DRIVE_PHRASES[idx] AUTO_DRIVE_PHRASES[idx]
} }
pub fn is_auto_drive_phrase(candidate: &str) -> bool {
let trimmed = candidate.trim();
AUTO_DRIVE_PHRASES.iter().any(|phrase| phrase == &trimmed)
}
@@ -325,7 +325,6 @@ impl AutoCoordinatorView {
} }
if let Some(hint_line) = self.manual_hint_line(ctx) { if let Some(hint_line) = self.manual_hint_line(ctx) {
lines.push(Line::default());
lines.push(hint_line); lines.push(hint_line);
} }
+74 -51
View File
@@ -452,6 +452,7 @@ struct AutoCoordinatorUiState {
countdown_id: u64, countdown_id: u64,
seconds_remaining: u8, seconds_remaining: u8,
awaiting_goal_input: bool, awaiting_goal_input: bool,
last_broadcast_thought: Option<String>,
} }
impl AutoCoordinatorUiState { impl AutoCoordinatorUiState {
@@ -6711,7 +6712,6 @@ impl ChatWidget<'_> {
ok ok
); );
self.seed_stream_order_key(StreamKind::Reasoning, &id, ok); self.seed_stream_order_key(StreamKind::Reasoning, &id, ok);
// Stream reasoning delta through StreamController
streaming::delta_text( streaming::delta_text(
self, self,
StreamKind::Reasoning, StreamKind::Reasoning,
@@ -6838,6 +6838,7 @@ impl ChatWidget<'_> {
self.maybe_hide_spinner(); self.maybe_hide_spinner();
self.emit_turn_complete_notification(last_agent_message); self.emit_turn_complete_notification(last_agent_message);
self.mark_needs_redraw(); self.mark_needs_redraw();
} }
EventMsg::AgentReasoningRawContentDelta(AgentReasoningRawContentDeltaEvent { EventMsg::AgentReasoningRawContentDelta(AgentReasoningRawContentDeltaEvent {
delta, delta,
@@ -9424,7 +9425,9 @@ impl ChatWidget<'_> {
self.auto_state self.auto_state
.current_thoughts .current_thoughts
.replace(auto_drive_strings::next_auto_drive_phrase().to_string()); .replace(auto_drive_strings::next_auto_drive_phrase().to_string());
self.auto_state.last_broadcast_thought = None;
self.auto_state.seconds_remaining = AUTO_COUNTDOWN_SECONDS; self.auto_state.seconds_remaining = AUTO_COUNTDOWN_SECONDS;
self.auto_state.waiting_for_response = true;
self.header_wave.set_enabled(true, Instant::now()); self.header_wave.set_enabled(true, Instant::now());
self.auto_rebuild_live_ring(); self.auto_rebuild_live_ring();
self.push_background_tail(format!("Auto Drive started: {goal_text}")); self.push_background_tail(format!("Auto Drive started: {goal_text}"));
@@ -9449,6 +9452,12 @@ impl ChatWidget<'_> {
.is_err() .is_err()
{ {
self.auto_stop(Some("Coordinator stopped unexpectedly.".to_string())); self.auto_stop(Some("Coordinator stopped unexpectedly.".to_string()));
} else {
self.auto_state.waiting_for_response = true;
self.auto_state.current_thoughts = None;
self.auto_state.last_broadcast_thought = None;
self.auto_rebuild_live_ring();
self.request_redraw();
} }
} }
@@ -9462,7 +9471,7 @@ impl ChatWidget<'_> {
return; return;
} }
self.auto_state.current_thoughts = Some(thoughts.clone()); self.auto_on_reasoning_final(&thoughts);
self.auto_state.paused_for_manual_edit = false; self.auto_state.paused_for_manual_edit = false;
self.auto_state.resume_after_manual_submit = false; self.auto_state.resume_after_manual_submit = false;
self.auto_state.awaiting_submission = false; self.auto_state.awaiting_submission = false;
@@ -9549,6 +9558,13 @@ impl ChatWidget<'_> {
} }
} }
pub(crate) fn auto_handle_thinking(&mut self, delta: String) {
if !self.auto_state.active {
return;
}
self.auto_on_reasoning_delta(&delta);
}
fn auto_submit_prompt(&mut self) { fn auto_submit_prompt(&mut self) {
if !self.auto_state.active { if !self.auto_state.active {
return; return;
@@ -9567,6 +9583,8 @@ impl ChatWidget<'_> {
self.auto_state.paused_for_manual_edit = false; self.auto_state.paused_for_manual_edit = false;
self.auto_state.resume_after_manual_submit = false; self.auto_state.resume_after_manual_submit = false;
self.auto_state.seconds_remaining = 0; self.auto_state.seconds_remaining = 0;
self.auto_state.current_thoughts = None;
self.auto_state.last_broadcast_thought = None;
self.bottom_pane.update_status_text(String::new()); self.bottom_pane.update_status_text(String::new());
self.bottom_pane.set_task_running(false); self.bottom_pane.set_task_running(false);
self.submit_text_message(prompt); self.submit_text_message(prompt);
@@ -9742,56 +9760,74 @@ impl ChatWidget<'_> {
.send(AppEvent::ScheduleFrameIn(Duration::from_millis(interval))); .send(AppEvent::ScheduleFrameIn(Duration::from_millis(interval)));
} }
fn auto_collect_thinking_titles( fn auto_broadcast_thoughts(&mut self, raw: &str) {
cell: &history_cell::CollapsibleReasoningCell, if !self.auto_state.active {
) -> Option<String> {
let mut titles: Vec<String> = cell
.display_lines()
.into_iter()
.map(|line| {
line.spans
.iter()
.map(|span| span.content.as_ref())
.collect::<String>()
.trim()
.to_string()
})
.filter(|text| !text.is_empty())
.collect();
if titles.is_empty() {
return None;
}
titles.reverse();
Some(titles.join("\n"))
}
fn auto_update_thinking_titles_if_new(&mut self, joined: Option<String>) {
if !self.auto_state.active || !self.auto_state.waiting_for_response {
return; return;
} }
let Some(joined) = joined else { let Some(first_line) = raw
.lines()
.find_map(|line| {
let trimmed = line.trim();
(!trimmed.is_empty()).then_some(trimmed.to_string())
})
else {
return; return;
}; };
if self if self
.auto_state .auto_state
.current_thoughts .last_broadcast_thought
.as_ref() .as_ref()
.map(|existing| existing == &joined) .map(|prev| prev == &first_line)
.unwrap_or(false) .unwrap_or(false)
{ {
return; return;
} }
if let Some(first_line) = joined.lines().next().map(|l| l.trim()).filter(|l| !l.is_empty()) { self.auto_state.last_broadcast_thought = Some(first_line.clone());
self.push_background_tail(format!("Auto Drive: {first_line}")); self.push_background_tail(format!("Auto Drive: {first_line}"));
}
fn auto_on_reasoning_delta(&mut self, delta: &str) {
if !self.auto_state.active || delta.trim().is_empty() {
return;
}
let payload = {
let entry = self
.auto_state
.current_thoughts
.get_or_insert_with(String::new);
if auto_drive_strings::is_auto_drive_phrase(entry) {
entry.clear();
}
entry.push_str(delta);
entry.clone()
};
self.auto_broadcast_thoughts(&payload);
if self.auto_state.waiting_for_response {
self.auto_rebuild_live_ring();
self.request_redraw();
}
}
fn auto_on_reasoning_final(&mut self, text: &str) {
if !self.auto_state.active {
return;
}
self.auto_state.current_thoughts = Some(text.to_string());
self.auto_broadcast_thoughts(text);
if self.auto_state.waiting_for_response {
self.auto_rebuild_live_ring();
self.request_redraw();
} }
self.auto_state.current_thoughts = Some(joined);
self.auto_rebuild_live_ring();
self.request_redraw();
} }
fn truncate_with_ellipsis(text: &str, max_chars: usize) -> String { fn truncate_with_ellipsis(text: &str, max_chars: usize) -> String {
@@ -12065,13 +12101,10 @@ impl ChatWidget<'_> {
); );
reasoning_cell.append_lines_dedup(lines); reasoning_cell.append_lines_dedup(lines);
reasoning_cell.set_in_progress(true); reasoning_cell.set_in_progress(true);
let updated_titles =
Self::auto_collect_thinking_titles(&*reasoning_cell);
self.invalidate_height_cache(); self.invalidate_height_cache();
self.autoscroll_if_near_bottom(); self.autoscroll_if_near_bottom();
self.request_redraw(); self.request_redraw();
self.refresh_reasoning_collapsed_visibility(); self.refresh_reasoning_collapsed_visibility();
self.auto_update_thinking_titles_if_new(updated_titles);
return; return;
} }
} }
@@ -12095,13 +12128,10 @@ impl ChatWidget<'_> {
); );
reasoning_cell.append_lines_dedup(lines); reasoning_cell.append_lines_dedup(lines);
reasoning_cell.set_in_progress(true); reasoning_cell.set_in_progress(true);
let updated_titles =
Self::auto_collect_thinking_titles(&*reasoning_cell);
self.invalidate_height_cache(); self.invalidate_height_cache();
self.autoscroll_if_near_bottom(); self.autoscroll_if_near_bottom();
self.request_redraw(); self.request_redraw();
self.refresh_reasoning_collapsed_visibility(); self.refresh_reasoning_collapsed_visibility();
self.auto_update_thinking_titles_if_new(updated_titles);
return; return;
} }
} else { } else {
@@ -12143,14 +12173,7 @@ impl ChatWidget<'_> {
if let Some(rid) = id { if let Some(rid) = id {
self.reasoning_index.insert(rid, idx); self.reasoning_index.insert(rid, idx);
} }
if let Some(reasoning_cell) = self.history_cells[idx] // Auto Drive status updates are handled via coordinator decisions.
.as_any()
.downcast_ref::<history_cell::CollapsibleReasoningCell>()
{
let updated_titles =
Self::auto_collect_thinking_titles(reasoning_cell);
self.auto_update_thinking_titles_if_new(updated_titles);
}
} }
StreamKind::Answer => { StreamKind::Answer => {
tracing::debug!( tracing::debug!(
@@ -126,6 +126,7 @@ fn run_auto_loop(
&developer_intro, &developer_intro,
&schema, &schema,
conv, conv,
&app_event_tx,
) { ) {
Ok((status, thoughts, prompt_opt)) => { Ok((status, thoughts, prompt_opt)) => {
let event = AppEvent::AutoCoordinatorDecision { let event = AppEvent::AutoCoordinatorDecision {
@@ -199,6 +200,7 @@ fn request_coordinator_decision(
developer_intro: &str, developer_intro: &str,
schema: &Value, schema: &Value,
mut conversation: Vec<ResponseItem>, mut conversation: Vec<ResponseItem>,
app_event_tx: &AppEventSender,
) -> Result<(AutoCoordinatorStatus, String, Option<String>)> { ) -> Result<(AutoCoordinatorStatus, String, Option<String>)> {
let mut prompt = Prompt::default(); let mut prompt = Prompt::default();
prompt.store = true; prompt.store = true;
@@ -215,7 +217,7 @@ fn request_coordinator_decision(
.unwrap_or_else(|| derive_default_model_family(MODEL_SLUG)); .unwrap_or_else(|| derive_default_model_family(MODEL_SLUG));
prompt.model_family_override = Some(family); prompt.model_family_override = Some(family);
let raw = request_decision(runtime, client, &prompt)?; let raw = request_decision(runtime, client, &prompt, app_event_tx)?;
let (decision, value) = parse_decision(&raw)?; let (decision, value) = parse_decision(&raw)?;
debug!("[Auto coordinator] model decision: {:?}", value); debug!("[Auto coordinator] model decision: {:?}", value);
@@ -249,8 +251,10 @@ fn request_decision(
runtime: &tokio::runtime::Runtime, runtime: &tokio::runtime::Runtime,
client: &ModelClient, client: &ModelClient,
prompt: &Prompt, prompt: &Prompt,
app_event_tx: &AppEventSender,
) -> Result<String> { ) -> Result<String> {
runtime.block_on(async { let tx = app_event_tx.clone();
runtime.block_on(async move {
let mut stream = client.stream(prompt).await?; let mut stream = client.stream(prompt).await?;
let mut out = String::new(); let mut out = String::new();
while let Some(ev) = stream.next().await { while let Some(ev) = stream.next().await {
@@ -265,6 +269,11 @@ fn request_decision(
} }
} }
} }
Ok(ResponseEvent::ReasoningSummaryDelta { delta, .. })
| Ok(ResponseEvent::ReasoningContentDelta { delta, .. }) => {
let message = strip_role_prefix(&delta).to_string();
tx.send(AppEvent::AutoCoordinatorThinking { delta: message });
}
Ok(ResponseEvent::Completed { .. }) => break, Ok(ResponseEvent::Completed { .. }) => break,
Err(err) => return Err(anyhow!("model stream error: {err}")), Err(err) => return Err(anyhow!("model stream error: {err}")),
_ => {} _ => {}
+1 -3
View File
@@ -2419,9 +2419,7 @@ fn popular_commands_lines(_latest_version: Option<&str>) -> Vec<Line<'static>> {
.style(Style::default().add_modifier(Modifier::DIM)), .style(Style::default().add_modifier(Modifier::DIM)),
Span::styled( Span::styled(
" Experimental", " Experimental",
Style::default() Style::default().fg(crate::colors::primary()),
.fg(crate::colors::primary())
.add_modifier(Modifier::BOLD),
), ),
])); ]));