automagik-cli
Version:
Automagik CLI - A powerful command-line interface for interacting with Automagik Hive multi-agent AI systems
408 lines (407 loc) • 19.2 kB
JavaScript
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';
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: "blue", children: [_jsx(Text, { bold: true, color: "blue", 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"] })] }))] }))] }));
};