fix(vscode): polish chat tool call previews

This commit is contained in:
Iván Uruchurtu
2026-06-11 19:37:26 -06:00
parent 22149292ea
commit 481f8361cf
13 changed files with 560 additions and 116 deletions
@@ -0,0 +1,5 @@
---
"kilo-code": patch
---
Polish chat tool-call previews for shell, search, and background-process output.
+195 -15
View File
@@ -35,14 +35,23 @@
[data-slot="basic-tool-tool-subtitle"] {
font-size: var(--kilo-font-size-12);
color: var(--text-weak);
}
[data-slot="basic-tool-tool-title"] {
font-size: var(--kilo-font-size-14);
font-size: var(--kilo-font-size-12);
font-weight: var(--font-weight-regular);
color: var(--text-weak);
}
[data-slot="message-part-title-text"] {
font-size: var(--kilo-font-size-14);
font-size: var(--kilo-font-size-12);
font-weight: var(--font-weight-regular);
}
[data-slot="basic-tool-tool-arg"] {
font-size: var(--kilo-font-size-12);
color: var(--text-weak);
}
[data-slot="message-part-title-filename"] {
@@ -108,7 +117,7 @@ html[data-theme="kilo-vscode"] [data-component="tool-trigger"] {
align-items: center;
justify-content: center;
flex-shrink: 0;
color: var(--icon-base);
color: var(--text-weak);
[data-component="icon"] {
display: flex;
@@ -118,27 +127,71 @@ html[data-theme="kilo-vscode"] [data-component="tool-trigger"] {
/* Card styling for tool calls in the VS Code sidebar theme */
html[data-theme="kilo-vscode"] [data-component="tool-part-wrapper"][data-part-type="tool"] {
border-radius: var(--radius-md);
border: 1px solid var(--border-weak-base, var(--vscode-panel-border));
overflow: hidden;
border-radius: 0;
border: 0;
overflow: visible;
/* Header trigger — add bottom border when the collapsible is open */
[data-component="collapsible"].tool-collapsible {
gap: 0px;
gap: 8px;
border-radius: 0;
[data-slot="collapsible-trigger"] {
padding: 0 8px;
height: 36px;
background-color: var(--surface-inset-base, var(--vscode-sideBar-background));
margin-inline: -6px;
width: calc(100% + 12px);
padding: 2px 6px;
min-height: 24px;
height: auto;
background-color: transparent;
&:hover {
background-color: var(--surface-inset-base-hover, var(--vscode-list-hoverBackground));
background-color: transparent;
}
}
[data-slot="basic-tool-tool-trigger-content"] {
align-self: center;
gap: 6px;
}
[data-slot="basic-tool-tool-info-structured"],
[data-slot="basic-tool-tool-info-main"] {
gap: 6px;
}
[data-slot="basic-tool-tool-title"]
+ :is(
[data-slot="basic-tool-tool-subtitle"],
[data-slot="basic-tool-tool-arg"],
[data-slot="message-part-meta-line"],
[data-slot="webfetch-meta"],
[data-component="shell-submessage"]
)::before {
content: "·";
margin-right: 6px;
color: var(--text-weak);
}
[data-slot="basic-tool-tool-title"],
[data-slot="basic-tool-tool-subtitle"],
[data-slot="basic-tool-tool-arg"],
[data-slot="message-part-title-text"],
[data-slot="message-part-title-filename"],
[data-slot="message-part-directory-inline"],
[data-slot="message-part-meta-line"] {
line-height: 16px;
}
[data-slot="collapsible-trigger"][aria-expanded="true"] {
border-bottom: 1px solid var(--border-weak-base, var(--vscode-panel-border));
border-bottom: 0;
}
[data-slot="collapsible-trigger"]:has([data-slot="collapsible-arrow"]) {
border-radius: 4px;
cursor: pointer;
transition:
background-color 0.15s ease,
color 0.15s ease;
}
/* Arrow always visible (no fade-on-hover) */
@@ -146,13 +199,32 @@ html[data-theme="kilo-vscode"] [data-component="tool-part-wrapper"][data-part-ty
opacity: 1;
}
/* Arrow: DOWN when collapsed, UP when expanded (matches question-dock toggle) */
/* Arrow: RIGHT when collapsed, DOWN when expanded. */
[data-slot="collapsible-arrow-icon"] {
transform: translateZ(0) rotate(0deg);
transform: translateZ(0) rotate(-90deg);
}
[data-slot="collapsible-trigger"][aria-expanded="true"] [data-slot="collapsible-arrow-icon"] {
transform: translateZ(0) rotate(180deg);
transform: translateZ(0) rotate(0deg);
}
[data-slot="collapsible-trigger"]:has([data-slot="collapsible-arrow"]):hover {
background-color: var(--vscode-list-hoverBackground, var(--surface-raised-base-hover));
[data-slot="basic-tool-tool-title"],
[data-slot="basic-tool-tool-subtitle"],
[data-slot="basic-tool-tool-arg"],
[data-slot="message-part-title-text"],
[data-slot="message-part-title-filename"],
[data-slot="message-part-directory-inline"],
[data-slot="message-part-meta-line"] {
color: var(--vscode-list-hoverForeground, var(--vscode-foreground, var(--text-strong)));
}
[data-slot="basic-tool-icon"],
[data-slot="collapsible-arrow-icon"] {
color: var(--vscode-list-hoverForeground, var(--vscode-foreground, var(--icon-base)));
}
}
}
@@ -174,6 +246,76 @@ html[data-theme="kilo-vscode"] [data-component="tool-part-wrapper"][data-part-ty
margin-top: 4px;
}
[data-component="context-tool-group-trigger"] {
min-height: 20px;
[data-slot="context-tool-group-title"] {
gap: 6px;
font-size: var(--kilo-font-size-12);
font-weight: var(--font-weight-regular);
line-height: 16px;
color: var(--text-weak);
}
[data-slot="context-tool-group-summary"] {
color: var(--text-weak);
}
[data-slot="context-tool-group-summary"]::before {
content: "·";
margin-right: 6px;
color: var(--text-weak);
}
}
[data-component="context-tool-expanded-list"] {
max-height: 180px;
overflow-y: auto;
padding: 4px 0;
border: 1px solid var(--border-weak-base);
background: color-mix(
in srgb,
var(--vscode-terminal-background, var(--vscode-panel-background)) 92%,
black 8%
);
scrollbar-width: none;
&::-webkit-scrollbar {
display: none;
}
}
[data-component="context-tool-expanded-row"],
[data-component="context-tool-rolling-row"] {
display: grid;
grid-template-columns: max-content minmax(0, 1fr);
align-items: baseline;
column-gap: 8px;
min-height: 20px;
font-size: var(--kilo-font-size-12);
line-height: 16px;
}
[data-component="context-tool-expanded-row"] {
padding: 2px 12px;
}
[data-slot="context-tool-expanded-action"],
[data-slot="context-tool-rolling-action"] {
color: var(--text-weak);
}
[data-slot="context-tool-expanded-detail"],
[data-slot="context-tool-rolling-detail"] {
min-width: 0;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
color: var(--vscode-terminal-foreground, var(--vscode-editor-foreground));
font-family: var(--font-family-mono);
font-feature-settings: var(--font-family-mono--font-feature-settings);
}
/* Expandable tool output content */
[data-component="tool-output"] {
padding: 8px 12px;
@@ -186,4 +328,42 @@ html[data-theme="kilo-vscode"] [data-component="tool-part-wrapper"][data-part-ty
overflow-y: auto;
}
}
[data-component="tool-output"][data-variant="preview"] {
max-height: 240px;
padding: 8px 12px 10px;
border: 1px solid var(--border-weak-base);
background: color-mix(
in srgb,
var(--vscode-terminal-background, var(--vscode-panel-background)) 92%,
black 8%
);
color: var(--vscode-terminal-foreground, var(--vscode-editor-foreground));
font-family: var(--font-family-mono);
font-feature-settings: var(--font-family-mono--font-feature-settings);
font-size: var(--kilo-font-size-12);
line-height: var(--line-height-large);
white-space: pre-wrap;
overflow-wrap: anywhere;
word-break: normal;
scrollbar-width: none;
&::-webkit-scrollbar {
display: none;
}
[data-component="markdown"] {
color: inherit;
font: inherit;
line-height: inherit;
white-space: inherit;
overflow-wrap: inherit;
}
[data-component="markdown"] > *,
[data-component="markdown"] p,
[data-component="markdown"] pre {
margin: 0;
}
}
}
@@ -6,13 +6,13 @@
}
}
/* VS Code theme: tool-collapsible arrows point DOWN (collapsed) and UP (expanded), never right */
/* VS Code theme: tool-collapsible arrows point RIGHT when collapsed and DOWN when expanded. */
html[data-theme="kilo-vscode"] [data-component="collapsible"].tool-collapsible {
[data-slot="collapsible-arrow-icon"] {
transform: translateZ(0) rotate(0deg);
transform: translateZ(0) rotate(-90deg);
}
[data-slot="collapsible-trigger"][aria-expanded="true"] [data-slot="collapsible-arrow-icon"] {
transform: translateZ(0) rotate(180deg);
transform: translateZ(0) rotate(0deg);
}
}
@@ -210,23 +210,68 @@
html[data-theme="kilo-vscode"] [data-component="bash-output"] {
border-radius: 0;
background: var(--vscode-terminal-background, var(--vscode-panel-background));
border: 0;
background: transparent;
overflow: visible;
[data-slot="bash-terminal"] {
background: color-mix(
in srgb,
var(--vscode-terminal-background, var(--vscode-panel-background)) 92%,
black 8%
);
}
[data-slot="bash-pre"] code {
color: var(--vscode-terminal-foreground, var(--vscode-editor-foreground));
}
[data-slot="bash-divider"] {
background: var(--vscode-panel-border, var(--border-weak-base));
}
}
/* Bash tool: section layout (command + output) */
/* Bash tool: static terminal preview with separate command/output surfaces. */
[data-component="bash-output"] {
display: flex;
flex-direction: column;
gap: 4px;
border: 0;
border-radius: 0;
background: transparent;
overflow: visible;
}
[data-component="bash-output"] [data-slot="bash-terminal"] {
border-radius: 0;
border: 1px solid var(--border-weak-base);
background: var(--surface-raised-base);
overflow: hidden;
}
[data-component="bash-output"] [data-slot="bash-section"] {
position: relative;
}
[data-component="bash-output"] [data-slot="bash-section"][data-kind="command"] {
display: grid;
grid-template-columns: max-content minmax(0, 1fr);
align-items: start;
column-gap: 8px;
padding: 8px 12px 7px;
}
[data-component="bash-output"] [data-slot="bash-section"][data-kind="output"] {
padding: 8px 12px 10px;
}
[data-component="bash-output"] [data-slot="bash-prompt"] {
color: var(--text-weak, var(--vscode-descriptionForeground));
font-family: var(--font-family-mono);
font-feature-settings: var(--font-family-mono--font-feature-settings);
font-size: var(--kilo-font-size-12);
line-height: var(--line-height-large);
user-select: none;
}
[data-component="bash-output"] [data-slot="bash-section-code"] {
min-width: 0;
overflow-y: auto;
overflow-x: hidden;
max-height: 240px;
@@ -254,25 +299,22 @@ html[data-theme="kilo-vscode"] [data-component="bash-output"] {
pointer-events: auto;
}
/* Edge-to-edge divider (matches MCP tool divider) */
[data-component="bash-output"] [data-slot="bash-divider"] {
height: 1px;
background: var(--border-weak-base);
[data-component="bash-output"] [data-slot="bash-pre"] {
margin: 0;
padding: 0;
}
/* Shiki syntax-highlighted shell output */
[data-component="bash-output"] .shiki,
[data-component="shell-expanded-output"] .shiki {
margin: 0;
padding: 12px;
background: transparent !important;
}
[data-component="bash-output"] .shiki code,
[data-component="bash-output"] [data-slot="bash-pre"] code,
[data-component="shell-expanded-output"] .shiki code {
font-family: var(--font-family-mono);
font-feature-settings: var(--font-family-mono--font-feature-settings);
font-size: var(--font-size-small);
font-size: var(--kilo-font-size-12);
line-height: var(--line-height-large);
white-space: pre-wrap;
overflow-wrap: anywhere;
@@ -56,8 +56,6 @@ import { ShellRollingResults } from "./shell-rolling-results"
import { reasoningHeading } from "./reasoning-heading"
import { extractFilePathFromHref } from "../file-path"
import { normalize } from "./session-diff"
import { deferredHighlight } from "../context/marked"
import { escapeHtml } from "../util/escape-html"
// Windows CLI tools (e.g. winget) use \r to overwrite progress bars in-place.
// Without this, every progress frame renders as a separate visual line.
@@ -1846,7 +1844,7 @@ ToolRegistry.register({
>
<Show when={props.output}>
{(output) => (
<div data-component="tool-output" data-scrollable>
<div data-component="tool-output" data-variant="preview" data-scrollable>
<Markdown text={output()} />
</div>
)}
@@ -1877,7 +1875,7 @@ ToolRegistry.register({
>
<Show when={props.output}>
{(output) => (
<div data-component="tool-output" data-scrollable>
<div data-component="tool-output" data-variant="preview" data-scrollable>
<Markdown text={output()} />
</div>
)}
@@ -1911,7 +1909,7 @@ ToolRegistry.register({
>
<Show when={props.output}>
{(output) => (
<div data-component="tool-output" data-scrollable>
<div data-component="tool-output" data-variant="preview" data-scrollable>
<Markdown text={output()} />
</div>
)}
@@ -2111,37 +2109,6 @@ function BashCopyButton(props: { value: () => string; label: string }) {
function BashHighlightedOutput(props: { cmd: string; output: string; outputPath?: string; active?: boolean }) {
const data = useData()
const i18n = useI18n()
const cmdState = { signal: { aborted: false } }
const outState = { signal: { aborted: false } }
let cmdRef: HTMLDivElement | undefined
let outRef: HTMLDivElement | undefined
createEffect(() => {
cmdState.signal.aborted = true
if (!props.active) return
const cmd = props.cmd
if (!cmdRef || !cmd) return
const signal = { aborted: false }
cmdState.signal = signal
cmdRef.innerHTML = `<pre data-slot="bash-pre"><code data-lang="shellscript">${escapeHtml(cmd)}</code></pre>`
void deferredHighlight(cmdRef, undefined, signal)
})
createEffect(() => {
outState.signal.aborted = true
if (!props.active) return
const out = props.output
if (!outRef || !out) return
const signal = { aborted: false }
outState.signal = signal
outRef.innerHTML = `<pre data-slot="bash-pre"><code data-lang="log">${escapeHtml(out)}</code></pre>`
void deferredHighlight(outRef, undefined, signal)
})
onCleanup(() => {
cmdState.signal.aborted = true
outState.signal.aborted = true
})
const openInEditor = () => {
// When output was truncated, open the full output file on disk
@@ -2156,34 +2123,40 @@ function BashHighlightedOutput(props: { cmd: string; output: string; outputPath?
return (
<div data-component="bash-output">
<Show when={props.cmd}>
<div data-slot="mcp-section-label">{i18n.t("ui.messagePart.shell.command")}</div>
<div data-slot="bash-section">
<div data-slot="bash-section-code" ref={cmdRef} />
<div data-slot="bash-section-actions">
<BashCopyButton value={() => props.cmd} label={i18n.t("ui.message.copy")} />
<div data-slot="bash-terminal" data-kind="command">
<div data-slot="bash-section" data-kind="command">
<span data-slot="bash-prompt" aria-hidden="true">
$
</span>
<div data-slot="bash-section-code">
<pre data-slot="bash-pre"><code>{props.cmd}</code></pre>
</div>
<div data-slot="bash-section-actions">
<BashCopyButton value={() => props.cmd} label={i18n.t("ui.message.copy")} />
</div>
</div>
</div>
</Show>
<Show when={props.cmd && props.output}>
<div data-slot="bash-divider" />
</Show>
<Show when={props.output}>
<div data-slot="mcp-section-label">{i18n.t("ui.messagePart.shell.output")}</div>
<div data-slot="bash-section">
<div data-slot="bash-section-code" data-scrollable ref={outRef} />
<div data-slot="bash-section-actions">
<Show when={data.openContent || (props.outputPath && data.openFile)}>
<Tooltip value={i18n.t("ui.messagePart.openInEditor")} placement="bottom" gutter={4}>
<IconButton
icon="square-arrow-top-right"
size="small"
variant="ghost"
onClick={openInEditor}
aria-label={i18n.t("ui.messagePart.openInEditor")}
/>
</Tooltip>
</Show>
<BashCopyButton value={() => props.output} label={i18n.t("ui.message.copy")} />
<div data-slot="bash-terminal" data-kind="output">
<div data-slot="bash-section" data-kind="output">
<div data-slot="bash-section-code" data-scrollable>
<pre data-slot="bash-pre"><code>{props.output}</code></pre>
</div>
<div data-slot="bash-section-actions">
<Show when={data.openContent || (props.outputPath && data.openFile)}>
<Tooltip value={i18n.t("ui.messagePart.openInEditor")} placement="bottom" gutter={4}>
<IconButton
icon="square-arrow-top-right"
size="small"
variant="ghost"
onClick={openInEditor}
aria-label={i18n.t("ui.messagePart.openInEditor")}
/>
</Tooltip>
</Show>
<BashCopyButton value={() => props.output} label={i18n.t("ui.message.copy")} />
</div>
</div>
</div>
</Show>
@@ -2223,7 +2196,6 @@ ToolRegistry.register({
<BasicTool
{...props}
icon="console"
animated
hasDetails
defaultOpen={props.defaultOpen ?? true}
onOpenChange={setOpen}
@@ -12,7 +12,7 @@ import { TextShimmer } from "./text-shimmer"
import { Tooltip } from "./tooltip"
import { GROW_SPRING } from "./motion"
import { useSpring } from "./motion-spring"
import { busy, createThrottledValue, updateScrollMask, useCollapsible, useRowWipe, useToolFade } from "./tool-utils"
import { busy, createThrottledValue, updateScrollMask, useRowWipe, useToolFade } from "./tool-utils"
import { readToolOpen, toolOpenKey, writeToolOpen } from "./tool-open-state"
function ShellRollingSubtitle(props: { text: string; animate?: boolean }) {
@@ -88,8 +88,6 @@ function ShellExpanded(props: { cmd: string; out: string; open: boolean }) {
const rowHeight = 22
const max = rows * rowHeight
let contentRef: HTMLDivElement | undefined
let bodyRef: HTMLDivElement | undefined
let scrollRef: HTMLDivElement | undefined
let topRef: HTMLDivElement | undefined
const [copied, setCopied] = createSignal(false)
@@ -104,11 +102,6 @@ function ShellExpanded(props: { cmd: string; out: string; open: boolean }) {
setCap(Math.max(rowHeight * 2, max - top - (props.out ? 1 : 0)))
}
const measure = () => {
resize()
return Math.ceil(bodyRef?.getBoundingClientRect().height ?? 0)
}
onMount(() => {
resize()
if (!topRef) return
@@ -120,20 +113,13 @@ function ShellExpanded(props: { cmd: string; out: string; open: boolean }) {
createEffect(() => {
props.cmd
props.out
if (!props.open) return
queueMicrotask(() => {
resize()
updateMask()
})
})
useCollapsible({
content: () => contentRef,
body: () => bodyRef,
open: () => props.open,
measure,
onOpen: updateMask,
})
const handleCopy = async (e: MouseEvent) => {
e.stopPropagation()
const cmd = props.cmd ? `$ ${props.cmd}` : ""
@@ -145,8 +131,14 @@ function ShellExpanded(props: { cmd: string; out: string; open: boolean }) {
}
return (
<div ref={contentRef} style={{ overflow: "clip", height: "0px", display: "none" }}>
<div ref={bodyRef} data-component="shell-expanded-shell">
<div
style={{
overflow: "clip",
height: props.open ? "auto" : "0px",
display: props.open ? "block" : "none",
}}
>
<div data-component="shell-expanded-shell">
<div data-slot="shell-expanded-body">
<div ref={topRef} data-slot="shell-expanded-top">
<div data-slot="shell-expanded-command">
+56 -1
View File
@@ -1,6 +1,6 @@
/** @jsxImportSource solid-js */
import type { Meta, StoryObj } from "storybook-solidjs-vite"
import { Icon } from "@opencode-ai/ui/icon"
import { Icon } from "../components/icon"
const meta: Meta<typeof Icon> = {
title: "Components/Icon",
@@ -89,6 +89,26 @@ const iconNames = [
"layout-bottom",
] as const
const toolIcons = [
{ tool: "Read", icon: "glasses", note: "File reads" },
{ tool: "List", icon: "bullet-list", note: "Directory lists" },
{ tool: "Glob", icon: "magnifying-glass-menu", note: "File pattern search" },
{ tool: "Grep", icon: "magnifying-glass-menu", note: "Content search" },
{ tool: "WebFetch", icon: "window-cursor", note: "Fetch URL" },
{ tool: "WebSearch", icon: "window-cursor", note: "Search web" },
{ tool: "CodeSearch", icon: "code", note: "Code search" },
{ tool: "Task", icon: "task", note: "Subagent" },
{ tool: "Bash", icon: "console", note: "Shell command" },
{ tool: "Edit", icon: "code-lines", note: "Modify file" },
{ tool: "Write", icon: "code-lines", note: "Write file" },
{ tool: "Apply Patch", icon: "code-lines", note: "Patch files" },
{ tool: "TodoWrite", icon: "checklist", note: "Update todos" },
{ tool: "TodoRead", icon: "checklist", note: "Read todos" },
{ tool: "Question", icon: "bubble-5", note: "Ask user" },
{ tool: "Skill", icon: "brain", note: "Load skill" },
{ tool: "MCP/default", icon: "mcp", note: "Fallback tool" },
] as const
export const AllIcons: Story = {
render: () => (
<div
@@ -116,3 +136,38 @@ export const AllIcons: Story = {
),
parameters: { layout: "fullscreen" },
}
export const ToolCallIcons: Story = {
render: () => (
<div
style={{
display: "grid",
"grid-template-columns": "repeat(auto-fill, minmax(180px, 1fr))",
gap: "8px",
padding: "16px",
}}
>
{toolIcons.map((item) => (
<div
style={{
display: "grid",
"grid-template-columns": "24px 1fr",
gap: "8px",
"align-items": "center",
padding: "8px",
"border-radius": "6px",
background: "var(--surface-inset-base)",
}}
>
<Icon name={item.icon} size="normal" />
<div style={{ display: "flex", "flex-direction": "column", gap: "2px", "min-width": "0" }}>
<span style={{ "font-size": "12px", color: "var(--text-strong)" }}>{item.tool}</span>
<span style={{ "font-size": "11px", color: "var(--text-weak)" }}>{item.icon}</span>
<span style={{ "font-size": "11px", color: "var(--text-weaker)" }}>{item.note}</span>
</div>
</div>
))}
</div>
),
parameters: { layout: "fullscreen" },
}
@@ -1,6 +1,6 @@
/** @jsxImportSource solid-js */
import type { Meta, StoryObj } from "storybook-solidjs-vite"
import { UserMessageDisplay, AssistantParts } from "../components/message-part"
import { UserMessageDisplay, AssistantParts, Part } from "../components/message-part"
import { DataProvider } from "@opencode-ai/ui/context/data"
import { DiffComponentProvider } from "@kilocode/kilo-ui/context/diff"
import { CodeComponentProvider } from "@kilocode/kilo-ui/context/code"
@@ -274,6 +274,38 @@ const mockDataContextGroup = createMockData([completedToolPart, grepCompleted, g
const mockDataEdit = createMockData([editCompletedPart])
const mockDataWrite = createMockData([writeCompletedPart])
const vscodeReadPart: ToolPart = {
...completedToolPart,
id: "part-vscode-read-001",
callID: "call-vscode-read-001",
state: {
...completedToolPart.state,
input: {
filePath: "/Users/helios/.superconductor/worktrees/kilo-code/sc-zero-helium-f589/packages/kilo-vscode/AGENTS.md",
offset: 1,
limit: 200,
},
metadata: {
loaded: ["/project/packages/kilo-vscode/AGENTS.md"],
},
},
}
const vscodeGlobPart: ToolPart = {
...globCompleted,
id: "part-vscode-glob-001",
callID: "call-vscode-glob-001",
state: {
...globCompleted.state,
input: {
path: "/Users/helios/.superconductor/worktrees/kilo-code/sc-zero-helium-f589",
pattern: "packages/kilo-vscode/webview-ui/src/**/*.tsx",
},
},
}
const vscodeToolParts = [vscodeGlobPart, vscodeGlobPart, vscodeGlobPart, vscodeReadPart]
function AllProviders(props: { children: any; data?: MockData; onOpenDiff?: () => void }) {
return (
<DataProvider data={props.data ?? mockData} directory="/project" onOpenDiff={props.onOpenDiff}>
@@ -326,6 +358,25 @@ export const AssistantMessageStory: Story = {
),
}
export const VscodeToolBlocks: Story = {
globals: { theme: "kilo-vscode", vscodeTheme: "dark-modern" },
render: () => (
<AllProviders>
<div style={{ display: "flex", "flex-direction": "column", gap: "8px", width: "700px" }}>
<p style={{ margin: 0, color: "var(--text-base)", "font-size": "16px", "line-height": "1.5" }}>
I’ll inspect the UI test setup and existing patterns first, then identify the smallest relevant test change
to make.
</p>
<div style={{ display: "flex", "flex-direction": "column", gap: "2px" }}>
{vscodeToolParts.map((part) => (
<Part part={part} message={mockAssistantMessage} />
))}
</div>
</div>
</AllProviders>
),
}
// --- Edit tool in running/pending state (Part → ToolPartDisplay path) ---
export const WithRunningTool: Story = {
@@ -138,7 +138,7 @@ function BackgroundProcessTool(props: ToolProps) {
</Show>
<Show when={data().output}>
{(value) => (
<div data-component="tool-output" data-scrollable>
<div data-component="tool-output" data-variant="preview" data-scrollable>
<pre data-slot="background-process-output">{value()}</pre>
</div>
)}
@@ -187,6 +187,9 @@ export function mockSessionValue(overrides?: {
loading: () => false,
loadingOlderMessages: () => false,
hasOlderMessages: () => false,
submitting: () => false,
draftSessionID: () => undefined,
setDraftSessionID: noop,
messageMutation: () => undefined,
messages: () => [],
visibleMessages: () => [],
@@ -195,6 +198,8 @@ export function mockSessionValue(overrides?: {
allParts: () => ({}),
allStatusMap: () => ({}),
getParts: () => [],
getSessionToolParts: () => [],
getSessionToolCount: () => 0,
isErrorHidden: () => false,
hydrateParts: noop,
todos: () => [],
@@ -255,6 +260,7 @@ export function mockSessionValue(overrides?: {
deleteSession: noop,
renameSession: noop,
syncSession: noop,
exportSessionTranscript: noop,
cloudPreviewId: () => null,
selectCloudSession: noop,
}
@@ -0,0 +1,132 @@
/** @jsxImportSource solid-js */
import { For } from "solid-js"
import type { Meta, StoryObj } from "storybook-solidjs-vite"
import { Part } from "@kilocode/kilo-ui/message-part"
import type { AssistantMessage as SDKAssistantMessage, Part as SDKPart, ToolPart } from "@kilocode/sdk/v2"
import { StoryProviders } from "./StoryProviders"
import { registerVscodeToolOverrides } from "../components/chat/VscodeToolOverrides"
registerVscodeToolOverrides()
const SID = "tool-call-lab-session"
const MID = "tool-call-lab-message"
const stamp = Date.now()
const base: SDKAssistantMessage = {
id: MID,
sessionID: SID,
role: "assistant",
parentID: "tool-call-lab-user-message",
time: { created: stamp - 9000, completed: stamp - 1000 },
modelID: "anthropic/claude-sonnet-4-6",
providerID: "kilo",
mode: "default",
agent: "default",
path: { cwd: "/project", root: "/project" },
cost: 0.0021,
tokens: { total: 742, input: 386, output: 356, reasoning: 0, cache: { read: 0, write: 0 } },
}
const long = [
"packages/kilo-vscode/webview-ui/src/components/chat/VscodeToolOverrides.tsx",
"packages/kilo-vscode/webview-ui/src/stories/tool-call-lab.stories.tsx",
"packages/kilo-ui/src/components/message-part.tsx",
].join("\n")
const hits = [
"packages/kilo-ui/src/components/message-part.tsx:1847: <div data-component=\"tool-output\">",
"packages/kilo-ui/src/components/basic-tool.css:250: [data-component=\"tool-output\"]",
"packages/kilo-vscode/webview-ui/src/components/chat/VscodeToolOverrides.tsx:141: background process output",
].join("\n")
const tree = [
"packages/kilo-ui/src/components/",
" basic-tool.css",
" context-tool-results.tsx",
" message-part.css",
" message-part.tsx",
"packages/kilo-vscode/webview-ui/src/stories/",
" tool-call-lab.stories.tsx",
].join("\n")
const proc = [
"pid: 48122",
"status: running",
"cwd: /project",
"command: bun run --cwd packages/kilo-vscode storybook",
"last_output:",
"Storybook 9.0.18 for solid-vite started",
"Local: http://localhost:6007/",
].join("\n")
function completed(input: Record<string, unknown>, title: string, value: string): ToolPart["state"] {
return {
status: "completed",
input,
output: value,
title,
metadata: {},
time: { start: stamp - 5000, end: stamp - 4400 },
}
}
function tool(id: string, call: string, name: string, state: ToolPart["state"]): ToolPart {
return {
id,
sessionID: SID,
messageID: MID,
type: "tool",
callID: call,
tool: name,
state,
}
}
const previews: SDKPart[] = [
tool(
"lab-preview-glob",
"lab-call-preview-glob",
"glob",
completed({ pattern: "packages/**/tool*.tsx", path: "." }, "Find tool files", long),
),
tool(
"lab-preview-grep",
"lab-call-preview-grep",
"grep",
completed({ pattern: "data-component=\"tool-output\"", include: "*.tsx", path: "." }, "Search tool output", hits),
),
tool(
"lab-preview-list",
"lab-call-preview-list",
"list",
completed({ path: "packages/kilo-ui/src/components" }, "List component files", tree),
),
tool(
"lab-preview-background",
"lab-call-preview-background",
"background_process",
completed({ action: "status", id: "bgp_storybook", description: "Check Storybook server" }, "Check Storybook server", proc),
),
]
const meta: Meta = {
title: "Labs/Tool Call Lab",
parameters: { layout: "padded" },
}
export default meta
type Story = StoryObj
export const SearchPreviews: Story = {
name: "Search Previews",
render: () => (
<StoryProviders noPadding sessionID={SID} status="idle">
<div style={{ padding: "16px", width: "520px", "max-width": "100%" }}>
<div style={{ display: "flex", "flex-direction": "column", gap: "10px" }}>
<For each={previews}>{(part) => <Part part={part} message={base} defaultOpen />}</For>
</div>
</div>
</StoryProviders>
),
}
@@ -211,10 +211,17 @@
.vscode-session-turn-assistant {
display: flex;
flex-direction: column;
gap: 12px;
gap: 2px;
width: 100%;
}
.vscode-session-turn-assistant [data-component="tool-part-wrapper"][data-part-type="text"]
+ [data-component="tool-part-wrapper"][data-part-type="tool"],
.vscode-session-turn-assistant [data-component="tool-part-wrapper"][data-part-type="tool"]
+ [data-component="tool-part-wrapper"][data-part-type="text"] {
margin-top: 6px;
}
.vscode-session-turn-diffs {
width: 100%;
}
@@ -47,7 +47,9 @@
[data-slot="background-process-output"] {
margin: 0;
font: inherit;
font-family: var(--font-mono);
font-family: var(--font-family-mono);
font-size: var(--kilo-font-size-12);
line-height: var(--line-height-large);
white-space: pre-wrap;
overflow-wrap: anywhere;
}