From d8072b9a0a654eec4db6dd0d444b2c24ddfff2b3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pascal=20Andr=C3=A9?= Date: Fri, 8 May 2026 23:57:05 +0200 Subject: [PATCH 1/2] fix(ui): compact collapsed tool calls Collapse tool output blocks by default so chat history stays readable on desktop and narrow screens while still showing enough context to understand what each tool did. The tool call header now truncates instead of forcing horizontal overflow, and collapsed tools render a compact input/output preview derived from the tool payload. Existing behavior settings continue to control whether users prefer expanded or collapsed output by default. Validation: - npm run typecheck --workspace @codenomad/ui - npm run build --workspace @codenomad/ui --- packages/ui/src/components/tool-call.tsx | 17 +++- packages/ui/src/components/tool-call/utils.ts | 91 +++++++++++++++++++ .../ui/src/lib/settings/behavior-registry.ts | 6 +- packages/ui/src/stores/preferences.tsx | 2 +- .../ui/src/styles/messaging/tool-call.css | 12 ++- 5 files changed, 121 insertions(+), 7 deletions(-) diff --git a/packages/ui/src/components/tool-call.tsx b/packages/ui/src/components/tool-call.tsx index af9be1ebd..d746110e9 100644 --- a/packages/ui/src/components/tool-call.tsx +++ b/packages/ui/src/components/tool-call.tsx @@ -29,6 +29,7 @@ import type { ToolScrollHelpers, } from "./tool-call/types" import { + buildToolPreview, buildToolSpeechText, ensureMarkdownContent, getRelativePath, @@ -606,7 +607,7 @@ export default function ToolCall(props: ToolCallProps) { return undefined }) - const toolOutputDefaultExpanded = createMemo(() => (preferences().toolOutputExpansion || "expanded") === "expanded") + const toolOutputDefaultExpanded = createMemo(() => (preferences().toolOutputExpansion || "collapsed") === "expanded") const diagnosticsDefaultExpanded = createMemo(() => (preferences().diagnosticsExpansion || "expanded") === "expanded") const defaultExpandedForTool = createMemo(() => { @@ -678,6 +679,7 @@ export default function ToolCall(props: ToolCallProps) { const [toolCallRootEl, setToolCallRootEl] = createSignal() const [scrollTopSnapshot, setScrollTopSnapshot] = createSignal(0) const [diagnosticsOverride, setDiagnosticsOverride] = createSignal(undefined) + const collapsedPreview = createMemo(() => buildToolPreview(toolName(), toolState())) const diagnosticsExpanded = () => { if (isPermissionActive() || isQuestionActive()) return true @@ -885,7 +887,7 @@ export default function ToolCall(props: ToolCallProps) { aria-expanded={expanded()} > - {headerText()} + {headerText()} @@ -969,6 +971,17 @@ export default function ToolCall(props: ToolCallProps) { setScrollTopSnapshot={setScrollTopSnapshot} /> + + + {(preview) => ( +
+
+ {preview().kind === "output" ? t("toolCall.io.output") : t("toolCall.io.input")} +
+
{preview().text}
+
+ )} +
diff --git a/packages/ui/src/components/tool-call/utils.ts b/packages/ui/src/components/tool-call/utils.ts index 766d8d998..fd6cd6093 100644 --- a/packages/ui/src/components/tool-call/utils.ts +++ b/packages/ui/src/components/tool-call/utils.ts @@ -152,6 +152,97 @@ export function formatUnknown(value: unknown): { text: string; language?: string return null } +function joinPreviewParts(...parts: Array): string | null { + const text = parts + .map((part) => (typeof part === "string" ? part.trim() : "")) + .filter(Boolean) + .join("\n") + + return text.length > 0 ? text : null +} + +function getToolInputPreview(toolName: string, input: Record): unknown { + switch (toolName) { + case "bash": + return joinPreviewParts( + typeof input.command === "string" ? `$ ${input.command}` : undefined, + typeof input.description === "string" ? input.description : undefined, + ) + case "read": + case "edit": + case "write": + case "patch": + case "apply_patch": + case "list": + return typeof input.filePath === "string" + ? input.filePath + : typeof input.path === "string" + ? input.path + : input + case "glob": + return joinPreviewParts( + typeof input.pattern === "string" ? input.pattern : undefined, + typeof input.path === "string" ? input.path : undefined, + ) + case "grep": + return joinPreviewParts( + typeof input.pattern === "string" ? input.pattern : undefined, + typeof input.include === "string" ? input.include : undefined, + typeof input.path === "string" ? input.path : undefined, + ) + case "webfetch": + return typeof input.url === "string" ? input.url : input + case "task": + return joinPreviewParts( + typeof input.description === "string" ? input.description : undefined, + typeof input.prompt === "string" ? input.prompt : undefined, + ) + default: + return input + } +} + +function formatPreviewText(value: unknown): string | null { + if (Array.isArray(value) && value.length === 0) { + return null + } + + if (value && typeof value === "object" && Object.keys(value as Record).length === 0) { + return null + } + + const formatted = formatUnknown(value) + const text = formatted?.text?.trim() + return text ? text : null +} + +export function buildToolPreview(toolName: string, state?: ToolState): { + kind: "input" | "output" + text: string +} | null { + const { input, metadata } = readToolStatePayload(state) + + const outputCandidate = !state + ? undefined + : isToolStateCompleted(state) + ? state.output ?? metadata.output ?? metadata.diff ?? metadata.preview + : (isToolStateRunning(state) || isToolStateError(state)) + ? metadata.output ?? metadata.diff ?? metadata.preview + : metadata.preview ?? metadata.diff + + const outputText = formatPreviewText(outputCandidate) + if (outputText) { + return { kind: "output", text: outputText } + } + + const inputText = formatPreviewText(getToolInputPreview(toolName, input)) + if (inputText) { + return { kind: "input", text: inputText } + } + + return null +} + export function inferLanguageFromPath(path?: string): string | undefined { return getLanguageFromPath(path || "") } diff --git a/packages/ui/src/lib/settings/behavior-registry.ts b/packages/ui/src/lib/settings/behavior-registry.ts index 25bd13e4b..07054f639 100644 --- a/packages/ui/src/lib/settings/behavior-registry.ts +++ b/packages/ui/src/lib/settings/behavior-registry.ts @@ -182,7 +182,7 @@ export function getBehaviorSettings(actions: BehaviorRegistryActions): BehaviorS id: "behavior.toolOutputsDefault", titleKey: "settings.behavior.toolOutputsDefault.title", subtitleKey: "settings.behavior.toolOutputsDefault.subtitle", - get: (p) => (p.toolOutputExpansion ?? "expanded") as ExpansionPreference, + get: (p) => (p.toolOutputExpansion ?? "collapsed") as ExpansionPreference, set: (next) => { if (updatePreferences) { updatePreferences({ toolOutputExpansion: next as ExpansionPreference }) @@ -418,7 +418,7 @@ export function getBehaviorCommands(actions: BehaviorRegistryActions): Command[] { id: "tool-output-default-visibility", label: () => { - const mode = actions.preferences().toolOutputExpansion || "expanded" + const mode = actions.preferences().toolOutputExpansion || "collapsed" const state = mode === "expanded" ? tGlobal("commands.common.expanded") : tGlobal("commands.common.collapsed") return tGlobal("commands.toolOutputsDefault.label", { state }) }, @@ -426,7 +426,7 @@ export function getBehaviorCommands(actions: BehaviorRegistryActions): Command[] category: "System", keywords: () => splitKeywords("commands.toolOutputsDefault.keywords"), action: () => { - const mode = actions.preferences().toolOutputExpansion || "expanded" + const mode = actions.preferences().toolOutputExpansion || "collapsed" const next: ExpansionPreference = mode === "expanded" ? "collapsed" : "expanded" actions.setToolOutputExpansion(next) }, diff --git a/packages/ui/src/stores/preferences.tsx b/packages/ui/src/stores/preferences.tsx index a39b07969..7eb2fd89f 100644 --- a/packages/ui/src/stores/preferences.tsx +++ b/packages/ui/src/stores/preferences.tsx @@ -141,7 +141,7 @@ const defaultUiSettings: UiSettings = { promptSubmitOnEnter: false, showPromptVoiceInput: true, diffViewMode: "split", - toolOutputExpansion: "expanded", + toolOutputExpansion: "collapsed", diagnosticsExpansion: "expanded", toolInputsVisibility: "collapsed", showUsageMetrics: true, diff --git a/packages/ui/src/styles/messaging/tool-call.css b/packages/ui/src/styles/messaging/tool-call.css index 3758cd521..e558413cb 100644 --- a/packages/ui/src/styles/messaging/tool-call.css +++ b/packages/ui/src/styles/messaging/tool-call.css @@ -99,6 +99,7 @@ background-color: transparent; color: var(--text-primary); border-bottom: 1px solid var(--tool-call-border-color); + min-width: 0; } .tool-call-header:hover { @@ -112,6 +113,7 @@ border-radius: 0; color: var(--text-primary); flex: 1; + min-width: 0; } .tool-call-header-toggle::before { @@ -186,6 +188,14 @@ .tool-call-summary { @apply flex-1 text-start inline-flex items-center gap-2; color: var(--text-primary); + min-width: 0; +} + +.tool-call-summary-text { + min-width: 0; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; } .tool-call-summary::before { @@ -247,7 +257,7 @@ white-space: pre-wrap; word-wrap: break-word; overflow-wrap: break-word; - max-height: var(--tool-call-max-height-compact, calc(25 * 1.4em)); + max-height: calc(6 * var(--tool-call-line-unit)); overflow-y: scroll; } From 08ee37e5e1da5bd8e21563784779da1ca0b7f84c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pascal=20Andr=C3=A9?= Date: Sat, 9 May 2026 01:55:34 +0200 Subject: [PATCH 2/2] fix(ui): simplify collapsed tool call header Remove the added collapsed preview so compact tool calls only rely on the existing header content. The tool label now sits inline with the collapsed header without the extra generic Tool call text or icon, keeping the collapsed state closer to the issue request while preserving the default collapsed preference. Validated with the existing UI typecheck and build before this commit; unrelated package-lock and Tauri generated changes were left unstaged. --- packages/ui/src/components/message-block.tsx | 3 --- packages/ui/src/components/tool-call.tsx | 16 +++++++--------- packages/ui/src/components/tool-call/utils.ts | 2 +- packages/ui/src/styles/messaging/tool-call.css | 5 +---- 4 files changed, 9 insertions(+), 17 deletions(-) diff --git a/packages/ui/src/components/message-block.tsx b/packages/ui/src/components/message-block.tsx index 07d6a9af7..f24d6a994 100644 --- a/packages/ui/src/components/message-block.tsx +++ b/packages/ui/src/components/message-block.tsx @@ -28,7 +28,6 @@ function DeleteUpToIcon() { ) } -const TOOL_ICON = "🔧" const USER_BORDER_COLOR = "var(--message-user-border)" const ASSISTANT_BORDER_COLOR = "var(--message-assistant-border)" const TOOL_BORDER_COLOR = "var(--message-tool-border)" @@ -612,8 +611,6 @@ function ToolCallItem(props: ToolCallItemProps) { /> - {TOOL_ICON} - {t("messageBlock.tool.header")} {toolName() || t("messageBlock.tool.unknown")} diff --git a/packages/ui/src/components/tool-call.tsx b/packages/ui/src/components/tool-call.tsx index d746110e9..376cffebd 100644 --- a/packages/ui/src/components/tool-call.tsx +++ b/packages/ui/src/components/tool-call.tsx @@ -866,19 +866,18 @@ export default function ToolCall(props: ToolCallProps) { const status = () => toolState()?.status || "" return ( -
{ +
{ setToolCallRootEl(element || undefined) }} class={`tool-call ${combinedStatusClass()}`} data-part-type="tool" data-tool-name={toolName()} data-instance-id={props.instanceId} - data-session-id={props.sessionId} - data-message-id={props.messageId} - data-part-id={toolCallIdentifier()} - > + data-session-id={props.sessionId} + data-message-id={props.messageId} + data-part-id={toolCallIdentifier()} + >
1 ? "true" : undefined}>