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
This commit is contained in:
Eric Traut
2026-08-11 17:43:10 +00:00
committed by copyberry
parent 34db7e5563
commit be751dd1df
2 changed files with 176 additions and 11 deletions

View File

@@ -40,6 +40,20 @@ pub(crate) struct McpInvocation {
pub(crate) arguments: Option<serde_json::Value>,
}
#[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<Line<'static>> {
fn render_lines(&self, width: u16, mode: McpToolCallRenderMode) -> Vec<Line<'static>> {
let mut lines: Vec<Line<'static>> = 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::<Vec<_>>().join(" "))
.filter(|title| !title.is_empty())
.map(|title| title.graphemes(true).take(80).collect::<String>())
.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::<NodeReplExecOutput>(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<Line<'static>> {
self.render_lines(width, McpToolCallRenderMode::Display)
}
fn transcript_lines(&self, width: u16) -> Vec<Line<'static>> {
self.render_lines(width, McpToolCallRenderMode::Transcript)
}
fn raw_lines(&self) -> Vec<Line<'static>> {
let header_text = if self.success().is_some() {

View File

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