UNPKG

automagik-cli

Version:

Automagik CLI - A powerful command-line interface for interacting with Automagik Hive multi-agent AI systems

409 lines (408 loc) 19.3 kB
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime"; import { useState, useCallback, useEffect, useRef } from 'react'; import { Box, Text, useInput, useStdin } from 'ink'; import { Colors } from '../colors.js'; export const EnhancedInputPrompt = ({ onSubmit, inputWidth, disabled = false, placeholder = 'Type your message...', multiline = true, maxLines = 10, }) => { const { isRawModeSupported } = useStdin(); // Input state const [input, setInput] = useState(''); const [lines, setLines] = useState(['']); const [cursorLine, setCursorLine] = useState(0); const [cursorCol, setCursorCol] = useState(0); // History state const [history, setHistory] = useState({ messages: [], currentIndex: -1 }); const [isInHistory, setIsInHistory] = useState(false); const tempInput = useRef(''); // Selection state const [isMultilineMode, setIsMultilineMode] = useState(false); const [showHelp, setShowHelp] = useState(false); // Convert between single string and lines array useEffect(() => { if (isMultilineMode) { const newLines = input.split('\n'); if (newLines.join('\n') !== lines.join('\n')) { setLines(newLines); // Adjust cursor position if needed if (cursorLine >= newLines.length) { setCursorLine(Math.max(0, newLines.length - 1)); } if (newLines[cursorLine] && cursorCol > newLines[cursorLine].length) { setCursorCol(newLines[cursorLine].length); } } } else { const singleLine = input.replace(/\n/g, ' '); if (singleLine !== input) { setInput(singleLine); } setLines([singleLine]); setCursorLine(0); } }, [input, isMultilineMode, cursorLine, cursorCol, lines]); const updateInputFromLines = useCallback(() => { const newInput = lines.join('\n'); if (newInput !== input) { setInput(newInput); } }, [lines, input]); const getCurrentLine = useCallback(() => lines[cursorLine] || '', [lines, cursorLine]); const insertAtCursor = useCallback((text) => { const currentLine = getCurrentLine(); const newLine = currentLine.slice(0, cursorCol) + text + currentLine.slice(cursorCol); const newLines = [...lines]; newLines[cursorLine] = newLine; setLines(newLines); setCursorCol(cursorCol + text.length); // Update input state const newInput = newLines.join('\n'); setInput(newInput); // Clear history navigation if active if (isInHistory) { setIsInHistory(false); setHistory(prev => ({ ...prev, currentIndex: -1 })); } }, [lines, cursorLine, cursorCol, getCurrentLine, isInHistory, setHistory]); const deleteAtCursor = useCallback((direction = 'backward') => { const currentLine = getCurrentLine(); if (direction === 'backward') { if (cursorCol > 0) { // Delete character before cursor const newLine = currentLine.slice(0, cursorCol - 1) + currentLine.slice(cursorCol); const newLines = [...lines]; newLines[cursorLine] = newLine; setLines(newLines); setCursorCol(cursorCol - 1); // Update input state const newInput = newLines.join('\n'); setInput(newInput); // Clear history navigation if active if (isInHistory) { setIsInHistory(false); setHistory(prev => ({ ...prev, currentIndex: -1 })); } } else if (cursorLine > 0 && isMultilineMode) { // Merge with previous line const prevLine = lines[cursorLine - 1]; const newLine = prevLine + currentLine; const newLines = lines.filter((_, i) => i !== cursorLine); newLines[cursorLine - 1] = newLine; setLines(newLines); setCursorLine(cursorLine - 1); setCursorCol(prevLine.length); // Update input state const newInput = newLines.join('\n'); setInput(newInput); } } else { if (cursorCol < currentLine.length) { // Delete character after cursor const newLine = currentLine.slice(0, cursorCol) + currentLine.slice(cursorCol + 1); const newLines = [...lines]; newLines[cursorLine] = newLine; setLines(newLines); // Update input state const newInput = newLines.join('\n'); setInput(newInput); // Clear history navigation if active if (isInHistory) { setIsInHistory(false); setHistory(prev => ({ ...prev, currentIndex: -1 })); } } else if (cursorLine < lines.length - 1 && isMultilineMode) { // Merge with next line const nextLine = lines[cursorLine + 1]; const newLine = currentLine + nextLine; const newLines = [...lines]; newLines[cursorLine] = newLine; newLines.splice(cursorLine + 1, 1); setLines(newLines); // Update input state const newInput = newLines.join('\n'); setInput(newInput); } } }, [lines, cursorLine, cursorCol, getCurrentLine, isMultilineMode, isInHistory, setHistory]); const handleSubmit = useCallback(() => { if (input.trim() && !disabled) { // Add to history setHistory(prev => ({ messages: [...prev.messages, input].slice(-50), // Keep last 50 messages currentIndex: -1, })); onSubmit(input.trim()); setInput(''); setLines(['']); setCursorLine(0); setCursorCol(0); setIsInHistory(false); tempInput.current = ''; } }, [input, onSubmit, disabled]); const navigateHistory = useCallback((direction) => { if (history.messages.length === 0) return; if (!isInHistory && direction === 'up') { // First time entering history - save current input tempInput.current = input; setIsInHistory(true); const newIndex = history.messages.length - 1; setHistory(prev => ({ ...prev, currentIndex: newIndex })); const historicalMessage = history.messages[newIndex]; setInput(historicalMessage); setLines(historicalMessage.split('\n')); setCursorLine(0); setCursorCol(0); } else if (isInHistory) { if (direction === 'up' && history.currentIndex > 0) { const newIndex = history.currentIndex - 1; setHistory(prev => ({ ...prev, currentIndex: newIndex })); const historicalMessage = history.messages[newIndex]; setInput(historicalMessage); setLines(historicalMessage.split('\n')); setCursorLine(0); setCursorCol(0); } else if (direction === 'down') { if (history.currentIndex < history.messages.length - 1) { const newIndex = history.currentIndex + 1; setHistory(prev => ({ ...prev, currentIndex: newIndex })); const historicalMessage = history.messages[newIndex]; setInput(historicalMessage); setLines(historicalMessage.split('\n')); setCursorLine(0); setCursorCol(0); } else { // Return to current input setIsInHistory(false); setHistory(prev => ({ ...prev, currentIndex: -1 })); setInput(tempInput.current); setLines(tempInput.current.split('\n')); setCursorLine(0); setCursorCol(0); } } } }, [history, input, isInHistory]); useInput((inputChar, key) => { if (disabled) { return; } // Help toggle if (key.ctrl && inputChar === 'h') { setShowHelp(!showHelp); return; } // Submit handling if (key.return) { if (isMultilineMode && !key.ctrl) { // Add new line in multiline mode if (lines.length < maxLines) { const currentLine = getCurrentLine(); const beforeCursor = currentLine.slice(0, cursorCol); const afterCursor = currentLine.slice(cursorCol); const newLines = [...lines]; newLines[cursorLine] = beforeCursor; newLines.splice(cursorLine + 1, 0, afterCursor); setLines(newLines); setCursorLine(cursorLine + 1); setCursorCol(0); setInput(newLines.join('\n')); } } else { // Submit (Enter in single-line mode, or Ctrl+Enter in multiline mode) handleSubmit(); } return; } // History navigation if (key.upArrow) { navigateHistory('up'); return; } if (key.downArrow) { navigateHistory('down'); return; } // Clear any history navigation if (isInHistory && inputChar && !key.upArrow && !key.downArrow && !key.ctrl) { setIsInHistory(false); setHistory(prev => ({ ...prev, currentIndex: -1 })); } // Cursor movement if (key.leftArrow) { if (cursorCol > 0) { setCursorCol(cursorCol - 1); } else if (cursorLine > 0 && isMultilineMode) { setCursorLine(cursorLine - 1); setCursorCol(lines[cursorLine - 1]?.length || 0); } return; } if (key.rightArrow) { const currentLine = getCurrentLine(); if (cursorCol < currentLine.length) { setCursorCol(cursorCol + 1); } else if (cursorLine < lines.length - 1 && isMultilineMode) { setCursorLine(cursorLine + 1); setCursorCol(0); } return; } // Deletion if (key.backspace) { deleteAtCursor('backward'); return; } if (key.delete) { deleteAtCursor('forward'); return; } // Text editing shortcuts if (key.ctrl && inputChar === 'a') { setCursorCol(0); return; } if (key.ctrl && inputChar === 'e') { setCursorCol(getCurrentLine().length); return; } if (key.ctrl && inputChar === 'u') { setInput(''); setLines(['']); setCursorLine(0); setCursorCol(0); return; } if (key.ctrl && inputChar === 'k') { const currentLine = getCurrentLine(); const newLine = currentLine.slice(0, cursorCol); const newLines = [...lines]; newLines[cursorLine] = newLine; setLines(newLines); setInput(newLines.join('\n')); return; } // Word navigation (fix the word boundary detection) if (key.ctrl && key.leftArrow) { const currentLine = getCurrentLine(); let newCol = cursorCol; // Skip spaces backwards while (newCol > 0 && currentLine[newCol - 1] === ' ') { newCol--; } // Find word boundary while (newCol > 0 && currentLine[newCol - 1] !== ' ') { newCol--; } setCursorCol(newCol); return; } if (key.ctrl && key.rightArrow) { const currentLine = getCurrentLine(); let newCol = cursorCol; // Skip spaces forward while (newCol < currentLine.length && currentLine[newCol] === ' ') { newCol++; } // Find word boundary while (newCol < currentLine.length && currentLine[newCol] !== ' ') { newCol++; } setCursorCol(newCol); return; } // Mode toggle if (key.ctrl && inputChar === 'm') { setIsMultilineMode(!isMultilineMode); return; } // Clipboard paste handling (Ctrl+V) if (key.ctrl && inputChar === 'v') { // In a real terminal environment, this would handle clipboard // For now, we'll just prevent the default behavior return; } // Regular character input - improved filtering if (inputChar && !key.ctrl && !key.meta && !key.alt && inputChar.length === 1) { // Filter out control characters except newlines const charCode = inputChar.charCodeAt(0); if (charCode >= 32 || charCode === 9) { // Allow printable chars and tab insertAtCursor(inputChar); } } }); // Handle paste events with better character filtering useEffect(() => { if (isRawModeSupported) { const handlePaste = (data) => { // Filter out control characters and normalize the text const filteredData = data .replace(/[\x00-\x08\x0E-\x1F\x7F]/g, '') // Remove control chars except \t, \n, \r .replace(/\r\n/g, '\n') // Normalize line endings .replace(/\r/g, '\n'); // Convert remaining \r to \n if (filteredData.length > 0) { insertAtCursor(filteredData); } }; // This is a basic implementation - real paste detection is limited in Ink return () => { }; // Cleanup if needed } return undefined; // Explicit return for all code paths }, [insertAtCursor, isRawModeSupported]); const renderInput = () => { const effectiveWidth = Math.min(inputWidth - 4, 120); if (isMultilineMode) { return (_jsxs(Box, { flexDirection: "column", children: [_jsxs(Text, { color: "cyan", children: ['┌─ ', _jsx(Text, { color: "yellow", children: "multiline mode" }), ' ─'.repeat(Math.max(0, (effectiveWidth - 20) / 2)), '─┐'] }), lines.map((line, lineIndex) => { const isCurrentLine = lineIndex === cursorLine; const displayLine = line || (isCurrentLine ? ' ' : ''); if (isCurrentLine && !disabled) { const beforeCursor = displayLine.slice(0, cursorCol); const atCursor = displayLine[cursorCol] || ' '; const afterCursor = displayLine.slice(cursorCol + 1); return (_jsxs(Box, { children: [_jsx(Text, { color: "cyan", children: '│ ' }), _jsx(Text, { children: beforeCursor }), _jsx(Text, { inverse: true, children: atCursor }), _jsx(Text, { children: afterCursor })] }, lineIndex)); } else { return (_jsxs(Box, { children: [_jsx(Text, { color: "cyan", children: '│ ' }), _jsx(Text, { color: disabled ? 'gray' : 'white', children: displayLine || (lineIndex === 0 && !input ? placeholder : '') })] }, lineIndex)); } }), _jsxs(Text, { color: "cyan", children: ['└', ('─'.repeat(effectiveWidth)), '┘'] })] })); } else { // Single line mode const displayText = input || placeholder; const isPlaceholder = !input; if (disabled) { return (_jsxs(Box, { children: [_jsx(Text, { color: "cyan", children: '> ' }), _jsx(Text, { color: "gray", children: placeholder })] })); } if (isPlaceholder) { return (_jsxs(Box, { children: [_jsx(Text, { color: "cyan", children: '> ' }), _jsx(Text, { color: "gray", children: placeholder })] })); } const beforeCursor = input.slice(0, cursorCol); const atCursor = input[cursorCol] || ' '; const afterCursor = input.slice(cursorCol + 1); return (_jsxs(Box, { children: [_jsx(Text, { color: "cyan", children: '> ' }), _jsx(Text, { children: beforeCursor }), _jsx(Text, { inverse: true, children: atCursor }), _jsx(Text, { children: afterCursor })] })); } }; const getStatusText = () => { const mode = isMultilineMode ? 'multi' : 'single'; const charCount = input.length; const lineCount = lines.length; let status = `${mode} • ${charCount} chars`; if (isMultilineMode) { status += ` • ${lineCount} lines`; } if (isInHistory) { status += ` • history ${history.currentIndex + 1}/${history.messages.length}`; } return status; }; return (_jsxs(Box, { flexDirection: "column", marginY: 1, children: [renderInput(), !disabled && (_jsxs(Box, { flexDirection: "column", marginTop: 1, children: [_jsx(Text, { color: "gray", dimColor: true, children: getStatusText() }), showHelp && (_jsxs(Box, { flexDirection: "column", marginTop: 1, padding: 1, borderStyle: "round", borderColor: Colors.AccentPurple, children: [_jsx(Text, { bold: true, color: Colors.AccentPurple, children: "Enhanced Input Controls:" }), _jsxs(Text, { children: ["\u2022 ", _jsx(Text, { color: "cyan", children: "Enter" }), ": Send message (single-line) / New line (multi-line)"] }), _jsxs(Text, { children: ["\u2022 ", _jsx(Text, { color: "cyan", children: "Ctrl+Enter" }), ": Send message (multi-line mode)"] }), _jsxs(Text, { children: ["\u2022 ", _jsx(Text, { color: "cyan", children: "Ctrl+M" }), ": Toggle single/multi-line mode"] }), _jsxs(Text, { children: ["\u2022 ", _jsx(Text, { color: "cyan", children: "\u2191/\u2193" }), ": Navigate message history"] }), _jsxs(Text, { children: ["\u2022 ", _jsx(Text, { color: "cyan", children: "Ctrl+A/E" }), ": Move to start/end of line"] }), _jsxs(Text, { children: ["\u2022 ", _jsx(Text, { color: "cyan", children: "Ctrl+U" }), ": Clear input"] }), _jsxs(Text, { children: ["\u2022 ", _jsx(Text, { color: "cyan", children: "Ctrl+K" }), ": Delete to end of line"] }), _jsxs(Text, { children: ["\u2022 ", _jsx(Text, { color: "cyan", children: "Ctrl+\u2190/\u2192" }), ": Move by word"] }), _jsxs(Text, { children: ["\u2022 ", _jsx(Text, { color: "cyan", children: "Ctrl+H" }), ": Toggle this help"] })] }))] }))] })); };