PromptInput
A chat input bar with slash commands, file mentions, and AI SDK integration
A chat input bar with slash command autocomplete, file mention suggestions, command history, and direct Vercel AI SDK integration. Supports both a self-contained default layout and a fully composable compound mode.
Run demo
bunx @gridland/demo prompt-inputcurl -fsSL https://raw.githubusercontent.com/thoughtfulllc/gridland/main/scripts/run-demo.sh | bash -s prompt-inputInstallation
bunx create-gridland add prompt-inputUsage
import { PromptInput } from "@/components/ui/prompt-input"<PromptInput
focusId="prompt"
autoFocus
placeholder="Message Claude..."
onSubmit={(msg) => console.log("Sent:", msg.text)}
/>PromptInput registers with the focus system via focusId. Wrap your app in
GridlandProvider (which mounts <FocusProvider selectable> implicitly) or a
standalone <FocusProvider selectable>. When the component is focused, it
accepts keystrokes and renders an <input> intrinsic in the terminal runtime.
Usage with AI SDKs
onSubmit receives { text: string }. Map it to your SDK of choice.
Vercel AI SDK
Direct pass-through — sendMessage accepts { text } natively.
import { useChat } from "@ai-sdk/react"
const { status, sendMessage, stop } = useChat({ /* transport config */ })
<PromptInput
focusId="prompt"
autoFocus
status={status}
onSubmit={sendMessage}
onStop={stop}
/>Anthropic SDK
const [status, setStatus] = useState<ChatStatus>("ready")
<PromptInput
focusId="prompt"
autoFocus
status={status}
onSubmit={async (msg) => {
setStatus("submitted")
const stream = anthropic.messages.stream({
model: "claude-sonnet-4-20250514",
messages: [...history, { role: "user", content: msg.text }],
})
setStatus("streaming")
for await (const event of stream) { /* handle deltas */ }
setStatus("ready")
}}
onStop={() => { stream.abort(); setStatus("ready") }}
/>OpenAI SDK
<PromptInput
focusId="prompt"
autoFocus
status={status}
onSubmit={async (msg) => {
setStatus("submitted")
const stream = await openai.chat.completions.create({
model: "gpt-4o",
messages: [...history, { role: "user", content: msg.text }],
stream: true,
})
setStatus("streaming")
for await (const chunk of stream) { /* handle deltas */ }
setStatus("ready")
}}
/>Examples
Slash Commands
Provide a commands array for / autocomplete.
<PromptInput
focusId="prompt"
autoFocus
commands={[
{ cmd: "/help", desc: "Show commands" },
{ cmd: "/model", desc: "Switch model" },
{ cmd: "/clear", desc: "Clear conversation" },
]}
onSubmit={(msg) => console.log(msg.text)}
/>File Mentions
Provide a files array for @ mention autocomplete.
<PromptInput
focusId="prompt"
autoFocus
files={["src/index.ts", "src/routes.ts", "package.json"]}
onSubmit={(msg) => console.log(msg.text)}
/>Custom Suggestion Provider
Override the built-in / and @ suggestions with your own logic.
<PromptInput
focusId="prompt"
autoFocus
getSuggestions={(value) => {
if (value.startsWith("#")) {
return [
{ text: "#bug", desc: "Bug report" },
{ text: "#feature", desc: "Feature request" },
].filter((s) => s.text.startsWith(value))
}
return []
}}
onSubmit={(msg) => console.log(msg.text)}
/>Controlled
Use value and onChange to control the input externally.
const [value, setValue] = useState("")
<PromptInput
focusId="prompt"
autoFocus
value={value}
onChange={setValue}
onSubmit={(msg) => console.log(msg.text)}
/>Model Label
Display the active model name below the input.
<PromptInput
focusId="prompt"
autoFocus
model="claude-sonnet-4-20250514"
onSubmit={(msg) => console.log(msg.text)}
/>Disabled (legacy)
When not using status, use disabled and disabledText directly.
<PromptInput
focusId="prompt"
disabled
disabledText="Generating..."
/>Compound Components
For full control over layout, pass children to enter compound mode. Subcomponents
read state from PromptInput via context — no prop drilling needed.
import { PromptInput } from "@/components/ui/prompt-input"
<PromptInput
focusId="prompt"
autoFocus
status={status}
onSubmit={handleSubmit}
onStop={stop}
>
<PromptInput.Divider />
<PromptInput.Suggestions />
<PromptInput.Textarea />
<PromptInput.Model />
<PromptInput.StatusText />
<PromptInput.Submit />
<PromptInput.Divider />
</PromptInput>Compound Components
| Component | Description |
|---|---|
PromptInput.Textarea | Prompt character + text with cursor |
PromptInput.Suggestions | Autocomplete dropdown (slash commands, @mentions) |
PromptInput.Submit | Status indicator: ⏎ ready, ◐ submitted, ■ streaming, ✕ error |
PromptInput.Divider | Horizontal rule (─) |
PromptInput.StatusText | Error text shown when status is "error" |
PromptInput.Model | Muted model label shown below the input |
Provider
Wrap your app in PromptInputProvider to lift input state outside of
PromptInput. This lets you read or modify the input value and suggestions from
sibling components.
import { PromptInputProvider, usePromptInputController } from "@/components/ui/prompt-input"
<PromptInputProvider initialInput="">
<Sidebar />
<PromptInput focusId="prompt" autoFocus onSubmit={handleSubmit} />
</PromptInputProvider>usePromptInputController
Access lifted state from anywhere inside the provider tree.
const controller = usePromptInputController()
controller.textInput.value // current text
controller.textInput.setValue(v) // set text
controller.textInput.clear() // clear text
controller.suggestions.suggestions // current suggestions
controller.suggestions.selectedIndex // selected index
controller.suggestions.setSuggestions(s) // set suggestions
controller.suggestions.setSelectedIndex(i)
controller.suggestions.clear()usePromptInput
Access rendering state from within any compound subcomponent.
import { usePromptInput } from "@/components/ui/prompt-input"
function MyCustomStatus() {
const { status, value, disabled } = usePromptInput()
return <text>{status}: {value.length} chars</text>
}Command Registry
Wrap your app in CommandProvider to let sibling components register slash
commands that automatically appear in PromptInput's autocomplete — without
passing commands as a prop.
import { CommandProvider, useRegisterCommand } from "@/components/ui/prompt-input"
function ModelSwitcher() {
useRegisterCommand({ cmd: "/model", desc: "Switch model" })
return null
}
function ClearButton() {
useRegisterCommand({ cmd: "/clear", desc: "Clear conversation", onExecute: () => clearChat() })
return null
}
<CommandProvider>
<ModelSwitcher />
<ClearButton />
<PromptInput focusId="prompt" autoFocus onSubmit={handleSubmit} />
</CommandProvider>useRegisterCommand
Register a single command. Automatically unregisters on unmount.
useRegisterCommand({ cmd: "/help", desc: "Show commands" })useRegisterCommands
Register multiple commands at once. Automatically unregisters on unmount.
useRegisterCommands([
{ cmd: "/model", desc: "Switch model" },
{ cmd: "/clear", desc: "Clear conversation" },
])useRegistryCommands
Subscribe to all registered commands. Returns the current command list reactively.
const commands = useRegistryCommands()
// Returns PromptInputCommand[] — updates when commands are added/removedCommandProvider
| Prop | Type | Default | Description |
|---|---|---|---|
children | ReactNode | - | Components that can register and consume commands |
registry | CommandRegistry | - | Use an existing registry instance. If omitted, a new one is created. |
Controls
- Enter: Submit message (or accept suggestion)
- Tab: Cycle through suggestions
- ↑/↓: Navigate suggestions or command history
- Escape: Dismiss suggestions, or stop generation when streaming
- /: Trigger slash command suggestions
- @: Trigger file mention suggestions
API Reference
PromptInput
| Prop | Type | Default | Description |
|---|---|---|---|
value | string | - | Controlled input value |
defaultValue | string | "" | Default value for uncontrolled mode |
onSubmit | (message: { text: string }) => void | Promise<void> | - | Called on submit. Clears on resolve, preserves on reject. |
onChange | (text: string) => void | - | Called when input value changes |
placeholder | string | "Type a message..." | Placeholder text when empty |
prompt | string | "❯ " | Prompt character before input |
promptColor | string | theme.muted | Color of the prompt character |
status | ChatStatus | - | AI chat status. Drives disabled state and hint text. |
onStop | () => void | - | Called when Escape is pressed during streaming |
onError | (error: unknown) => void | - | Called when async onSubmit rejects |
submittedText | string | "Thinking..." | Hint text when status is "submitted" |
streamingText | string | "Generating..." | Hint text when status is "streaming" |
errorText | string | "An error occurred. Try again." | Text shown when status is "error" |
disabled | boolean | false | Disable input. Ignored when status is provided. |
disabledText | string | "Generating..." | Text shown when disabled. Ignored when status is provided. |
commands | PromptInputCommand[] | [] | Slash commands for autocomplete |
skills | PromptInputCommand[] | [] | Dynamically-provided skills merged into autocomplete alongside commands |
files | string[] | [] | File paths for @ mention autocomplete |
getSuggestions | (value: string) => Suggestion[] | - | Custom suggestion provider — overrides commands/files |
maxSuggestions | number | 5 | Max visible suggestions |
enableHistory | boolean | true | Enable command history with ↑/↓ |
model | string | - | Model name displayed below the input |
focusId | string | auto-generated | Stable id for the focus system |
autoFocus | boolean | false | Focus this component on mount |
showDividers | boolean | true | Show horizontal dividers above and below input |
dividerColor | string | - | Override divider line color (e.g. for focus indicators) |
dividerDashed | boolean | - | Use dashed divider lines (╌) instead of solid (─) |
children | ReactNode | - | When provided, enables compound mode |
PromptInputProvider
| Prop | Type | Default | Description |
|---|---|---|---|
initialInput | string | "" | Initial text input value |
children | ReactNode | - | Components that can access the provider context |
PromptInputCommand
| Field | Type | Description |
|---|---|---|
cmd | string | Slash command string (e.g. "/help") |
desc | string? | Description shown in autocomplete |
group | string? | Group name for categorization in autocomplete |
onExecute | () => void? | When provided, PromptInput calls this directly instead of onSubmit |
hidden | boolean? | Hide from autocomplete suggestions but still executable |
Suggestion
| Field | Type | Description |
|---|---|---|
text | string | Suggestion text |
desc | string? | Optional description |
trigger | string? | Character that triggered this suggestion (e.g. "@", "#"). Used to determine replacement range. |
ChatStatus
| Value | Description |
|---|---|
"ready" | Input enabled, accepts user input |
"submitted" | Input disabled, shows submitted text |
"streaming" | Input disabled, shows streaming text, Escape calls onStop |
"error" | Input enabled, shows error indicator |