# AI UI Kit: full docs > AI UI Kit is an open-source UI kit for agent products built on Mantine: the chat, tool cards and composer, and the screens around them (settings, MCP servers, agents, skills, permissions, hooks, memory, sessions, background tasks, diff review), plus a theme provider and an embeddable launcher. Package `@sinups/ai-kit` 0.2.0. Install: `npm install @sinups/ai-kit @mantine/core @mantine/hooks @tabler/icons-react`. Index: https://sinups.github.io/ai-kit/llms.txt --- # Essentials Rules that apply across the kit. Component pages below give the details. - Components are controlled: data comes in through props, intent goes out through callbacks. Async callbacks may return a promise; the component shows a pending state and the rejection message. - Everything is exported from the package root. Pure helpers are listed under Hooks and utilities. - Built-in tool cards render automatically for tool parts: `tool-Bash`, `tool-Edit`, `tool-Write`, `tool-Grep`, `tool-Glob`, `tool-WebSearch`, `tool-TodoWrite`, `tool-PlanWrite`, `tool-Question`, `tool-Task`, `tool-Agent`, `tool-Thinking`, `tool-mcp____`. - `toolRenderers` on `AgentChat`, `MessageList` and `ToolRenderer` adds or replaces cards. Keys are full part types, `tool-`, such as `tool-Deploy` or `tool-mcp__git__search`; a bare name only matches `mcp__user-tools__`. Renderers receive `CustomToolRendererProps`: `name`, `input`, `output`, `status`, `toolCallId`, `part`, `onAction`. `onAction` reports to `onToolAction`. - Empty chat: `emptyState` with the default `welcome` layout shows `avatar`, `title`, `description` and starter `actions` (`id`, `label`, `icon`, `badge`) above the composer at the bottom. `layout: "center"` centers the greeting and the composer, with suggestion pills above the composer. - `AgentChat` `emptySuggestionsPosition` is deprecated: suggestions always render above the composer and `"bottom"` behaves as `"top"`. Remove the prop. - `contentWidth` sets the message column and composer width: `420px` by default, a number such as `760` on full pages, `"100%"` in panels and widgets. Pass `wrapLines` in narrow containers. - Layout adapts to the component's own width, from a 360px widget to a 900px page. Data views handle loading, error and empty states. - Visible text has English defaults overridable through `labels` (`DEFAULT__LABELS` holds them); labels of nested parts sit under a key, for example `labels.wizard`. --- # Introduction URL: https://sinups.github.io/ai-kit/docs AI UI Kit is an open-source UI kit for agent products built on Mantine: the chat, tool cards and composer, and the screens around them (settings, MCP servers, agents, skills, permissions, hooks, memory, sessions, background tasks, diff review), plus a theme provider and an embeddable launcher. Messages are structurally compatible with `UIMessage` from the AI SDK (`ChatMessage` in this package) and status is `ChatStatus`, so `useChat` output plugs in without importing `ai`. The project is a fork of Agent Elements by 21st.dev (MIT), rebuilt on Mantine as one npm package. ## Component groups - **Chat**: AgentChat, MessageList, UserMessage, ErrorMessage, Markdown, CodeBlock, ImageLightbox, TextShimmer, SpiralLoader, AgentStatus, ContextUsage, ContextBreakdown, CompactBoundary, TurnSummary, ContextEventRow, HookActivity, TranscriptSearch, IdleReturnPrompt, SpendThresholdNotice, MessageActions, EditMessageComposer, FeedbackForm, PlanApproval, RewindDialog, ToolResultNotice, MemoryNotice, CommandChip - **Tools**: ToolRenderer, BashTool, EditTool, SearchTool, TodoTool, PlanTool, ToolGroup, SubagentTool, QuestionTool, McpTool, ThinkingTool, GenericTool, ToolApprovalFooter, ElicitationForm, ShellOutput, DiffView, ActionRow, ToolRowBase, FileExtIcon - **Input**: InputBar, Suggestions, ModelPicker, ModeSelector, SendButton, AttachmentButton, FileAttachment, PastedTextAttachment, PromptHistorySearch, InputPopover, QuestionPrompt - **Primitives**: Wizard, ConfirmDialog, SettingsLayout, SettingsModal, MasterDetail, EntityList, CommandPalette, StatusBadge, ShortcutHint, KeyValueEditor, SchemaView, ValidationErrorsList, InvalidSettingsNotice - **Agents & skills**: AgentsSettingsPanel, AgentList, AgentDetail, AgentEditor, AgentCreateWizard, AgentIdentityFields, ToolSelector, AgentAvatar, SkillsSettingsPanel, SkillCatalog, SkillDetail, SkillEditor, SkillPicker - **MCP**: McpSettingsPanel, McpServerList, McpServerDetail, McpToolDetail, McpServerWizard, McpImportDialog, McpDiscoveredServers, McpConfigWarnings, McpToolAnnotationBadges, McpTransportIcon - **Permissions & hooks**: PermissionRulesPanel, AddPermissionRuleWizard, PermissionRuleInput, PermissionModeSelector, HooksPanel, HookWizard - **Sessions & tasks**: SessionList, SessionPreview, ExportDialog, BackgroundTasksPanel, TaskList, TaskDetail, AgentTree, TaskStatusPill, TaskElapsed, AgentMessage - **Diff**: DiffReview, DiffFileList, DiffFileView, DiffStats - **Settings**: ModelSettingsPanel, EffortSelector, OutputStylePicker, UsagePanel, StatusPanel, MemoryPanel, MemoryFileDetail, CommandsHelp, AiKitProvider, AiKitThemeCustomizer, ChatLauncher --- # Installation URL: https://sinups.github.io/ai-kit/docs/installation Install AI UI Kit from npm. Prerequisites, stylesheet and provider setup, and your first usage snippet. ## Package - `@sinups/ai-kit` 0.2.0 - Peer dependency: `@mantine/core` ^9.4.0 - Peer dependency: `@mantine/hooks` ^9.4.0 - Peer dependency: `@tabler/icons-react` ^3.0.0 - Peer dependency: `react` ^19.2.0 - Peer dependency: `react-dom` ^19.2.0 ## Install ```bash npm install @sinups/ai-kit @mantine/core @mantine/hooks @tabler/icons-react ``` ## Styles and provider ```tsx import "@mantine/core/styles.css"; import "@sinups/ai-kit/styles.css"; ``` Or import styles per component, like `@mantine/core/styles/Button.css`: `styles/base.css` (the `--ae-*` tokens) once, then one file per component you use. Each file includes the styles of the components it renders. ```tsx import "@mantine/core/styles.css"; import "@sinups/ai-kit/styles/base.css"; import "@sinups/ai-kit/styles/Wizard.css"; ``` Render the app inside `MantineProvider`; light and dark schemes follow its color scheme. Wrap kit screens in `AiKitProvider` to give stock Mantine components inside them the kit look and to expose accent, radius and density settings. ## Usage ```tsx "use client"; import { AgentChat } from "@sinups/ai-kit"; import { useChat } from "@ai-sdk/react"; import { IconBook2, IconBug, IconSparkles } from "@tabler/icons-react"; export function Chat() { const { messages, status, sendMessage, stop } = useChat(); return ( sendMessage({ text: content })} onStop={stop} contentWidth={760} emptyState={{ avatar: , title: "How can I help you today?", actions: [ { id: "explain", label: "Explain this repository", icon: }, { id: "bug", label: "Find the cause of a bug", icon: }, ], }} /> ); } ``` --- # Architecture URL: https://sinups.github.io/ai-kit/docs/architecture How AI UI Kit is organized: layers, the controlled component contract, async actions, data states, width-adaptive layout, and the primitives every domain module is built from. --- # Use cases URL: https://sinups.github.io/ai-kit/docs/use-cases Realistic agent scenarios built with AI UI Kit: coding-agent-style coding agents with plans and diffs, internal support workflows that issue refunds, lightweight support chat widgets, and engineering rollout plans with tool calls. --- # MCP URL: https://sinups.github.io/ai-kit/docs/mcp Use AI UI Kit docs from your AI assistant via the Model Context Protocol. Configure any MCP client to read the component catalog and examples. - Index: https://sinups.github.io/ai-kit/llms.txt - Full docs: https://sinups.github.io/ai-kit/llms-full.txt ## With a fetch server ```json { "mcpServers": { "fetch": { "command": "uvx", "args": ["mcp-server-fetch"] } } } ``` ## Without MCP ```markdown ## UI This project builds agent screens with @sinups/ai-kit (Mantine). Before writing UI code, fetch https://sinups.github.io/ai-kit/llms-full.txt and use only components, props and exports listed there. ``` ## Context7 Context7 does not index @sinups/ai-kit yet. Until it does, use a fetch server or the project instructions below; both read the same files this site publishes. ## Example prompts - Read the AI UI Kit docs and add a full-page AgentChat with a welcome empty state: avatar, greeting and three starter actions. - Embed ChatLauncher in the bottom-right corner of our dashboard with an unread badge, and keep the chat mounted while it is closed. - Show the agent's file changes in DiffReview next to the chat, with accept and reject per file. - Wrap the settings screen in AiKitProvider with the indigo accent and compact density, and add AiKitThemeCustomizer to it. - Render our custom tool-Deploy parts with a toolRenderers entry that reports approve through onToolAction. --- # Skills URL: https://sinups.github.io/ai-kit/docs/skills Give AI coding assistants project-aware context about AI UI Kit so they install, compose, and customise components using the right APIs and patterns. ```bash npx skills add sinups/ai-kit ``` --- # What you can build URL: https://sinups.github.io/ai-kit/docs/what-you-can-build Live recipes built from AI UI Kit components: full-page chat, chat with a history sidebar, chat with an inspector, a settings screen, an embedded widget and an onboarding wizard. ## Full-page chat One centered column: a header, the feed and the composer pinned to the bottom. Switch Empty to see the welcome state with starter actions. Uses: AgentChat, MessageList, InputBar, TranscriptSearch ```tsx
, title: "How can I help you today?", actions: [{ id: "explain", label: "Explain this repository", icon: }], }} style={{ flex: 1, minHeight: 0 }} /> ``` ## Chat with a history sidebar Session history beside the chat. On a phone the history moves into a drawer opened from the header. Uses: SessionList, AgentChat ```tsx {!compact && ( )} ``` ## Chat with an inspector A resizable pane with the diff review or background tasks. On a phone it opens as a bottom drawer. Uses: DiffReview, BackgroundTasksPanel, AgentChat ```tsx {kind === "diff" ? ( ) : ( )} ``` ## Settings screen Section navigation beside the content when wide, a list with a back action when narrow. Uses: SettingsLayout, McpSettingsPanel, UsagePanel ```tsx {activeId === "general" && ( } /> )} {activeId === "mcp" && } {activeId === "usage" && } ``` ## Embedded widget A floating assistant on a host page. Click the button in the corner. To mount it on a page that is not a React app, see Embedding the launcher. Uses: ChatLauncher, AgentChat ```tsx ``` ## Onboarding wizard Validated steps, an optional step and a review before an async finish. Uses: Wizard ```tsx } labels={{ finish: "Create workspace" }} onComplete={createWorkspace} /> ``` --- # Theming URL: https://sinups.github.io/ai-kit/docs/theming Theme the kit with AiKitProvider: accent, radius, density and color scheme settings, a customizer panel, --ae-* tokens and nesting inside a host MantineProvider. ```tsx ``` - Without a provider the kit follows the host primary color, fonts and color scheme through Mantine CSS variables and `--ae-*` tokens. - `AiKitProvider` goes inside the host `MantineProvider` and themes only its subtree and its portals. - `accent`: `gray`, `blue`, `indigo`, `violet`, `grape`, `pink`. `radius`: `sharp`, `default`, `round`. `density`: `default`, `compact`. `colorScheme`: `light`, `dark`, `auto` (changes the whole app). - `theme`: a `MantineThemeOverride` merged last. `tokens`: `--ae-*` values without the prefix (`AeTokenOverrides`, names in `AE_TOKENS`). `persistKey`: stores changes in localStorage. - `useAiKitTheme()` returns `settings`, `defaults`, `setSettings`, `reset`, `hostTheme`, `aiKit`; `useOptionalAiKitTheme()` returns `null` outside a provider. `AiKitHostScope` renders host UI with the host theme inside a kit subtree. - Global setup: `` with the `ae-kit` class (`AI_KIT_SCOPE_CLASS`) on the app root. - Opt a stock component out of kit styles with `unstyled`, `variant="unstyled"` or `data-ai-kit-unstyled`. --- # Layouts URL: https://sinups.github.io/ai-kit/docs/layouts Compose full-page chat, chat with a history sidebar, chat with an inspector, settings pages and an embedded widget from kit components. - Give the chat a bounded height: a flex column with `minHeight: 0` on every level down to `AgentChat`. - Measure the container, not the viewport. Below about 720px pass `wrapLines` and move side panes into a `Drawer`. - Full-page chat: a header aligned with the column, `AgentChat` with `contentWidth={760}`, `alignComposer`, `topFade`, `collapseToolRuns`, `withSearch`, `stickyPrompt`. - Chat with a sidebar: a 272px `SessionList` column on `--ae-bg-tertiary`; a drawer when narrow. - Chat with an inspector: Mantine `Splitter` with a collapsible pane for `DiffReview` or `BackgroundTasksPanel`; a bottom drawer on phones. - Settings page: `SettingsLayout` with `SettingsSection` and `SettingRow`; `fill: true` sections for panels that scroll themselves such as `McpSettingsPanel`. - Widget: `ChatLauncher` around `AgentChat` with `contentWidth="100%"`. --- # Embedding the launcher URL: https://sinups.github.io/ai-kit/docs/launcher Embed a floating chat launcher in a React app or on any page with mountChatLauncher and Shadow DOM isolation. ```tsx ``` - `ChatLauncher` is a floating button that opens a non-modal chat panel. Escape and the close button return focus to the button; `keepMounted` (default `true`) keeps the chat state while closed; the panel opens full screen below `fullScreenBreakpoint` (520px). `withinPortal={false}` positions it inside a transformed container. - `mountChatLauncher(target, element, options)` renders into an open shadow root with its own `MantineProvider` and returns `{ container, unmount }`. Options: `shadow` (default `true`), `styles` (CSS text; `:root`, `html`, `body` are scoped to the widget), `styleUrls`, `adoptDocumentStyles` (development), `theme`, `colorScheme` (default `light`), `wrap`. ```tsx import mantineCss from "@mantine/core/styles.css?inline"; import baseCss from "@sinups/ai-kit/styles/base.css?inline"; import launcherCss from "@sinups/ai-kit/styles/ChatLauncher.css?inline"; import chatCss from "@sinups/ai-kit/styles/AgentChat.css?inline"; import providerCss from "@sinups/ai-kit/styles/AiKitProvider.css?inline"; const widget = mountChatLauncher(host, , { styles: [mantineCss, baseCss, launcherCss, chatCss, providerCss], wrap: (element) => {element}, }); widget.unmount(); ``` --- # Bundle size URL: https://sinups.github.io/ai-kit/docs/bundle-size How much JavaScript and CSS each AI UI Kit component adds to your app, gzip, with per-component stylesheets and tree-shaking. --- # What's new URL: https://sinups.github.io/ai-kit/docs/whats-new Components, props, theming and testing infrastructure added in this release, and the deprecated AgentChat emptySuggestionsPosition prop. - New modules: primitives, MCP, agents, skills, permissions, hooks configuration, memory, sessions, message actions, background tasks, diff review, model settings, help, elicitation. - New chat components: AgentStatus, ContextUsage, ContextBreakdown, CompactBoundary, TurnSummary, ContextEventRow, HookActivity, IdleReturnPrompt, SpendThresholdNotice, TranscriptSearch, PromptHistorySearch, PastedTextAttachment, CodeBlock, ShellOutput. - AgentChat gained `emptyState`, `statusBar`, `messageActions`, `withSearch`, `stickyPrompt`, `collapseToolRuns`, `alignComposer`, `topFade`, `wrapLines`, `inputBarProps` and more; InputBar gained completions, a message queue, collapsed pastes and prompt history. - Theming: AiKitProvider, AiKitThemeCustomizer, createAiKitTheme, mergeAiKitTheme. Launcher: ChatLauncher, mountChatLauncher. - Deprecated: `emptySuggestionsPosition` (suggestions always render above the composer). --- # Chat ## AgentChat URL: https://sinups.github.io/ai-kit/docs/agent-chat ### Code ```tsx import { AgentChat } from "@sinups/ai-kit"; import { IconGitPullRequest, IconTestPipe } from "@tabler/icons-react"; const messages = [ { id: "msg-1", role: "assistant", parts: [{ type: "text", text: "Welcome to AI UI Kit." }], }, ]; const welcomeActions = [ { id: "review", label: "Review my pull request", value: "Review the changes in my branch.", icon: }, { id: "tests", label: "Write tests for a file", value: "Write tests for ", icon: }, ]; export function Example() { return (
{}} onStop={() => {}} showCopyToolbar emptyState={{ layout: "welcome", title: "How can I help you today?", actions: welcomeActions, }} />
); } ``` ### Usage Create a full chat surface with messages, status, and send/stop handlers. Add `attachments` to wire file/image context, `questionTool` to handle Question tool answers, and `showCopyToolbar` for text copy actions. The same component works as a narrow side widget and as a full-page chat: set `contentWidth` (for example `760` or `'100%'`) so the transcript and composer use the available width, and `collapseToolRuns` to keep long tool sequences compact. Use `emptyState` with `layout: 'welcome'` for an empty chat: avatar, greeting and starter actions placed in the free space above the composer, a little below the middle, and close to the composer in containers narrower than 600px. Suggestions always sit above the composer (`emptySuggestionsPosition='bottom'` is deprecated and behaves as `'top'`). ### Example: Basic ```tsx {}} onStop={() => {}} /> ``` ### Example: Empty centered ```tsx {}} onStop={() => {}} emptyStatePosition="center" /> ``` ### Example: Welcome empty state ```tsx const actions = [ { id: "review", label: "Review my pull request", value: "Review the changes in my current branch.", icon: , badge: "New" }, { id: "bug", label: "Find the cause of a bug", value: "Help me find why ", icon: }, { id: "tests", label: "Write tests for a file", value: "Write tests for ", icon: }, ]; , title: "How can I help you today?", description: "Ask about the code, fix a bug or plan a change.", actions, }} /> ``` ### Example: With attachments ```tsx {}} onStop={() => {}} attachments={{ onAttach: () => {}, images: [{ id: "img-1", filename: "preview.png", url: imageUrl }], files: [{ id: "file-1", filename: "spec.md", size: 3200 }], onRemoveImage: () => {}, onRemoveFile: () => {}, }} /> ``` ### Example: Copy toolbar ```tsx {}} onStop={() => {}} showCopyToolbar /> ``` ### Example: Full-page chat ```tsx ``` ### API reference | Prop | Type | Required | Description | | --- | --- | --- | --- | | messages | `ChatMessage[]` | Yes | | | onSend | `(message: { role: 'user'; content: string }) => void` | Yes | | | status | `ChatStatus` | Yes | | | onStop | `() => void` | Yes | | | error | `Error` | No | | | classNames | `Partial` | No | | | slots | `Partial` | No | | | toolRenderers | `Record>` | No | | | attachments | `{ onAttach?: () => void; images?: AttachedImage[]; files?: AttachedFile[]; onRemoveImage?: (id: string) => void; onRemoveFile?: (id: string) => void; onPaste?: (e: React.ClipboardEvent) => void; isDragOver?: boolean; }` | No | Attachment configuration | | showCopyToolbar | `boolean` | No | Show copy toolbar on text turns, `true` by default | | collapseToolRuns | `boolean \| CollapseToolRunsOptions` | No | Collapse runs of consecutive read and search tool calls into one summary row, `false` by default | | messageActions | `MessageListActions` | No | Message actions under messages: edit, retry, rewind, branch and feedback; plain copy toolbar when omitted | | onRetry | `() => void` | No | Adds a retry button to error cards, including the card rendered for `error` | | onToolAction | `ToolActionHandler` | No | Receives actions reported by custom tool renderers through `onAction` | | inputBarProps | `Omit< Partial, 'onSend' \| 'status' \| 'onStop' \| 'value' \| 'onChange' >` | No | Extra props for the composer: `completions`, `leftActions`, `rightActions`, `placeholder`, `onQueue`, `queuedMessages`, `onRemoveQueued`, `labels` and the rest of `InputBarProps`. `AgentChat` owns `onSend`, `status`, `onStop`, the draft value and the question bar; `attachments` and `suggestions` take precedence over the same fields here. | | statusBar | `React.ReactNode` | No | Content above the composer aligned with the message column, for example `AgentStatus` | | withSearch | `boolean` | No | Adds the conversation search, opened with Mod+F anywhere inside the chat, including the composer, `false` by default | | stickyPrompt | `boolean` | No | Pins the prompt of the answer being read to the top while scrolling a long answer | | highlighter | `SyntaxHighlighter` | No | Syntax highlighter for code blocks in answers; plain code blocks when omitted | | longMessageThreshold | `LongTextThreshold \| boolean` | No | Collapses long user messages to head and tail; `true` uses `{ chars: 2000, lines: 30 }`, `false` by default | | contentWidth | `number \| string` | No | Max width of the message column and composer: a number in px or any CSS width, `420px` by default. Pass `'100%'` or `960` for a full-page chat | | initialScrollBehavior | `'bottom' \| 'top'` | No | Where to position the scroll container on initial mount, `'bottom'` by default | | enableImagePreview | `boolean` | No | Opens an attached image in a fullscreen lightbox on click, `true` by default | | suggestions | `InputSuggestions` | No | | | emptyStatePosition | `'default' \| 'center'` | No | | | emptyState | `AgentChatEmptyState` | No | Greeting of an empty chat; `layout` is `welcome` by default, `center` needs `layout: 'center'` or `emptyStatePosition="center"` | | emptyStateWidth | `number \| string` | No | Width of the centered empty state and its composer: a number in px or any CSS width; `600px` with `emptyState`, the message column width otherwise | | hideSuggestionsWhenNotEmpty | `boolean` | No | Shows composer suggestions only while the chat has no messages | | alignComposer | `boolean` | No | Lines the composer and status bar up with the text edge of the message column, `false` by default | | topFade | `boolean` | No | Fades the top edge of the message list once it is scrolled, `false` by default | | wrapLines | `boolean` | No | Wraps long lines in code blocks and diffs instead of scrolling them sideways, for narrow layouts, `false` by default | | responsiveTables | `boolean` | No | Shows answer tables with too many columns for the width as one card per row, `false` by default | | emptySuggestionsPlacement | `'input' \| 'empty' \| 'both'` | No | | | emptySuggestionsPosition | `'top' \| 'bottom'` | No | Deprecated. Suggestions are always rendered above the composer; `bottom` is treated as `top`. | | questionTool | `{ submitLabel?: string; skipLabel?: string; allowSkip?: boolean; onAnswer?: (payload: { toolCallId?: string; question: QuestionConfig; answer: QuestionAnswer; }) => void; }` | No | | | className | `string` | No | | | style | `React.CSSProperties` | No | | ## MessageList URL: https://sinups.github.io/ai-kit/docs/message-list ### Code ```tsx import { MessageList } from "@sinups/ai-kit"; import type { ChatMessage } from "@sinups/ai-kit"; const messages: ChatMessage[] = [ { id: "msg-1", role: "user", parts: [{ type: "text", text: "Share the latest status." }], createdAt: new Date(), }, { id: "msg-2", role: "assistant", parts: [{ type: "text", text: "All systems are green." }], }, ]; export function Example() { return ; } ``` ### Usage Render the full transcript from ChatMessage[]. Use showCopyToolbar for user/assistant text copy, className for container sizing, and slots/classNames/toolRenderers for custom rendering. contentWidth sets the column width: keep the 420px default in a side widget, pass 720 or "100%" in a full-page chat. collapseToolRuns folds three or more consecutive read and search calls into one summary row, and a compaction part renders a CompactBoundary divider. ### Example: Basic transcript ```tsx const messages: ChatMessage[] = [ { id: "msg-1", role: "user", parts: [{ type: "text", text: "Share the latest status." }], }, { id: "msg-2", role: "assistant", parts: [{ type: "text", text: "All systems are green." }], }, { id: "msg-3", role: "user", parts: [{ type: "text", text: "Any regressions from last deploy?" }], }, { id: "msg-4", role: "assistant", parts: [{ type: "text", text: "No new errors in the last 24 hours." }], }, { id: "msg-5", role: "user", parts: [{ type: "text", text: "Summarize open tickets." }], }, { id: "msg-6", role: "assistant", parts: [ { type: "text", text: "3 open: billing follow-up, onboarding issue, and API timeout investigation.", }, ], }, ]; ``` ### Example: With timestamps ```tsx const messages: ChatMessage[] = [ { id: "msg-1", role: "user", parts: [{ type: "text", text: "Can you summarize this?" }], createdAt: new Date(), }, { id: "msg-2", role: "assistant", parts: [{ type: "text", text: "Here is the summary." }], }, ]; ``` ### Example: Collapsed tool runs ```tsx ``` ### Example: Compacted history ```tsx const messages: ChatMessage[] = [ { id: "cmp-s1", role: "system", parts: [ { type: "compaction", tokensBefore: 182_400, tokensAfter: 12_300, summary: "- Migrated the upload client\n- Tests for backoff are green", }, ], }, { id: "cmp-u1", role: "user", parts: [{ type: "text", text: "Keep 5 attempts and ship it." }] }, ]; ``` ### API reference | Prop | Type | Required | Description | | --- | --- | --- | --- | | messages | `ChatMessage[]` | Yes | | | status | `ChatStatus` | Yes | | | className | `string` | No | | | style | `React.CSSProperties` | No | | | contentWidth | `ContentWidth` | No | Max width of the message column: a number in px or any CSS width, `420px` by default. Pass `'100%'` for a full-width chat | | showCopyToolbar | `boolean` | No | | | suppressQuestionTool | `boolean` | No | Hides every `tool-Question` part. Prefer `suppressQuestionToolCallId`. | | suppressQuestionToolCallId | `string` | No | Hides only the `tool-Question` part with this `toolCallId` (e.g. the one shown in the input bar) | | initialScrollBehavior | `'bottom' \| 'top'` | No | Where to position the scroll container on initial mount. - "bottom" (default): classic chat behavior, pinned to the latest message. - "top": start from the top of the conversation, useful for static demos or read-only transcripts where the user should read top-to-bottom. | | enableImagePreview | `boolean` | No | Opens an attached image of a user message in a fullscreen lightbox on click, `true` by default | | collapseToolRuns | `boolean \| CollapseToolRunsOptions` | No | Collapses runs of consecutive read and search tool calls in an assistant message into one summary row, for example `Read 3 files, searched 2 patterns`, `false` by default. | | messageActions | `MessageListActions` | No | Turns on message actions: edit and rewind for user messages, retry, feedback and branch for assistant turns. Each action appears only when its callback is set; without this prop the plain copy toolbar is rendered. | | commands | `SlashCommandInfo[]` | No | Known slash commands; a user message that starts with one of them is shown as a command chip | | slots | `{ UserMessage?: React.ComponentType<{ message: ChatMessage; className?: string; enableImagePreview?: boolean; commands?: SlashCommandInfo[]; longMessageThreshold?: LongTextThreshold \| boolean; }>; ToolRenderer?: React.ComponentType; }` | No | | | classNames | `{ userMessage?: string; }` | No | | | toolRenderers | `Record>` | No | | | onToolAction | `ToolActionHandler` | No | Receives actions reported by custom tool renderers through `onAction` | | onRetry | `() => void` | No | Adds a retry button to error parts | | withSearch | `boolean` | No | Adds the conversation search, opened with Mod+F while focus is inside the list, `false` by default | | searchOpened | `boolean` | No | Controlled open state of the search | | onSearchOpenedChange | `(opened: boolean) => void` | No | Called when the search opens or closes | | stickyPrompt | `boolean` | No | Pins the prompt of the answer being read to the top while scrolling a long answer | | longMessageThreshold | `LongTextThreshold \| boolean` | No | Collapses long user messages to head and tail; `true` uses `{ chars: 2000, lines: 30 }`, `false` by default | | topFade | `boolean` | No | Fades the top edge of the list once it is scrolled, so content does not end abruptly under a header | | wrapLines | `boolean` | No | Wraps long lines in code blocks and diffs instead of scrolling them sideways, for narrow layouts, `false` by default | | responsiveTables | `boolean` | No | Shows answer tables with too many columns for the width as one card per row, `false` by default | | onScrollbarWidthChange | `(width: number) => void` | No | Called with the width in px of the vertical scrollbar whenever it appears, disappears or resizes | | highlighter | `SyntaxHighlighter` | No | Syntax highlighter for code blocks in answers and compaction summaries; plain code blocks when omitted | | labels | `Partial` | No | Overrides of the default English labels | ## UserMessage URL: https://sinups.github.io/ai-kit/docs/user-message ### Code ```tsx import { UserMessage } from "@sinups/ai-kit"; import type { ChatMessage } from "@sinups/ai-kit"; const message: ChatMessage = { id: "user-1", role: "user", parts: [{ type: "text", text: "Share the latest status." }], }; export function Example() { return ; } ``` ### Usage Render a single user bubble. Supports text, image parts (image/data-image/image file), and file attachments. ### Example: Text only ```tsx const message: ChatMessage = { id: "user-1", role: "user", parts: [{ type: "text", text: "Share the latest status." }], }; ``` ### Example: With image ```tsx const message: ChatMessage = { id: "user-2", role: "user", parts: [ { type: "text", text: "Here is the screenshot." }, { type: "data-image", data: { url: imageUrl } }, ], }; ``` ### API reference | Prop | Type | Required | Description | | --- | --- | --- | --- | | message | `ChatMessage` | Yes | | | className | `string` | No | | | enableImagePreview | `boolean` | No | Opens an attached image in a fullscreen lightbox on click, plain thumbnails when `false`; `true` by default | | commands | `SlashCommandInfo[]` | No | Known slash commands. Text that starts with one of them, for example `/review src/auth`, is shown as a command chip with its arguments; unknown `/…` text such as a file path stays plain. | | longMessageThreshold | `LongTextThreshold \| boolean` | No | Shows the head and tail of a text longer than the threshold with a button that expands it; `true` uses `{ chars: 2000, lines: 30 }`, `false` by default | | labels | `Partial` | No | Overrides of the default English labels | ## ErrorMessage URL: https://sinups.github.io/ai-kit/docs/error-message ### Code ```tsx import { ErrorMessage } from "@sinups/ai-kit"; export function Example() { return ( ); } ``` ### Usage Render a failed assistant turn. retry shows a live countdown to the next automatic attempt, onRetry adds a button to retry right away, and variant="warning" with resetsAt fits usage limits that lift at a known time. ### Example: Basic ```tsx ``` ### Example: Retry countdown ```tsx ``` ### Example: Usage limit ```tsx ``` ### API reference | Prop | Type | Required | Description | | --- | --- | --- | --- | | title | `string` | No | Card title, `Something went wrong` by default | | message | `string` | Yes | Error details shown under the title | | variant | `'error' \| 'warning'` | No | Visual tone: `error` for failures, `warning` for rate limits and transient problems | | retry | `ErrorMessageRetry` | No | Automatic retry schedule, renders a live countdown | | resetsAt | `number \| Date` | No | When a usage limit resets; rendered as a local time | | collapsible | `boolean` | No | Collapses a message longer than 6 lines or 600 characters behind "Show more", `false` by default | | onRetry | `() => void` | No | Renders a retry button | | labels | `Partial` | No | Overrides of the default English labels | | className | `string` | No | Class name added to the root element | ## Markdown URL: https://sinups.github.io/ai-kit/docs/markdown ### Code ```tsx import { Markdown } from "@sinups/ai-kit"; const content = [ "# Release notes", "", "- Added streaming markdown", "- Improved tool rendering", "", "## Example", "", "Use \"Markdown\" to render assistant text as it streams.", "", "```ts", "console.log(\"Hello from markdown\");", "```", ].join(\"\n\"); export function Example() { return ; } ``` ### Usage Render streaming markdown with headings, lists, tables, blockquotes, and code fences. External links get safe target/rel handling. ### Example: Release note snippet ```tsx const content = [ "# Release notes", "", "- Added streaming markdown", "- Improved tool rendering", "", "## Example", "", "Use \"Markdown\" to render assistant text as it streams.", "", "```ts", "console.log(\"Hello from markdown\");", "```", ].join(\"\n\"); ``` ### Example: Tables + links ```tsx const content = [ "| Tool | Status |", "| --- | --- |", "| Search | Ready |", "| Bash | Ready |", "", "Visit [docs](https://example.com) for details.", ].join(\"\n\"); ``` ### Example: Streaming update ```tsx import { useEffect, useState } from "react"; const fullContent = [ "### Working plan", "", "- Parse input context", "- Extract constraints", "- Draft outline", "", "#### Draft", "We will deliver a tight summary, then provide supporting details.", "", "```ts", "const steps = [\"parse\", \"outline\", \"draft\"];", "```", "", "| Step | Status |", "| --- | --- |", "| Parse | Done |", "| Outline | Done |", "| Draft | Running |", "", "Final answer coming next...", ].join(\"\n\"); export function Example() { const [content, setContent] = useState(\"\"); const [isStreaming, setIsStreaming] = useState(false); const runStream = () => { setContent(\"\"); setIsStreaming(true); let i = 0; const tick = () => { i += 1; setContent(fullContent.slice(0, i)); if (i >= fullContent.length) { setIsStreaming(false); return; } setTimeout(tick, 18); }; setTimeout(tick, 120); }; useEffect(() => { runStream(); }, []); return (
{isStreaming ? \"Streaming...\" : \"Idle\"}
); } ``` ### API reference | Prop | Type | Required | Description | | --- | --- | --- | --- | | content | `string` | Yes | Markdown source | | className | `string` | No | Class name added to the root element | | textContrast | `'normal' \| 'high'` | No | | | controls | `{ code?: boolean }` | No | Controls rendered in code blocks, `{ code: true }` by default | | wrapLines | `boolean` | No | Wraps long lines in fenced code blocks instead of scrolling them horizontally, `false` by default | | highlighter | `SyntaxHighlighter` | No | Syntax highlighter for fenced code blocks | | streaming | `boolean` | No | Content is still arriving: finished blocks are parsed once and only the growing tail is re-parsed | | responsiveTables | `boolean` | No | Shows tables with too many columns for the available width as one card per row, `false` by default | ## CodeBlock URL: https://sinups.github.io/ai-kit/docs/code-block ### Code ```tsx import { CodeBlock, createShikiHighlighter } from "@sinups/ai-kit"; import { createHighlighter } from "shiki"; const shiki = await createHighlighter({ themes: ["github-light", "github-dark"], langs: ["ts", "bash"], }); const highlighter = createShikiHighlighter(shiki, { light: "github-light", dark: "github-dark" }); export function Example({ code }: { code: string }) { return ( ); } ``` ### Usage A code block with a header (the `title`, or the language), a copy button, optional line numbers and optional collapsing. Without `highlighter` the code is plain text; pass any function that returns token lines, or adapt a shiki instance you created with `createShikiHighlighter`, which uses a light and a dark theme so colors follow the color scheme. Results are cached, and async highlighters render plain text until tokens arrive. `collapsedLines` hides the rest behind `Show N more lines` only when at least 5 lines would be hidden; `streaming` pauses collapsing so new lines stay visible. Long lines scroll horizontally, or wrap with `wrapLines`, which reads better in narrow widgets. Markdown uses CodeBlock for fenced code, and AgentChat passes its `highlighter` prop down. Related helpers: `countCodeLines`, `getCollapsedLineCount`, `highlightCode`, `useHighlightedLines`, `clearHighlightCache`. ### Example: Line numbers and collapsing ```tsx ``` ### Example: Narrow: scroll or wrap ```tsx ``` ### API reference | Prop | Type | Required | Description | | --- | --- | --- | --- | | code | `string` | Yes | Source code | | language | `string` | No | Language passed to the highlighter and shown in the header, `text` by default | | title | `React.ReactNode` | No | Header text shown instead of the language, for example a file name | | highlighter | `SyntaxHighlighter` | No | Syntax highlighter, the code stays plain when omitted or when it returns `null` | | withLineNumbers | `boolean` | No | Shows line numbers | | startLineNumber | `number` | No | Number of the first line, `1` by default | | wrapLines | `boolean` | No | Wraps long lines instead of scrolling horizontally, `false` by default | | withCopy | `boolean` | No | Shows the copy button, `true` by default | | collapsedLines | `number \| false` | No | Collapses long code behind "Show N more lines" after this many lines, `false` by default | | streaming | `boolean` | No | Code is still arriving: highlighting and collapsing wait until it is complete | | labels | `Partial` | No | Overrides of the default English labels | | className | `string` | No | Class name added to the root element | | style | `React.CSSProperties` | No | Inline styles added to the root element | ## ImageLightbox URL: https://sinups.github.io/ai-kit/docs/image-lightbox ### Code ```tsx "use client"; import { useState } from "react"; import { ImageLightbox, type LightboxImage } from "@sinups/ai-kit"; export function Example({ images }: { images: LightboxImage[] }) { const [index, setIndex] = useState(null); return ( <> {images.map((image, position) => ( ))} setIndex(null)} images={images} initialIndex={index ?? 0} /> ); } ``` ### Usage A fullscreen image viewer rendered into `document.body`. It traps focus, locks page scroll and returns focus to the opener on close. Close with the X button, a click on the backdrop or Escape. With more than one image it shows previous and next buttons, dots and a counter, and ArrowLeft and ArrowRight navigate with wrap-around. UserMessage already opens it for attached images; use it directly for images elsewhere, such as tool output. It renders nothing when closed or when the active image has no `url`. ### Example: Gallery ```tsx setIndex(null)} images={images} initialIndex={index ?? 0} /> ``` ### API reference | Prop | Type | Required | Description | | --- | --- | --- | --- | | open | `boolean` | Yes | Whether the overlay is open | | onClose | `() => void` | Yes | Close handler, wired to overlay click, X button and Esc key | | images | `LightboxImage[]` | Yes | Full set of images for gallery navigation | | initialIndex | `number` | No | Index in `images` to start on, `0` by default | | className | `string` | No | | | style | `React.CSSProperties` | No | | ## TextShimmer URL: https://sinups.github.io/ai-kit/docs/text-shimmer ### Code ```tsx import { TextShimmer } from "@sinups/ai-kit"; export function Example() { return ( Syncing metadata ); } ``` ### Usage Render shimmering status text. Tune duration, spread, and delay. ### Example: Inline status ```tsx Syncing metadata ``` ### Example: Delayed shimmer ```tsx Calculating risk score ``` ### Example: Fast shimmer ```tsx Rapid sync ``` ### API reference | Prop | Type | Required | Description | | --- | --- | --- | --- | | children | `React.ReactNode` | Yes | | | as | `React.ElementType` | No | Element to render, `'span'` by default | | duration | `number` | No | Duration of one shimmer sweep in seconds, `2` by default | | delay | `number` | No | Delay before the animation starts in seconds, `0` by default | | spread | `number` | No | Width of the highlight in px exposed as `--ae-shimmer-spread`, `100` by default | ## SpiralLoader URL: https://sinups.github.io/ai-kit/docs/spiral-loader ### Code ```tsx import { SpiralLoader } from "@sinups/ai-kit"; export function Example() { return ; } ``` ### Usage Render the spiral loader. Use size to control the square canvas and className for layout styling. ### Example: Sizes ```tsx
``` ### API reference | Prop | Type | Required | Description | | --- | --- | --- | --- | | size | `number` | No | Width and height in px, `16` by default | ## AgentStatus URL: https://sinups.github.io/ai-kit/docs/agent-status ### Code ```tsx import { AgentStatus } from "@sinups/ai-kit"; export function Example() { return ( ); } ``` ### Usage Show that the agent is working: a shimmering label, live elapsed time and received tokens. Update lastActivityAt on every streamed chunk; when nothing arrives for stallAfterMs (3 seconds by default) the line fades to the error color so the user knows the response is stuck. Set paused while tools run, since silence is expected then. ### Example: Live and stalled ```tsx ``` ### Example: Paused while tools run ```tsx ``` ### API reference | Prop | Type | Required | Description | | --- | --- | --- | --- | | label | `string` | No | Status text while the agent is working, `Thinking` by default | | startedAt | `number \| Date` | No | Moment the turn started, used for the live elapsed time | | tokens | `number` | No | Tokens received in this turn | | lastActivityAt | `number \| Date` | No | Moment the last token or event arrived, `startedAt` by default | | stallAfterMs | `number` | No | Inactivity in ms after which the status is shown as stalled, `3000` by default | | paused | `boolean` | No | Disables stall detection, for example while tools are running | | onStop | `() => void` | No | Renders the stop button when provided | | labels | `Partial` | No | Overrides of the default English labels | | className | `string` | No | Class name added to the root element | | style | `React.CSSProperties` | No | Inline styles added to the root element | ## ContextUsage URL: https://sinups.github.io/ai-kit/docs/context-usage ### Code ```tsx import { ContextUsage, InputBar } from "@sinups/ai-kit"; export function Example() { return ( } /> ); } ``` ### Usage Show how full the context window is. The ring is split by segments below warnAt (80%), turns yellow at warnAt and red at dangerAt (95%). Hover or click opens the breakdown; with onCompact the details offer to compact the conversation once usage is high. Sized to sit in InputBar rightActions. ### Example: Usage levels ```tsx <> ``` ### Example: Inside InputBar ```tsx } /> ``` ### API reference | Prop | Type | Required | Description | | --- | --- | --- | --- | | used | `number` | Yes | Tokens currently in the context window | | total | `number` | Yes | Context window size in tokens | | segments | `ContextUsageSegment[]` | No | Breakdown of `used`, for example system prompt, tools and messages | | breakdown | `ContextBreakdownGroup[]` | No | Detailed groups shown in the details instead of `segments`, expandable to their items | | suggestions | `ContextSuggestion[]` | No | Ways to free context, shown under the breakdown | | warnAt | `number` | No | Ratio at which the ring turns to the warning color, `0.8` by default | | dangerAt | `number` | No | Ratio at which the ring turns to the danger color, `0.95` by default | | size | `number` | No | Ring size in px, `20` by default | | withLabel | `boolean` | No | Shows the percentage next to the ring, `false` by default | | onCompact | `() => void` | No | Renders the compact button in the details once usage reaches `warnAt` | | ariaLabel | `string` | No | Accessible label of the trigger, `Context usage` by default | | labels | `Partial` | No | Overrides of the default English labels | | className | `string` | No | Class name added to the root element | | style | `React.CSSProperties` | No | Inline styles added to the root element | ## ContextBreakdown URL: https://sinups.github.io/ai-kit/docs/context-breakdown ### Code ```tsx import { ContextBreakdown, type ContextBreakdownGroup, type ContextSuggestion, } from "@sinups/ai-kit"; const groups: ContextBreakdownGroup[] = [ { id: "system", label: "System prompt", tokens: 6_200 }, { id: "mcpTools", label: "MCP tools", items: [ { id: "postgres", label: "postgres", tokens: 11_800, description: "14 tools" }, { id: "git", label: "git", tokens: 4_300, description: "9 tools" }, ], }, { id: "messages", label: "Messages", tokens: 71_500 }, ]; export function Example({ disableServers }: { disableServers: () => void }) { const suggestions: ContextSuggestion[] = [ { severity: "warning", title: "Disable unused MCP servers", savings: 11_800, action: { label: "Review", onClick: disableServers }, }, ]; return ( ); } ``` ### Usage Show what fills the context window, group by group: a stacked usage bar, one row per group with tokens and percent, and optional suggestions to free space. A group with `items` expands to its items, sorted by tokens. `used` defaults to the sum of the groups. Suggestions are sorted critical first, then by `savings`; each can carry an action button. Use `variant="full"` (default) on a settings or usage page and `variant="compact"` in a popover: compact hides item and suggestion descriptions and bar tooltips. You rarely render it directly inside a chat: pass `breakdown` and `suggestions` to ContextUsage and the ring shows this component in its details. The layout is a single column and fits a 360px widget. Related pure helpers: `getGroupTokens`, `getBreakdownTotal`, `sortBreakdownItems`, `sortSuggestions`, `getTotalSavings`, `getGroupColor`, `getGroupShade`. ### Example: Wide ```tsx ``` ### Example: Narrow ```tsx
``` ### Example: Inside ContextUsage ```tsx ``` ### API reference | Prop | Type | Required | Description | | --- | --- | --- | --- | | groups | `ContextBreakdownGroup[]` | Yes | Groups of the context, for example System, Tools, MCP tools, Agents, Memory files, Skills, Messages | | total | `number` | Yes | Context window size in tokens | | used | `number` | No | Tokens in the context, the sum of `groups` by default | | suggestions | `ContextSuggestion[]` | No | Ways to free context, most severe first | | variant | `'compact' \| 'full'` | No | `compact` fits the ContextUsage popover, `full` a settings page, `full` by default | | defaultExpanded | `string[]` | No | Ids of groups expanded on the first render | | labels | `Partial` | No | Overrides of the default English labels | | className | `string` | No | Class name added to the root element | | style | `React.CSSProperties` | No | Inline styles added to the root element | ## CompactBoundary URL: https://sinups.github.io/ai-kit/docs/compact-boundary ### Code ```tsx import { CompactBoundary } from "@sinups/ai-kit"; export function Example() { return ( ); } ``` ### Usage Mark the place where earlier history was replaced by a summary. MessageList renders it automatically for a { type: "compaction", summary, tokensBefore, tokensAfter } part, including inside a system message; the summary opens on click. ### Example: With summary ```tsx ``` ### Example: Without summary ```tsx <> ``` ### API reference | Prop | Type | Required | Description | | --- | --- | --- | --- | | summary | `string` | No | Summary that replaced the compacted history, rendered as markdown when expanded | | tokensBefore | `number` | No | Context size before compaction, in tokens | | tokensAfter | `number` | No | Context size after compaction, in tokens | | direction | `'from' \| 'up-to'` | No | Which side of the boundary was summarized, picks the default label | | userContext | `string` | No | What the user asked the summary to keep, shown with the summary | | label | `string` | No | Divider text that replaces the one picked from `labels` by `direction` | | defaultExpanded | `boolean` | No | Show the summary on mount, `false` by default | | highlighter | `SyntaxHighlighter` | No | Syntax highlighter for code blocks in the summary | | labels | `Partial` | No | Overrides of the default English labels | | className | `string` | No | Class name added to the root element | | style | `React.CSSProperties` | No | Inline styles added to the root element | ## TurnSummary URL: https://sinups.github.io/ai-kit/docs/turn-summary ### Code ```tsx import { TurnSummary } from "@sinups/ai-kit"; export function Example() { return ( `Done in ${duration}` }} /> ); } ``` ### Usage Close a turn with one muted line: how long the agent worked, tokens used (against a budget when `tokenBudget` is set) and how many background tasks are still running. While background tasks run, the clock icon becomes a small loader. Labels are functions, so you can change wording and pluralization. MessageList renders it automatically for a `{ type: "turn-summary", durationMs, tokens?, tokenBudget?, backgroundTasks? }` part. The line does not wrap, so keep labels short in narrow widgets. Related helper: `getTurnSummarySegments`, which returns the text segments if you build your own layout. ### Example: Variants ```tsx ``` ### API reference | Prop | Type | Required | Description | | --- | --- | --- | --- | | labels | `Partial` | No | Overrides of the default English labels | | className | `string` | No | Class name added to the root element | | style | `React.CSSProperties` | No | Inline styles added to the root element | | durationMs | `number` | Yes | Time the turn took in ms | | tokens | `number` | No | Tokens used by the turn | | tokenBudget | `number` | No | Token budget of the turn or session | | backgroundTasks | `number` | No | Background tasks still running after the turn ended | ## ContextEventRow URL: https://sinups.github.io/ai-kit/docs/context-event-row ### Code ```tsx import { ContextEventRow } from "@sinups/ai-kit"; export function Example() { return ( <> ); } ``` ### Usage Record something the agent pulled into its context: a file it read, a directory it listed, a memory file, an MCP resource, a skill, or diagnostics. The row shows an icon per `kind`, a verb (`Read`, `Listed`, `Loaded memory`, `Attached`, `Loaded skill`, `Found diagnostics in`) and the label with an optional detail. With `items` the row expands to show them, closed unless `defaultExpanded`. Override verbs per kind with `labels`. MessageList renders it automatically for a `{ type: "context-event", kind, label, detail?, items? }` part, so in a chat you only emit the part. The row is one line and truncates its detail, so it works at any width. ### Example: All kinds ```tsx ``` ### API reference | Prop | Type | Required | Description | | --- | --- | --- | --- | | defaultExpanded | `boolean` | No | Initial expanded state when `items` are given, `false` by default | | labels | `Partial` | No | Verbs per kind, for example `{ file: 'Opened' }` | | className | `string` | No | Class name added to the root element | | style | `React.CSSProperties` | No | Inline styles added to the root element | | kind | `ContextEventKind` | Yes | | | label | `string` | Yes | Object of the event, for example a file path or a skill name | | detail | `string` | No | Extra detail after the label, for example `120 lines` | | items | `string[]` | No | Entries shown when the row is expanded, for example files of a directory | ## HookActivity URL: https://sinups.github.io/ai-kit/docs/hook-activity ### Code ```tsx import { HookActivity } from "@sinups/ai-kit"; export function Example() { return ( ); } ``` ### Usage Show the hooks that ran for an event, such as `PreToolUse`, `PostToolUse` or `Stop`. `status` is `running` (spinner and shimmer title), `done`, `blocked` (orange, with the reason) or `error` (red, with per-hook errors). The row expands to the reason and the list of hooks with their durations; blocked and failed rows start expanded, running rows cannot be expanded. Titles come from function labels (`running`, `done`, `blocked`, `error`), and `getHookActivityTitle` returns the same text for use elsewhere. MessageList renders it automatically for a `{ type: "hook-activity", event, status, hooks?, reason? }` part. To configure hooks rather than display their runs, use HooksPanel. ### Example: Statuses ```tsx ``` ### API reference | Prop | Type | Required | Description | | --- | --- | --- | --- | | defaultExpanded | `boolean` | No | Initial expanded state, `true` for blocked and failed hooks by default | | labels | `Partial` | No | Overrides of the default English labels | | className | `string` | No | Class name added to the root element | | style | `React.CSSProperties` | No | Inline styles added to the root element | | event | `string` | Yes | Hook event name, for example `PreToolUse` | | status | `HookActivityStatus` | Yes | | | hooks | `HookRun[]` | No | | | reason | `string` | No | Why the hooks blocked the action or failed | ## TranscriptSearch URL: https://sinups.github.io/ai-kit/docs/transcript-search ### Code ```tsx "use client"; import { useState } from "react"; import { TranscriptSearch, stepMatchIndex } from "@sinups/ai-kit"; export function Example({ total, onClose }: { total: number; onClose: () => void }) { const [query, setQuery] = useState(""); const [activeIndex, setActiveIndex] = useState(-1); return ( { setQuery(value); setActiveIndex(0); }} activeIndex={total === 0 ? -1 : activeIndex} total={total} onNext={() => setActiveIndex((index) => stepMatchIndex(index, total, 1))} onPrevious={() => setActiveIndex((index) => stepMatchIndex(index, total, -1))} onClose={onClose} /> ); } ``` ### Usage A find bar for a conversation: query input, a `3/12` counter, previous and next buttons and close. Enter goes to the next match, Shift+Enter to the previous one, Escape closes. The counter appears once the query is not empty and shows `0/0` when nothing matches; the arrows are disabled without matches. The component is controlled and does not search by itself. In most apps you do not render it: set `withSearch` on AgentChat or MessageList and Mod+F opens this bar, highlights matches in the transcript and scrolls to the active one. Render it yourself for a custom transcript, using the exported helpers: `findTextMatches` (case-insensitive matches in a string), `findDomMatches` (DOM ranges inside an element, skipping `[data-search-ignore]`) and `stepMatchIndex` (wrap-around navigation). The bar is a compact Paper that fits a 360px widget. ### Example: Custom transcript ```tsx const perMessage = messages.map((text) => findTextMatches(text, query)); const total = perMessage.reduce((sum, ranges) => sum + ranges.length, 0); setActiveIndex(stepMatchIndex(activeIndex, total, 1))} onPrevious={() => setActiveIndex(stepMatchIndex(activeIndex, total, -1))} onClose={close} /> ``` ### API reference | Prop | Type | Required | Description | | --- | --- | --- | --- | | value | `string` | Yes | Search query | | onChange | `(value: string) => void` | Yes | Called with the new query | | activeIndex | `number` | Yes | Index of the active match, `-1` when there is none | | total | `number` | Yes | Number of matches | | onNext | `() => void` | Yes | Moves to the next match, also on Enter | | onPrevious | `() => void` | Yes | Moves to the previous match, also on Shift+Enter | | onClose | `() => void` | Yes | Closes the search, also on Escape | | inputRef | `React.Ref` | No | Ref of the input, for focusing it from a shortcut | | labels | `Partial` | No | Overrides of the default English labels | | className | `string` | No | Class name added to the root element | | style | `React.CSSProperties` | No | Inline styles added to the root element | ## IdleReturnPrompt URL: https://sinups.github.io/ai-kit/docs/idle-return-prompt ### Code ```tsx import { IdleReturnPrompt } from "@sinups/ai-kit"; export function Example({ awayMs, tokens, continueHere, startNewChat, disablePrompt, }: { awayMs: number; tokens: number; continueHere: () => void; startNewChat: () => void; disablePrompt: () => void; }) { return ( ); } ``` ### Usage Ask a user who comes back to a long conversation whether to continue it or start fresh: `Welcome back, 3h since your last message. This chat already holds 182k tokens. Keep going here or start fresh?`. Continue is always shown; New chat with this message and Stop asking render only when their callbacks are set. All strings can be replaced through `labels`. Without `tokens` the message omits the size. Your app decides when to show it, for example after an idle period when the conversation is large. Place it above the composer, for example through AgentChat `statusBar`. Actions wrap below the text in narrow containers. Related helper: `formatAwayDuration` (`45m`, `3h`, `2d`). ### Example: Wide ```tsx ``` ### Example: Narrow ```tsx
``` ### API reference | Prop | Type | Required | Description | | --- | --- | --- | --- | | awayMs | `number` | Yes | How long the user was away, in milliseconds | | tokens | `number` | No | Size of the conversation in tokens | | onContinue | `() => void` | Yes | Keeps working in this conversation | | onNewChat | `() => void` | No | Starts a new chat carrying over the drafted message, the button is rendered only when set | | onDontAskAgain | `() => void` | No | Turns the prompt off, the button is rendered only when set | | labels | `Partial` | No | Overrides of the default English labels | | className | `string` | No | Class name added to the root element | | style | `React.CSSProperties` | No | Inline styles added to the root element | ## SpendThresholdNotice URL: https://sinups.github.io/ai-kit/docs/spend-threshold-notice ### Code ```tsx import { SpendThresholdNotice } from "@sinups/ai-kit"; export function Example({ spent, openUsage, dismiss }: { spent: number; openUsage: () => void; dismiss: () => void }) { return ( ); } ``` ### Usage Tell the user that spending in the session passed a threshold: `You've spent $20.40 in this session. Your limit is $50.00.`. Money is formatted with `Intl.NumberFormat` using `currency` and `locale`. Add buttons with `actions` (`primary` or `secondary`), extra text with `description`, and a close button with `onDismiss`. Use `getReachedThreshold(amount, thresholds)` to decide when to show it and remember which threshold was dismissed. Shares its look with IdleReturnPrompt; actions wrap below the text in narrow containers. `formatSpend` is exported for use in your own text. ### Example: Wide ```tsx ``` ### Example: Narrow, other currency ```tsx ``` ### API reference | Prop | Type | Required | Description | | --- | --- | --- | --- | | amount | `number` | Yes | Money spent in this session | | currency | `string` | No | ISO 4217 currency code, `USD` by default | | locale | `string` | No | BCP 47 locale used to format money, `en` by default | | limit | `number` | No | Spending limit, shown in the description when set | | description | `React.ReactNode` | No | Extra explanation after the title | | actions | `SpendThresholdNoticeAction[]` | No | Buttons, for example View usage or Set a limit | | onDismiss | `() => void` | No | Renders a close button when set | | labels | `Partial` | No | Overrides of the default English labels | | className | `string` | No | Class name added to the root element | | style | `React.CSSProperties` | No | Inline styles added to the root element | ## MessageActions URL: https://sinups.github.io/ai-kit/docs/message-actions ### Code ```tsx import { MessageList } from "@sinups/ai-kit"; export function Example() { return ( resendEdited(messageId, text), onRetry: (messageId) => regenerate(messageId), onRewind: (messageId) => setRewindTarget(messageId), onBranch: (messageId) => branchFrom(messageId), onFeedback: (messageId, value, details) => api.sendFeedback(messageId, value, details), feedback, }} /> ); } ``` ### Usage Add a toolbar under chat messages: copy for every message, edit and rewind for user messages, retry, branch and thumbs up or down for assistant messages. Pass `messageActions` to `MessageList` to get it on every message: edit swaps the message for `EditMessageComposer`, a thumbs down opens `FeedbackForm`, and actions are hidden while the turn is streaming. Async actions show a loader. With `visibility="hover"` (default) the toolbar appears on hover and focus; touch devices always see it. Use `MessageActions` directly in a custom message renderer. ### Example: Toolbar ```tsx <> ``` ### Example: In MessageList ```tsx <> ``` ### Example: Narrow widget ```tsx
``` ### API reference | Prop | Type | Required | Description | | --- | --- | --- | --- | | messageRole | `'user' \| 'assistant'` | Yes | Author of the message: user messages get edit and rewind, assistant messages get retry, feedback and branch | | text | `string` | No | Text copied by the copy action, the action is hidden when empty | | timestamp | `string` | No | Time label rendered before the actions | | onEdit | `() => void` | No | Opens inline editing of a user message | | onRetry | `() => void \| Promise` | No | Regenerates an assistant message; the button shows a loader until the promise settles | | onRewind | `() => void \| Promise` | No | Returns the conversation to this message | | onBranch | `() => void \| Promise` | No | Starts a new branch from this message | | onFeedback | `(value: MessageFeedbackValue, details?: FeedbackDetails) => void \| Promise` | No | Rates an assistant message, adds the thumbs up and thumbs down buttons | | feedback | `MessageFeedbackValue \| null` | No | Controlled rating | | feedbackReasons | `FeedbackReason[]` | No | Reasons offered after a thumbs down | | disabled | `boolean` | No | Disables every action except copy, for example while the agent is responding | | visibility | `'hover' \| 'always'` | No | `hover` reveals the actions on hover and focus of the toolbar or of an ancestor with `data-message-actions-host`, touch devices always see them; `always` keeps them visible, `hover` by default | | align | `'start' \| 'end'` | No | Horizontal placement of the actions, `start` by default | | labels | `Partial` | No | Overrides of the default English labels | | className | `string` | No | Class name added to the root element | | style | `React.CSSProperties` | No | Inline styles added to the root element | ## EditMessageComposer URL: https://sinups.github.io/ai-kit/docs/edit-message-composer ### Code ```tsx import { EditMessageComposer } from "@sinups/ai-kit"; export function Example() { return ( resendEdited(message.id, text)} onCancel={stopEditing} /> ); } ``` ### Usage Edit a sent user message in place. Enter resends the trimmed text, Shift+Enter adds a line and Escape cancels; the composer stays open with a loader until `onSubmit` settles. `MessageList` renders it automatically when `messageActions.onEdit` is set. ### Example: Inline edit ```tsx ``` ### API reference | Prop | Type | Required | Description | | --- | --- | --- | --- | | defaultValue | `string` | Yes | Text the editor starts with, usually the original message | | onSubmit | `(text: string) => void \| Promise` | Yes | Saves and resends the edited text, trimmed; the composer stays open and shows a loader until the promise settles | | onCancel | `() => void` | Yes | Leaves editing without saving | | maxRows | `number` | No | Maximum number of rows before the editor scrolls, `10` by default | | withHint | `boolean` | No | Shows the keyboard hint, `true` by default | | labels | `Partial` | No | Overrides of the default English labels | | className | `string` | No | Class name added to the root element | | style | `React.CSSProperties` | No | Inline styles added to the root element | ## FeedbackForm URL: https://sinups.github.io/ai-kit/docs/feedback-form ### Code ```tsx import { FeedbackForm } from "@sinups/ai-kit"; export function Example() { return ( api.sendFeedback(messageId, "down", { reasons, comment })} onSkip={() => api.sendFeedback(messageId, "down")} /> ); } ``` ### Usage Collect details for a rating. For `down` the form offers reason chips (Inaccurate, Not helpful, Too slow, Unsafe, Other by default) and a comment, and shows a thank-you note once `onSubmit` resolves; `up` renders only the note. `MessageActions` opens it after a thumbs down. ### Example: Thumbs down and up ```tsx <> ``` ### API reference | Prop | Type | Required | Description | | --- | --- | --- | --- | | value | `MessageFeedbackValue` | Yes | Rating the form belongs to: `up` renders only the thank-you note, `down` renders reasons and a comment | | onSubmit | `(details: FeedbackDetails) => void \| Promise` | No | Sends the negative feedback details; the form shows the thank-you note once the promise resolves | | onSkip | `() => void \| Promise` | No | Adds a skip button that sends the rating without details | | reasons | `FeedbackReason[]` | No | Reasons rendered as chips, `Inaccurate`, `Not helpful`, `Too slow`, `Unsafe`, `Other` by default | | submitted | `boolean` | No | Renders the thank-you note, for feedback that was already sent | | labels | `Partial` | No | Overrides of the default English labels | | className | `string` | No | Class name added to the root element | | style | `React.CSSProperties` | No | Inline styles added to the root element | ## PlanApproval URL: https://sinups.github.io/ai-kit/docs/plan-approval ### Code ```tsx import { PlanApproval } from "@sinups/ai-kit"; export function Example() { return ( agent.approvePlan()} onApproveWithEdits={(edits) => agent.approvePlan({ edits })} onReject={(feedback) => agent.rejectPlan(feedback)} /> ); } ``` ### Usage Review a plan before the agent starts working, for example after plan mode. The Markdown plan is collapsed to `collapsedLines` (8 by default) and expands on demand. Approve, Approve with edits and Reject with feedback show a loader while their promise runs and an error when it rejects; pass `decision` to render the final state instead of the buttons. The plan uses the same shape as `PlanTool`. ### Example: Review a plan ```tsx ``` ### Example: Failed action ```tsx { throw new Error("The session has ended, start a new one to continue"); }} /> ``` ### API reference | Prop | Type | Required | Description | | --- | --- | --- | --- | | plan | `Plan` | Yes | Plan in the same shape as `PlanTool`: `summary` is rendered as Markdown | | onApprove | `(mode?: string) => void \| Promise` | Yes | Approves the plan as is, with the picked `approveOptions` value when options are set; buttons show a loader until the promise settles | | approveOptions | `PlanApproveOption[]` | No | Approval modes such as auto-accept edits; the approve button uses the first one and a menu offers all of them | | onApproveWithEdits | `(edits: string) => void \| Promise` | No | Adds "Approve with edits", called with the trimmed edits | | onReject | `(feedback: string) => void \| Promise` | No | Adds "Reject with feedback", called with the trimmed feedback | | decision | `PlanDecision \| null` | No | Decision already made, renders the final state instead of the actions | | collapsedLines | `number` | No | Number of lines the plan is collapsed to, `8` by default | | labels | `Partial` | No | Overrides of the default English labels | | className | `string` | No | Class name added to the root element | | style | `React.CSSProperties` | No | Inline styles added to the root element | ## RewindDialog URL: https://sinups.github.io/ai-kit/docs/rewind-dialog ### Code ```tsx import { RewindDialog } from "@sinups/ai-kit"; export function Example() { return ( agent.rewind(messageId, { restoreCode: mode === "conversation-and-code" })} /> ); } ``` ### Usage Return the conversation to an earlier user message. User messages become rewind points with their time and the number of messages that will be removed; the user chooses whether to restore only the conversation or the conversation and code (`modes`). The dialog closes when `onRewind` resolves and shows the error when it rejects. ### Example: Rewind ```tsx ``` ### API reference | Prop | Type | Required | Description | | --- | --- | --- | --- | | opened | `boolean` | Yes | Whether the dialog is open | | onClose | `() => void` | Yes | Closes the dialog | | messages | `ChatMessage[]` | Yes | Conversation messages; user messages become rewind points | | onRewind | `(request: RewindRequest) => void \| Promise` | Yes | Rewinds the conversation; the dialog closes when the promise resolves and shows the error when it rejects | | onSummarize | `(request: SummarizeRequest) => void \| Promise` | No | Summarizes the conversation from or up to the selected point; the summarize actions are shown only when set, the dialog closes when the promise resolves | | defaultMessageId | `string` | No | Point selected when the dialog opens, the newest message by default | | modes | `RewindMode[]` | No | Restore modes offered, both by default; with a single mode the choice is hidden | | labels | `Partial` | No | Overrides of the default English labels | ## ToolResultNotice URL: https://sinups.github.io/ai-kit/docs/tool-result-notice ### Code ```tsx import { ToolResultNotice } from "@sinups/ai-kit"; export function Example() { return ( ); } ``` ### Usage Leave a compact trace of a tool call that did not complete: rejected by the user, cancelled, failed or interrupted. The row names the tool and its subject and expands to the user's feedback, the error output or an explanation. ### Example: Variants ```tsx <> ``` ### API reference | Prop | Type | Required | Description | | --- | --- | --- | --- | | variant | `ToolResultNoticeVariant` | Yes | Why the tool call did not complete | | toolName | `string` | Yes | Tool name, for example `Edit` or `git_search` | | detail | `string` | No | Short subject of the call shown after the tool name, for example a file path | | feedback | `string` | No | What the user told the agent when rejecting the call, quoted in the expanded row | | errorText | `string` | No | Error output of a failed call, shown as code in the expanded row | | reason | `React.ReactNode` | No | Extra explanation shown in the expanded row, for example why the call was interrupted | | defaultExpanded | `boolean` | No | Initial expanded state, `false` by default | | labels | `Partial` | No | Overrides of the default English labels | | className | `string` | No | Class name added to the root element | | style | `React.CSSProperties` | No | Inline styles added to the root element | ## MemoryNotice URL: https://sinups.github.io/ai-kit/docs/memory-notice ### Code ```tsx import { MemoryNotice } from "@sinups/ai-kit"; export function Example() { return ( openFile("AGENTS.md")} onUndo={() => api.removeMemory(memoryId)} /> ); } ``` ### Usage Tell the user that the agent saved something to memory. The collapsed row previews the first line as plain text, without Markdown markers, and expands to the saved text rendered as Markdown and names where it was saved; Open and Undo buttons appear when their callbacks are set, and the notice switches to the removed state once `onUndo` resolves. ### Example: Saved memories ```tsx <> ``` ### API reference | Prop | Type | Required | Description | | --- | --- | --- | --- | | content | `string` | Yes | What was remembered, rendered as Markdown when expanded | | target | `string` | No | Where the memory was saved, for example `AGENTS.md` | | onOpen | `() => void` | No | Adds an open button, for example to show the memory file | | onUndo | `() => void \| Promise` | No | Adds an undo button that removes the memory; the notice switches to the removed state when the promise resolves | | defaultExpanded | `boolean` | No | Initial expanded state, `false` by default | | labels | `Partial` | No | Overrides of the default English labels | | className | `string` | No | Class name added to the root element | | style | `React.CSSProperties` | No | Inline styles added to the root element | ## CommandChip URL: https://sinups.github.io/ai-kit/docs/command-chip ### Code ```tsx import { CommandChip } from "@sinups/ai-kit"; export function Example() { return ; } ``` ### Usage Show a slash command invocation as a badge followed by its arguments, with the description in a tooltip. Pass `commands` to `MessageList` or `UserMessage` and user messages that start with a known command render this chip automatically. ### Example: Sizes ```tsx <> ``` ### API reference | Prop | Type | Required | Description | | --- | --- | --- | --- | | name | `string` | Yes | Command name with or without the leading slash, for example `review` | | args | `string` | No | Arguments typed after the command | | description | `string` | No | Command description shown in a tooltip | | icon | `React.ReactNode` | No | Icon rendered in the badge instead of the slash | | size | `Extract` | No | Badge and text size, `sm` by default | | className | `string` | No | Class name added to the root element | | style | `React.CSSProperties` | No | Inline styles added to the root element | --- # Tools ## ToolRenderer URL: https://sinups.github.io/ai-kit/docs/tool-renderer ### Code ```tsx import { ToolRenderer, type CustomToolRendererProps, type ToolPart } from "@sinups/ai-kit"; function DeployCard({ input, status, onAction }: CustomToolRendererProps) { return ( ); } export function Example({ part, chatStatus }: { part: ToolPart; chatStatus: string }) { return ( console.log(toolCallId, action, payload)} /> ); } ``` ### Usage Render one tool part (AI SDK v5 shape: `{ type, toolCallId, state, input, output }`) with the matching card: `tool-Bash` → BashTool, `tool-Edit` and `tool-Write` → EditTool, `tool-Grep`, `tool-Glob` and `tool-WebSearch` → SearchTool, `tool-TodoWrite` → TodoTool, `tool-PlanWrite` → PlanTool, `tool-Question` → QuestionTool, `tool-Task` and `tool-Agent` → ToolGroup with `nestedTools`, `tool-Thinking` → ThinkingTool, `tool-mcp____` → McpTool. Types listed in `toolRegistry` render as GenericTool with a title, and anything else as a generic row with the tool name. `dynamic-tool` parts are routed by their `toolName`. Entries in `toolRenderers` are keyed by the full part type (`tool-Bash`, `tool-mcp__git__search`) and take precedence over built-in cards; a bare name only matches `mcp__user-tools__`. They receive `name`, `input`, `output` (unwrapped for MCP), `status` (`streaming`, `pending`, `success`, `error`), `part` and `onAction`, which reports to `onToolAction`. Pass `chatStatus` so a tool without output is shown as pending while the chat streams and as finished after it stops. MessageList uses ToolRenderer for every tool part, so in a chat you usually pass `toolRenderers` to AgentChat instead. The card parts are also exported for custom layouts: `BashToolTerminalCard`, `EditToolDiffCard`, `SearchGroupRich`, `ThinkingCollapsed` and `GenericToolRow`. ### Example: Built-in cards ```tsx {parts.map((part) => ( ))} ``` ### Example: Custom renderer ```tsx handle(toolCallId, action, payload)} /> ``` ### API reference | Prop | Type | Required | Description | | --- | --- | --- | --- | | part | `ToolPart` | Yes | Tool part in AI SDK v5 shape: `{ type, toolCallId, state, input, output }` | | nestedTools | `ToolPart[]` | No | Tool parts nested under this part, passed to `ToolGroup` for `tool-Task` / `tool-Agent` | | chatStatus | `string` | No | Chat status from `useChat()`, used to tell a pending tool from an interrupted one | | toolRenderers | `Record>` | No | Custom renderers keyed by part type (`tool-Bash`, `tool-mcp__git__search`), which take precedence over the built-in card, or by `` for `mcp__user-tools__` | | onToolAction | `ToolActionHandler` | No | Receives actions reported by custom renderers through `onAction` | | wrapLines | `boolean` | No | Wraps long lines in diffs instead of scrolling them sideways | ## BashTool URL: https://sinups.github.io/ai-kit/docs/bash-tool ### Code ```tsx import { BashTool } from "@sinups/ai-kit"; const part = { type: "tool-Bash", toolCallId: "bash-1", state: "output-available", input: { command: "ls -la" }, output: { stdout: "app\nlib\nREADME.md" }, }; export function Example() { return ; } ``` ### Usage Render a command tool card. Provide input.command and optional output.stdout; use input.approval for the footer. ### Example: Terminal card ```tsx ``` ### Example: Running state ```tsx const pendingPart = { type: "tool-Bash", toolCallId: "bash-2", state: "input-streaming", input: { command: "git status" }, }; ``` ### Example: Approval footer ```tsx const approvalPart = { type: "tool-Bash", toolCallId: "bash-3", state: "input-available", input: { command: "pnpm test --filter ./apps/web -- --runInBand", approval: { labels: { approve: "Run", reject: "Skip" } }, }, }; ``` ### API reference | Prop | Type | Required | Description | | --- | --- | --- | --- | | part | `ToolPart` | Yes | Tool part in AI SDK v5 shape: `{ type, toolCallId, state, input, output }` | | withOutputMeta | `boolean` | No | Shows exit code, duration, timeout, size and a copy button above the output, `false` by default | | formatOutput | `boolean` | No | Renders ANSI colors, clickable links, pretty JSON and a Show all toggle in the output | | commandSummary | `'short' \| 'full'` | No | Header text: `short` lists the programs of a pipeline (`ls, grep`), `full` shows the whole command on one line, `short` by default | | className | `string` | No | Class name added to the root element | | style | `React.CSSProperties` | No | Inline styles added to the root element | ## EditTool URL: https://sinups.github.io/ai-kit/docs/edit-tool ### Code ```tsx import { EditTool } from "@sinups/ai-kit"; const part = { type: "tool-Edit", toolCallId: "edit-1", state: "output-available", input: { file_path: "/app/page.tsx" }, output: { old_content: "export const metadata = { title: 'Old' };\n\nexport default function Page() {\n return
Old content
;\n}\n", content: "export const metadata = { title: 'Updated' };\n\nexport default function Page() {\n return (\n
\n

Release notes

\n

New layout applied.

\n
\n );\n}\n", }, }; export function Example() { return ; } ``` ### Usage Render a diff card for file edits. Supply input.file_path plus diff content (old/new or structuredPatch); use input.approval for the footer. ### Example: Diff card ```tsx ``` ### Example: Approval footer ```tsx const approvalPart = { type: "tool-Edit", toolCallId: "edit-7", state: "output-available", input: { file_path: "/app/page.tsx", approval: { labels: { approve: "Apply", reject: "Skip" } }, }, output: { old_content: "export const metadata = { title: 'Old' };\n\nexport default function Page() {\n return
Old content
;\n}\n", content: "export const metadata = { title: 'Updated' };\n\nexport default function Page() {\n return
New content
;\n}\n", }, }; ``` ### Example: Collapsible diff ```tsx const longPart = { type: "tool-Edit", toolCallId: "edit-1b", state: "output-available", input: { file_path: "/app/page.tsx" }, output: { old_content: "export const metadata = { title: 'Old' };\n\nexport default function Page() {\n return (\n
\n

Dashboard

\n

Old copy here.

\n
\n

Highlights

\n
    \n
  • Shipping ETA
  • \n
  • Billing status
  • \n
  • Support inbox
  • \n
\n
\n
\n

Activity

\n

Recent items...

\n
\n
\n );\n}\n", content: "export const metadata = { title: 'Updated' };\n\nexport default function Page() {\n return (\n
\n
\n

Release notes

\n

New layout applied.

\n
\n
\n

Highlights

\n
    \n
  • Sync latency improvements
  • \n
  • Workspace search redesign
  • \n
  • Billing transparency
  • \n
\n
\n
\n

Activity

\n

Recent items with timestamps...

\n
\n
\n

More

\n

Additional details and links.

\n
\n
\n );\n}\n", }, }; ``` ### Example: Pending edit ```tsx const pendingPart = { type: "tool-Edit", toolCallId: "edit-2", state: "input-streaming", input: { file_path: "/app/page.tsx", old_string: "const title = 'Old';\n", new_string: "const title = 'Updated';\n", }, }; ``` ### Example: Waiting for diff ```tsx const placeholderPart = { type: "tool-Edit", toolCallId: "edit-2b", state: "input-streaming", input: {}, }; ``` ### Example: Structured patch ```tsx const patchPart = { type: "tool-Edit", toolCallId: "edit-3", state: "output-available", input: { file_path: "/app/page.tsx" }, output: { structuredPatch: [ { lines: [ "-const title = 'Old';", "+const title = 'Updated';", ], }, ], }, }; ``` ### Example: Write tool ```tsx const writePart = { type: "tool-Write", toolCallId: "write-1", state: "output-available", input: { file_path: "/app/new.tsx" }, output: { content: "export const Demo = () => null\n" }, }; ``` ### Example: Missing file path ```tsx const noPathPart = { type: "tool-Edit", toolCallId: "edit-4", state: "output-available", input: { old_string: "foo", new_string: "bar" }, }; ``` ### API reference | Prop | Type | Required | Description | | --- | --- | --- | --- | | part | `ToolPart` | Yes | Tool part in AI SDK v5 shape: `{ type, toolCallId, state, input, output }` | | isCollapsible | `boolean` | No | Clamp the diff to 260px with a "Show more" toggle, `false` by default | | wordHighlight | `boolean` | No | Highlights the changed words inside replaced lines, `false` by default | | wrapLines | `boolean` | No | Wraps long diff lines instead of scrolling horizontally, `false` by default | | highlighter | `SyntaxHighlighter` | No | Colors the diff with this highlighter, the language comes from the file extension | | className | `string` | No | Class name added to the root element | | style | `React.CSSProperties` | No | Inline styles added to the root element | ## SearchTool URL: https://sinups.github.io/ai-kit/docs/search-tool ### Code ```tsx import { SearchTool } from "@sinups/ai-kit"; const mockResults = { results: [ { source: "google", title: "United UA837 SFO→NRT · $1,105 economy", date: "google.com/flights" }, { source: "expedia", title: "SFO–Tokyo · 14 results from $1,089", date: "expedia.com" }, ], }; const part = { type: "tool-WebSearch", toolCallId: "search-1", state: "output-available", input: { query: "best flights to Tokyo" }, output: mockResults, }; export function Example() { return ; } ``` ### Usage Render grouped search results. Provide input.query/pattern and output.results (or pass results directly with the results prop). Use defaultOpen to keep it expanded. ### Example: Rich results ```tsx ``` ### Example: Pending search ```tsx const pendingPart = { type: "tool-WebSearch", toolCallId: "search-2", state: "input-streaming", input: { query: "redis sliding window rate limiting" }, }; ``` ### Example: Alt source set ```tsx const altResults = { results: [ { source: "arxiv", title: "Quantum error correction below threshold · Acharya 2024", date: "arxiv.org" }, { source: "scholar", title: "Utility of quantum computing · Kim et al · 567 cites", date: "scholar.google.com" }, ], }; const altPart = { type: "tool-WebSearch", toolCallId: "search-3", state: "output-available", input: { query: "quantum error correction" }, output: altResults, }; ``` ### API reference | Prop | Type | Required | Description | | --- | --- | --- | --- | | part | `ToolPart` | Yes | Tool part in AI SDK v5 shape: `{ type, toolCallId, state, input, output }` | | results | `SearchResult[]` | No | Results override, `part.output.results` is used when omitted | | defaultOpen | `boolean` | No | Initial expanded state of the results panel | | className | `string` | No | Class name added to the root element | | style | `React.CSSProperties` | No | Inline styles added to the root element | ## TodoTool URL: https://sinups.github.io/ai-kit/docs/todo-tool ### Code ```tsx import { TodoTool } from "@sinups/ai-kit"; const part = { type: "tool-TodoWrite", toolCallId: "todo-1", state: "output-available", input: { todos: [ { content: "Audit components", status: "completed" }, { content: "Tighten spacing", status: "in_progress", activeForm: "Tightening spacing" }, { content: "Ship updates", status: "pending" }, ], }, output: { oldTodos: [] }, }; export function Example() { return ; } ``` ### Usage Render task list changes from input.todos, optionally diffed against output.oldTodos. ### Example: New list ```tsx const newListPart = { type: "tool-TodoWrite", toolCallId: "todo-1", state: "output-available", input: { todos: [ { content: "Audit components", status: "completed" }, { content: "Tighten spacing", status: "in_progress", activeForm: "Tightening spacing" }, { content: "Ship updates", status: "pending" }, ], }, output: { oldTodos: [] }, }; ``` ### Example: Single update ```tsx const singleUpdatePart = { type: "tool-TodoWrite", toolCallId: "todo-2", state: "output-available", input: { todos: [ { content: "Audit components", status: "completed" }, { content: "Tighten spacing", status: "completed" }, { content: "Ship updates", status: "pending" }, ], }, output: { oldTodos: [ { content: "Audit components", status: "completed" }, { content: "Tighten spacing", status: "in_progress" }, { content: "Ship updates", status: "pending" }, ], }, }; ``` ### Example: Multiple updates ```tsx const multipleUpdatePart = { type: "tool-TodoWrite", toolCallId: "todo-3", state: "output-available", input: { todos: [ { content: "Audit components", status: "completed" }, { content: "Tighten spacing", status: "completed" }, { content: "Ship updates", status: "in_progress" }, ], }, output: { oldTodos: [ { content: "Audit components", status: "completed" }, { content: "Tighten spacing", status: "pending" }, { content: "Ship updates", status: "pending" }, ], }, }; ``` ### Example: Pending update ```tsx const pendingPart = { type: "tool-TodoWrite", toolCallId: "todo-4", state: "input-streaming", input: { todos: [{ content: "Ship updates", status: "in_progress" }] }, output: { oldTodos: [{ content: "Ship updates", status: "pending" }] }, }; ``` ### API reference | Prop | Type | Required | Description | | --- | --- | --- | --- | | part | `ToolPart` | Yes | Tool part in AI SDK v5 shape: `{ type, toolCallId, state, input, output }` | | chatStatus | `string` | No | Chat status from `useChat()`, used to tell a pending tool from an interrupted one | | maxVisible | `number` | No | Shows at most this many todos, in-progress first, with a summary of the rest | | labels | `Partial` | No | Overrides of the default English labels | | className | `string` | No | Class name added to the root element | | style | `React.CSSProperties` | No | Inline styles added to the root element | ## PlanTool URL: https://sinups.github.io/ai-kit/docs/plan-tool ### Code ```tsx import { PlanTool } from "@sinups/ai-kit"; const part = { type: "tool-PlanWrite", toolCallId: "plan-1", state: "output-available", input: { plan: { id: "plan-1", title: "Refresh UI previews", summary: "Unify tool card spacing and interaction patterns so docs previews feel cohesive across all tool components.\n\n1. Standardize card chrome (header height, borders, radius, and muted labels) for Plan, Approval, Edit, Search, and Todo previews.\n2. Align content density and typography so title, metadata, and body text read consistently at a glance.\n3. Normalize interaction states: loading shimmer, pending indicators, hover affordances, and disabled action buttons.\n4. Validate responsive behavior on narrow widths, including truncation rules and action-row wrapping.\n5. Run a visual QA pass in both light and dark themes and tighten spacing where cards feel too loose or cramped.\n\nOutcome: preview gallery feels intentionally designed, easier to scan, and stable across viewport sizes.", }, }, }; export function Example() { return ; } ``` ### Usage Display a plan title and summary with expand/collapse. Set input.approved to hide approval controls. ### Example: In progress ```tsx ``` ### Example: Approved ```tsx const approvedPart = { type: "tool-PlanWrite", toolCallId: "plan-2", state: "output-available", input: { approved: true, plan: { id: "plan-2", title: "Gateway rollout", summary: "Plan approved and ready to execute." }, }, }; ``` ### Example: Pending update ```tsx const pendingPart = { type: "tool-PlanWrite", toolCallId: "plan-4", state: "input-streaming", input: { plan: { id: "plan-4", title: "Expand tool docs", summary: "Drafting an updated plan..." } }, }; ``` ### API reference | Prop | Type | Required | Description | | --- | --- | --- | --- | | part | `ToolPart` | Yes | Tool part in AI SDK v5 shape: `{ type, toolCallId, state, input, output }` | | chatStatus | `string` | No | Chat status from `useChat()`, used to tell a pending tool from an interrupted one | | className | `string` | No | Class name added to the root element | | style | `React.CSSProperties` | No | Inline styles added to the root element | ## ToolGroup URL: https://sinups.github.io/ai-kit/docs/tool-group ### Code ```tsx import { ToolGroup } from "@sinups/ai-kit"; const part = { type: "tool-Task", toolCallId: "task-1", state: "output-available", input: { description: "Collect previews", subagent_type: "explore" }, output: { totalDurationMs: 6200 }, }; const nestedTools = [ { type: "tool-Bash", state: "output-available", input: { command: "pnpm lint" } }, { type: "tool-Grep", state: "output-available", input: { pattern: "InputBar" } }, { type: "tool-Read", state: "output-available", input: { file_path: "/package/src/input/InputBar.tsx" } }, ]; export function Example() { return ( ); } ``` ### Usage Summarize task runs with optional nested tools. Use defaultOpen for initial expand state, maxVisibleTools for streaming height, and showElapsed to hide/show elapsed time. ### Example: Completed with tools ```tsx ``` ### Example: Streaming demo ```tsx ``` ### Example: Interrupted ```tsx ``` ### API reference | Prop | Type | Required | Description | | --- | --- | --- | --- | | part | `ToolPart` | Yes | Tool part in AI SDK v5 shape: `{ type, toolCallId, state, input, output }` | | nestedTools | `ToolPart[]` | No | Tool parts executed inside this group, rendered as nested rows | | chatStatus | `string` | No | Chat status from `useChat()`, used to tell a pending tool from an interrupted one | | completeLabel | `string` | Yes | Row label once the group has finished | | shimmerLabel | `string` | No | Shimmer label while the group is running | | interruptedLabel | `string` | Yes | Row label when the chat stopped before the group finished | | maxVisibleTools | `number` | No | Rows visible in the streaming viewport, `5` by default | | defaultOpen | `boolean` | No | Auto-expand the group when it starts running, `false` disables it | | showElapsed | `boolean` | No | Show elapsed time at the end of the row, `true` by default | | className | `string` | No | Class name added to the root element | | style | `React.CSSProperties` | No | Inline styles added to the root element | ## SubagentTool URL: https://sinups.github.io/ai-kit/docs/subagent-tool ### Code ```tsx import { SubagentTool } from "@sinups/ai-kit"; const part = { type: "tool-Task", toolCallId: "task-1", state: "output-available", input: { description: "Collect previews", subagent_type: "explore" }, output: { totalDurationMs: 6200 }, }; const nestedTools = [ { type: "tool-Bash", state: "output-available", input: { command: "pnpm lint" } }, { type: "tool-Grep", state: "output-available", input: { pattern: "InputBar" } }, { type: "tool-Read", state: "output-available", input: { file_path: "/package/src/input/InputBar.tsx" } }, ]; export function Example() { return ; } ``` ### Usage Render a task with nested tool calls. Shows elapsed time and the last nested tool while running. ### Example: Completed ```tsx ``` ### Example: Pending ```tsx ``` ### Example: Interrupted ```tsx ``` ### API reference | Prop | Type | Required | Description | | --- | --- | --- | --- | | part | `ToolPart` | Yes | Tool part in AI SDK v5 shape: `{ type, toolCallId, state, input, output }` | | nestedTools | `ToolPart[]` | No | Tool parts executed by the subagent, rendered as nested rows | | chatStatus | `string` | No | Chat status from `useChat()`, used to tell a pending tool from an interrupted one | | className | `string` | No | Class name added to the root element | | style | `React.CSSProperties` | No | Inline styles added to the root element | ## QuestionTool URL: https://sinups.github.io/ai-kit/docs/question-tool ### Code ```tsx import { useState } from "react"; import { QuestionTool } from "@sinups/ai-kit"; const questions = [ { kind: "single", title: "Which direction should I take?", options: [ { id: "small", label: "Small patch" }, { id: "full", label: "Full refactor" }, ], allowCustom: true, }, { kind: "single", title: "How cautious should the rollout be?", options: [ { id: "safe", label: "Safe and incremental" }, { id: "fast", label: "Fast rollout" }, ], allowCustom: true, }, ]; export function Example() { const [questionIndex, setQuestionIndex] = useState(1); const totalQuestions = questions.length; const part = { type: "tool-Question", toolCallId: "question-1", state: "input-available", input: { questions, questionIndex, totalQuestions, onPreviousQuestion: () => setQuestionIndex((prev) => Math.max(1, prev - 1)), onNextQuestion: () => setQuestionIndex((prev) => Math.min(totalQuestions, prev + 1)), submitLabel: "Submit", skipLabel: "Skip", onSubmitAnswer: (answer) => console.log(answer), }, }; return ; } ``` ### Usage Support single, multi, and free-text questions. It auto-advances and summarizes by default; wire questionIndex + totalQuestions for controlled navigation. ### Example: Single choice ```tsx ``` ### Example: Multiple choice ```tsx ``` ### Example: Text answer ```tsx ``` ### API reference | Prop | Type | Required | Description | | --- | --- | --- | --- | | part | `QuestionToolPart` | Yes | | | chatStatus | `string` | No | | | className | `string` | No | | | style | `React.CSSProperties` | No | | ## McpTool URL: https://sinups.github.io/ai-kit/docs/mcp-tool ### Code ```tsx import { McpTool } from "@sinups/ai-kit"; import { parseMcpToolType } from "@sinups/ai-kit"; const mcpInfo = parseMcpToolType("tool-ListMcpResources"); const part = { type: "tool-ListMcpResources", toolCallId: "mcp-1", state: "output-available", input: { query: "resources" }, output: [ { type: "text", text: "[{\"id\":\"res_1\",\"name\":\"Billing\"},{\"id\":\"res_2\",\"name\":\"Support\"}]" }, ], }; export function Example() { return ; } ``` ### Usage Render MCP tool calls with expandable output. Provide part + mcpInfo from parseMcpToolType, use chatStatus to reflect streaming/interrupted state, and defaultOpen to keep output expanded. ### Example: Completed output ```tsx ``` ### Example: Pending ```tsx const pendingPart = { type: "tool-ListMcpResources", toolCallId: "mcp-2", state: "input-streaming", input: { query: "resources" }, }; ``` ### Example: Interrupted ```tsx const interruptedPart = { type: "tool-ListMcpResources", toolCallId: "mcp-3", state: "input-streaming", input: { query: "resources" }, }; ``` ### API reference | Prop | Type | Required | Description | | --- | --- | --- | --- | | part | `ToolPart` | Yes | Tool part in AI SDK v5 shape: `{ type, toolCallId, state, input, output }` | | mcpInfo | `McpToolInfo` | Yes | Parsed server/tool names, see `parseMcpToolType` | | chatStatus | `string` | No | Chat status from `useChat()`, used to tell a pending tool from an interrupted one | | defaultOpen | `boolean` | No | Initial expanded state of the output panel | | className | `string` | No | Class name added to the root element | | style | `React.CSSProperties` | No | Inline styles added to the root element | ## ThinkingTool URL: https://sinups.github.io/ai-kit/docs/thinking-tool ### Code ```tsx import { ThinkingTool } from "@sinups/ai-kit"; const part = { type: "tool-Thinking", toolCallId: "think-1", state: "output-available", input: { thought: "Reviewing component coverage and preview density." }, }; export function Example() { return ; } ``` ### Usage Render assistant reasoning in a collapsible row. Use defaultOpen for uncontrolled expand, or expanded + onToggleExpand for controlled state. You can also render from mapped step/state/onComplete instead of part. ### Example: Streaming text ```tsx const streamingPart = { type: "tool-Thinking", toolCallId: "think-2", state: "input-streaming", input: { thought: "Drafting a response with tool coverage and previews.\n" + "First outline the sections, then refine the examples and polish copy.\n" + "Keep the final response concise and actionable.", }, }; ``` ### Example: Collapsed ```tsx ``` ### API reference | Prop | Type | Required | Description | | --- | --- | --- | --- | | part | `ToolPart` | No | Tool part in AI SDK v5 shape, `input.thought` or string output is the reasoning text | | step | `ToolCallStep` | No | Timeline step, used together with `state` and `onComplete` instead of `part` | | state | `StepState` | No | Animation state of the step | | onComplete | `() => void` | No | Called once `step.duration` elapses while animating | | defaultOpen | `boolean` | No | Initial expanded state in uncontrolled mode | | expanded | `boolean` | No | Controlled expanded state | | onToggleExpand | `() => void` | No | Called when the row is toggled | | className | `string` | No | Class name added to the root element | | style | `React.CSSProperties` | No | Inline styles added to the root element | ## GenericTool URL: https://sinups.github.io/ai-kit/docs/generic-tool ### Code ```tsx import { GenericTool } from "@sinups/ai-kit"; export function Example() { return ( ); } ``` ### Usage Render a simple tool row for custom tools. Provide title/subtitle and control loading with isPending. icon lets you pass a custom icon component. ### Example: Completed ```tsx ``` ### Example: Pending ```tsx ``` ### Example: Compatibility flag ```tsx ``` ### API reference | Prop | Type | Required | Description | | --- | --- | --- | --- | | icon | `React.ComponentType<{ className?: string }>` | No | Icon component rendered in the 12x12 slot before the title | | title | `string` | Yes | Row label, shown with shimmer while `isPending` | | subtitle | `string` | No | Muted, truncated text after the title | | isPending | `boolean` | Yes | Whether the tool is still running | | isError | `boolean` | No | Whether the tool failed, reserved for future styling | | className | `string` | No | Class name added to the root element | | style | `React.CSSProperties` | No | Inline styles added to the root element | ## ToolApprovalFooter URL: https://sinups.github.io/ai-kit/docs/tool-approval-footer ### Code ```tsx import { ToolApprovalFooter } from "@sinups/ai-kit"; export function Example() { return ( approve(scope)} onReject={() => reject()} onRejectWithFeedback={(feedback) => reject(feedback)} /> ); } ``` ### Usage Ask the user to confirm a tool call. approveOptions adds a menu next to the approve button so the user can pick a scope (once, session, always) that arrives in onApprove(scope); the main button still calls onApprove() without a scope. reason explains why confirmation is needed, and onRejectWithFeedback lets the user reject with instructions for the agent. BashTool and EditTool accept the same fields through their approval prop. ### Example: Approval scopes and feedback ```tsx approve(scope)} onRejectWithFeedback={(feedback) => reject(feedback)} /> ``` ### Example: Basic and pending ```tsx <> ``` ### API reference | Prop | Type | Required | Description | | --- | --- | --- | --- | | isPending | `boolean` | No | The tool call has not finished yet; after approval the footer shows "Starting..." while it is set | | isComplete | `boolean` | No | The tool call finished, the footer is no longer needed and renders nothing | | className | `string` | No | Class name added to the root element | | style | `React.CSSProperties` | No | Inline styles added to the root element | | approveLabel | `string` | No | @deprecated Use `labels.approve` | | rejectLabel | `string` | No | @deprecated Use `labels.reject` | | onApprove | `(scope?: string) => void` | No | Called once when the tool is approved; receives the scope picked from `approveOptions` | | onReject | `() => void` | No | Called once when the reject button is clicked | | approveOptions | `ToolApprovalOption[]` | No | Approval scopes shown in a menu next to the approve button | | hideWhenComplete | `boolean` | No | Removes the footer from a tool card once its call has finished, `false` by default | | reason | `React.ReactNode` | No | Why confirmation is needed, shown until a decision is made | | onRejectWithFeedback | `(feedback: string) => void` | No | Adds a "Reject with feedback" menu item; called with the typed feedback on Enter | | onExplain | `() => Promise` | No | Loads a risk assessment shown under the footer by the "Why?" button; the result is cached | | ruleSuggestion | `ToolApprovalRuleSuggestion` | No | Rule saved by the `always` scope; picking that scope opens an editor before approving | | matchedRule | `React.ReactNode` | No | Permission rule that caused the prompt, shown after `labels.matchedRule` | | requestedBy | `ToolApprovalRequester` | No | Worker agent that requested the tool call, shown as a badge | | labels | `Partial` | No | Overrides of the default English labels | ## ElicitationForm URL: https://sinups.github.io/ai-kit/docs/elicitation-form ### Code ```tsx import { ElicitationForm } from "@sinups/ai-kit"; export function Example() { return ( respond({ action: "accept", content })} onDecline={() => respond({ action: "decline" })} onCancel={() => respond({ action: "cancel" })} /> ); } ``` ### Usage Answer MCP elicitation requests. Pass the server's requestedSchema and the form builds Mantine fields for strings (email, uri, date, date-time), numbers, booleans, single and multi-select enums, validates required fields and bounds, and calls onAccept with only the filled values. Use mode="url" when the server asks the user to open a link. Short fields go into two columns once the form itself is wider than 520px, so the same component fits a narrow widget and a full-page chat. ### Example: Form request ```tsx respond({ action: "accept", content })} onDecline={() => respond({ action: "decline" })} onCancel={() => respond({ action: "cancel" })} /> ``` ### Example: URL request ```tsx respond({ action: "accept" })} onDecline={() => respond({ action: "decline" })} /> ``` ### Example: Disabled ```tsx ``` ### API reference | Prop | Type | Required | Description | | --- | --- | --- | --- | | message | `string` | Yes | Message from the server explaining what is requested | | requestedSchema | `ElicitationRequestedSchema` | No | Flat object schema of the requested fields, used in `form` mode | | mode | `'form' \| 'url'` | No | `form` collects values, `url` asks to open an external link, `form` by default | | url | `string` | No | Link opened in `url` mode, only `http` and `https` links are shown | | serverName | `string` | No | Name of the MCP server that sent the request, shown in the header | | onAccept | `(content: ElicitationContent) => void` | No | Called with the collected values, `{}` in `url` mode after the link is opened | | onDecline | `() => void` | No | Called when the user explicitly refuses to answer | | onCancel | `() => void` | No | Called when the user dismisses the request, the cancel button is rendered only when set | | title | `string` | No | Header title when `serverName` is not set, `Input requested` by default | | disabled | `boolean` | No | Disables all fields and actions | | labels | `Partial` | No | Overrides of the default English labels | | className | `string` | No | Class name added to the root element | | style | `React.CSSProperties` | No | Inline styles added to the root element | ## ShellOutput URL: https://sinups.github.io/ai-kit/docs/shell-output ### Code ```tsx import { ShellOutput } from "@sinups/ai-kit"; export function Example({ output, exitCode, durationMs }: { output: string; exitCode: number; durationMs: number }) { return ; } export function Running({ output, startedAt }: { output: string; startedAt: number }) { return ; } ``` ### Usage Terminal output as a log: ANSI colors and styles, clickable links, pretty-printed JSON (when the whole output is a JSON object or array and has no ANSI codes), a copy button that strips ANSI codes, and a meta line with exit code (green for 0, red otherwise), duration, timeout and size. Collapsed, it shows the last `maxLines` lines (12 by default) with a `+N more lines` hint and a Show all toggle; expanded, the log scrolls up to `maxHeight`. With `live` it shows Running, ticks the duration from `startedAt`, follows new lines, and offers Scroll to latest when the user scrolls up. Empty output shows `No output`. `variant="compact"` drops the tinted panel for use inside another card. BashTool renders it for Bash tool parts; use it directly for any other command output. Related helpers: `parseAnsiLines`, `stripAnsi`, `hasAnsi`, `splitLinks`, `formatJsonOutput`, `tailLines`, `byteLength`, `formatBytes`, and `getBashRunInfo`, which reads output and metadata from a Bash tool part. ### Example: Finished commands ```tsx ``` ### Example: Live, narrow ```tsx ``` ### API reference | Prop | Type | Required | Description | | --- | --- | --- | --- | | output | `string` | Yes | Raw command output, ANSI escapes included | | maxLines | `number` | No | Lines shown from the end while collapsed, `12` by default; `0` shows everything | | live | `boolean` | No | The command is still running: shows a loader instead of the exit code | | exitCode | `number \| null` | No | Exit code, green for `0` and red otherwise | | durationMs | `number` | No | How long the command ran, in ms | | startedAt | `number \| Date` | No | Moment the command started, shows a ticking duration while `live` and `durationMs` is not set | | timeoutMs | `number` | No | Timeout of the command, in ms | | sizeBytes | `number` | No | Output size in bytes, measured from `output` by default | | formatJson | `boolean` | No | Pretty-prints output that is a JSON object or array, `true` by default | | withCopy | `boolean` | No | Shows the copy button, `true` by default | | withMeta | `boolean` | No | Shows exit code, duration, timeout and size above the log, `true` by default | | defaultExpanded | `boolean` | No | Starts expanded, showing every line in a scrollable log | | maxHeight | `number \| string` | No | Maximum height of the expanded log before it scrolls, `400` by default; while `live` it follows new lines until scrolled up | | variant | `'default' \| 'compact'` | No | `default` draws the log on a tinted panel, `compact` blends into a parent card | | labels | `Partial` | No | Overrides of the default English labels | | className | `string` | No | Class name added to the root element | | style | `React.CSSProperties` | No | Inline styles added to the root element | ## DiffView URL: https://sinups.github.io/ai-kit/docs/diff-view ### Code ```tsx import { DiffView } from "@sinups/ai-kit"; export function Example({ before, after }: { before: string; after: string }) { return ; } ``` ### Usage A unified line diff of two strings with line numbers, add and remove gutter markers and, for replaced lines, highlighted changed words (`wordHighlight`, off by default). Pass a `highlighter` and `language` to color the code; highlighting is combined with the change marks. Long lines scroll horizontally; set `wrapLines` in narrow containers. It shows the whole file without collapsing unchanged regions, so use it for edits of a few dozen lines. EditTool and EditToolDiffCard render it for Edit and Write tool parts. For multi-file review with split view and collapsed context, use DiffReview. Related helpers: `diffLines` and `countDiffStats`. ### Example: Wide ```tsx ``` ### Example: Narrow, wrapped ```tsx
``` ### API reference | Prop | Type | Required | Description | | --- | --- | --- | --- | | oldText | `string` | Yes | Content before the change | | newText | `string` | Yes | Content after the change | | wordHighlight | `boolean` | No | Highlights the changed words inside replaced lines, `false` by default | | wrapLines | `boolean` | No | Wraps long lines instead of scrolling horizontally, `false` by default | | highlighter | `SyntaxHighlighter` | No | Colors the code with this highlighter, plain text when omitted | | language | `string` | No | Language passed to `highlighter`, for example `ts` | | className | `string` | No | Class name added to the root element | | style | `React.CSSProperties` | No | Inline styles added to the root element | ## ActionRow URL: https://sinups.github.io/ai-kit/docs/action-row ### Code ```tsx "use client"; import { useState } from "react"; import { ActionRow, type StepState, type ToolCallStep } from "@sinups/ai-kit"; const step: ToolCallStep = { id: "step-1", type: "tool-call", toolName: "Updated upload client", toolDetail: "src/upload/client.ts", duration: 2_000, }; export function Example() { const [state, setState] = useState("animating"); return setState("complete")} />; } ``` ### Usage A row for scripted timelines rather than live tool parts. While `state` is `animating` it shows a rotating shimmer label (`Brewing...`, `Crafting...`, chosen by `index`); after `step.duration` milliseconds it calls `onComplete`, and once complete it shows `step.toolName`. Use it for product tours, demos and onboarding replays built from `ToolCallStep` objects. For real agent output, render tool parts with ToolRenderer or MessageList instead. `useToolComplete` is the hook behind the timer. ### Example: Animated step ```tsx setState("complete")} /> ``` ### API reference | Prop | Type | Required | Description | | --- | --- | --- | --- | | step | `ToolCallStep` | Yes | Timeline step describing the tool call | | state | `StepState` | Yes | Animation state of the step, the card shows shimmer while `animating` | | onComplete | `() => void` | Yes | Called once `step.duration` elapses while animating | | index | `number` | Yes | Index of the action in the timeline, selects the shimmer label | | className | `string` | No | Class name added to the root element | | style | `React.CSSProperties` | No | Inline styles added to the root element | ## ToolRowBase URL: https://sinups.github.io/ai-kit/docs/tool-row-base ### Code ```tsx import { Code } from "@mantine/core"; import { ToolRowBase } from "@sinups/ai-kit"; import { IconFileText } from "@tabler/icons-react"; export function Example({ running, preview }: { running: boolean; preview: string }) { return ( } shimmerLabel="Reading" completeLabel="Read" detail="src/upload/retry.ts" isAnimating={running} expandable={!running} > {preview} ); } ``` ### Usage The single-line row most tool cards are built on: a 12px icon, a label that shimmers while `isAnimating` (`shimmerLabel`) and settles to `completeLabel`, a muted truncated `detail`, and optional `trailingContent` such as elapsed time. With `expandable` the row becomes a button with a chevron and reveals `children` in a Collapse; it works uncontrolled (`defaultOpen`) or controlled (`expanded` with `onToggleExpand`). Other Mantine Box props pass through to the root, so you can add `data-*` attributes and style props. Use it to build a custom tool renderer that sits in the same visual rhythm as the built-in cards. ContextEventRow, HookActivity, ActionRow and GenericToolRow are built on it. The detail truncates, so the row fits any width. ### Example: Running and expandable ```tsx } shimmerLabel="Checking out branch" completeLabel="Checked out" detail="fix/upload-retry" isAnimating={running} /> } completeLabel="Read" detail="src/upload/retry.ts" isAnimating={false} expandable trailingContent={24 lines} > {preview} ``` ### API reference | Prop | Type | Required | Description | | --- | --- | --- | --- | | icon | `React.ReactNode` | No | Icon rendered before the label, 12x12 box | | shimmerLabel | `string` | No | Label shown with shimmer while `isAnimating` is true | | completeLabel | `string` | Yes | Label shown when the tool has completed | | isAnimating | `boolean` | Yes | | | detail | `string` | No | Muted, truncated text after the label | | trailingContent | `React.ReactNode` | No | Content rendered at the end of the row, for example elapsed time | | expandable | `boolean` | No | Whether the row can be expanded to reveal `children` | | expanded | `boolean` | No | Controlled expanded state | | defaultOpen | `boolean` | No | Initial expanded state in uncontrolled mode | | onToggleExpand | `() => void` | No | | | children | `React.ReactNode` | No | | ## FileExtIcon URL: https://sinups.github.io/ai-kit/docs/file-ext-icon ### Code ```tsx import { AgentModeIcon, FileExtIcon, PlanModeIcon, type ModeOption } from "@sinups/ai-kit"; const modes: ModeOption[] = [ { id: "agent", label: "Agent", icon: AgentModeIcon }, { id: "plan", label: "Plan", icon: PlanModeIcon }, ]; export function Example({ path }: { path: string }) { return ( {path} ); } ``` ### Usage Small language icons for file names: TypeScript (`ts`, `tsx`), JavaScript (`js`, `jsx`, `mjs`, `cjs`) and JSON (`json`, `jsonc`). For any other extension it renders nothing, so place a generic file icon next to it when you need one. `size` defaults to 10px, matching tool rows; EditTool and the DiffReview file icons use it. `AgentModeIcon` and `PlanModeIcon` are the icons of the two default composer modes; pass them as `icon` in ModeSelector options. All icons use Mantine color variables and follow the color scheme. ### Example: Icons ```tsx ``` ### API reference | Prop | Type | Required | Description | | --- | --- | --- | --- | | filename | `string` | Yes | | | size | `number \| string` | No | | | className | `string` | No | | | style | `React.CSSProperties` | No | | --- # Input ## InputBar URL: https://sinups.github.io/ai-kit/docs/input-bar ### Code ```tsx import { InputBar } from "@sinups/ai-kit"; export function Example() { return ( console.log(content)} status="ready" onStop={() => {}} /> ); } ``` ### Usage Collect prompts and attachments in the composer. Supports controlled mode (value/onChange), drag/paste handling, info bar, typing animation, multi-question navigation, and free-form toolbar slots (leftActions/rightActions) for composing model/mode pickers or any custom controls. ### Example: Basic input ```tsx ``` ### Example: With attachments ```tsx ``` ### Example: Focus outline ```tsx
``` ### Example: Info bar ```tsx {}, }} /> ``` ### Example: Info bar (bottom) ```tsx {}, position: "bottom", }} /> ``` ### Example: Question bar ```tsx const questions = [ { kind: "single", title: "Which direction should I take?", options: [ { id: "small", label: "Small patch" }, { id: "full", label: "Full refactor" }, ], allowCustom: true, }, { kind: "single", title: "How cautious should the rollout be?", options: [ { id: "safe", label: "Safe and incremental" }, { id: "fast", label: "Fast rollout" }, ], allowCustom: true, }, ]; console.log(answer), }} /> ``` ### Example: Toolbar actions (model + mode) ```tsx import { InputBar } from "@sinups/ai-kit"; import { ModelPicker } from "@sinups/ai-kit"; import { ModeSelector } from "@sinups/ai-kit"; import { IconCursor, IconBulb } from "@tabler/icons-react"; const models = [ { id: "llama-3.3-70b", name: "Llama 3.3", version: "70B" }, { id: "qwen-2.5-coder-32b", name: "Qwen 2.5 Coder", version: "32B" }, ]; const modes = [ { id: "agent", label: "Agent", icon: IconCursor }, { id: "plan", label: "Plan", icon: IconBulb }, ]; } /> ``` ### Example: Commands and mentions ```tsx const completions: CompletionSource[] = [ { trigger: "/", items: [ { value: "review", label: "/review", description: "Review the current diff", group: "Commands" }, { value: "compact", label: "/compact", description: "Summarize the conversation", group: "Commands" }, ], }, { trigger: "@", items: async (query) => searchPeople(query) }, ]; ``` ### Example: Message queue ```tsx setQueue((prev) => [...prev, { id: crypto.randomUUID(), content }])} queuedMessages={queue} onRemoveQueued={(id) => setQueue((prev) => prev.filter((item) => item.id !== id))} /> ``` ### Example: Full-width composer ```tsx ``` ### API reference | Prop | Type | Required | Description | | --- | --- | --- | --- | | onSend | `(message: { role: 'user'; content: string }) => void` | Yes | | | status | `ChatStatus` | Yes | | | onStop | `() => void` | Yes | | | placeholder | `string` | No | | | className | `string` | No | | | style | `React.CSSProperties` | No | | | contentWidth | `ContentWidth` | No | Max width of the composer: a number in px or any CSS width, `420px` by default. Pass `'100%'` for a full-width chat | | onAttach | `() => void` | No | Renders the attach button when provided | | attachedImages | `AttachedImage[]` | No | | | attachedFiles | `AttachedFile[]` | No | | | onRemoveImage | `(id: string) => void` | No | | | onRemoveFile | `(id: string) => void` | No | | | onPaste | `(e: React.ClipboardEvent) => void` | No | | | isDragOver | `boolean` | No | Highlights the field border while files are dragged over it | | enableImagePreview | `boolean` | No | Opens a staged image attachment in a fullscreen lightbox on click, `true` by default | | value | `string` | No | Controlled input value | | onChange | `(value: string) => void` | No | | | disabled | `boolean` | No | | | autoFocus | `boolean` | No | | | suggestions | `InputSuggestions` | No | Suggestion pills rendered above the field, never below it | | typingAnimation | `{ text: string; duration: number; image?: string; isActive: boolean; onComplete: () => void; }` | No | Simulated typing shown in place of the textarea, for demos | | infoBar | `{ title?: string; description?: string; onClose?: () => void; position?: 'top' \| 'bottom'; action?: { label: string; onClick: () => void; }; }` | No | Dismissible message strip above or below the field | | questionBar | `{ id: string; questions: QuestionConfig[]; questionIndex?: number; totalQuestions?: number; onPreviousQuestion?: () => void; onNextQuestion?: () => void; submitLabel?: string; skipLabel?: string; allowSkip?: boolean; onSubmit: (answer: QuestionAnswer, meta: { questionIndex: number }) => void; onSkip?: (meta: { questionIndex: number }) => void; }` | No | Inline question panel docked above the field. Changing `id` starts a new question set: the active index and stored answers are reset. | | completions | `CompletionSource[]` | No | Trigger-based autocomplete lists, for example slash commands on `/` and mentions on `@` | | queuedMessages | `QueuedMessage[]` | No | Messages waiting to be sent after the current response, shown above the field | | onRemoveQueued | `(id: string) => void` | No | Renders a remove button on every queued message | | onQueue | `(message: { role: 'user'; content: string }) => void` | No | When set, submitting while the response is streaming queues the message instead of ignoring it | | leftActions | `React.ReactNode` | No | Content rendered on the left of the toolbar, next to the attachment button | | rightActions | `React.ReactNode` | No | Content rendered on the right of the toolbar, before the send button | | pasteCollapseThreshold | `PasteCollapseThreshold \| false` | No | Pastes at or above the threshold collapse into an attachment chip and are expanded again in `onSend`; `false` turns collapsing off. 10000 characters or 50 lines by default | | history | `string[]` | No | Previously sent prompts, oldest first: ArrowUp in an empty field or at the start of the text shows older ones, ArrowDown returns to the draft | | historySearchHotkey | `string \| null` | No | Hotkey that opens the prompt history search over `history`, `mod+R` by default; `null` turns it off | | onHistorySearch | `() => void` | No | Called by the history search hotkey instead of opening the built-in dialog | | labels | `Partial` | No | Overrides of the default English labels | ## Suggestions URL: https://sinups.github.io/ai-kit/docs/suggestions ### Code ```tsx import { InputBar } from "@sinups/ai-kit"; import { IconCalendar, IconCode, IconPencil, IconSearch } from "@tabler/icons-react"; import { useState } from "react"; const items = [ { id: "write", label: "Write", value: "Write a concise project update with key milestones.", icon: , }, { id: "learn", label: "Learn", value: "Explain this codebase architecture in plain language.", icon: , }, { id: "code", label: "Code", value: "Generate a clean starter implementation for this feature.", icon: , }, { id: "calendar", label: "From Calendar", value: "Draft my agenda from tomorrow's calendar events.", icon: , }, ]; export function Example() { const [value, setValue] = useState(""); return ( console.log(content)} status="ready" onStop={() => {}} suggestions={{ items, className: "justify-center", itemClassName: "h-7 rounded-[6px] px-2 text-sm", }} /> ); } ``` ### Usage Show quick prompt chips and write the selected suggestion into InputBar for fast message drafting. Use disabled to pause interaction and item.className for per-chip styling. ### Example: Icons + text ```tsx import { IconCalendar, IconCode, IconPencil, IconSearch } from "@tabler/icons-react"; const items = [ { id: "write", label: "Write", icon: }, { id: "learn", label: "Learn", icon: }, { id: "code", label: "Code", icon: }, { id: "calendar", label: "From Calendar", icon: }, ]; console.log(item)} className="justify-center" itemClassName="h-7 rounded-[6px] px-2 text-sm" /> ``` ### Example: Fill InputBar ```tsx const [value, setValue] = useState(""); ``` ### API reference | Prop | Type | Required | Description | | --- | --- | --- | --- | | items | `SuggestionItem[]` | Yes | | | onSelect | `(item: SuggestionItem) => void` | Yes | | | disabled | `boolean` | No | | | className | `string` | No | | | itemClassName | `string` | No | Class name applied to every suggestion chip | | style | `React.CSSProperties` | No | | ## ModelPicker URL: https://sinups.github.io/ai-kit/docs/model-picker ### Code ```tsx import { ModelPicker } from "@sinups/ai-kit"; import { useState } from "react"; const models = [ { id: "llama-3.3-70b", name: "Llama 3.3", version: "70B" }, { id: "qwen-2.5-coder-32b", name: "Qwen 2.5 Coder", version: "32B" }, { id: "mistral-small-24b", name: "Mistral Small", version: "24B" }, ]; export function Example() { const [model, setModel] = useState("llama-3.3-70b"); return ( ); } ``` ### Usage Standalone model picker. Drop it into InputBar via leftActions/rightActions, a header, a settings sheet, or anywhere else. It does not depend on InputBar. Supports controlled and uncontrolled modes. Use ModelBadge for a read-only variant. ### Example: Uncontrolled ```tsx ``` ### Example: Inside InputBar ```tsx } /> ``` ### Example: Read-only badge ```tsx ``` ### API reference | Prop | Type | Required | Description | | --- | --- | --- | --- | | models | `ModelOption[]` | Yes | | | value | `string` | No | Controlled selected model id | | defaultValue | `string` | No | Initial selected model id in uncontrolled mode | | onChange | `(modelId: string) => void` | No | | | placeholder | `string` | No | Label shown when no model matches, `'Auto'` by default | | className | `string` | No | | | style | `React.CSSProperties` | No | | ## ModeSelector URL: https://sinups.github.io/ai-kit/docs/mode-selector ### Code ```tsx import { ModeSelector, type ModeOption } from "@sinups/ai-kit"; import { IconCursor, IconBulb } from "@tabler/icons-react"; import { useState } from "react"; const modes: ModeOption[] = [ { id: "agent", label: "Agent", icon: IconCursor }, { id: "plan", label: "Plan", icon: IconBulb, description: "Think before acting" }, ]; export function Example() { const [mode, setMode] = useState("agent"); return ; } ``` ### Usage Standalone mode selector: agent mode, plan mode, or any custom set. Bring your own icons or omit them. With a single mode the selector renders a non-interactive label. ### Example: Uncontrolled ```tsx ``` ### Example: Inside InputBar ```tsx } /> ``` ### API reference | Prop | Type | Required | Description | | --- | --- | --- | --- | | modes | `ModeOption[]` | Yes | | | value | `string` | No | Controlled selected mode id | | defaultValue | `string` | No | Initial selected mode id in uncontrolled mode | | onChange | `(modeId: string) => void` | No | | | className | `string` | No | | | style | `React.CSSProperties` | No | | ## SendButton URL: https://sinups.github.io/ai-kit/docs/send-button ### Code ```tsx import { SendButton } from "@sinups/ai-kit"; export function Example() { return ( ); } ``` ### Usage Render the send/stop control. Use state=idle | typing | streaming. ### Example: Idle ```tsx ``` ### Example: Typing ```tsx ``` ### Example: Streaming ```tsx ``` ### API reference | Prop | Type | Required | Description | | --- | --- | --- | --- | | state | `'idle' \| 'typing' \| 'streaming'` | Yes | `idle` when there is nothing to send, `typing` when input is non-empty, `streaming` while a response is in flight | | className | `string` | No | | | style | `React.CSSProperties` | No | | ## AttachmentButton URL: https://sinups.github.io/ai-kit/docs/attachment-button ### Code ```tsx import { AttachmentButton } from "@sinups/ai-kit"; export function Example() { return {}} />; } ``` ### Usage Render the round plus attachment trigger used by InputBar. Use onClick to open your picker action. ### Example: Default ```tsx {}} /> ``` ### Example: Paperclip icon ```tsx {}} icon="paperclip" /> ``` ### Example: Without handler ```tsx ``` ### API reference | Prop | Type | Required | Description | | --- | --- | --- | --- | | onClick | `() => void` | No | | | icon | `AttachmentButtonIcon \| React.ReactNode` | No | Icon to render inside the button. - `'plus'` (default): a `+` glyph, matches the generic "add something" affordance. - `'paperclip'`: a paperclip glyph, matches the more literal "attach file" affordance. - Any ReactNode fully overrides the icon; built-in sizing and color apply only to presets. | | className | `string` | No | | | style | `React.CSSProperties` | No | | ## FileAttachment URL: https://sinups.github.io/ai-kit/docs/file-attachment ### Code ```tsx import { FileAttachment } from "@sinups/ai-kit"; export function Example() { return ( {}} /> ); } ``` ### Usage Render a file/image chip. Use isImage + url for thumbnails, display="image-only" for previews, and onRemove to show the close control. ### Example: File + image ```tsx ``` ### Example: Image only ```tsx {}} /> ``` ### Example: Removable file ```tsx {}} /> ``` ### API reference | Prop | Type | Required | Description | | --- | --- | --- | --- | | id | `string` | Yes | | | filename | `string` | Yes | | | size | `number` | No | File size in bytes, shown under the name when provided | | isImage | `boolean` | No | | | url | `string` | No | Image URL, required for thumbnails and preview | | onRemove | `() => void` | No | | | className | `string` | No | | | display | `'chip' \| 'image-only'` | No | `'chip'` renders icon + name, `'image-only'` renders a square thumbnail (images with `url` only) | | enableImagePreview | `boolean` | No | Opens the image thumbnail in a fullscreen preview on click, `true` by default | | style | `React.CSSProperties` | No | | ## PastedTextAttachment URL: https://sinups.github.io/ai-kit/docs/pasted-text-attachment ### Code ```tsx import { PastedTextAttachment, type PastedText } from "@sinups/ai-kit"; export function Example({ paste, remove }: { paste: PastedText; remove: () => void }) { return ( ); } ``` ### Usage A chip for a large paste that was collapsed out of the composer. It looks like a file attachment, shows `Pasted text #1` and the line count, opens a read-only preview on click and has a remove button when `onRemove` is set. InputBar does this automatically: pastes at or above `pasteCollapseThreshold` (10,000 characters or 50 lines by default, `false` to turn off) become a `[Pasted text #N]` placeholder plus this chip, and `onSend` receives the expanded text. Render the chip yourself only in a custom composer, together with the helpers `shouldCollapsePaste`, `insertPastePlaceholder`, `expandPastedText`, `prunePastes`, `removePastePlaceholder` and `formatPasteLabel`. ### Example: Chips ```tsx {pastes.map((paste) => ( remove(paste.id)} /> ))} ``` ### API reference | Prop | Type | Required | Description | | --- | --- | --- | --- | | paste | `PastedText` | Yes | Collapsed paste | | onRemove | `() => void` | No | Renders the remove button when set | | labels | `Partial` | No | Overrides of the default English labels | ## PromptHistorySearch URL: https://sinups.github.io/ai-kit/docs/prompt-history-search ### Code ```tsx "use client"; import { useState } from "react"; import { Button } from "@mantine/core"; import { PromptHistorySearch } from "@sinups/ai-kit"; export function Example({ history, setDraft }: { history: string[]; setDraft: (value: string) => void }) { const [opened, setOpened] = useState(false); return ( <> setOpened(false)} history={history} onSelect={setDraft} /> ); } ``` ### Usage A fuzzy search dialog over prompts the user already sent. Pass `history` oldest first; the dialog lists unique prompts newest first, shows the first line of each and marks multi-line prompts with a line count. Picking a prompt calls `onSelect` and closes the dialog; an empty query shows all prompts, no match shows the `empty` label. It is built on CommandPalette, so it goes full screen on phones. InputBar already includes it: pass `history` to InputBar and Mod+R opens this dialog, while ArrowUp and ArrowDown browse history inline. Use `historySearchHotkey` to change or disable the hotkey, or `onHistorySearch` to open your own dialog. Related helpers for custom composers: `navigatePromptHistory`, `canBrowseOlder`, `canBrowseNewer`, `getSearchablePrompts`. ### Example: Dialog ```tsx ``` ### API reference | Prop | Type | Required | Description | | --- | --- | --- | --- | | opened | `boolean` | Yes | Whether the dialog is open | | onClose | `() => void` | Yes | Called on Escape, outside click and after a prompt is picked | | history | `string[]` | Yes | Previously sent prompts, oldest first | | onSelect | `(prompt: string) => void` | Yes | Called with the picked prompt | | labels | `Partial` | No | Overrides of the default English labels | ## InputPopover URL: https://sinups.github.io/ai-kit/docs/input-popover ### Code ```tsx "use client"; import { useState } from "react"; import { Button, Stack } from "@mantine/core"; import { InputPopover } from "@sinups/ai-kit"; export function Example({ branches }: { branches: string[] }) { const [branch, setBranch] = useState(branches[0]); const [open, setOpen] = useState(false); return ( {branch}} > {branches.map((name) => ( ))} ); } ``` ### Usage A thin wrapper over Mantine Popover with the dropdown look of the composer pickers (ModelPicker, ModeSelector). Use it for your own toolbar controls in InputBar `leftActions` or `rightActions` so they match. The trigger element's own `onClick` still runs and then toggles the dropdown; a non-element trigger is wrapped in a span. It works controlled (`open` and `onOpenChange`) or uncontrolled (`defaultOpen`). `side` and `align` map to Mantine positions (`top` and `start` by default, so it opens above the composer), `sideOffset` sets the gap. The dropdown renders in a portal, so it is not clipped by a narrow widget. ### Example: Branch picker ```tsx {branch}} > {branchList} ``` ### API reference | Prop | Type | Required | Description | | --- | --- | --- | --- | | trigger | `React.ReactNode` | Yes | Element that toggles the popover | | children | `React.ReactNode` | Yes | Dropdown content | | open | `boolean` | No | Controlled opened state | | defaultOpen | `boolean` | No | Initial opened state in uncontrolled mode | | onOpenChange | `(open: boolean) => void` | No | | | side | `PopoverSide` | No | Side of the trigger the dropdown is placed on, `'top'` by default | | align | `PopoverAlign` | No | Alignment along the chosen side, `'start'` by default | | sideOffset | `number` | No | Gap between trigger and dropdown in px, `6` by default | | className | `string` | No | Class name applied to the dropdown | | style | `React.CSSProperties` | No | | ## QuestionPrompt URL: https://sinups.github.io/ai-kit/docs/question-prompt ### Code ```tsx import { QuestionPrompt, type QuestionConfig } from "@sinups/ai-kit"; const questions: QuestionConfig[] = [ { kind: "single", title: "Which retry strategy should the upload client use?", options: [ { id: "exponential", label: "Exponential backoff", preview: { kind: "code", language: "ts", content: "delay: (attempt) => 500 * 2 ** attempt" }, }, { id: "fixed", label: "Fixed delay" }, ], allowCustom: true, allowNotes: true, }, ]; export function Example({ answer }: { answer: (value: unknown) => void }) { return ; } ``` ### Usage The form for one question the agent asks the user. `kind` is `single` (lettered options, one choice), `multi` (several choices, bounded by `minSelections` and `maxSelections`) or `text` (a free-text area). `allowCustom` adds a typed answer as the last option and `allowNotes` adds an optional notes field. An option can carry a `preview` (Markdown or code): from 640px of prompt width it is shown beside the options, below that under the chosen or hovered option. The primary button reads Next while more questions follow and Send on the last one; it stays disabled until the answer is valid. Skip calls `onSkip`, or `onSubmit({ kind: "skip" })` when `onSkip` is not set. `initialAnswer` is read only on mount, so remount with `key` when the question changes. QuestionTool and InputBar `questionBar` use this component and handle multi-step navigation for you; render it directly only for a custom flow. Related helpers: `getInitialQuestionDraft`, `canSubmitQuestion`, `buildQuestionAnswer`, `formatQuestionAnswer`. ### Example: Wide, with previews ```tsx ``` ### Example: Narrow ```tsx
``` ### Example: Free text ```tsx ``` ### API reference | Prop | Type | Required | Description | | --- | --- | --- | --- | | questions | `QuestionConfig[]` | Yes | | | questionIndex | `number` | No | 1-based index of the active question, `1` by default | | totalQuestions | `number` | No | | | onPreviousQuestion | `() => void` | No | | | onNextQuestion | `() => void` | No | | | initialAnswer | `QuestionAnswer` | No | Answer used to pre-fill the form when revisiting a question. Read only on mount: hosts remount the prompt (via `key`) when the active question changes. | | submitLabel | `string` | No | Label for the primary action on the LAST question, `'Send'` by default | | nextLabel | `string` | No | Label for the primary action when there are more questions ahead, `'Next'` by default. The host (for example QuestionTool) is expected to advance to the next question after `onSubmit` fires. | | skipLabel | `string` | No | `'Skip'` by default | | allowSkip | `boolean` | No | Whether the skip button is shown, `true` by default | | onSubmit | `(answer: QuestionAnswer) => void` | Yes | | | onSkip | `() => void` | No | Called when the skip button is pressed. When provided, `onSubmit` is NOT called for the skip. When omitted, skipping is reported as `onSubmit({ kind: 'skip' })`. | | className | `string` | No | Class name added to the root element | | style | `React.CSSProperties` | No | Inline styles added to the root element | --- # Primitives ## Wizard URL: https://sinups.github.io/ai-kit/docs/wizard ### Code ```tsx "use client"; import { Wizard, type WizardStep } from "@sinups/ai-kit"; import { TextInput } from "@mantine/core"; type Values = { name: string; url: string }; const steps: WizardStep[] = [ { id: "name", label: "Name", validate: (values) => (values.name ? null : { name: "Enter a name" }), render: ({ values, setValue, errors }) => ( setValue("name", event.currentTarget.value)} /> ), }, { id: "url", label: "URL", validate: async (values) => ((await ping(values.url)) ? null : { url: "Server is unreachable" }), render: ({ values, setValue, errors }) => ( setValue("url", event.currentTarget.value)} /> ), }, ]; export function Example() { return (
{JSON.stringify(values, null, 2)}
} onComplete={(values) => saveServer(values)} onCancel={() => {}} /> ); } ``` ### Usage Use Wizard for multi-step flows: adding a server, creating a rule, configuring an agent. Every step validates before moving on (sync or async), `when` hides conditional steps, `review` appends a summary step, and a rejected `onComplete` is shown in an alert. The stepper is vertical from 640px of wizard width and collapses into a compact header below that. `WizardModal` wraps the same flow in a modal that goes full screen on phones. Pass `nonLinear` when editing existing values: any step can be opened and Finish validates all steps, jumping to the first invalid one. `useWizard` exposes the state machine without the UI. ### Example: Wide ```tsx ``` ### Example: Narrow ```tsx
``` ### Example: In a modal ```tsx ``` ### Example: Editing (non-linear) ```tsx ``` ### API reference | Prop | Type | Required | Description | | --- | --- | --- | --- | | steps | `WizardStep[]` | Yes | Steps in order, hidden ones are filtered with `step.when` | | initialValues | `V` | Yes | Initial values when `values` is not controlled | | values | `V` | No | Controlled values | | onValuesChange | `(values: V) => void` | No | Called with the next values on every change | | onComplete | `(values: V) => void \| Promise` | Yes | Called after the last step passes validation, a rejected promise is shown in an alert | | onCancel | `() => void` | No | Called by the cancel button, the button is rendered only when set | | review | `(values: V) => React.ReactNode` | No | Renders a final `Review` step with a summary of the values | | labels | `Partial` | No | Overrides of the default English button and step labels | | busy | `boolean` | No | Blocks navigation, cancel and the step content while the host is busy, for example saving a draft | | nonLinear | `boolean` | No | Allows jumping to any step and finishing from any step, every visible step is validated on finish | | scrollContent | `boolean` | No | Keeps the stepper and the action buttons in place and scrolls only the step content; the parent must limit the height, as `WizardModal` does | | orientation | `'auto' \| 'horizontal' \| 'vertical'` | No | `auto` shows a vertical stepper when the wizard is at least 640px wide and a compact header otherwise, `auto` by default | | className | `string` | No | Class name added to the root element | | style | `React.CSSProperties` | No | Inline styles added to the root element | ## ConfirmDialog URL: https://sinups.github.io/ai-kit/docs/confirm-dialog ### Code ```tsx "use client"; import { useState } from "react"; import { Button, Code } from "@mantine/core"; import { ConfirmDialog } from "@sinups/ai-kit"; export function Example() { const [opened, setOpened] = useState(false); return ( <> setOpened(false)} title="Delete rule" message={<>Bash(npm run test:*) will be removed from the project settings.} labels={{ confirm: "Delete" }} danger onConfirm={() => deleteRule("allow-test")} /> ); } ``` ### Usage Ask for confirmation before a destructive or irreversible action. `onConfirm` may return a promise: the confirm button shows a loader until it settles, the dialog closes on success and shows the rejection message in an alert on failure, so the user can retry or cancel. Name the affected object in `message`. ### Example: Destructive action ```tsx ``` ### Example: Rejected action ```tsx { throw new Error(".agent/settings.json is read-only"); }} /> ``` ### API reference | Prop | Type | Required | Description | | --- | --- | --- | --- | | opened | `boolean` | Yes | Whether the dialog is open | | title | `React.ReactNode` | Yes | Dialog title, for example `Delete rule` | | message | `React.ReactNode` | No | Explanation that names what is affected | | danger | `boolean` | No | Colors the confirm button red for destructive actions | | onConfirm | `() => void \| Promise` | Yes | Called by the confirm button; the dialog closes when it settles and shows the rejection in an alert | | onClose | `() => void` | Yes | Called when the dialog is dismissed or the confirmed action succeeded | | labels | `Partial` | No | Overrides of the default English labels | ## SettingsLayout URL: https://sinups.github.io/ai-kit/docs/settings-layout ### Code ```tsx "use client"; import { useState } from "react"; import { Switch } from "@mantine/core"; import { SettingRow, SettingsLayout, SettingsSection, type SettingsNavItem } from "@sinups/ai-kit"; const sections: SettingsNavItem[] = [ { id: "general", label: "General" }, { id: "models", label: "Models", description: "Default model and limits", group: "Agent" }, { id: "mcp", label: "MCP servers", group: "Integrations" }, ]; export function Example() { const [activeId, setActiveId] = useState("general"); return (
} />
); } ``` ### Usage Build a settings screen from three pieces. `SettingsLayout` shows grouped `NavLink` navigation beside the content from 720px of its own width and a section picker above the content when narrower; `withSearch` filters sections by label and description. `SettingsSection` is a borderless group with a title, description, actions and dividers between rows, set apart by spacing; `danger` marks destructive settings. `SettingRow` places the control beside the label from 480px of row width and under it when narrower (`layout` forces either). The layout fills the parent height and scrolls navigation and content separately. ### Example: Wide ```tsx {sectionContent} ``` ### Example: Narrow ```tsx
{sectionContent}
``` ### Example: Row layouts ```tsx import { Switch, TextInput } from "@mantine/core"; import { SettingRow, SettingsSection } from "@sinups/ai-kit"; export function Example() { return ( } /> } /> ); } ``` ### API reference | Prop | Type | Required | Description | | --- | --- | --- | --- | | sections | `SettingsNavItem[]` | Yes | Sections listed in the navigation, in display order | | activeId | `string` | Yes | Id of the section whose content is rendered in `children` | | onActiveIdChange | `(id: string) => void` | Yes | Called with the id of the section the user picked | | children | `React.ReactNode` | No | Content of the active section, usually one or more `SettingsSection` | | title | `React.ReactNode` | No | Heading above the navigation | | withSearch | `boolean` | No | Adds a search field that filters the navigation by label and description, `false` by default | | navWidth | `number` | No | Navigation column width in px when wide, `240` by default | | fillContent | `boolean` | No | Renders the content without its own scroll area and padding, stretched to the full height, for panels that scroll themselves such as master-detail lists; `SettingsNavItem.fill` overrides it per section | | breakpoint | `number` | No | Component width in px from which navigation and content sit side by side, `720` by default | | labels | `Partial` | No | Overrides of the default English labels | | className | `string` | No | Class name added to the root element | | style | `React.CSSProperties` | No | Inline styles added to the root element | ## SettingsModal URL: https://sinups.github.io/ai-kit/docs/settings-modal ### Code ```tsx "use client"; import { useState } from "react"; import { Button, Switch } from "@mantine/core"; import { SettingRow, SettingsModal, SettingsSection, type SettingsNavItem } from "@sinups/ai-kit"; const sections: SettingsNavItem[] = [ { id: "general", label: "General" }, { id: "models", label: "Models", description: "Default model and limits", group: "Agent" }, { id: "mcp", label: "MCP servers", group: "Integrations", fill: true }, ]; export function Example() { const [opened, setOpened] = useState(false); const [activeId, setActiveId] = useState("general"); return ( <> setOpened(false)} sections={sections} activeId={activeId} onActiveIdChange={setActiveId} withSearch > } /> ); } ``` ### Usage Use SettingsModal when settings open over the current screen instead of on their own route. It renders SettingsLayout inside a Mantine Modal with a fixed height, so switching sections does not resize the dialog. It accepts every SettingsLayout prop except `title`, which becomes the modal title (`Settings` by default). `size` sets the width (`70rem` by default); below `fullScreenQuery` (`(max-width: 48em)` by default) the modal goes full screen and the layout switches to its narrow form with a section picker. Sections with `fill: true` get the full height without the layout scroll area, which suits panels that scroll themselves, such as McpSettingsPanel or MemoryPanel. ### Example: Modal ```tsx } /> } /> ``` ### API reference | Prop | Type | Required | Description | | --- | --- | --- | --- | | opened | `boolean` | Yes | Whether the modal is open | | onClose | `() => void` | Yes | Called when the modal is dismissed | | title | `React.ReactNode` | No | Modal title, `Settings` by default | | size | `ModalProps['size']` | No | Modal width when not full screen, `70rem` by default | | fullScreenQuery | `string` | No | Media query that switches the modal to full screen, `(max-width: 48em)` by default | | sections | `SettingsNavItem[]` | Yes | Sections listed in the navigation, in display order | | activeId | `string` | Yes | Id of the section whose content is rendered in `children` | | onActiveIdChange | `(id: string) => void` | Yes | Called with the id of the section the user picked | | children | `React.ReactNode` | No | Content of the active section, usually one or more `SettingsSection` | | withSearch | `boolean` | No | Adds a search field that filters the navigation by label and description, `false` by default | | navWidth | `number` | No | Navigation column width in px when wide, `240` by default | | fillContent | `boolean` | No | Renders the content without its own scroll area and padding, stretched to the full height, for panels that scroll themselves such as master-detail lists; `SettingsNavItem.fill` overrides it per section | | breakpoint | `number` | No | Component width in px from which navigation and content sit side by side, `720` by default | | labels | `Partial` | No | Overrides of the default English labels | | className | `string` | No | Class name added to the root element | | style | `React.CSSProperties` | No | Inline styles added to the root element | ## MasterDetail URL: https://sinups.github.io/ai-kit/docs/master-detail ### Code ```tsx "use client"; import { useState } from "react"; import { MasterDetail, McpServerList, McpServerDetail, type McpServer } from "@sinups/ai-kit"; export function Example({ servers }: { servers: McpServer[] }) { const [selectedId, setSelectedId] = useState(null); const selected = servers.find((server) => server.id === selectedId); return (
setSelectedId(server.id)} />} detail={selected ? : null} onBack={() => setSelectedId(null)} resizable />
); } ``` ### Usage Show a list and the details of the selected item. From `breakpoint` (720px of the component width) both panes sit side by side, with `resizable` the border can be dragged; below it the detail replaces the list and `onBack` renders a back button. When nothing is selected the wide layout shows `emptyDetail`. The component fills its parent height and each pane scrolls on its own. ### Example: Wide ```tsx ``` ### Example: Narrow ```tsx
``` ### Example: Resizable ```tsx ``` ### API reference | Prop | Type | Required | Description | | --- | --- | --- | --- | | list | `React.ReactNode` | Yes | List pane content | | detail | `React.ReactNode \| null` | Yes | Detail of the selected item, `null` when nothing is selected | | detailOpened | `boolean` | No | When narrow, shows the detail instead of the list, `true` whenever `detail` is set by default | | onBack | `() => void` | No | Called by the back button shown above the detail when narrow, the button is rendered only when set | | listWidth | `number` | No | List pane width in px when wide, `320` by default | | breakpoint | `number` | No | Component width in px from which list and detail sit side by side, `720` by default | | resizable | `boolean` | No | Lets the user drag the border between the panes when wide | | emptyDetail | `React.ReactNode` | No | Shown in the detail pane when wide and `detail` is `null`, an empty state with `labels.emptyTitle` and `labels.emptyDescription` by default | | labels | `Partial` | No | Overrides of the default English labels | | className | `string` | No | Class name added to the root element | | style | `React.CSSProperties` | No | Inline styles added to the root element | ## EntityList URL: https://sinups.github.io/ai-kit/docs/entity-list ### Code ```tsx "use client"; import { useState } from "react"; import { Button } from "@mantine/core"; import { EntityList, EntityListItem } from "@sinups/ai-kit"; type Server = { id: string; name: string; description: string; tools: number }; export function Example({ servers, loading, error }: { servers: Server[]; loading: boolean; error?: string }) { const [query, setQuery] = useState(""); const [selectedId, setSelectedId] = useState(null); return ( items={servers} getId={(server) => server.id} loading={loading} error={error} selectedId={selectedId} onSelect={(server) => setSelectedId(server.id)} search={{ value: query, onChange: setQuery, filter: (server, q) => server.name.includes(q) }} empty={{ title: "No servers", action: }} renderItem={(server, { selected }) => ( removeServer(server.id) }]} /> )} /> ); } ``` ### Usage Render any collection of entities (servers, agents, skills, sessions) with the four data states: `loading` skeleton rows, `error` alert with retry, `empty` state with a call to action, and data. Add `search`, a `filters` control (segmented when wide and at most four options, a select otherwise), `groupBy` headers and a `toolbar`. The list is a keyboard-navigable listbox: arrows, Home/End and Enter. `EntityListItem` is the matching row with icon, status, badges, meta and an actions menu that does not select the row. Badges never truncate: the title does, and in rows narrower than 320px the badges move under the title. ### Example: Search and actions ```tsx items={servers} getId={(server) => server.id} isItemDisabled={(server) => !!server.disabled} selectedId={selectedId} onSelect={(server) => setSelectedId(server.id)} search={{ value: query, onChange: setQuery, placeholder: "Search servers", filter: byName }} toolbar={addButton} renderItem={renderServer} /> ``` ### Example: Filters and groups ```tsx items={servers} getId={(server) => server.id} groupBy={(server) => server.scope} filters={{ value: transport, onChange: setTransport, filter: (server, value) => value === "all" || server.transport === value, options: [ { value: "all", label: "All", count: 4 }, { value: "stdio", label: "stdio", count: 2 }, { value: "http", label: "HTTP", count: 2 }, ], }} renderItem={renderServer} /> ``` ### Example: Loading, empty and error ```tsx <> ``` ### API reference | Prop | Type | Required | Description | | --- | --- | --- | --- | | items | `T[]` | Yes | Items to render | | getId | `(item: T) => string` | Yes | Returns a stable unique id of an item | | renderItem | `(item: T, state: EntityListItemState) => React.ReactNode` | Yes | Renders an item, usually with `EntityListItem` | | selectedId | `string \| null` | No | Id of the selected item | | onSelect | `(item: T) => void` | No | Called when an item is clicked or chosen with Enter | | isItemDisabled | `(item: T) => boolean` | No | Returns whether an item is disabled, disabled items are skipped by the keyboard and cannot be selected | | loading | `boolean` | No | Shows skeleton rows instead of items | | skeletonCount | `number` | No | Number of skeleton rows, `4` by default | | error | `React.ReactNode` | No | Error message, shown instead of items | | onRetry | `() => void` | No | Called by the retry button of the error alert, the button is rendered only when set | | empty | `EntityListEmpty` | No | Empty state shown when there are no items and no search query | | search | `EntityListSearch` | No | Search input above the list | | filters | `EntityListFilters` | No | Filter control above the list | | groupBy | `(item: T) => string` | No | Returns the group of an item, groups get headers | | groupOrder | `string[]` | No | Group keys rendered first in this order, other groups follow in order of appearance | | toolbar | `React.ReactNode` | No | Content rendered to the right of the search input, for example an add button | | ariaLabel | `string` | No | Accessible label of the list | | labels | `Partial` | No | Overrides of the default English labels | | className | `string` | No | Class name added to the root element | | style | `React.CSSProperties` | No | Inline styles added to the root element | ## CommandPalette URL: https://sinups.github.io/ai-kit/docs/command-palette ### Code ```tsx "use client"; import { useState } from "react"; import { CommandPalette, type PaletteCommand } from "@sinups/ai-kit"; const commands: PaletteCommand[] = [ { id: "new-chat", label: "New chat", group: "Chat", shortcut: "mod+N", onSelect: startChat }, { id: "mcp", label: "Manage MCP servers", group: "Settings", keywords: ["tools"], onSelect: openMcp }, ]; export function Example() { const [opened, setOpened] = useState(false); return ( setOpened(true)} onClose={() => setOpened(false)} hotkey="mod+K" commands={commands} recentIds={["mcp"]} /> ); } ``` ### Usage Give power users one place to run commands. The palette fuzzy-matches label, keywords and description, highlights matched characters, groups commands, shows `recentIds` first while the query is empty, and renders shortcuts with `ShortcutHint`. `hotkey` together with `onOpen` registers the global shortcut. Each command runs its own `onSelect`, and the palette's `onSelect` sees every choice, for example to update the recent list. `useFuzzySearch` is available for custom lists. ### Example: Basic ```tsx ``` ### Example: Recent commands ```tsx setRecentIds((ids) => [command.id, ...ids.filter((id) => id !== command.id)].slice(0, 3))} /> ``` ### API reference | Prop | Type | Required | Description | | --- | --- | --- | --- | | opened | `boolean` | Yes | Whether the palette is open | | onClose | `() => void` | Yes | Called when the palette should close: Escape, outside click or after running a command | | commands | `PaletteCommand[]` | Yes | Commands to search and run | | onSelect | `(command: PaletteCommand) => void` | No | Called with the command that runs, after its own `onSelect` | | placeholder | `string` | No | Search input placeholder, `Search commands...` by default | | recentIds | `string[]` | No | Ids of recently used commands, listed first while the query is empty | | hotkey | `string` | No | Hotkey that toggles the palette, for example `mod+K`; registered whenever set: it calls `onOpen` while closed and `onClose` while open, so without `onOpen` it can only close the palette | | onOpen | `() => void` | No | Called when the hotkey is pressed while the palette is closed; set `opened` to `true` here | | maxHeight | `number \| string` | No | Maximum height of the results list, `400` by default | | labels | `Partial` | No | Overrides of the default English labels | ## StatusBadge URL: https://sinups.github.io/ai-kit/docs/status-badge ### Code ```tsx import { StatusBadge } from "@sinups/ai-kit"; export function Example() { return ( <> ); } ``` ### Usage Show the state of a server, agent, task or tool with one shared vocabulary: `idle`, `pending`, `running`, `success`, `warning`, `error`, `disabled` and `needs-auth`. Each status has a color, an icon and a default label; `pending` and `running` show a loader. Map your domain status to it (for example `getMcpAgentUiStatus`) and override the text with `label`. `variant="dot"` fits dense rows. ### Example: All statuses ```tsx import { AGENT_UI_STATUSES, StatusBadge } from "@sinups/ai-kit"; export function Example() { return ( <> {AGENT_UI_STATUSES.map((status) => )} {AGENT_UI_STATUSES.map((status) => )} ); } ``` ### Example: In rows ```tsx ``` ### API reference | Prop | Type | Required | Description | | --- | --- | --- | --- | | status | `AgentUiStatus` | Yes | Status from the shared vocabulary | | label | `string` | No | Overrides the default English label of the status | | variant | `'badge' \| 'dot'` | No | `badge` renders a Mantine `Badge`, `dot` renders a colored dot with text, `badge` by default | | size | `MantineSize` | No | Badge and text size, `sm` by default | | withIcon | `boolean` | No | Shows the status icon, or a loader for `pending` and `running`, `true` by default | | className | `string` | No | Class name added to the root element | | style | `React.CSSProperties` | No | Inline styles added to the root element | ## ShortcutHint URL: https://sinups.github.io/ai-kit/docs/shortcut-hint ### Code ```tsx import { ShortcutHint } from "@sinups/ai-kit"; export function Example() { return ; } ``` ### Usage Render a keyboard shortcut with Mantine `Kbd`. `mod` resolves to ⌘ on macOS and Ctrl elsewhere; the platform is detected in the browser and can be forced with `platform`. Accepts `mod+shift+P` strings or key arrays, and an optional label. ### Example: Platforms and sizes ```tsx <> ``` ### Example: Shortcut list ```tsx ``` ### API reference | Prop | Type | Required | Description | | --- | --- | --- | --- | | keys | `string \| string[]` | Yes | Shortcut such as `mod+K`, or keys of one combination such as `['shift', 'enter']`; `mod` is ⌘ on macOS and Ctrl elsewhere | | label | `React.ReactNode` | No | Text shown before the keys | | platform | `ShortcutPlatform` | No | Platform used to render modifier keys, detected from `navigator` by default | | size | `MantineSize` | No | Size of the keys and the label, `xs` by default | | className | `string` | No | Class name added to the root element | | style | `React.CSSProperties` | No | Inline styles added to the root element | ## KeyValueEditor URL: https://sinups.github.io/ai-kit/docs/key-value-editor ### Code ```tsx "use client"; import { useState } from "react"; import { KeyValueEditor, headerKeyValidator, type KeyValuePair } from "@sinups/ai-kit"; export function Example() { const [env, setEnv] = useState([ { id: "1", key: "DATABASE_URL", value: "postgres://localhost:5432/app" }, { id: "2", key: "API_KEY", value: "sk-live-123", secret: true }, ]); const [headers, setHeaders] = useState([]); return ( <> ); } ``` ### Usage Edit environment variables or HTTP headers. Keys are validated as you type (empty, duplicate and `validateKey`, `envKeyValidator` by default), secret values use a password field, and pasting `.env` text or `Header: value` lines into a key field expands into rows. Key and value stack when the editor is narrower than 440px. `parseKeyValueText` is exported for imports from files. ### Example: Environment variables ```tsx ``` ### Example: Headers ```tsx ``` ### API reference | Prop | Type | Required | Description | | --- | --- | --- | --- | | value | `KeyValuePair[]` | Yes | Rows in display order | | onChange | `(pairs: KeyValuePair[]) => void` | Yes | Called with the full list of rows after every edit | | keyPlaceholder | `string` | No | Key input placeholder and accessible label prefix, `Key` by default | | valuePlaceholder | `string` | No | Value input placeholder and accessible label prefix, `Value` by default | | addLabel | `string` | No | Add button label, `Add` by default | | allowSecrets | `boolean` | No | Shows a button on every row that masks or unmasks its value | | validateKey | `KeyValidator` | No | Returns an error message for an invalid key; empty and duplicate keys are always reported, `envKeyValidator` by default | | disabled | `boolean` | No | Disables every input and button | | maxRows | `number` | No | Maximum number of rows, the add button is disabled once reached | | labels | `Partial` | No | Accessible labels and built-in error messages | | className | `string` | No | Class name added to the root element | | style | `React.CSSProperties` | No | Inline styles added to the root element | ## SchemaView URL: https://sinups.github.io/ai-kit/docs/schema-view ### Code ```tsx import { SchemaView, type JsonSchema } from "@sinups/ai-kit"; const inputSchema: JsonSchema = { type: "object", required: ["repo", "title"], properties: { repo: { type: "string", description: "Repository in owner/name form" }, title: { type: "string" }, labels: { type: "array", items: { type: "string" }, default: [] }, assignee: { type: "object", properties: { login: { type: "string" } } }, }, }; export function Example() { return ; } ``` ### Usage Show a JSON Schema, such as an MCP tool `inputSchema`, as a parameter reference. From 560px of width it is a table with name, type, description and default; narrower it becomes stacked rows. Required parameters get a badge, enums and `const` unions list allowed values, ranges and formats are shown, and nested objects, arrays of objects and `oneOf`/`anyOf` variants collapse (`defaultExpandedDepth`). `flattenSchema` returns the rows for custom renderers. ### Example: Wide ```tsx ``` ### Example: Narrow ```tsx
``` ### API reference | Prop | Type | Required | Description | | --- | --- | --- | --- | | schema | `JsonSchema` | Yes | JSON Schema of an object, for example a tool `inputSchema` | | defaultExpandedDepth | `number` | No | Nesting levels expanded initially, `1` by default (top-level objects open, deeper ones closed) | | breakpoint | `number` | No | Component width in px from which parameters are shown as a table, `560` by default | | labels | `Partial` | No | Overrides of the default English labels | | className | `string` | No | Class name added to the root element | | style | `React.CSSProperties` | No | Inline styles added to the root element | ## ValidationErrorsList URL: https://sinups.github.io/ai-kit/docs/validation-errors-list ### Code ```tsx import { ValidationErrorsList, type SettingsValidationError } from "@sinups/ai-kit"; const errors: SettingsValidationError[] = [ { file: ".agent/settings.json", path: "permissions.allow[2]", message: 'Unknown tool "Shell(npm test)"', suggestion: 'Did you mean "Bash(npm test)"?', docsUrl: "https://example.com/docs/permissions", }, { file: ".agent/settings.json", path: "model", message: "Expected a string, received a number" }, { file: ".agent/settings.local.json", path: "env.EDITOR", message: "Overrides the value from the project settings", severity: "warning", }, ]; export function Example({ openFile }: { openFile: (file: string) => void }) { return ; } ``` ### Usage Show the problems found while validating settings files. Errors are grouped by `file`; each group has a header with the file name, a problem count (red when the group has errors, yellow when it has only warnings) and an Open file button when `onOpenFile` is set. Each row shows the field `path`, the `message`, an optional `suggestion` and a Docs link for `docsUrl`. Identical errors (same file, path and message) are shown once. `maxItems` limits rows per file and adds a Show more button. Set `withFileHeaders={false}` when the file is already named nearby, as InvalidSettingsNotice does. With no errors nothing renders, or `emptyLabel` if you pass one. File names are truncated from the start, so the file name stays visible in a narrow column. The grouping logic is exported as pure functions: `groupValidationErrors`, `dedupeValidationErrors`, `getValidationSeverity`, `getValidationErrorKey` and `fillValidationTemplate`. ### Example: Wide ```tsx ``` ### Example: Narrow ```tsx
``` ### API reference | Prop | Type | Required | Description | | --- | --- | --- | --- | | errors | `SettingsValidationError[]` | Yes | Errors of one or more settings files; identical errors are shown once | | onOpenFile | `(file: string) => void` | No | Adds an open button to each file header | | maxItems | `number` | No | Rows shown per file before a "Show more" button, all rows by default | | withFileHeaders | `boolean` | No | Renders file headers, `true` by default; hide them when the file is already named nearby | | emptyLabel | `React.ReactNode` | No | Shown when there are no errors, nothing is rendered by default | | labels | `Partial` | No | Overrides of the default English labels | | className | `string` | No | Class name added to the root element | | style | `React.CSSProperties` | No | Inline styles added to the root element | ## InvalidSettingsNotice URL: https://sinups.github.io/ai-kit/docs/invalid-settings-notice ### Code ```tsx import { InvalidSettingsNotice, type SettingsValidationError } from "@sinups/ai-kit"; export function Example({ errors, openFile, skipFile, dismiss, }: { errors: SettingsValidationError[]; openFile: (file: string) => void; skipFile: () => Promise; dismiss: () => void; }) { return ( ); } ``` ### Usage Tell the user that a settings file failed validation and is ignored, and offer a way out. Place it above the chat or at the top of a settings screen. The description is built from the problem count: pass `errors` to show the count and an expandable ValidationErrorsList (Show details), or only `count` when the details are not available. The color follows the errors: red when at least one is an error, yellow when all are warnings; `severity` overrides it. Actions appear only for the callbacks you pass: Open file (`onOpenFile`), Continue without this file (`onContinueWithout`, which may return a promise: the button shows a loader and a rejection message is shown inside the alert) and a close button (`onDismiss`). `defaultExpanded` opens the details initially; `title`, `description` and `labels` (with `labels.list` for the error list) replace the English texts. ### Example: Errors with actions ```tsx ``` ### Example: Warnings, count only, failing action ```tsx <> Promise.reject(new Error("The file is locked by another process"))} /> ``` ### API reference | Prop | Type | Required | Description | | --- | --- | --- | --- | | file | `string` | Yes | Settings file that failed validation | | errors | `SettingsValidationError[]` | No | Problems found in the file; their count is shown and they can be expanded | | count | `number` | No | Number of problems when `errors` is not passed | | severity | `SettingsValidationSeverity` | No | `error` renders a red alert, `warning` a yellow one; derived from `errors` by default | | title | `React.ReactNode` | No | Overrides the title | | description | `React.ReactNode` | No | Overrides the description | | onContinueWithout | `() => void \| Promise` | No | Adds "Continue without this file"; the button shows a loader until the promise settles | | onOpenFile | `(file: string) => void` | No | Adds "Open file" | | onDismiss | `() => void` | No | Renders a close button | | defaultExpanded | `boolean` | No | Shows the error list expanded initially | | labels | `Partial` | No | Overrides of the default English labels | | className | `string` | No | Class name added to the root element | | style | `React.CSSProperties` | No | Inline styles added to the root element | --- # Agents & skills ## AgentsSettingsPanel URL: https://sinups.github.io/ai-kit/docs/agents-settings-panel ### Code ```tsx "use client"; import { AgentsSettingsPanel, type AgentDefinition, type ToolCatalogItem } from "@sinups/ai-kit"; export function AgentsSettings({ agents, catalog }: { agents: AgentDefinition[]; catalog: ToolCatalogItem[] }) { return (
api.createAgent(draft)} onUpdate={(agent, draft) => api.updateAgent(agent.id, draft)} onDelete={(agent) => api.deleteAgent(agent.id)} onGenerate={(task) => api.generateAgentDraft(task)} onUseInChat={(agent) => startChat(agent.name)} />
); } ``` ### Usage Drop in a complete subagent settings screen built on `MasterDetail`: `AgentList` beside `AgentDetail` from 720px of width, one pane with back navigation below. New agents are created in `AgentCreateWizard` (with Generate with AI when `onGenerate` is set); Edit and Duplicate open `AgentEditor` in the detail pane, leaving it with unsaved changes asks for confirmation; Delete is confirmed in a dialog. Resolve `onCreate` with the created agent to select it. The panel fills its parent height. ### Example: Full page ```tsx ``` ### Example: Narrow widget ```tsx
``` ### API reference | Prop | Type | Required | Description | | --- | --- | --- | --- | | agents | `AgentDefinition[]` | Yes | Agents to manage | | catalog | `ToolCatalogItem[]` | Yes | Tools that can be picked | | models | `ModelOption[]` | No | Models offered in addition to `inherit` | | skills | `string[]` | No | Skills offered in the skills picker | | colors | `MantineColor[]` | No | Colors offered in the color picker | | loading | `boolean` | No | Shows skeleton rows in the list | | error | `React.ReactNode` | No | Error shown instead of the list | | onRetry | `() => void` | No | Called by the retry button of the error alert | | onCreate | `(draft: AgentDraft) => Promise \| AgentDefinition \| void` | Yes | Creates an agent from the wizard or a duplicate; resolve with the agent to select it | | onUpdate | `(agent: AgentDefinition, draft: AgentDraft) => Promise \| void` | Yes | Saves changes of an existing agent | | onDelete | `(agent: AgentDefinition) => Promise \| void` | Yes | Deletes an agent after confirmation | | onGenerate | `(task: string) => Promise>` | No | Generates a draft from a task description in the wizard | | onUseInChat | `(agent: AgentDefinition) => void` | No | Called by the Use in chat button of the detail | | locale | `string` | No | BCP 47 locale for dates, `en` by default | | selectedId | `string \| null` | No | Id of the agent shown in the detail, uncontrolled when omitted | | defaultSelectedId | `string \| null` | No | Initially selected agent id when uncontrolled | | onSelectedIdChange | `(id: string \| null) => void` | No | Called when the selected agent changes | | breakpoint | `number` | No | Component width in px from which list and detail sit side by side, `720` by default | | labels | `Partial` | No | Overrides of the default English labels of the panel and its parts | | className | `string` | No | Class name added to the root element | | style | `React.CSSProperties` | No | Inline styles added to the root element | ## AgentList URL: https://sinups.github.io/ai-kit/docs/agent-list ### Code ```tsx import { AgentList } from "@sinups/ai-kit"; export function Example() { return ( setSelectedId(agent.id)} onCreate={openWizard} onEdit={editAgent} onDuplicate={duplicateAgent} onDelete={deleteAgent} /> ); } ``` ### Usage List subagent definitions with avatar, description and model, grouped by source (built-in, user, project, plugin) unless `groupBySource` is false. The list is searchable and filters by source when agents come from more than one (`withSearch` and `withSourceFilter` turn these off), offers a New agent button when `onCreate` is set and an actions menu with Edit, Duplicate and Delete; Edit and Delete are disabled for read-only agents. Handles loading, error and empty states. ### Example: Agents ```tsx ``` ### Example: Loading, error and empty ```tsx <> ``` ### API reference | Prop | Type | Required | Description | | --- | --- | --- | --- | | agents | `AgentDefinition[]` | Yes | Agents to list | | selectedId | `string \| null` | No | Id of the selected agent | | onSelect | `(agent: AgentDefinition) => void` | No | Called when an agent row is chosen | | onCreate | `() => void` | No | Called by the New agent button, the button is rendered only when set | | onEdit | `(agent: AgentDefinition) => void` | No | Called by the Edit action, disabled for read-only agents | | onDuplicate | `(agent: AgentDefinition) => void` | No | Called by the Duplicate action | | onDelete | `(agent: AgentDefinition) => void` | No | Called by the Delete action, disabled for read-only agents | | models | `ModelOption[]` | No | Models used to show a readable model name | | groupBySource | `boolean` | No | Groups agents under source headers, `true` by default | | withSearch | `boolean` | No | Shows the search input, `true` by default | | withSourceFilter | `boolean` | No | Shows the source filter when agents come from more than one source, `true` by default | | loading | `boolean` | No | Shows skeleton rows | | error | `React.ReactNode` | No | Error message shown instead of the list | | onRetry | `() => void` | No | Called by the retry button of the error alert | | labels | `Partial` | No | Overrides of the default English labels | | className | `string` | No | Class name added to the root element | | style | `React.CSSProperties` | No | Inline styles added to the root element | ## AgentDetail URL: https://sinups.github.io/ai-kit/docs/agent-detail ### Code ```tsx import { AgentDetail } from "@sinups/ai-kit"; export function Example() { return ( ); } ``` ### Usage Show one subagent: avatar, name, source and when to use it, the configuration (model, max turns, tools with a summary that expands into the list, disallowed tools, skills, last update) and the system prompt rendered as Markdown. Use in chat renders only with `onUseInChat`; Edit and Delete are hidden for read-only agents, which can still be duplicated. ### Example: Project agent ```tsx ``` ### Example: Read-only agent ```tsx ``` ### API reference | Prop | Type | Required | Description | | --- | --- | --- | --- | | agent | `AgentDefinition` | Yes | Agent to show | | catalog | `ToolCatalogItem[]` | No | Tool catalog used to summarize and list tools | | models | `ModelOption[]` | No | Models used to show a readable model name | | onUseInChat | `(agent: AgentDefinition) => void` | No | Called by the Use in chat button, the button is rendered only when set | | onEdit | `(agent: AgentDefinition) => void` | No | Called by the Edit button, hidden for read-only agents | | onDuplicate | `(agent: AgentDefinition) => void` | No | Called by the Duplicate action | | onDelete | `(agent: AgentDefinition) => void` | No | Called by the Delete action, hidden for read-only agents | | locale | `string` | No | BCP 47 locale for dates, `en` by default so server and client render the same text | | formatDate | `(iso: string) => string` | No | Formats `updatedAt`, a medium date in `locale` by default | | labels | `Partial` | No | Overrides of the default English labels | | className | `string` | No | Class name added to the root element | | style | `React.CSSProperties` | No | Inline styles added to the root element | ## AgentEditor URL: https://sinups.github.io/ai-kit/docs/agent-editor ### Code ```tsx import { AgentEditor } from "@sinups/ai-kit"; export function Example() { return ( api.updateAgent(agent.id, draft)} onCancel={closeEditor} onDirtyChange={setDirty} /> ); } ``` ### Usage Edit or create a subagent in one form: identity (display name, name, when to use), instructions (system prompt), tools and disallowed tools with `ToolSelector`, and model and appearance (model, max turns, skills, color). Fields are validated, names must be unique among `existingNames`, and a rejected `onSave` is shown in an alert. Cancel with unsaved changes asks for confirmation; `onDirtyChange` lets the host guard its own navigation. Omit `agent` to create one. ### Example: Edit an agent ```tsx ``` ### Example: New agent ```tsx ``` ### Example: Narrow ```tsx
``` ### API reference | Prop | Type | Required | Description | | --- | --- | --- | --- | | agent | `AgentDefinition` | No | Agent being edited, a new agent is created when omitted | | initialDraft | `Partial` | No | Values the form starts from, for example a duplicated agent; applied on top of `agent` they count as unsaved changes | | catalog | `ToolCatalogItem[]` | Yes | Tools that can be picked | | models | `ModelOption[]` | No | Models offered in addition to `inherit` | | skills | `string[]` | No | Skills offered in the skills picker | | colors | `MantineColor[]` | No | Colors offered in the color picker | | existingNames | `string[]` | No | Names of other agents, used to check that the name is unique | | allowRename | `boolean` | No | Allows renaming an existing agent, `false` by default | | onSave | `(draft: AgentDraft) => void \| Promise` | Yes | Called with a valid draft; a rejected promise is shown in an alert | | onCancel | `() => void` | No | Called by Cancel, after a confirmation when there are unsaved changes | | onDirtyChange | `(dirty: boolean) => void` | No | Called whenever the dirty state changes | | labels | `Partial` | No | Overrides of the default English labels | | className | `string` | No | Class name added to the root element | | style | `React.CSSProperties` | No | Inline styles added to the root element | ## AgentCreateWizard URL: https://sinups.github.io/ai-kit/docs/agent-create-wizard ### Code ```tsx import { AgentCreateWizard } from "@sinups/ai-kit"; export function Example() { return ( agent.name)} onGenerate={(task) => api.generateAgentDraft(task)} onCreate={(draft) => api.createAgent(draft)} /> ); } ``` ### Usage Create a subagent step by step in a modal: Method, Identity, Prompt, Tools and Model, then a review. With `onGenerate` the first step offers Generate with AI, which turns a task description into a draft to adjust on the next steps; otherwise the wizard starts from an empty agent. The review warns when the agent can call destructive tools. A rejected `onCreate` is shown in the wizard, success closes it. ### Example: Generate with AI ```tsx ``` ### Example: Manual configuration ```tsx ``` ### API reference | Prop | Type | Required | Description | | --- | --- | --- | --- | | opened | `boolean` | Yes | Whether the wizard modal is open | | onClose | `() => void` | Yes | Called when the wizard is dismissed and after a successful `onCreate` | | onCreate | `(draft: AgentDraft) => void \| Promise` | Yes | Called with a valid draft on the last step; a rejected promise is shown in an alert | | onGenerate | `(task: string) => Promise>` | No | Generates a draft from a task description, the Generate with AI method is offered only when set | | catalog | `ToolCatalogItem[]` | Yes | Tools that can be picked | | models | `ModelOption[]` | No | Models offered in addition to `inherit` | | skills | `string[]` | No | Skills offered in the skills picker | | colors | `MantineColor[]` | No | Colors offered in the color picker | | existingNames | `string[]` | No | Names of existing agents, used to check that the name is unique | | initialDraft | `Partial` | No | Values the wizard starts from | | labels | `Partial` | No | Overrides of the default English labels | ## AgentIdentityFields URL: https://sinups.github.io/ai-kit/docs/agent-identity-fields ### Code ```tsx "use client"; import { useState } from "react"; import { Stack } from "@mantine/core"; import { AgentIdentityFields, AgentModelFields, AgentPromptField, DEFAULT_AGENT_FIELD_LABELS, createAgentDraft, validateAgentDraft, type AgentDraft, } from "@sinups/ai-kit"; const models = [ { id: "qwen3-coder", name: "Qwen3 Coder", version: "30B" }, { id: "llama-4-scout", name: "Llama 4 Scout" }, ]; export function AgentForm({ existingNames }: { existingNames: string[] }) { const [draft, setDraft] = useState(() => createAgentDraft()); const [autoName, setAutoName] = useState(true); const errors = validateAgentDraft(draft, { existingNames }); const onChange = (patch: Partial) => setDraft((prev) => ({ ...prev, ...patch })); const labels = DEFAULT_AGENT_FIELD_LABELS; return ( setAutoName(false)} /> ); } ``` ### Usage The field groups that AgentEditor and AgentCreateWizard are built from, exported for your own agent forms. All of them are controlled: they read an `AgentDraft`, report partial changes through `onChange` and show messages from `errors`. AgentIdentityFields renders display name, name and "when to use" description; while `autoName` is true the name follows the display name through `slugifyAgentName`, and `onNameEdited` fires when the user types a name by hand; `nameDisabled` locks the name when editing an existing agent. AgentPromptField is the system prompt editor with Write and Preview tabs (Markdown preview). AgentModelFields renders the model select (with an Inherit from the session option), max turns, a skills multiselect when `skills` or the draft has any, and AgentColorPicker. AgentColorPicker can be used alone: clicking the selected color clears it, `colors` defaults to `AGENT_COLORS`. Name and model fields sit in pairs when the form is wide and stack below that. Validate with `validateAgentDraft` and create drafts with `createAgentDraft` or `toAgentDraft`. ### Example: Wide ```tsx ``` ### Example: Narrow ```tsx
``` ### API reference | Prop | Type | Required | Description | | --- | --- | --- | --- | | autoName | `boolean` | Yes | Keeps `name` in sync with `displayName` until the name is edited | | onNameEdited | `() => void` | Yes | Called when the name is edited by hand | | nameDisabled | `boolean` | No | Locks the name, for example when editing an existing agent | | draft | `AgentDraft` | Yes | | | errors | `AgentDraftErrors` | Yes | | | onChange | `PatchDraft` | Yes | | | labels | `AgentFieldLabels` | Yes | | ## ToolSelector URL: https://sinups.github.io/ai-kit/docs/tool-selector ### Code ```tsx "use client"; import { useState } from "react"; import { ToolSelector, type AgentToolSelection, type ToolCatalogItem } from "@sinups/ai-kit"; export function Example({ catalog }: { catalog: ToolCatalogItem[] }) { const [tools, setTools] = useState(["Read", "Grep"]); return ; } ``` ### Usage Pick the tools an agent may call. Switch between All tools and Selected; in Selected mode tools are searchable, grouped by `group` (built-in, each MCP server) with collapsible headers, and marked read-only or destructive. A counter shows how many tools are selected. Shows an empty state when the catalog is empty. ### Example: Wide ```tsx ``` ### Example: Narrow ```tsx
``` ### API reference | Prop | Type | Required | Description | | --- | --- | --- | --- | | catalog | `ToolCatalogItem[]` | Yes | Tools that can be picked | | value | `AgentToolSelection` | Yes | `all` or the selected tool names | | onChange | `(value: AgentToolSelection) => void` | Yes | Called with the next selection | | label | `React.ReactNode` | No | Label above the control | | description | `React.ReactNode` | No | Description under the label | | error | `React.ReactNode` | No | Validation message | | disabled | `boolean` | No | Disables every control | | defaultCollapsedGroups | `string[]` | No | Groups collapsed on the first render, all groups are expanded by default | | labels | `Partial` | No | Overrides of the default English labels | | className | `string` | No | Class name added to the root element | | style | `React.CSSProperties` | No | Inline styles added to the root element | ## AgentAvatar URL: https://sinups.github.io/ai-kit/docs/agent-avatar ### Code ```tsx import { AgentAvatar } from "@sinups/ai-kit"; export function Example() { return ; } ``` ### Usage Show an agent in lists, headers and chat. The avatar uses the agent `icon` when set, otherwise initials of `displayName` or `name`, tinted with the agent `color`. ### Example: Agents and sizes ```tsx <> ``` ### API reference | Prop | Type | Required | Description | | --- | --- | --- | --- | | agent | `Pick` | Yes | Agent to render, `icon` wins over initials of `displayName` or `name` | | size | `MantineSize \| number` | No | Avatar size, `md` by default | | className | `string` | No | Class name added to the root element | | style | `React.CSSProperties` | No | Inline styles added to the root element | ## SkillsSettingsPanel URL: https://sinups.github.io/ai-kit/docs/skills-settings-panel ### Code ```tsx "use client"; import { SkillsSettingsPanel, type Skill } from "@sinups/ai-kit"; export function SkillsSettings({ skills }: { skills: Skill[] }) { return ( api.setSkillEnabled(skill.id, enabled)} onCreate={(draft) => api.createSkill(draft)} onUpdate={(skill, draft) => api.updateSkill(skill.id, draft)} onRemove={(skill) => api.removeSkill(skill.id)} /> ); } ``` ### Usage Manage agent skills in one screen built on `MasterDetail`: `SkillCatalog` beside `SkillDetail` from 720px of width, one pane with a back button below. New skill, Edit and Duplicate open `SkillEditor`; only user and project skills are editable by default (`isEditable`); removal is confirmed in a dialog. Selection can be controlled with `selectedId`. Give the panel a height. ### Example: Full page ```tsx ``` ### Example: Narrow widget ```tsx
``` ### API reference | Prop | Type | Required | Description | | --- | --- | --- | --- | | skills | `Skill[]` | Yes | Skills to manage | | loading | `boolean` | No | Shows skeletons in the catalog | | error | `React.ReactNode` | No | Error message shown instead of the catalog | | onRetry | `() => void` | No | Called by the retry button of the error alert | | availableTools | `string[]` | No | Tool names offered in the editor | | onToggle | `(skill: Skill, enabled: boolean) => Promise \| void` | No | Enables or disables a skill | | onCreate | `(draft: SkillDraft) => Promise \| Skill \| void` | No | Creates a skill from a draft, New skill and Duplicate are available only when set; return the created skill to select it | | onUpdate | `(skill: Skill, draft: SkillDraft) => Promise \| void` | No | Saves edits of a skill, Edit is available only when set | | onRemove | `(skill: Skill) => void \| Promise` | No | Removes a skill after confirmation, the dialog stays open with the error when the returned promise rejects | | isEditable | `(skill: Skill) => boolean` | No | Returns whether a skill can be edited, user and project skills by default | | selectedId | `string \| null` | No | Selected skill id, uncontrolled when omitted | | onSelectedIdChange | `(id: string \| null) => void` | No | Called when the selected skill changes | | catalogVariant | `'list' \| 'grid'` | No | Catalog layout, `list` by default | | breakpoint | `number` | No | Component width in px from which catalog and detail sit side by side, `720` by default | | listWidth | `number` | No | Catalog pane width in px when wide, `340` by default | | labels | `Partial` | No | Overrides of the default English labels of the panel and its parts | | className | `string` | No | Class name added to the root element | | style | `React.CSSProperties` | No | Inline styles added to the root element, give the panel a height | ## SkillCatalog URL: https://sinups.github.io/ai-kit/docs/skill-catalog ### Code ```tsx import { SkillCatalog } from "@sinups/ai-kit"; export function Example() { return ( setSelectedId(skill.id)} onToggle={(skill, enabled) => api.setSkillEnabled(skill.id, enabled)} onCreate={openEditor} onEdit={editSkill} onDuplicate={duplicateSkill} onRemove={removeSkill} /> ); } ``` ### Usage Browse skills with search and a source filter. The `list` variant shows rows grouped by source, `grid` shows cards whose column count follows the component width. Every skill has an enable switch that shows a loader until `onToggle` settles, and an actions menu with Edit, Duplicate and Remove when those callbacks are set (`isEditable` limits Edit and Remove). Handles loading, error and empty states. ### Example: List ```tsx ``` ### Example: Grid ```tsx ``` ### Example: Loading, error and empty ```tsx <> ``` ### API reference | Prop | Type | Required | Description | | --- | --- | --- | --- | | skills | `Skill[]` | Yes | Skills to show | | loading | `boolean` | No | Shows skeletons instead of skills | | error | `React.ReactNode` | No | Error message shown instead of skills | | onRetry | `() => void` | No | Called by the retry button of the error alert | | selectedId | `string \| null` | No | Id of the selected skill | | onSelect | `(skill: Skill) => void` | No | Called when a skill is clicked or chosen with Enter | | onToggle | `(skill: Skill, enabled: boolean) => Promise \| void` | No | Enables or disables a skill, the switch shows a loader until the returned promise settles | | onEdit | `(skill: Skill) => void` | No | Opens the editor, the Edit action is shown only when set | | onDuplicate | `(skill: Skill) => void` | No | Duplicates a skill, the Duplicate action is shown only when set | | onRemove | `(skill: Skill) => void` | No | Removes a skill, the Remove action is shown only when set | | isEditable | `(skill: Skill) => boolean` | No | Returns whether Edit and Remove are offered for a skill, all skills by default | | onCreate | `() => void` | No | Starts creating a skill, the New skill button is shown only when set | | variant | `'list' \| 'grid'` | No | `list` renders rows, `grid` renders cards whose column count follows the component width, `list` by default | | groupBySource | `boolean` | No | Groups rows by source in the `list` variant, `true` by default | | query | `string` | No | Search query, uncontrolled when omitted | | onQueryChange | `(query: string) => void` | No | Called when the search query changes | | source | `SkillSourceFilter` | No | Source filter, uncontrolled with `all` by default | | onSourceChange | `(source: SkillSourceFilter) => void` | No | Called when the source filter changes | | labels | `Partial` | No | Overrides of the default English labels | | className | `string` | No | Class name added to the root element | | style | `React.CSSProperties` | No | Inline styles added to the root element | ## SkillDetail URL: https://sinups.github.io/ai-kit/docs/skill-detail ### Code ```tsx import { SkillDetail } from "@sinups/ai-kit"; export function Example() { return ( api.setSkillEnabled(skill.id, enabled)} /> ); } ``` ### Usage Show one skill: status, description, source, version, author, path, update date and usage count, the tools it may use without asking, and its Markdown instructions. Edit and Enable/Disable buttons render when their callbacks are set; `actions` adds your own buttons. ### Example: Skill ```tsx ``` ### Example: Disabled skill ```tsx ``` ### API reference | Prop | Type | Required | Description | | --- | --- | --- | --- | | skill | `Skill` | Yes | Skill to show | | onEdit | `(skill: Skill) => void` | No | Opens the editor, the Edit button is shown only when set | | onToggle | `(skill: Skill, enabled: boolean) => Promise \| void` | No | Enables or disables the skill, the button shows a loader until the returned promise settles | | actions | `React.ReactNode` | No | Extra buttons rendered after Edit and Enable | | locale | `string` | No | Locale used to format the update date, the browser locale by default | | labels | `Partial` | No | Overrides of the default English labels | | className | `string` | No | Class name added to the root element | | style | `React.CSSProperties` | No | Inline styles added to the root element | ## SkillEditor URL: https://sinups.github.io/ai-kit/docs/skill-editor ### Code ```tsx import { SkillEditor } from "@sinups/ai-kit"; export function Example() { return ( api.updateSkill(skill.id, draft)} onCancel={closeEditor} /> ); } ``` ### Usage Create or edit a skill: a slug name checked against `existingNames`, description, tags, allowed tools and Markdown instructions with Write and Preview tabs. A rejected `onSave` keeps the form open with the message; leaving with unsaved changes asks for confirmation. Omit `skill` to create one, pass `initialDraft` to start from a duplicate. ### Example: Edit a skill ```tsx ``` ### Example: New skill ```tsx ``` ### API reference | Prop | Type | Required | Description | | --- | --- | --- | --- | | skill | `Skill \| null` | No | Skill to edit, a new skill is created when omitted | | initialDraft | `Partial` | No | Initial values of a new skill, for example a duplicate | | availableTools | `string[]` | No | Tool names offered in the allowed tools select | | existingNames | `string[]` | No | Names of other skills, used to check that the name is unique | | onSave | `(draft: SkillDraft) => Promise \| void` | Yes | Saves the draft, the form stays open and shows the rejection message when the promise rejects | | onCancel | `() => void` | No | Leaves the editor, asks for confirmation first when there are unsaved changes | | onDirtyChange | `(dirty: boolean) => void` | No | Called when the form gains or loses unsaved changes, lets the host guard its own navigation | | labels | `Partial` | No | Overrides of the default English labels and validation messages | | className | `string` | No | Class name added to the root element | | style | `React.CSSProperties` | No | Inline styles added to the root element | ## SkillPicker URL: https://sinups.github.io/ai-kit/docs/skill-picker ### Code ```tsx "use client"; import { useState } from "react"; import { SkillPicker, type Skill } from "@sinups/ai-kit"; export function Example({ skills }: { skills: Skill[] }) { const [value, setValue] = useState([]); return ; } ``` ### Usage Pick several skills by id in a form, for example the skills loaded for an agent. The combobox searches skill names and tags, shows picked skills as pills and marks disabled skills. ### Example: Pick skills ```tsx ``` ### API reference | Prop | Type | Required | Description | | --- | --- | --- | --- | | skills | `Skill[]` | Yes | Skills that can be picked | | value | `string[]` | Yes | Ids of picked skills | | onChange | `(value: string[]) => void` | Yes | Called with the new ids | | label | `React.ReactNode` | No | Input label | | description | `React.ReactNode` | No | Text under the label | | error | `React.ReactNode` | No | Validation message | | placeholder | `string` | No | Placeholder shown while nothing is picked, `Pick skills` by default | | disabled | `boolean` | No | Disables the input | | maxDropdownHeight | `number` | No | Maximum height of the options dropdown in px, `240` by default | | labels | `Partial` | No | Overrides of the default English labels | | className | `string` | No | Class name added to the root element | | style | `React.CSSProperties` | No | Inline styles added to the root element | --- # MCP ## McpSettingsPanel URL: https://sinups.github.io/ai-kit/docs/mcp-settings-panel ### Code ```tsx "use client"; import { McpSettingsPanel, type McpServer, type McpServerDraft } from "@sinups/ai-kit"; export function McpSettings({ servers }: { servers: McpServer[] }) { return (
api.addServer(draft)} onUpdate={(draft) => api.updateServer(draft.id!, draft)} onReconnect={(server) => api.reconnect(server.id)} onAuthenticate={(server) => api.startOAuth(server.id)} onEnable={(server) => api.setEnabled(server.id, true)} onDisable={(server) => api.setEnabled(server.id, false)} onRemove={(server) => api.removeServer(server.id)} onTryTool={(server, tool) => openToolRunner(server, tool)} />
); } ``` ### Usage Drop in a complete MCP settings screen. It combines `McpServerList`, `McpServerDetail`, `McpToolDetail` and the add/edit `McpServerWizardModal` inside `MasterDetail`: list and detail side by side when wide, one pane with back navigation in a narrow widget. Everything is data and callbacks: pass `servers` from your MCP client, return promises from actions to show pending states and errors. Omit a callback to hide its action. Fills the parent height. ### Example: Full page ```tsx ``` ### Example: Narrow widget ```tsx
``` ### API reference | Prop | Type | Required | Description | | --- | --- | --- | --- | | servers | `McpServer[]` | Yes | Configured servers | | loading | `boolean` | No | Shows skeleton rows in the list | | error | `React.ReactNode` | No | Error message shown instead of the list | | onRetry | `() => void` | No | Called by the retry button of the error alert | | selectedId | `string \| null` | No | Controlled id of the selected server | | onSelectedIdChange | `(id: string \| null) => void` | No | Called when the selected server changes | | onAdd | `(draft: McpServerDraft) => void \| Promise` | No | Enables the add wizard; resolve to close it, reject to show the message | | onUpdate | `(draft: McpServerDraft) => void \| Promise` | No | Enables editing through the wizard; `draft.id` is the edited server id | | onReconnect | `ServerAction` | No | Adds a "Reconnect" action to servers that are not disabled | | onAuthenticate | `ServerAction` | No | Adds an "Authenticate" action to servers that need auth | | onEnable | `ServerAction` | No | Adds an "Enable" action to disabled servers | | onDisable | `ServerAction` | No | Adds a "Disable" action to enabled servers | | onRemove | `ServerAction` | No | Adds a "Remove" action that asks for confirmation first | | onTryTool | `(server: McpServer, tool: McpToolDefinition) => void` | No | Renders a "Try tool" button in the tool detail | | labels | `Partial` | No | Overrides of the default English labels of the list, detail, tool detail and wizard | | listWidth | `number` | No | Width of the server list when wide, `380` by default | | className | `string` | No | Class name added to the root element | | style | `React.CSSProperties` | No | Inline styles added to the root element | ## McpServerList URL: https://sinups.github.io/ai-kit/docs/mcp-server-list ### Code ```tsx import { McpServerList } from "@sinups/ai-kit"; export function Example() { return ( setSelectedId(server.id)} onAdd={openWizard} onReconnect={reconnect} onAuthenticate={authenticate} onDisable={disable} onEnable={enable} onRemove={remove} /> ); } ``` ### Usage List configured MCP servers with their transport, status, command or URL, connection error and tool count. Search matches the name, command and URL; the filter narrows by connected, needs attention, disabled or scope, and `withSearch={false}` or `withFilter={false}` hides either control; servers are grouped by scope when there is more than one. The actions menu offers Authenticate, Reconnect, Enable or Disable depending on the status, and Remove asks for confirmation. Handles loading, error and empty states. ### Example: Servers ```tsx ``` ### Example: Loading, error and empty ```tsx <> ``` ### API reference | Prop | Type | Required | Description | | --- | --- | --- | --- | | servers | `McpServer[]` | Yes | Configured servers | | selectedId | `string \| null` | No | Id of the highlighted server | | onSelect | `(server: McpServer) => void` | No | Called when a server row is clicked | | loading | `boolean` | No | Shows skeleton rows | | error | `React.ReactNode` | No | Error message shown instead of the list | | onRetry | `() => void` | No | Called by the retry button of the error alert | | onAdd | `() => void` | No | Renders the "Add server" button when set | | onReconnect | `ServerAction` | No | Adds a "Reconnect" action to servers that are not disabled | | onAuthenticate | `ServerAction` | No | Adds an "Authenticate" action to servers that need auth | | onEnable | `ServerAction` | No | Adds an "Enable" action to disabled servers | | onDisable | `ServerAction` | No | Adds a "Disable" action to enabled servers | | onRemove | `ServerAction` | No | Adds a "Remove" action that asks for confirmation first | | groupByScope | `boolean` | No | Groups servers by scope when they use more than one, `true` by default | | withSearch | `boolean` | No | Shows the search input, `true` by default | | withFilter | `boolean` | No | Shows the status and scope filter, `true` by default | | labels | `Partial` | No | Button and message overrides | | className | `string` | No | Class name added to the root element | | style | `React.CSSProperties` | No | Inline styles added to the root element | ## McpServerDetail URL: https://sinups.github.io/ai-kit/docs/mcp-server-detail ### Code ```tsx import { McpServerDetail } from "@sinups/ai-kit"; export function Example() { return ( setToolName(tool.name)} onReconnect={reconnect} onAuthenticate={authenticate} onDisable={disable} onEnable={enable} onEdit={openEditWizard} onRemove={remove} /> ); } ``` ### Usage Show one MCP server: name, transport, scope, version and status with Authenticate, Reconnect and Enable/Disable buttons that show pending state. A failed connection is shown in an alert. Tabs list tools with behavior annotations (read-only, destructive, idempotent, open world), resources, prompts with their arguments, and the configuration with command, arguments or URL and masked secrets that can be revealed. Edit and Remove live in the configuration tab. ### Example: Connected server ```tsx ``` ### Example: Needs authentication ```tsx ``` ### Example: Connection error ```tsx ``` ### Example: Configuration ```tsx ``` ### API reference | Prop | Type | Required | Description | | --- | --- | --- | --- | | server | `McpServer` | Yes | Server to show | | loading | `boolean` | No | Shows skeletons instead of tools, resources and prompts | | tab | `McpServerDetailTab` | No | Controlled active tab | | onTabChange | `(tab: McpServerDetailTab) => void` | No | Called when the user switches tabs | | onSelectTool | `(tool: McpToolDefinition) => void` | No | Called when a tool row is clicked | | onReconnect | `ServerAction` | No | Renders the "Reconnect" button for servers that are not disabled | | onAuthenticate | `ServerAction` | No | Renders the "Authenticate" button for servers that need auth | | onEnable | `ServerAction` | No | Renders the "Enable" button for disabled servers | | onDisable | `ServerAction` | No | Renders the "Disable" button for enabled servers | | onEdit | `(server: McpServer) => void` | No | Renders the "Edit" button in the configuration tab | | onRemove | `ServerAction` | No | Renders the "Remove" button in the configuration tab, the removal is confirmed in a dialog | | labels | `Partial` | No | Button, tab and field overrides | | className | `string` | No | Class name added to the root element | | style | `React.CSSProperties` | No | Inline styles added to the root element | ## McpToolDetail URL: https://sinups.github.io/ai-kit/docs/mcp-tool-detail ### Code ```tsx import { McpToolDetail } from "@sinups/ai-kit"; export function Example() { return ( setToolName(null)} onTry={(tool) => openToolRunner(tool)} /> ); } ``` ### Usage Document one MCP tool from `tools/list`: title and name, description, behavior annotations with explanations, and `SchemaView` tables for `inputSchema` and `outputSchema`. Add `onTry` to offer running the tool and `onBack` to return to the server. ### Example: With output schema ```tsx ``` ### Example: Narrow ```tsx
``` ### API reference | Prop | Type | Required | Description | | --- | --- | --- | --- | | tool | `McpToolDefinition` | Yes | Tool definition from `tools/list` | | serverName | `string` | No | Name of the server that provides the tool, shown above the tool name | | onBack | `() => void` | No | Renders a back button above the header | | onTry | `(tool: McpToolDefinition) => void` | No | Renders a "Try tool" button | | labels | `Partial` | No | Heading and button overrides | | className | `string` | No | Class name added to the root element | | style | `React.CSSProperties` | No | Inline styles added to the root element | ## McpServerWizard URL: https://sinups.github.io/ai-kit/docs/mcp-server-wizard ### Code ```tsx import { McpServerWizardModal } from "@sinups/ai-kit"; export function Example() { return ( server.name)} onSubmit={(draft) => (draft.id ? api.updateServer(draft.id, draft) : api.addServer(draft))} /> ); } ``` ### Usage Add or edit an MCP server in four steps: Basics (unique name, scope, transport), Connection (command and arguments for stdio, validated URL for HTTP and SSE), Environment or Headers with secrets, and Review. Pasting a full command line splits it into command and arguments. `onSubmit` receives a normalized `McpServerDraft`; a rejected promise is shown in the wizard. With `initialServer` the wizard edits: every step can be opened directly and Save validates them all. Use `McpServerWizard` inline or `McpServerWizardModal`, which closes after a successful submit. ### Example: Add a server ```tsx ``` ### Example: Edit a server ```tsx ``` ### Example: Modal ```tsx ``` ### API reference | Prop | Type | Required | Description | | --- | --- | --- | --- | | onSubmit | `SubmitHandler` | Yes | Called with the normalized draft after the review step, a rejected promise is shown in an alert | | onCancel | `() => void` | No | Called by the cancel button | | initialServer | `McpServer` | No | Server to edit, the wizard adds a new server when omitted | | defaultTransport | `McpTransport` | No | Transport preselected for a new server, `stdio` by default | | existingNames | `string[]` | No | Names already in use, compared case-insensitively; the edited server's own name is ignored | | labels | `Partial` | No | Step, field and message overrides | | className | `string` | No | Class name added to the root element | | style | `React.CSSProperties` | No | Inline styles added to the root element | ## McpImportDialog URL: https://sinups.github.io/ai-kit/docs/mcp-import-dialog ### Code ```tsx "use client"; import { useState } from "react"; import { Button } from "@mantine/core"; import { McpImportDialog, type McpServerCandidate } from "@sinups/ai-kit"; const candidates: McpServerCandidate[] = [ { id: "git", name: "git", transport: "stdio", command: "uvx", args: ["mcp-server-git"] }, { id: "issues", name: "issues", transport: "http", url: "https://issues.example.com/mcp" }, ]; export function Example({ save }: { save: (servers: McpServerCandidate[]) => Promise }) { const [opened, setOpened] = useState(false); return ( <> setOpened(false)} sourceLabel="Desktop client" servers={candidates} existingNames={["git", "filesystem"]} onImport={save} /> ); } ``` ### Usage Import MCP servers found in another client's configuration. Every candidate is selected initially and shows its transport and target (command or URL). Names that collide with `existingNames` (case-insensitive) get a `_1`, `_2` suffix, marked Renamed, and stay editable; invalid, taken or duplicate names block the Import button with a message under the field. `onImport` receives the selected servers with their final names. While its promise is pending the button shows a loader and the inputs are disabled; a rejection keeps the dialog open with the message in an alert; on success the dialog closes. With an empty `servers` list the dialog shows an empty state. The rename and validation rules are exported as `resolveImportNames` and `validateImportNames`, so you can apply the same rules on the server side. ### Example: Import with renames ```tsx ``` ### Example: Empty ```tsx ``` ### API reference | Prop | Type | Required | Description | | --- | --- | --- | --- | | opened | `boolean` | Yes | Whether the dialog is open | | onClose | `() => void` | Yes | Called when the dialog is dismissed, cancelled or after a successful import | | sourceLabel | `string` | Yes | Name of the client the servers come from, for example `Desktop client` | | servers | `McpServerCandidate[]` | Yes | Servers found in the other client | | existingNames | `string[]` | No | Names of configured servers, compared case-insensitively; colliding imports are renamed `name_1`, `name_2` | | onImport | `(servers: McpServerCandidate[]) => void \| Promise` | Yes | Called with the selected servers carrying their final names; a rejected promise keeps the dialog open and shows the message | | labels | `Partial` | No | Title, button and validation message overrides | ## McpDiscoveredServers URL: https://sinups.github.io/ai-kit/docs/mcp-discovered-servers ### Code ```tsx import { McpDiscoveredServers, type McpDiscoveredServer } from "@sinups/ai-kit"; const discovered: McpDiscoveredServer[] = [ { id: "filesystem", name: "filesystem", transport: "stdio", command: "npx", args: ["-y", "mcp-filesystem", "./docs"], source: ".mcp.json" }, { id: "errors", name: "errors", transport: "http", url: "https://errors.example.com/mcp", source: ".mcp.json" }, ]; export function Example({ approve, reject, }: { approve: (servers: McpDiscoveredServer[]) => Promise; reject: (servers: McpDiscoveredServer[]) => Promise; }) { return ; } ``` ### Usage Ask the user to approve MCP servers that a project configuration adds, before any of them runs. Pass only servers the user has not decided on yet; with an empty list nothing renders, so remove servers from the list once the decision is saved. All servers are selected by default (`defaultSelectedIds` changes that). `onApprove` receives the selected servers; `onReject` receives every listed server and the Reject button appears only when it is set. Both buttons show a loader while the promise is pending, and a rejection is shown in a dismissible alert. The card works in a 360px widget and on a settings page. ### Example: Wide ```tsx ``` ### Example: Narrow ```tsx
``` ### Example: Saving fails ```tsx Promise.reject(new Error("Could not write .agent/settings.local.json"))} onReject={reject} /> ``` ### API reference | Prop | Type | Required | Description | | --- | --- | --- | --- | | servers | `McpDiscoveredServer[]` | Yes | Servers found in project configuration that the user has not approved or rejected yet; nothing renders when empty | | onApprove | `(servers: McpDiscoveredServer[]) => void \| Promise` | Yes | Called with the selected servers; the buttons show a loader until the promise settles | | onReject | `(servers: McpDiscoveredServer[]) => void \| Promise` | No | Called with all listed servers, selected or not, to dismiss the prompt; the Reject button is hidden when omitted | | defaultSelectedIds | `string[]` | No | Ids selected initially, all servers by default; servers added to the list later start selected | | labels | `Partial` | No | Button and message overrides | | className | `string` | No | Class name added to the root element | | style | `React.CSSProperties` | No | Inline styles added to the root element | ## McpConfigWarnings URL: https://sinups.github.io/ai-kit/docs/mcp-config-warnings ### Code ```tsx import { McpConfigWarnings, type McpConfigWarning } from "@sinups/ai-kit"; const warnings: McpConfigWarning[] = [ { file: ".mcp.json", path: "mcpServers.git", kind: "duplicate-name", message: "Also defined in ~/.agent/mcp.json; the project entry wins" }, { file: ".mcp.json", path: "mcpServers.postgres.timeout", kind: "unknown-field", message: "Field is ignored" }, { file: "~/.agent/mcp.json", path: "mcpServers.issues.url", kind: "invalid-value", message: "Expected an http or https URL" }, ]; export function Example({ openFile }: { openFile: (file: string) => void }) { return ; } ``` ### Usage List problems found while reading MCP configuration files, usually above McpServerList. Warnings are grouped by file with the field path, a kind badge and the message; exact duplicates are shown once. Known kinds are `duplicate-name`, `unknown-field` and `invalid-value`; any other string is accepted and shown as is unless you add a label for it in `labels.kinds`. With no warnings nothing renders. `onOpenFile` adds an Open file button to each group. The grouping is exported as `groupConfigWarnings`. ### Example: Wide ```tsx ``` ### Example: Narrow ```tsx
``` ### API reference | Prop | Type | Required | Description | | --- | --- | --- | --- | | warnings | `McpConfigWarning[]` | Yes | Warnings produced while reading MCP configuration files; nothing renders when empty, exact duplicates are shown once | | onOpenFile | `(file: string) => void` | No | Adds an "Open file" button to every file group | | labels | `Partial` | No | Title, button and warning kind overrides | | className | `string` | No | Class name added to the root element | | style | `React.CSSProperties` | No | Inline styles added to the root element | ## McpToolAnnotationBadges URL: https://sinups.github.io/ai-kit/docs/mcp-tool-annotation-badges ### Code ```tsx import { Group, Text } from "@mantine/core"; import { McpToolAnnotationBadges, McpTransportIcon, type McpToolDefinition } from "@sinups/ai-kit"; export function ToolRow({ tool }: { tool: McpToolDefinition }) { return ( {tool.name} ); } ``` ### Usage Show the behavior hints an MCP server declares for a tool: read-only (teal), destructive (red), idempotent (gray) and open world (blue), mapped from `readOnlyHint`, `destructiveHint`, `idempotentHint` and `openWorldHint`. Hints that are false or missing produce no badge; with no hints the component renders nothing. `withTooltips` adds a one-line explanation to each badge; `labels` replaces the label and description of every kind (defaults are in `DEFAULT_MCP_TOOL_ANNOTATION_LABELS`). McpToolDetail and McpServerDetail use it; use it in your own tool lists and approval prompts. `getMcpToolAnnotationKinds` returns the kinds without rendering. ### Example: Annotations and transports ```tsx delete_issue ``` ### API reference | Prop | Type | Required | Description | | --- | --- | --- | --- | | annotations | `McpToolAnnotations` | No | Tool annotations from the MCP server, nothing is rendered when none apply | | labels | `McpToolAnnotationLabels` | No | Badge labels and tooltip descriptions per annotation kind | | withTooltips | `boolean` | No | Shows the description of each badge in a tooltip, `false` by default | ## McpTransportIcon URL: https://sinups.github.io/ai-kit/docs/mcp-transport-icon ### Code ```tsx import { Group, Text } from "@mantine/core"; import { McpTransportIcon, type McpTransport } from "@sinups/ai-kit"; const TRANSPORT_NAMES: Record = { stdio: "stdio", http: "HTTP", sse: "SSE" }; export function TransportLabel({ transport }: { transport: McpTransport }) { return ( {TRANSPORT_NAMES[transport]} ); } ``` ### Usage Icon for an MCP transport: a terminal for `stdio`, a globe for `http` and a broadcast icon for `sse`. `size` is in px, 16 by default. The icon is decorative (`aria-hidden`), so put the transport name next to it. The MCP list, detail, wizard, import and discovery components use it. ### Example: Transports ```tsx <> ``` ### API reference | Prop | Type | Required | Description | | --- | --- | --- | --- | | transport | `McpTransport` | Yes | Server transport: `stdio` shows a terminal, `sse` a broadcast icon, `http` a globe | | size | `number` | No | Icon size in px, `16` by default | | className | `string` | No | Class name added to the icon | | style | `React.CSSProperties` | No | Inline styles added to the icon | --- # Permissions & hooks ## PermissionRulesPanel URL: https://sinups.github.io/ai-kit/docs/permission-rules-panel ### Code ```tsx import { PermissionRulesPanel } from "@sinups/ai-kit"; export function Example() { return ( (mode === "edit" ? api.updateRule(rule) : api.addRule(rule))} onDeleteRule={(rule) => api.deleteRule(rule.id)} onMoveRule={(rule, scope) => api.moveRule(rule.id, scope)} onAddDirectory={(directory) => api.addDirectory(directory)} onRemoveDirectory={(directory) => api.removeDirectory(directory.path)} /> ); } ``` ### Usage Manage tool permission rules like `Bash(npm run test:*)`. Tabs split rules into Allow, Ask and Deny, show recently denied tool calls (Allow this opens the rule wizard prefilled from the denial) and additional working directories. Rules show their scope (session, local, project, user or read-only policy) and can be added or edited in `AddPermissionRuleWizard`, moved between scopes and deleted. Omit a callback to hide its action. ### Example: Wide ```tsx ``` ### Example: Narrow ```tsx
``` ### API reference | Prop | Type | Required | Description | | --- | --- | --- | --- | | rules | `PermissionRule[]` | Yes | Rules of all behaviors and scopes | | denials | `PermissionDenial[]` | No | Recently denied tool calls, the tab is hidden when omitted | | directories | `WorkspaceDirectory[]` | No | Additional working directories, the tab is hidden when omitted | | loading | `boolean` | No | Shows skeleton rows instead of the lists | | error | `React.ReactNode` | No | Error message shown instead of the lists | | onRetry | `() => void` | No | Called by the retry button of the error alert | | onSaveRule | `(rule: PermissionRule, mode: PermissionRuleSaveMode) => void \| Promise` | No | Called with a rule created or edited in the wizard, add and edit actions are hidden when omitted | | onDeleteRule | `(rule: PermissionRule) => void \| Promise` | No | Called by the delete action, hidden when omitted | | onMoveRule | `(rule: PermissionRule, scope: PermissionScope) => void \| Promise` | No | Called by the move actions with the new scope, hidden when omitted | | onAddDirectory | `(directory: WorkspaceDirectory) => void \| Promise` | No | Called with a new directory, the add form is hidden when omitted | | onRemoveDirectory | `(directory: WorkspaceDirectory) => void \| Promise` | No | Called by the remove action of a directory, hidden when omitted | | knownTools | `string[]` | No | Tool names suggested in the rule wizard | | tab | `PermissionRulesTab` | No | Controlled active tab | | defaultTab | `PermissionRulesTab` | No | Initial tab when `tab` is not controlled, `allow` by default | | onTabChange | `(tab: PermissionRulesTab) => void` | No | Called with the tab the user picked | | labels | `Partial` | No | Overrides of the default English labels of the panel | | className | `string` | No | Class name added to the root element | | style | `React.CSSProperties` | No | Inline styles added to the root element | ## AddPermissionRuleWizard URL: https://sinups.github.io/ai-kit/docs/add-permission-rule-wizard ### Code ```tsx import { AddPermissionRuleWizard } from "@sinups/ai-kit"; export function Example() { return ( api.addRule(rule)} /> ); } ``` ### Usage Create or edit one permission rule in a modal: behavior, the rule itself with tool suggestions and examples, and the scope it is stored in. Prefill `initialRule` from a denied tool call to turn it into an allow rule in two clicks; pass a rule with `id` to edit it. A rejected `onSubmit` keeps the wizard open and shows the error. ### Example: New rule ```tsx ``` ### Example: From a denial ```tsx ``` ### API reference | Prop | Type | Required | Description | | --- | --- | --- | --- | | opened | `boolean` | Yes | Whether the wizard modal is open | | onClose | `() => void` | Yes | Called when the wizard is cancelled or the rule is saved | | onSubmit | `(rule: PermissionRule) => void \| Promise` | Yes | Called with the built rule, a rejected promise keeps the wizard open and shows the error | | initialRule | `Partial` | No | Rule to edit; a rule without `id` prefills a new rule, for example from a denial | | knownTools | `string[]` | No | Tool names suggested in the rule field | | scopes | `PermissionScope[]` | No | Scopes offered in the scope step, all scopes except `policy` by default | | defaultScope | `PermissionScope` | No | Scope preselected for a new rule, `local` by default | | examples | `string[]` | No | Rule examples inserted by a click | | createId | `() => string` | No | Creates ids for new rules | | labels | `Partial` | No | Overrides of the default English labels | ## PermissionRuleInput URL: https://sinups.github.io/ai-kit/docs/permission-rule-input ### Code ```tsx "use client"; import { useState } from "react"; import { PermissionRuleInput } from "@sinups/ai-kit"; export function Example() { const [rule, setRule] = useState("Bash(npm run test:*)"); return ; } ``` ### Usage Type a permission rule in the `Tool(specifier)` syntax. The field suggests known tool names, validates the syntax on blur (or immediately with `forceValidation`), warns about unknown tools and explains the rule in plain words under the field, for the given `behavior`. ### Example: States ```tsx <> ``` ### API reference | Prop | Type | Required | Description | | --- | --- | --- | --- | | value | `string` | Yes | Rule text, for example `Bash(npm run test:*)` | | onChange | `(value: string) => void` | Yes | Called with the new rule text | | knownTools | `string[]` | No | Tool names suggested while typing the tool part and used to warn about unknown tools | | behavior | `PermissionBehavior` | No | Behavior used in the plain-words description under the field, `allow` by default | | label | `React.ReactNode` | No | Field label, `Rule` by default | | placeholder | `string` | No | Placeholder, `Bash(npm run test:*)` by default | | error | `React.ReactNode` | No | Error from the host, shown instead of the validation error | | forceValidation | `boolean` | No | Shows the validation error before the field is blurred | | disabled | `boolean` | No | Disables the field | | onBlur | `() => void` | No | Called when the field loses focus | | className | `string` | No | Class name added to the root element | | style | `React.CSSProperties` | No | Inline styles added to the root element | ## PermissionModeSelector URL: https://sinups.github.io/ai-kit/docs/permission-mode-selector ### Code ```tsx "use client"; import { useState } from "react"; import { PermissionModeSelector, type PermissionMode } from "@sinups/ai-kit"; export function Example() { const [mode, setMode] = useState("default"); return ; } ``` ### Usage Switch the agent permission mode: Default, Accept edits, Plan or Bypass. Each mode shows its description, and dangerous modes show a warning. `variant="auto"` renders a segmented control when there is room and a select in narrow containers. ### Example: Segmented ```tsx ``` ### Example: Select ```tsx ``` ### API reference | Prop | Type | Required | Description | | --- | --- | --- | --- | | value | `PermissionMode` | Yes | Selected mode | | onChange | `(mode: PermissionMode) => void` | Yes | Called with the mode the user picked | | modes | `PermissionMode[]` | No | Modes offered, all modes by default | | label | `React.ReactNode` | No | Field label, `Permission mode` by default | | variant | `'auto' \| 'segmented' \| 'select'` | No | `segmented` shows all modes at once, `select` a dropdown, `auto` picks by component width, `auto` by default | | disabled | `boolean` | No | Disables the control | | labels | `Partial` | No | Overrides of the default English labels | | className | `string` | No | Class name added to the root element | | style | `React.CSSProperties` | No | Inline styles added to the root element | ## HooksPanel URL: https://sinups.github.io/ai-kit/docs/hooks-panel ### Code ```tsx import { HooksPanel } from "@sinups/ai-kit"; export function Example() { return ( (mode === "edit" ? api.updateHook(hook) : api.addHook(hook))} onDelete={(hook) => api.deleteHook(hook.id)} onToggle={(hook, enabled) => api.setHookEnabled(hook.id, enabled)} /> ); } ``` ### Usage Configure lifecycle hooks such as PreToolUse, PostToolUse, SessionStart or Stop. Hooks are grouped by event with a description of when it runs; each hook shows its matcher, command or prompt, timeout and scope, and can be toggled, edited in `HookWizard` or deleted. Handles loading, error and empty states. ### Example: Wide ```tsx ``` ### Example: Narrow ```tsx
``` ### API reference | Prop | Type | Required | Description | | --- | --- | --- | --- | | hooks | `HookConfig[]` | Yes | Configured hooks of all events | | loading | `boolean` | No | Shows skeleton rows instead of hooks | | error | `React.ReactNode` | No | Error message shown instead of hooks | | onRetry | `() => void` | No | Called by the retry button of the error alert | | onSave | `(hook: HookConfig, mode: HookSaveMode) => void \| Promise` | No | Called with a hook created or edited in the wizard, add and edit actions are hidden when omitted | | onDelete | `(hook: HookConfig) => void \| Promise` | No | Called by the delete action, the action is hidden when omitted | | onToggle | `(hook: HookConfig, enabled: boolean) => void \| Promise` | No | Called by the enabled switch, the switch is read-only when omitted | | knownTools | `string[]` | No | Tool names suggested for matchers in the wizard | | defaultExpanded | `HookEvent[]` | No | Events expanded initially, events with hooks by default | | labels | `Partial` | No | Overrides of the default English labels of the panel | | className | `string` | No | Class name added to the root element | | style | `React.CSSProperties` | No | Inline styles added to the root element | ## HookWizard URL: https://sinups.github.io/ai-kit/docs/hook-wizard ### Code ```tsx import { HookWizard } from "@sinups/ai-kit"; export function Example() { return ( api.saveHook(hook)} /> ); } ``` ### Usage Create or edit a hook in a modal: pick the event, a tool matcher for tool events, a shell command or an LLM prompt with a timeout, and the scope. The wizard shows the JSON payload the hook receives for the chosen event. A rejected `onSubmit` keeps the wizard open with the error. ### Example: New hook ```tsx ``` ### Example: Edit a hook ```tsx ``` ### API reference | Prop | Type | Required | Description | | --- | --- | --- | --- | | opened | `boolean` | Yes | Whether the wizard modal is open | | onClose | `() => void` | Yes | Called when the wizard is cancelled or the hook is saved | | initialHook | `Partial` | No | Hook to edit; a hook without `id` prefills a new hook | | onSubmit | `(hook: HookConfig) => void \| Promise` | Yes | Called with the built hook, a rejected promise keeps the wizard open and shows the error | | knownTools | `string[]` | No | Tool names suggested for the matcher | | createId | `() => string` | No | Creates ids for new hooks | | labels | `Partial` | No | Overrides of the default English labels | --- # Sessions & tasks ## SessionList URL: https://sinups.github.io/ai-kit/docs/session-list ### Code ```tsx import { SessionList } from "@sinups/ai-kit"; export function Example() { return ( setSelectedId(session.id)} onRename={(session, title) => api.renameSession(session.id, title)} onPin={(session, pinned) => api.updateSession(session.id, { pinned })} onArchive={(session, archived) => api.updateSession(session.id, { archived })} onDelete={(session) => api.deleteSession(session.id)} onExport={(session) => setExporting(session)} /> ); } ``` ### Usage List past conversations for a history sidebar or page. Sessions are grouped by date (Pinned, Today, Yesterday, Previous 7 days, Previous 30 days, then by month), searched with fuzzy matching, and filtered into all, pinned or archived from a compact menu button in the header that shows the count. Rename, Pin, Archive, Delete (with confirmation) and Export actions appear only when their callbacks are set. Handles loading, error and empty states. Combine it with `SessionPreview` and `ExportDialog` inside `MasterDetail` for a full history page. ### Example: Sidebar ```tsx ``` ### Example: Loading, error and empty ```tsx <> ``` ### Example: History page ```tsx <> } detail={selected ? : null} onBack={clearSelection} /> ``` ### Example: History in a narrow widget ```tsx
``` ### API reference | Prop | Type | Required | Description | | --- | --- | --- | --- | | sessions | `SessionSummary[]` | Yes | Sessions to list, archived ones included | | loading | `boolean` | No | Shows skeleton rows | | error | `React.ReactNode` | No | Error message shown instead of the list | | onRetry | `() => void` | No | Called by the retry button of the error alert | | selectedId | `string \| null` | No | Id of the selected session | | onSelect | `(session: SessionSummary) => void` | No | Called when a session is clicked or chosen with Enter | | onRename | `(session: SessionSummary, title: string) => Promise \| void` | No | Saves a new title, the Rename action is shown only when set | | onPin | `(session: SessionSummary, pinned: boolean) => void` | No | Pins or unpins a session, the Pin action is shown only when set | | onArchive | `(session: SessionSummary, archived: boolean) => void` | No | Archives or restores a session, the Archive action is shown only when set | | onDelete | `(session: SessionSummary) => Promise \| void` | No | Deletes a session after confirmation, the Delete action is shown only when set | | onExport | `(session: SessionSummary) => void` | No | Opens the export flow for a session, the Export action is shown only when set | | filter | `SessionFilter` | No | Selected filter, uncontrolled with `all` by default | | onFilterChange | `(filter: SessionFilter) => void` | No | Called when the filter changes | | toolbar | `React.ReactNode` | No | Content rendered next to the search input, for example a new chat button | | withSearch | `boolean \| 'on-demand'` | No | Search field: `true` shows it above the list, `on-demand` shows a search button that opens a borderless field, `true` by default | | withFilter | `boolean` | No | Shows the compact All / Pinned / Archived filter, `true` by default | | now | `Date` | No | Reference time for date groups and relative times, the current time by default | | locale | `string` | No | Locale of relative times and month names, `en` by default | | labels | `Partial` | No | Overrides of the default English labels | | className | `string` | No | Class name added to the root element | | style | `React.CSSProperties` | No | Inline styles added to the root element | ## SessionPreview URL: https://sinups.github.io/ai-kit/docs/session-preview ### Code ```tsx import { SessionPreview } from "@sinups/ai-kit"; export function Example() { return ( openChat(session.id)} onExport={(session) => setExporting(session)} /> ); } ``` ### Usage Preview a past conversation before resuming it: the header shows the title, relative time, model, branch, token count and cost when present, followed by the first `maxMessages` messages (6 by default). Resume and Export buttons render only when their callbacks are set; `loading` and `error` replace the messages. ### Example: Conversation ```tsx ``` ### Example: Loading ```tsx ``` ### API reference | Prop | Type | Required | Description | | --- | --- | --- | --- | | session | `SessionSummary` | Yes | Session shown in the header | | messages | `ChatMessage[]` | No | Conversation of the session, only the first `maxMessages` are rendered | | loading | `boolean` | No | Shows skeletons instead of the messages | | error | `React.ReactNode` | No | Error message shown instead of the messages | | onRetry | `() => void` | No | Called by the retry button of the error alert | | onResume | `(session: SessionSummary) => void` | No | Continues the session, the button is rendered only when set | | onExport | `(session: SessionSummary) => void` | No | Opens the export flow, the button is rendered only when set | | maxMessages | `number` | No | Number of messages rendered, `6` by default | | now | `Date` | No | Reference time for the relative time, the current time by default | | locale | `string` | No | Locale of numbers and relative time, `en` by default | | labels | `Partial` | No | Overrides of the default English labels | | className | `string` | No | Class name added to the root element | | style | `React.CSSProperties` | No | Inline styles added to the root element | ## ExportDialog URL: https://sinups.github.io/ai-kit/docs/export-dialog ### Code ```tsx import { ExportDialog } from "@sinups/ai-kit"; export function Example() { return ( ); } ``` ### Usage Export a conversation as Markdown, JSON or plain text. The user chooses the format and whether to include tool calls, thinking and timestamps, sees a live preview, and can copy or download the result. By default the file downloads through a temporary link; pass `onDownload` to save it yourself. The pure `exportConversation` helper is exported for server-side exports. ### Example: Export dialog ```tsx saveFile(filename, content, mimeType)} /> ``` ### API reference | Prop | Type | Required | Description | | --- | --- | --- | --- | | opened | `boolean` | Yes | Whether the dialog is open | | onClose | `() => void` | Yes | Called when the dialog closes | | messages | `ChatMessage[]` | Yes | Conversation to export | | title | `string` | No | Conversation title, written at the top of the export and used for the file name | | defaultFormat | `ExportFormat` | No | Format selected when the dialog opens, `markdown` by default | | defaultOptions | `Partial` | No | Options selected when the dialog opens, tool calls and timestamps on, thinking off by default | | onDownload | `(filename: string, content: string, mimeType: string) => void` | No | Saves the file, downloads it through a temporary link by default | | previewHeight | `number` | No | Maximum height of the preview, `320` by default | | labels | `Partial` | No | Overrides of the default English labels | ## BackgroundTasksPanel URL: https://sinups.github.io/ai-kit/docs/background-tasks-panel ### Code ```tsx import { BackgroundTasksPanel, type BackgroundTask } from "@sinups/ai-kit"; export function Tasks({ tasks }: { tasks: BackgroundTask[] }) { return (
runner.stop(task.id)} onRetryTask={(task) => runner.retry(task.id)} onRemove={(task) => runner.remove(task.id)} />
); } ``` ### Usage Monitor background work of an agent session: shell commands, subagents, remote jobs and workflows. `TaskList` and `TaskDetail` sit side by side in `MasterDetail` from 720px of width; narrower, the detail replaces the list with a back action. The selected task can be a subtask, controlled with `selectedId` or uncontrolled with `defaultSelectedId`. `BackgroundTasksDrawer` opens the same panel in a drawer that slides from the bottom on small screens, usually from a `TaskStatusPill`. ### Example: Wide ```tsx ``` ### Example: Narrow ```tsx
``` ### Example: Drawer from a status pill ```tsx <> ``` ### API reference | Prop | Type | Required | Description | | --- | --- | --- | --- | | tasks | `BackgroundTask[]` | Yes | Top-level tasks with nested `children` | | selectedId | `string \| null` | No | Id of the task shown in the detail, controlled; may point at a subtask | | defaultSelectedId | `string \| null` | No | Initial selected task id when uncontrolled | | onSelectedIdChange | `(id: string \| null) => void` | No | Called when the selected task changes | | onStop | `TaskAction` | No | Stops a queued or running task | | onRetryTask | `TaskAction` | No | Retries a failed or cancelled task | | onRemove | `TaskAction` | No | Removes a finished task | | loading | `boolean` | No | Shows skeleton rows in the list | | error | `React.ReactNode` | No | Error message shown instead of the list | | onRetry | `() => void` | No | Called by the retry button of the error alert | | onSteer | `(taskId: string, text: string) => void \| Promise` | No | Sends an instruction to a running agent task from its detail | | maxVisible | `number` | No | Shows at most this many tasks in the list with a summary of the rest | | searchAutofocus | `boolean` | No | Marks the task search input with `data-autofocus` so an enclosing Drawer or Modal focuses it on open | | header | `React.ReactNode` | No | Content at the start of the first row, on the same inner edge as the list, for example a panel title | | compact | `boolean` | No | Ghost list toolbar with the search behind an icon; follows the width when omitted | | listWidth | `number` | No | List pane width in px when wide, `340` by default | | breakpoint | `number` | No | Component width in px from which list and detail sit side by side, `720` by default | | labels | `Partial` | No | Overrides of the default English labels | | className | `string` | No | Class name added to the root element | | style | `React.CSSProperties` | No | Inline styles added to the root element | ## TaskList URL: https://sinups.github.io/ai-kit/docs/task-list ### Code ```tsx import { TaskList } from "@sinups/ai-kit"; export function Example() { return ( setSelectedId(task.id)} onStop={stop} onRetryTask={retry} onRemove={remove} /> ); } ``` ### Usage List background tasks grouped into Running, Queued and Finished, with kind icon, status, live elapsed time, last activity, progress and subtask count. Search and a kind filter (shown when tasks have more than one kind) narrow the list. Stop, Retry and Remove appear only when their callbacks are set and the task status allows them. Handles loading, error and empty states. ### Example: Tasks ```tsx ``` ### Example: Loading, error and empty ```tsx <> ``` ### API reference | Prop | Type | Required | Description | | --- | --- | --- | --- | | tasks | `BackgroundTask[]` | Yes | Top-level tasks, subtasks are counted on the row and shown in `TaskDetail` | | selectedId | `string \| null` | No | Id of the selected task | | onSelect | `(task: BackgroundTask) => void` | No | Called when a task is clicked or chosen with Enter | | onStop | `TaskAction` | No | Stops a queued or running task, the action is shown only when set | | onRetryTask | `TaskAction` | No | Retries a failed or cancelled task, the action is shown only when set | | onRemove | `TaskAction` | No | Removes a finished task from the list, the action is shown only when set | | loading | `boolean` | No | Shows skeleton rows | | error | `React.ReactNode` | No | Error message shown instead of the list | | onRetry | `() => void` | No | Called by the retry button of the error alert | | withSearch | `boolean` | No | Shows the search input, `true` by default | | searchAutofocus | `boolean` | No | Marks the search input with `data-autofocus` so an enclosing Drawer or Modal focuses it on open | | compact | `boolean` | No | One ghost toolbar row: search behind an icon and the kind filter without a border | | withKindFilter | `boolean` | No | Shows the kind filter when tasks have more than one kind, `true` by default | | maxVisible | `number` | No | Shows at most this many tasks, running first, with a summary of the rest and a Show all button; ignored while searching | | recentWindowMs | `number` | No | Tasks completed within this many ms are highlighted, `30000` by default; `0` turns it off | | labels | `Partial` | No | Overrides of the default English labels | | className | `string` | No | Class name added to the root element | | style | `React.CSSProperties` | No | Inline styles added to the root element | ## TaskDetail URL: https://sinups.github.io/ai-kit/docs/task-detail ### Code ```tsx import { TaskDetail } from "@sinups/ai-kit"; export function Example() { return ( setSelectedId(subtask.id)} /> ); } ``` ### Usage Show one background task: header with kind, status, elapsed time, tokens and tool uses, progress, the failure message, and tabs for the output log and subtasks. The log follows new output while scrolled to the bottom, offers a jump to the latest line otherwise, and can be copied. Stop is shown for queued and running tasks, Retry for failed and cancelled ones. ### Example: Running agent ```tsx ``` ### Example: Failed task ```tsx ``` ### API reference | Prop | Type | Required | Description | | --- | --- | --- | --- | | task | `BackgroundTask` | Yes | Task to show | | onStop | `(task: BackgroundTask) => void \| Promise` | No | Stops the task, the button is shown for queued and running tasks when set | | onRetryTask | `(task: BackgroundTask) => void \| Promise` | No | Retries the task, the button is shown for failed and cancelled tasks when set | | onSelectSubtask | `(task: BackgroundTask) => void` | No | Called when a subtask is chosen in the Subtasks tab | | defaultTab | `'output' \| 'subtasks' \| 'messages'` | No | Tab opened first, `output` by default | | outputHeight | `number \| string` | No | Maximum height of the expanded output log, `320` by default | | outputLines | `number` | No | Output lines shown from the end before "Show all", `20` by default | | allTasks | `BackgroundTask[]` | No | Every task of the tree, used to name the tasks in `blockedBy` | | onSteer | `(taskId: string, text: string) => void \| Promise` | No | Sends an instruction to a running agent task, renders the instruction field when set | | labels | `Partial` | No | Overrides of the default English labels | | className | `string` | No | Class name added to the root element | | style | `React.CSSProperties` | No | Inline styles added to the root element | ## AgentTree URL: https://sinups.github.io/ai-kit/docs/agent-tree ### Code ```tsx import { AgentTree } from "@sinups/ai-kit"; export function Example() { return setSelectedId(task.id)} />; } ``` ### Usage Show an agent and the subagents and commands it started as a tree, with status, last activity and live elapsed time on every node. Nodes with children can be collapsed; all are expanded by default unless listed in `defaultCollapsedIds`. Build the nested structure from a flat list with `buildTaskTree`. ### Example: Agent with subagents ```tsx ``` ### API reference | Prop | Type | Required | Description | | --- | --- | --- | --- | | tasks | `BackgroundTask[]` | Yes | Root tasks with nested `children` | | selectedId | `string \| null` | No | Id of the selected task | | onSelect | `(task: BackgroundTask) => void` | No | Called when a node is clicked or chosen with Enter | | defaultCollapsedIds | `string[]` | No | Ids of collapsed nodes when uncontrolled, every node is expanded by default | | labels | `Partial` | No | Overrides of the default English labels | | className | `string` | No | Class name added to the root element | | style | `React.CSSProperties` | No | Inline styles added to the root element | ## TaskStatusPill URL: https://sinups.github.io/ai-kit/docs/task-status-pill ### Code ```tsx import { TaskStatusPill, flattenTaskTree } from "@sinups/ai-kit"; export function Example() { return ; } ``` ### Usage Summarize background tasks in a status bar or header, for example `2 running · 1 failed`. Pass `flattenTaskTree(tasks)` to count subtasks too, or a precomputed `summary`. The pill is blue while tasks run or wait and neutral otherwise; only the failed count is colored as an error. With `onOpen` the pill is a button, typically opening `BackgroundTasksDrawer`. It renders nothing without tasks unless `showWhenEmpty` is set. ### Example: States ```tsx <> ``` ### API reference | Prop | Type | Required | Description | | --- | --- | --- | --- | | tasks | `BackgroundTask[]` | No | Tasks to summarize, nested `children` included | | countSubtasks | `boolean` | No | Counts subtasks from `children` as well as top-level tasks, `true` by default | | summary | `BackgroundTaskSummary` | No | Precomputed summary, takes precedence over `tasks` | | onOpen | `() => void` | No | Called on click, renders the pill as a button when set | | showWhenEmpty | `boolean` | No | Renders `pillEmpty` instead of nothing when there are no tasks, `false` by default | | labels | `Partial` | No | Overrides of the default English labels | | className | `string` | No | Class name added to the root element | | style | `React.CSSProperties` | No | Inline styles added to the root element | ## TaskElapsed URL: https://sinups.github.io/ai-kit/docs/task-elapsed ### Code ```tsx import { Group, Text } from "@mantine/core"; import { TaskElapsed, TaskKindIcon, type BackgroundTask } from "@sinups/ai-kit"; export function TaskRow({ task }: { task: BackgroundTask }) { return ( {task.title} ); } ``` ### Usage Small pieces of task metadata for your own task rows. TaskElapsed prints the time between `startedAt` and `endedAt` (for example `1m 23s`) and ticks every second while the task is `running` and has no end time; it renders nothing for a task that has not started. It renders a bare `span`, so wrap it in Text for size and color. TaskKindIcon shows the icon of a task kind (`shell`, `agent`, `remote`, `workflow`) in a light gray ThemeIcon; `size` is a Mantine size, `md` by default. TaskList, TaskDetail and AgentTree use both. The underlying helpers are `getTaskElapsedMs`, `toTimestamp` and the `useNow` hook. ### Example: Kinds and elapsed time ```tsx yarn test --watch ``` ### API reference | Prop | Type | Required | Description | | --- | --- | --- | --- | | task | `Pick` | Yes | Task whose `startedAt`/`endedAt` give the elapsed time | ## AgentMessage URL: https://sinups.github.io/ai-kit/docs/agent-message ### Code ```tsx import { AgentMessage, type AgentMessageData } from "@sinups/ai-kit"; const message: AgentMessageData = { id: "m1", from: { name: "planner", color: "violet" }, to: { name: "test-runner", color: "teal" }, summary: "Run the upload queue tests after the retry change", content: "The retry logic moved into upload-queue.ts. Run yarn jest upload-queue and report failures.", timestamp: Date.now(), }; export function Example() { return ; } ``` ### Usage Show a message that one agent sent to another in a multi-agent run, for example in TaskDetail or in a team activity feed. The header shows the sender and the recipient as colored dot badges (use the same `color` for an agent everywhere) or `everyone` when `to` is omitted, and the time from `timestamp` (`formatTime` changes the format). The `summary` is always visible; `content` that differs from the summary is revealed with a Show message button, or from the start with `defaultExpanded`. Labels come from the background task labels (`labels`). ### Example: Wide ```tsx {messages.map((message) => ( ))} ``` ### Example: Narrow ```tsx
{messages.map((message) => ( ))}
``` ### API reference | Prop | Type | Required | Description | | --- | --- | --- | --- | | message | `AgentMessageData` | Yes | Message between agents | | defaultExpanded | `boolean` | No | Shows the full content from the start | | formatTime | `(date: Date) => string` | No | Formats the timestamp, `HH:MM` in the user locale by default | | labels | `Partial` | No | Overrides of the default English labels | | className | `string` | No | Class name added to the root element | | style | `React.CSSProperties` | No | Inline styles added to the root element | --- # Diff ## DiffReview URL: https://sinups.github.io/ai-kit/docs/diff-review ### Code ```tsx "use client"; import { useState } from "react"; import { DiffReview, type FileChange, type FileDecision } from "@sinups/ai-kit"; export function Review({ changes }: { changes: FileChange[] }) { const [decisions, setDecisions] = useState>({}); return (
{ await agent.applyFile(change.path); setDecisions((current) => ({ ...current, [change.path]: "accepted" })); }} onReject={async (change) => { await agent.revertFile(change.path); setDecisions((current) => ({ ...current, [change.path]: "rejected" })); }} onAcceptAll={() => agent.applyAll()} />
); } ``` ### Usage Review the files an agent changed before applying them. `DiffFileList` and `DiffFileView` sit side by side in `MasterDetail` from 900px of width; narrower, the diff replaces the list. Move between files with Previous/Next or `j`/`k` while focus is inside the review, mark files as viewed with a progress count, and accept or reject each file or all of them; the buttons appear only for the callbacks you pass and show pending state. `DiffReviewModal` shows the same review in a modal that fills small screens. ### Example: Wide ```tsx ``` ### Example: Narrow ```tsx
``` ### Example: Read-only, split view ```tsx ``` ### Example: Modal ```tsx ``` ### API reference | Prop | Type | Required | Description | | --- | --- | --- | --- | | changes | `FileChange[]` | No | Changed files in review order, ignored when `sources` is set | | sources | `DiffSource[]` | No | Alternative change sets, for example uncommitted changes and each agent turn, switched above the list | | sourceId | `string` | No | Id of the shown source, controlled | | defaultSourceId | `string` | No | Initial source id when uncontrolled, the first source by default | | onSourceChange | `(id: string) => void` | No | Called when another source is picked; the first file of that source is opened | | selectedPath | `string \| null` | No | Path of the open file, controlled | | defaultSelectedPath | `string \| null` | No | Initial open file when uncontrolled, the first file by default | | onSelectedPathChange | `(path: string \| null) => void` | No | Called when another file is opened | | viewedPaths | `string[]` | No | Paths marked as viewed, controlled | | defaultViewedPaths | `string[]` | No | Initial viewed paths when uncontrolled | | onViewedPathsChange | `(paths: string[]) => void` | No | Called when a file is marked or unmarked as viewed | | decisions | `Record` | No | Accept or reject decision per path, shown in the list and on the buttons | | onAccept | `FileChangeAction` | No | Accepts one file, renders the Accept button when set | | onReject | `FileChangeAction` | No | Rejects one file, renders the Reject button when set | | onAcceptAll | `() => void \| Promise` | No | Accepts every file, renders the Accept all button when set | | onRejectAll | `() => void \| Promise` | No | Rejects every file, renders the Reject all button when set | | defaultView | `DiffFileListView` | No | Initial file list layout, `list` by default; Prev/Next follow the order of the shown layout | | highlighter | `SyntaxHighlighter` | No | Colors the diff with this highlighter | | defaultMode | `DiffViewMode` | No | Initial diff layout, `unified` by default | | header | `React.ReactNode` | No | Content at the start of the first row, on the same inner edge as the controls, for example a panel title | | compact | `boolean` | No | Lighter controls for side panels: ghost buttons and the file search behind an icon; follows the width when omitted | | searchAutofocus | `boolean` | No | Marks the file search with `data-autofocus` so an enclosing Drawer or Modal focuses it, `true` by default | | withHotkeys | `boolean` | No | Enables `j`/`k` to move between files while focus is inside the review, `true` by default | | loading | `boolean` | No | Shows skeleton rows in the file list | | error | `React.ReactNode` | No | Error message shown instead of the file list | | onRetry | `() => void` | No | Called by the retry button of the error alert | | listWidth | `number` | No | File list width in px when wide, `320` by default | | breakpoint | `number` | No | Component width in px from which the list and the diff sit side by side, `900` by default | | labels | `Partial` | No | Overrides of the default English labels | | className | `string` | No | Class name added to the root element | | style | `React.CSSProperties` | No | Inline styles added to the root element | ## DiffFileList URL: https://sinups.github.io/ai-kit/docs/diff-file-list ### Code ```tsx import { DiffFileList } from "@sinups/ai-kit"; export function Example() { return ( setSelectedPath(change.path)} viewedPaths={viewedPaths} decisions={decisions} /> ); } ``` ### Usage List changed files with their status (added, modified, deleted, renamed), added and removed line counts, a viewed check and the accept or reject decision. Switch between a flat list and a folder tree, search by path and filter by status. Handles loading and error states. ### Example: List ```tsx ``` ### Example: Folder tree ```tsx ``` ### API reference | Prop | Type | Required | Description | | --- | --- | --- | --- | | changes | `FileChange[]` | Yes | Changed files | | selectedPath | `string \| null` | No | Path of the selected file | | onSelect | `(change: FileChange) => void` | No | Called when a file is clicked or chosen with Enter | | viewedPaths | `string[]` | No | Paths marked as viewed, shown with a check | | decisions | `Record` | No | Accept or reject decision per path | | view | `DiffFileListView` | No | Layout, controlled | | defaultView | `DiffFileListView` | No | Initial layout when uncontrolled, `list` by default | | onViewChange | `(view: DiffFileListView) => void` | No | Called when the user switches between list and tree | | compact | `boolean` | No | One toolbar row: search behind an icon, ghost status filter and view toggles | | searchAutofocus | `boolean` | No | Marks the search field, or the search button when compact, with `data-autofocus` for an enclosing Drawer or Modal | | loading | `boolean` | No | Shows skeleton rows | | error | `React.ReactNode` | No | Error message shown instead of the files | | onRetry | `() => void` | No | Called by the retry button of the error alert | | labels | `Partial` | No | Overrides of the default English labels | | className | `string` | No | Class name added to the root element | | style | `React.CSSProperties` | No | Inline styles added to the root element | ## DiffFileView URL: https://sinups.github.io/ai-kit/docs/diff-file-view ### Code ```tsx import { DiffFileView } from "@sinups/ai-kit"; export function Example() { return ( ); } ``` ### Usage Show the diff of one file as unified or split rows with word-level highlights of what changed inside a line. Unchanged runs longer than `contextLines` collapse and expand on click. The split layout is available from `splitMinWidth` (720px of width). Binary, deleted, empty and renamed-without-changes files get dedicated states. `headerActions` adds controls to the header, for example a Viewed checkbox. ### Example: Unified and split ```tsx ``` ### Example: Narrow ```tsx
``` ### Example: Deleted, renamed and binary files ```tsx <> ``` ### API reference | Prop | Type | Required | Description | | --- | --- | --- | --- | | change | `FileChange` | Yes | File change to show | | mode | `DiffViewMode` | No | Layout, controlled | | defaultMode | `DiffViewMode` | No | Initial layout when uncontrolled, `unified` by default | | onModeChange | `(mode: DiffViewMode) => void` | No | Called when the user switches the layout | | contextLines | `number` | No | Unchanged lines kept around each change, `3` by default | | splitMinWidth | `number` | No | Component width in px from which the split layout is available, `720` by default | | highlighter | `SyntaxHighlighter` | No | Colors the code with this highlighter; the language is `change.language` or the file extension | | maxBytes | `number` | No | Files larger than this many bytes show a stub with a Show anyway button, `512000` by default | | headerActions | `React.ReactNode` | No | Content rendered at the end of the header, for example a Viewed checkbox | | labels | `Partial` | No | Overrides of the default English labels | | className | `string` | No | Class name added to the root element | | style | `React.CSSProperties` | No | Inline styles added to the root element | ## DiffStats URL: https://sinups.github.io/ai-kit/docs/diff-stats ### Code ```tsx import { Group, Text } from "@mantine/core"; import { DiffStats, FileIcon, FileStatusBadge, type FileChangeStatus } from "@sinups/ai-kit"; const statusLabels: Record = { added: "Added", modified: "Modified", deleted: "Deleted", renamed: "Renamed", }; export function ChangedFileRow({ path, status, additions, deletions, }: { path: string; status: FileChangeStatus; additions: number; deletions: number; }) { return ( {path} ); } ``` ### Usage Building blocks of the diff file rows, exported for summaries outside DiffReview, such as a turn summary or a commit dialog. DiffStats prints `+N −M` in green and red monospace; `size` is the text size, `xs` by default. FileStatusBadge is a one-letter badge (A, M, D, R) colored by status, with the full `label` in a tooltip and as the accessible name; `untracked` shows a teal U instead. FileIcon shows a file type icon for TypeScript, JavaScript and JSON files and a generic file icon otherwise; `size` is in px. `getFileStatusLabel` picks the status label from diff labels, and `computeFileStats` / `summarizeChanges` count lines for you. ### Example: File rows ```tsx src/upload/upload-queue.ts ``` ### API reference | Prop | Type | Required | Description | | --- | --- | --- | --- | | additions | `number` | Yes | Added lines | | deletions | `number` | Yes | Removed lines | | size | `MantineSize` | No | Text size, `xs` by default | --- # Settings ## ModelSettingsPanel URL: https://sinups.github.io/ai-kit/docs/model-settings-panel ### Code ```tsx "use client"; import { useState } from "react"; import { ModelSettingsPanel, type EffortLevelValue, type UsagePeriod } from "@sinups/ai-kit"; export function ModelSettings() { const [model, setModel] = useState("qwen-2.5-coder-32b"); const [effort, setEffort] = useState("high"); const [style, setStyle] = useState("default"); const [period, setPeriod] = useState("week"); return (
); } ``` ### Usage Assemble a model settings screen in `SettingsLayout`: Model (model select and `EffortSelector`), Output style (`OutputStylePicker`), Usage (`UsagePanel`) and Status (`StatusPanel`). A section appears only when its prop is passed, so the same panel works for a model picker alone or a full settings page. The active section can be controlled with `activeSection` and `onActiveSectionChange`. Navigation sits beside the content when wide and turns into a section picker in a narrow widget. ### Example: Wide ```tsx ``` ### Example: Narrow ```tsx
``` ### API reference | Prop | Type | Required | Description | | --- | --- | --- | --- | | models | `ModelOption[]` | No | Models offered in the model select | | model | `string \| null` | No | Id of the selected model | | onModelChange | `(modelId: string) => void` | No | Called with the id of the picked model | | effort | `Omit` | No | Reasoning effort control, rendered in the model section | | outputStyle | `Omit` | No | Output style section | | usage | `UsagePanelProps` | No | Usage section | | status | `StatusPanelProps` | No | Status section | | activeSection | `ModelSettingsSection` | No | Controlled active section | | onActiveSectionChange | `(section: ModelSettingsSection) => void` | No | Called with the picked section | | defaultActiveSection | `ModelSettingsSection` | No | Section shown first in uncontrolled mode, the first available one by default | | labels | `Partial` | No | Overrides of the default English labels | | className | `string` | No | Class name added to the root element | | style | `React.CSSProperties` | No | Inline styles added to the root element | ## EffortSelector URL: https://sinups.github.io/ai-kit/docs/effort-selector ### Code ```tsx "use client"; import { useState } from "react"; import { EffortSelector, type EffortLevelValue } from "@sinups/ai-kit"; export function Example() { const [effort, setEffort] = useState("high"); const [thinking, setThinking] = useState(true); return ( ); } ``` ### Usage Pick the reasoning effort: Low, Medium, High or Max by default, or your own `levels`. The selected level's description is shown under the control. It is a segmented control from `breakpoint` (380px of its own width) and a select when narrower; `variant="inline"` renders a compact menu button for a composer toolbar. Pass `thinking` together with `onThinkingChange` to add an extended thinking switch. ### Example: Wide and narrow ```tsx ``` ### Example: Inline in a toolbar ```tsx ``` ### API reference | Prop | Type | Required | Description | | --- | --- | --- | --- | | value | `EffortLevelValue` | Yes | Selected level value | | onChange | `(value: EffortLevelValue) => void` | Yes | Called with the picked level value | | levels | `EffortLevel[]` | No | Levels in order from the least to the most effort, `low`, `medium`, `high`, `max` by default | | thinking | `boolean` | No | State of the extended thinking switch; the switch is rendered only together with `onThinkingChange` | | onThinkingChange | `(thinking: boolean) => void` | No | Called when extended thinking is toggled | | variant | `'default' \| 'inline'` | No | `default` renders a labeled control, `inline` renders a compact menu button for a composer toolbar, `default` by default | | breakpoint | `number` | No | Component width in px from which the levels are a segmented control instead of a select, `380` by default | | disabled | `boolean` | No | Disables the control | | labels | `Partial` | No | Overrides of the default English labels | | className | `string` | No | Class name added to the root element | | style | `React.CSSProperties` | No | Inline styles added to the root element | ## OutputStylePicker URL: https://sinups.github.io/ai-kit/docs/output-style-picker ### Code ```tsx "use client"; import { useState } from "react"; import { OutputStylePicker, type OutputStyle } from "@sinups/ai-kit"; const styles: OutputStyle[] = [ { id: "default", name: "Default", description: "Concise answers focused on the task", example: "Fixed the retry in client.ts." }, { id: "explanatory", name: "Explanatory", description: "Explains the trade-offs behind each change" }, ]; export function Example() { const [style, setStyle] = useState("default"); return ; } ``` ### Usage Choose how the agent writes its answers. Each style is a radio card with its name, description and an optional sample answer; cards form one to three columns depending on the component width. `withExamples={false}` keeps the cards short. ### Example: Wide ```tsx ``` ### Example: Narrow without examples ```tsx ``` ### API reference | Prop | Type | Required | Description | | --- | --- | --- | --- | | styles | `OutputStyle[]` | Yes | Styles to choose from | | value | `string \| null` | Yes | Id of the selected style | | onChange | `(id: string) => void` | Yes | Called with the id of the picked style | | label | `React.ReactNode` | No | Group label | | description | `React.ReactNode` | No | Group description under the label | | labelledBy | `string` | No | Id of an element that names the group when `label` is not set, for example a section heading | | withExamples | `boolean` | No | Shows the example answer on each card, `true` by default | | disabled | `boolean` | No | Disables every card | | className | `string` | No | Class name added to the root element | | style | `React.CSSProperties` | No | Inline styles added to the root element | ## UsagePanel URL: https://sinups.github.io/ai-kit/docs/usage-panel ### Code ```tsx "use client"; import { useState } from "react"; import { UsagePanel, type UsagePeriod } from "@sinups/ai-kit"; export function Example() { const [period, setPeriod] = useState("week"); return ( ); } ``` ### Usage Show token and cost usage for a day, week or month: period totals, plan limits with progress bars and reset countdowns, usage split by model and usage per day. Limit bars turn yellow at `warnAt` (75%) and red at `dangerAt` (90%). The period switch appears only with `onPeriodChange`. Handles `loading` with skeletons and `error` with an optional retry. ### Example: Wide ```tsx ``` ### Example: Narrow ```tsx
``` ### Example: Loading and error ```tsx <> ``` ### API reference | Prop | Type | Required | Description | | --- | --- | --- | --- | | period | `UsagePeriod` | Yes | Selected period | | onPeriodChange | `(period: UsagePeriod) => void` | No | Called with the picked period; the period switch is hidden when omitted | | periods | `UsagePeriod[]` | No | Periods offered by the switch, `day`, `week`, `month` by default | | summary | `UsageSummary` | No | Totals of the period | | limits | `UsageLimit[]` | No | Plan limits with their progress | | models | `ModelUsage[]` | No | Usage split by model | | daily | `DailyUsage[]` | No | Usage per day, rendered as horizontal bars | | warnAt | `number` | No | Ratio of a limit at which its bar turns yellow, `0.75` by default | | dangerAt | `number` | No | Ratio of a limit at which its bar turns red, `0.9` by default | | currency | `string` | No | Currency of all costs, `USD` by default | | loading | `boolean` | No | Shows skeletons instead of data | | error | `React.ReactNode` | No | Error message shown instead of data | | onRetry | `() => void` | No | Called by the retry button of the error alert | | now | `Date` | No | Time used to compute reset countdowns, the current time by default | | locale | `string` | No | Locale of numbers, costs, dates and durations, `en-US` by default | | withTitle | `boolean` | No | Shows the heading, `true` by default | | labels | `Partial` | No | Overrides of the default English labels | | className | `string` | No | Class name added to the root element | | style | `React.CSSProperties` | No | Inline styles added to the root element | ## StatusPanel URL: https://sinups.github.io/ai-kit/docs/status-panel ### Code ```tsx import { StatusPanel } from "@sinups/ai-kit"; export function Example() { return ( ); } ``` ### Usage Summarize the agent environment, like a `/status` command: version, model, account and organization, working directory, MCP servers counted by status, loaded memory files and context window usage with `ContextUsage`. Add rows with `items` and buttons with `actions`; an action that returns a promise shows a loader until it settles. ### Example: Wide ```tsx ``` ### Example: Narrow ```tsx
``` ### API reference | Prop | Type | Required | Description | | --- | --- | --- | --- | | version | `string` | No | Agent or app version | | model | `React.ReactNode` | No | Name of the active model | | account | `{ email?: string; name?: string; plan?: string }` | No | Signed-in account | | organization | `string` | No | Organization of the account | | cwd | `string` | No | Working directory of the session | | mcpServers | `StatusMcpServer[]` | No | MCP servers, summarized as counters per status | | memoryFiles | `StatusMemoryFile[]` | No | Memory files loaded into the context | | context | `{ used: number; total: number; segments?: ContextUsageSegment[]; onCompact?: () => void; }` | No | Context window usage | | items | `{ label: React.ReactNode; value: React.ReactNode }[]` | No | Extra rows appended after the built-in ones | | actions | `StatusAction[]` | No | Buttons under the summary, for example `Doctor` or `Log out` | | withTitle | `boolean` | No | Shows the heading, `true` by default | | labels | `Partial` | No | Overrides of the default English labels | | className | `string` | No | Class name added to the root element | | style | `React.CSSProperties` | No | Inline styles added to the root element | ## MemoryPanel URL: https://sinups.github.io/ai-kit/docs/memory-panel ### Code ```tsx "use client"; import { useState } from "react"; import { MemoryPanel, type MemoryFile } from "@sinups/ai-kit"; export function Example({ files, loading, error, reload, save, reveal, }: { files: MemoryFile[]; loading: boolean; error?: string; reload: () => void; save: (file: MemoryFile, content: string) => Promise; reveal: (file: MemoryFile) => void; }) { const [selectedId, setSelectedId] = useState(null); return ( ); } ``` ### Usage Let the user read and edit the instruction files the agent loads as memory. Files are grouped by scope in a fixed order: User, Project, Local and Agent (agent files show the agent name). The list is searchable by path, agent name and content. Selecting a file opens MemoryFileDetail with the rendered Markdown. From `breakpoint` (720px of panel width by default) the list and the file sit side by side, with a placeholder when nothing is selected; below it the file replaces the list and a back button returns. The panel fills its container, so give it a height. Edit appears only with `onSave`; a rejected save keeps the editor open with the message, and leaving a file with unsaved edits asks for confirmation. `onOpenLocation` and `onCreate` add Open location and New file actions. States: `loading` shows skeleton rows, `error` shows an alert with Retry (`onRetry`), an empty `files` list shows an empty state. `selectedId` can be controlled, for example to open the file named in a Saved to memory notice. Helpers: `sortMemoryFiles`, `matchesMemoryQuery`, `getMemoryFileName`, `formatMemoryUpdatedAt`. ### Example: Wide ```tsx ``` ### Example: Narrow ```tsx
``` ### Example: Loading, error, empty ```tsx <> ``` ### API reference | Prop | Type | Required | Description | | --- | --- | --- | --- | | files | `MemoryFile[]` | Yes | Memory files of all scopes | | loading | `boolean` | No | Shows skeleton rows in the list | | error | `React.ReactNode` | No | Error message shown instead of the list | | onRetry | `() => void` | No | Called by the retry button of the error alert | | selectedId | `string \| null` | No | Selected file id, uncontrolled when omitted; set it to open a file, for example from a "Saved to memory" notice | | onSelectedIdChange | `(id: string \| null) => void` | No | Called when the selected file changes | | onSave | `(file: MemoryFile, content: string) => void \| Promise` | No | Saves edited content, Edit is available only when set; a rejection keeps the editor open with the message | | onOpenLocation | `(file: MemoryFile) => void` | No | Shows Open location for each file, for example to reveal it in the file manager or editor | | onCreate | `() => void` | No | Shows a New file button | | breakpoint | `number` | No | Component width in px from which list and detail sit side by side, `720` by default | | listWidth | `number` | No | List pane width in px when wide, `340` by default | | now | `number` | No | Current time used for relative update times, `Date.now()` by default | | locale | `string` | No | Locale of relative update times, `en` by default | | labels | `Partial` | No | Overrides of the default English labels of the panel | | className | `string` | No | Class name added to the root element | | style | `React.CSSProperties` | No | Inline styles added to the root element, give the panel a height | ## MemoryFileDetail URL: https://sinups.github.io/ai-kit/docs/memory-file-detail ### Code ```tsx "use client"; import { useState } from "react"; import { MemoryFileDetail, type MemoryFile } from "@sinups/ai-kit"; export function Example({ file, save, }: { file: MemoryFile; save: (file: MemoryFile, content: string) => Promise; }) { const [editing, setEditing] = useState(false); return ( setEditing(true)} onCancelEdit={() => setEditing(false)} onSave={save} onSaved={() => setEditing(false)} /> ); } ``` ### Usage One memory file on its own: file name, scope badge, path, relative update time and the content rendered as Markdown. Use it where MemoryPanel is too much, for example after a Saved to memory notice. Editing is controlled: `editing` switches to a text editor that starts from `file.content`; the Edit button appears when both `onEdit` and `onSave` are set. Save calls `onSave` and shows a loader; a rejection keeps the editor open with the message, success calls `onSaved`. `onDirtyChange` reports unsaved edits so you can confirm before leaving. An empty file shows `This file is empty.`. `now` and `locale` control the relative update time. ### Example: View and edit ```tsx ``` ### API reference | Prop | Type | Required | Description | | --- | --- | --- | --- | | file | `MemoryFile` | Yes | File to show | | editing | `boolean` | No | Renders the editor instead of the rendered Markdown; the editor starts from `file.content` when it mounts | | onEdit | `(file: MemoryFile) => void` | No | Opens the editor, the Edit button is shown only when set together with `onSave` | | onCancelEdit | `() => void` | No | Closes the editor without saving | | onSave | `(file: MemoryFile, content: string) => void \| Promise` | No | Saves the edited content; the editor stays open and shows the rejection message when the promise rejects | | onSaved | `() => void` | No | Called after a successful save | | onDirtyChange | `(dirty: boolean) => void` | No | Reports whether the editor holds unsaved changes | | onOpenLocation | `(file: MemoryFile) => void` | No | Shows the Open location button | | now | `number` | No | Current time used for the relative update time, `Date.now()` by default | | locale | `string` | No | Locale of the relative update time, `en` by default | | labels | `Partial` | No | Overrides of the default English labels | | className | `string` | No | Class name added to the root element | | style | `React.CSSProperties` | No | Inline styles added to the root element | ## CommandsHelp URL: https://sinups.github.io/ai-kit/docs/commands-help ### Code ```tsx import { CommandsHelp } from "@sinups/ai-kit"; export function Example() { return ( ", description: "Review changes in a path", group: "Code" }, { name: "compact", description: "Summarize the conversation", group: "Session" }, ]} shortcuts={[{ keys: "mod+K", description: "Open the command palette", group: "General" }]} onCommandSelect={(command) => insertIntoComposer(`/${command.name} `)} /> ); } ``` ### Usage Show a reference of slash commands and keyboard shortcuts with fuzzy search and groups. Commands show their arguments and shortcut; with `onCommandSelect` they become clickable, for example to insert the command into the composer. Tabs between commands and shortcuts appear only when `shortcuts` are given. `toPaletteCommands` turns the same commands into `CommandPalette` entries. ### Example: Wide ```tsx ``` ### Example: Narrow ```tsx
``` ### Example: In the command palette ```tsx import { useMemo, useState } from "react"; import { CommandPalette, toPaletteCommands } from "@sinups/ai-kit"; export function Example() { const [opened, setOpened] = useState(false); const paletteCommands = useMemo(() => toPaletteCommands(commands, (command) => runCommand(command)), []); return setOpened(false)} commands={paletteCommands} />; } ``` ### API reference | Prop | Type | Required | Description | | --- | --- | --- | --- | | commands | `CommandHelpItem[]` | Yes | Slash commands | | shortcuts | `ShortcutHelpItem[]` | No | Keyboard shortcuts, the tabs are hidden when there are none | | onCommandSelect | `(command: CommandHelpItem) => void` | No | Makes commands clickable, for example to insert `/name ` into the composer | | defaultTab | `HelpTab` | No | Tab shown first, `commands` by default | | labels | `Partial` | No | Overrides of the default English labels | | className | `string` | No | Class name added to the root element | | style | `React.CSSProperties` | No | Inline styles added to the root element | ## AiKitProvider URL: https://sinups.github.io/ai-kit/docs/ai-kit-provider ### Code ```tsx "use client"; import "@mantine/core/styles.css"; import "@sinups/ai-kit/styles.css"; import { MantineProvider } from "@mantine/core"; import { AgentChat, AiKitProvider, type AgentChatProps } from "@sinups/ai-kit"; export function App(chat: AgentChatProps) { return ( ); } ``` ### Usage Apply the kit theme to a subtree. Render it inside your MantineProvider around the kit components; host components outside it keep the host theme. Settings: `accent` sets the primary color (gray, blue, indigo, violet, grape, pink), `radius` sets the default radius (sharp, default, round), `density` sets control heights and paddings (default, compact), `colorScheme` switches the color scheme of the whole app through Mantine. `theme` merges Mantine overrides on top of the kit theme, and `tokens` sets `--ae-*` variables for the subtree (keys without the `--ae-` prefix); tokens win over settings. The provider re-declares the CSS variables on its own element and on portals it opens, so menus and modals inside match. `persistKey` saves changes made through `useAiKitTheme().setSettings` to localStorage and restores them. `useAiKitTheme` returns the effective settings, the defaults from props, `setSettings`, `reset` and the resolved `aiKit` theme values; it throws outside a provider, and `useOptionalAiKitTheme` returns null instead. Wrap host UI placed inside a kit subtree in `AiKitHostScope` to give it the host theme back. See Theming (/docs/theming) for nesting and token details. ### Example: Settings ```tsx <> ``` ### API reference | Prop | Type | Required | Description | | --- | --- | --- | --- | | theme | `MantineThemeOverride` | No | Overrides applied on top of the kit theme, for example `components.Button.defaultProps` | | tokens | `AeTokenOverrides` | No | `--ae-*` token values for this subtree, keys without the `--ae-` prefix; they win over settings | | persistKey | `string` | No | localStorage key; when set, changes made through `useAiKitTheme` are saved and restored | | children | `React.ReactNode` | No | Kit components; must be rendered inside the host `MantineProvider` | | accent | `AiKitAccent` | No | Sets `theme.primaryColor` and `theme.primaryShade`; unset keeps the host primary color | | radius | `AiKitRadius` | No | Sets `theme.defaultRadius` (`sharp` → xs, `default` → md, `round` → lg); kit radii follow it | | density | `AiKitDensity` | No | Kit control heights and paddings; `default` matches the original chat surface | | colorScheme | `MantineColorScheme` | No | Applied through Mantine `useMantineColorScheme`, so it changes the color scheme of the whole app | ## AiKitThemeCustomizer URL: https://sinups.github.io/ai-kit/docs/ai-kit-theme-customizer ### Code ```tsx "use client"; import { AiKitProvider, AiKitThemeCustomizer, SettingRow, SettingsSection } from "@sinups/ai-kit"; export function AppearanceSettings() { return ( ); } ``` ### Usage Appearance settings for end users: Color (accent swatches), Radius, Density and Mode (light, dark, auto), plus a Reset button. Without `value` and `defaultValue` it reads and changes the nearest AiKitProvider, so every kit component under that provider updates at once and `persistKey` on the provider saves the choice. Pass `value` and `onChange` to control it yourself (for example to store the settings on the server and feed them to AiKitProvider as props), or `defaultValue` for uncontrolled use without a provider. `accents` limits the offered colors to a subset of `AI_KIT_ACCENTS`; every accent passes contrast checks in both schemes. `sections` hides sections, for example `{ mode: false }` when the host app owns the color scheme. It fits a 360px panel as well as a settings page column. See Theming (/docs/theming). ### Example: Wide, with a live sample ```tsx const [settings, setSettings] = useState({ accent: "blue" }); ``` ### Example: Narrow, controlled ```tsx
``` ### API reference | Prop | Type | Required | Description | | --- | --- | --- | --- | | value | `AiKitThemeSettings` | No | Controlled settings; without `value` and `defaultValue` the nearest `AiKitProvider` is used | | defaultValue | `AiKitThemeSettings` | No | Initial settings for uncontrolled use without a provider | | onChange | `(settings: AiKitThemeSettings) => void` | No | Called with the full settings after every change and after reset | | accents | `readonly AiKitAccent[]` | No | Accents offered in the Color section, a subset of `AI_KIT_ACCENTS` | | sections | `Partial>` | No | Hides sections, for example when the host controls the color scheme itself | | labels | `Partial` | No | Overrides of the default English labels | ## ChatLauncher URL: https://sinups.github.io/ai-kit/docs/chat-launcher ### Code ```tsx "use client"; import { ActionIcon } from "@mantine/core"; import { IconRefresh } from "@tabler/icons-react"; import { AgentChat, ChatLauncher, type AgentChatProps } from "@sinups/ai-kit"; export function SupportWidget({ chat, unread, reset }: { chat: AgentChatProps; unread: number; reset: () => void }) { return ( } > ); } ``` ### Usage A floating chat button in a corner of the page that opens a chat panel. The panel is not modal: the page stays interactive, Escape closes it while focus is inside, and focus returns to the button. The chat stays mounted while the panel is closed (`keepMounted`, true by default), so the conversation and a running stream survive closing. Place it with `position` (`bottom-right` or `bottom-left`) and `offset` (24px, a number or `{ x, y }`); size the panel with `panelWidth` (380) and `panelHeight` (640), both clamped to the viewport. When the panel does not fit, or the available width is below `fullScreenBreakpoint` (520px), it opens full screen and page scroll is frozen (`mobileFullScreen`). `unreadCount` shows a badge on the closed button. Open state can be controlled with `opened` / `onOpenedChange` or left to `defaultOpened`. It renders in a portal by default; set `withinPortal={false}` to keep it inside a container. To add the chat to a page that is not a React app, or to isolate it from the page CSS, use `mountChatLauncher(target, element, options)`: it renders into a Shadow DOM with its own MantineProvider, injects the stylesheets you pass in `styles` or `styleUrls`, and returns `unmount`. See [Embedding the launcher](/docs/launcher). The previews render the launcher with `withinPortal={false}` inside a page frame, so it measures the frame instead of the window. ### Example: Desktop and mobile ```tsx
``` ### Example: Full screen below 520px ```tsx
``` ### Example: Unread badge and keepMounted ```tsx opened && setUnread(0)} > ``` ### Mount on any page ```tsx import mantineCss from "@mantine/core/styles.css?inline"; import baseCss from "@sinups/ai-kit/styles/base.css?inline"; import launcherCss from "@sinups/ai-kit/styles/ChatLauncher.css?inline"; import chatCss from "@sinups/ai-kit/styles/AgentChat.css?inline"; import providerCss from "@sinups/ai-kit/styles/AiKitProvider.css?inline"; import { AiKitProvider, ChatLauncher, mountChatLauncher } from "@sinups/ai-kit"; const host = document.createElement("div"); document.body.append(host); const widget = mountChatLauncher( host, , { styles: [mantineCss, baseCss, launcherCss, chatCss, providerCss], colorScheme: "light", wrap: (element) => {element}, } ); // later widget.unmount(); ``` ### API reference | Prop | Type | Required | Description | | --- | --- | --- | --- | | children | `React.ReactNode` | Yes | Panel content, usually `AgentChat` | | opened | `boolean` | No | Controlled open state | | defaultOpened | `boolean` | No | Initial open state when `opened` is not controlled, `false` by default | | onOpenedChange | `(opened: boolean) => void` | No | Called when the button, the close button or Escape changes the open state | | position | `ChatLauncherPosition` | No | Corner of the viewport, `bottom-right` by default | | offset | `ChatLauncherOffset` | No | Distance from the viewport edges in px, `24` by default; drops to 12px when the panel only fits that way | | panelWidth | `number` | No | Panel width in px, never wider than the viewport, `380` by default | | panelHeight | `number` | No | Panel height in px, never taller than the viewport, `640` by default | | title | `React.ReactNode` | No | Panel header title | | headerActions | `React.ReactNode` | No | Actions in the panel header before the close button | | icon | `React.ReactNode` | No | Button icon while the panel is closed | | unreadCount | `number` | No | Unread messages shown as a badge on the closed button, hidden at `0` | | keepMounted | `boolean` | No | Keeps the panel content mounted while closed so the chat keeps its state, `true` by default | | closeOnEscape | `boolean` | No | Closes the panel on Escape while focus is inside the launcher, `true` by default | | mobileFullScreen | `boolean` | No | Opens the panel full screen when it does not fit or the available width is below `fullScreenBreakpoint`, `true` by default | | fullScreenBreakpoint | `number` | No | Available width in px below which the panel opens full screen, `520` by default | | withinPortal | `boolean` | No | Renders in a portal at the end of the document, `true` by default; turn off to position the launcher against a transformed container | | zIndex | `number` | No | Stacking order of the button and the panel, `200` by default | | labels | `Partial` | No | Overrides of the default English labels | | className | `string` | No | Class name added to the root element | | style | `React.CSSProperties` | No | Inline styles added to the root element | | classNames | `ChatLauncherClassNames` | No | Class names of inner elements | --- # Hooks and utilities URL: https://sinups.github.io/ai-kit/docs/utilities Every hook and pure function exported by @sinups/ai-kit, grouped by module, with signatures. ## Chat - `useStalled({ startedAt, lastActivityAt, stallAfterMs = 3000, paused = false }: UseStalledOptions)` - `buildElicitationContent(fields: ElicitationField[], draft: ElicitationDraft): { content: ElicitationContent; errors: Record }`: Validates every field and builds the `accept` content, skipping empty optional fields - `buildQuestionAnswer(question: QuestionConfig, draft: QuestionDraft): QuestionAnswer` - `canBrowseNewer(value: string, selectionStart: number, selectionEnd: number): boolean`: Arrow down browses back while the caret is on the last line and nothing is selected - `canBrowseOlder(value: string, selectionStart: number, selectionEnd: number): boolean`: Arrow up browses history in an empty field or with the caret at the very start - `canSubmitQuestion(question: QuestionConfig, draft: QuestionDraft): boolean` - `countCodeLines(code: string): number` - `expandPastedText(value: string, pastes: readonly PastedText[], label?: string): string`: Replaces placeholders with the pasted text; placeholders of unknown pastes stay as typed - `findDomMatches(root: Node, query: string): Range[]`: Ranges of every match inside the text nodes of `root`, matching across adjacent text nodes - `findTextMatches(text: string, query: string): TextMatch[]`: Case-insensitive, non-overlapping occurrences of `query` in `text` - `formatAwayDuration(ms: number): string`: Coarse away duration: `45m`, `3h`, `2d`; under a minute is `1m` - `formatPasteLabel(paste: PastedText, template: string): string`: `Pasted text #1 · 240 lines`, `{id}` and `{lines}` are replaced - `formatQuestionAnswer(answer: QuestionAnswer, options: QuestionOption[]): string` - `formatSpend(amount: number, currency = 'USD', locale = 'en'): string` - `getBreakdownTotal(groups: readonly ContextBreakdownGroup[]): number` - `getCollapsedLineCount(totalLines: number, collapsedLines: number = DEFAULT_COLLAPSED_LINES): number | null`: Number of lines to show while collapsed, or `null` when the code is short enough to show in full - `getElicitationFields(schema: ElicitationRequestedSchema): ElicitationField[]`: Turns an elicitation schema into an ordered list of renderable fields - `getGroupShade(group: ContextBreakdownGroup, index: number): string` - `getGroupTokens(group: ContextBreakdownGroup): number` - `getHookActivityTitle(event: string, status: HookActivityStatus, hookCount: number, labels: HookActivityLabels = DEFAULT_HOOK_ACTIVITY_LABELS): string` - `getInitialQuestionDraft(answer: QuestionAnswer | undefined, question: QuestionConfig | undefined): QuestionDraft` - `getSearchablePrompts(history: readonly string[]): string[]`: Unique prompts, newest first, for the search dialog - `getTurnSummarySegments(summary: { durationMs: number; tokens?: number; tokenBudget?: number; backgroundTasks?: number }, labels: TurnSummaryLabels = DEFAULT_TURN_SUMMARY_LABELS): string[]` - `getUsageLevel(ratio: number, warnAt = 0.8, dangerAt = 0.95): ContextUsageLevel` - `getUsageRatio(used: number, total: number): number` - `insertPastePlaceholder(value: string, selectionStart: number, selectionEnd: number, id: number, label?: string): { text: string; caret: number }`: Inserts the placeholder of `paste` in place of the selection and returns the new text and caret - `navigatePromptHistory(history: readonly string[], state: PromptHistoryState, direction: PromptHistoryDirection, currentValue: string): { state: PromptHistoryState; value: string } | null`: Moves through `history` (oldest first, newest last). Returns `null` when there is nowhere to go, the field then keeps its default arrow behavior. - `prunePastes(value: string, pastes: readonly PastedText[], label?: string): PastedText[]`: Pastes whose placeholder is still present in `value` - `removePastePlaceholder(value: string, id: number, label?: string): string` - `shouldCollapsePaste(text: string, threshold: PasteCollapseThreshold | false | undefined): boolean` - `sortBreakdownItems(items: readonly ContextBreakdownItem[]): ContextBreakdownItem[]`: Items ordered by tokens, largest first, ties keep their order - `sortSuggestions(suggestions: readonly ContextSuggestion[]): ContextSuggestion[]`: Critical first, then by savings, largest first - `stepMatchIndex(current: number, total: number, direction: 1 | -1): number`: Next active match index, wrapping around; `-1` when there are no matches - `validateElicitationField(field: ElicitationField, value: ElicitationDraftValue | undefined): string | null`: Returns an error message for the value, or `null` when it is valid ## Tools - `getBashRunInfo(part: ToolPart): BashRunInfo`: Output and run metadata of a Bash tool part in any of the shapes agents report - `parseMcpToolType(partType: string): McpToolInfo | null`: Parses `tool-mcp____` part types (and built-in MCP resource tools) - `routeToolCall(step: ToolCallStep, state: StepState, onComplete: () => void, actionIndex: number): React.ReactNode`: Picks the card component for a timeline `tool-call` step - `unwrapMcpOutput(output: any): any`: Unwraps MCP results (`CallToolResult`, `[{ type: 'text', text }]`) and parses JSON payloads when possible ## Primitives - `useFuzzySearch({ items, keys, query, limit }: UseFuzzySearchOptions): FuzzyResult[]` - `useWizard({ steps, initialValues, values: controlledValues, onValuesChange, nonLinear = false, busy = false }: UseWizardOptions): UseWizardReturn` - `createKeyValuePair(entry: Partial = {}): KeyValuePair` - `dedupeValidationErrors(errors: SettingsValidationError[]): SettingsValidationError[]` - `detectShortcutPlatform(): ShortcutPlatform` - `envKeyValidator(key)` - `fillValidationTemplate(template: string, values: Record): string` - `filterSettingsNav(sections: SettingsNavItem[], query: string): SettingsNavItem[]` - `flattenSchema(schema: JsonSchema): SchemaRow[]` - `formatShortcut(keys: string | readonly string[], platform: ShortcutPlatform): string[]`: Display keys of a shortcut: a string is split on `+` (`mod+K`), an array lists keys of one combination - `fuzzyFilter(items: T[], query: string, keys: FuzzyKey[]): FuzzyResult[]`: Keeps items matching `query` in any key, sorted by the best key score; a blank query keeps the order - `getSchemaTypeLabel(schema: JsonSchema): string` - `getValidationErrorKey(error: SettingsValidationError): string` - `getValidationSeverity(errors: SettingsValidationError[]): SettingsValidationSeverity` - `groupSettingsNav(sections: SettingsNavItem[]): SettingsNavGroup[]` - `groupValidationErrors(errors: SettingsValidationError[]): ValidationErrorGroup[]` - `headerKeyValidator(key)` - `parseKeyValueText(text: string): KeyValueEntry[]` - `validateKeyValuePairs(pairs: KeyValuePair[], validateKey: KeyValidator = envKeyValidator, labels: KeyValueErrorLabels = { keyRequired: 'Key is required', duplicateKey: 'Duplicate key' }): Record` ## MCP - `createMcpServerDraft(server?: McpServer): McpServerDraft` - `getMcpAgentUiStatus(status: McpServerStatus): AgentUiStatus` - `getMcpServerTarget(server: Pick)` - `getMcpToolAnnotationKinds(annotations: McpToolAnnotations | undefined): McpToolAnnotationKind[]` - `getMcpToolDisplayName(tool: McpToolDefinition): string` - `groupConfigWarnings(warnings: readonly McpConfigWarning[]): McpConfigWarningGroup[]` - `isValidMcpUrl(value: string): boolean` - `needsMcpAttention(status: McpServerStatus): boolean` - `resolveImportNames(candidates: readonly McpServerCandidate[], existingNames: readonly string[] = []): Record` - `splitMcpCommandLine(line: string): string[]` - `validateImportNames(selectedIds: readonly string[], names: Readonly>, existingNames: readonly string[] = [], labels: McpImportNameLabels = DEFAULT_MCP_IMPORT_NAME_LABELS): Record` ## Agents - `createAgentDraft(agent?: Partial): AgentDraft` - `resolveAgentTools(tools: AgentToolSelection, disallowedTools: readonly string[] = [], catalog: readonly ToolCatalogItem[] = []): string[]`: Tools the agent can actually call: the selection without disallowed tools, `all` expanded against the catalog - `slugifyAgentName(text: string): string` - `summarizeTools(tools: AgentToolSelection, catalog: readonly ToolCatalogItem[] = [], labels: Partial = {}): string` - `toAgentDraft({ method: _method, task: _task, generated: _generated, nameEdited: _nameEdited, ...draft }: AgentWizardValues): AgentDraft` - `validateAgentDraft(draft: AgentDraft, { existingNames = [], messages = {} }: ValidateAgentDraftOptions = {}): AgentDraftErrors` - `validateAgentName(name: string, existingNames: readonly string[] = [], messages: Partial = {}): string | null` ## Skills - `searchSkills(skills: Skill[], query: string): Skill[]` - `toSkillSlug(value: string): string` - `validateSkillDraft(draft: SkillDraft, takenNames: string[] = [], messages: SkillValidationMessages = DEFAULT_SKILL_VALIDATION_MESSAGES): SkillDraftErrors` - `validateSkillName(name: string, takenNames: string[] = [], messages: SkillValidationMessages = DEFAULT_SKILL_VALIDATION_MESSAGES): string | null` ## Permissions - `buildPermissionRule(draft: PermissionRuleDraft, id: string, base?: Partial): PermissionRule` - `describeRule(rule: Pick): string` - `formatRule(rule: ParsedPermissionRule): string` - `getBroadRuleWarning(rule: Pick): string | null` - `matchRule(rule: Pick, toolName: string, input: string): boolean` - `matchToolName(ruleToolName: string, toolName: string): boolean` - `parseRule(text: string): ParsedPermissionRule | null` - `suggestRuleFromDenial(denial: PermissionDenial): ParsedPermissionRule` - `validateDirectoryPath(path: string, directories: readonly WorkspaceDirectory[] = []): string | null` - `validateRule(text: string, knownTools?: readonly string[]): PermissionRuleValidation` ## Hooks configuration - `buildHook(draft: HookDraft, id: string): HookConfig` - `countHooksByEvent(hooks: HookConfig[]): Record` - `describeMatcher(matcher: string | undefined, messages: Pick = DEFAULT_HOOK_MESSAGES): string` - `eventSupportsMatcher(event: HookEvent | null): boolean` - `getHookPayloadExample(event: HookEvent, matcher?: string): string` - `getHookSummary(hook: Pick): string` - `isRegexMatcher(matcher: string): boolean` - `validateHookDraft(draft: HookDraft, messages: HookMessages = DEFAULT_HOOK_MESSAGES): HookDraftErrors` - `validateMatcher(matcher: string, messages: HookMessages = DEFAULT_HOOK_MESSAGES): string | null` ## Sessions - `downloadFile(filename: string, content: string, mimeType: string): void` - `exportConversation(messages: ChatMessage[], options: ExportOptions): ExportResult` - `formatRelativeTime(date: SessionDate, now: SessionDate = new Date(), locale = 'en'): string` - `getExportFilename(title: string | undefined, format: ExportFormat): string` - `groupSessionsByDate(sessions: SessionSummary[], now: Date = new Date(), labels: Partial = {}, locale = 'en'): SessionDateGroup[]` ## Message actions - `buildRewindPoints(messages: ChatMessage[]): RewindPoint[]`: User messages the conversation can return to, newest first - `getMessagePreview(message: ChatMessage, maxLength = PREVIEW_LENGTH): string` - `parseSlashCommand(text: string): ParsedSlashCommand | null`: Parses `/name args`; returns `null` when the text is not a command invocation ## Tasks - `useNow(active: boolean, intervalMs = 1000): number`: Current time in ms, refreshed every `intervalMs` while `active` - `buildTaskTree(flat: BackgroundTask[]): BackgroundTask[]`: Nests a flat list by `parentId`; unknown parents and cycles make a task a root, order is kept - `canRetryTask(task: BackgroundTask): boolean` - `canStopTask(task: BackgroundTask): boolean` - `countTasksByKind(tasks: BackgroundTask[]): Record` - `describeTaskSummary(summary: BackgroundTaskSummary, labels: BackgroundTaskLabels): string[]`: Parts of the compact status text, for example `['2 running', '1 failed']` - `findTask(tasks: BackgroundTask[], id: string | null | undefined): BackgroundTask | undefined` - `flattenTaskTree(tasks: BackgroundTask[]): BackgroundTask[]`: Depth-first list of every task in the tree, children removed and `parentId` filled in - `getProgressPercent(progress: BackgroundTaskProgress): number`: Progress as a 0–100 percentage - `getTaskElapsedMs(task: Pick, now: number): number | undefined` - `getTaskKindLabel(kind: BackgroundTaskKind, labels: BackgroundTaskLabels): string` - `getTaskStatusLabel(status: BackgroundTaskStatus, labels: BackgroundTaskLabels): string` - `isTaskActive(status: BackgroundTaskStatus): boolean` - `matchesTaskQuery(task: BackgroundTask, query: string): boolean` - `summarizeTasks(tasks: BackgroundTask[]): BackgroundTaskSummary`: Counts tasks by status; pass `flattenTaskTree(tasks)` to include subtasks - `toTimestamp(value: number | Date | undefined): number | undefined` ## Diff - `computeFileStats(change: FileChange): FileStats`: Added and removed line counts, taken from the change when given and computed from the contents otherwise - `countChangesByStatus(changes: FileChange[]): Record` - `filterChanges(changes: FileChange[], query: string, status: FileChangeStatus | 'all'): FileChange[]` - `getFileStatusLabel(status: FileChangeStatus, labels: DiffLabels, untracked = false): string` - `summarizeChanges(changes: FileChange[]): FileStats & { files: number }` ## Model settings - `formatCost(value: number, currency = 'USD', locale = 'en-US'): string` - `formatLimitValue(value: number, unit: UsageLimit['unit'], currency?: string, locale = 'en-US')` - `formatResetIn(resetsAt: Date | string | undefined, now: Date = new Date(), locale = 'en-US')`: `Resets in 2h 15m`-style duration until `resetsAt`, `null` when unknown or already passed - `formatUsageDay(date: Date | string, locale = 'en-US'): string` ## Help - `formatCommandUsage(command: Pick): string`: `/name args` as typed in the composer - `toPaletteCommands(commands: CommandHelpItem[], onRun?: (command: CommandHelpItem) => void): PaletteCommand[]`: Slash commands as `CommandPalette` commands; `onRun` receives the command picked in the palette ## Memory - `formatMemoryUpdatedAt(updatedAt: string | undefined, now: number, locale?: string): string | null` - `getMemoryFileName(path: string): string` - `matchesMemoryQuery(file: MemoryFile, query: string): boolean` - `sortMemoryFiles(files: MemoryFile[]): MemoryFile[]` ## Theme - `useAiKitTheme(): AiKitThemeContextValue & { aiKit: AiKitThemeOther }`: Reads and changes the settings of the nearest `AiKitProvider`; `aiKit` is `theme.other.aiKit` resolved from `useMantineTheme()` - `useOptionalAiKitTheme(): AiKitThemeContextValue | null` - `createAiKitTheme(overrides?: MantineThemeOverride): MantineThemeOverride`: Mantine theme of the kit subtree, built on the standard theme object: `fontWeights.medium`, `cursorType`, `autoContrast`, `respectReducedMotion`, `activeClassName` and a `variantColorResolver` that speaks the main-branch language. Component extensions apply to every stock component unless it opts out with `unstyled` or `data-ai-kit-unstyled`. - `getAiKitCssVariables(hostTheme: MantineTheme, kitTheme: MantineTheme, tokens?: AeTokenOverrides): AiKitCssVariables`: CSS variables the kit subtree needs on top of the host ones: the Mantine variables that differ between the host theme and the kit theme (Mantine writes them only at the document root), kit surfaces, and `--ae-*` tokens that differ from the defaults of `vars.module.css`. - `getAiKitSettingsTheme(settings: AiKitThemeSettings): MantineThemeOverride`: Theme override that expresses the settings with standard theme fields and `theme.other.aiKit` - `mergeAiKitTheme(hostTheme?: MantineThemeOverride): MantineThemeOverride`: Adds the kit theme under a host theme for a global setup: host values win, kit component styles fill the gaps. Portals get the `ae-kit` class; put the same class on the app root element. - `readAiKitSettings(key: string | undefined): AiKitThemeSettings` - `writeAiKitSettings(key: string | undefined, settings: AiKitThemeSettings): boolean` ## Launcher - `mountChatLauncher(target: HTMLElement | ShadowRoot, element: React.ReactNode, { shadow = true, styles = [], styleUrls = [], adoptDocumentStyles = false, theme, colorScheme = 'light', wrap }: MountChatLauncherOptions = {}): MountedChatLauncher`: Mounts a chat launcher on a third-party page, isolated in a shadow root with its own Mantine provider ## Shared utilities - `useHighlightedLines(code: string, language: string | undefined, highlighter: SyntaxHighlighter | undefined): HighlightedLinesState`: Highlights code with a module-level cache; stale async results for older code are dropped - `useInputTyping(text: string, duration: number, isActive: boolean, onComplete: () => void)`: Simulates a user typing `text` into the composer over `duration` ms - `useStreamingText(fullText: string, options: UseStreamingTextOptions = {})`: Reveals `fullText` word by word, useful for demos and static transcripts - `useToolComplete(isAnimating: boolean, duration: number, onComplete: () => void)`: Calls `onComplete` after `duration` ms while `isAnimating` is true - `areToolPropsEqual

(prevProps: P, nextProps: P): boolean`: Props comparator for tool cards wrapped in `React.memo()`. AI SDK v5 `useChat` emits new part objects on every update, so parts are compared by identity of their state, input, output and error text; other props are compared shallowly. - `byteLength(text: string): number`: UTF-8 size of the text in bytes - `clearHighlightCache()` - `countDiffStats(lines: DiffLine[]): { added: number; removed: number }` - `createShikiHighlighter(shiki: ShikiHighlighterLike, themes: { light: NoInfer & string; dark: NoInfer & string }): SyntaxHighlighter`: Adapts a shiki highlighter created by the host, with a light and a dark theme - `diffLines(oldText: string, newText: string): DiffLine[]` - `formatBytes(bytes: number): string` - `formatJsonOutput(text: string): string | null`: Pretty-printed JSON when the whole output is a JSON object or array, `null` otherwise - `formatTokens(value: number): string` - `getToolStatus(part: ToolPart, chatStatus?: string): ToolStatus`: Get tool status from part state - `hasAnsi(text: string): boolean` - `highlightCode(code: string, language: string | undefined, highlighter: SyntaxHighlighter): HighlightedLines | null | Promise`: Runs the highlighter through the module cache; failures resolve to `null` - `mapToolInvocationToStep(toolCallId: string, toolInvocation: ToolInvocation): ToolCallStep`: Converts an AI SDK tool invocation into a `ToolCallStep` consumed by tool cards - `mapToolNameToVariant(toolName: string): 'thinking' | 'action' | 'search' | undefined` - `mapToolStateToStepState(aiState: 'partial-call' | 'call' | 'result'): StepState` - `normalizeAssistantToolParts(parts: unknown[]): unknown[]` - `normalizeToolPart(part: unknown): unknown`: Parses stringified JSON in `input`/`output`/`result` of tool parts - `parseAnsiLines(text: string): AnsiSegment[][]`: Splits terminal output into lines of styled segments; SGR colors map to Mantine colors, other escapes are dropped - `splitLinks(text: string): TextLink[]`: Splits text into plain parts and http(s) links, trailing punctuation stays outside the link - `stripAnsi(text: string): string` - `tailLines(lines: T[], limit: number): { visible: T[]; hidden: number }`: Last `limit` lines and how many were hidden before them