KQ-2115 · FALLTHROUGH/ASK
Fallthrough Ask
A command palette that never dead-ends: typing filters commands and pages with fuzzy matching under a highlight pill that slides on springs.snap, every query ends with an ask row, and a query that matches nothing turns the empty row into it while pages sharing a word stay listed as Related. Enter unfolds the ask row in place on springs.glide into an answer card — while it reads, the pages it is reading arrive one by one on snap; the answer then rises sentence by sentence with citation marks popping in on springs.flick, and its sources settle as chips, a numbered list or footnotes. The field is a combobox whose Up and Down move one highlight across results and the answer (the ask option is described by the answer text), Tab walks the sources, Escape folds the answer and keeps the query, and the no-answer and error states keep it too while still offering a next step.
Up and Down move through the results and the ask row, Enter opens or asks, Tab reaches an answer’s sources, Escape folds the answer.
Nothing matches “how many unused days carry over”
Related pages
asks when nothing matches · “how many unused days carry over”
Install
One command — the source lands in your repo. Or copy it from the Code tab.
$ pnpm dlx shadcn@latest add @kinetiq/fallthrough-ask
Props
- ref
- React.Ref<FallthroughAskHandle>
- default —
- ask(query?) asks the assistant (the field's query, or this one), fold() folds the answer, focus() puts focus in the field.
- commands
- AskCommand[]
- default defaultAskCommands
- Commands the palette runs: { id, label, icon, shortcut?, keywords? }.
- pages
- AskPage[]
- default defaultAskPages
- Pages it opens and answers cite: { id, title, path, excerpt?, keywords? }; excerpts and keywords also find related pages.
- answers
- AskAnswer[]
- default defaultAskAnswers
- Answers it can give without a host: { id, keywords, text with [1]-style marks, sources (page ids in mark order) }.
- recent
- string[]
- default ["leave", "expenses", "first-week"]
- Page ids listed for an empty query.
- query / defaultQuery
- string
- default ""
- The field's text, controlled or initial.
- onQueryChange
- (query: string) => void
- default —
- Fires on every keystroke with the new query.
- onSelect
- (item: { kind: "command" | "page"; id: string }) => void
- default —
- A command or page was chosen with Enter or a click.
- onAsk
- (query: string) => Promise<AskAnswer | null> | void
- default —
- The visitor asked. Return a promise of an answer (null for none) to answer yourself — a rejection shows the error state; return nothing and the palette answers from answers after thinkingMs.
- onOpenSource
- (pageId: string, answerId: string | null) => void
- default —
- A source page was opened from an answer (answerId is null for the no-answer state's closest page).
- onAnswer
- (state: "answered" | "none" | "error", answer: AskAnswer | null) => void
- default —
- The assistant settled.
- thinkingMs
- number
- default 1200
- How long the palette reads before answering from answers, in ms.
- answerHeight
- number
- default 200
- The tallest the answer grows before it scrolls inside the card, in px.
- sourceStyle
- "chips" | "list" | "notes"
- default "chips"
- How sources show: numbered page chips, a list with each page's path, or footnote marks with a source line.
- accent
- "cobalt" | "plum" | "mint" | "ink" | string
- default "cobalt"
- Highlight, ask disc and citations. A name or any CSS colour.
- assistantName
- string
- default "Handbook assistant"
- Who answers, in the card header and the footer.
- placeholder
- string
- default "Search or ask the handbook"
- The field's placeholder.
- maxHeight
- number
- default 460
- The palette's tallest height in px; its body follows its content on glide up to this.
- label
- string
- default "Search the Fernworks handbook"
- The field's accessible name.
- disabled
- boolean
- default false
- Read only: nothing can be typed, chosen or asked.
- className
- string
- default —
- Classes for the palette.
| Prop | Type | Default | Description |
|---|---|---|---|
| ref | React.Ref<FallthroughAskHandle> | — | ask(query?) asks the assistant (the field's query, or this one), fold() folds the answer, focus() puts focus in the field. |
| commands | AskCommand[] | defaultAskCommands | Commands the palette runs: { id, label, icon, shortcut?, keywords? }. |
| pages | AskPage[] | defaultAskPages | Pages it opens and answers cite: { id, title, path, excerpt?, keywords? }; excerpts and keywords also find related pages. |
| answers | AskAnswer[] | defaultAskAnswers | Answers it can give without a host: { id, keywords, text with [1]-style marks, sources (page ids in mark order) }. |
| recent | string[] | ["leave", "expenses", "first-week"] | Page ids listed for an empty query. |
| query / defaultQuery | string | "" | The field's text, controlled or initial. |
| onQueryChange | (query: string) => void | — | Fires on every keystroke with the new query. |
| onSelect | (item: { kind: "command" | "page"; id: string }) => void | — | A command or page was chosen with Enter or a click. |
| onAsk | (query: string) => Promise<AskAnswer | null> | void | — | The visitor asked. Return a promise of an answer (null for none) to answer yourself — a rejection shows the error state; return nothing and the palette answers from answers after thinkingMs. |
| onOpenSource | (pageId: string, answerId: string | null) => void | — | A source page was opened from an answer (answerId is null for the no-answer state's closest page). |
| onAnswer | (state: "answered" | "none" | "error", answer: AskAnswer | null) => void | — | The assistant settled. |
| thinkingMs | number | 1200 | How long the palette reads before answering from answers, in ms. |
| answerHeight | number | 200 | The tallest the answer grows before it scrolls inside the card, in px. |
| sourceStyle | "chips" | "list" | "notes" | "chips" | How sources show: numbered page chips, a list with each page's path, or footnote marks with a source line. |
| accent | "cobalt" | "plum" | "mint" | "ink" | string | "cobalt" | Highlight, ask disc and citations. A name or any CSS colour. |
| assistantName | string | "Handbook assistant" | Who answers, in the card header and the footer. |
| placeholder | string | "Search or ask the handbook" | The field's placeholder. |
| maxHeight | number | 460 | The palette's tallest height in px; its body follows its content on glide up to this. |
| label | string | "Search the Fernworks handbook" | The field's accessible name. |
| disabled | boolean | false | Read only: nothing can be typed, chosen or asked. |
| className | string | — | Classes for the palette. |