"use client" import { IconBolt as Zap } from "nucleo-pixel" import * as React from "react" import { useResolvedTerminalTheme } from "@/hooks/use-resolved-terminal-theme" import { cn } from "@/lib/utils" /** Default composer meta — lightning level indicator (e.g. max). */ export function InputLevelMeta({ children = "max" }: { children?: React.ReactNode }) { return ( {children} ) } export type InputProps = { value?: string defaultValue?: string onValueChange?: (value: string) => void onSubmit?: (value: string) => void prompt?: string status?: React.ReactNode /** Footer hint on the left — composer (default) and Claude stacked layouts. */ metaLeft?: React.ReactNode /** Footer hint on the right — composer (default) and Claude stacked layouts. */ metaRight?: React.ReactNode placeholder?: string disabled?: boolean /** Keep the blinking cursor visible even when unfocused. */ showCursor?: boolean /** * `auto` — composer on default, inline box on grok, stacked lines on Claude. * `inline` — status on the right in a bordered box. `stacked` — always stacked. */ layout?: "auto" | "inline" | "stacked" className?: string } function refocusTextarea(textarea: HTMLTextAreaElement | null) { if (!textarea) return textarea.focus() requestAnimationFrame(() => { textarea.focus() requestAnimationFrame(() => textarea.focus()) }) } export const Input = React.forwardRef(function Input( { value, defaultValue = "", onValueChange, onSubmit, prompt, status, metaLeft, metaRight, placeholder, disabled = false, showCursor = false, layout = "auto", className, }, ref ) { const [internalValue, setInternalValue] = React.useState(defaultValue) const [focused, setFocused] = React.useState(false) const [caretLeft, setCaretLeft] = React.useState(0) const textareaRef = React.useRef(null) const mirrorRef = React.useRef(null) const { ref: themeRef, isClaude, isBubble } = useResolvedTerminalTheme() const currentValue = value ?? internalValue const stacked = layout === "stacked" || (layout === "auto" && isClaude) const composer = layout === "auto" && isBubble const inline = !stacked && !composer const resolvedPrompt = prompt ?? "❯" const footerRight = metaRight ?? (stacked ? status : undefined) const showComposerMeta = composer && (metaLeft != null || metaRight != null || status != null) const showStackedFooter = stacked && (metaLeft != null || footerRight != null) const textColor = stacked ? "var(--terminal-fg)" : "var(--terminal-white)" const promptColor = "var(--terminal-teal)" const cursorColor = stacked ? "var(--terminal-fg)" : "var(--terminal-progress-fill)" const fieldLineHeight = stacked ? 18 : 16 const cursorHeight = stacked ? 13 : 12 React.useImperativeHandle(ref, () => textareaRef.current as HTMLTextAreaElement) const syncCaretPosition = React.useCallback(() => { const textarea = textareaRef.current const mirror = mirrorRef.current if (!textarea || !mirror) return const selectionStart = textarea.selectionStart ?? currentValue.length mirror.textContent = currentValue.slice(0, selectionStart) || "\u200b" setCaretLeft(mirror.offsetWidth) }, [currentValue]) React.useLayoutEffect(() => { syncCaretPosition() }, [syncCaretPosition]) const handleChange = (next: string) => { if (value === undefined) setInternalValue(next) onValueChange?.(next) } const handleKeyDown = (event: React.KeyboardEvent) => { if (event.key === "Enter" && !event.shiftKey) { event.preventDefault() if (!currentValue.trim() || disabled) return onSubmit?.(currentValue) refocusTextarea(textareaRef.current) } } const field = (
{resolvedPrompt}