Codex Handbook
tui/src/multi_agents.rs 953 lines
//! Helpers for rendering and navigating multi-agent state in the TUI.//!//! This module owns the shared presentation contracts for multi-agent history rows, `/agent` picker//! entries, and the fast-switch keyboard shortcuts. Higher-level coordination, such as deciding//! which thread becomes active or when a thread closes, stays in [`crate::app::App`].use crate::history_cell::PlainHistoryCell;use crate::render::line_utils::prefix_lines;use crate::text_formatting::truncate_text;use codex_app_server_protocol::CollabAgentState;use codex_app_server_protocol::CollabAgentStatus;use codex_app_server_protocol::CollabAgentTool;use codex_app_server_protocol::CollabAgentToolCallStatus;use codex_app_server_protocol::SubAgentActivityKind;use codex_app_server_protocol::ThreadItem;use codex_protocol::ThreadId;use codex_protocol::openai_models::ReasoningEffort as ReasoningEffortConfig;use crossterm::event::KeyCode;use crossterm::event::KeyEvent;#[cfg(target_os = "macos")]use crossterm::event::KeyEventKind;#[cfg(target_os = "macos")]use crossterm::event::KeyModifiers;use ratatui::style::Stylize;use ratatui::text::Line;use ratatui::text::Span;use std::collections::HashSet;const COLLAB_PROMPT_PREVIEW_GRAPHEMES: usize = 160;const COLLAB_AGENT_ERROR_PREVIEW_GRAPHEMES: usize = 160;const COLLAB_AGENT_RESPONSE_PREVIEW_GRAPHEMES: usize = 240;#[derive(Debug, Clone, PartialEq, Eq)]pub(crate) struct AgentPickerThreadEntry {    /// Human-friendly nickname shown in picker rows and footer labels.    pub(crate) agent_nickname: Option<String>,    /// Agent type shown in brackets when present, for example `worker`.    pub(crate) agent_role: Option<String>,    /// Canonical v2 agent path, when the thread was observed through v2 activity.    pub(crate) agent_path: Option<String>,    /// Whether the latest liveness refresh says the agent thread is actively working.    pub(crate) is_running: bool,    /// Whether the thread has emitted a close event and should render dimmed.    pub(crate) is_closed: bool,}#[derive(Debug, Clone, PartialEq, Eq)]pub(crate) struct SubAgentActivityDisplay {    pub(crate) thread_id: ThreadId,    pub(crate) agent_path: String,    pub(crate) is_running_hint: bool,}#[derive(Clone, Debug, Default, PartialEq, Eq)]pub(crate) struct AgentMetadata {    /// Human-friendly nickname shown in rendered tool-call rows.    pub(crate) agent_nickname: Option<String>,    /// Agent type shown in brackets when present, for example `worker`.    pub(crate) agent_role: Option<String>,}#[derive(Clone, Copy)]struct AgentLabel<'a> {    thread_id: Option<ThreadId>,    nickname: Option<&'a str>,    role: Option<&'a str>,}#[derive(Debug, Clone, PartialEq, Eq)]pub(crate) struct SpawnRequestSummary {    pub(crate) model: String,    pub(crate) reasoning_effort: ReasoningEffortConfig,}pub(crate) fn agent_picker_status_dot_spans(is_closed: bool) -> Vec<Span<'static>> {    let dot = if is_closed {        "•".into()    } else {        "•".green()    };    vec![dot, " ".into()]}pub(crate) fn format_agent_picker_item_name(    agent_nickname: Option<&str>,    agent_role: Option<&str>,    is_primary: bool,) -> String {    if is_primary {        return "Main [default]".to_string();    }    let agent_nickname = agent_nickname        .map(str::trim)        .filter(|nickname| !nickname.is_empty());    let agent_role = agent_role.map(str::trim).filter(|role| !role.is_empty());    match (agent_nickname, agent_role) {        (Some(agent_nickname), Some(agent_role)) => format!("{agent_nickname} [{agent_role}]"),        (Some(agent_nickname), None) => agent_nickname.to_string(),        (None, Some(agent_role)) => format!("[{agent_role}]"),        (None, None) => "Agent".to_string(),    }}pub(crate) fn previous_agent_shortcut() -> crate::key_hint::KeyBinding {    crate::key_hint::alt(KeyCode::Left)}pub(crate) fn next_agent_shortcut() -> crate::key_hint::KeyBinding {    crate::key_hint::alt(KeyCode::Right)}/// Matches the canonical "previous agent" binding plus platform-specific fallbacks that keep agent/// navigation working when enhanced key reporting is unavailable.pub(crate) fn previous_agent_shortcut_matches(    key_event: KeyEvent,    allow_word_motion_fallback: bool,) -> bool {    previous_agent_shortcut().is_press(key_event)        || previous_agent_word_motion_fallback(key_event, allow_word_motion_fallback)}/// Matches the canonical "next agent" binding plus platform-specific fallbacks that keep agent/// navigation working when enhanced key reporting is unavailable.pub(crate) fn next_agent_shortcut_matches(    key_event: KeyEvent,    allow_word_motion_fallback: bool,) -> bool {    next_agent_shortcut().is_press(key_event)        || next_agent_word_motion_fallback(key_event, allow_word_motion_fallback)}#[cfg(target_os = "macos")]fn previous_agent_word_motion_fallback(    key_event: KeyEvent,    allow_word_motion_fallback: bool,) -> bool {    // Some terminals, especially on macOS, send Option+b/f as word-motion keys instead of    // Option+arrow events unless enhanced keyboard reporting is enabled. Callers should only    // enable this fallback when the composer is empty so draft editing retains the expected    // word-wise motion behavior.    allow_word_motion_fallback        && matches!(            key_event,            KeyEvent {                code: KeyCode::Char('b'),                modifiers: KeyModifiers::ALT,                kind: KeyEventKind::Press | KeyEventKind::Repeat,                ..            }        )}#[cfg(not(target_os = "macos"))]fn previous_agent_word_motion_fallback(    _key_event: KeyEvent,    _allow_word_motion_fallback: bool,) -> bool {    false}#[cfg(target_os = "macos")]fn next_agent_word_motion_fallback(key_event: KeyEvent, allow_word_motion_fallback: bool) -> bool {    // Some terminals, especially on macOS, send Option+b/f as word-motion keys instead of    // Option+arrow events unless enhanced keyboard reporting is enabled. Callers should only    // enable this fallback when the composer is empty so draft editing retains the expected    // word-wise motion behavior.    allow_word_motion_fallback        && matches!(            key_event,            KeyEvent {                code: KeyCode::Char('f'),                modifiers: KeyModifiers::ALT,                kind: KeyEventKind::Press | KeyEventKind::Repeat,                ..            }        )}#[cfg(not(target_os = "macos"))]fn next_agent_word_motion_fallback(    _key_event: KeyEvent,    _allow_word_motion_fallback: bool,) -> bool {    false}pub(crate) fn spawn_request_summary(item: &ThreadItem) -> Option<SpawnRequestSummary> {    match item {        ThreadItem::CollabAgentToolCall {            tool: CollabAgentTool::SpawnAgent,            model: Some(model),            reasoning_effort: Some(reasoning_effort),            ..        } => Some(SpawnRequestSummary {            model: model.clone(),            reasoning_effort: reasoning_effort.clone(),        }),        _ => None,    }}pub(crate) fn tool_call_history_cell(    item: &ThreadItem,    cached_spawn_request: Option<&SpawnRequestSummary>,    mut agent_metadata: impl FnMut(ThreadId) -> AgentMetadata,) -> Option<PlainHistoryCell> {    let ThreadItem::CollabAgentToolCall {        tool,        status,        receiver_thread_ids,        prompt,        agents_states,        ..    } = item    else {        return None;    };    let first_receiver = receiver_thread_ids        .first()        .and_then(|id| parse_thread_id(id));    let prompt = prompt.as_deref().unwrap_or_default();    match tool {        CollabAgentTool::SpawnAgent => {            if matches!(status, CollabAgentToolCallStatus::InProgress) {                return None;            }            let fallback_spawn_request = spawn_request_summary(item);            let spawn_request = cached_spawn_request.or(fallback_spawn_request.as_ref());            Some(spawn_end(                first_receiver,                prompt,                spawn_request,                &mut agent_metadata,            ))        }        CollabAgentTool::SendInput => {            if matches!(status, CollabAgentToolCallStatus::InProgress) {                return None;            }            first_receiver.map(|receiver_thread_id| {                interaction_end(receiver_thread_id, prompt, &mut agent_metadata)            })        }        CollabAgentTool::ResumeAgent => first_receiver.map(|receiver_thread_id| {            if matches!(status, CollabAgentToolCallStatus::InProgress) {                resume_begin(receiver_thread_id, &mut agent_metadata)            } else {                let state = first_agent_state(receiver_thread_ids, agents_states);                resume_end(                    receiver_thread_id,                    state,                    "Agent resume failed",                    &mut agent_metadata,                )            }        }),        CollabAgentTool::Wait => {            if matches!(status, CollabAgentToolCallStatus::InProgress) {                Some(waiting_begin(receiver_thread_ids, &mut agent_metadata))            } else {                Some(waiting_end(                    receiver_thread_ids,                    agents_states,                    &mut agent_metadata,                ))            }        }        CollabAgentTool::CloseAgent => {            if matches!(status, CollabAgentToolCallStatus::InProgress) {                return None;            }            first_receiver                .map(|receiver_thread_id| close_end(receiver_thread_id, &mut agent_metadata))        }    }}pub(crate) fn sub_agent_activity_display(item: &ThreadItem) -> Option<SubAgentActivityDisplay> {    let ThreadItem::SubAgentActivity {        kind,        agent_thread_id,        agent_path,        ..    } = item    else {        return None;    };    Some(SubAgentActivityDisplay {        thread_id: parse_thread_id(agent_thread_id)?,        agent_path: agent_path.clone(),        is_running_hint: !matches!(kind, SubAgentActivityKind::Interrupted),    })}pub(crate) fn sub_agent_activity_history_cell(item: &ThreadItem) -> Option<PlainHistoryCell> {    let ThreadItem::SubAgentActivity {        kind, agent_path, ..    } = item    else {        return None;    };    Some(collab_event(        sub_agent_activity_title(*kind, agent_path),        Vec::new(),    ))}pub(crate) fn sub_agent_activity_summary(kind: SubAgentActivityKind, agent_path: &str) -> String {    match kind {        SubAgentActivityKind::Started => format!("Started `{agent_path}`"),        SubAgentActivityKind::Interacted => format!("Interacted with `{agent_path}`"),        SubAgentActivityKind::Interrupted => format!("Interrupted `{agent_path}`"),    }}fn sub_agent_activity_title(kind: SubAgentActivityKind, agent_path: &str) -> Line<'static> {    let (prefix, path) = match kind {        SubAgentActivityKind::Started => ("Started ", agent_path),        SubAgentActivityKind::Interacted => ("Interacted with ", agent_path),        SubAgentActivityKind::Interrupted => ("Interrupted ", agent_path),    };    title_spans_line(vec![        Span::from(prefix).bold(),        Span::from(format!("`{path}`")).cyan(),    ])}fn spawn_end(    new_thread_id: Option<ThreadId>,    prompt: &str,    spawn_request: Option<&SpawnRequestSummary>,    agent_metadata: &mut impl FnMut(ThreadId) -> AgentMetadata,) -> PlainHistoryCell {    let title = match new_thread_id {        Some(thread_id) => title_with_agent(            "Spawned",            agent_label(thread_id, &agent_metadata(thread_id)),            spawn_request,        ),        None => title_text("Agent spawn failed"),    };    let mut details = Vec::new();    if let Some(line) = prompt_line(prompt) {        details.push(line);    }    collab_event(title, details)}fn interaction_end(    receiver_thread_id: ThreadId,    prompt: &str,    agent_metadata: &mut impl FnMut(ThreadId) -> AgentMetadata,) -> PlainHistoryCell {    let title = title_with_agent(        "Sent input to",        agent_label(receiver_thread_id, &agent_metadata(receiver_thread_id)),        /*spawn_request*/ None,    );    let mut details = Vec::new();    if let Some(line) = prompt_line(prompt) {        details.push(line);    }    collab_event(title, details)}fn waiting_begin(    receiver_thread_ids: &[String],    agent_metadata: &mut impl FnMut(ThreadId) -> AgentMetadata,) -> PlainHistoryCell {    let receiver_agents = receiver_thread_ids        .iter()        .filter_map(|thread_id| parse_thread_id(thread_id))        .map(|thread_id| (thread_id, agent_metadata(thread_id)))        .collect::<Vec<_>>();    let title = match receiver_agents.as_slice() {        [(thread_id, metadata)] => title_with_agent(            "Waiting for",            agent_label(*thread_id, metadata),            /*spawn_request*/ None,        ),        [] => title_text("Waiting for agents"),        _ => title_text(format!("Waiting for {} agents", receiver_agents.len())),    };    let details = if receiver_agents.len() > 1 {        receiver_agents            .iter()            .map(|(thread_id, metadata)| agent_label_line(agent_label(*thread_id, metadata)))            .collect()    } else {        Vec::new()    };    collab_event(title, details)}fn waiting_end(    receiver_thread_ids: &[String],    agents_states: &std::collections::HashMap<String, CollabAgentState>,    agent_metadata: &mut impl FnMut(ThreadId) -> AgentMetadata,) -> PlainHistoryCell {    let details = wait_complete_lines(receiver_thread_ids, agents_states, agent_metadata);    collab_event(title_text("Finished waiting"), details)}fn close_end(    receiver_thread_id: ThreadId,    agent_metadata: &mut impl FnMut(ThreadId) -> AgentMetadata,) -> PlainHistoryCell {    collab_event(        title_with_agent(            "Closed",            agent_label(receiver_thread_id, &agent_metadata(receiver_thread_id)),            /*spawn_request*/ None,        ),        Vec::new(),    )}fn resume_begin(    receiver_thread_id: ThreadId,    agent_metadata: &mut impl FnMut(ThreadId) -> AgentMetadata,) -> PlainHistoryCell {    collab_event(        title_with_agent(            "Resuming",            agent_label(receiver_thread_id, &agent_metadata(receiver_thread_id)),            /*spawn_request*/ None,        ),        Vec::new(),    )}fn resume_end(    receiver_thread_id: ThreadId,    status: Option<&CollabAgentState>,    fallback_error: &str,    agent_metadata: &mut impl FnMut(ThreadId) -> AgentMetadata,) -> PlainHistoryCell {    collab_event(        title_with_agent(            "Resumed",            agent_label(receiver_thread_id, &agent_metadata(receiver_thread_id)),            /*spawn_request*/ None,        ),        vec![status_summary_line(status, fallback_error)],    )}fn collab_event(title: Line<'static>, details: Vec<Line<'static>>) -> PlainHistoryCell {    let mut lines: Vec<Line<'static>> = vec![title];    if !details.is_empty() {        lines.extend(prefix_lines(details, "  └ ".dim(), "    ".into()));    }    PlainHistoryCell::new(lines)}fn title_text(title: impl Into<String>) -> Line<'static> {    title_spans_line(vec![Span::from(title.into()).bold()])}fn title_with_agent(    prefix: &str,    agent: AgentLabel<'_>,    spawn_request: Option<&SpawnRequestSummary>,) -> Line<'static> {    let mut spans = vec![Span::from(format!("{prefix} ")).bold()];    spans.extend(agent_label_spans(agent));    spans.extend(spawn_request_spans(spawn_request));    title_spans_line(spans)}fn title_spans_line(mut spans: Vec<Span<'static>>) -> Line<'static> {    let mut title = Vec::with_capacity(spans.len() + 1);    title.push(Span::from("• ").dim());    title.append(&mut spans);    title.into()}fn parse_thread_id(thread_id: &str) -> Option<ThreadId> {    ThreadId::from_string(thread_id).ok()}fn agent_label(thread_id: ThreadId, metadata: &AgentMetadata) -> AgentLabel<'_> {    AgentLabel {        thread_id: Some(thread_id),        nickname: metadata.agent_nickname.as_deref(),        role: metadata.agent_role.as_deref(),    }}fn agent_label_line(agent: AgentLabel<'_>) -> Line<'static> {    agent_label_spans(agent).into()}fn agent_label_spans(agent: AgentLabel<'_>) -> Vec<Span<'static>> {    let mut spans = Vec::new();    let nickname = agent        .nickname        .map(str::trim)        .filter(|nickname| !nickname.is_empty());    let role = agent.role.map(str::trim).filter(|role| !role.is_empty());    if let Some(nickname) = nickname {        spans.push(Span::from(nickname.to_string()).cyan().bold());    } else if let Some(thread_id) = agent.thread_id {        spans.push(Span::from(thread_id.to_string()).cyan());    } else {        spans.push(Span::from("agent").cyan());    }    if let Some(role) = role {        spans.push(Span::from(" ").dim());        spans.push(Span::from(format!("[{role}]")));    }    spans}fn spawn_request_spans(spawn_request: Option<&SpawnRequestSummary>) -> Vec<Span<'static>> {    let Some(spawn_request) = spawn_request else {        return Vec::new();    };    let model = spawn_request.model.trim();    if model.is_empty() && spawn_request.reasoning_effort == ReasoningEffortConfig::default() {        return Vec::new();    }    let details = if model.is_empty() {        format!("({})", spawn_request.reasoning_effort)    } else {        format!("({model} {})", spawn_request.reasoning_effort)    };    vec![Span::from(" ").dim(), Span::from(details).magenta()]}fn prompt_line(prompt: &str) -> Option<Line<'static>> {    let trimmed = prompt.trim();    if trimmed.is_empty() {        None    } else {        Some(Line::from(Span::from(truncate_text(            trimmed,            COLLAB_PROMPT_PREVIEW_GRAPHEMES,        ))))    }}fn wait_complete_lines(    receiver_thread_ids: &[String],    agents_states: &std::collections::HashMap<String, CollabAgentState>,    agent_metadata: &mut impl FnMut(ThreadId) -> AgentMetadata,) -> Vec<Line<'static>> {    let mut seen = HashSet::new();    let mut entries = receiver_thread_ids        .iter()        .filter_map(|thread_id| {            let parsed_thread_id = parse_thread_id(thread_id)?;            let status = agents_states.get(thread_id)?;            seen.insert(parsed_thread_id);            Some((parsed_thread_id, agent_metadata(parsed_thread_id), status))        })        .collect::<Vec<_>>();    let mut extras = agents_states        .iter()        .filter_map(|(thread_id, status)| {            let parsed_thread_id = parse_thread_id(thread_id)?;            (!seen.contains(&parsed_thread_id))                .then(|| (parsed_thread_id, agent_metadata(parsed_thread_id), status))        })        .collect::<Vec<_>>();    extras.sort_by_key(|entry| entry.0.to_string());    entries.extend(extras);    if entries.is_empty() {        vec![Line::from(Span::from("No agents completed yet"))]    } else {        entries            .into_iter()            .map(|(thread_id, metadata, status)| {                let mut spans = agent_label_spans(agent_label(thread_id, &metadata));                spans.push(Span::from(": ").dim());                spans.extend(status_summary_spans(status));                spans.into()            })            .collect()    }}fn first_agent_state<'a>(    receiver_thread_ids: &[String],    agents_states: &'a std::collections::HashMap<String, CollabAgentState>,) -> Option<&'a CollabAgentState> {    receiver_thread_ids        .iter()        .find_map(|thread_id| agents_states.get(thread_id))        .or_else(|| {            agents_states                .iter()                .min_by(|left, right| left.0.cmp(right.0))                .map(|(_, status)| status)        })}fn status_summary_line(status: Option<&CollabAgentState>, fallback_error: &str) -> Line<'static> {    match status {        Some(status) => status_summary_spans(status).into(),        None => error_summary_spans(fallback_error).into(),    }}fn status_summary_spans(status: &CollabAgentState) -> Vec<Span<'static>> {    match status.status {        CollabAgentStatus::PendingInit => vec![Span::from("Pending init").cyan()],        CollabAgentStatus::Running => vec![Span::from("Running").cyan().bold()],        // Allow `.yellow()`        #[allow(clippy::disallowed_methods)]        CollabAgentStatus::Interrupted => vec![Span::from("Interrupted").yellow()],        CollabAgentStatus::Completed => {            let mut spans = vec![Span::from("Completed").green()];            if let Some(message) = status.message.as_ref() {                let message_preview = truncate_text(                    &message.split_whitespace().collect::<Vec<_>>().join(" "),                    COLLAB_AGENT_RESPONSE_PREVIEW_GRAPHEMES,                );                if !message_preview.is_empty() {                    spans.push(Span::from(" - ").dim());                    spans.push(Span::from(message_preview));                }            }            spans        }        CollabAgentStatus::Errored => {            error_summary_spans(status.message.as_deref().unwrap_or("Agent errored"))        }        CollabAgentStatus::Shutdown => vec![Span::from("Shutdown")],        CollabAgentStatus::NotFound => vec![Span::from("Not found").red()],    }}fn error_summary_spans(error: &str) -> Vec<Span<'static>> {    let mut spans = vec![Span::from("Error").red()];    let error_preview = truncate_text(        &error.split_whitespace().collect::<Vec<_>>().join(" "),        COLLAB_AGENT_ERROR_PREVIEW_GRAPHEMES,    );    if !error_preview.is_empty() {        spans.push(Span::from(" - ").dim());        spans.push(Span::from(error_preview));    }    spans}#[cfg(test)]mod tests {    use super::*;    use crate::history_cell::HistoryCell;    #[cfg(target_os = "macos")]    use crossterm::event::KeyEvent;    #[cfg(target_os = "macos")]    use crossterm::event::KeyModifiers;    use insta::assert_snapshot;    use pretty_assertions::assert_eq;    use ratatui::style::Color;    use ratatui::style::Modifier;    use std::collections::HashMap;    #[test]    fn collab_events_snapshot() {        let sender_thread_id = ThreadId::from_string("00000000-0000-0000-0000-000000000001")            .expect("valid sender thread id");        let robie_id = ThreadId::from_string("00000000-0000-0000-0000-000000000002")            .expect("valid robie thread id");        let bob_id = ThreadId::from_string("00000000-0000-0000-0000-000000000003")            .expect("valid bob thread id");        let spawn = tool_call_history_cell(            &ThreadItem::CollabAgentToolCall {                id: "call-spawn".to_string(),                tool: CollabAgentTool::SpawnAgent,                status: CollabAgentToolCallStatus::Completed,                sender_thread_id: sender_thread_id.to_string(),                receiver_thread_ids: vec![robie_id.to_string()],                prompt: Some("Compute 11! and reply with just the integer result.".to_string()),                model: Some("gpt-5".to_string()),                reasoning_effort: Some(ReasoningEffortConfig::High),                agents_states: HashMap::from([(                    robie_id.to_string(),                    agent_state(CollabAgentStatus::PendingInit, /*message*/ None),                )]),            },            /*cached_spawn_request*/ None,            |thread_id| metadata_for(thread_id, robie_id, bob_id),        )        .expect("spawn item renders");        let send = tool_call_history_cell(            &ThreadItem::CollabAgentToolCall {                id: "call-send".to_string(),                tool: CollabAgentTool::SendInput,                status: CollabAgentToolCallStatus::Completed,                sender_thread_id: sender_thread_id.to_string(),                receiver_thread_ids: vec![robie_id.to_string()],                prompt: Some("Please continue and return the answer only.".to_string()),                model: None,                reasoning_effort: None,                agents_states: HashMap::from([(                    robie_id.to_string(),                    agent_state(CollabAgentStatus::Running, /*message*/ None),                )]),            },            /*cached_spawn_request*/ None,            |thread_id| metadata_for(thread_id, robie_id, bob_id),        )        .expect("send-input item renders");        let waiting = tool_call_history_cell(            &ThreadItem::CollabAgentToolCall {                id: "call-wait".to_string(),                tool: CollabAgentTool::Wait,                status: CollabAgentToolCallStatus::InProgress,                sender_thread_id: sender_thread_id.to_string(),                receiver_thread_ids: vec![robie_id.to_string()],                prompt: None,                model: None,                reasoning_effort: None,                agents_states: HashMap::new(),            },            /*cached_spawn_request*/ None,            |thread_id| metadata_for(thread_id, robie_id, bob_id),        )        .expect("wait begin item renders");        let finished = tool_call_history_cell(            &ThreadItem::CollabAgentToolCall {                id: "call-wait".to_string(),                tool: CollabAgentTool::Wait,                status: CollabAgentToolCallStatus::Completed,                sender_thread_id: sender_thread_id.to_string(),                receiver_thread_ids: vec![robie_id.to_string(), bob_id.to_string()],                prompt: None,                model: None,                reasoning_effort: None,                agents_states: HashMap::from([                    (                        robie_id.to_string(),                        agent_state(CollabAgentStatus::Completed, Some("39916800")),                    ),                    (                        bob_id.to_string(),                        agent_state(CollabAgentStatus::Errored, Some("tool timeout")),                    ),                ]),            },            /*cached_spawn_request*/ None,            |thread_id| metadata_for(thread_id, robie_id, bob_id),        )        .expect("wait end item renders");        let close = tool_call_history_cell(            &ThreadItem::CollabAgentToolCall {                id: "call-close".to_string(),                tool: CollabAgentTool::CloseAgent,                status: CollabAgentToolCallStatus::Completed,                sender_thread_id: sender_thread_id.to_string(),                receiver_thread_ids: vec![robie_id.to_string()],                prompt: None,                model: None,                reasoning_effort: None,                agents_states: HashMap::from([(                    robie_id.to_string(),                    agent_state(CollabAgentStatus::Completed, Some("39916800")),                )]),            },            /*cached_spawn_request*/ None,            |thread_id| metadata_for(thread_id, robie_id, bob_id),        )        .expect("close item renders");        let snapshot = [spawn, send, waiting, finished, close]            .iter()            .map(cell_to_text)            .collect::<Vec<_>>()            .join("\n\n");        assert_snapshot!("collab_agent_transcript", snapshot);    }    #[cfg(target_os = "macos")]    #[test]    fn agent_shortcut_matches_option_arrow_word_motion_fallbacks_only_when_allowed() {        assert!(previous_agent_shortcut_matches(            KeyEvent::new(KeyCode::Left, KeyModifiers::ALT),            /*allow_word_motion_fallback*/ false,        ));        assert!(next_agent_shortcut_matches(            KeyEvent::new(KeyCode::Right, KeyModifiers::ALT),            /*allow_word_motion_fallback*/ false,        ));        assert!(previous_agent_shortcut_matches(            KeyEvent::new(KeyCode::Char('b'), KeyModifiers::ALT),            /*allow_word_motion_fallback*/ true,        ));        assert!(next_agent_shortcut_matches(            KeyEvent::new(KeyCode::Char('f'), KeyModifiers::ALT),            /*allow_word_motion_fallback*/ true,        ));        assert!(!previous_agent_shortcut_matches(            KeyEvent::new(KeyCode::Char('b'), KeyModifiers::ALT),            /*allow_word_motion_fallback*/ false,        ));        assert!(!next_agent_shortcut_matches(            KeyEvent::new(KeyCode::Char('f'), KeyModifiers::ALT),            /*allow_word_motion_fallback*/ false,        ));    }    #[cfg(not(target_os = "macos"))]    #[test]    fn agent_shortcut_matches_option_arrows_only() {        assert!(previous_agent_shortcut_matches(            KeyEvent::new(KeyCode::Left, crossterm::event::KeyModifiers::ALT,),            /*allow_word_motion_fallback*/ false        ));        assert!(next_agent_shortcut_matches(            KeyEvent::new(KeyCode::Right, crossterm::event::KeyModifiers::ALT,),            /*allow_word_motion_fallback*/ false        ));        assert!(!previous_agent_shortcut_matches(            KeyEvent::new(KeyCode::Char('b'), crossterm::event::KeyModifiers::ALT,),            /*allow_word_motion_fallback*/ false        ));        assert!(!next_agent_shortcut_matches(            KeyEvent::new(KeyCode::Char('f'), crossterm::event::KeyModifiers::ALT,),            /*allow_word_motion_fallback*/ false        ));    }    #[test]    fn title_styles_nickname_and_role() {        let sender_thread_id = ThreadId::from_string("00000000-0000-0000-0000-000000000001")            .expect("valid sender thread id");        let robie_id = ThreadId::from_string("00000000-0000-0000-0000-000000000002")            .expect("valid robie thread id");        let cell = tool_call_history_cell(            &ThreadItem::CollabAgentToolCall {                id: "call-spawn".to_string(),                tool: CollabAgentTool::SpawnAgent,                status: CollabAgentToolCallStatus::Completed,                sender_thread_id: sender_thread_id.to_string(),                receiver_thread_ids: vec![robie_id.to_string()],                prompt: Some(String::new()),                model: Some("gpt-5".to_string()),                reasoning_effort: Some(ReasoningEffortConfig::High),                agents_states: HashMap::from([(                    robie_id.to_string(),                    agent_state(CollabAgentStatus::PendingInit, /*message*/ None),                )]),            },            /*cached_spawn_request*/ None,            |thread_id| metadata_for(thread_id, robie_id, ThreadId::new()),        )        .expect("spawn item renders");        let lines = cell.display_lines(/*width*/ 200);        let title = &lines[0];        assert_eq!(title.spans[2].content.as_ref(), "Robie");        assert_eq!(title.spans[2].style.fg, Some(Color::Cyan));        assert!(title.spans[2].style.add_modifier.contains(Modifier::BOLD));        assert_eq!(title.spans[4].content.as_ref(), "[explorer]");        assert_eq!(title.spans[4].style.fg, None);        assert!(!title.spans[4].style.add_modifier.contains(Modifier::DIM));        assert_eq!(title.spans[6].content.as_ref(), "(gpt-5 high)");        assert_eq!(title.spans[6].style.fg, Some(Color::Magenta));    }    #[test]    fn collab_resume_interrupted_snapshot() {        let sender_thread_id = ThreadId::from_string("00000000-0000-0000-0000-000000000001")            .expect("valid sender thread id");        let robie_id = ThreadId::from_string("00000000-0000-0000-0000-000000000002")            .expect("valid robie thread id");        let cell = tool_call_history_cell(            &ThreadItem::CollabAgentToolCall {                id: "call-resume".to_string(),                tool: CollabAgentTool::ResumeAgent,                status: CollabAgentToolCallStatus::Completed,                sender_thread_id: sender_thread_id.to_string(),                receiver_thread_ids: vec![robie_id.to_string()],                prompt: None,                model: None,                reasoning_effort: None,                agents_states: HashMap::from([(                    robie_id.to_string(),                    agent_state(CollabAgentStatus::Interrupted, /*message*/ None),                )]),            },            /*cached_spawn_request*/ None,            |thread_id| metadata_for(thread_id, robie_id, ThreadId::new()),        )        .expect("resume item renders");        assert_snapshot!("collab_resume_interrupted", cell_to_text(&cell));    }    fn agent_state(status: CollabAgentStatus, message: Option<&str>) -> CollabAgentState {        CollabAgentState {            status,            message: message.map(str::to_string),        }    }    fn metadata_for(thread_id: ThreadId, robie_id: ThreadId, bob_id: ThreadId) -> AgentMetadata {        if thread_id == robie_id {            AgentMetadata {                agent_nickname: Some("Robie".to_string()),                agent_role: Some("explorer".to_string()),            }        } else if thread_id == bob_id {            AgentMetadata {                agent_nickname: Some("Bob".to_string()),                agent_role: Some("worker".to_string()),            }        } else {            AgentMetadata::default()        }    }    fn cell_to_text(cell: &PlainHistoryCell) -> String {        cell.display_lines(/*width*/ 200)            .iter()            .map(line_to_text)            .collect::<Vec<_>>()            .join("\n")    }    fn line_to_text(line: &Line<'static>) -> String {        line.spans            .iter()            .map(|span| span.content.as_ref())            .collect::<Vec<_>>()            .join("")    }}