From b6e88d2d345dad942859c97d18d954ce3660893d Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 11 Jun 2026 02:18:46 +0000 Subject: [PATCH] =?UTF-8?q?fix(tui):=20#3031=20audit=20fix=20=E2=80=94=20m?= =?UTF-8?q?ap=20the=20literal=20'(no=20output)'=20ToolResult=20placeholder?= =?UTF-8?q?=20to=20None=20at=20the=20routing=20layer=20(exec=20+=20generic?= =?UTF-8?q?=20cells)=20so=20compact-mode=20suppression=20actually=20fires;?= =?UTF-8?q?=20add=20helper=20+=20render-mode=20tests?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude https://claude.ai/code/session_018zaP8vUfTAsrE38L6h6fw5 --- crates/tui/src/tui/tool_routing.rs | 86 ++++++++++++++++++++++++++++-- 1 file changed, 81 insertions(+), 5 deletions(-) diff --git a/crates/tui/src/tui/tool_routing.rs b/crates/tui/src/tui/tool_routing.rs index 2981e59b..e7f8ee81 100644 --- a/crates/tui/src/tui/tool_routing.rs +++ b/crates/tui/src/tui/tool_routing.rs @@ -449,6 +449,19 @@ fn record_spillover_artifact_if_any( )); } +/// #3031: shell/tasks tools embed the literal `"(no output)"` into successful +/// `ToolResult` content (the model-facing transcript needs a non-empty tool +/// result). Treat it as no output on the TUI side so the compact-mode +/// suppression gate in `history.rs` actually fires; the raw content remains +/// available through the tool-detail store. +fn visible_tool_output(content: &str) -> Option { + if content.trim() == "(no output)" { + None + } else { + Some(content.to_string()) + } +} + pub(super) fn handle_tool_call_complete( app: &mut App, id: &str, @@ -549,9 +562,11 @@ pub(super) fn handle_tool_call_complete( .and_then(|m| m.get("duration_ms")) .and_then(serde_json::Value::as_u64); if status != ToolStatus::Running && exec.interaction.is_none() { - exec.output = Some(tool_result.content.clone()); - exec.output_summary = - Some(super::history::summarize_tool_output(&tool_result.content)); + exec.output = visible_tool_output(&tool_result.content); + exec.output_summary = exec + .output + .as_deref() + .map(super::history::summarize_tool_output); exec.live_output = None; } else if status == ToolStatus::Running && exec.interaction.is_none() @@ -642,8 +657,9 @@ pub(super) fn handle_tool_call_complete( generic.status = status; match result.as_ref() { Ok(tool_result) => { - generic.output = Some(tool_result.content.clone()); - generic.output_summary = Some(summarize_tool_output(&tool_result.content)); + generic.output = visible_tool_output(&tool_result.content); + generic.output_summary = + generic.output.as_deref().map(summarize_tool_output); generic.is_diff = output_looks_like_diff(&tool_result.content); } Err(err) => { @@ -1273,4 +1289,64 @@ mod tests { assert_eq!(snapshot.items[0].step, "render all fields"); assert_eq!(snapshot.items[0].status, StepStatus::Pending); } + + // ── #3031: "(no output)" placeholder must not defeat compact rendering ─ + + #[test] + fn visible_tool_output_maps_no_output_placeholder_to_none() { + assert_eq!(visible_tool_output("(no output)"), None); + assert_eq!(visible_tool_output(" (no output)\n"), None); + } + + #[test] + fn visible_tool_output_preserves_real_content() { + assert_eq!( + visible_tool_output("compiled 3 crates").as_deref(), + Some("compiled 3 crates") + ); + // Output that merely CONTAINS the placeholder is real output. + assert_eq!( + visible_tool_output("step 1: (no output) — continuing").as_deref(), + Some("step 1: (no output) — continuing") + ); + assert_eq!(visible_tool_output("").as_deref(), Some("")); + } + + #[test] + fn exec_cell_without_output_suppresses_placeholder_in_live_mode() { + use crate::tui::history::{ExecCell, ExecSource, ToolCell, ToolStatus}; + + let cell = ToolCell::Exec(ExecCell { + command: "true".to_string(), + status: ToolStatus::Success, + output: None, + live_output: None, + shell_task_id: None, + started_at: None, + duration_ms: Some(120), + source: ExecSource::Assistant, + interaction: None, + output_summary: None, + }); + + let live: String = cell + .lines(80) + .iter() + .flat_map(|line| line.spans.iter().map(|s| s.content.to_string())) + .collect(); + assert!( + !live.contains("(no output)"), + "Live mode must suppress the placeholder: {live:?}" + ); + + let transcript: String = cell + .transcript_lines(80) + .iter() + .flat_map(|line| line.spans.iter().map(|s| s.content.to_string())) + .collect(); + assert!( + transcript.contains("(no output)"), + "Transcript mode still records the placeholder: {transcript:?}" + ); + } }