automagik-cli
Version:
Automagik CLI - A powerful command-line interface for interacting with Automagik Hive multi-agent AI systems
407 lines (406 loc) • 17 kB
JavaScript
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
/**
* Simplified InputPrompt component inspired by gemini-cli
* Focuses on the core text editing functionality
*/
import React, { useCallback, useState } from 'react';
import { Box, Text } from 'ink';
import { useKeypress } from '../hooks/useKeypress.js';
import { cpSlice, cpLen, toCodePoints } from '../utils/textUtils.js';
import stripAnsi from 'strip-ansi';
// Simple helper for word-wise ops
function isWordChar(ch) {
if (ch === undefined) {
return false;
}
return !/[\s,.;!?]/.test(ch);
}
// Strip characters that can break terminal rendering
function stripUnsafeCharacters(str) {
const stripped = stripAnsi(str);
return toCodePoints(stripped)
.filter((char) => {
if (char.length > 1)
return false;
const code = char.codePointAt(0);
if (code === undefined) {
return false;
}
const isUnsafe = code === 127 || (code <= 31 && code !== 13 && code !== 10);
return !isUnsafe;
})
.join('');
}
export const GeminiInputPrompt = ({ onSubmit, disabled = false, placeholder = 'Type your message...', focus = true, inputWidth = 60, }) => {
const [lines, setLines] = useState(['']);
const [cursorRow, setCursorRow] = useState(0);
const [cursorCol, setCursorCol] = useState(0);
// Debug log to see if component is mounting
React.useEffect(() => {
console.log('GeminiInputPrompt mounted:', { focus, disabled, placeholder });
}, []);
const getText = useCallback(() => {
return lines.join('\n');
}, [lines]);
const setText = useCallback((newText) => {
const newLines = newText.replace(/\r\n?/g, '\n').split('\n');
setLines(newLines.length === 0 ? [''] : newLines);
const lastLineIndex = newLines.length - 1;
setCursorRow(lastLineIndex);
setCursorCol(cpLen(newLines[lastLineIndex] || ''));
}, []);
const insertText = useCallback((text) => {
const newLines = [...lines];
const cleanText = stripUnsafeCharacters(text.replace(/\r\n/g, '\n').replace(/\r/g, '\n'));
const parts = cleanText.split('\n');
const currentLine = newLines[cursorRow] || '';
const before = cpSlice(currentLine, 0, cursorCol);
const after = cpSlice(currentLine, cursorCol);
if (parts.length > 1) {
// Multi-line insertion
newLines[cursorRow] = before + parts[0];
const remainingParts = parts.slice(1);
const lastPart = remainingParts.pop() || '';
newLines.splice(cursorRow + 1, 0, ...remainingParts);
newLines.splice(cursorRow + parts.length - 1, 0, lastPart + after);
setCursorRow(cursorRow + parts.length - 1);
setCursorCol(cpLen(lastPart));
}
else {
// Single line insertion
newLines[cursorRow] = before + parts[0] + after;
setCursorCol(cpLen(before) + cpLen(parts[0]));
}
setLines(newLines);
}, [lines, cursorRow, cursorCol]);
const backspace = useCallback(() => {
if (cursorCol > 0) {
// Delete character before cursor
const newLines = [...lines];
const currentLine = newLines[cursorRow] || '';
const before = cpSlice(currentLine, 0, cursorCol - 1);
const after = cpSlice(currentLine, cursorCol);
newLines[cursorRow] = before + after;
setLines(newLines);
setCursorCol(cursorCol - 1);
}
else if (cursorRow > 0) {
// Join with previous line
const newLines = [...lines];
const currentLine = newLines[cursorRow] || '';
const prevLine = newLines[cursorRow - 1] || '';
const newCol = cpLen(prevLine);
newLines[cursorRow - 1] = prevLine + currentLine;
newLines.splice(cursorRow, 1);
setLines(newLines);
setCursorRow(cursorRow - 1);
setCursorCol(newCol);
}
}, [lines, cursorRow, cursorCol]);
const deleteChar = useCallback(() => {
const currentLine = lines[cursorRow] || '';
if (cursorCol < cpLen(currentLine)) {
// Delete character at cursor
const newLines = [...lines];
const before = cpSlice(currentLine, 0, cursorCol);
const after = cpSlice(currentLine, cursorCol + 1);
newLines[cursorRow] = before + after;
setLines(newLines);
}
else if (cursorRow < lines.length - 1) {
// Join with next line
const newLines = [...lines];
const nextLine = newLines[cursorRow + 1] || '';
newLines[cursorRow] = currentLine + nextLine;
newLines.splice(cursorRow + 1, 1);
setLines(newLines);
}
}, [lines, cursorRow, cursorCol]);
const deleteWordLeft = useCallback(() => {
const currentLine = lines[cursorRow] || '';
const codePoints = toCodePoints(currentLine);
let pos = cursorCol - 1;
// Skip whitespace
while (pos >= 0 && !isWordChar(codePoints[pos])) {
pos--;
}
// Delete word characters
while (pos >= 0 && isWordChar(codePoints[pos])) {
pos--;
}
const deleteCount = cursorCol - pos - 1;
if (deleteCount > 0) {
const newLines = [...lines];
const before = cpSlice(currentLine, 0, cursorCol - deleteCount);
const after = cpSlice(currentLine, cursorCol);
newLines[cursorRow] = before + after;
setLines(newLines);
setCursorCol(cursorCol - deleteCount);
}
}, [lines, cursorRow, cursorCol]);
const deleteWordRight = useCallback(() => {
const currentLine = lines[cursorRow] || '';
const codePoints = toCodePoints(currentLine);
let pos = cursorCol;
// Skip whitespace
while (pos < codePoints.length && !isWordChar(codePoints[pos])) {
pos++;
}
// Delete word characters
while (pos < codePoints.length && isWordChar(codePoints[pos])) {
pos++;
}
const deleteCount = pos - cursorCol;
if (deleteCount > 0) {
const newLines = [...lines];
const before = cpSlice(currentLine, 0, cursorCol);
const after = cpSlice(currentLine, cursorCol + deleteCount);
newLines[cursorRow] = before + after;
setLines(newLines);
}
}, [lines, cursorRow, cursorCol]);
const moveCursor = useCallback((direction) => {
switch (direction) {
case 'left':
if (cursorCol > 0) {
setCursorCol(cursorCol - 1);
}
else if (cursorRow > 0) {
setCursorRow(cursorRow - 1);
setCursorCol(cpLen(lines[cursorRow - 1] || ''));
}
break;
case 'right':
const currentLine = lines[cursorRow] || '';
if (cursorCol < cpLen(currentLine)) {
setCursorCol(cursorCol + 1);
}
else if (cursorRow < lines.length - 1) {
setCursorRow(cursorRow + 1);
setCursorCol(0);
}
break;
case 'up':
if (cursorRow > 0) {
const prevLine = lines[cursorRow - 1] || '';
setCursorRow(cursorRow - 1);
setCursorCol(Math.min(cursorCol, cpLen(prevLine)));
}
break;
case 'down':
if (cursorRow < lines.length - 1) {
const nextLine = lines[cursorRow + 1] || '';
setCursorRow(cursorRow + 1);
setCursorCol(Math.min(cursorCol, cpLen(nextLine)));
}
break;
case 'wordLeft': {
const currentLine = lines[cursorRow] || '';
const codePoints = toCodePoints(currentLine);
let pos = cursorCol - 1;
// Skip whitespace
while (pos >= 0 && !isWordChar(codePoints[pos])) {
pos--;
}
// Move to start of word
while (pos >= 0 && isWordChar(codePoints[pos])) {
pos--;
}
setCursorCol(Math.max(0, pos + 1));
break;
}
case 'wordRight': {
const currentLine = lines[cursorRow] || '';
const codePoints = toCodePoints(currentLine);
let pos = cursorCol;
// Skip whitespace
while (pos < codePoints.length && !isWordChar(codePoints[pos])) {
pos++;
}
// Move to end of word
while (pos < codePoints.length && isWordChar(codePoints[pos])) {
pos++;
}
setCursorCol(Math.min(cpLen(currentLine), pos));
break;
}
case 'home':
setCursorCol(0);
break;
case 'end':
setCursorCol(cpLen(lines[cursorRow] || ''));
break;
}
}, [lines, cursorRow, cursorCol]);
const newline = useCallback(() => {
const newLines = [...lines];
const currentLine = newLines[cursorRow] || '';
const before = cpSlice(currentLine, 0, cursorCol);
const after = cpSlice(currentLine, cursorCol);
newLines[cursorRow] = before;
newLines.splice(cursorRow + 1, 0, after);
setLines(newLines);
setCursorRow(cursorRow + 1);
setCursorCol(0);
}, [lines, cursorRow, cursorCol]);
const killLineRight = useCallback(() => {
const newLines = [...lines];
const currentLine = newLines[cursorRow] || '';
const before = cpSlice(currentLine, 0, cursorCol);
newLines[cursorRow] = before;
setLines(newLines);
}, [lines, cursorRow, cursorCol]);
const killLineLeft = useCallback(() => {
const newLines = [...lines];
const currentLine = newLines[cursorRow] || '';
const after = cpSlice(currentLine, cursorCol);
newLines[cursorRow] = after;
setLines(newLines);
setCursorCol(0);
}, [lines, cursorRow, cursorCol]);
const handleSubmit = useCallback(() => {
const text = getText().trim();
if (text) {
onSubmit(text);
setText('');
}
}, [getText, onSubmit, setText]);
const handleInput = useCallback((key) => {
console.log('GeminiInputPrompt handleInput received:', key);
if (!focus || disabled) {
console.log('GeminiInputPrompt handleInput skipped:', { focus, disabled });
return;
}
// Handle submission
if (key.name === 'return' && !key.ctrl && !key.meta && !key.paste) {
const currentLine = lines[cursorRow] || '';
const charBefore = cursorCol > 0 ? cpSlice(currentLine, cursorCol - 1, cursorCol) : '';
if (charBefore === '\\') {
backspace();
newline();
}
else {
handleSubmit();
}
return;
}
// Multi-line newline
if (key.name === 'return' && (key.ctrl || key.meta || key.paste)) {
newline();
return;
}
// Navigation
if (key.name === 'left') {
moveCursor(key.meta ? 'wordLeft' : 'left');
return;
}
if (key.name === 'right') {
moveCursor(key.meta ? 'wordRight' : 'right');
return;
}
if (key.name === 'up') {
moveCursor('up');
return;
}
if (key.name === 'down') {
moveCursor('down');
return;
}
// Home/End
if (key.ctrl && key.name === 'a') {
moveCursor('home');
return;
}
if (key.ctrl && key.name === 'e') {
moveCursor('end');
return;
}
// Deletion
if (key.name === 'backspace') {
if (key.meta) {
deleteWordLeft();
}
else {
backspace();
}
return;
}
if (key.name === 'delete') {
if (key.meta) {
deleteWordRight();
}
else {
deleteChar();
}
return;
}
// Kill line commands
if (key.ctrl && key.name === 'k') {
killLineRight();
return;
}
if (key.ctrl && key.name === 'u') {
killLineLeft();
return;
}
// Clear all
if (key.ctrl && key.name === 'l') {
setText('');
return;
}
// Handle paste
if (key.paste && key.sequence) {
insertText(key.sequence);
return;
}
// Regular character input
if (key.sequence && key.sequence.length === 1 && !key.ctrl) {
const char = key.sequence;
const charCode = char.charCodeAt(0);
if (charCode >= 32 || charCode === 9) { // Printable chars and tab
insertText(char);
}
return;
}
}, [
focus, disabled, lines, cursorRow, cursorCol, backspace, newline,
handleSubmit, moveCursor, deleteWordLeft, deleteWordRight, deleteChar,
killLineRight, killLineLeft, setText, insertText
]);
useKeypress(handleInput, { isActive: focus && !disabled });
// Render the input
const isMultiline = lines.length > 1;
const text = getText();
const renderSingleLine = () => {
const currentLine = lines[0] || '';
const displayText = currentLine || placeholder;
const isPlaceholder = !currentLine;
if (disabled || !focus) {
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 = cpSlice(currentLine, 0, cursorCol);
const atCursor = cpSlice(currentLine, cursorCol, cursorCol + 1) || ' ';
const afterCursor = cpSlice(currentLine, 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 renderMultiline = () => {
return (_jsxs(Box, { flexDirection: "column", children: [_jsxs(Text, { color: "cyan", children: ["\u250C\u2500 multiline \u2500 ", lines.length, " lines, ", text.length, " chars \u2500\u2510"] }), lines.slice(0, 6).map((line, lineIndex) => {
const isCurrentLine = lineIndex === cursorRow;
if (isCurrentLine && focus && !disabled) {
const beforeCursor = cpSlice(line, 0, cursorCol);
const atCursor = cpSlice(line, cursorCol, cursorCol + 1) || ' ';
const afterCursor = cpSlice(line, cursorCol + 1);
return (_jsxs(Box, { children: [_jsx(Text, { color: "cyan", children: "\u2502 " }), _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: "\u2502 " }), _jsx(Text, { color: disabled ? 'gray' : 'white', children: line || (lineIndex === 0 && !text ? placeholder : '') })] }, lineIndex));
}
}), lines.length > 6 && (_jsxs(Box, { children: [_jsx(Text, { color: "cyan", children: "\u2502 " }), _jsxs(Text, { color: "gray", children: ["... ", lines.length - 6, " more lines"] })] })), _jsxs(Text, { color: "cyan", children: ["\u2514", '─'.repeat(50), "\u2518"] })] }));
};
return (_jsxs(Box, { flexDirection: "column", marginY: 1, children: [isMultiline ? renderMultiline() : renderSingleLine(), focus && !disabled && (_jsx(Box, { marginTop: 1, children: _jsx(Text, { color: "gray", children: isMultiline
? 'Ctrl+Enter: Send • Enter: New line • Meta+←/→: Word jump • Meta+Backspace: Delete word'
: 'Enter: Send • Ctrl+Enter: New line • Meta+←/→: Word jump' }) }))] }));
};