Skip to content

Commit 9800afb

Browse files
feat(ui): toggle tool call input YAML (#182)
* feat(ui): toggle tool call input yaml * ui: rename tool input toggle and add IO headers * ui: add input/output accordions in tool calls * ui: refine tool IO accordion styling * ui: remove extra padding around IO sections * ui: remove semibold from IO headers * feat(ui): add tool input visibility preference * fix(ui): scope tool input toggle to current tool call * ui: left-align tool IO header text * fix(ui): let palette tool input visibility override per-call * ui: default tool input visibility to collapsed * fix(ui): expand read tool calls on error --------- Co-authored-by: Shantur Rathore <i@shantur.com>
1 parent e84adeb commit 9800afb

19 files changed

Lines changed: 364 additions & 16 deletions

File tree

package-lock.json

Lines changed: 2 additions & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

packages/ui/package.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,8 @@
3030
"shiki": "^3.13.0",
3131
"solid-js": "^1.8.0",
3232
"solid-toast": "^0.5.0",
33-
"tauri-plugin-keepawake-api": "^0.1.0"
33+
"tauri-plugin-keepawake-api": "^0.1.0",
34+
"yaml": "^2.4.2"
3435
},
3536
"devDependencies": {
3637
"@vite-pwa/assets-generator": "^1.0.2",

packages/ui/src/App.tsx

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -72,6 +72,7 @@ const App: Component = () => {
7272
setToolOutputExpansion,
7373
setDiagnosticsExpansion,
7474
setThinkingBlocksExpansion,
75+
setToolInputsVisibility,
7576
} = useConfig()
7677
const [escapeInDebounce, setEscapeInDebounce] = createSignal(false)
7778
interface LaunchErrorState {
@@ -402,6 +403,7 @@ const App: Component = () => {
402403
setToolOutputExpansion,
403404
setDiagnosticsExpansion,
404405
setThinkingBlocksExpansion,
406+
setToolInputsVisibility,
405407
handleNewInstanceRequest,
406408
handleCloseInstance,
407409
handleNewSession,

packages/ui/src/components/tool-call.tsx

Lines changed: 170 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import { createSignal, Show, createEffect, createMemo, onCleanup } from "solid-js"
2-
import { Copy } from "lucide-solid"
2+
import { ArrowRightSquare, Copy } from "lucide-solid"
3+
import { stringify as stringifyYaml } from "yaml"
34
import { messageStoreBus } from "../stores/message-v2/bus"
45
import { useTheme } from "../lib/theme"
56
import { useGlobalCache } from "../lib/hooks/use-global-cache"
@@ -27,7 +28,17 @@ import type {
2728
ToolRendererContext,
2829
ToolScrollHelpers,
2930
} from "./tool-call/types"
30-
import { getRelativePath, getToolIcon, getToolName, isToolStateCompleted, isToolStateError, isToolStateRunning, getDefaultToolAction } from "./tool-call/utils"
31+
import {
32+
ensureMarkdownContent,
33+
getRelativePath,
34+
getToolIcon,
35+
getToolName,
36+
isToolStateCompleted,
37+
isToolStateError,
38+
isToolStateRunning,
39+
getDefaultToolAction,
40+
readToolStatePayload,
41+
} from "./tool-call/utils"
3142
import { resolveTitleForTool } from "./tool-call/tool-title"
3243
import { getLogger } from "../lib/logger"
3344

@@ -155,12 +166,33 @@ export default function ToolCall(props: ToolCallProps) {
155166
const prefExpanded = toolOutputDefaultExpanded()
156167
const toolName = toolCallMemo()?.tool || ""
157168
if (toolName === "read") {
169+
const state = toolState()
170+
if (state?.status === "error") {
171+
return true
172+
}
158173
return false
159174
}
160175
return prefExpanded
161176
})
162177

163178
const [userExpanded, setUserExpanded] = createSignal<boolean | null>(null)
179+
const toolInputsVisibility = createMemo(() => preferences().toolInputsVisibility || "collapsed")
180+
const [toolInputVisibilityOverride, setToolInputVisibilityOverride] = createSignal<"hidden" | "expanded" | null>(null)
181+
const effectiveToolInputsVisibility = createMemo(() => toolInputVisibilityOverride() ?? toolInputsVisibility())
182+
const isToolInputVisible = createMemo(() => effectiveToolInputsVisibility() !== "hidden")
183+
const inputDefaultExpanded = createMemo(() => effectiveToolInputsVisibility() === "expanded")
184+
const [inputSectionOverride, setInputSectionOverride] = createSignal<boolean | null>(null)
185+
const [outputSectionOverride, setOutputSectionOverride] = createSignal<boolean | null>(null)
186+
const inputSectionExpanded = () => {
187+
const override = inputSectionOverride()
188+
if (override !== null) return override
189+
return inputDefaultExpanded()
190+
}
191+
const outputSectionExpanded = () => {
192+
const override = outputSectionOverride()
193+
if (override !== null) return override
194+
return true
195+
}
164196

165197
const isPermissionActive = createMemo(() => {
166198
const pending = pendingPermission()
@@ -183,6 +215,35 @@ export default function ToolCall(props: ToolCallProps) {
183215
return defaultExpandedForTool()
184216
}
185217

218+
const toolInput = createMemo(() => {
219+
const state = toolState()
220+
return readToolStatePayload(state).input
221+
})
222+
223+
const hasToolInput = createMemo(() => {
224+
const input = toolInput()
225+
return input && Object.keys(input).length > 0
226+
})
227+
228+
const toolInputMarkdown = createMemo(() => {
229+
const input = toolInput()
230+
if (!input || Object.keys(input).length === 0) return null
231+
232+
try {
233+
const yamlText = stringifyYaml(input)
234+
return ensureMarkdownContent(yamlText, "yaml", true)
235+
} catch (error) {
236+
log.error("Failed to convert tool call input to YAML", error)
237+
try {
238+
const jsonText = JSON.stringify(input, null, 2)
239+
return ensureMarkdownContent(jsonText, "json", true)
240+
} catch (nestedError) {
241+
log.error("Failed to stringify tool call input", nestedError)
242+
return null
243+
}
244+
}
245+
})
246+
186247
const permissionDetails = createMemo(() => pendingPermission()?.permission)
187248
const questionDetails = createMemo(() => pendingQuestion()?.request)
188249

@@ -548,6 +609,25 @@ export default function ToolCall(props: ToolCallProps) {
548609
})
549610
}
550611

612+
createEffect(() => {
613+
// When global preference changes, reset per-tool-call overrides so palette changes apply.
614+
toolInputsVisibility()
615+
setToolInputVisibilityOverride(null)
616+
setInputSectionOverride(null)
617+
setOutputSectionOverride(null)
618+
})
619+
620+
const handleToggleInputVisibility = (event: MouseEvent) => {
621+
event.preventDefault()
622+
event.stopPropagation()
623+
if (!expanded()) {
624+
toggle()
625+
}
626+
627+
const currentlyVisible = isToolInputVisible()
628+
setToolInputVisibilityOverride(currentlyVisible ? "hidden" : "expanded")
629+
}
630+
551631
const renderer = createMemo(() => resolveToolRenderer(toolName()))
552632

553633
const { renderAnsiContent } = createAnsiContentRenderer({
@@ -789,6 +869,23 @@ export default function ToolCall(props: ToolCallProps) {
789869
</span>
790870
</button>
791871

872+
<Show when={hasToolInput()}>
873+
<button
874+
type="button"
875+
class="tool-call-header-input"
876+
onClick={handleToggleInputVisibility}
877+
aria-pressed={isToolInputVisible()}
878+
aria-label={
879+
isToolInputVisible()
880+
? t("toolCall.header.hideInputAriaLabel")
881+
: t("toolCall.header.showInputAriaLabel")
882+
}
883+
title={isToolInputVisible() ? t("toolCall.header.hideInputTitle") : t("toolCall.header.showInputTitle")}
884+
>
885+
<ArrowRightSquare class="w-3.5 h-3.5" />
886+
</button>
887+
</Show>
888+
792889
<button
793890
type="button"
794891
class="tool-call-header-copy"
@@ -806,19 +903,79 @@ export default function ToolCall(props: ToolCallProps) {
806903

807904
{expanded() && (
808905
<div class="tool-call-details">
809-
{renderToolBody()}
810-
811-
{renderError()}
812-
813-
{renderPermissionBlock()}
814-
{renderQuestionBlock()}
815-
816-
<Show when={status() === "pending" && !pendingPermission()}>
817-
<div class="tool-call-pending-message">
818-
<span class="spinner-small"></span>
819-
<span>{t("toolCall.pending.waitingToRun")}</span>
906+
<Show
907+
when={isToolInputVisible() && hasToolInput()}
908+
fallback={
909+
<>
910+
{renderToolBody()}
911+
{renderError()}
912+
913+
<Show when={status() === "pending" && !pendingPermission()}>
914+
<div class="tool-call-pending-message">
915+
<span class="spinner-small"></span>
916+
<span>{t("toolCall.pending.waitingToRun")}</span>
917+
</div>
918+
</Show>
919+
</>
920+
}
921+
>
922+
<div class="tool-call-io-sections">
923+
<div class="tool-call-io-section">
924+
<button
925+
type="button"
926+
class="tool-call-io-toggle"
927+
aria-expanded={inputSectionExpanded()}
928+
onClick={() => setInputSectionOverride((prev) => {
929+
const current = prev === null ? inputSectionExpanded() : prev
930+
return !current
931+
})}
932+
>
933+
<span class="tool-call-io-title">{t("toolCall.io.input")}</span>
934+
</button>
935+
936+
<Show when={inputSectionExpanded()}>
937+
<div class="tool-call-io-body">
938+
{(() => {
939+
const content = toolInputMarkdown()
940+
if (!content) return null
941+
return renderMarkdownContent({ content, cacheKey: "input" })
942+
})()}
943+
</div>
944+
</Show>
945+
</div>
946+
947+
<div class="tool-call-io-section">
948+
<button
949+
type="button"
950+
class="tool-call-io-toggle"
951+
aria-expanded={outputSectionExpanded()}
952+
onClick={() => setOutputSectionOverride((prev) => {
953+
const current = prev === null ? outputSectionExpanded() : prev
954+
return !current
955+
})}
956+
>
957+
<span class="tool-call-io-title">{t("toolCall.io.output")}</span>
958+
</button>
959+
960+
<Show when={outputSectionExpanded()}>
961+
<div class="tool-call-io-body">
962+
{renderToolBody()}
963+
{renderError()}
964+
965+
<Show when={status() === "pending" && !pendingPermission()}>
966+
<div class="tool-call-pending-message">
967+
<span class="spinner-small"></span>
968+
<span>{t("toolCall.pending.waitingToRun")}</span>
969+
</div>
970+
</Show>
971+
</div>
972+
</Show>
973+
</div>
820974
</div>
821975
</Show>
976+
977+
{renderPermissionBlock()}
978+
{renderQuestionBlock()}
822979
</div>
823980
)}
824981

packages/ui/src/lib/hooks/use-commands.ts

Lines changed: 25 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import { createSignal, onMount } from "solid-js"
22
import type { Accessor } from "solid-js"
3-
import type { Preferences, ExpansionPreference } from "../../stores/preferences"
3+
import type { Preferences, ExpansionPreference, ToolInputsVisibilityPreference } from "../../stores/preferences"
44
import { createCommandRegistry, type Command } from "../commands"
55
import { instances, activeInstanceId, setActiveInstanceId } from "../../stores/instances"
66
import type { ClientPart, MessageInfo } from "../../types/message"
@@ -38,6 +38,7 @@ export interface UseCommandsOptions {
3838
setToolOutputExpansion: (mode: ExpansionPreference) => void
3939
setDiagnosticsExpansion: (mode: ExpansionPreference) => void
4040
setThinkingBlocksExpansion: (mode: ExpansionPreference) => void
41+
setToolInputsVisibility: (mode: ToolInputsVisibilityPreference) => void
4142
handleNewInstanceRequest: () => void
4243
handleCloseInstance: (instanceId: string) => Promise<void>
4344
handleNewSession: (instanceId: string) => Promise<void>
@@ -551,6 +552,29 @@ export function useCommands(options: UseCommandsOptions) {
551552
},
552553
})
553554

555+
commandRegistry.register({
556+
id: "tool-inputs-visibility",
557+
label: () => {
558+
const mode = options.preferences().toolInputsVisibility || "hidden"
559+
const state =
560+
mode === "expanded"
561+
? tGlobal("commands.common.expanded")
562+
: mode === "collapsed"
563+
? tGlobal("commands.common.collapsed")
564+
: tGlobal("commands.common.hidden")
565+
return tGlobal("commands.toolInputsVisibility.label", { state })
566+
},
567+
description: () => tGlobal("commands.toolInputsVisibility.description"),
568+
category: "System",
569+
keywords: () => splitKeywords("commands.toolInputsVisibility.keywords"),
570+
action: () => {
571+
const mode = options.preferences().toolInputsVisibility || "hidden"
572+
const next: ToolInputsVisibilityPreference =
573+
mode === "hidden" ? "collapsed" : mode === "collapsed" ? "expanded" : "hidden"
574+
options.setToolInputsVisibility(next)
575+
},
576+
})
577+
554578
commandRegistry.register({
555579
id: "token-usage-visibility",
556580
label: () => {

packages/ui/src/lib/i18n/messages/en/commands.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -130,6 +130,10 @@ export const commandMessages = {
130130
"commands.diagnosticsDefault.description": "Toggle default expansion for diagnostics output",
131131
"commands.diagnosticsDefault.keywords": "diagnostics, expand, collapse",
132132

133+
"commands.toolInputsVisibility.label": "Tool Inputs Visibility · {state}",
134+
"commands.toolInputsVisibility.description": "Set default visibility for tool call input arguments",
135+
"commands.toolInputsVisibility.keywords": "tool, inputs, arguments, visibility, hide, show, expand, collapse",
136+
133137
"commands.tokenUsageDisplay.label": "Token Usage Display · {state}",
134138
"commands.tokenUsageDisplay.description": "Show or hide token and cost stats for assistant messages",
135139
"commands.tokenUsageDisplay.keywords": "token, usage, cost, stats",

packages/ui/src/lib/i18n/messages/en/toolCall.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,14 @@ export const toolCallMessages = {
55
"toolCall.header.copyTitle": "Copy tool call title",
66
"toolCall.header.copyAriaLabel": "Copy tool call title",
77

8+
"toolCall.header.showInputTitle": "Show Tool Arguments",
9+
"toolCall.header.showInputAriaLabel": "Show Tool Arguments",
10+
"toolCall.header.hideInputTitle": "Hide Tool Arguments",
11+
"toolCall.header.hideInputAriaLabel": "Hide Tool Arguments",
12+
13+
"toolCall.io.input": "Tool Input",
14+
"toolCall.io.output": "Tool Output",
15+
816
"toolCall.diff.label": "Diff",
917
"toolCall.diff.label.withPath": "Diff · {path}",
1018
"toolCall.diff.viewMode.ariaLabel": "Diff view mode",

packages/ui/src/lib/i18n/messages/es/commands.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -130,6 +130,10 @@ export const commandMessages = {
130130
"commands.diagnosticsDefault.description": "Alternar la expansión por defecto de la salida de diagnósticos",
131131
"commands.diagnosticsDefault.keywords": "diagnósticos, expandir, colapsar",
132132

133+
"commands.toolInputsVisibility.label": "Visibilidad de entradas de herramientas · {state}",
134+
"commands.toolInputsVisibility.description": "Configurar la visibilidad por defecto de los argumentos de entrada de llamadas de herramienta",
135+
"commands.toolInputsVisibility.keywords": "herramienta, entradas, argumentos, visibilidad, ocultar, mostrar, expandir, colapsar",
136+
133137
"commands.tokenUsageDisplay.label": "Mostrar uso de tokens · {state}",
134138
"commands.tokenUsageDisplay.description": "Mostrar u ocultar estadísticas de tokens y costo en los mensajes del asistente",
135139
"commands.tokenUsageDisplay.keywords": "token, uso, costo, estadísticas",

packages/ui/src/lib/i18n/messages/es/toolCall.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,14 @@ export const toolCallMessages = {
55
"toolCall.header.copyTitle": "Copy tool call title",
66
"toolCall.header.copyAriaLabel": "Copy tool call title",
77

8+
"toolCall.header.showInputTitle": "Show Tool Arguments",
9+
"toolCall.header.showInputAriaLabel": "Show Tool Arguments",
10+
"toolCall.header.hideInputTitle": "Hide Tool Arguments",
11+
"toolCall.header.hideInputAriaLabel": "Hide Tool Arguments",
12+
13+
"toolCall.io.input": "Tool Input",
14+
"toolCall.io.output": "Tool Output",
15+
816
"toolCall.diff.label": "Diff",
917
"toolCall.diff.label.withPath": "Diff · {path}",
1018
"toolCall.diff.viewMode.ariaLabel": "Modo de vista de diff",

packages/ui/src/lib/i18n/messages/fr/commands.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -130,6 +130,10 @@ export const commandMessages = {
130130
"commands.diagnosticsDefault.description": "Choisir l'ouverture par défaut de la sortie des diagnostics",
131131
"commands.diagnosticsDefault.keywords": "diagnostics, développer, réduire",
132132

133+
"commands.toolInputsVisibility.label": "Visibilité des entrées d'outil · {state}",
134+
"commands.toolInputsVisibility.description": "Définir la visibilité par défaut des arguments d'entrée des appels d'outil",
135+
"commands.toolInputsVisibility.keywords": "outil, entrées, arguments, visibilité, masquer, afficher, développer, réduire",
136+
133137
"commands.tokenUsageDisplay.label": "Affichage de l'usage des tokens · {state}",
134138
"commands.tokenUsageDisplay.description": "Afficher ou masquer les stats de tokens et de coût pour les messages de l'assistant",
135139
"commands.tokenUsageDisplay.keywords": "token, usage, coût, stats",

0 commit comments

Comments
 (0)