Codex Handbook
tui/src/app/agent_status_feed.rs 211 lines
//! Bounded, best-effort previews for the v2 `/agent` status output.use super::ThreadBufferedEvent;use super::ThreadEventStore;use crate::history_cell::HistoryCell;use crate::history_cell::plain_lines;use crate::text_formatting::truncate_text;use codex_app_server_protocol::CollabAgentTool;use codex_app_server_protocol::ServerNotification;use codex_app_server_protocol::SubAgentActivityKind;use codex_app_server_protocol::ThreadItem;use ratatui::style::Stylize;use ratatui::text::Line;use std::collections::HashSet;const AGENT_STATUS_PREVIEW_LINES: usize = 3;const AGENT_STATUS_PREVIEW_ITEMS: usize = 6;const AGENT_STATUS_PREVIEW_GRAPHEMES: usize = 240;const AGENT_STATUS_PREVIEW_INDENT: u16 = 4;#[derive(Debug)]pub(super) struct AgentStatusHistoryCell {    entries: Vec<AgentStatusThreadPreview>,}impl AgentStatusHistoryCell {    pub(super) fn new(entries: Vec<AgentStatusThreadPreview>) -> Self {        Self { entries }    }}impl HistoryCell for AgentStatusHistoryCell {    fn display_lines(&self, width: u16) -> Vec<Line<'static>> {        let mut lines: Vec<Line<'static>> = vec![            "/agent".magenta().into(),            "Sub-agents running".bold().into(),            "".into(),        ];        if self.entries.is_empty() {            lines.push("  • No sub-agents running.".italic().into());            return lines;        }        for entry in &self.entries {            lines.push(entry.title_line());            let preview_width = width.saturating_sub(AGENT_STATUS_PREVIEW_INDENT).max(1);            let preview_lines = entry.preview_lines(preview_width);            if preview_lines.is_empty() {                lines.push(vec!["    ".into(), "No recent activity yet.".dim().italic()].into());            } else {                lines.extend(preview_lines.into_iter().map(indent_preview_line));            }            lines.push("".into());        }        let _ = lines.pop();        lines    }    fn raw_lines(&self) -> Vec<Line<'static>> {        plain_lines(self.display_lines(u16::MAX))    }}#[derive(Debug)]pub(super) struct AgentStatusThreadPreview {    agent_path: String,    activity: Vec<String>,}impl AgentStatusThreadPreview {    pub(super) fn from_store(agent_path: String, store: &ThreadEventStore) -> Self {        Self::from_events(agent_path, store.buffer.iter().rev())    }    pub(super) fn empty(agent_path: String) -> Self {        Self::from_events(agent_path, std::iter::empty())    }    fn from_events<'a>(        agent_path: String,        events: impl Iterator<Item = &'a ThreadBufferedEvent>,    ) -> Self {        let mut seen_item_ids = HashSet::new();        let mut activity = Vec::new();        for event in events {            let item = match event {                ThreadBufferedEvent::Notification(ServerNotification::ItemCompleted(event)) => {                    &event.item                }                ThreadBufferedEvent::Notification(ServerNotification::ItemStarted(event)) => {                    &event.item                }                ThreadBufferedEvent::Notification(_)                | ThreadBufferedEvent::Request(_)                | ThreadBufferedEvent::HistoryEntryResponse(_)                | ThreadBufferedEvent::FeedbackSubmission(_) => continue,            };            if !seen_item_ids.insert(item.id().to_string()) {                continue;            }            if let Some(summary) = activity_summary(item) {                activity.push(summary);                if activity.len() == AGENT_STATUS_PREVIEW_ITEMS {                    break;                }            }        }        activity.reverse();        Self {            agent_path,            activity,        }    }    fn title_line(&self) -> Line<'static> {        vec!["  • ".dim(), format!("`{}`", self.agent_path).cyan()].into()    }    fn preview_lines(&self, width: u16) -> Vec<Line<'static>> {        let mut lines = self            .activity            .iter()            .flat_map(|activity| textwrap::wrap(activity, width as usize))            .filter(|line| !line.trim().is_empty())            .map(|line| line.into_owned().dim().into())            .collect::<Vec<_>>();        if lines.len() > AGENT_STATUS_PREVIEW_LINES {            lines.drain(..lines.len() - AGENT_STATUS_PREVIEW_LINES);        }        lines    }}fn activity_summary(item: &ThreadItem) -> Option<String> {    let summary = match item {        ThreadItem::AgentMessage { text, .. } | ThreadItem::Plan { text, .. } => text,        ThreadItem::Reasoning { summary, .. } => summary.last()?,        ThreadItem::CommandExecution { command, .. } => {            let command = truncate_text(                command,                AGENT_STATUS_PREVIEW_GRAPHEMES.saturating_sub("$ ".len()),            );            return bounded_summary(&format!("$ {command}"));        }        ThreadItem::FileChange { changes, .. } => {            return bounded_summary(&format!("Updated {} file(s)", changes.len()));        }        ThreadItem::McpToolCall { server, tool, .. } => {            return bounded_summary(&format!("MCP {server}/{tool}"));        }        ThreadItem::DynamicToolCall {            namespace, tool, ..        } => {            let tool = namespace                .as_ref()                .map(|namespace| format!("{namespace}/{tool}"))                .unwrap_or_else(|| tool.clone());            return bounded_summary(&format!("Tool {tool}"));        }        ThreadItem::CollabAgentToolCall { tool, .. } => {            let action = match tool {                CollabAgentTool::SpawnAgent => "Spawned an agent",                CollabAgentTool::SendInput => "Sent input to an agent",                CollabAgentTool::ResumeAgent => "Resumed an agent",                CollabAgentTool::Wait => "Waited for an agent",                CollabAgentTool::CloseAgent => "Closed an agent",            };            return Some(action.to_string());        }        ThreadItem::SubAgentActivity {            kind, agent_path, ..        } => {            let action = match kind {                SubAgentActivityKind::Started => "Started",                SubAgentActivityKind::Interacted => "Contacted",                SubAgentActivityKind::Interrupted => "Interrupted",            };            return bounded_summary(&format!("{action} {agent_path}"));        }        ThreadItem::WebSearch { query, .. } => {            return bounded_summary(&format!("Web search: {query}"));        }        ThreadItem::ImageView { path, .. } => {            return bounded_summary(&format!("Viewed {}", path.display()));        }        ThreadItem::ImageGeneration { .. } => return Some("Generated an image".to_string()),        ThreadItem::EnteredReviewMode { .. } => return Some("Entered review mode".to_string()),        ThreadItem::ExitedReviewMode { .. } => return Some("Exited review mode".to_string()),        ThreadItem::ContextCompaction { .. } => return Some("Compacted context".to_string()),        ThreadItem::UserMessage { .. }        | ThreadItem::HookPrompt { .. }        | ThreadItem::Sleep { .. } => return None,    };    bounded_summary(summary)}fn bounded_summary(summary: &str) -> Option<String> {    let summary = truncate_text(summary, AGENT_STATUS_PREVIEW_GRAPHEMES);    let summary = summary.split_whitespace().collect::<Vec<_>>().join(" ");    (!summary.is_empty()).then_some(summary)}fn indent_preview_line(mut line: Line<'static>) -> Line<'static> {    line.spans.insert(0, "    ".into());    line}#[cfg(test)]#[path = "agent_status_feed_tests.rs"]mod tests;