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);
}
}
AppEvent::AutoCoordinatorThinking { delta } => {
if let AppState::Chat { widget } = &mut self.app_state {
widget.auto_handle_thinking(delta);
}
}
AppEvent::AutoCoordinatorCountdown { countdown_id, seconds_left } => {
if let AppState::Chat { widget } = &mut self.app_state {
widget.auto_handle_countdown(countdown_id, seconds_left);
+3
View File
@@ -132,6 +132,9 @@ pub(crate) enum AppEvent {
thoughts: String,
prompt: Option<String>,
},
AutoCoordinatorThinking {
delta: String,
},
AutoCoordinatorCountdown {
countdown_id: u64,
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[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) {
lines.push(Line::default());
lines.push(hint_line);
}
+74 -51
View File
@@ -452,6 +452,7 @@ struct AutoCoordinatorUiState {
countdown_id: u64,
seconds_remaining: u8,
awaiting_goal_input: bool,
last_broadcast_thought: Option<String>,
}
impl AutoCoordinatorUiState {
@@ -6711,7 +6712,6 @@ impl ChatWidget<'_> {
ok
);
self.seed_stream_order_key(StreamKind::Reasoning, &id, ok);
// Stream reasoning delta through StreamController
streaming::delta_text(
self,
StreamKind::Reasoning,
@@ -6838,6 +6838,7 @@ impl ChatWidget<'_> {
self.maybe_hide_spinner();
self.emit_turn_complete_notification(last_agent_message);
self.mark_needs_redraw();
}
EventMsg::AgentReasoningRawContentDelta(AgentReasoningRawContentDeltaEvent {
delta,
@@ -9424,7 +9425,9 @@ impl ChatWidget<'_> {
self.auto_state
.current_thoughts
.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.waiting_for_response = true;
self.header_wave.set_enabled(true, Instant::now());
self.auto_rebuild_live_ring();
self.push_background_tail(format!("Auto Drive started: {goal_text}"));
@@ -9449,6 +9452,12 @@ impl ChatWidget<'_> {
.is_err()
{
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;
}
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.resume_after_manual_submit = 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) {
if !self.auto_state.active {
return;
@@ -9567,6 +9583,8 @@ impl ChatWidget<'_> {
self.auto_state.paused_for_manual_edit = false;
self.auto_state.resume_after_manual_submit = false;
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.set_task_running(false);
self.submit_text_message(prompt);
@@ -9742,56 +9760,74 @@ impl ChatWidget<'_> {
.send(AppEvent::ScheduleFrameIn(Duration::from_millis(interval)));
}
fn auto_collect_thinking_titles(
cell: &history_cell::CollapsibleReasoningCell,
) -> 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 {
fn auto_broadcast_thoughts(&mut self, raw: &str) {
if !self.auto_state.active {
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;
};
if self
.auto_state
.current_thoughts
.last_broadcast_thought
.as_ref()
.map(|existing| existing == &joined)
.map(|prev| prev == &first_line)
.unwrap_or(false)
{
return;
}
if let Some(first_line) = joined.lines().next().map(|l| l.trim()).filter(|l| !l.is_empty()) {
self.push_background_tail(format!("Auto Drive: {first_line}"));
self.auto_state.last_broadcast_thought = Some(first_line.clone());
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 {
@@ -12065,13 +12101,10 @@ impl ChatWidget<'_> {
);
reasoning_cell.append_lines_dedup(lines);
reasoning_cell.set_in_progress(true);
let updated_titles =
Self::auto_collect_thinking_titles(&*reasoning_cell);
self.invalidate_height_cache();
self.autoscroll_if_near_bottom();
self.request_redraw();
self.refresh_reasoning_collapsed_visibility();
self.auto_update_thinking_titles_if_new(updated_titles);
return;
}
}
@@ -12095,13 +12128,10 @@ impl ChatWidget<'_> {
);
reasoning_cell.append_lines_dedup(lines);
reasoning_cell.set_in_progress(true);
let updated_titles =
Self::auto_collect_thinking_titles(&*reasoning_cell);
self.invalidate_height_cache();
self.autoscroll_if_near_bottom();
self.request_redraw();
self.refresh_reasoning_collapsed_visibility();
self.auto_update_thinking_titles_if_new(updated_titles);
return;
}
} else {
@@ -12143,14 +12173,7 @@ impl ChatWidget<'_> {
if let Some(rid) = id {
self.reasoning_index.insert(rid, idx);
}
if let Some(reasoning_cell) = self.history_cells[idx]
.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);
}
// Auto Drive status updates are handled via coordinator decisions.
}
StreamKind::Answer => {
tracing::debug!(
@@ -126,6 +126,7 @@ fn run_auto_loop(
&developer_intro,
&schema,
conv,
&app_event_tx,
) {
Ok((status, thoughts, prompt_opt)) => {
let event = AppEvent::AutoCoordinatorDecision {
@@ -199,6 +200,7 @@ fn request_coordinator_decision(
developer_intro: &str,
schema: &Value,
mut conversation: Vec<ResponseItem>,
app_event_tx: &AppEventSender,
) -> Result<(AutoCoordinatorStatus, String, Option<String>)> {
let mut prompt = Prompt::default();
prompt.store = true;
@@ -215,7 +217,7 @@ fn request_coordinator_decision(
.unwrap_or_else(|| derive_default_model_family(MODEL_SLUG));
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)?;
debug!("[Auto coordinator] model decision: {:?}", value);
@@ -249,8 +251,10 @@ fn request_decision(
runtime: &tokio::runtime::Runtime,
client: &ModelClient,
prompt: &Prompt,
app_event_tx: &AppEventSender,
) -> 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 out = String::new();
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,
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)),
Span::styled(
" Experimental",
Style::default()
.fg(crate::colors::primary())
.add_modifier(Modifier::BOLD),
Style::default().fg(crate::colors::primary()),
),
]));