# JDS (Jaco Design System) > An opinionated design system for AI agent and voice interfaces. Built on Base UI, installed as source with the shadcn CLI. Registry: `https://jds-ruddy.vercel.app/r/{name}.json`, namespace `@jds`. Install a component: `npx shadcn@latest add @jds/` after adding `"@jds": "https://jds-ruddy.vercel.app/r/{name}.json"` to `registries` in components.json. Start with `@jds/style`. ## Docs - [Introduction](https://jds-ruddy.vercel.app/docs) - [Get Started](https://jds-ruddy.vercel.app/docs/get-started) - [Styling](https://jds-ruddy.vercel.app/docs/styling) - [Motion](https://jds-ruddy.vercel.app/docs/motion) - [Icons](https://jds-ruddy.vercel.app/docs/icons) - [Roadmap](https://jds-ruddy.vercel.app/docs/roadmap) - [Changelog](https://jds-ruddy.vercel.app/docs/changelog) - [Contributing](https://jds-ruddy.vercel.app/docs/contributing) ## Agent components - [Agent Panel](https://jds-ruddy.vercel.app/llms/components/agent-panel.md): A configurable agent side panel: docked or floating, left or right, card, page or glass surface, three entrance motions, and chat/voice modes. - [Agent Steps](https://jds-ruddy.vercel.app/llms/components/agent-steps.md): Plan and progress for a multi-step run: what's done, what's running, what's next. - [Approval](https://jds-ruddy.vercel.app/llms/components/approval.md): Human-in-the-loop gate. The agent pauses on an irreversible action until the user approves or denies it. - [Artifact](https://jds-ruddy.vercel.app/llms/components/artifact.md): A generated document or file in its own panel beside the chat, opened from a card in the message. - [Branch](https://jds-ruddy.vercel.app/llms/components/branch.md): Step through versions of a response after regenerating. New versions are selected as they arrive. - [Command Bar](https://jds-ruddy.vercel.app/llms/components/command-bar.md): A ⌘K palette that also asks the agent. Typing filters commands; the Ask row turns the same text into a question, and the answer replaces the list until Escape. Choose whether Enter asks or runs a command. - [Context Meter](https://jds-ruddy.vercel.app/llms/components/context-meter.md): How full the context window is. A small ring for the toolbar that shifts to warning at 80% and destructive at 95%, with a breakdown on hover. - [Conversation](https://jds-ruddy.vercel.app/llms/components/conversation.md): Scroll container for a chat thread. Stays pinned to the bottom while tokens stream and lets go when the user scrolls up. - [Message](https://jds-ruddy.vercel.app/llms/components/message.md): A single turn from the user or the assistant, with hover-revealed actions. - [Model Picker](https://jds-ruddy.vercel.app/llms/components/model-picker.md): Choose the model for the next message. A quiet trigger for the prompt toolbar; the menu says what each model is for. - [Prompt Input](https://jds-ruddy.vercel.app/llms/components/prompt-input.md): Composer for chat and agent apps. Enter to send, Shift+Enter for a new line, paste or drop files, stop while streaming. - [Reasoning](https://jds-ruddy.vercel.app/llms/components/reasoning.md): Model thinking, open while it streams, then collapsed to a one-line summary with the time it took. - [Response](https://jds-ruddy.vercel.app/llms/components/response.md): Markdown renderer that is safe to stream. Closes unterminated syntax as it arrives and highlights code with Shiki. - [Shimmer](https://jds-ruddy.vercel.app/llms/components/shimmer.md): In-progress text and a typing indicator for the gap before the first token. - [Sources](https://jds-ruddy.vercel.app/llms/components/sources.md): Inline numbered citations with hover previews, and a strip of source cards. - [Suggestions](https://jds-ruddy.vercel.app/llms/components/suggestions.md): Follow-up prompts as a row of chips. - [Tool Call](https://jds-ruddy.vercel.app/llms/components/tool-call.md): What the agent called, with what input, and what came back. States match AI SDK tool parts. ## Voice components - [Call Controls](https://jds-ruddy.vercel.app/llms/components/call-controls.md): Controls for a realtime voice session: status and timer, mute, interrupt the agent, end the call. - [Live Transcript](https://jds-ruddy.vercel.app/llms/components/live-transcript.md): Rolling speech transcript per speaker. Interim text is dimmed until recognition commits it. - [Push to Talk](https://jds-ruddy.vercel.app/llms/components/push-to-talk.md): Hold to speak with pointer or the Space key. The ring grows with input level so users know they're heard. - [Voice Orb](https://jds-ruddy.vercel.app/llms/components/voice-orb.md): Presence for a voice agent, drawn in the primary color. Ten variants: particle mesh, glowing ring, twisting ribbons, soft aura, circular bars, halftone dots, three WebGL shaders (plasma, liquid and glass), and a minimal dot for small spots. All share the same states, speed, glow and sensitivity, and react to voice level. Tune one in the Studio. - [Voice Picker](https://jds-ruddy.vercel.app/llms/components/voice-picker.md): Choose a synthetic voice, with a spoken preview for each option. - [Waveform](https://jds-ruddy.vercel.app/llms/components/waveform.md): Live spectrum bars for microphone input or agent speech. ## Primitives - [Accordion](https://jds-ruddy.vercel.app/llms/components/accordion.md): Stacked sections that expand one or several at a time. - [Alert](https://jds-ruddy.vercel.app/llms/components/alert.md): A callout for status or warnings, inline with content. - [Alert Dialog](https://jds-ruddy.vercel.app/llms/components/alert-dialog.md): A modal that interrupts to confirm a destructive action. - [Avatar](https://jds-ruddy.vercel.app/llms/components/avatar.md): An image with a text fallback for users and agents. - [Badge](https://jds-ruddy.vercel.app/llms/components/badge.md): A small status label. - [Breadcrumb](https://jds-ruddy.vercel.app/llms/components/breadcrumb.md): Where you are in a hierarchy, with links back up. - [Button](https://jds-ruddy.vercel.app/llms/components/button.md): A button or a component that looks like a button. - [Calendar](https://jds-ruddy.vercel.app/llms/components/calendar.md): A month grid for picking a date or range. - [Card](https://jds-ruddy.vercel.app/llms/components/card.md): A container for grouped content. - [Carousel](https://jds-ruddy.vercel.app/llms/components/carousel.md): Swipe or step through a row of cards. - [Checkbox](https://jds-ruddy.vercel.app/llms/components/checkbox.md): An on/off choice, usually in a list of settings. - [Collapsible](https://jds-ruddy.vercel.app/llms/components/collapsible.md): Shows and hides a panel. - [Combobox](https://jds-ruddy.vercel.app/llms/components/combobox.md): A text input that filters a list of options as you type. - [Command](https://jds-ruddy.vercel.app/llms/components/command.md): A searchable command palette with groups and shortcuts. - [Context Menu](https://jds-ruddy.vercel.app/llms/components/context-menu.md): Actions on right-click or long press. - [Dialog](https://jds-ruddy.vercel.app/llms/components/dialog.md): A modal window over the page. - [Drawer](https://jds-ruddy.vercel.app/llms/components/drawer.md): A panel that slides up from the bottom and can be swiped away. - [Empty](https://jds-ruddy.vercel.app/llms/components/empty.md): A placeholder for empty states, with a next step. - [Field](https://jds-ruddy.vercel.app/llms/components/field.md): Label, control, description and error, laid out consistently. - [Form](https://jds-ruddy.vercel.app/llms/components/form.md): Consistent form layout with native validation, built on Base UI Form. - [Hover Card](https://jds-ruddy.vercel.app/llms/components/hover-card.md): A rich preview when hovering a link or mention. - [Input](https://jds-ruddy.vercel.app/llms/components/input.md): A single-line text field. - [Input Group](https://jds-ruddy.vercel.app/llms/components/input-group.md): An input with icons, text or buttons attached. - [Input OTP](https://jds-ruddy.vercel.app/llms/components/input-otp.md): One-time code entry, one character per slot. - [Kbd](https://jds-ruddy.vercel.app/llms/components/kbd.md): A keyboard key or shortcut. - [Label](https://jds-ruddy.vercel.app/llms/components/label.md): Accessible text for a form control. - [Menu](https://jds-ruddy.vercel.app/llms/components/dropdown-menu.md): A list of actions in a dropdown. - [Menubar](https://jds-ruddy.vercel.app/llms/components/menubar.md): A desktop-style bar of menus. - [Meter](https://jds-ruddy.vercel.app/llms/components/meter.md): A measurement within a known range, like quota used. - [Navigation Menu](https://jds-ruddy.vercel.app/llms/components/navigation-menu.md): Top-level site navigation with rich dropdowns. - [Number Field](https://jds-ruddy.vercel.app/llms/components/number-field.md): A numeric input with steppers, arrow keys and drag-to-scrub. - [Pagination](https://jds-ruddy.vercel.app/llms/components/pagination.md): Move between pages of results. - [Popover](https://jds-ruddy.vercel.app/llms/components/popover.md): A floating panel anchored to a trigger. - [Progress](https://jds-ruddy.vercel.app/llms/components/progress.md): Completion of a known-length task. - [Radio Group](https://jds-ruddy.vercel.app/llms/components/radio-group.md): Pick exactly one of a few options. - [Resizable](https://jds-ruddy.vercel.app/llms/components/resizable.md): Panels the user can resize by dragging the divider. - [Scroll Area](https://jds-ruddy.vercel.app/llms/components/scroll-area.md): A scroll container with styled scrollbars. - [Select](https://jds-ruddy.vercel.app/llms/components/select.md): Pick one value from a list. - [Separator](https://jds-ruddy.vercel.app/llms/components/separator.md): A visual divider. - [Sheet](https://jds-ruddy.vercel.app/llms/components/sheet.md): A panel that slides in from an edge. Can render inside a container, non-modal. - [Skeleton](https://jds-ruddy.vercel.app/llms/components/skeleton.md): A placeholder while content loads. - [Slider](https://jds-ruddy.vercel.app/llms/components/slider.md): Choose a value within a range. - [Spinner](https://jds-ruddy.vercel.app/llms/components/spinner.md): Indeterminate loading indicator. - [Switch](https://jds-ruddy.vercel.app/llms/components/switch.md): Turn a setting on or off, taking effect immediately. - [Table](https://jds-ruddy.vercel.app/llms/components/table.md): Rows and columns of structured data. - [Tabs](https://jds-ruddy.vercel.app/llms/components/tabs.md): Switch between related views. - [Textarea](https://jds-ruddy.vercel.app/llms/components/textarea.md): A multi-line text field that grows with content. - [Toast](https://jds-ruddy.vercel.app/llms/components/sonner.md): Brief notifications, via Sonner. - [Toggle](https://jds-ruddy.vercel.app/llms/components/toggle.md): A button that stays pressed. - [Toggle Group](https://jds-ruddy.vercel.app/llms/components/toggle-group.md): A set of toggles, single or multiple choice. - [Toolbar](https://jds-ruddy.vercel.app/llms/components/toolbar.md): A row of controls with arrow-key navigation between them. - [Tooltip](https://jds-ruddy.vercel.app/llms/components/tooltip.md): A label on hover or focus. ## Optional - [Everything in one file](https://jds-ruddy.vercel.app/llms-full.txt): all component docs with source - [Recipes](https://jds-ruddy.vercel.app/recipes): complete agent and voice experiences, installable as blocks --- # Conversation Scroll container for a chat thread. Stays pinned to the bottom while tokens stream and lets go when the user scrolls up. Docs: https://jds-ruddy.vercel.app/docs/components/conversation ## Installation ```bash npx shadcn@latest add @jds/conversation ``` npm dependencies: `cn`, `motion` Also installs: `@jds/button`, `@jds/message`, `@jds/utils` ## Usage ```tsx import { Conversation, ConversationContent, ConversationScrollButton, } from "@/components/ai/conversation" {/* messages */} ``` ## Example ```tsx "use client" import { Conversation, ConversationContent, ConversationScrollButton } from "@/components/ai/conversation" import { Message, MessageContent } from "@/components/ai/message" const turns = [ ["user", "What changed in the Q3 pricing?"], [ "assistant", "Two things: the Team plan moved from €12 to €14 per seat, and annual billing now gets two months free instead of one.", ], ["user", "Which customers are affected?"], [ "assistant", "Everyone on monthly Team billing at renewal. Annual customers keep their price until their term ends.", ], ["user", "Draft the email."], [ "assistant", "Here's a first draft. I kept it short and led with the annual discount so the change reads as an option, not only a price rise.", ], ] as const export default function ConversationDemo() { return (
{turns.map(([from, text], i) => ( {text} ))}
) } ``` ### Empty state ```tsx import { ConversationEmpty } from "@/components/ai/conversation" import { Suggestion, Suggestions } from "@/components/ai/suggestions" import { SparkleIcon } from "@/lib/icons" export default function ConversationEmptyDemo() { return ( } title="How can I help?" description="Ask about orders, refunds or shipping." > ) } ``` ## API Reference ### Conversation | Prop | Type | Default | Description | | --- | --- | --- | --- | | `bubbles` | `"user" \| "all" \| "none"` | `"user"` | Who gets a bubble. Applies to every Message inside. | | `shape` | `"tail" \| "round" \| "square"` | `"tail"` | Bubble corners: a tail toward the speaker, fully round, or squarer. | | `density` | `"comfortable" \| "compact"` | `"comfortable"` | Spacing between and inside messages. Compact suits side panels. | ### ConversationEmpty | Prop | Type | Default | Description | | --- | --- | --- | --- | | `title` | `string` | `"Start a conversation"` | Heading shown before the first message. | | `description` | `string` | | Supporting text. | | `icon` | `ReactNode` | | Optional icon above the title. | ## Source ### components/ai/conversation.tsx ```tsx "use client" import * as React from "react" import { AnimatePresence, motion } from "motion/react" import { cn } from "cn" import { MessageStyleProvider, useMessageStyle, type MessageStyle } from "@/components/ai/message" import { Button } from "@/components/ui/button" import { useStickToBottom } from "@/hooks/use-stick-to-bottom" import { ScrollDownIcon } from "@/lib/icons" import { spring } from "@/lib/motion" type ConversationContextValue = { contentRef: React.RefObject isAtBottom: boolean scrollToBottom: (behavior?: ScrollBehavior) => void } const ConversationContext = React.createContext(null) function useConversation() { const ctx = React.useContext(ConversationContext) if (!ctx) throw new Error("Conversation parts must be used inside ") return ctx } /** * A scrolling message log that sticks to the bottom while content streams in. `bubbles`, * `shape` and `density` style every Message inside. */ function Conversation({ bubbles, shape, density, className, children, ...props }: React.ComponentProps<"div"> & MessageStyle) { const { scrollRef, contentRef, isAtBottom, scrollToBottom } = useStickToBottom() const value = React.useMemo( () => ({ contentRef, isAtBottom, scrollToBottom }), [contentRef, isAtBottom, scrollToBottom] ) return (
{children}
) } function ConversationContent({ className, ...props }: React.ComponentProps<"div">) { const { contentRef } = useConversation() const { density } = useMessageStyle() return (
) } function ConversationScrollButton({ className, ...props }: React.ComponentProps) { const { isAtBottom, scrollToBottom } = useConversation() return ( {!isAtBottom && ( )} ) } function ConversationEmpty({ className, title = "Start a conversation", description, icon, children, ...props }: React.ComponentProps<"div"> & { title?: string; description?: string; icon?: React.ReactNode }) { return (
{icon &&
{icon}
}

{title}

{description &&

{description}

}
{children}
) } export { Conversation, ConversationContent, ConversationScrollButton, ConversationEmpty, useConversation } ``` ### hooks/use-stick-to-bottom.ts ```ts "use client" import * as React from "react" /** * Keeps a scroll container pinned to the bottom while content grows (streaming), * and releases the pin as soon as the user scrolls up. */ export function useStickToBottom(threshold = 48) { const scrollRef = React.useRef(null) const contentRef = React.useRef(null) const pinned = React.useRef(true) const [isAtBottom, setIsAtBottom] = React.useState(true) const scrollToBottom = React.useCallback((behavior: ScrollBehavior = "smooth") => { const el = scrollRef.current if (!el) return pinned.current = true el.scrollTo({ top: el.scrollHeight, behavior }) }, []) React.useEffect(() => { const el = scrollRef.current if (!el) return const onScroll = () => { const atBottom = el.scrollHeight - el.scrollTop - el.clientHeight <= threshold pinned.current = atBottom setIsAtBottom(atBottom) } el.addEventListener("scroll", onScroll, { passive: true }) return () => el.removeEventListener("scroll", onScroll) }, [threshold]) React.useEffect(() => { const content = contentRef.current if (!content) return const observer = new ResizeObserver(() => { if (pinned.current) scrollToBottom("instant") }) observer.observe(content) return () => observer.disconnect() }, [scrollToBottom]) return { scrollRef, contentRef, isAtBottom, scrollToBottom } } ``` --- # Message A single turn from the user or the assistant, with hover-revealed actions. Docs: https://jds-ruddy.vercel.app/docs/components/message ## Installation ```bash npx shadcn@latest add @jds/message ``` npm dependencies: `cn`, `motion` Also installs: `@jds/avatar`, `@jds/button`, `@jds/tooltip`, `@jds/utils` ## Usage ```tsx import { Message, MessageContent } from "@/components/ai/message" Summarize this thread. ``` ## Example ```tsx "use client" import { toast } from "sonner" import { Message, MessageAction, MessageActions, MessageAvatar, MessageContent } from "@/components/ai/message" import { CopyIcon, RegenerateIcon, ThumbsDownIcon, ThumbsUpIcon } from "@/lib/icons" export default function MessageDemo() { return (
Can you shorten this paragraph?

Sure. Shipping is free over €50 and takes two to four working days.

toast("Copied")}>
) } ``` ## API Reference ### Message | Prop | Type | Default | Description | | --- | --- | --- | --- | | `from` | `"user" \| "assistant" \| "system"` | | Sets alignment and bubble style. | ### MessageStyleProvider | Prop | Type | Default | Description | | --- | --- | --- | --- | | `bubbles` | `"user" \| "all" \| "none"` | `"user"` | Who gets a bubble. For messages outside a Conversation. | | `shape` | `"tail" \| "round" \| "square"` | `"tail"` | Bubble corners: a tail toward the speaker, fully round, or squarer. | | `density` | `"comfortable" \| "compact"` | `"comfortable"` | Spacing between and inside messages. Compact suits side panels. | ### MessageAction | Prop | Type | Default | Description | | --- | --- | --- | --- | | `label` | `string` | | Accessible name and tooltip text. | ## Source ### components/ai/message.tsx ```tsx "use client" import * as React from "react" import { motion, type HTMLMotionProps } from "motion/react" import { cn } from "cn" import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar" import { Button } from "@/components/ui/button" import { Tooltip, TooltipContent, TooltipTrigger } from "@/components/ui/tooltip" import { rise } from "@/lib/motion" type MessageRole = "user" | "assistant" | "system" /** * How messages look across a conversation. Set it once on (or with * ); every Message inside follows. Defaults are the classic chat look: * the user in a bubble with a tail corner, the assistant as plain text. */ type MessageStyle = { /** Who gets a bubble. */ bubbles?: "user" | "all" | "none" /** Bubble corners: a tail toward the speaker, fully round, or squarer. */ shape?: "tail" | "round" | "square" density?: "comfortable" | "compact" } const MessageStyleContext = React.createContext>({ bubbles: "user", shape: "tail", density: "comfortable", }) function MessageStyleProvider({ children, ...style }: MessageStyle & { children: React.ReactNode }) { const parent = React.useContext(MessageStyleContext) const { bubbles = parent.bubbles, shape = parent.shape, density = parent.density } = style const value = React.useMemo(() => ({ bubbles, shape, density }), [bubbles, shape, density]) return {children} } const useMessageStyle = () => React.useContext(MessageStyleContext) const MessageContext = React.createContext<{ from: MessageRole }>({ from: "assistant" }) const corners = { tail: { user: "rounded-2xl rounded-tr-md", assistant: "rounded-2xl rounded-tl-md" }, round: { user: "rounded-2xl", assistant: "rounded-2xl" }, square: { user: "rounded-lg", assistant: "rounded-lg" }, } function Message({ from, className, ...props }: HTMLMotionProps<"div"> & { from: MessageRole }) { return ( ) } function MessageContent({ className, ...props }: React.ComponentProps<"div">) { const { from } = React.useContext(MessageContext) const { bubbles, shape, density } = useMessageStyle() const user = from === "user" const bubble = bubbles === "all" || (bubbles === "user" && user) return (
) } function MessageAvatar({ src, name, className, ...props }: React.ComponentProps & { src?: string; name: string }) { return ( {src && } {name.slice(0, 2).toUpperCase()} ) } /** Hover-revealed row of actions under a message (copy, regenerate, feedback). */ function MessageActions({ className, ...props }: React.ComponentProps<"div">) { return (
) } function MessageAction({ label, className, ...props }: React.ComponentProps & { label: string }) { return ( } /> {label} ) } export { Message, MessageContent, MessageAvatar, MessageActions, MessageAction, MessageStyleProvider, useMessageStyle, type MessageRole, type MessageStyle, } ``` --- # Command Bar A ⌘K palette that also asks the agent. Typing filters commands; the Ask row turns the same text into a question, and the answer replaces the list until Escape. Choose whether Enter asks or runs a command. Docs: https://jds-ruddy.vercel.app/docs/components/command-bar ## Installation ```bash npx shadcn@latest add @jds/command-bar ``` npm dependencies: `cn` Also installs: `@jds/command`, `@jds/dialog`, `@jds/kbd`, `@jds/utils` ## Usage ```tsx import { CommandBar, CommandBarAnswer, CommandBarAsk, CommandBarDialog, CommandBarFooter, CommandBarInput, CommandBarList, } from "@/components/ai/command-bar" import { CommandGroup, CommandItem } from "@/components/ui/command" sendMessage({ text: question })}> New invoice Continue in chat}> {answer} ``` ## Example ```tsx "use client" import * as React from "react" import { toast } from "sonner" import { CommandBar, CommandBarAnswer, CommandBarAsk, CommandBarFooter, CommandBarInput, CommandBarList, } from "@/components/ai/command-bar" import { Response } from "@/components/ai/response" import { Button } from "@/components/ui/button" import { CommandGroup, CommandItem, CommandShortcut } from "@/components/ui/command" import { AddIcon, ChartIcon, FileIcon, SettingsIcon, SparkleIcon } from "@/lib/icons" const ANSWER = "Revenue is **€48.2k** this month, up 12% on last month. Most of the growth came from annual plans; monthly churn is flat." /** Streams a canned answer word by word, the way a model reply arrives. */ function useFakeAnswer() { const [text, setText] = React.useState("") const run = React.useRef(0) const start = async () => { const id = ++run.current setText("") let out = "" for (const w of ANSWER.split(/(\s+)/)) { await new Promise((r) => setTimeout(r, 25)) if (run.current !== id) return out += w setText(out) } } const cancel = () => { run.current++ setText("") } return { text, start, cancel } } export default function CommandBarDemo() { const answer = useFakeAnswer() return ( toast("Summarizing…")}> Summarize this page toast("Drafting…")}> Draft a reply to the latest email New invoice ⌘N Open revenue report Settings ⌘, } > {answer.text} ) } ``` ## API Reference ### CommandBar | Prop | Type | Default | Description | | --- | --- | --- | --- | | `onAsk` | `(question: string) => void` | | Called when someone asks. Stream the reply into CommandBarAnswer. | | `onBack` | `() => void` | | Called when leaving the answer, e.g. to cancel a stream. | ### CommandBarAsk | Prop | Type | Default | Description | | --- | --- | --- | --- | | `label` | `string` | `"Ask AI"` | Row text before the question. Place the row before your groups to make Enter ask, after them to make Enter run the best-matching command. | ### CommandBarAnswer | Prop | Type | Default | Description | | --- | --- | --- | --- | | `actions` | `ReactNode` | | Buttons under the answer, like Copy or Continue in chat. | ### CommandBarDialog | Prop | Type | Default | Description | | --- | --- | --- | --- | | `hotkey` | `string \| false` | `"k"` | Toggles with ⌘ or Ctrl plus this key. | | `open` | `boolean` | | Controlled open state. Leave out to let the hotkey manage it. | ## Source ### components/ai/command-bar.tsx ```tsx "use client" import * as React from "react" import { cn } from "cn" import { Command, CommandGroup, CommandInput, CommandItem, CommandList, CommandShortcut, } from "@/components/ui/command" import { Dialog, DialogContent, DialogDescription, DialogHeader, DialogTitle } from "@/components/ui/dialog" import { Kbd } from "@/components/ui/kbd" import { SparkleIcon } from "@/lib/icons" type CommandBarContextValue = { query: string setQuery: (query: string) => void /** The question being answered, or null while browsing commands. */ question: string | null ask: (question: string) => void back: () => void } const CommandBarContext = React.createContext(null) function useCommandBar() { const ctx = React.useContext(CommandBarContext) if (!ctx) throw new Error("CommandBar parts must be used inside ") return ctx } /** * A command palette that also asks the agent. Typing filters commands as usual; the * row turns the same text into a question. Place it before your groups and * Enter asks; after them and Enter runs the best-matching command. While answering, * replaces the list and Escape goes back to it. */ function CommandBar({ onAsk, onBack, className, children, onKeyDown, ...props }: React.ComponentProps & { /** Called with the question when someone asks. Stream your answer into . */ onAsk?: (question: string) => void /** Called when leaving the answer to browse commands again. */ onBack?: () => void }) { const [query, setQuery] = React.useState("") const [question, setQuestion] = React.useState(null) const value = React.useMemo( () => ({ query, setQuery, question, ask: (q) => { if (!q.trim()) return setQuestion(q.trim()) onAsk?.(q.trim()) }, back: () => { setQuestion(null) onBack?.() }, }), [query, question, onAsk, onBack], ) return ( { onKeyDown?.(e) // Escape backs out of an answer before it closes anything around the bar. if (e.key === "Escape" && question !== null) { e.preventDefault() e.stopPropagation() value.back() } }} // h-auto: size to content; the base command fills its container, which suits a dialog but not a bar. className={cn("h-auto rounded-xl! border shadow-2xl", className)} {...props} > {children} ) } function CommandBarInput({ placeholder = "Search or ask anything…", ...props }: Omit, "value" | "onValueChange">) { const { query, setQuery, question, back } = useCommandBar() return ( { setQuery(v) // Editing the text after asking returns to the list, so it can become a new search or question. if (question !== null) back() }} placeholder={placeholder} {...props} /> ) } /** The list of commands. Hidden, not unmounted, while an answer shows, so commands stay registered. */ function CommandBarList(props: React.ComponentProps) { const { question } = useCommandBar() return