import React, { useCallback } from 'react'; import { Box, Text, useInput } from 'ink'; import TextInput from 'ink-text-input'; import type { ParsedCommand, CommandDef } from '@mosaic/types'; import { parseSlashCommand, commandRegistry } from '../commands/index.js'; import { CommandAutocomplete } from './command-autocomplete.js'; import { useInputHistory } from '../hooks/use-input-history.js'; import { useState } from 'react'; export interface InputBarProps { /** Controlled input value — caller owns the state */ value: string; onChange: (val: string) => void; onSubmit: (value: string) => void; onSystemMessage?: (message: string) => void; onLocalCommand?: (parsed: ParsedCommand) => void; onGatewayCommand?: (parsed: ParsedCommand) => void; isStreaming: boolean; connected: boolean; /** Whether this input bar is focused/active (default true). When false, * keyboard input is not captured — e.g. when the sidebar has focus. */ focused?: boolean; placeholder?: string; allCommands?: CommandDef[]; } export function InputBar({ value: input, onChange: setInput, onSubmit, onSystemMessage, onLocalCommand, onGatewayCommand, isStreaming, connected, focused = true, placeholder: placeholderOverride, allCommands, }: InputBarProps) { const [showAutocomplete, setShowAutocomplete] = useState(false); const [autocompleteIndex, setAutocompleteIndex] = useState(0); const { addToHistory, navigateUp, navigateDown } = useInputHistory(); // Determine which commands to show in autocomplete const availableCommands = allCommands ?? commandRegistry.getAll(); const handleChange = useCallback( (value: string) => { setInput(value); if (value.startsWith('/')) { setShowAutocomplete(true); setAutocompleteIndex(0); } else { setShowAutocomplete(false); } }, [setInput], ); const handleSubmit = useCallback( (value: string) => { if (!value.trim() || isStreaming || !connected) return; const trimmed = value.trim(); addToHistory(trimmed); setShowAutocomplete(false); setAutocompleteIndex(0); if (trimmed.startsWith('/')) { const parsed = parseSlashCommand(trimmed); if (!parsed) { // Bare "/" or malformed — ignore silently (autocomplete handles discovery) return; } const def = commandRegistry.find(parsed.command); if (!def) { onSystemMessage?.( `Unknown command: /${parsed.command}. Type /help for available commands.`, ); setInput(''); return; } if (def.execution === 'local') { onLocalCommand?.(parsed); setInput(''); return; } // Gateway-executed commands onGatewayCommand?.(parsed); setInput(''); return; } onSubmit(value); setInput(''); }, [ onSubmit, onSystemMessage, onLocalCommand, onGatewayCommand, isStreaming, connected, addToHistory, setInput, ], ); // Handle Tab: fill in selected autocomplete command const fillAutocompleteSelection = useCallback(() => { if (!showAutocomplete) return false; const query = input.startsWith('/') ? input.slice(1) : input; const filtered = availableCommands.filter( (c) => !query || c.name.includes(query.toLowerCase()) || c.aliases.some((a) => a.includes(query.toLowerCase())) || c.description.toLowerCase().includes(query.toLowerCase()), ); if (filtered.length === 0) return false; const idx = Math.min(autocompleteIndex, filtered.length - 1); const selected = filtered[idx]; if (selected) { setInput(`/${selected.name} `); setShowAutocomplete(false); setAutocompleteIndex(0); return true; } return false; }, [showAutocomplete, input, availableCommands, autocompleteIndex, setInput]); useInput( (_ch, key) => { if (key.escape && showAutocomplete) { setShowAutocomplete(false); setAutocompleteIndex(0); return; } // Tab: fill autocomplete selection if (key.tab) { fillAutocompleteSelection(); return; } // Up arrow if (key.upArrow) { if (showAutocomplete) { setAutocompleteIndex((prev) => Math.max(0, prev - 1)); } else { const prev = navigateUp(input); if (prev !== null) { setInput(prev); if (prev.startsWith('/')) setShowAutocomplete(true); } } return; } // Down arrow if (key.downArrow) { if (showAutocomplete) { const query = input.startsWith('/') ? input.slice(1) : input; const filteredLen = availableCommands.filter( (c) => !query || c.name.includes(query.toLowerCase()) || c.aliases.some((a) => a.includes(query.toLowerCase())) || c.description.toLowerCase().includes(query.toLowerCase()), ).length; const maxVisible = Math.min(filteredLen, 8); setAutocompleteIndex((prev) => Math.min(prev + 1, maxVisible - 1)); } else { const next = navigateDown(); if (next !== null) { setInput(next); setShowAutocomplete(next.startsWith('/')); } } return; } // Return/Enter on autocomplete: fill selected command if (key.return && showAutocomplete) { fillAutocompleteSelection(); return; } }, { isActive: focused }, ); const placeholder = placeholderOverride ?? (!connected ? 'disconnected — waiting for gateway…' : isStreaming ? 'waiting for response…' : 'message mosaic…'); return ( {showAutocomplete && ( )} {'❯ '} ); }