gridland
Components

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.

PromptInput
?

Run demo

Terminal
bunx @gridland/demo prompt-input
Terminal
curl -fsSL https://raw.githubusercontent.com/thoughtfulllc/gridland/main/scripts/run-demo.sh | bash -s prompt-input

Installation

Terminal
bunx create-gridland add prompt-input

Usage

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.

Vercel AI SDK
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

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

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.

Slash commands
<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.

File mentions
<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.

Custom suggestions
<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.

Controlled
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.

With model
<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.

Disabled
<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.

Compound usage
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

ComponentDescription
PromptInput.TextareaPrompt character + text with cursor
PromptInput.SuggestionsAutocomplete dropdown (slash commands, @mentions)
PromptInput.SubmitStatus indicator: ⏎ ready, ◐ submitted, ■ streaming, ✕ error
PromptInput.DividerHorizontal rule ()
PromptInput.StatusTextError text shown when status is "error"
PromptInput.ModelMuted 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.

Provider usage
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.

Controller hook
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.

Custom 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.

Command Registry
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.

Single command
useRegisterCommand({ cmd: "/help", desc: "Show commands" })

useRegisterCommands

Register multiple commands at once. Automatically unregisters on unmount.

Multiple commands
useRegisterCommands([
  { cmd: "/model", desc: "Switch model" },
  { cmd: "/clear", desc: "Clear conversation" },
])

useRegistryCommands

Subscribe to all registered commands. Returns the current command list reactively.

Consuming commands
const commands = useRegistryCommands()
// Returns PromptInputCommand[] — updates when commands are added/removed

CommandProvider

PropTypeDefaultDescription
childrenReactNode-Components that can register and consume commands
registryCommandRegistry-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

PropTypeDefaultDescription
valuestring-Controlled input value
defaultValuestring""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
placeholderstring"Type a message..."Placeholder text when empty
promptstring"❯ "Prompt character before input
promptColorstringtheme.mutedColor of the prompt character
statusChatStatus-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
submittedTextstring"Thinking..."Hint text when status is "submitted"
streamingTextstring"Generating..."Hint text when status is "streaming"
errorTextstring"An error occurred. Try again."Text shown when status is "error"
disabledbooleanfalseDisable input. Ignored when status is provided.
disabledTextstring"Generating..."Text shown when disabled. Ignored when status is provided.
commandsPromptInputCommand[][]Slash commands for autocomplete
skillsPromptInputCommand[][]Dynamically-provided skills merged into autocomplete alongside commands
filesstring[][]File paths for @ mention autocomplete
getSuggestions(value: string) => Suggestion[]-Custom suggestion provider — overrides commands/files
maxSuggestionsnumber5Max visible suggestions
enableHistorybooleantrueEnable command history with ↑/↓
modelstring-Model name displayed below the input
focusIdstringauto-generatedStable id for the focus system
autoFocusbooleanfalseFocus this component on mount
showDividersbooleantrueShow horizontal dividers above and below input
dividerColorstring-Override divider line color (e.g. for focus indicators)
dividerDashedboolean-Use dashed divider lines () instead of solid ()
childrenReactNode-When provided, enables compound mode

PromptInputProvider

PropTypeDefaultDescription
initialInputstring""Initial text input value
childrenReactNode-Components that can access the provider context

PromptInputCommand

FieldTypeDescription
cmdstringSlash command string (e.g. "/help")
descstring?Description shown in autocomplete
groupstring?Group name for categorization in autocomplete
onExecute() => void?When provided, PromptInput calls this directly instead of onSubmit
hiddenboolean?Hide from autocomplete suggestions but still executable

Suggestion

FieldTypeDescription
textstringSuggestion text
descstring?Optional description
triggerstring?Character that triggered this suggestion (e.g. "@", "#"). Used to determine replacement range.

ChatStatus

ValueDescription
"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