ui: add input/output accordions in tool calls

This commit is contained in:
Shantur Rathore
2026-02-19 18:37:46 +00:00
parent 5fd985f0c2
commit 6b7162f50f
2 changed files with 75 additions and 43 deletions

View File

@@ -172,7 +172,9 @@ export default function ToolCall(props: ToolCallProps) {
}) })
const [userExpanded, setUserExpanded] = createSignal<boolean | null>(null) const [userExpanded, setUserExpanded] = createSignal<boolean | null>(null)
const [inputExpanded, setInputExpanded] = createSignal(false) const [inputVisible, setInputVisible] = createSignal(false)
const [inputSectionExpanded, setInputSectionExpanded] = createSignal(false)
const [outputSectionExpanded, setOutputSectionExpanded] = createSignal(true)
const isPermissionActive = createMemo(() => { const isPermissionActive = createMemo(() => {
const pending = pendingPermission() const pending = pendingPermission()
@@ -589,13 +591,19 @@ export default function ToolCall(props: ToolCallProps) {
}) })
} }
const handleToggleInput = (event: MouseEvent) => { const handleToggleInputVisibility = (event: MouseEvent) => {
event.preventDefault() event.preventDefault()
event.stopPropagation() event.stopPropagation()
if (!expanded()) { if (!expanded()) {
toggle() toggle()
} }
setInputExpanded((prev) => !prev) setInputVisible((prev) => {
const next = !prev
if (!next) {
setInputSectionExpanded(false)
}
return next
})
} }
const renderer = createMemo(() => resolveToolRenderer(toolName())) const renderer = createMemo(() => resolveToolRenderer(toolName()))
@@ -728,17 +736,6 @@ export default function ToolCall(props: ToolCallProps) {
return renderer().renderBody(rendererContext) return renderer().renderBody(rendererContext)
} }
const renderToolBodyWithHeader = () => {
const body = renderToolBody()
if (!body) return null
return (
<>
<div class="tool-call-io-header">{t("toolCall.io.output")}</div>
{body}
</>
)
}
async function handlePermissionResponse(permission: PermissionRequestLike, response: "once" | "always" | "reject") { async function handlePermissionResponse(permission: PermissionRequestLike, response: "once" | "always" | "reject") {
if (!permission) return if (!permission) return
setPermissionSubmitting(true) setPermissionSubmitting(true)
@@ -854,14 +851,14 @@ export default function ToolCall(props: ToolCallProps) {
<button <button
type="button" type="button"
class="tool-call-header-input" class="tool-call-header-input"
onClick={handleToggleInput} onClick={handleToggleInputVisibility}
aria-pressed={inputExpanded()} aria-pressed={inputVisible()}
aria-label={ aria-label={
inputExpanded() inputVisible()
? t("toolCall.header.hideInputAriaLabel") ? t("toolCall.header.hideInputAriaLabel")
: t("toolCall.header.showInputAriaLabel") : t("toolCall.header.showInputAriaLabel")
} }
title={inputExpanded() ? t("toolCall.header.hideInputTitle") : t("toolCall.header.showInputTitle")} title={inputVisible() ? t("toolCall.header.hideInputTitle") : t("toolCall.header.showInputTitle")}
> >
<ArrowRightSquare class="w-3.5 h-3.5" /> <ArrowRightSquare class="w-3.5 h-3.5" />
</button> </button>
@@ -884,34 +881,48 @@ export default function ToolCall(props: ToolCallProps) {
{expanded() && ( {expanded() && (
<div class="tool-call-details"> <div class="tool-call-details">
<Show when={inputExpanded() && hasToolInput()}> <Show when={inputVisible() && hasToolInput()}>
{(() => { <button
const content = toolInputMarkdown() type="button"
if (!content) return null class="tool-call-io-toggle"
return ( aria-expanded={inputSectionExpanded()}
<> onClick={() => setInputSectionExpanded((prev) => !prev)}
<div class="tool-call-io-header">{t("toolCall.io.input")}</div> >
{renderMarkdownContent({ content, cacheKey: "input" })} <span class="tool-call-io-title">{t("toolCall.io.input")}</span>
</> </button>
)
})()} <Show when={inputSectionExpanded()}>
{(() => {
const content = toolInputMarkdown()
if (!content) return null
return renderMarkdownContent({ content, cacheKey: "input" })
})()}
</Show>
</Show> </Show>
<Show when={inputExpanded() && hasToolInput()} fallback={renderToolBody()}> <button
{renderToolBodyWithHeader()} type="button"
</Show> class="tool-call-io-toggle"
aria-expanded={outputSectionExpanded()}
onClick={() => setOutputSectionExpanded((prev) => !prev)}
>
<span class="tool-call-io-title">{t("toolCall.io.output")}</span>
</button>
{renderError()} <Show when={outputSectionExpanded()}>
{renderToolBody()}
{renderError()}
<Show when={status() === "pending" && !pendingPermission()}>
<div class="tool-call-pending-message">
<span class="spinner-small"></span>
<span>{t("toolCall.pending.waitingToRun")}</span>
</div>
</Show>
</Show>
{renderPermissionBlock()} {renderPermissionBlock()}
{renderQuestionBlock()} {renderQuestionBlock()}
<Show when={status() === "pending" && !pendingPermission()}>
<div class="tool-call-pending-message">
<span class="spinner-small"></span>
<span>{t("toolCall.pending.waitingToRun")}</span>
</div>
</Show>
</div> </div>
)} )}

View File

@@ -232,12 +232,33 @@
font-size: var(--font-size-xs); font-size: var(--font-size-xs);
} }
.tool-call-io-header { .tool-call-io-toggle {
@apply flex items-center justify-between gap-3 px-3 py-2; display: flex;
align-items: center;
justify-content: space-between;
gap: 0.75rem;
padding: 0.5rem;
background-color: var(--surface-secondary); background-color: var(--surface-secondary);
border: none;
border-top: 1px solid var(--tool-call-border-color); border-top: 1px solid var(--tool-call-border-color);
font-family: var(--font-family-mono); font-family: var(--font-family-mono);
font-size: 13px; font-size: 13px;
color: inherit;
cursor: pointer;
}
.tool-call-io-toggle::before {
content: "▶";
font-size: 11px;
margin-right: 0.35rem;
color: var(--text-secondary);
}
.tool-call-io-toggle[aria-expanded="true"]::before {
content: "▼";
}
.tool-call-io-title {
font-weight: var(--font-weight-semibold); font-weight: var(--font-weight-semibold);
color: var(--text-primary); color: var(--text-primary);
} }