From 4a490380c30a91771c48aa6cdd1a1441abac29c4 Mon Sep 17 00:00:00 2001 From: ShobhitPatra Date: Sat, 7 Mar 2026 23:20:02 +0530 Subject: [PATCH 1/3] feat(core): add ToolGroup chord --- packages/core/src/index.ts | 1 + .../src/primitives/tool-group/ToolGroup.tsx | 179 +++++++++++++++++ .../tool-group/__tests__/ToolGroup.test.tsx | 187 ++++++++++++++++++ .../src/primitives/tool-group/defaults.ts | 13 ++ .../core/src/primitives/tool-group/index.ts | 1 + 5 files changed, 381 insertions(+) create mode 100644 packages/core/src/primitives/tool-group/ToolGroup.tsx create mode 100644 packages/core/src/primitives/tool-group/__tests__/ToolGroup.test.tsx create mode 100644 packages/core/src/primitives/tool-group/defaults.ts create mode 100644 packages/core/src/primitives/tool-group/index.ts diff --git a/packages/core/src/index.ts b/packages/core/src/index.ts index 131f660..3ddce41 100644 --- a/packages/core/src/index.ts +++ b/packages/core/src/index.ts @@ -13,4 +13,5 @@ export * from "./primitives/tool-call-renderer"; export * from "./primitives/reasoning-accordion"; export * from "./primitives/feedback-buttons"; export * from "./primitives/thread-list"; +export * from "./primitives/tool-group"; export * from "./primitives/thread-list-item"; diff --git a/packages/core/src/primitives/tool-group/ToolGroup.tsx b/packages/core/src/primitives/tool-group/ToolGroup.tsx new file mode 100644 index 0000000..d0f7def --- /dev/null +++ b/packages/core/src/primitives/tool-group/ToolGroup.tsx @@ -0,0 +1,179 @@ +"use client"; + +import { useState, useCallback, useRef } from "react"; +import { useAuiState, useScrollLock } from "@assistant-ui/react"; +import { + DEFAULT_ROOT_CLASSNAME, + DEFAULT_TRIGGER_CLASSNAME, + DEFAULT_CONTENT_CLASSNAME, + DEFAULT_CHEVRON_CLASSNAME, + DEFAULT_SPINNER_CLASSNAME, +} from "./defaults"; + +const ANIMATION_DURATION = 200; + +export type ToolGroupProps = React.PropsWithChildren<{ + startIndex: number; + endIndex: number; + /** Root container class. */ + className?: string; + /** Trigger row class. */ + triggerClassName?: string; + /** Collapsible content area class. */ + contentClassName?: string; + /** Whether the group starts open. @default false */ + defaultOpen?: boolean; + /** Custom label function. @default (n) => `${n} tool call(s)` */ + label?: (count: number) => string; + /** Custom trigger renderer. */ + renderTrigger?: (props: { + count: number; + active: boolean; + open: boolean; + }) => React.ReactNode; +}>; + +/** + * Groups consecutive tool calls into a collapsible container with a + * count badge and spinner during execution. + * + * Used as the `ToolGroup` component in `MessagePrimitive.Parts`: + * + * ```tsx + * + * ``` + * + * Must be rendered inside a message context. + */ +export function ToolGroup({ + children, + startIndex, + endIndex, + className, + triggerClassName, + contentClassName, + defaultOpen = false, + label, + renderTrigger, +}: ToolGroupProps) { + const toolCount = endIndex - startIndex + 1; + + const isActive = useAuiState((s) => { + if (s.message.status?.type !== "running") return false; + for (let i = startIndex; i <= endIndex; i++) { + const part = s.message.parts[i]; + if (part?.type === "tool-call" && part.status?.type === "running") { + return true; + } + } + return false; + }); + + const [open, setOpen] = useState(defaultOpen); + const contentRef = useRef(null); + const lockScroll = useScrollLock(contentRef, ANIMATION_DURATION); + + const handleToggle = useCallback(() => { + setOpen((prev) => { + if (prev) lockScroll(); + return !prev; + }); + }, [lockScroll]); + + const labelText = label + ? label(toolCount) + : `${toolCount} tool ${toolCount === 1 ? "call" : "calls"}`; + + const triggerContent = renderTrigger ? ( + renderTrigger({ count: toolCount, active: isActive, open }) + ) : ( + <> + {isActive && } + + {labelText} + {isActive && ( + + {labelText} + + )} + + + + ); + + return ( +
+ + {open && ( +
+
{children}
+
+ )} +
+ ); +} + +ToolGroup.displayName = "ToolGroup"; + +// --- Inline SVG icons --- + +function SpinnerIcon({ className }: { className?: string }) { + return ( + + + + ); +} + +function ChevronIcon({ + className, + style, +}: { + className?: string; + style?: React.CSSProperties; +}) { + return ( + + + + ); +} diff --git a/packages/core/src/primitives/tool-group/__tests__/ToolGroup.test.tsx b/packages/core/src/primitives/tool-group/__tests__/ToolGroup.test.tsx new file mode 100644 index 0000000..c446681 --- /dev/null +++ b/packages/core/src/primitives/tool-group/__tests__/ToolGroup.test.tsx @@ -0,0 +1,187 @@ +import { describe, it, expect, vi } from "vitest"; +import { render, screen, fireEvent } from "@testing-library/react"; + +const mockState = { + message: { + status: { type: "complete" }, + parts: [ + { type: "tool-call", status: { type: "complete" } }, + { type: "tool-call", status: { type: "complete" } }, + ], + }, +}; + +vi.mock("@assistant-ui/react", () => ({ + useAuiState: (selector: any) => selector(mockState), + useScrollLock: () => () => {}, +})); + +import { ToolGroup } from "../ToolGroup"; +import { + DEFAULT_ROOT_CLASSNAME, + DEFAULT_TRIGGER_CLASSNAME, + DEFAULT_CONTENT_CLASSNAME, +} from "../defaults"; + +describe("ToolGroup", () => { + it("renders with default class", () => { + const { container } = render( + +
tool 1
+
tool 2
+
, + ); + expect(container.firstChild).toHaveClass( + ...DEFAULT_ROOT_CLASSNAME.split(" "), + ); + }); + + it("renders with custom class", () => { + const { container } = render( + +
tool
+
, + ); + expect(container.firstChild).toHaveClass("custom"); + }); + + it("shows correct count label for multiple tools", () => { + render( + +
tool
+
, + ); + expect(screen.getByRole("button")).toHaveTextContent("3 tool calls"); + }); + + it("shows correct count label for single tool", () => { + render( + +
tool
+
, + ); + expect(screen.getByRole("button")).toHaveTextContent("1 tool call"); + }); + + it("uses custom label function", () => { + render( + `${n} functions`} + > +
tool
+
, + ); + expect(screen.getByRole("button")).toHaveTextContent("2 functions"); + }); + + it("children hidden by default", () => { + render( + +
tool
+
, + ); + expect(screen.queryByTestId("child")).not.toBeInTheDocument(); + }); + + it("children visible when defaultOpen", () => { + render( + +
tool
+
, + ); + expect(screen.getByTestId("child")).toBeInTheDocument(); + }); + + it("toggles on click", () => { + render( + +
tool
+
, + ); + expect(screen.queryByTestId("child")).not.toBeInTheDocument(); + fireEvent.click(screen.getByRole("button")); + expect(screen.getByTestId("child")).toBeInTheDocument(); + fireEvent.click(screen.getByRole("button")); + expect(screen.queryByTestId("child")).not.toBeInTheDocument(); + }); + + it("has aria-expanded attribute", () => { + render( + +
tool
+
, + ); + const btn = screen.getByRole("button"); + expect(btn).toHaveAttribute("aria-expanded", "false"); + fireEvent.click(btn); + expect(btn).toHaveAttribute("aria-expanded", "true"); + }); + + it("uses default trigger class", () => { + render( + +
tool
+
, + ); + expect(screen.getByRole("button")).toHaveClass( + ...DEFAULT_TRIGGER_CLASSNAME.split(" "), + ); + }); + + it("renders custom trigger via renderTrigger", () => { + render( + ( + {count} tools + )} + > +
tool
+
, + ); + expect(screen.getByTestId("custom-trigger")).toHaveTextContent("2 tools"); + }); + + it("uses custom triggerClassName", () => { + render( + +
tool
+
, + ); + expect(screen.getByRole("button")).toHaveClass("custom-trigger"); + }); + + it("uses custom contentClassName", () => { + render( + +
tool
+
, + ); + // content div wraps an inner flex container that holds children + const content = screen.getByTestId("child").parentElement!.parentElement; + expect(content).toHaveClass("custom-content"); + }); + + it("uses default content class when open", () => { + render( + +
tool
+
, + ); + const content = screen.getByTestId("child").parentElement!.parentElement; + expect(content).toHaveClass(...DEFAULT_CONTENT_CLASSNAME.split(" ")); + }); + + it("renders chevron icon", () => { + render( + +
tool
+
, + ); + const btn = screen.getByRole("button"); + expect(btn.querySelector("svg")).toBeInTheDocument(); + }); +}); diff --git a/packages/core/src/primitives/tool-group/defaults.ts b/packages/core/src/primitives/tool-group/defaults.ts new file mode 100644 index 0000000..3d4fddb --- /dev/null +++ b/packages/core/src/primitives/tool-group/defaults.ts @@ -0,0 +1,13 @@ +export const DEFAULT_ROOT_CLASSNAME = + "w-full rounded-lg border border-zinc-200 dark:border-zinc-800"; + +export const DEFAULT_TRIGGER_CLASSNAME = + "flex w-full items-center gap-2 px-4 py-3 text-sm font-medium transition-colors"; + +export const DEFAULT_CONTENT_CLASSNAME = + "border-t border-zinc-200 px-4 pt-3 pb-3 dark:border-zinc-800"; + +export const DEFAULT_CHEVRON_CLASSNAME = + "size-4 shrink-0 transition-transform duration-200 ease-out"; + +export const DEFAULT_SPINNER_CLASSNAME = "size-4 shrink-0 animate-spin"; diff --git a/packages/core/src/primitives/tool-group/index.ts b/packages/core/src/primitives/tool-group/index.ts new file mode 100644 index 0000000..0145775 --- /dev/null +++ b/packages/core/src/primitives/tool-group/index.ts @@ -0,0 +1 @@ +export { ToolGroup, type ToolGroupProps } from "./ToolGroup"; From d527a8eee1b0394ca44e280d5fc35ab33e2ade07 Mon Sep 17 00:00:00 2001 From: ShobhitPatra Date: Sat, 7 Mar 2026 23:20:07 +0530 Subject: [PATCH 2/3] feat(example): add ToolGroup to example app --- apps/example/app/components/Assistant.tsx | 2 ++ 1 file changed, 2 insertions(+) diff --git a/apps/example/app/components/Assistant.tsx b/apps/example/app/components/Assistant.tsx index 43eda59..5b348be 100644 --- a/apps/example/app/components/Assistant.tsx +++ b/apps/example/app/components/Assistant.tsx @@ -26,6 +26,7 @@ import { ThreadList, ThreadListItem, ToolCallRenderer, + ToolGroup, } from "@assistant-ui/chords"; import { mockModelAdapter } from "../lib/mock-model-adapter"; import type { FC } from "react"; @@ -176,6 +177,7 @@ const AssistantMessage: FC = () => { Text: ({ text }) => {text}, Reasoning: ({ text }) => {text}, ReasoningGroup: ReasoningAccordion, + ToolGroup: ToolGroup, tools: { Fallback: ToolCallRenderer }, }} /> From 46cd7cce3599b31aa494ff3e5f1170a3d6b456a8 Mon Sep 17 00:00:00 2001 From: ShobhitPatra Date: Sat, 7 Mar 2026 23:20:16 +0530 Subject: [PATCH 3/3] feat(docs): add ToolGroup docs and playground --- apps/docs/app/page.tsx | 6 + apps/docs/components/demo/tool-group-demo.tsx | 125 +++++++++++++++++ .../code-generators/generate-code.ts | 14 ++ .../components/playground/controls-panel.tsx | 2 + .../controls/tool-group-controls.tsx | 47 +++++++ .../components/playground/preview-panel.tsx | 2 + .../previews/tool-group-preview.tsx | 130 ++++++++++++++++++ apps/docs/content/docs/major-chords/meta.json | 3 +- .../content/docs/major-chords/tool-group.mdx | 64 +++++++++ apps/docs/lib/playground/chord-registry.ts | 9 ++ apps/docs/lib/playground/types.ts | 1 + apps/docs/mdx-components.tsx | 2 + 12 files changed, 404 insertions(+), 1 deletion(-) create mode 100644 apps/docs/components/demo/tool-group-demo.tsx create mode 100644 apps/docs/components/playground/controls/tool-group-controls.tsx create mode 100644 apps/docs/components/playground/previews/tool-group-preview.tsx create mode 100644 apps/docs/content/docs/major-chords/tool-group.mdx diff --git a/apps/docs/app/page.tsx b/apps/docs/app/page.tsx index 627b995..6c3596e 100644 --- a/apps/docs/app/page.tsx +++ b/apps/docs/app/page.tsx @@ -62,6 +62,12 @@ const majorChords = [ "Sidebar thread list with new thread button, loading skeletons, and context menus.", href: "/docs/major-chords/thread-list", }, + { + name: "ToolGroup", + description: + "Collapsible container grouping consecutive tool calls with count badge and spinner.", + href: "/docs/major-chords/tool-group", + }, ]; const minorChords = [ diff --git a/apps/docs/components/demo/tool-group-demo.tsx b/apps/docs/components/demo/tool-group-demo.tsx new file mode 100644 index 0000000..d78c1c4 --- /dev/null +++ b/apps/docs/components/demo/tool-group-demo.tsx @@ -0,0 +1,125 @@ +"use client"; + +import { + AssistantRuntimeProvider, + ThreadPrimitive, + MessagePrimitive, + ComposerPrimitive, + useLocalRuntime, +} from "@assistant-ui/react"; +import { + ToolCallRenderer, + ToolGroup, + ComposerActionStatus, +} from "@assistant-ui/chords"; +import type { ChatModelAdapter } from "@assistant-ui/react"; +import { FC } from "react"; + +const demoAdapter: ChatModelAdapter = { + async *run({ abortSignal }) { + const toolCallId1 = `call_${Date.now()}_1`; + const toolCallId2 = `call_${Date.now()}_2`; + + const toolCall1 = { + type: "tool-call" as const, + toolCallId: toolCallId1, + toolName: "get_weather", + args: { location: "Paris", units: "celsius" }, + argsText: JSON.stringify({ location: "Paris", units: "celsius" }), + }; + const toolCall2 = { + type: "tool-call" as const, + toolCallId: toolCallId2, + toolName: "get_time", + args: { timezone: "CET", format: "24h" }, + argsText: JSON.stringify({ timezone: "CET", format: "24h" }), + }; + + // Yield two tool calls running + yield { content: [toolCall1] }; + yield { content: [toolCall1, toolCall2] }; + + await new Promise((r) => setTimeout(r, 1500)); + if (abortSignal.aborted) return; + + // Yield with results + text + yield { + content: [ + { ...toolCall1, result: { temperature: 22, condition: "Sunny" } }, + { ...toolCall2, result: { time: "14:30" } }, + { + type: "text" as const, + text: "It's 22°C and sunny in Paris. The local time is 14:30 CET.", + }, + ], + }; + }, +}; + +export function ToolGroupDemo() { + const runtime = useLocalRuntime(demoAdapter); + + return ( + +
+ +
+ + + +
+
+ + + + +
+
+ + + Check weather & time in Paris + + +
+
+
+
+ ); +} + +const DemoUserMessage: FC = () => ( + +
+ +
+
+); + +const DemoAssistantMessage: FC = () => ( + +
+ A +
+
+ {text}, + tools: { Fallback: ToolCallRenderer }, + ToolGroup: ToolGroup, + }} + /> +
+
+); diff --git a/apps/docs/components/playground/code-generators/generate-code.ts b/apps/docs/components/playground/code-generators/generate-code.ts index 50a52a4..8a7afa1 100644 --- a/apps/docs/components/playground/code-generators/generate-code.ts +++ b/apps/docs/components/playground/code-generators/generate-code.ts @@ -179,6 +179,20 @@ const generators: Record CodeGenResult> = { ThreadListItem={() => ( )} +/>`, + }; + }, + "tool-group": (config) => { + const defaults = chordRegistry["tool-group"].defaultConfig; + const props = buildPropsString(config, defaults); + return { + imports: `import { ToolCallRenderer, ToolGroup } from "@assistant-ui/chords";`, + jsx: ` {text}, + tools: { Fallback: ToolCallRenderer }, + ToolGroup: ToolGroup, + }} />`, }; }, diff --git a/apps/docs/components/playground/controls-panel.tsx b/apps/docs/components/playground/controls-panel.tsx index 0391843..bfefde9 100644 --- a/apps/docs/components/playground/controls-panel.tsx +++ b/apps/docs/components/playground/controls-panel.tsx @@ -15,6 +15,7 @@ import { ScrollToBottomControls } from "./controls/scroll-to-bottom-controls"; import { ReasoningAccordionControls } from "./controls/reasoning-accordion-controls"; import { FeedbackButtonsControls } from "./controls/feedback-buttons-controls"; import { ThreadListControls } from "./controls/thread-list-controls"; +import { ToolGroupControls } from "./controls/tool-group-controls"; type ControlsPanelProps = { chordId: ChordId; @@ -39,6 +40,7 @@ const controlsMap: Partial< "reasoning-accordion": ReasoningAccordionControls, "feedback-buttons": FeedbackButtonsControls, "thread-list": ThreadListControls, + "tool-group": ToolGroupControls, }; export function ControlsPanel({ chordId, config, onChange }: ControlsPanelProps) { diff --git a/apps/docs/components/playground/controls/tool-group-controls.tsx b/apps/docs/components/playground/controls/tool-group-controls.tsx new file mode 100644 index 0000000..527aaa1 --- /dev/null +++ b/apps/docs/components/playground/controls/tool-group-controls.tsx @@ -0,0 +1,47 @@ +"use client"; + +import type { ChordConfig } from "@/lib/playground/types"; +import { StyleBuilder, CheckboxInput } from "./shared"; + +export function ToolGroupControls({ + config, + onChange, +}: { + config: ChordConfig; + onChange: (c: ChordConfig) => void; +}) { + return ( +
+ onChange({ ...config, defaultOpen: v })} + /> + onChange({ ...config, className: v || undefined })} + controls={["bg", "rounded", "p"]} + /> + + onChange({ ...config, triggerClassName: v || undefined }) + } + controls={["bg", "text"]} + /> + + onChange({ ...config, contentClassName: v || undefined }) + } + controls={["bg", "text"]} + /> +
+ ); +} diff --git a/apps/docs/components/playground/preview-panel.tsx b/apps/docs/components/playground/preview-panel.tsx index 6aacf47..79d52b9 100644 --- a/apps/docs/components/playground/preview-panel.tsx +++ b/apps/docs/components/playground/preview-panel.tsx @@ -20,6 +20,7 @@ import { ScrollToBottomPreview } from "./previews/scroll-to-bottom-preview"; import { ReasoningAccordionPreview } from "./previews/reasoning-accordion-preview"; import { FeedbackButtonsPreview } from "./previews/feedback-buttons-preview"; import { ThreadListPreview } from "./previews/thread-list-preview"; +import { ToolGroupPreview } from "./previews/tool-group-preview"; type PreviewPanelProps = { chordId: ChordId; @@ -42,6 +43,7 @@ const previewMap: Record> = { "reasoning-accordion": ReasoningAccordionPreview, "feedback-buttons": FeedbackButtonsPreview, "thread-list": ThreadListPreview, + "tool-group": ToolGroupPreview, }; export function PreviewPanel({ chordId, config }: PreviewPanelProps) { diff --git a/apps/docs/components/playground/previews/tool-group-preview.tsx b/apps/docs/components/playground/previews/tool-group-preview.tsx new file mode 100644 index 0000000..7e122c5 --- /dev/null +++ b/apps/docs/components/playground/previews/tool-group-preview.tsx @@ -0,0 +1,130 @@ +"use client"; + +import { + AssistantRuntimeProvider, + ThreadPrimitive, + MessagePrimitive, + ComposerPrimitive, + useLocalRuntime, +} from "@assistant-ui/react"; +import { + ToolCallRenderer, + ToolGroup, + ComposerActionStatus, +} from "@assistant-ui/chords"; +import type { ChordConfig } from "@/lib/playground/types"; +import type { ChatModelAdapter } from "@assistant-ui/react"; +import { useRef } from "react"; + +const toolAdapter: ChatModelAdapter = { + async *run({ abortSignal }) { + const id1 = `call_${Date.now()}_1`; + const id2 = `call_${Date.now()}_2`; + + const tc1 = { + type: "tool-call" as const, + toolCallId: id1, + toolName: "search", + args: { query: "hello" }, + argsText: JSON.stringify({ query: "hello" }), + }; + const tc2 = { + type: "tool-call" as const, + toolCallId: id2, + toolName: "fetch", + args: { url: "https://example.com" }, + argsText: JSON.stringify({ url: "https://example.com" }), + }; + + yield { content: [tc1] }; + yield { content: [tc1, tc2] }; + + await new Promise((r) => setTimeout(r, 1200)); + if (abortSignal.aborted) return; + + yield { + content: [ + { ...tc1, result: { hits: 42 } }, + { ...tc2, result: { status: 200 } }, + { + type: "text" as const, + text: "Found 42 results. The page returned status 200.", + }, + ], + }; + }, +}; + +export function ToolGroupPreview({ config }: { config: ChordConfig }) { + const adapter = useRef(toolAdapter); + const runtime = useLocalRuntime(adapter.current); + + return ( + +
+ +
+ + ( + +
+ +
+
+ ), + AssistantMessage: () => ( + +
+ A +
+
+ {text}, + tools: { Fallback: ToolCallRenderer }, + ToolGroup: (props) => ( + + ), + }} + /> +
+
+ ), + }} + /> +
+
+
+ + + Run tool calls + + +
+
+
+
+ ); +} diff --git a/apps/docs/content/docs/major-chords/meta.json b/apps/docs/content/docs/major-chords/meta.json index c4f6d49..9f590f2 100644 --- a/apps/docs/content/docs/major-chords/meta.json +++ b/apps/docs/content/docs/major-chords/meta.json @@ -11,6 +11,7 @@ "tool-call-renderer", "reasoning-accordion", "feedback-buttons", - "thread-list" + "thread-list", + "tool-group" ] } diff --git a/apps/docs/content/docs/major-chords/tool-group.mdx b/apps/docs/content/docs/major-chords/tool-group.mdx new file mode 100644 index 0000000..90fb6f2 --- /dev/null +++ b/apps/docs/content/docs/major-chords/tool-group.mdx @@ -0,0 +1,64 @@ +--- +title: ToolGroup +description: Collapsible container that groups consecutive tool calls with a count badge and spinner. +--- + +## Overview + +`ToolGroup` groups consecutive tool calls into a single collapsible container. It shows a count badge ("3 tool calls"), a spinner during execution, and a shimmer effect while tools are running. + +## Demo + + + +## When to use + +- Group multiple consecutive tool calls visually +- Show a spinner while tools execute +- Let users expand/collapse tool call details + +## Features + +- **Auto-grouping**: Automatically receives consecutive tool-call parts from `MessagePrimitive.Parts` +- **Active detection**: Shows spinner and shimmer when any tool in the group is running +- **Collapsible**: Click to expand/collapse tool call details +- **Pluralized label**: "1 tool call" vs "3 tool calls" + +## Props + +| Prop | Type | Default | Description | +|------|------|---------|-------------| +| `className` | string | — | Root container class | +| `triggerClassName` | string | — | Trigger row class | +| `contentClassName` | string | — | Collapsible content area class | +| `defaultOpen` | boolean | `false` | Whether to start expanded | +| `label` | `(count: number) => string` | — | Custom label function | +| `renderTrigger` | `(props) => ReactNode` | — | Custom trigger renderer | + +## Basic Usage + +```tsx +import { ToolCallRenderer, ToolGroup } from "@assistant-ui/chords"; + + {text}, + tools: { Fallback: ToolCallRenderer }, + ToolGroup: ToolGroup, + }} +/> +``` + +## With Custom Label + +```tsx + `${n} function ${n === 1 ? "executed" : "executions"}`} +/> +``` + +## Underlying Primitives + +`ToolGroup` is used as the `ToolGroup` component in `MessagePrimitive.Parts`. The framework automatically groups consecutive `tool-call` message parts and passes them as children. + +It reads message state via `useAuiState` to detect whether any tool in the group is currently running. diff --git a/apps/docs/lib/playground/chord-registry.ts b/apps/docs/lib/playground/chord-registry.ts index 4d7b441..fb46170 100644 --- a/apps/docs/lib/playground/chord-registry.ts +++ b/apps/docs/lib/playground/chord-registry.ts @@ -133,6 +133,15 @@ export const chordRegistry: Record = { actions: ["archive", "delete"], }, }, + "tool-group": { + id: "tool-group", + name: "ToolGroup", + category: "major", + description: "Collapsible container grouping consecutive tool calls with count badge and spinner", + defaultConfig: { + defaultOpen: false, + }, + }, "scroll-to-bottom": { id: "scroll-to-bottom", name: "ScrollToBottom", diff --git a/apps/docs/lib/playground/types.ts b/apps/docs/lib/playground/types.ts index 8a1c6b1..eb84a72 100644 --- a/apps/docs/lib/playground/types.ts +++ b/apps/docs/lib/playground/types.ts @@ -14,6 +14,7 @@ export const CHORD_IDS = [ "reasoning-accordion", "feedback-buttons", "thread-list", + "tool-group", ] as const; export type ChordId = (typeof CHORD_IDS)[number]; diff --git a/apps/docs/mdx-components.tsx b/apps/docs/mdx-components.tsx index ba0daf2..fcf3acf 100644 --- a/apps/docs/mdx-components.tsx +++ b/apps/docs/mdx-components.tsx @@ -29,6 +29,7 @@ import { ToolCallRendererDemo } from "@/components/demo/tool-call-renderer-demo" import { ReasoningAccordionDemo } from "@/components/demo/reasoning-accordion-demo"; import { FeedbackButtonsDemo } from "@/components/demo/feedback-buttons-demo"; import { ThreadListDemo } from "@/components/demo/thread-list-demo"; +import { ToolGroupDemo } from "@/components/demo/tool-group-demo"; type Components = Record>; @@ -63,6 +64,7 @@ export function getMDXComponents(components?: Components): Components { ReasoningAccordionDemo, FeedbackButtonsDemo, ThreadListDemo, + ToolGroupDemo, blockquote: (props: any) => {props.children}, ...components, };