diff --git a/src/app/review/mod.rs b/src/app/review/mod.rs index ddf26e1..77b8f0d 100644 --- a/src/app/review/mod.rs +++ b/src/app/review/mod.rs @@ -368,6 +368,7 @@ pub fn trigger_review(state: &mut AppStateRest) { SubagentEvent::StepFailed { step, error } => { tracing::warn!("[review] step {} failed: {}", step, error); } + SubagentEvent::Progress(_) => {} SubagentEvent::Completed { .. } => { tracing::debug!("[review] completed"); } diff --git a/src/app/subagent/engine.rs b/src/app/subagent/engine.rs index e8f64cd..c5c484a 100644 --- a/src/app/subagent/engine.rs +++ b/src/app/subagent/engine.rs @@ -296,6 +296,17 @@ fn generate_workspace_tree(roots: &[std::path::PathBuf]) -> String { out } +fn format_subagent_progress(prefix: &str, text: &str) -> String { + let lines: Vec<&str> = text.lines().filter(|l| !l.trim().is_empty()).collect(); + if lines.is_empty() { + format!("{prefix}...") + } else if lines.len() == 1 { + format!("{prefix}: {}", lines[0]) + } else { + lines[lines.len() - 2..].join("\n") + } +} + /// Synchronous subagent entry point: run up to `ctx.max_steps` iterations /// of the LLM tool loop. /// @@ -369,6 +380,10 @@ pub fn run_subagent(ctx: &SubagentContext, tx: &mpsc::Sender) -> anyhow::bail!("subagent aborted by parent at step {step}"); } + let tx_clone = tx.clone(); + let mut current_thinking = String::new(); + let mut current_token = String::new(); + // Use streaming API so the abort flag is checked per SSE event, // making the subagent responsive to cancellation even during an // LLM call (non-streaming would block for 10-30s unchecked). @@ -377,13 +392,24 @@ pub fn run_subagent(ctx: &SubagentContext, tx: &mpsc::Sender) -> tdefs_opt.clone(), Some(0.7), Some(4096), - |_event| -> bool { + |event| -> bool { // Check abort on every SSE event for responsive cancellation. if ctx.abort_flag.as_ref().is_some_and(|f| f.load(std::sync::atomic::Ordering::SeqCst)) { return false; // signals provider to abort } - // We don't stream tokens to the UI for subagents — just - // need the assembled message at the end. + match event { + crate::app::runtime::stream::StreamEvent::Reasoning(text) => { + current_thinking.push_str(text); + let prog = format_subagent_progress("thinking", ¤t_thinking); + let _ = tx_clone.blocking_send(SubagentEvent::Progress(prog)); + } + crate::app::runtime::stream::StreamEvent::Token(text) => { + current_token.push_str(text); + let prog = format_subagent_progress("replying", ¤t_token); + let _ = tx_clone.blocking_send(SubagentEvent::Progress(prog)); + } + _ => {} + } true }, ); diff --git a/src/app/subagent/event.rs b/src/app/subagent/event.rs index e3f7f87..ff70b74 100644 --- a/src/app/subagent/event.rs +++ b/src/app/subagent/event.rs @@ -31,4 +31,5 @@ pub enum SubagentEvent { #[allow(dead_code)] output: String, }, + Progress(String), } diff --git a/src/app/workflow/engine.rs b/src/app/workflow/engine.rs index 7a91fe9..98a740b 100644 --- a/src/app/workflow/engine.rs +++ b/src/app/workflow/engine.rs @@ -238,6 +238,21 @@ fn spawn_single_agent( SubagentEvent::StepFailed { step, error } => { tracing::warn!("[subagent] step {} failed: {}", step, error); } + SubagentEvent::Progress(prog) => { + if let Some(ref f) = drain_live { + f( + drain_agent_id.clone(), + drain_agent_name.clone(), + AgentStatus { + state: AgentState::Running, + started_at: Some(drain_started_at), + completed_at: None, + error: None, + progress: Some(prog.clone()), + }, + ); + } + } SubagentEvent::Completed { .. } => { tracing::debug!("[subagent] completed"); } diff --git a/src/view/workflow.rs b/src/view/workflow.rs index 20c42c5..adf3fb7 100644 --- a/src/view/workflow.rs +++ b/src/view/workflow.rs @@ -141,10 +141,12 @@ pub fn draw_workflow_panel(frame: &mut Frame, area: Rect, state: &crate::app::st Span::styled(err.clone(), Style::default().fg(Theme::ERROR)), ])); } else if let Some(ref prog) = agent.status.progress { - card_lines.push(Line::from(vec![ - Span::styled(" ", Style::default()), - Span::styled(prog.clone(), Style::default().fg(Theme::TEXT_DIM).add_modifier(Modifier::ITALIC)), - ])); + for line in prog.lines().take(2) { + card_lines.push(Line::from(vec![ + Span::styled(" ", Style::default()), + Span::styled(line.to_string(), Style::default().fg(Theme::TEXT_DIM).add_modifier(Modifier::ITALIC)), + ])); + } } }