Sweep every first-party crate source (1956 .rs files) to the project comment guidelines: delete redundant restatements, decorative banners, change narration, and end-of-line comments; keep and tighten the crucial ones (invariants, bug rationale, SAFETY blocks, ported-source attribution). No functional code changed. Every edit is proven comment-only against the prior tree by a comment-stripping lexer (string/char/raw-string aware) plus a separate doctest-fence check. Where removing a comment made rustfmt or clippy want to re-lay-out adjacent code, the minimal triggering comment is restored so code tokens stay byte-identical. Gates green: cargo fmt --all --check (0 diffs), cargo check and cargo clippy --workspace --all-targets (0 warnings). Adds scripts/check_codegen_comment_guidelines.py — the enforcement gate for these guidelines (flags banners, end-of-line comments, change narration, and commented-out code).
315 lines
11 KiB
Rust
315 lines
11 KiB
Rust
//! SubagentBlock — scrollback entries for subagent lifecycle.
|
|
//!
|
|
//! Similar to BgTaskBlock: always collapsed, animated bullet while running,
|
|
//! colored bullet when done. Enter / Ctrl-F opens the subagent view.
|
|
//!
|
|
//! Two modes:
|
|
//! - **Blocking** (sync): Single `Started` block. Blinks while running,
|
|
//! turns green/red when done. Text: `Subagent "description"`
|
|
//! - **Background** (async): `Started` block stays forever (turns gray).
|
|
//! A separate `Completed`/`Failed` block is added when done.
|
|
//! Started text: `Subagent started: "description"`
|
|
//! Completed text: `Subagent completed in 43s: "description"`
|
|
|
|
use std::time::Duration;
|
|
|
|
use ratatui::style::Modifier;
|
|
use ratatui::text::{Line, Span};
|
|
use unicode_width::UnicodeWidthStr;
|
|
|
|
use crate::app::subagent::format_subagent_meta;
|
|
use crate::render::color::blend_color;
|
|
use crate::render::line_utils::truncate_str;
|
|
use crate::scrollback::block::BlockContent;
|
|
use crate::scrollback::types::{AccentStyle, BlockContext, BlockOutput, DisplayMode};
|
|
use crate::theme::Theme;
|
|
use crate::util::format_duration;
|
|
|
|
/// What kind of subagent lifecycle event this block represents.
|
|
#[derive(Debug, Clone)]
|
|
pub enum SubagentBlockKind {
|
|
/// Subagent is running (or was running — `finish_running` stops animation).
|
|
Started,
|
|
/// Subagent completed successfully.
|
|
Completed { elapsed: Duration },
|
|
/// Subagent failed.
|
|
Failed {
|
|
elapsed: Duration,
|
|
error: Option<String>,
|
|
},
|
|
/// Subagent was cancelled.
|
|
Cancelled { elapsed: Duration },
|
|
}
|
|
|
|
/// Subagent scrollback block.
|
|
///
|
|
/// Always collapsed, not foldable, groupable, selectable.
|
|
/// Enter / Ctrl-F opens the subagent view.
|
|
#[derive(Debug, Clone)]
|
|
pub struct SubagentBlock {
|
|
pub description: String,
|
|
/// Child session ID (for opening the subagent view).
|
|
pub child_session_id: String,
|
|
/// Subagent type (e.g. "general-purpose", "explore").
|
|
pub subagent_type: String,
|
|
pub persona: Option<String>,
|
|
/// Role that supplied defaults for this subagent, if any.
|
|
pub role: Option<String>,
|
|
pub model: Option<String>,
|
|
pub is_background: bool,
|
|
pub kind: SubagentBlockKind,
|
|
/// Live activity label from the child session's turn tracker.
|
|
///
|
|
/// Updated on each `SubagentProgress` tick while the subagent is running.
|
|
/// Shown inline in the collapsed scrollback line (e.g. "Thinking",
|
|
/// "Running: cargo build") so the user sees interactive progress without
|
|
/// opening the subagent view.
|
|
pub activity_label: Option<String>,
|
|
}
|
|
|
|
impl SubagentBlock {
|
|
/// Create a "Subagent started" block (for both sync and async).
|
|
pub fn started(
|
|
description: impl Into<String>,
|
|
child_session_id: impl Into<String>,
|
|
subagent_type: impl Into<String>,
|
|
persona: Option<String>,
|
|
role: Option<String>,
|
|
model: Option<String>,
|
|
is_background: bool,
|
|
) -> Self {
|
|
Self {
|
|
description: description.into(),
|
|
child_session_id: child_session_id.into(),
|
|
subagent_type: subagent_type.into(),
|
|
persona,
|
|
role,
|
|
model,
|
|
is_background,
|
|
kind: SubagentBlockKind::Started,
|
|
activity_label: None,
|
|
}
|
|
}
|
|
|
|
/// Create a "Subagent completed" block (background mode only).
|
|
pub fn completed(
|
|
description: impl Into<String>,
|
|
child_session_id: impl Into<String>,
|
|
elapsed: Duration,
|
|
) -> Self {
|
|
Self {
|
|
description: description.into(),
|
|
child_session_id: child_session_id.into(),
|
|
subagent_type: String::new(),
|
|
persona: None,
|
|
role: None,
|
|
model: None,
|
|
is_background: true,
|
|
kind: SubagentBlockKind::Completed { elapsed },
|
|
activity_label: None,
|
|
}
|
|
}
|
|
|
|
/// Create a "Subagent failed" block (background mode only).
|
|
pub fn failed(
|
|
description: impl Into<String>,
|
|
child_session_id: impl Into<String>,
|
|
elapsed: Duration,
|
|
error: Option<String>,
|
|
) -> Self {
|
|
Self {
|
|
description: description.into(),
|
|
child_session_id: child_session_id.into(),
|
|
subagent_type: String::new(),
|
|
persona: None,
|
|
role: None,
|
|
model: None,
|
|
is_background: true,
|
|
kind: SubagentBlockKind::Failed { elapsed, error },
|
|
activity_label: None,
|
|
}
|
|
}
|
|
|
|
/// Create a "Subagent cancelled" block (background mode only).
|
|
pub fn cancelled(
|
|
description: impl Into<String>,
|
|
child_session_id: impl Into<String>,
|
|
elapsed: Duration,
|
|
) -> Self {
|
|
Self {
|
|
description: description.into(),
|
|
child_session_id: child_session_id.into(),
|
|
subagent_type: String::new(),
|
|
persona: None,
|
|
role: None,
|
|
model: None,
|
|
is_background: true,
|
|
kind: SubagentBlockKind::Cancelled { elapsed },
|
|
activity_label: None,
|
|
}
|
|
}
|
|
|
|
pub fn is_running(&self) -> bool {
|
|
matches!(self.kind, SubagentBlockKind::Started)
|
|
}
|
|
}
|
|
|
|
fn quoted_desc(desc: &str, max_width: usize) -> String {
|
|
// Reserve 2 chars for quotes
|
|
if max_width <= 2 {
|
|
// "…"
|
|
return "\u{201C}\u{2026}\u{201D}".to_string();
|
|
}
|
|
let inner = truncate_str(desc, max_width - 2);
|
|
format!("\u{201C}{inner}\u{201D}")
|
|
}
|
|
|
|
impl BlockContent for SubagentBlock {
|
|
fn output(&self, ctx: &BlockContext) -> BlockOutput {
|
|
let theme = Theme::current();
|
|
// When selected, lift only the bold "Subagent" label to
|
|
// `text_primary` so it reads as undimmed (mirrors `read.rs` /
|
|
// `search.rs`, which bump only the label and leave the rest at
|
|
// `muted`). The detail text (verb + description + meta) stays
|
|
// muted in every state.
|
|
let bold = if ctx.is_selected {
|
|
theme.primary().add_modifier(Modifier::BOLD)
|
|
} else {
|
|
theme.muted().add_modifier(Modifier::BOLD)
|
|
};
|
|
let muted = theme.muted();
|
|
let w = ctx.width as usize;
|
|
|
|
let line = match (&self.kind, self.is_background) {
|
|
(SubagentBlockKind::Started, bg) => {
|
|
let verb = if bg { "started: " } else { "running: " };
|
|
let activity_suffix: String = self
|
|
.activity_label
|
|
.as_deref()
|
|
.filter(|s| !s.is_empty())
|
|
.map(|a| format!(" \u{2014} {a}"))
|
|
.unwrap_or_default();
|
|
let meta = format_subagent_meta(
|
|
self.persona.as_deref(),
|
|
self.role.as_deref(),
|
|
self.model.as_deref(),
|
|
);
|
|
// "Subagent running: " / "Subagent started: " = 18 chars
|
|
let overhead = 18 + meta.width() + activity_suffix.width();
|
|
let desc = quoted_desc(&self.description, w.saturating_sub(overhead));
|
|
let mut spans = vec![
|
|
Span::styled("Subagent ", bold),
|
|
Span::styled(verb, muted),
|
|
Span::styled(desc, muted),
|
|
];
|
|
if !activity_suffix.is_empty() {
|
|
spans.push(Span::styled(activity_suffix, muted));
|
|
}
|
|
spans.push(Span::styled(meta, muted));
|
|
Line::from(spans)
|
|
}
|
|
(SubagentBlockKind::Completed { elapsed }, _) => {
|
|
let time_str = format_duration(*elapsed);
|
|
// "Subagent completed in Xs: " = 26 + time_str.len()
|
|
let prefix_len = 26 + time_str.len();
|
|
let desc = quoted_desc(&self.description, w.saturating_sub(prefix_len));
|
|
Line::from(vec![
|
|
Span::styled("Subagent ", bold),
|
|
Span::styled(format!("completed in {time_str}: "), muted),
|
|
Span::styled(desc, muted),
|
|
])
|
|
}
|
|
(SubagentBlockKind::Failed { elapsed, error }, _) => {
|
|
let time_str = format_duration(*elapsed);
|
|
let detail = error
|
|
.as_deref()
|
|
.map(|e| format!(" ({e})"))
|
|
.unwrap_or_default();
|
|
let prefix_len = 21 + time_str.len() + detail.len();
|
|
let desc = quoted_desc(&self.description, w.saturating_sub(prefix_len));
|
|
Line::from(vec![
|
|
Span::styled("Subagent ", bold),
|
|
Span::styled(format!("failed in {time_str}{detail}: "), muted),
|
|
Span::styled(desc, muted),
|
|
])
|
|
}
|
|
(SubagentBlockKind::Cancelled { elapsed }, _) => {
|
|
let time_str = format_duration(*elapsed);
|
|
// "Subagent cancelled in Xs: " = 26 + time_str.len()
|
|
let prefix_len = 26 + time_str.len();
|
|
let desc = quoted_desc(&self.description, w.saturating_sub(prefix_len));
|
|
Line::from(vec![
|
|
Span::styled("Subagent ", bold),
|
|
Span::styled(format!("cancelled in {time_str}: "), muted),
|
|
Span::styled(desc, muted),
|
|
])
|
|
}
|
|
};
|
|
|
|
BlockOutput {
|
|
lines: vec![line.into()],
|
|
}
|
|
}
|
|
|
|
fn accent(&self, ctx: &BlockContext) -> Option<AccentStyle> {
|
|
let theme = Theme::current();
|
|
match &self.kind {
|
|
SubagentBlockKind::Started if ctx.is_running => {
|
|
Some(AccentStyle::static_color(theme.accent_running))
|
|
}
|
|
_ => None,
|
|
}
|
|
}
|
|
|
|
fn bullet(&self, ctx: &BlockContext) -> Option<AccentStyle> {
|
|
let theme = Theme::current();
|
|
match &self.kind {
|
|
SubagentBlockKind::Started => {
|
|
if ctx.is_running {
|
|
let dim = ctx.appearance.scrollback.display.dim_accent;
|
|
let dimmed = blend_color(theme.bg_base, theme.accent_running, dim)
|
|
.unwrap_or(theme.accent_running);
|
|
Some(AccentStyle::animated(dimmed))
|
|
} else {
|
|
// Finished — gray bullet (same as bg task "started" after completion)
|
|
None
|
|
}
|
|
}
|
|
SubagentBlockKind::Completed { .. } => {
|
|
Some(AccentStyle::static_color(theme.accent_success))
|
|
}
|
|
SubagentBlockKind::Failed { .. } | SubagentBlockKind::Cancelled { .. } => {
|
|
Some(AccentStyle::static_color(theme.accent_error))
|
|
}
|
|
}
|
|
}
|
|
|
|
fn has_vpad(&self, _ctx: &BlockContext) -> bool {
|
|
false
|
|
}
|
|
|
|
fn has_raw_mode(&self) -> bool {
|
|
false
|
|
}
|
|
|
|
fn is_foldable(&self) -> bool {
|
|
false
|
|
}
|
|
|
|
fn default_display_mode(&self) -> DisplayMode {
|
|
DisplayMode::Collapsed
|
|
}
|
|
|
|
fn is_selectable(&self) -> bool {
|
|
true
|
|
}
|
|
|
|
fn has_bullet(&self, _ctx: &BlockContext) -> bool {
|
|
true
|
|
}
|
|
|
|
fn is_groupable(&self) -> bool {
|
|
true
|
|
}
|
|
}
|