From 3fc487e0e0788045460df1abe21dc9de47f3b7f9 Mon Sep 17 00:00:00 2001 From: jif-oai Date: Thu, 15 Jan 2026 15:53:02 +0000 Subject: [PATCH] feat: basic tui for event emission (#9209) --- codex-rs/tui/src/chatwidget.rs | 25 ++++-- codex-rs/tui/src/collab.rs | 142 ++++++++++++++++++++++++++++++++ codex-rs/tui/src/lib.rs | 1 + codex-rs/tui2/src/chatwidget.rs | 25 ++++-- codex-rs/tui2/src/collab.rs | 142 ++++++++++++++++++++++++++++++++ codex-rs/tui2/src/lib.rs | 1 + 6 files changed, 318 insertions(+), 18 deletions(-) create mode 100644 codex-rs/tui/src/collab.rs create mode 100644 codex-rs/tui2/src/collab.rs diff --git a/codex-rs/tui/src/chatwidget.rs b/codex-rs/tui/src/chatwidget.rs index 6d9f9dafa..4866d7bad 100644 --- a/codex-rs/tui/src/chatwidget.rs +++ b/codex-rs/tui/src/chatwidget.rs @@ -134,6 +134,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; use crate::diff_render::display_path_for; use crate::exec_cell::CommandOutput; use crate::exec_cell::ExecCell; @@ -1213,6 +1214,12 @@ impl ChatWidget { self.add_to_history(history_cell::new_web_search_call(ev.query)); } + fn on_collab_event(&mut self, cell: PlainHistoryCell) { + self.flush_answer_stream_with_separator(); + self.add_to_history(cell); + self.request_redraw(); + } + fn on_get_history_entry_response( &mut self, event: codex_core::protocol::GetHistoryEntryResponseEvent, @@ -2444,16 +2451,16 @@ impl ChatWidget { } EventMsg::ExitedReviewMode(review) => self.on_exited_review_mode(review), EventMsg::ContextCompacted(_) => self.on_agent_message("Context compacted".to_owned()), - EventMsg::CollabAgentSpawnBegin(_) - | EventMsg::CollabAgentSpawnEnd(_) - | EventMsg::CollabAgentInteractionBegin(_) - | EventMsg::CollabAgentInteractionEnd(_) - | EventMsg::CollabWaitingBegin(_) - | EventMsg::CollabWaitingEnd(_) - | EventMsg::CollabCloseBegin(_) - | EventMsg::CollabCloseEnd(_) => { - // TODO(jif) handle collab tools. + EventMsg::CollabAgentSpawnBegin(_) => {} + EventMsg::CollabAgentSpawnEnd(ev) => self.on_collab_event(collab::spawn_end(ev)), + EventMsg::CollabAgentInteractionBegin(_) => {} + EventMsg::CollabAgentInteractionEnd(ev) => { + self.on_collab_event(collab::interaction_end(ev)) } + EventMsg::CollabWaitingBegin(ev) => self.on_collab_event(collab::waiting_begin(ev)), + EventMsg::CollabWaitingEnd(ev) => self.on_collab_event(collab::waiting_end(ev)), + EventMsg::CollabCloseBegin(_) => {} + EventMsg::CollabCloseEnd(ev) => self.on_collab_event(collab::close_end(ev)), EventMsg::ThreadRolledBack(_) => {} EventMsg::RawResponseItem(_) | EventMsg::ItemStarted(_) diff --git a/codex-rs/tui/src/collab.rs b/codex-rs/tui/src/collab.rs new file mode 100644 index 000000000..6b1042810 --- /dev/null +++ b/codex-rs/tui/src/collab.rs @@ -0,0 +1,142 @@ +use crate::history_cell::PlainHistoryCell; +use crate::render::line_utils::prefix_lines; +use crate::text_formatting::truncate_text; +use codex_core::protocol::AgentStatus; +use codex_core::protocol::CollabAgentInteractionEndEvent; +use codex_core::protocol::CollabAgentSpawnEndEvent; +use codex_core::protocol::CollabCloseEndEvent; +use codex_core::protocol::CollabWaitingBeginEvent; +use codex_core::protocol::CollabWaitingEndEvent; +use ratatui::style::Stylize; +use ratatui::text::Line; + +const COLLAB_PROMPT_PREVIEW_GRAPHEMES: usize = 160; + +pub(crate) fn spawn_end(ev: CollabAgentSpawnEndEvent) -> PlainHistoryCell { + let CollabAgentSpawnEndEvent { + call_id, + sender_thread_id, + new_thread_id, + prompt, + status, + } = ev; + let new_agent = new_thread_id + .map(|id| id.to_string()) + .unwrap_or_else(|| "none".to_string()); + let mut details = vec![ + detail_line("call", call_id), + detail_line("sender", sender_thread_id), + detail_line("new_agent", new_agent), + status_line(&status), + ]; + if let Some(line) = prompt_line(&prompt) { + details.push(line); + } + collab_event("Collab spawn", details) +} + +pub(crate) fn interaction_end(ev: CollabAgentInteractionEndEvent) -> PlainHistoryCell { + let CollabAgentInteractionEndEvent { + call_id, + sender_thread_id, + receiver_thread_id, + prompt, + status, + } = ev; + let mut details = vec![ + detail_line("call", call_id), + detail_line("sender", sender_thread_id), + detail_line("receiver", receiver_thread_id), + status_line(&status), + ]; + if let Some(line) = prompt_line(&prompt) { + details.push(line); + } + collab_event("Collab send input", details) +} + +pub(crate) fn waiting_begin(ev: CollabWaitingBeginEvent) -> PlainHistoryCell { + let CollabWaitingBeginEvent { + call_id, + sender_thread_id, + receiver_thread_id, + } = ev; + let details = vec![ + detail_line("call", call_id), + detail_line("sender", sender_thread_id), + detail_line("receiver", receiver_thread_id), + ]; + collab_event("Collab wait begin", details) +} + +pub(crate) fn waiting_end(ev: CollabWaitingEndEvent) -> PlainHistoryCell { + let CollabWaitingEndEvent { + call_id, + sender_thread_id, + receiver_thread_id, + status, + } = ev; + let details = vec![ + detail_line("call", call_id), + detail_line("sender", sender_thread_id), + detail_line("receiver", receiver_thread_id), + status_line(&status), + ]; + collab_event("Collab wait end", details) +} + +pub(crate) fn close_end(ev: CollabCloseEndEvent) -> PlainHistoryCell { + let CollabCloseEndEvent { + call_id, + sender_thread_id, + receiver_thread_id, + status, + } = ev; + let details = vec![ + detail_line("call", call_id), + detail_line("sender", sender_thread_id), + detail_line("receiver", receiver_thread_id), + status_line(&status), + ]; + collab_event("Collab close", details) +} + +fn collab_event(title: impl Into, details: Vec>) -> PlainHistoryCell { + let title = title.into(); + let mut lines: Vec> = vec![vec!["• ".dim(), title.bold()].into()]; + if !details.is_empty() { + lines.extend(prefix_lines(details, " └ ".dim(), " ".into())); + } + PlainHistoryCell::new(lines) +} + +fn detail_line(label: &str, value: impl std::fmt::Display) -> Line<'static> { + Line::from(format!("{label}: {value}").dim()) +} + +fn status_line(status: &AgentStatus) -> Line<'static> { + Line::from(format!("status: {}", status_text(status)).dim()) +} + +fn status_text(status: &AgentStatus) -> &'static str { + match status { + AgentStatus::PendingInit => "pending_init", + AgentStatus::Running => "running", + AgentStatus::Completed(_) => "completed", + AgentStatus::Errored(_) => "errored", + AgentStatus::Shutdown => "shutdown", + AgentStatus::NotFound => "not_found", + } +} + +fn prompt_line(prompt: &str) -> Option> { + let trimmed = prompt.trim(); + if trimmed.is_empty() { + None + } else { + Some(detail_line( + "prompt", + truncate_text(trimmed, COLLAB_PROMPT_PREVIEW_GRAPHEMES), + )) + } +} diff --git a/codex-rs/tui/src/lib.rs b/codex-rs/tui/src/lib.rs index 4bae970ee..a7bab5c65 100644 --- a/codex-rs/tui/src/lib.rs +++ b/codex-rs/tui/src/lib.rs @@ -45,6 +45,7 @@ mod bottom_pane; mod chatwidget; mod cli; mod clipboard_paste; +mod collab; mod color; pub mod custom_terminal; mod diff_render; diff --git a/codex-rs/tui2/src/chatwidget.rs b/codex-rs/tui2/src/chatwidget.rs index 1e38e4483..15914c102 100644 --- a/codex-rs/tui2/src/chatwidget.rs +++ b/codex-rs/tui2/src/chatwidget.rs @@ -128,6 +128,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; use crate::diff_render::display_path_for; use crate::exec_cell::CommandOutput; use crate::exec_cell::ExecCell; @@ -1021,6 +1022,12 @@ impl ChatWidget { self.add_to_history(history_cell::new_web_search_call(ev.query)); } + fn on_collab_event(&mut self, cell: PlainHistoryCell) { + self.flush_answer_stream_with_separator(); + self.add_to_history(cell); + self.request_redraw(); + } + fn on_get_history_entry_response( &mut self, event: codex_core::protocol::GetHistoryEntryResponseEvent, @@ -2194,16 +2201,16 @@ impl ChatWidget { } EventMsg::ExitedReviewMode(review) => self.on_exited_review_mode(review), EventMsg::ContextCompacted(_) => self.on_agent_message("Context compacted".to_owned()), - EventMsg::CollabAgentSpawnBegin(_) - | EventMsg::CollabAgentSpawnEnd(_) - | EventMsg::CollabAgentInteractionBegin(_) - | EventMsg::CollabAgentInteractionEnd(_) - | EventMsg::CollabWaitingBegin(_) - | EventMsg::CollabWaitingEnd(_) - | EventMsg::CollabCloseBegin(_) - | EventMsg::CollabCloseEnd(_) => { - // TODO(jif) handle collab tools. + EventMsg::CollabAgentSpawnBegin(_) => {} + EventMsg::CollabAgentSpawnEnd(ev) => self.on_collab_event(collab::spawn_end(ev)), + EventMsg::CollabAgentInteractionBegin(_) => {} + EventMsg::CollabAgentInteractionEnd(ev) => { + self.on_collab_event(collab::interaction_end(ev)) } + EventMsg::CollabWaitingBegin(ev) => self.on_collab_event(collab::waiting_begin(ev)), + EventMsg::CollabWaitingEnd(ev) => self.on_collab_event(collab::waiting_end(ev)), + EventMsg::CollabCloseBegin(_) => {} + EventMsg::CollabCloseEnd(ev) => self.on_collab_event(collab::close_end(ev)), EventMsg::RawResponseItem(_) | EventMsg::ThreadRolledBack(_) | EventMsg::ItemStarted(_) diff --git a/codex-rs/tui2/src/collab.rs b/codex-rs/tui2/src/collab.rs new file mode 100644 index 000000000..6b1042810 --- /dev/null +++ b/codex-rs/tui2/src/collab.rs @@ -0,0 +1,142 @@ +use crate::history_cell::PlainHistoryCell; +use crate::render::line_utils::prefix_lines; +use crate::text_formatting::truncate_text; +use codex_core::protocol::AgentStatus; +use codex_core::protocol::CollabAgentInteractionEndEvent; +use codex_core::protocol::CollabAgentSpawnEndEvent; +use codex_core::protocol::CollabCloseEndEvent; +use codex_core::protocol::CollabWaitingBeginEvent; +use codex_core::protocol::CollabWaitingEndEvent; +use ratatui::style::Stylize; +use ratatui::text::Line; + +const COLLAB_PROMPT_PREVIEW_GRAPHEMES: usize = 160; + +pub(crate) fn spawn_end(ev: CollabAgentSpawnEndEvent) -> PlainHistoryCell { + let CollabAgentSpawnEndEvent { + call_id, + sender_thread_id, + new_thread_id, + prompt, + status, + } = ev; + let new_agent = new_thread_id + .map(|id| id.to_string()) + .unwrap_or_else(|| "none".to_string()); + let mut details = vec![ + detail_line("call", call_id), + detail_line("sender", sender_thread_id), + detail_line("new_agent", new_agent), + status_line(&status), + ]; + if let Some(line) = prompt_line(&prompt) { + details.push(line); + } + collab_event("Collab spawn", details) +} + +pub(crate) fn interaction_end(ev: CollabAgentInteractionEndEvent) -> PlainHistoryCell { + let CollabAgentInteractionEndEvent { + call_id, + sender_thread_id, + receiver_thread_id, + prompt, + status, + } = ev; + let mut details = vec![ + detail_line("call", call_id), + detail_line("sender", sender_thread_id), + detail_line("receiver", receiver_thread_id), + status_line(&status), + ]; + if let Some(line) = prompt_line(&prompt) { + details.push(line); + } + collab_event("Collab send input", details) +} + +pub(crate) fn waiting_begin(ev: CollabWaitingBeginEvent) -> PlainHistoryCell { + let CollabWaitingBeginEvent { + call_id, + sender_thread_id, + receiver_thread_id, + } = ev; + let details = vec![ + detail_line("call", call_id), + detail_line("sender", sender_thread_id), + detail_line("receiver", receiver_thread_id), + ]; + collab_event("Collab wait begin", details) +} + +pub(crate) fn waiting_end(ev: CollabWaitingEndEvent) -> PlainHistoryCell { + let CollabWaitingEndEvent { + call_id, + sender_thread_id, + receiver_thread_id, + status, + } = ev; + let details = vec![ + detail_line("call", call_id), + detail_line("sender", sender_thread_id), + detail_line("receiver", receiver_thread_id), + status_line(&status), + ]; + collab_event("Collab wait end", details) +} + +pub(crate) fn close_end(ev: CollabCloseEndEvent) -> PlainHistoryCell { + let CollabCloseEndEvent { + call_id, + sender_thread_id, + receiver_thread_id, + status, + } = ev; + let details = vec![ + detail_line("call", call_id), + detail_line("sender", sender_thread_id), + detail_line("receiver", receiver_thread_id), + status_line(&status), + ]; + collab_event("Collab close", details) +} + +fn collab_event(title: impl Into, details: Vec>) -> PlainHistoryCell { + let title = title.into(); + let mut lines: Vec> = vec![vec!["• ".dim(), title.bold()].into()]; + if !details.is_empty() { + lines.extend(prefix_lines(details, " └ ".dim(), " ".into())); + } + PlainHistoryCell::new(lines) +} + +fn detail_line(label: &str, value: impl std::fmt::Display) -> Line<'static> { + Line::from(format!("{label}: {value}").dim()) +} + +fn status_line(status: &AgentStatus) -> Line<'static> { + Line::from(format!("status: {}", status_text(status)).dim()) +} + +fn status_text(status: &AgentStatus) -> &'static str { + match status { + AgentStatus::PendingInit => "pending_init", + AgentStatus::Running => "running", + AgentStatus::Completed(_) => "completed", + AgentStatus::Errored(_) => "errored", + AgentStatus::Shutdown => "shutdown", + AgentStatus::NotFound => "not_found", + } +} + +fn prompt_line(prompt: &str) -> Option> { + let trimmed = prompt.trim(); + if trimmed.is_empty() { + None + } else { + Some(detail_line( + "prompt", + truncate_text(trimmed, COLLAB_PROMPT_PREVIEW_GRAPHEMES), + )) + } +} diff --git a/codex-rs/tui2/src/lib.rs b/codex-rs/tui2/src/lib.rs index ddb38cf29..9ababcab3 100644 --- a/codex-rs/tui2/src/lib.rs +++ b/codex-rs/tui2/src/lib.rs @@ -46,6 +46,7 @@ mod chatwidget; mod cli; mod clipboard_copy; mod clipboard_paste; +mod collab; mod color; pub mod custom_terminal; mod diff_render;