From e205e503df2794f86d0f2cfdf26e1280346c08b0 Mon Sep 17 00:00:00 2001 From: Jessica Cheng Date: Fri, 20 Mar 2026 15:39:30 -0400 Subject: [PATCH 1/9] Fix to the non-printable keys --- packages/web/src/browser-renderer.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/web/src/browser-renderer.ts b/packages/web/src/browser-renderer.ts index 8cfc7d18..278ab1ce 100644 --- a/packages/web/src/browser-renderer.ts +++ b/packages/web/src/browser-renderer.ts @@ -509,7 +509,7 @@ export class BrowserRenderer { meta: event.metaKey, shift: event.shiftKey, option: event.altKey, - sequence: event.key, + sequence: event.key.length === 1 ? event.key : "", number: false, raw: event.key, eventType: "press" as const, From 7b6a4e38ab2c1c6414b7002bee5040f4b1808a18 Mon Sep 17 00:00:00 2001 From: Jessica Cheng Date: Fri, 20 Mar 2026 17:08:56 -0400 Subject: [PATCH 2/9] added from OpentTUI to prompt input component --- demos/prompt-input.tsx | 1 + packages/docs/components/ui/demo-window.tsx | 4 +- .../content/docs/components/prompt-input.mdx | 2 +- .../components/prompt-input/prompt-input.tsx | 147 ++++++++++-------- 4 files changed, 83 insertions(+), 71 deletions(-) diff --git a/demos/prompt-input.tsx b/demos/prompt-input.tsx index 2601a91d..15db55c5 100644 --- a/demos/prompt-input.tsx +++ b/demos/prompt-input.tsx @@ -66,6 +66,7 @@ export function PromptInputApp() { files={files} placeholder="Message Claude..." showDividers + autoFocus useKeyboard={useKeyboard} onSubmit={handleSubmit} /> diff --git a/packages/docs/components/ui/demo-window.tsx b/packages/docs/components/ui/demo-window.tsx index ce6c036b..0481bf05 100644 --- a/packages/docs/components/ui/demo-window.tsx +++ b/packages/docs/components/ui/demo-window.tsx @@ -22,6 +22,7 @@ export interface DemoWindowProps { cols?: number rows?: number cursorHighlight?: boolean + autoFocus?: boolean children: ReactNode } @@ -32,6 +33,7 @@ export function DemoWindow({ cols = 80, rows = 24, cursorHighlight = false, + autoFocus = false, children, }: DemoWindowProps) { const { resolvedTheme } = usePageTheme() @@ -93,7 +95,7 @@ export function DemoWindow({
- + {children}
diff --git a/packages/docs/content/docs/components/prompt-input.mdx b/packages/docs/content/docs/components/prompt-input.mdx index e6e929e8..d3be88db 100644 --- a/packages/docs/content/docs/components/prompt-input.mdx +++ b/packages/docs/content/docs/components/prompt-input.mdx @@ -12,7 +12,7 @@ command history, and direct Vercel AI SDK integration. Supports both a self-contained default layout and a fully composable compound mode.
- +
diff --git a/packages/ui/components/prompt-input/prompt-input.tsx b/packages/ui/components/prompt-input/prompt-input.tsx index 3dcfa7b4..8dd46cc6 100644 --- a/packages/ui/components/prompt-input/prompt-input.tsx +++ b/packages/ui/components/prompt-input/prompt-input.tsx @@ -1,3 +1,4 @@ +// @ts-nocheck — OpenTUI intrinsic elements conflict with React's HTML/SVG types import { useState, useRef, @@ -108,6 +109,7 @@ export function PromptInputProvider({ initialInput = "", children }: PromptInput export interface PromptInputContextValue { value: string + isFocused: boolean disabled: boolean status?: ChatStatus onStop?: () => void @@ -121,6 +123,9 @@ export interface PromptInputContextValue { errorText: string model?: string theme: ReturnType + handleInput: (value: string) => void + handleInputSubmit: (value: string) => void + handleInputKeyDown: (key: any) => void } const PromptInputContext = createContext(null) @@ -187,6 +192,8 @@ export interface PromptInputProps { enableHistory?: boolean /** Model name displayed below the input */ model?: string + /** Whether the input is focused and accepting keystrokes */ + focus?: boolean /** Show horizontal dividers above and below the input */ showDividers?: boolean /** Auto-focus the input on mount (ensures canvas has keyboard focus in the browser) */ @@ -276,26 +283,31 @@ function PromptInputSuggestions() { ) } -const CURSOR_CHAR = "\u258D" - -/** Prompt char + text with syntax highlighting + cursor. */ +/** Prompt char + input element when focused, static text otherwise. */ function PromptInputTextarea() { - const { value, disabled, statusHintText, placeholder, prompt, promptColor, theme } = usePromptInput() + const { value, isFocused, disabled, statusHintText, placeholder, prompt, promptColor, theme, handleInput, handleInputSubmit, handleInputKeyDown } = usePromptInput() return ( - - {prompt} - {value.length === 0 ? ( - <> - {!disabled && {CURSOR_CHAR}} - {disabled ? statusHintText : " " + placeholder} - + + {prompt} + {isFocused ? ( + + ) : disabled && value.length === 0 ? ( + {statusHintText} ) : ( - <> - {value} - {!disabled && {CURSOR_CHAR}} - + {value || placeholder} )} - + ) } @@ -376,6 +388,7 @@ export function PromptInput({ maxSuggestions = 5, enableHistory = true, model, + focus = true, showDividers = true, autoFocus = false, useKeyboard: useKeyboardProp, @@ -397,6 +410,7 @@ export function PromptInput({ // Status-driven state const disabled = status ? status === "submitted" || status === "streaming" : disabledProp + const isFocused = focus && !disabled const statusHintText = resolveStatusHintText(status, submittedText, streamingLabel, errorText, disabledText) // ── Dual-mode state: provider-managed or self-managed ────────────────── @@ -518,56 +532,51 @@ export function PromptInput({ } }, [onSubmit, clearInput]) - // ── Keyboard handler ─────────────────────────────────────────────────── + // ── Input handlers (passed to intrinsic via context) ─────────── - useKeyboard?.((event: any) => { - // Escape during submitted/streaming calls onStop - if (event.name === "escape" && (status === "streaming" || status === "submitted") && onStop) { - onStop() - return + const handleInputSubmit = (text: string) => { + const trimmed = text.trim() + if (!trimmed) return + if (enableHistory) { + setHist([trimmed, ...historyRef.current]) } + updateValue("") + setHistI(-1) + handleSubmit(trimmed) + } - if (disabled) return - - if (event.name === "return") { - if (suggestionsRef.current.length > 0) { - const sel = suggestionsRef.current[sugIdxRef.current] - if (sel) { - if (valueRef.current.startsWith("/")) { - // Slash commands: submit immediately on selection - setSug([]) - updateValue("") - if (enableHistory) { - setHist([sel.text, ...historyRef.current]) - } - setHistI(-1) - handleSubmit(sel.text) - } else { - const base = valueRef.current.slice(0, valueRef.current.lastIndexOf("@")) - updateValue(base + sel.text + " ") - setSug([]) + const handleInputKeyDown = (key: any) => { + // Return with active suggestions: custom submit logic + if (key.name === "return" && suggestionsRef.current.length > 0) { + const sel = suggestionsRef.current[sugIdxRef.current] + if (sel) { + if (valueRef.current.startsWith("/")) { + // Slash commands: submit immediately on selection + updateValue("") + if (enableHistory) { + setHist([sel.text, ...historyRef.current]) } + setHistI(-1) + handleSubmit(sel.text) + } else { + const base = valueRef.current.slice(0, valueRef.current.lastIndexOf("@")) + updateValue(base + sel.text + " ") + setSug([]) } - } else { - const trimmed = valueRef.current.trim() - if (!trimmed) return - if (enableHistory) { - setHist([trimmed, ...historyRef.current]) - } - updateValue("") - setHistI(-1) - setSug([]) - handleSubmit(trimmed) } + key.preventDefault() return } - if (event.name === "tab" && suggestionsRef.current.length > 0) { + // Tab: cycle suggestions + if (key.name === "tab" && suggestionsRef.current.length > 0) { setSugI((sugIdxRef.current + 1) % suggestionsRef.current.length) + key.preventDefault() return } - if (event.name === "up") { + // Up: navigate suggestions or history + if (key.name === "up") { if (suggestionsRef.current.length > 0) { setSugI(Math.max(0, sugIdxRef.current - 1)) } else if (enableHistory && historyRef.current.length > 0) { @@ -575,10 +584,12 @@ export function PromptInput({ setHistI(idx) updateValue(historyRef.current[idx]!) } + key.preventDefault() return } - if (event.name === "down") { + // Down: navigate suggestions or history + if (key.name === "down") { if (suggestionsRef.current.length > 0) { setSugI(Math.min(suggestionsRef.current.length - 1, sugIdxRef.current + 1)) } else if (enableHistory && histIdxRef.current > 0) { @@ -589,31 +600,25 @@ export function PromptInput({ setHistI(-1) updateValue("") } + key.preventDefault() return } - if (event.name === "escape") { + // Escape: dismiss suggestions + if (key.name === "escape") { if (suggestionsRef.current.length > 0) { setSug([]) + key.preventDefault() } return } + } - // Character-level input fallback (used when intrinsic is not available, e.g. in tests) - if (event.name === "backspace" || event.name === "delete") { - updateValue(valueRef.current.slice(0, -1)) - return - } - - if (event.ctrl || event.meta) return - - if (event.name === "space") { - updateValue(valueRef.current + " ") - return - } + // ── Keyboard handler (Escape→onStop when input is disabled) ──────────── - if (event.name && event.name.length === 1) { - updateValue(valueRef.current + event.name) + useKeyboard?.((event: any) => { + if (event.name === "escape" && (status === "streaming" || status === "submitted") && onStop) { + onStop() } }) @@ -623,6 +628,7 @@ export function PromptInput({ const ctxValue: PromptInputContextValue = { value, + isFocused, disabled, status, onStop, @@ -636,6 +642,9 @@ export function PromptInput({ errorText, model, theme, + handleInput: updateValue, + handleInputSubmit, + handleInputKeyDown, } // ── Render ───────────────────────────────────────────────────────────── From dbfbb935eb5e5516a63049e363d1917a37ffbce8 Mon Sep 17 00:00:00 2001 From: Jessica Cheng Date: Fri, 20 Mar 2026 17:20:07 -0400 Subject: [PATCH 3/9] Fix unit tests: pass focus={false} to avoid RenderLib crash in test env The intrinsic requires the native RenderLib which isn't available in unit tests. Tests use mockUseKeyboard for keyboard simulation, so focus={false} prevents from rendering while the useKeyboard fallback handles character input. Also adds focus prop to ChatPanel for pass-through, and restores the full keyboard fallback in useKeyboard handler when not focused. Co-Authored-By: Claude Opus 4.6 (1M context) --- .../components/chat/chat.behavioral.test.tsx | 24 +++++ .../ui/components/chat/chat.snapshot.test.tsx | 4 + packages/ui/components/chat/chat.tsx | 4 + .../prompt-input.behavioral.test.tsx | 38 ++++---- .../prompt-input.snapshot.test.tsx | 2 +- .../components/prompt-input/prompt-input.tsx | 95 ++++++++++++++++++- 6 files changed, 146 insertions(+), 21 deletions(-) diff --git a/packages/ui/components/chat/chat.behavioral.test.tsx b/packages/ui/components/chat/chat.behavioral.test.tsx index 288a4ae2..deaf97a7 100644 --- a/packages/ui/components/chat/chat.behavioral.test.tsx +++ b/packages/ui/components/chat/chat.behavioral.test.tsx @@ -9,6 +9,7 @@ describe("ChatPanel behavior", () => { it("renders user messages with > prefix", () => { const { screen } = renderTui( {}} />, @@ -22,6 +23,7 @@ describe("ChatPanel behavior", () => { it("renders assistant messages with < prefix", () => { const { screen } = renderTui( {}} />, @@ -35,6 +37,7 @@ describe("ChatPanel behavior", () => { it("renders multiple messages in order", () => { const { screen } = renderTui( { it("renders streaming text with cursor", () => { const { screen } = renderTui( {}} @@ -68,6 +72,7 @@ describe("ChatPanel behavior", () => { it("renders loading indicator when isLoading and no streaming", () => { const { screen } = renderTui( {}} @@ -80,6 +85,7 @@ describe("ChatPanel behavior", () => { it("streaming text takes priority over loading", () => { const { screen } = renderTui( { it("renders tool call cards", () => { const { screen } = renderTui( { it("shows ellipsis for pending/in_progress tool calls", () => { const { screen } = renderTui( { it("does not show ellipsis for completed tool calls", () => { const { screen } = renderTui( { it("renders placeholder in input", () => { const { screen } = renderTui( {}} />, @@ -156,6 +166,7 @@ describe("ChatPanel behavior", () => { it("renders custom placeholder", () => { const { screen } = renderTui( {}} @@ -168,6 +179,7 @@ describe("ChatPanel behavior", () => { it("renders custom loading text", () => { const { screen } = renderTui( { it("handles empty messages array", () => { const { screen } = renderTui( {}} />, @@ -196,6 +209,7 @@ describe("ChatPanel behavior", () => { const mockUseKeyboard = (handler) => { savedHandler = handler } const tui = renderTui( { sent = text }} useKeyboard={mockUseKeyboard} @@ -215,6 +229,7 @@ describe("ChatPanel behavior", () => { const mockUseKeyboard = (handler) => { savedHandler = handler } const tui = renderTui( { sent = text }} useKeyboard={mockUseKeyboard} @@ -232,6 +247,7 @@ describe("ChatPanel behavior", () => { const mockUseKeyboard = (handler) => { savedHandler = handler } const tui = renderTui( { sent = text }} useKeyboard={mockUseKeyboard} @@ -252,6 +268,7 @@ describe("ChatPanel behavior", () => { const mockUseKeyboard = (handler) => { savedHandler = handler } const tui = renderTui( { sent = text }} @@ -271,6 +288,7 @@ describe("ChatPanel behavior", () => { const mockUseKeyboard = (handler) => { savedHandler = handler } const tui = renderTui( { cancelled = true }} @@ -290,6 +308,7 @@ describe("ChatPanel behavior", () => { const mockUseKeyboard = (handler) => { savedHandler = handler } const tui = renderTui( { cancelled = true }} @@ -308,6 +327,7 @@ describe("ChatPanel behavior", () => { it("shows loading indicator when status is submitted", () => { const { screen } = renderTui( {}} @@ -320,6 +340,7 @@ describe("ChatPanel behavior", () => { it("shows streaming text when status is streaming", () => { const { screen } = renderTui( { const mockUseKeyboard = (handler) => { savedHandler = handler } const tui = renderTui( { sent = text }} @@ -355,6 +377,7 @@ describe("ChatPanel behavior", () => { const mockUseKeyboard = (handler) => { savedHandler = handler } const tui = renderTui( { const mockUseKeyboard = (handler) => { savedHandler = handler } const tui = renderTui( { sent = text }} diff --git a/packages/ui/components/chat/chat.snapshot.test.tsx b/packages/ui/components/chat/chat.snapshot.test.tsx index c2a7166f..c924288d 100644 --- a/packages/ui/components/chat/chat.snapshot.test.tsx +++ b/packages/ui/components/chat/chat.snapshot.test.tsx @@ -9,6 +9,7 @@ describe("ChatPanel snapshots", () => { it("renders messages with input", () => { const { screen } = renderTui( { it("renders with streaming text", () => { const { screen } = renderTui( {}} @@ -35,6 +37,7 @@ describe("ChatPanel snapshots", () => { it("renders with loading state", () => { const { screen } = renderTui( {}} @@ -47,6 +50,7 @@ describe("ChatPanel snapshots", () => { it("renders with tool calls", () => { const { screen } = renderTui( void) => void } @@ -158,6 +160,7 @@ export function ChatPanel({ userColor, assistantColor, loadingText = "Thinking...", + focus, useKeyboard: useKeyboardProp, }: ChatPanelProps) { const theme = useTheme() @@ -217,6 +220,7 @@ export function ChatPanel({ promptColor={resolvedPromptColor} foregroundColor={theme.foreground} submittedText={loadingText} + focus={focus} useKeyboard={useKeyboardProp} /> diff --git a/packages/ui/components/prompt-input/prompt-input.behavioral.test.tsx b/packages/ui/components/prompt-input/prompt-input.behavioral.test.tsx index 091809e5..d0eb5666 100644 --- a/packages/ui/components/prompt-input/prompt-input.behavioral.test.tsx +++ b/packages/ui/components/prompt-input/prompt-input.behavioral.test.tsx @@ -10,7 +10,7 @@ describe("PromptInput behavior", () => { it("renders prompt and placeholder", () => { const { screen } = renderTui( - , + , { cols: 40, rows: 4 }, ) const text = screen.text() @@ -20,26 +20,18 @@ describe("PromptInput behavior", () => { it("renders custom prompt", () => { const { screen } = renderTui( - , + , { cols: 40, rows: 4 }, ) expect(screen.text()).toContain(">") }) - it("shows cursor when not disabled", () => { + it("shows placeholder when not focused", () => { const { screen } = renderTui( - , + , { cols: 40, rows: 4 }, ) - expect(screen.text()).toContain("\u258D") - }) - - it("hides cursor when disabled", () => { - const { screen } = renderTui( - , - { cols: 40, rows: 4 }, - ) - expect(screen.text()).not.toContain("\u258D") + expect(screen.text()).toContain("Type here...") }) it("shows disabled text when disabled", () => { @@ -58,6 +50,7 @@ describe("PromptInput behavior", () => { const mockUseKeyboard = (handler) => { savedHandler = handler } renderTui( { submitted = msg.text }} />, @@ -75,6 +68,7 @@ describe("PromptInput behavior", () => { const mockUseKeyboard = (handler) => { savedHandler = handler } renderTui( { submitted = msg.text }} />, @@ -90,6 +84,7 @@ describe("PromptInput behavior", () => { const mockUseKeyboard = (handler) => { savedHandler = handler } renderTui( { changed = text }} onSubmit={() => {}} @@ -108,6 +103,7 @@ describe("PromptInput behavior", () => { const mockUseKeyboard = (handler) => { savedHandler = handler } renderTui( { changed = text }} />, @@ -125,6 +121,7 @@ describe("PromptInput behavior", () => { const mockUseKeyboard = (handler) => { savedHandler = handler } renderTui( { changed = text }} />, @@ -159,6 +156,7 @@ describe("PromptInput behavior", () => { const mockUseKeyboard = (handler) => { savedHandler = handler } renderTui( { changed = text }} />, @@ -278,6 +276,7 @@ describe("PromptInput behavior", () => { const mockUseKeyboard = (handler) => { savedHandler = handler } renderTui( { changed = text }} onSubmit={() => {}} @@ -445,7 +444,7 @@ describe("PromptInput behavior", () => { it("shows error text when status is error", () => { const { screen } = renderTui( - , + , { cols: 40, rows: 4 }, ) expect(screen.text()).toContain("Something went wrong") @@ -457,7 +456,7 @@ describe("PromptInput behavior", () => { describe("PromptInput compound mode", () => { it("renders compound subcomponents when children provided", () => { const { screen } = renderTui( - + @@ -476,7 +475,7 @@ describe("PromptInput compound mode", () => { it("renders submit icon for ready status", () => { const { screen } = renderTui( - + , { cols: 40, rows: 4 }, @@ -506,7 +505,7 @@ describe("PromptInput compound mode", () => { it("renders submit icon for error status", () => { const { screen } = renderTui( - + , { cols: 40, rows: 4 }, @@ -526,7 +525,7 @@ describe("PromptInput compound mode", () => { it("hides StatusText when not in error", () => { const { screen } = renderTui( - + , { cols: 40, rows: 4 }, @@ -539,7 +538,7 @@ describe("PromptInput compound mode", () => { let savedHandler = null const mockUseKeyboard = (handler) => { savedHandler = handler } renderTui( - { submitted = msg.text }}> + { submitted = msg.text }}> , { cols: 40, rows: 4 }, @@ -556,6 +555,7 @@ describe("PromptInput compound mode", () => { const mockUseKeyboard = (handler) => { savedHandler = handler } renderTui( { submitted = msg.text }} diff --git a/packages/ui/components/prompt-input/prompt-input.snapshot.test.tsx b/packages/ui/components/prompt-input/prompt-input.snapshot.test.tsx index 35c52c30..c46572ee 100644 --- a/packages/ui/components/prompt-input/prompt-input.snapshot.test.tsx +++ b/packages/ui/components/prompt-input/prompt-input.snapshot.test.tsx @@ -8,7 +8,7 @@ afterEach(() => cleanup()) describe("PromptInput snapshots", () => { it("renders default state", () => { const { screen } = renderTui( - , + , { cols: 40, rows: 4 }, ) expect(screen.text()).toMatchSnapshot() diff --git a/packages/ui/components/prompt-input/prompt-input.tsx b/packages/ui/components/prompt-input/prompt-input.tsx index 8dd46cc6..5e4bc809 100644 --- a/packages/ui/components/prompt-input/prompt-input.tsx +++ b/packages/ui/components/prompt-input/prompt-input.tsx @@ -614,11 +614,104 @@ export function PromptInput({ } } - // ── Keyboard handler (Escape→onStop when input is disabled) ──────────── + // ── Keyboard handler ─────────────────────────────────────────────────── + // When focused, handles text input; useKeyboard only handles Escape→onStop. + // When not focused (no rendered), useKeyboard provides full keyboard fallback + // for character input, suggestions, and history (used in tests and unfocused state). useKeyboard?.((event: any) => { + // Escape during submitted/streaming calls onStop (always active) if (event.name === "escape" && (status === "streaming" || status === "submitted") && onStop) { onStop() + return + } + + // When is rendered, it handles everything else + if (isFocused) return + + if (disabled) return + + if (event.name === "return") { + if (suggestionsRef.current.length > 0) { + const sel = suggestionsRef.current[sugIdxRef.current] + if (sel) { + if (valueRef.current.startsWith("/")) { + updateValue("") + if (enableHistory) { + setHist([sel.text, ...historyRef.current]) + } + setHistI(-1) + handleSubmit(sel.text) + } else { + const base = valueRef.current.slice(0, valueRef.current.lastIndexOf("@")) + updateValue(base + sel.text + " ") + setSug([]) + } + } + } else { + const trimmed = valueRef.current.trim() + if (!trimmed) return + if (enableHistory) { + setHist([trimmed, ...historyRef.current]) + } + updateValue("") + setHistI(-1) + handleSubmit(trimmed) + } + return + } + + if (event.name === "tab" && suggestionsRef.current.length > 0) { + setSugI((sugIdxRef.current + 1) % suggestionsRef.current.length) + return + } + + if (event.name === "up") { + if (suggestionsRef.current.length > 0) { + setSugI(Math.max(0, sugIdxRef.current - 1)) + } else if (enableHistory && historyRef.current.length > 0) { + const idx = Math.min(historyRef.current.length - 1, histIdxRef.current + 1) + setHistI(idx) + updateValue(historyRef.current[idx]!) + } + return + } + + if (event.name === "down") { + if (suggestionsRef.current.length > 0) { + setSugI(Math.min(suggestionsRef.current.length - 1, sugIdxRef.current + 1)) + } else if (enableHistory && histIdxRef.current > 0) { + const nextIdx = histIdxRef.current - 1 + setHistI(nextIdx) + updateValue(historyRef.current[nextIdx]!) + } else if (enableHistory && histIdxRef.current === 0) { + setHistI(-1) + updateValue("") + } + return + } + + if (event.name === "escape") { + if (suggestionsRef.current.length > 0) { + setSug([]) + } + return + } + + if (event.name === "backspace" || event.name === "delete") { + updateValue(valueRef.current.slice(0, -1)) + return + } + + if (event.ctrl || event.meta) return + + if (event.name === "space") { + updateValue(valueRef.current + " ") + return + } + + if (event.name && event.name.length === 1) { + updateValue(valueRef.current + event.name) } }) From cef6cf853f1206af73d364bbb4ae60f7a61bf771 Mon Sep 17 00:00:00 2001 From: Jessica Cheng Date: Fri, 20 Mar 2026 17:25:58 -0400 Subject: [PATCH 4/9] Fix remaining tests missing focus={false} Ensure all PromptInput and ChatPanel test usages pass focus={false} to prevent intrinsic from rendering in unit test env. Delete stale snapshots to allow regeneration. Co-Authored-By: Claude Opus 4.6 (1M context) --- .../__snapshots__/chat.snapshot.test.tsx.snap | 38 ------------------- .../prompt-input.snapshot.test.tsx.snap | 13 ------- .../prompt-input.behavioral.test.tsx | 11 +++++- 3 files changed, 10 insertions(+), 52 deletions(-) delete mode 100644 packages/ui/components/chat/__snapshots__/chat.snapshot.test.tsx.snap delete mode 100644 packages/ui/components/prompt-input/__snapshots__/prompt-input.snapshot.test.tsx.snap diff --git a/packages/ui/components/chat/__snapshots__/chat.snapshot.test.tsx.snap b/packages/ui/components/chat/__snapshots__/chat.snapshot.test.tsx.snap deleted file mode 100644 index cf0501ab..00000000 --- a/packages/ui/components/chat/__snapshots__/chat.snapshot.test.tsx.snap +++ /dev/null @@ -1,38 +0,0 @@ -// Bun Snapshot v1, https://bun.sh/docs/test/snapshots - -exports[`ChatPanel snapshots renders messages with input 1`] = ` -" > Hello! - < Hi there! - -────────────────────────────────────────────────── - > ▍ Type a message... -──────────────────────────────────────────────────" -`; - -exports[`ChatPanel snapshots renders with streaming text 1`] = ` -" > Tell me a story - < Once upon a time_ - -────────────────────────────────────────────────── - > Generating... -──────────────────────────────────────────────────" -`; - -exports[`ChatPanel snapshots renders with loading state 1`] = ` -" > Hello - Thinking... - -────────────────────────────────────────────────── - > Thinking... -──────────────────────────────────────────────────" -`; - -exports[`ChatPanel snapshots renders with tool calls 1`] = ` -" > Read my file - ⠋ Read file ... - ✓ Edit file - -────────────────────────────────────────────────── - > ▍ Type a message... -──────────────────────────────────────────────────" -`; diff --git a/packages/ui/components/prompt-input/__snapshots__/prompt-input.snapshot.test.tsx.snap b/packages/ui/components/prompt-input/__snapshots__/prompt-input.snapshot.test.tsx.snap deleted file mode 100644 index 7baea230..00000000 --- a/packages/ui/components/prompt-input/__snapshots__/prompt-input.snapshot.test.tsx.snap +++ /dev/null @@ -1,13 +0,0 @@ -// Bun Snapshot v1, https://bun.sh/docs/test/snapshots - -exports[`PromptInput snapshots renders default state 1`] = ` -"──────────────────────────────────────── - ❯ ▍ Type a message... -────────────────────────────────────────" -`; - -exports[`PromptInput snapshots renders disabled state 1`] = ` -"──────────────────────────────────────── - ❯ Generating... -────────────────────────────────────────" -`; diff --git a/packages/ui/components/prompt-input/prompt-input.behavioral.test.tsx b/packages/ui/components/prompt-input/prompt-input.behavioral.test.tsx index d0eb5666..8b3d8b59 100644 --- a/packages/ui/components/prompt-input/prompt-input.behavioral.test.tsx +++ b/packages/ui/components/prompt-input/prompt-input.behavioral.test.tsx @@ -174,6 +174,7 @@ describe("PromptInput behavior", () => { const mockUseKeyboard = (handler) => { savedHandler = handler } renderTui( { const mockUseKeyboard = (handler) => { savedHandler = handler } renderTui( { const mockUseKeyboard = (handler) => { savedHandler = handler } renderTui( { submitted = msg.text }} @@ -235,6 +238,7 @@ describe("PromptInput behavior", () => { const mockUseKeyboard = (handler) => { savedHandler = handler } renderTui( { changed = text }} @@ -257,6 +261,7 @@ describe("PromptInput behavior", () => { const mockUseKeyboard = (handler) => { savedHandler = handler } renderTui( { changed = text }} @@ -307,6 +312,7 @@ describe("PromptInput behavior", () => { const mockUseKeyboard = (handler) => { savedHandler = handler } renderTui( { changed = text }} @@ -396,6 +402,7 @@ describe("PromptInput behavior", () => { const mockUseKeyboard = (handler) => { savedHandler = handler } renderTui( { stopped = true }} useKeyboard={mockUseKeyboard} @@ -412,6 +419,7 @@ describe("PromptInput behavior", () => { const mockUseKeyboard = (handler) => { savedHandler = handler } renderTui( { submitted = msg.text }} @@ -430,6 +438,7 @@ describe("PromptInput behavior", () => { const mockUseKeyboard = (handler) => { savedHandler = handler } renderTui( { submitted = msg.text }} @@ -515,7 +524,7 @@ describe("PromptInput compound mode", () => { it("shows error text via StatusText subcomponent", () => { const { screen } = renderTui( - + , { cols: 40, rows: 4 }, From feb7e40e128171c9e6ad3d4a5260cdc933f82d68 Mon Sep 17 00:00:00 2001 From: Jessica Cheng Date: Fri, 20 Mar 2026 17:31:27 -0400 Subject: [PATCH 5/9] up date prompt inpt component in registry folder. --- opentui | 1 + packages/ui/registry/ui/prompt-input.tsx | 144 +++++++++++++++++++---- thoughtful-ai | 1 + 3 files changed, 125 insertions(+), 21 deletions(-) create mode 160000 opentui create mode 160000 thoughtful-ai diff --git a/opentui b/opentui new file mode 160000 index 00000000..44cb164f --- /dev/null +++ b/opentui @@ -0,0 +1 @@ +Subproject commit 44cb164ffd97548cf6964890a30a41b16531fee3 diff --git a/packages/ui/registry/ui/prompt-input.tsx b/packages/ui/registry/ui/prompt-input.tsx index c2950e0b..04de007c 100644 --- a/packages/ui/registry/ui/prompt-input.tsx +++ b/packages/ui/registry/ui/prompt-input.tsx @@ -1,3 +1,4 @@ +// @ts-nocheck — OpenTUI intrinsic elements conflict with React's HTML/SVG types import { useState, useRef, @@ -108,6 +109,7 @@ export function PromptInputProvider({ initialInput = "", children }: PromptInput export interface PromptInputContextValue { value: string + isFocused: boolean disabled: boolean status?: ChatStatus onStop?: () => void @@ -121,6 +123,9 @@ export interface PromptInputContextValue { errorText: string model?: string theme: ReturnType + handleInput: (value: string) => void + handleInputSubmit: (value: string) => void + handleInputKeyDown: (key: any) => void } const PromptInputContext = createContext(null) @@ -187,6 +192,8 @@ export interface PromptInputProps { enableHistory?: boolean /** Model name displayed below the input */ model?: string + /** Whether the input is focused and accepting keystrokes */ + focus?: boolean /** Show horizontal dividers above and below the input */ showDividers?: boolean /** Auto-focus the input on mount (ensures canvas has keyboard focus in the browser) */ @@ -276,26 +283,31 @@ function PromptInputSuggestions() { ) } -const CURSOR_CHAR = "\u258D" - -/** Prompt char + text with syntax highlighting + cursor. */ +/** Prompt char + input element when focused, static text otherwise. */ function PromptInputTextarea() { - const { value, disabled, statusHintText, placeholder, prompt, promptColor, theme } = usePromptInput() + const { value, isFocused, disabled, statusHintText, placeholder, prompt, promptColor, theme, handleInput, handleInputSubmit, handleInputKeyDown } = usePromptInput() return ( - - {prompt} - {value.length === 0 ? ( - <> - {!disabled && {CURSOR_CHAR}} - {disabled ? statusHintText : " " + placeholder} - + + {prompt} + {isFocused ? ( + + ) : disabled && value.length === 0 ? ( + {statusHintText} ) : ( - <> - {value} - {!disabled && {CURSOR_CHAR}} - + {value || placeholder} )} - + ) } @@ -376,6 +388,7 @@ export function PromptInput({ maxSuggestions = 5, enableHistory = true, model, + focus = true, showDividers = true, autoFocus = false, useKeyboard: useKeyboardProp, @@ -397,6 +410,7 @@ export function PromptInput({ // Status-driven state const disabled = status ? status === "submitted" || status === "streaming" : disabledProp + const isFocused = focus && !disabled const statusHintText = resolveStatusHintText(status, submittedText, streamingLabel, errorText, disabledText) // ── Dual-mode state: provider-managed or self-managed ────────────────── @@ -518,15 +532,103 @@ export function PromptInput({ } }, [onSubmit, clearInput]) + // ── Input handlers (passed to intrinsic via context) ─────────── + + const handleInputSubmit = (text: string) => { + const trimmed = text.trim() + if (!trimmed) return + if (enableHistory) { + setHist([trimmed, ...historyRef.current]) + } + updateValue("") + setHistI(-1) + handleSubmit(trimmed) + } + + const handleInputKeyDown = (key: any) => { + // Return with active suggestions: custom submit logic + if (key.name === "return" && suggestionsRef.current.length > 0) { + const sel = suggestionsRef.current[sugIdxRef.current] + if (sel) { + if (valueRef.current.startsWith("/")) { + // Slash commands: submit immediately on selection + updateValue("") + if (enableHistory) { + setHist([sel.text, ...historyRef.current]) + } + setHistI(-1) + handleSubmit(sel.text) + } else { + const base = valueRef.current.slice(0, valueRef.current.lastIndexOf("@")) + updateValue(base + sel.text + " ") + setSug([]) + } + } + key.preventDefault() + return + } + + // Tab: cycle suggestions + if (key.name === "tab" && suggestionsRef.current.length > 0) { + setSugI((sugIdxRef.current + 1) % suggestionsRef.current.length) + key.preventDefault() + return + } + + // Up: navigate suggestions or history + if (key.name === "up") { + if (suggestionsRef.current.length > 0) { + setSugI(Math.max(0, sugIdxRef.current - 1)) + } else if (enableHistory && historyRef.current.length > 0) { + const idx = Math.min(historyRef.current.length - 1, histIdxRef.current + 1) + setHistI(idx) + updateValue(historyRef.current[idx]!) + } + key.preventDefault() + return + } + + // Down: navigate suggestions or history + if (key.name === "down") { + if (suggestionsRef.current.length > 0) { + setSugI(Math.min(suggestionsRef.current.length - 1, sugIdxRef.current + 1)) + } else if (enableHistory && histIdxRef.current > 0) { + const nextIdx = histIdxRef.current - 1 + setHistI(nextIdx) + updateValue(historyRef.current[nextIdx]!) + } else if (enableHistory && histIdxRef.current === 0) { + setHistI(-1) + updateValue("") + } + key.preventDefault() + return + } + + // Escape: dismiss suggestions + if (key.name === "escape") { + if (suggestionsRef.current.length > 0) { + setSug([]) + key.preventDefault() + } + return + } + } + // ── Keyboard handler ─────────────────────────────────────────────────── + // When focused, handles text input; useKeyboard only handles Escape→onStop. + // When not focused (no rendered), useKeyboard provides full keyboard fallback + // for character input, suggestions, and history (used in tests and unfocused state). useKeyboard?.((event: any) => { - // Escape during submitted/streaming calls onStop + // Escape during submitted/streaming calls onStop (always active) if (event.name === "escape" && (status === "streaming" || status === "submitted") && onStop) { onStop() return } + // When is rendered, it handles everything else + if (isFocused) return + if (disabled) return if (event.name === "return") { @@ -534,8 +636,6 @@ export function PromptInput({ const sel = suggestionsRef.current[sugIdxRef.current] if (sel) { if (valueRef.current.startsWith("/")) { - // Slash commands: submit immediately on selection - setSug([]) updateValue("") if (enableHistory) { setHist([sel.text, ...historyRef.current]) @@ -556,7 +656,6 @@ export function PromptInput({ } updateValue("") setHistI(-1) - setSug([]) handleSubmit(trimmed) } return @@ -599,7 +698,6 @@ export function PromptInput({ return } - // Character-level input fallback (used when intrinsic is not available, e.g. in tests) if (event.name === "backspace" || event.name === "delete") { updateValue(valueRef.current.slice(0, -1)) return @@ -623,6 +721,7 @@ export function PromptInput({ const ctxValue: PromptInputContextValue = { value, + isFocused, disabled, status, onStop, @@ -636,6 +735,9 @@ export function PromptInput({ errorText, model, theme, + handleInput: updateValue, + handleInputSubmit, + handleInputKeyDown, } // ── Render ───────────────────────────────────────────────────────────── diff --git a/thoughtful-ai b/thoughtful-ai new file mode 160000 index 00000000..b48fde08 --- /dev/null +++ b/thoughtful-ai @@ -0,0 +1 @@ +Subproject commit b48fde08b8bffb44be428b0bc8e196f9166b4eab From 9c7206483efe282d5965ca959b992bd56a386660 Mon Sep 17 00:00:00 2001 From: Jessica Cheng Date: Fri, 20 Mar 2026 17:56:37 -0400 Subject: [PATCH 6/9] added auto focus to demos --- demos/spinner.tsx | 9 ++------- .../docs/components/demos/ai-chat-interface-demo.tsx | 2 +- packages/docs/components/demos/chain-of-thought-demo.tsx | 2 +- packages/docs/components/demos/spinner-demo.tsx | 2 +- packages/docs/components/demos/tabs-demo.tsx | 2 +- packages/docs/components/demos/text-input-demo.tsx | 2 +- packages/docs/content/docs/components/ascii.mdx | 2 +- packages/docs/content/docs/components/chat.mdx | 2 +- packages/docs/content/docs/components/gradient.mdx | 2 +- packages/docs/content/docs/components/link.mdx | 2 +- packages/docs/content/docs/components/modal.mdx | 2 +- packages/docs/content/docs/components/multi-select.mdx | 2 +- packages/docs/content/docs/components/primitives.mdx | 2 +- packages/docs/content/docs/components/select-input.mdx | 2 +- packages/docs/content/docs/components/status-bar.mdx | 2 +- .../docs/content/docs/components/terminal-window.mdx | 2 +- packages/docs/content/docs/components/tui.mdx | 2 +- .../docs/content/docs/guides/focus-and-navigation.mdx | 2 +- 18 files changed, 19 insertions(+), 24 deletions(-) diff --git a/demos/spinner.tsx b/demos/spinner.tsx index 063c74a0..5888a01b 100644 --- a/demos/spinner.tsx +++ b/demos/spinner.tsx @@ -1,16 +1,11 @@ // @ts-nocheck import { useKeyboard } from "@gridland/utils" -import { SpinnerPicker, StatusBar } from "@gridland/ui" +import { SpinnerPicker } from "@gridland/ui" export function SpinnerApp() { return ( - - - - - - + ) } diff --git a/packages/docs/components/demos/ai-chat-interface-demo.tsx b/packages/docs/components/demos/ai-chat-interface-demo.tsx index 459659c0..16f6f0c2 100644 --- a/packages/docs/components/demos/ai-chat-interface-demo.tsx +++ b/packages/docs/components/demos/ai-chat-interface-demo.tsx @@ -78,7 +78,7 @@ function AIChatInterfaceApp() { export default function AIChatInterfaceDemo() { return ( - + ) diff --git a/packages/docs/components/demos/chain-of-thought-demo.tsx b/packages/docs/components/demos/chain-of-thought-demo.tsx index 3a88dcc3..092f9c46 100644 --- a/packages/docs/components/demos/chain-of-thought-demo.tsx +++ b/packages/docs/components/demos/chain-of-thought-demo.tsx @@ -6,7 +6,7 @@ import { ChainOfThought, ChainOfThoughtHeader, ChainOfThoughtContent, ChainOfTho export default function ChainOfThoughtDemo() { return ( - + ) diff --git a/packages/docs/components/demos/spinner-demo.tsx b/packages/docs/components/demos/spinner-demo.tsx index a4152a43..71b46668 100644 --- a/packages/docs/components/demos/spinner-demo.tsx +++ b/packages/docs/components/demos/spinner-demo.tsx @@ -6,7 +6,7 @@ import { Spinner, SpinnerShowcase } from "@gridland/ui" export default function SpinnerPickerDemo() { return ( - + ) diff --git a/packages/docs/components/demos/tabs-demo.tsx b/packages/docs/components/demos/tabs-demo.tsx index 78fc7284..aaa8f92a 100644 --- a/packages/docs/components/demos/tabs-demo.tsx +++ b/packages/docs/components/demos/tabs-demo.tsx @@ -109,7 +109,7 @@ function ContentTabBarApp() { export function TabsSimpleDemo() { return ( - + ) diff --git a/packages/docs/components/demos/text-input-demo.tsx b/packages/docs/components/demos/text-input-demo.tsx index 0d9fdfe6..54065efb 100644 --- a/packages/docs/components/demos/text-input-demo.tsx +++ b/packages/docs/components/demos/text-input-demo.tsx @@ -48,7 +48,7 @@ function TextInputPickerApp() { export function TextInputPickerDemo() { return ( - + ) diff --git a/packages/docs/content/docs/components/ascii.mdx b/packages/docs/content/docs/components/ascii.mdx index efce48fd..0c753b6a 100644 --- a/packages/docs/content/docs/components/ascii.mdx +++ b/packages/docs/content/docs/components/ascii.mdx @@ -11,7 +11,7 @@ Renders text as large ASCII art using the `` OpenTUI intrinsic. Supports multiple font styles and custom colors.
- +
diff --git a/packages/docs/content/docs/components/chat.mdx b/packages/docs/content/docs/components/chat.mdx index cce9c280..8a938dee 100644 --- a/packages/docs/content/docs/components/chat.mdx +++ b/packages/docs/content/docs/components/chat.mdx @@ -12,7 +12,7 @@ call status cards, streaming text, a loading indicator, and a `PromptInput`. SDK-agnostic — the consumer passes messages and handles submission.
- +
diff --git a/packages/docs/content/docs/components/gradient.mdx b/packages/docs/content/docs/components/gradient.mdx index a533680c..2b461fe0 100644 --- a/packages/docs/content/docs/components/gradient.mdx +++ b/packages/docs/content/docs/components/gradient.mdx @@ -11,7 +11,7 @@ Applies a color gradient across text characters. Choose from 13 named presets or provide custom hex colors.
- +
diff --git a/packages/docs/content/docs/components/link.mdx b/packages/docs/content/docs/components/link.mdx index a9fcda29..44d9fc27 100644 --- a/packages/docs/content/docs/components/link.mdx +++ b/packages/docs/content/docs/components/link.mdx @@ -11,7 +11,7 @@ A clickable hyperlink for terminal UIs with configurable underline style and color.
- +
diff --git a/packages/docs/content/docs/components/modal.mdx b/packages/docs/content/docs/components/modal.mdx index ed4efa62..56992cbf 100644 --- a/packages/docs/content/docs/components/modal.mdx +++ b/packages/docs/content/docs/components/modal.mdx @@ -11,7 +11,7 @@ A bordered container that overlays content, optionally displays a title, and listens for the Escape key to trigger a close callback.
- +
diff --git a/packages/docs/content/docs/components/multi-select.mdx b/packages/docs/content/docs/components/multi-select.mdx index fef5a1a8..ea8d2d43 100644 --- a/packages/docs/content/docs/components/multi-select.mdx +++ b/packages/docs/content/docs/components/multi-select.mdx @@ -11,7 +11,7 @@ A multi-selection list with checkbox indicators, keyboard navigation, group headers, a submit row, and a submitted state.
- +
diff --git a/packages/docs/content/docs/components/primitives.mdx b/packages/docs/content/docs/components/primitives.mdx index 00c3b03b..d3fa7a81 100644 --- a/packages/docs/content/docs/components/primitives.mdx +++ b/packages/docs/content/docs/components/primitives.mdx @@ -12,7 +12,7 @@ Gridland uses JSX intrinsic elements that map to renderable components in the op These are used inside ``.
- +
diff --git a/packages/docs/content/docs/components/select-input.mdx b/packages/docs/content/docs/components/select-input.mdx index 941eb27c..c943c2ba 100644 --- a/packages/docs/content/docs/components/select-input.mdx +++ b/packages/docs/content/docs/components/select-input.mdx @@ -11,7 +11,7 @@ A single-selection list with radio indicators, keyboard navigation, group headers, and a submitted state.
- +
diff --git a/packages/docs/content/docs/components/status-bar.mdx b/packages/docs/content/docs/components/status-bar.mdx index f23a7e4e..20b21d2a 100644 --- a/packages/docs/content/docs/components/status-bar.mdx +++ b/packages/docs/content/docs/components/status-bar.mdx @@ -11,7 +11,7 @@ A horizontal bar that displays keybinding hints with key labels and descriptions. Commonly placed at the bottom of a view.
- +
diff --git a/packages/docs/content/docs/components/terminal-window.mdx b/packages/docs/content/docs/components/terminal-window.mdx index ad54cd79..86f0d334 100644 --- a/packages/docs/content/docs/components/terminal-window.mdx +++ b/packages/docs/content/docs/components/terminal-window.mdx @@ -11,7 +11,7 @@ A decorative container that mimics a terminal window with traffic light buttons and an optional centered title. Defaults to a dark gray background.
- +
diff --git a/packages/docs/content/docs/components/tui.mdx b/packages/docs/content/docs/components/tui.mdx index 01b18772..ebe852c1 100644 --- a/packages/docs/content/docs/components/tui.mdx +++ b/packages/docs/content/docs/components/tui.mdx @@ -13,7 +13,7 @@ canvas. It handles client detection, canvas creation, React reconciler setup, and automatic resizing. Gridland is built on the [opentui](https://opentui.com) engine.
- +
diff --git a/packages/docs/content/docs/guides/focus-and-navigation.mdx b/packages/docs/content/docs/guides/focus-and-navigation.mdx index 81f0cc27..50fc0fef 100644 --- a/packages/docs/content/docs/guides/focus-and-navigation.mdx +++ b/packages/docs/content/docs/guides/focus-and-navigation.mdx @@ -13,7 +13,7 @@ import { CursorHighlightApp } from '@demos/cursor-highlight' Gridland provides a unified input system where focus, keyboard routing, mouse/pointer events, and tab navigation work identically in both terminal and web runtimes.
- +
From 6e8ebc54027390831f7bd07aa4613246c0da122c Mon Sep 17 00:00:00 2001 From: Jessica Cheng Date: Fri, 20 Mar 2026 22:14:48 -0400 Subject: [PATCH 7/9] Revert non-printable keys fix in sequence field Co-Authored-By: Claude Opus 4.6 (1M context) --- packages/web/src/browser-renderer.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/web/src/browser-renderer.ts b/packages/web/src/browser-renderer.ts index 278ab1ce..8cfc7d18 100644 --- a/packages/web/src/browser-renderer.ts +++ b/packages/web/src/browser-renderer.ts @@ -509,7 +509,7 @@ export class BrowserRenderer { meta: event.metaKey, shift: event.shiftKey, option: event.altKey, - sequence: event.key.length === 1 ? event.key : "", + sequence: event.key, number: false, raw: event.key, eventType: "press" as const, From 3fed8e91229b335bf932978a62a177bae36ce905 Mon Sep 17 00:00:00 2001 From: Jessica Cheng Date: Fri, 20 Mar 2026 22:15:03 -0400 Subject: [PATCH 8/9] Revert non-printable keys fix in sequence field Co-Authored-By: Claude Opus 4.6 (1M context) --- packages/web/src/browser-renderer.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/web/src/browser-renderer.ts b/packages/web/src/browser-renderer.ts index 278ab1ce..8cfc7d18 100644 --- a/packages/web/src/browser-renderer.ts +++ b/packages/web/src/browser-renderer.ts @@ -509,7 +509,7 @@ export class BrowserRenderer { meta: event.metaKey, shift: event.shiftKey, option: event.altKey, - sequence: event.key.length === 1 ? event.key : "", + sequence: event.key, number: false, raw: event.key, eventType: "press" as const, From f894ed9ce13ab677e9a87f5b6234753a22dcd040 Mon Sep 17 00:00:00 2001 From: Jessica Cheng Date: Fri, 20 Mar 2026 22:22:38 -0400 Subject: [PATCH 9/9] remove thoughtful ai --- thoughtful-ai | 1 - 1 file changed, 1 deletion(-) delete mode 160000 thoughtful-ai diff --git a/thoughtful-ai b/thoughtful-ai deleted file mode 160000 index b48fde08..00000000 --- a/thoughtful-ai +++ /dev/null @@ -1 +0,0 @@ -Subproject commit b48fde08b8bffb44be428b0bc8e196f9166b4eab