Hooks and utilities

Components keep their logic in pure functions next to them: parsing, validation, filtering, grouping and formatting. The package exports them, so you can validate a draft on the server, pre-filter a list or format a value the same way the UI does. This page lists 14 hooks and 176 functions, generated from the package source. Everything is imported from @sinups/ai-kit.

Chat

useFileIntake({ accept, maxFiles, maxFileSize, current, multiple = true, onFiles, onReject }: UseFileIntakeOptions): FileIntake

Picks, pastes and drops files through one policy; uploading them stays with the host

useStalled({ startedAt, lastActivityAt, stallAfterMs = 3000, paused = false }: UseStalledOptions)
buildElicitationContent(fields: ElicitationField[], draft: ElicitationDraft): { content: ElicitationContent; errors: Record<string, string> }

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

filterFiles(files: File[], policy: FileIntakePolicy = {}): FileIntakeResult

Splits files into those the policy accepts and those it rejects, with the reason for each

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, units?: Partial<DurationUnits>): 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

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

useToolPresentation(): ToolPresentation
createToolCallLookups(messages: readonly (ChatMessage | MessagePart)[]): ToolCallLookups

Indexes a transcript by tool call id. Results are looked up across messages, so a call restored with its result in a later message is never shown as running; among the calls that have neither a result nor an open decision, the first one runs and the ones after it are queued.

deriveToolCallState(part: ToolPart, { chatStatus, lookups }: DeriveToolCallStateOptions = {}): ToolCallState

Resolves the state a tool card shows. `part.state` alone cannot tell a call that waits for a decision from one that is running, and a restored transcript carries its result in another part, so the transcript is asked first and the part state is only the fallback.

findToolCatalogEntry(catalog: ToolCatalog | undefined, part: Pick<ToolPart, 'type' | 'toolName'>): ToolCatalogEntry | undefined

Catalog entry of the tool behind a call, `undefined` when the host has none for it

formatToolProgress(progress: ToolCallProgress): string

`45%` against a known total, `3` steps otherwise

getBashRunInfo(part: ToolPart): BashRunInfo

Output and run metadata of a Bash tool part in any of the shapes agents report

getToolCatalogTitle(entry: ToolCatalogEntry | undefined): string | undefined

Readable name of a catalog tool: its `title`, then `annotations.title`

getToolProgress(part: ToolPart): ToolCallProgress | undefined

Latest MCP `notifications/progress` of a call, read from `part.progress` or from the provider metadata a host attaches instead. Nothing is shown when the server never reports progress.

getToolProgressRatio(progress: ToolCallProgress): number | undefined

Share of the work done, `undefined` while the server reports no total

parseMcpToolType(partType: string): McpToolInfo | null
routeToolCall(step: ToolCallStep, state: StepState, onComplete: () => void, actionIndex: number): React.ReactNode

Picks the card component for a timeline `tool-call` step

summarizeToolArgs(args: Record<string, unknown>, { schema, locale }: SummarizeArgsOptions = {}): string

A few arguments as `title: value · title: value`, in the order and with the titles of the input schema: required first, empty values and `false` flags skipped, a `true` flag shows its title alone.

unfoldToolArgs(input: unknown): Record<string, unknown>

Arguments with every string argument that holds a JSON object replaced by its fields. strings: a string argument is a string. Unfold them yourself in `toolArgs` if a server needs it.

unwrapMcpOutput(output: any): any

Output of an MCP call as 0.3 read it, kept for existing renderers; see `unwrapToolOutput`

Primitives

useFuzzySearch<T>({ items, keys, query, limit }: UseFuzzySearchOptions<T>): FuzzyResult<T>[]
useWizard<V>({ steps, initialValues, values: controlledValues, onValuesChange, nonLinear = false, busy = false }: UseWizardOptions<V>): UseWizardReturn<V>
createKeyValuePair(entry: Partial<KeyValueEntry> = {}): KeyValuePair
dedupeValidationErrors(errors: SettingsValidationError[]): SettingsValidationError[]
detectShortcutPlatform(): ShortcutPlatform
envKeyValidator(key)
fillValidationTemplate(template: string, values: Record<string, string>): string
filterSettingsNav(sections: SettingsNavItem[], query: string): SettingsNavItem[]
flattenSchema(schema: JsonSchema): SchemaRow[]
flattenSchemaValues(schema: JsonSchema, values: unknown): SchemaValueRow[]

Flattens `values` against `schema` into rows: every described field in schema order, then keys the schema does not mention. Missing optional fields keep their row with `present: false`.

formatSchemaValue(value: unknown): string

One-line form of a leaf value: strings as they are, everything else as JSON

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<T>(items: T[], query: string, keys: FuzzyKey<T>[]): FuzzyResult<T>[]

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[]
resolveValueSchema(schema: JsonSchema | undefined, value: unknown): JsonSchema | undefined

Picks the `oneOf`/`anyOf` variant that describes the value, so a union renders as one shape

validateKeyValuePairs(pairs: KeyValuePair[], validateKey: KeyValidator = envKeyValidator, labels: KeyValueErrorLabels = { keyRequired: 'Key is required', duplicateKey: 'Duplicate key' }): Record<string, string>

MCP

createMcpServerDraft(server?: McpServer): McpServerDraft
getMcpAgentUiStatus(status: McpServerStatus): AgentUiStatus
getMcpServerTarget(server: Pick<McpServer, 'transport' | 'command' | 'args' | 'url'>)
getMcpToolAnnotationKinds(annotations: McpToolAnnotations | undefined): McpToolAnnotationKind[]

Badges of a tool by its annotations and the defaults of the MCP specification: a tool is read-only only when it says so; otherwise it may be destructive unless `destructiveHint` is `false`, and idempotent only when `idempotentHint` is `true`. It reaches an open world unless `openWorldHint` is `false`. Annotations are hints from the server, not guarantees.

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<string, string>
splitMcpCommandLine(line: string): string[]
validateImportNames(selectedIds: readonly string[], names: Readonly<Record<string, string>>, existingNames: readonly string[] = [], labels: McpImportNameLabels = DEFAULT_MCP_IMPORT_NAME_LABELS): Record<string, string>

Agents

createAgentDraft(agent?: Partial<AgentDefinition>): 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<AgentToolSummaryLabels> = {}): 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<AgentValidationMessages> = {}): 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>): PermissionRule
describeRule(rule: Pick<PermissionRule, 'behavior' | 'toolName' | 'specifier'>): string
formatRule(rule: ParsedPermissionRule): string
getBroadRuleWarning(rule: Pick<PermissionRule, 'behavior' | 'toolName' | 'specifier'>): string | null
matchRule(rule: Pick<PermissionRule, 'toolName' | 'specifier'>, 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<HookEvent, number>
describeMatcher(matcher: string | undefined, messages: Pick<HookMessages, 'allTools' | 'toolsMatching'> = DEFAULT_HOOK_MESSAGES): string
eventSupportsMatcher(event: HookEvent | null): boolean
getHookPayloadExample(event: HookEvent, matcher?: string): string
getHookSummary(hook: Pick<HookConfig, 'type' | 'command' | 'prompt'>): 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<SessionDateGroupLabels> = {}, 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<BackgroundTaskKind, number>
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<BackgroundTask, 'startedAt' | 'endedAt'>, 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<FileChangeStatus, number>
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<CommandHelpItem, 'name' | 'args'>): 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()`

useAiKitThemePreview(): AiKitThemePreviewValue

Shows settings live in the nearest `AiKitProvider` before they are stored, with `savePreview` and `cancelPreview`

useAiKitThemeSetting(): AiKitThemeSettingValue

Reads and changes what the nearest `AiKitProvider` stores, ignoring any running preview

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<P extends { part: ToolPart; chatStatus?: string }>(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<Lang extends string, Theme>(shiki: ShikiHighlighterLike<Lang, Theme>, themes: { light: NoInfer<Theme> & string; dark: NoInfer<Theme> & 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
formatDuration(ms: number, units?: Partial<DurationUnits>): string

`1h 5m`, `2m 3s`, `45s`; under a second in milliseconds, `400ms`

formatElapsedTime(ms: number, units?: Partial<DurationUnits>): 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<HighlightedLines | null>

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

stripAnsi(text: string): string
tailLines<T>(lines: T[], limit: number): { visible: T[]; hidden: number }

Last `limit` lines and how many were hidden before them