From be751dd1dfc5409a9a490ed75ec8426fba79ae6e Mon Sep 17 00:00:00 2001 From: Eric Traut Date: Tue, 11 Aug 2026 17:43:10 +0000 Subject: [PATCH] Compact code mode tool calls in TUI history (#38044) ## What changed - Render `node_repl.js` calls in conversation history with their title and the meaningful output from successful executions. - Keep the full invocation and raw result available in the `Ctrl+T` transcript, including failure details. ## Testing - Add snapshots covering successful and failed code mode calls in both history and transcript views. GitOrigin-RevId: c8e24c2eae5e60020e9e8136f386590ca7c42367 --- codex-rs/tui/src/history_cell/mcp.rs | 96 +++++++++++++++++++++++--- codex-rs/tui/src/history_cell/tests.rs | 91 ++++++++++++++++++++++++ 2 files changed, 176 insertions(+), 11 deletions(-) diff --git a/codex-rs/tui/src/history_cell/mcp.rs b/codex-rs/tui/src/history_cell/mcp.rs index 27205ca460..3825548cef 100644 --- a/codex-rs/tui/src/history_cell/mcp.rs +++ b/codex-rs/tui/src/history_cell/mcp.rs @@ -40,6 +40,20 @@ pub(crate) struct McpInvocation { pub(crate) arguments: Option, } +#[derive(Clone, Copy, Eq, PartialEq)] +enum McpToolCallRenderMode { + /// Compact presentation used in normal conversation history. + Display, + /// Complete invocation and result used by the Ctrl+T transcript. + Transcript, +} + +#[derive(serde::Deserialize)] +struct NodeReplExecOutput { + exit_code: i64, + output: String, +} + impl McpToolCallCell { pub(crate) fn new( call_id: String, @@ -116,12 +130,11 @@ impl McpToolCallCell { _ => format_and_truncate_tool_result(&block.to_string(), TOOL_CALL_MAX_LINES, width), } } -} - -impl HistoryCell for McpToolCallCell { - fn display_lines(&self, width: u16) -> Vec> { + fn render_lines(&self, width: u16, mode: McpToolCallRenderMode) -> Vec> { let mut lines: Vec> = Vec::new(); let status = self.success(); + let node_repl = self.invocation.server == "node_repl" && self.invocation.tool == "js"; + let compact = node_repl && mode == McpToolCallRenderMode::Display; let bullet = match status { Some(true) => "•".green().bold(), Some(false) => "•".red().bold(), @@ -138,7 +151,21 @@ impl HistoryCell for McpToolCallCell { "Calling" }; - let invocation_line = line_to_static(&format_mcp_invocation(self.invocation.clone())); + let invocation_line = if compact { + let title = self + .invocation + .arguments + .as_ref() + .and_then(|arguments| arguments.get("title")) + .and_then(serde_json::Value::as_str) + .map(|title| title.split_whitespace().collect::>().join(" ")) + .filter(|title| !title.is_empty()) + .map(|title| title.graphemes(true).take(80).collect::()) + .unwrap_or_else(|| "node_repl.js".to_string()); + Line::from(title.cyan()) + } else { + line_to_static(&format_mcp_invocation(self.invocation.clone())) + }; let mut compact_spans = vec![bullet.clone(), " ".into(), header_text.bold(), " ".into()]; let mut compact_header = Line::from(compact_spans.clone()); let reserved = compact_header.width(); @@ -170,7 +197,39 @@ impl HistoryCell for McpToolCallCell { Ok(codex_protocol::mcp::CallToolResult { content, .. }) => { if !content.is_empty() { for block in content { - let text = Self::render_content_block(block, detail_wrap_width); + let text = if compact && status == Some(true) { + let meaningful_output = block + .get("text") + .and_then(serde_json::Value::as_str) + .and_then(|text| { + if text.starts_with("Script completed\n") { + return text + .split_once("\nOutput:\n") + .map(|(_, output)| output.to_string()); + } + serde_json::from_str::(text) + .ok() + .filter(|output| output.exit_code == 0) + .map(|output| output.output) + }); + match meaningful_output { + Some(output) if output.is_empty() => continue, + Some(output) => format_and_truncate_tool_result( + &output, + TOOL_CALL_MAX_LINES, + detail_wrap_width, + ), + None => Self::render_content_block(block, detail_wrap_width), + } + } else if node_repl + && mode == McpToolCallRenderMode::Transcript + && let Some(output) = + block.get("text").and_then(serde_json::Value::as_str) + { + output.trim_end_matches('\n').to_string() + } else { + Self::render_content_block(block, detail_wrap_width) + }; for segment in text.split('\n') { let line = Line::from(segment.to_string().dim()); let wrapped = adaptive_wrap_line( @@ -185,11 +244,16 @@ impl HistoryCell for McpToolCallCell { } } Err(err) => { - let err_text = format_and_truncate_tool_result( - &format!("Error: {err}"), - TOOL_CALL_MAX_LINES, - width as usize, - ); + let err_text = format!("Error: {err}"); + let err_text = if node_repl && mode == McpToolCallRenderMode::Transcript { + err_text + } else { + format_and_truncate_tool_result( + &err_text, + TOOL_CALL_MAX_LINES, + width as usize, + ) + }; let err_line = Line::from(err_text.dim()); let wrapped = adaptive_wrap_line( &err_line, @@ -213,6 +277,16 @@ impl HistoryCell for McpToolCallCell { lines } +} + +impl HistoryCell for McpToolCallCell { + fn display_lines(&self, width: u16) -> Vec> { + self.render_lines(width, McpToolCallRenderMode::Display) + } + + fn transcript_lines(&self, width: u16) -> Vec> { + self.render_lines(width, McpToolCallRenderMode::Transcript) + } fn raw_lines(&self) -> Vec> { let header_text = if self.success().is_some() { diff --git a/codex-rs/tui/src/history_cell/tests.rs b/codex-rs/tui/src/history_cell/tests.rs index e81efd2024..2970aadd20 100644 --- a/codex-rs/tui/src/history_cell/tests.rs +++ b/codex-rs/tui/src/history_cell/tests.rs @@ -1302,6 +1302,97 @@ fn active_mcp_tool_call_snapshot() { insta::assert_snapshot!(rendered); } +#[test] +fn code_mode_tool_call_uses_title_and_preserves_full_transcript() { + let output = format!("{} transcript tail", "0123456789".repeat(20)); + let mut cell = new_active_mcp_tool_call( + "call-code-mode".into(), + McpInvocation { + server: "node_repl".into(), + tool: "js".into(), + arguments: Some(json!({ + "title": "Inspect Spotify workspace", + "code": "await tools.exec_command({ cmd: 'git status' })", + })), + }, + /*animations_enabled*/ false, + ); + cell.complete( + Duration::ZERO, + Ok(CallToolResult { + content: vec![ + text_block("Script completed\nWall time 0.1 seconds\nOutput:\n"), + text_block( + &json!({"chunk_id": "chunk-1", "output": output, "exit_code": 0}).to_string(), + ), + ], + is_error: None, + structured_content: None, + meta: None, + }), + ); + + let history = render_lines(&cell.display_lines(/*width*/ 40)).join("\n"); + let transcript = render_lines(&cell.transcript_lines(/*width*/ 180)).join("\n"); + insta::assert_snapshot!(format!("history:\n{history}\n\ntranscript:\n{transcript}"), @r#" + history: + • Called Inspect Spotify workspace + └ 012345678901234567890123456789012345 + 67890123456789012345678901234567 + 89012345678901234567890123456789 + 01234567890123456789012345678901 + 23456789012345678901234567890123 + 45678901... + + transcript: + • Called node_repl.js({"title":"Inspect Spotify workspace","code":"await tools.exec_command({ cmd: 'git status' })"}) + └ Script completed + Wall time 0.1 seconds + Output: + {"chunk_id":"chunk- + 1","output":"012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678 + 90123456789012345678901234567890123456789 transcript tail","exit_code":0} + "#); +} + +#[test] +fn code_mode_tool_call_preserves_failure_details() { + let mut cell = new_active_mcp_tool_call( + "call-code-mode-failed".into(), + McpInvocation { + server: "node_repl".into(), + tool: "js".into(), + arguments: Some(json!({"title": "Inspect workspace", "code": "throw Error('denied')"})), + }, + /*animations_enabled*/ false, + ); + cell.complete( + Duration::ZERO, + Ok(CallToolResult { + content: vec![text_block("Script failed\nOutput:\npermission denied")], + is_error: Some(true), + structured_content: None, + meta: None, + }), + ); + + let history = render_lines(&cell.display_lines(/*width*/ 80)).join("\n"); + let transcript = render_lines(&cell.transcript_lines(/*width*/ 120)).join("\n"); + insta::assert_snapshot!(format!("history:\n{history}\n\ntranscript:\n{transcript}"), @r#" + history: + • Called Inspect workspace + └ Script failed + Output: + permission denied + + transcript: + • Called node_repl.js({"title":"Inspect workspace","code":"throw Error('denied')"}) + └ Script failed + Output: + permission denied + "#); +} + #[test] fn mcp_inventory_loading_snapshot() { let cell = new_mcp_inventory_loading(/*animations_enabled*/ true);