mirror of
https://github.com/openai/codex.git
synced 2026-09-13 11:47:17 +00:00
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:
@@ -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() {
|
||||
|
||||
@@ -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);
|
||||
|
||||
Reference in New Issue
Block a user