automagik-cli
Version:
Automagik CLI - A powerful command-line interface for interacting with Automagik Hive multi-agent AI systems
328 lines (327 loc) • 14.7 kB
JavaScript
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
import { useState, useCallback, useEffect } from 'react';
import { Box, Text, useInput, useStdin } from 'ink';
export const MultilineInputPrompt = ({ onSubmit, disabled = false, placeholder = 'Type your message...', maxHeight = 10, inputWidth = 80, }) => {
const { isRawModeSupported } = useStdin();
// Input state
const [lines, setLines] = useState(['']);
const [cursorRow, setCursorRow] = useState(0);
const [cursorCol, setCursorCol] = useState(0);
const [isMultiline, setIsMultiline] = useState(false);
// Calculate visible area
const visibleLines = Math.min(lines.length, maxHeight);
const scrollOffset = Math.max(0, cursorRow - maxHeight + 1);
const visibleLinesSlice = lines.slice(scrollOffset, scrollOffset + visibleLines);
const currentLine = lines[cursorRow] || '';
const inputText = lines.join('\n');
const wrapText = useCallback((text, width) => {
if (!text)
return [''];
const lines = [];
const rawLines = text.split('\n');
for (const line of rawLines) {
if (line.length <= width) {
lines.push(line);
}
else {
// Wrap long lines
for (let i = 0; i < line.length; i += width) {
lines.push(line.slice(i, i + width));
}
}
}
return lines.length > 0 ? lines : [''];
}, []);
const insertAtCursor = useCallback((text) => {
const newLines = [...lines];
const before = currentLine.slice(0, cursorCol);
const after = currentLine.slice(cursorCol);
if (text.includes('\n') || text.length > 100) {
// Handle multiline paste or large content
const pastedLines = text.split('\n');
// If it's a single long line, wrap it
if (pastedLines.length === 1 && text.length > inputWidth - 10) {
const wrappedLines = wrapText(text, inputWidth - 10);
newLines[cursorRow] = before + wrappedLines[0];
for (let i = 1; i < wrappedLines.length; i++) {
newLines.splice(cursorRow + i, 0, wrappedLines[i]);
}
newLines[cursorRow + wrappedLines.length - 1] += after;
setLines(newLines);
setCursorRow(cursorRow + wrappedLines.length - 1);
setCursorCol(newLines[cursorRow + wrappedLines.length - 1].length - after.length);
}
else {
// Handle true multiline content
newLines[cursorRow] = before + pastedLines[0];
// Insert additional lines
for (let i = 1; i < pastedLines.length; i++) {
newLines.splice(cursorRow + i, 0, pastedLines[i]);
}
// Update the last pasted line with the after text
const lastPastedIndex = cursorRow + pastedLines.length - 1;
newLines[lastPastedIndex] = newLines[lastPastedIndex] + after;
setLines(newLines);
setCursorRow(lastPastedIndex);
setCursorCol(newLines[lastPastedIndex].length - after.length);
}
// Auto-enable multiline mode for large pastes
setIsMultiline(true);
}
else {
// Single line insertion
const newContent = before + text + after;
// Auto-wrap if line becomes too long
if (newContent.length > inputWidth - 10) {
const wrappedLines = wrapText(newContent, inputWidth - 10);
newLines[cursorRow] = wrappedLines[0];
for (let i = 1; i < wrappedLines.length; i++) {
newLines.splice(cursorRow + i, 0, wrappedLines[i]);
}
setLines(newLines);
if (wrappedLines.length > 1) {
setIsMultiline(true);
}
setCursorCol(cursorCol + text.length);
}
else {
newLines[cursorRow] = newContent;
setLines(newLines);
setCursorCol(cursorCol + text.length);
}
}
}, [lines, cursorRow, cursorCol, currentLine, inputWidth, wrapText]);
const deleteAtCursor = useCallback((direction = 'backward') => {
const newLines = [...lines];
if (direction === 'backward') {
if (cursorCol > 0) {
// Delete character before cursor
const newLine = currentLine.slice(0, cursorCol - 1) + currentLine.slice(cursorCol);
newLines[cursorRow] = newLine;
setLines(newLines);
setCursorCol(cursorCol - 1);
}
else if (cursorRow > 0) {
// Merge with previous line
const prevLine = lines[cursorRow - 1];
const mergedLine = prevLine + currentLine;
newLines[cursorRow - 1] = mergedLine;
newLines.splice(cursorRow, 1);
setLines(newLines);
setCursorRow(cursorRow - 1);
setCursorCol(prevLine.length);
}
}
else {
if (cursorCol < currentLine.length) {
// Delete character after cursor
const newLine = currentLine.slice(0, cursorCol) + currentLine.slice(cursorCol + 1);
newLines[cursorRow] = newLine;
setLines(newLines);
}
else if (cursorRow < lines.length - 1) {
// Merge with next line
const nextLine = lines[cursorRow + 1];
const mergedLine = currentLine + nextLine;
newLines[cursorRow] = mergedLine;
newLines.splice(cursorRow + 1, 1);
setLines(newLines);
}
}
}, [lines, cursorRow, cursorCol, currentLine]);
const handleSubmit = useCallback(() => {
const text = inputText.trim();
if (text && !disabled) {
onSubmit(text);
setLines(['']);
setCursorRow(0);
setCursorCol(0);
setIsMultiline(false);
}
}, [inputText, onSubmit, disabled]);
const moveCursor = useCallback((direction) => {
switch (direction) {
case 'left':
if (cursorCol > 0) {
setCursorCol(cursorCol - 1);
}
else if (cursorRow > 0) {
setCursorRow(cursorRow - 1);
setCursorCol(lines[cursorRow - 1]?.length || 0);
}
break;
case 'right':
if (cursorCol < currentLine.length) {
setCursorCol(cursorCol + 1);
}
else if (cursorRow < lines.length - 1) {
setCursorRow(cursorRow + 1);
setCursorCol(0);
}
break;
case 'up':
if (cursorRow > 0) {
setCursorRow(cursorRow - 1);
setCursorCol(Math.min(cursorCol, lines[cursorRow - 1]?.length || 0));
}
break;
case 'down':
if (cursorRow < lines.length - 1) {
setCursorRow(cursorRow + 1);
setCursorCol(Math.min(cursorCol, lines[cursorRow + 1]?.length || 0));
}
break;
case 'home':
setCursorCol(0);
break;
case 'end':
setCursorCol(currentLine.length);
break;
}
}, [cursorRow, cursorCol, currentLine, lines]);
const addNewLine = useCallback(() => {
const newLines = [...lines];
const before = currentLine.slice(0, cursorCol);
const after = currentLine.slice(cursorCol);
newLines[cursorRow] = before;
newLines.splice(cursorRow + 1, 0, after);
setLines(newLines);
setCursorRow(cursorRow + 1);
setCursorCol(0);
setIsMultiline(true);
}, [lines, cursorRow, cursorCol, currentLine]);
useInput((inputChar, key) => {
if (disabled)
return;
// Submit handling
if (key.return) {
if (key.ctrl || (!isMultiline && !key.shift)) {
// Ctrl+Enter or Enter in single-line mode = submit
handleSubmit();
}
else {
// Shift+Enter or Enter in multiline mode = new line
addNewLine();
}
return;
}
// Navigation
if (key.leftArrow) {
moveCursor('left');
return;
}
if (key.rightArrow) {
moveCursor('right');
return;
}
if (key.upArrow) {
moveCursor('up');
return;
}
if (key.downArrow) {
moveCursor('down');
return;
}
// Home/End
if (key.ctrl && key.leftArrow) {
moveCursor('home');
return;
}
if (key.ctrl && key.rightArrow) {
moveCursor('end');
return;
}
// Deletion
if (key.backspace) {
deleteAtCursor('backward');
return;
}
if (key.delete) {
deleteAtCursor('forward');
return;
}
// Clear input
if (key.ctrl && inputChar === 'u') {
setLines(['']);
setCursorRow(0);
setCursorCol(0);
setIsMultiline(false);
return;
}
// Toggle multiline mode
if (key.ctrl && inputChar === 'm') {
setIsMultiline(!isMultiline);
return;
}
// Regular character input
if (inputChar && !key.ctrl && !key.meta && inputChar.length === 1) {
const charCode = inputChar.charCodeAt(0);
if (charCode >= 32 || charCode === 9) { // Printable chars and tab
insertAtCursor(inputChar);
}
}
});
// Handle paste events
useEffect(() => {
if (!isRawModeSupported)
return;
const handlePaste = (data) => {
const cleanData = data
.replace(/[\x00-\x08\x0E-\x1F\x7F]/g, '') // Remove control chars
.replace(/\r\n/g, '\n') // Normalize line endings
.replace(/\r/g, '\n');
if (cleanData.length > 0) {
insertAtCursor(cleanData);
}
};
// This is a simplified paste handler - real clipboard detection is limited in Ink
return () => { };
}, [insertAtCursor, isRawModeSupported]);
const renderInput = () => {
const effectiveWidth = inputWidth - 4; // Account for prompt and borders
if (isMultiline || lines.length > 1) {
return (_jsxs(Box, { flexDirection: "column", children: [_jsxs(Text, { color: "cyan", children: ['┌─ ', _jsx(Text, { color: "yellow", children: "multiline" }), ' ─'.repeat(Math.max(0, (effectiveWidth - 12) / 2)), '─┐'] }), visibleLinesSlice.map((line, index) => {
const actualRow = scrollOffset + index;
const isCurrentRow = actualRow === cursorRow;
const displayLine = line || (isCurrentRow ? ' ' : '');
if (isCurrentRow && !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 })] }, actualRow));
}
else {
return (_jsxs(Box, { children: [_jsx(Text, { color: "cyan", children: '│ ' }), _jsx(Text, { color: disabled ? 'gray' : 'white', children: displayLine || (actualRow === 0 && !inputText ? placeholder : '') })] }, actualRow));
}
}), _jsxs(Text, { color: "cyan", children: ['└', ('─'.repeat(effectiveWidth)), '┘'] }), lines.length > maxHeight && (_jsxs(Text, { color: "gray", dimColor: true, children: [scrollOffset > 0 ? '↑ ' : ' ', "Line ", cursorRow + 1, "/", lines.length, scrollOffset + visibleLines < lines.length ? ' ↓' : ''] }))] }));
}
else {
// Single line mode
const displayText = inputText || placeholder;
const isPlaceholder = !inputText;
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 = inputText.slice(0, cursorCol);
const atCursor = inputText[cursorCol] || ' ';
const afterCursor = inputText.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 = isMultiline || lines.length > 1 ? 'multi' : 'single';
const charCount = inputText.length;
const lineCount = lines.length;
let status = `${mode} • ${charCount} chars`;
if (isMultiline || lines.length > 1) {
status += ` • ${lineCount} lines`;
}
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() }), _jsx(Text, { color: "gray", dimColor: true, children: isMultiline || lines.length > 1
? 'Ctrl+Enter: Send • Enter: New line • Ctrl+M: Toggle mode'
: 'Enter: Send • Shift+Enter: New line • Ctrl+M: Multiline mode' })] }))] }));
};