diff --git a/codex-rs/tui/src/chatwidget.rs b/codex-rs/tui/src/chatwidget.rs index 129e8c2667..46663655ce 100644 --- a/codex-rs/tui/src/chatwidget.rs +++ b/codex-rs/tui/src/chatwidget.rs @@ -24,6 +24,7 @@ use codex_core::protocol::AgentReasoningRawContentDeltaEvent; use codex_core::protocol::AgentReasoningRawContentEvent; use codex_core::protocol::ApplyPatchApprovalRequestEvent; use codex_core::protocol::BackgroundEventEvent; +use codex_core::protocol::CollabInteractionEvent; use codex_core::protocol::CreditsSnapshot; use codex_core::protocol::DeprecationNoticeEvent; use codex_core::protocol::ErrorEvent; @@ -102,6 +103,7 @@ use crate::bottom_pane::SelectionViewParams; use crate::bottom_pane::custom_prompt_view::CustomPromptView; use crate::bottom_pane::popup_consts::standard_popup_hint_line; use crate::clipboard_paste::paste_image_to_temp_png; +use crate::collab_event_cell; use crate::diff_render::display_path_for; use crate::exec_cell::CommandOutput; use crate::exec_cell::ExecCell; @@ -1073,6 +1075,11 @@ impl ChatWidget { self.set_status_header(message); } + fn on_collab_interaction(&mut self, event: CollabInteractionEvent) { + self.add_to_history(collab_event_cell::new_collab_interaction(event)); + self.request_redraw(); + } + fn on_undo_started(&mut self, event: UndoStartedEvent) { self.bottom_pane.ensure_status_indicator(); self.bottom_pane.set_interrupt_hint_visible(false); @@ -2182,9 +2189,7 @@ impl ChatWidget { } EventMsg::ExitedReviewMode(review) => self.on_exited_review_mode(review), EventMsg::ContextCompacted(_) => self.on_agent_message("Context compacted".to_owned()), - EventMsg::CollabInteraction(_) => { - // TODO(jif) handle collab tools. - } + EventMsg::CollabInteraction(event) => self.on_collab_interaction(event), EventMsg::ThreadRolledBack(_) => {} EventMsg::RawResponseItem(_) | EventMsg::ItemStarted(_) diff --git a/codex-rs/tui/src/collab_event_cell.rs b/codex-rs/tui/src/collab_event_cell.rs new file mode 100644 index 0000000000..6d577e1768 --- /dev/null +++ b/codex-rs/tui/src/collab_event_cell.rs @@ -0,0 +1,140 @@ +use codex_core::protocol::AgentStatus; +use codex_core::protocol::CollabInteractionEvent; +use ratatui::style::Stylize; +use ratatui::text::Line; + +use crate::history_cell::HistoryCell; +use crate::text_formatting::truncate_text; +use crate::wrapping::RtOptions; +use crate::wrapping::word_wrap_lines; + +const COLLAB_PROMPT_MAX_GRAPHEMES: usize = 120; + +#[derive(Debug)] +pub(crate) struct CollabInteractionCell { + summary: Line<'static>, + detail: Option>, +} + +impl CollabInteractionCell { + fn new(summary: Line<'static>, detail: Option>) -> Self { + Self { summary, detail } + } +} + +impl HistoryCell for CollabInteractionCell { + fn display_lines(&self, width: u16) -> Vec> { + let wrap_width = width.max(1) as usize; + let mut lines = word_wrap_lines( + std::iter::once(self.summary.clone()), + RtOptions::new(wrap_width) + .initial_indent("• ".dim().into()) + .subsequent_indent(" ".into()), + ); + + if let Some(detail) = &self.detail { + let detail_lines = word_wrap_lines( + std::iter::once(detail.clone()), + RtOptions::new(wrap_width) + .initial_indent(" └ ".dim().into()) + .subsequent_indent(" ".into()), + ); + lines.extend(detail_lines); + } + + lines + } +} + +fn collab_status_label(status: &AgentStatus) -> String { + match status { + AgentStatus::PendingInit => "pending init".to_string(), + AgentStatus::Running => "running".to_string(), + AgentStatus::Completed(message) => format!("completed: {message:?}"), + AgentStatus::Errored(_) => "errored".to_string(), + AgentStatus::Shutdown => "shutdown".to_string(), + AgentStatus::NotFound => "not found".to_string(), + } +} + +fn collab_detail_line(label: &str, message: &str) -> Option> { + let trimmed = message.trim(); + if trimmed.is_empty() { + return None; + } + + let collapsed = trimmed + .lines() + .map(str::trim) + .filter(|line| !line.is_empty()) + .collect::>() + .join(" "); + let truncated = truncate_text(&collapsed, COLLAB_PROMPT_MAX_GRAPHEMES); + let label = format!("{label}: "); + Some(Line::from(vec![label.dim(), truncated.into()])) +} + +pub(crate) fn new_collab_interaction(event: CollabInteractionEvent) -> CollabInteractionCell { + let (summary, detail) = match event { + CollabInteractionEvent::AgentSpawned { new_id, prompt, .. } => { + let summary = Line::from(vec![ + "Spawned agent".bold(), + " ".into(), + new_id.to_string().dim(), + ]); + let detail = collab_detail_line("Prompt", &prompt); + (summary, detail) + } + CollabInteractionEvent::AgentInteraction { + receiver_id, + prompt, + .. + } => { + let summary = Line::from(vec![ + "Sent to agent".bold(), + " ".into(), + receiver_id.to_string().dim(), + ]); + let detail = collab_detail_line("Message", &prompt); + (summary, detail) + } + CollabInteractionEvent::WaitingBegin { receiver_id, .. } => { + let summary = Line::from(vec![ + "Waiting on agent".bold(), + " ".into(), + receiver_id.to_string().dim(), + ]); + (summary, None) + } + CollabInteractionEvent::WaitingEnd { + receiver_id, + status, + .. + } => { + let summary = Line::from(vec![ + "Wait ended for agent".bold(), + " ".into(), + receiver_id.to_string().dim(), + " · ".dim(), + collab_status_label(&status).dim(), + ]); + (summary, None) + } + CollabInteractionEvent::Close { + receiver_id, + status, + .. + } => { + let summary = Line::from(vec![ + "Closed agent".bold(), + " ".into(), + receiver_id.to_string().dim(), + " · ".dim(), + collab_status_label(&status).dim(), + ]); + (summary, None) + } + }; + + CollabInteractionCell::new(summary, detail) +} diff --git a/codex-rs/tui/src/lib.rs b/codex-rs/tui/src/lib.rs index 0f38c5ee74..76bb8fb926 100644 --- a/codex-rs/tui/src/lib.rs +++ b/codex-rs/tui/src/lib.rs @@ -43,6 +43,7 @@ mod bottom_pane; mod chatwidget; mod cli; mod clipboard_paste; +mod collab_event_cell; mod color; pub mod custom_terminal; mod diff_render;