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): FileIntakePicks, 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 = {}): FileIntakeResultSplits 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 = {}): ToolCallStateResolves 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 = {}): stringA 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> = {}): KeyValuePairdedupeValidationErrors(errors: SettingsValidationError[]): SettingsValidationError[]
detectShortcutPlatform(): ShortcutPlatform
envKeyValidator(key)
fillValidationTemplate(template: string, values: Record<string, string>): string
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
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> = {}): stringtoAgentDraft({ method: _method, task: _task, generated: _generated, nameEdited: _nameEdited, ...draft }: AgentWizardValues): AgentDraftvalidateAgentDraft(draft: AgentDraft, { existingNames = [], messages = {} }: ValidateAgentDraftOptions = {}): AgentDraftErrorsvalidateAgentName(name: string, existingNames: readonly string[] = [], messages: Partial<AgentValidationMessages> = {}): string | nullSkills
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 = {}): MountedChatLauncherMounts a chat launcher on a third-party page, isolated in a shadow root with its own Mantine provider