diff --git a/packages/ui/src/components/message-block.tsx b/packages/ui/src/components/message-block.tsx index 07d6a9af..f24d6a99 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 af9be1eb..376cffeb 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 @@ -864,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}> @@ -969,9 +970,19 @@ export default function ToolCall(props: ToolCallProps) { setScrollTopSnapshot={setScrollTopSnapshot} /> - - + + {(preview) => ( +
+
+ {preview().kind === "output" ? t("toolCall.io.output") : t("toolCall.io.input")} +
+
{preview().text}
+
+ )} +
+ + {renderDiagnosticsSection( t, diagnosticsEntries(), diff --git a/packages/ui/src/components/tool-call/utils.ts b/packages/ui/src/components/tool-call/utils.ts index 766d8d99..4c52dab7 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 25bd13e4..07054f63 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 a39b0796..7eb2fd89 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 3758cd52..0f6df2e4 100644 --- a/packages/ui/src/styles/messaging/tool-call.css +++ b/packages/ui/src/styles/messaging/tool-call.css @@ -17,6 +17,7 @@ .tool-call-header-meta { @apply flex items-center gap-2; + min-width: 0; } .tool-call-header-button { @@ -61,10 +62,6 @@ display: inline-flex; } -.tool-call-header-label .tool-call-icon { - @apply text-base; -} - .tool-call-header-label .tool-name { font-family: var(--font-family-mono); color: inherit; @@ -99,6 +96,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 +110,7 @@ border-radius: 0; color: var(--text-primary); flex: 1; + min-width: 0; } .tool-call-header-toggle::before { @@ -186,6 +185,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 +254,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; }