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 ( +
+
+
+
{props.children}
+
+
+
+ ) +} + +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'"} -
+