返回 CodeWhale
agent_activity.rs
根目录 / crates / tui / src / tui / history / agent_activity.rs
1 //! Compact transcript rendering for agent and activity metadata cells.
2
3 use ratatui::style::Style;
4 use ratatui::text::{Line, Span};
5
6 use codewhale_palette as palette;
7
8 use super::{
9 GenericToolCell, render_tool_header_with_family_and_summary, tool_status_label, truncate_text,
10 };
11
12 pub(super) fn render_agent_compact(cell: &GenericToolCell, low_motion: bool) -> Vec<Line<'static>> {
13 let family = crate::tui::widgets::tool_card::ToolFamily::Delegate;
14 let agent_id = cell
15 .output
16 .as_deref()
17 .and_then(extract_agent_id)
18 .map(str::to_string)
19 .unwrap_or_else(|| delegate_identity_fallback(cell));
20 // Inspections and joins must not draw the same "delegate done" line as a
21 // spawn — during a fan-out session every peek/status/wait would otherwise
22 // read as yet another completed delegate (#4112, dogfood A5). The action
23 // is stamped at the front of the args summary by tool_routing.
24 let state_label = match agent_inspection_action(cell) {
25 Some(AgentCompactAction::Check) => match cell.status {
26 super::ToolStatus::Running => "checking",
27 _ => "checked",
28 },
29 Some(AgentCompactAction::Wait) => match cell.status {
30 super::ToolStatus::Running => "waiting",
31 _ => "waited",
32 },
33 None => tool_status_label(cell.status),
34 };
35 vec![render_tool_header_with_family_and_summary(
36 family,
37 Some(agent_id.as_str()),
38 state_label,
39 cell.status,
40 None,
41 low_motion,
42 )]
43 }
44
45 enum AgentCompactAction {
46 /// Read-only inspection: peek / status / progress / list / inspect.
47 Check,
48 /// Blocking join: wait / join / await / block.
49 Wait,
50 }
51
52 /// Whether this `agent` cell is a read-only inspection or join rather than a
53 /// spawn — those stay compact even in Transcript mode.
54 pub(super) fn is_agent_inspection(cell: &GenericToolCell) -> bool {
55 agent_inspection_action(cell).is_some()
56 }
57
58 fn agent_inspection_action(cell: &GenericToolCell) -> Option<AgentCompactAction> {
59 let summary = cell.input_summary.as_deref()?;
60 let action = summary.strip_prefix("action:")?.trim_start();
61 let action = action.split_whitespace().next().unwrap_or("");
62 match action.trim_end_matches(',') {
63 "peek" | "progress" | "status" | "list" | "inspect" => Some(AgentCompactAction::Check),
64 "wait" | "join" | "await" | "block" => Some(AgentCompactAction::Wait),
65 _ => None,
66 }
67 }
68
69 pub(super) fn render_activity_group(cell: &GenericToolCell, width: u16) -> Vec<Line<'static>> {
70 let summary = cell.input_summary.as_deref().unwrap_or("Updated metadata");
71 let suffix = cell
72 .output_summary
73 .as_deref()
74 .map_or(" ›".to_string(), |count| format!(" {count} ›"));
75 let budget = usize::from(width)
76 .saturating_sub(unicode_width::UnicodeWidthStr::width(suffix.as_str()))
77 .max(1);
78 vec![Line::from(Span::styled(
79 format!("{}{suffix}", truncate_text(summary, budget)),
80 Style::default().fg(palette::TEXT_MUTED),
81 ))]
82 }
83
84 fn delegate_identity_fallback(cell: &GenericToolCell) -> String {
85 if let Some(summary) = cell.input_summary.as_deref() {
86 let summary = summary.trim();
87 if let Some(rest) = summary.strip_prefix("role:") {
88 let role = rest.split_whitespace().next().unwrap_or(rest).trim();
89 if !role.is_empty() {
90 return role.to_string();
91 }
92 }
93 if let Some(rest) = summary.strip_prefix("prompt:") {
94 let title = rest.trim();
95 if !title.is_empty() {
96 let slug: String = title
97 .chars()
98 .take(24)
99 .map(|ch| {
100 if ch.is_ascii_alphanumeric() {
101 ch.to_ascii_lowercase()
102 } else {
103 '-'
104 }
105 })
106 .collect();
107 let slug = slug.trim_matches('-');
108 if !slug.is_empty() {
109 return slug.to_string();
110 }
111 }
112 }
113 }
114 // #4148: never surface the raw internal fallback token ("unknown child")
115 // in the default transcript. When we can't resolve a concrete role, slug,
116 // or agent id, a friendly, non-leaky label reads best next to the
117 // "delegate" verb ("delegate running · subagent").
118 "subagent".to_string()
119 }
120
121 pub(super) fn extract_agent_id(output: &str) -> Option<&str> {
122 let key = "\"agent_id\"";
123 let key_idx = output.find(key)?;
124 let rest = &output[key_idx + key.len()..];
125 let colon = rest.find(':')?;
126 let after_colon = rest[colon + 1..].trim_start();
127 let after_colon = after_colon.strip_prefix('"')?;
128 let end = after_colon.find('"')?;
129 let id = &after_colon[..end];
130 (!id.is_empty()).then_some(id)
131 }
132
132 lines RUST