From 6749535c8f717749cd21374b321590b9975ab1f7 Mon Sep 17 00:00:00 2001 From: Eric Traut Date: Thu, 17 Sep 2026 01:31:47 +0000 Subject: [PATCH] Bound code-mode output previews across result blocks (#46073) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## Why Multiple result blocks and wrapped text can exceed the compact preview's row budget. Truncation must also preserve trailing failure diagnostics. ## What changed Apply a shared rendered-row limit to code-mode output previews after wrapping, including overlong URLs. Keep the first and last rows around an `… more · ctrl+t` indicator, while retaining the full text in the expanded transcript. ## Testing Add coverage for multiple result blocks, trailing failure diagnostics, Unicode wrapping, long URLs, and error results at several terminal widths, including transcript preservation. GitOrigin-RevId: ad9cf0fafb909e4df9a132f83722d6cd4e669687 --- codex-rs/tui/src/history_cell/mcp.rs | 18 +++ codex-rs/tui/src/history_cell/mcp_tests.rs | 135 +++++++++++++++++++++ codex-rs/tui/src/history_cell/tests.rs | 3 +- 3 files changed, 154 insertions(+), 2 deletions(-) diff --git a/codex-rs/tui/src/history_cell/mcp.rs b/codex-rs/tui/src/history_cell/mcp.rs index 3f0bc2a106..60ae6b1efb 100644 --- a/codex-rs/tui/src/history_cell/mcp.rs +++ b/codex-rs/tui/src/history_cell/mcp.rs @@ -1,4 +1,6 @@ //! MCP tool-call, inventory, and output history cells. +//! Code-mode output previews share a rendered-row budget across all result blocks; +//! the expanded transcript retains the full text. use super::*; @@ -276,6 +278,22 @@ impl McpToolCallCell { } } + if compact { + // Adaptive wrapping keeps URLs intact; split overlong preview rows before counting + // them so a URL cannot exceed the budget. The transcript keeps the original URL. + detail_lines = crate::wrapping::word_wrap_lines(detail_lines, detail_wrap_width); + if detail_lines.len() > TOOL_CALL_MAX_LINES { + // Retain the tail so stdout cannot crowd out a trailing failure diagnostic. + let tail = detail_lines.split_off(detail_lines.len() - TOOL_CALL_MAX_LINES / 2); + detail_lines.truncate((TOOL_CALL_MAX_LINES - 1) / 2); + detail_lines.push(crate::line_truncation::truncate_line_to_width( + "… more · ctrl+t".dim().into(), + detail_wrap_width, + )); + detail_lines.extend(tail); + } + } + if !detail_lines.is_empty() { let initial_prefix: Span<'static> = if inline_invocation { " └ ".dim() diff --git a/codex-rs/tui/src/history_cell/mcp_tests.rs b/codex-rs/tui/src/history_cell/mcp_tests.rs index 5b59cf06da..390b15de0a 100644 --- a/codex-rs/tui/src/history_cell/mcp_tests.rs +++ b/codex-rs/tui/src/history_cell/mcp_tests.rs @@ -133,6 +133,141 @@ fn result(content: Vec) -> CallToolResult { } } +#[test] +fn code_mode_output_shares_a_row_budget_across_blocks() { + let mut cell = new_active_mcp_tool_call( + "browser-call".to_string(), + McpInvocation { + server: "node_repl".to_string(), + tool: "js".to_string(), + arguments: Some(json!({"title": "Inspect page", "code": "await tab.snapshot()"})), + }, + /*animations_enabled*/ false, + ); + cell.complete( + Duration::ZERO, + Ok(result(vec![ + json!({"type": "text", "text": "Script completed\nOutput:\n"}), + json!({"type": "text", "text": "Page title\nNavigation\nMain content"}), + json!({"type": "text", "text": "Button\nLink\nFooter"}), + ])), + ); + + let display = cell + .display_lines(/*width*/ 40) + .iter() + .map(ToString::to_string) + .collect::>() + .join("\n"); + insta::assert_snapshot!(display, @" + • Called Inspect page + └ Page title + Navigation + … more · ctrl+t + Link + Footer + "); + let transcript = cell + .transcript_lines(/*width*/ 100) + .iter() + .map(ToString::to_string) + .collect::>() + .join("\n"); + assert!(transcript.contains("await tab.snapshot()")); + assert!(transcript.ends_with(" Button\n Link\n Footer")); +} + +#[test] +fn code_mode_output_keeps_trailing_failure_diagnostics() { + let mut cell = new_active_mcp_tool_call( + "browser-error".to_string(), + McpInvocation { + server: "node_repl".to_string(), + tool: "js".to_string(), + arguments: Some(json!({"title": "Inspect page"})), + }, + /*animations_enabled*/ false, + ); + cell.complete( + Duration::ZERO, + Ok(CallToolResult { + is_error: Some(true), + ..result(vec![ + json!({"type": "text", "text": "Script failed"}), + json!({"type": "text", "text": "Page title\nNavigation\nMain content\nButton\nLink\nFooter"}), + json!({"type": "text", "text": "Script error:\npermission denied"}), + ]) + }), + ); + let display = cell + .display_lines(/*width*/ 40) + .iter() + .map(ToString::to_string) + .collect::>() + .join("\n"); + insta::assert_snapshot!(display, @" + • Called Inspect page + └ Script failed + Page title + … more · ctrl+t + Script error: + permission denied + "); +} + +#[test] +fn code_mode_output_row_budget_applies_after_wrapping_and_to_errors() { + let output = format!("{}\ntranscript tail", "Browser 页面 👩‍💻\n".repeat(40)); + for server in ["node_repl", "cua_repl"] { + for completion in [ + Ok(result(vec![json!({"type": "text", "text": output})])), + Ok(result(vec![json!({ + "type": "text", + "text": format!("https://example.com/{}\ntranscript tail", "页面".repeat(100)), + })])), + Ok(CallToolResult { + is_error: Some(true), + ..result(vec![json!({"type": "text", "text": output})]) + }), + Err(output.clone()), + ] { + let mut cell = new_active_mcp_tool_call( + "browser-call".to_string(), + McpInvocation { + server: server.to_string(), + tool: "js".to_string(), + arguments: Some(json!({"title": "Inspect"})), + }, + /*animations_enabled*/ false, + ); + cell.complete(Duration::ZERO, completion); + for width in [20, 40, 80] { + let display = cell.display_lines(width); + assert_eq!(display.len(), 1 + TOOL_CALL_MAX_LINES); + assert!( + display + .iter() + .all(|line| line.width() <= usize::from(width)) + ); + assert_eq!(display[3].to_string(), " … more · ctrl+t"); + let transcript = cell + .transcript_lines(width) + .iter() + .map(ToString::to_string) + .collect::>() + .join("\n"); + assert!( + transcript + .split_whitespace() + .collect::>() + .join(" ") + .contains("transcript tail") + ); + } + } + } +} + #[test] fn projected_content_preserves_width_dependent_rendering() { let text = "{\"result\": [1, 2, 3], \"text\": \"long output 🦀\"}"; diff --git a/codex-rs/tui/src/history_cell/tests.rs b/codex-rs/tui/src/history_cell/tests.rs index 8385516759..a4b963b328 100644 --- a/codex-rs/tui/src/history_cell/tests.rs +++ b/codex-rs/tui/src/history_cell/tests.rs @@ -1444,8 +1444,7 @@ fn code_mode_tool_call_uses_title_and_preserves_full_transcript() { • Called Inspect Spotify workspace └ 012345678901234567890123456789012345 67890123456789012345678901234567 - 89012345678901234567890123456789 - 01234567890123456789012345678901 + … more · ctrl+t 23456789012345678901234567890123 45678901...