Bound code-mode output previews across result blocks (#46073)

## 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
This commit is contained in:
Eric Traut
2026-09-17 01:31:47 +00:00
committed by copyberry
parent b974893c90
commit 6749535c8f
3 changed files with 154 additions and 2 deletions

View File

@@ -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()

View File

@@ -133,6 +133,141 @@ fn result(content: Vec<Value>) -> 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::<Vec<_>>()
.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::<Vec<_>>()
.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::<Vec<_>>()
.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::<Vec<_>>()
.join("\n");
assert!(
transcript
.split_whitespace()
.collect::<Vec<_>>()
.join(" ")
.contains("transcript tail")
);
}
}
}
}
#[test]
fn projected_content_preserves_width_dependent_rendering() {
let text = "{\"result\": [1, 2, 3], \"text\": \"long output 🦀\"}";

View File

@@ -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...