diff --git a/crates/rebon-render/src/summary.rs b/crates/rebon-render/src/summary.rs index c3bf4a8..78ff109 100644 --- a/crates/rebon-render/src/summary.rs +++ b/crates/rebon-render/src/summary.rs @@ -205,25 +205,40 @@ fn snake_to_title(s: &str) -> String { .join(" ") } +/// The one decision about what to call a tool in a transcript row, for every +/// caller that has the input as a map. pub fn streaming_tool_display_name<'a>( tool_name: &'a str, raw_input: Option<&'a HashMap>, +) -> &'a str { + display_name_for_tool(tool_name, |key| { + raw_input + .and_then(|input| input.get(key)) + .and_then(Value::as_str) + }) +} + +/// [`streaming_tool_display_name`] for callers holding the tool input as a +/// `serde_json::Value` — the same names for the same tools. +pub fn streaming_tool_display_name_for_value<'a>( + tool_name: &'a str, + raw_input: &'a Value, +) -> &'a str { + display_name_for_tool(tool_name, |key| raw_input.get(key).and_then(Value::as_str)) +} + +fn display_name_for_tool<'a>( + tool_name: &'a str, + input_field: impl Fn(&str) -> Option<&'a str>, ) -> &'a str { match tool_name { crate::code_mode::RUN_CODE_TOOL_NAME => "Run sequence", - "Agent" => raw_input - .and_then(|input| { - input - .get("subagent_type") - .or_else(|| input.get("subagentType")) - }) - .and_then(Value::as_str) + "Agent" => input_field("subagent_type") + .or_else(|| input_field("subagentType")) .map(str::trim) .filter(|agent_type| !agent_type.is_empty() && *agent_type != "general-purpose") .unwrap_or(tool_name), - "InvokeDeferredTool" => raw_input - .and_then(|input| input.get("tool_name")) - .and_then(Value::as_str) + "InvokeDeferredTool" => input_field("tool_name") .map(str::trim) .filter(|name| !name.is_empty()) .unwrap_or(tool_name), diff --git a/crates/rebon-tui/src/render/projection.rs b/crates/rebon-tui/src/render/projection.rs index 53eec8e..ef1b006 100644 --- a/crates/rebon-tui/src/render/projection.rs +++ b/crates/rebon-tui/src/render/projection.rs @@ -69,15 +69,11 @@ pub(super) fn to_message_row( }) } AssistantContentBlock::ToolUse(tu) => { - let display_name = if tu.name == "InvokeDeferredTool" { - tu.input - .get("tool_name") - .and_then(Value::as_str) - .map(str::to_string) - .unwrap_or_else(|| tu.name.clone()) - } else { - tu.name.clone() - }; + let display_name = + rebon_render::summary::streaming_tool_display_name_for_value( + &tu.name, &tu.input, + ) + .to_string(); let is_web_search = display_name == "WebSearch"; let shell_management_summary = tu.raw_output.as_ref().and_then(|raw| { rebon_render::shell_output::shell_management_header_summary_from_value( diff --git a/crates/rebon-tui/src/render/tests/thinking.rs b/crates/rebon-tui/src/render/tests/thinking.rs index 999e081..bca4de6 100644 --- a/crates/rebon-tui/src/render/tests/thinking.rs +++ b/crates/rebon-tui/src/render/tests/thinking.rs @@ -493,11 +493,51 @@ fn compact_transcript_keeps_thinking_preview_before_agent_tool_rows() { "only the first thinking line should be visible in compact view: {snap:?}" ); assert!(snap.contains("Ctrl+O to expand"), "{snap:?}"); - assert!(snap.contains("Agent ("), "{snap:?}"); + // The row is named by the shared display-name map, so an Agent is shown + // under its subagent type here exactly as the streaming card shows it. + assert!(snap.contains("Explore ("), "{snap:?}"); assert!(snap.contains("Review ACP code"), "{snap:?}"); assert_no_adjacent_blank_rows(&rows); } +#[test] +fn committed_rows_name_run_code_from_the_shared_display_name_map() { + use crate::state::{reducer, Action}; + + let mut s = AppState::new(); + reducer( + &mut s, + Action::Commit(assistant_thinking_and_tool_uses( + "a-code", + "sequence reasoning", + vec![( + "code-1", + "run_code", + json!({ + "description": "Inspect tasks", + "code": "return await tools.TaskList();" + }), + )], + )), + ); + + let mut buf = new_buf(100, 12); + render_transcript( + &s, + Rect::new(0, 0, 100, 12), + &mut buf, + &RenderTheme::plain(), + 0, + ToolOutputVerbosity::Verbose, + 0, + None, + ); + let snap = semantic_rows(&buf).join("\n"); + + assert!(snap.contains("Run sequence ("), "{snap:?}"); + assert!(!snap.contains("run_code ("), "{snap:?}"); +} + #[test] fn inline_render_keeps_active_thinking_visible_before_later_text() { use crate::state::{reducer, Action};