diff --git a/packages/ui/src/components/basic-tool.stories.tsx b/packages/ui/src/components/basic-tool.stories.tsx
index 9d9d97acfe..b9cefc1329 100644
--- a/packages/ui/src/components/basic-tool.stories.tsx
+++ b/packages/ui/src/components/basic-tool.stories.tsx
@@ -4,19 +4,21 @@ import * as mod from "./basic-tool"
import { create } from "../storybook/scaffold"
const docs = `### Overview
-Expandable tool panel with a structured trigger and optional details.
+Tool call surface with explicit row and panel variants.
Use structured triggers for consistent layout; custom triggers allowed.
### API
-- Required: \`icon\` and \`trigger\` (structured or custom JSX).
-- Optional: \`status\`, \`defaultOpen\`, \`forceOpen\`, \`defer\`, \`locked\`.
+- Required: \`variant\`, \`icon\`, and \`trigger\`.
+- Row tools render summary-only.
+- Panel/group tools support \`defaultOpen\`, \`forceOpen\`, \`defer\`, and \`locked\`.
### Variants and states
- Pending/running status animates the title via TextShimmer.
### Behavior
-- Uses Collapsible; can defer content rendering until open.
+- Row tools skip collapsible state and render lightweight trigger-only markup.
+- Panel/group tools use Collapsible and can defer content rendering until open.
- Locked state prevents closing.
### Accessibility
@@ -28,13 +30,15 @@ Use structured triggers for consistent layout; custom triggers allowed.
`
const story = create({
- title: "UI/Basic Tool",
+ title: "UI/Tool Call",
mod,
+ name: "ToolCall",
args: {
+ variant: "panel",
icon: "mcp",
defaultOpen: true,
trigger: {
- title: "Basic Tool",
+ title: "Tool Call",
subtitle: "Example subtitle",
args: ["--flag", "value"],
},
@@ -43,8 +47,8 @@ const story = create({
})
export default {
- title: "UI/Basic Tool",
- id: "components-basic-tool",
+ title: "UI/Tool Call",
+ id: "components-tool-call",
component: story.meta.component,
tags: ["autodocs"],
parameters: {
@@ -60,6 +64,7 @@ export const Basic = story.Basic
export const Pending = {
args: {
+ variant: "panel",
status: "pending",
trigger: {
title: "Running tool",
@@ -71,6 +76,7 @@ export const Pending = {
export const Locked = {
args: {
+ variant: "panel",
locked: true,
trigger: {
title: "Locked tool",
@@ -82,6 +88,7 @@ export const Locked = {
export const Deferred = {
args: {
+ variant: "panel",
defer: true,
defaultOpen: false,
trigger: {
@@ -94,6 +101,7 @@ export const Deferred = {
export const ForceOpen = {
args: {
+ variant: "panel",
forceOpen: true,
trigger: {
title: "Forced open",
@@ -103,14 +111,14 @@ export const ForceOpen = {
},
}
-export const HideDetails = {
+export const Row = {
args: {
- hideDetails: true,
+ variant: "row",
+ icon: "mcp",
trigger: {
title: "Summary only",
- subtitle: "Details hidden",
+ subtitle: "Lightweight row",
},
- children: "Hidden content",
},
}
@@ -120,13 +128,14 @@ export const SubtitleAction = {
return (
{message()}
-
setMessage("Subtitle clicked")}
>
Subtitle action details
-
+
)
},
diff --git a/packages/ui/src/components/basic-tool.tsx b/packages/ui/src/components/basic-tool.tsx
index 1dfca0c929..621b337ae0 100644
--- a/packages/ui/src/components/basic-tool.tsx
+++ b/packages/ui/src/components/basic-tool.tsx
@@ -1,4 +1,16 @@
-import { createEffect, createSignal, For, Match, on, onCleanup, onMount, Show, Switch, type JSX } from "solid-js"
+import {
+ createEffect,
+ createSignal,
+ For,
+ Match,
+ on,
+ onCleanup,
+ onMount,
+ Show,
+ splitProps,
+ Switch,
+ type JSX,
+} from "solid-js"
import {
animate,
type AnimationPlaybackControls,
@@ -26,7 +38,7 @@ const isTriggerTitle = (val: any): val is TriggerTitle => {
)
}
-export interface BasicToolProps {
+interface ToolCallPanelBaseProps {
icon: IconProps["name"]
trigger: TriggerTitle | JSX.Element
children?: JSX.Element
@@ -44,7 +56,78 @@ export interface BasicToolProps {
onSubtitleClick?: () => void
}
-export function BasicTool(props: BasicToolProps) {
+function ToolCallTriggerBody(props: {
+ trigger: TriggerTitle | JSX.Element
+ pending: boolean
+ onSubtitleClick?: () => void
+ arrow?: boolean
+}) {
+ return (
+
+
+
+
+
+ {(trigger) => (
+
+
+
+
+
+
+
+ {
+ if (!props.onSubtitleClick) return
+ e.stopPropagation()
+ props.onSubtitleClick()
+ }}
+ >
+ {trigger().subtitle}
+
+
+
+
+ {(arg) => (
+
+ {arg}
+
+ )}
+
+
+
+
+
{trigger().action}
+
+ )}
+
+ {props.trigger as JSX.Element}
+
+
+
+
+
+
+
+ )
+}
+
+function ToolCallPanel(props: ToolCallPanelBaseProps) {
const [open, setOpen] = createSignal(props.defaultOpen ?? false)
const [ready, setReady] = createSignal(open())
const pending = () => props.status === "pending" || props.status === "running"
@@ -197,68 +280,12 @@ export function BasicTool(props: BasicToolProps) {
return (
-
-
-
-
-
- {(trigger) => (
-
-
-
-
-
-
-
- {
- if (props.onSubtitleClick) {
- e.stopPropagation()
- props.onSubtitleClick()
- }
- }}
- >
- {trigger().subtitle}
-
-
-
-
- {(arg) => (
-
- {arg}
-
- )}
-
-
-
-
-
{trigger().action}
-
- )}
-
- {props.trigger as JSX.Element}
-
-
-
-
-
-
-
+
+export interface ToolCallRowProps {
+ variant: "row"
+ icon: IconProps["name"]
+ trigger: TriggerTitle | JSX.Element
+ status?: string
+ animate?: boolean
+ onSubtitleClick?: () => void
+}
+
+// `group` currently shares the same behavior as `panel`; the separate variant is semantic so grouped call sites can stay explicit.
+export interface ToolCallPanelProps extends Omit {
+ variant: "panel" | "group"
+}
+
+export type ToolCallProps = ToolCallRowProps | ToolCallPanelProps
+function ToolCallRoot(props: ToolCallProps) {
+ const [local, rest] = splitProps(props, ["variant", "trigger", "status", "onSubtitleClick"])
+ const pending = () => local.status === "pending" || local.status === "running"
+ if (local.variant === "row") {
+ return (
+
+ )
+ }
+
+ return (
+
+ )
+}
+
+function ToolCallList(props: { children?: JSX.Element }) {
+ return {props.children}
+}
+
+function ToolCallRow(props: { children: JSX.Element }) {
+ return (
+
+ )
+}
+
+export const ToolCall = Object.assign(ToolCallRoot, {
+ List: ToolCallList,
+ Row: ToolCallRow,
+})
+
+export function GenericTool(props: { tool: string; status?: string; hideDetails?: boolean }) {
+ return (
+
+ )
}
diff --git a/packages/ui/src/components/message-part.css b/packages/ui/src/components/message-part.css
index fc4504e2a4..470e256e58 100644
--- a/packages/ui/src/components/message-part.css
+++ b/packages/ui/src/components/message-part.css
@@ -637,16 +637,18 @@
}
}
-[data-component="context-tool-group-list"] {
+[data-component="context-tool-group-list"],
+[data-component="tool-call-list"] {
margin-top: -4px;
display: flex;
flex-direction: column;
gap: 0px;
+}
- [data-slot="context-tool-group-item"] {
- min-width: 0;
- padding: 4px 0;
- }
+[data-component="context-tool-group-list"] [data-slot="context-tool-group-item"],
+[data-component="tool-call-list"] [data-slot="tool-call-item"] {
+ min-width: 0;
+ padding: 4px 0;
}
[data-component="diagnostics"] {
diff --git a/packages/ui/src/components/message-part.tsx b/packages/ui/src/components/message-part.tsx
index 2fb3c7915a..6a227223a6 100644
--- a/packages/ui/src/components/message-part.tsx
+++ b/packages/ui/src/components/message-part.tsx
@@ -33,8 +33,7 @@ import { useData } from "../context"
import { useFileComponent } from "../context/file"
import { useDialog } from "../context/dialog"
import { useI18n } from "../context/i18n"
-import { BasicTool } from "./basic-tool"
-import { GenericTool } from "./basic-tool"
+import { GenericTool, ToolCall } from "./basic-tool"
import { Accordion } from "./accordion"
import { StickyAccordionHeader } from "./sticky-accordion-header"
import { Card } from "./card"
@@ -653,7 +652,8 @@ function ContextToolGroup(props: { parts: ToolPart[]; busy?: boolean; animate?:
const summary = createMemo(() => contextToolSummary(props.parts))
return (
-
}
>
-
+
{(part) => {
const trigger = contextToolTrigger(part, i18n)
const running = createMemo(() => busy(part.state.status))
const reveal = useToolReveal(running, () => props.animate !== false)
return (
-
-
-
-
-
-
-
-
-
- {(text) => }
-
-
- {(arg, idx) => }
-
-
-
-
-
+
+
+
+
+
+
+ {(text) => }
+
+
+ {(arg, idx) => }
+
+
-
+
)
}}
-
-
+
+
)
}
@@ -1251,7 +1245,8 @@ ToolRegistry.register({
const pending = createMemo(() => busy(props.status))
return (
<>
-
busy(props.status))
return (
-
)}
-
+
)
},
})
@@ -1315,7 +1311,8 @@ ToolRegistry.register({
const i18n = useI18n()
const pending = createMemo(() => busy(props.status))
return (
-
)}
-
+
)
},
})
@@ -1349,7 +1346,8 @@ ToolRegistry.register({
if (props.input.include) args.push("include=" + props.input.include)
const pending = createMemo(() => busy(props.status))
return (
-
)}
-
+
)
},
})
@@ -1623,9 +1621,9 @@ ToolRegistry.register({
return value
})
return (
-
@@ -1716,7 +1714,7 @@ ToolRegistry.register({
)
- return
+ return
},
})
@@ -1753,7 +1751,8 @@ ToolRegistry.register({
}
return (
-
-
+
)
},
})
@@ -1811,7 +1810,8 @@ ToolRegistry.register({
const reveal = useToolReveal(pending, () => props.reveal !== false)
return (
-
-
+
)
},
@@ -1879,7 +1879,8 @@ ToolRegistry.register({
const reveal = useToolReveal(pending, () => props.reveal !== false)
return (
-
-
+
)
},
@@ -1967,7 +1968,8 @@ ToolRegistry.register({
return (
-
)}
-
+
)
},
@@ -2150,7 +2152,8 @@ ToolRegistry.register({
})
return (
-
-
+
)
},
})
@@ -2201,7 +2204,8 @@ ToolRegistry.register({
})
return (
-
-
+
)
},
})
@@ -2252,6 +2256,6 @@ ToolRegistry.register({
)
- return
+ return
},
})
diff --git a/packages/ui/src/components/shell-submessage-motion.stories.tsx b/packages/ui/src/components/shell-submessage-motion.stories.tsx
index 444fc0fa9a..414d0a9d87 100644
--- a/packages/ui/src/components/shell-submessage-motion.stories.tsx
+++ b/packages/ui/src/components/shell-submessage-motion.stories.tsx
@@ -1,6 +1,6 @@
// @ts-nocheck
import { createEffect, createSignal, onCleanup } from "solid-js"
-import { BasicTool } from "./basic-tool"
+import { ToolCall } from "./basic-tool"
import { animate } from "motion"
export default {
@@ -97,12 +97,7 @@ const ease = {
linear: "linear",
}
-function SpringSubmessage(props: {
- text: string
- visible: boolean
- visualDuration: number
- bounce: number
-}) {
+function SpringSubmessage(props: { text: string; visible: boolean; visualDuration: number; bounce: number }) {
let ref: HTMLSpanElement | undefined
let widthRef: HTMLSpanElement | undefined
@@ -194,19 +189,15 @@ export const Playground = {
>
-
Shell
-
+
}
@@ -225,7 +216,7 @@ export const Playground = {
>
{"$ cat <<'TOPIC1'"}
-
+