askme-cli
Version:
askme-cli MCP server that collects user's next plan or confirmation through terminal window
320 lines • 16.4 kB
JavaScript
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
import { useState, useEffect } from 'react';
import { Box, Text, useInput } from 'ink';
import { createClipboardManager } from '../../shared/utils/clipboard.js';
import { createImageAttachment, insertImagePlaceholder } from '../../shared/utils/image.js';
// Custom multi-line text input component
export const MultiLineTextInput = ({ value, onChange, onSubmit, placeholder = "", images, onImagesChange, onProcessingStateChange }) => {
const [cursor, setCursor] = useState(0);
const [composition, setComposition] = useState(''); // IME candidates
const [lastEnterTime, setLastEnterTime] = useState(0);
const [isProcessingPaste, setIsProcessingPaste] = useState(false);
// Clean up deleted image placeholders when text changes (but not during paste operations)
useEffect(() => {
// Skip cleanup during paste operations to avoid conflicts
if (isProcessingPaste || images.length === 0)
return;
// Find all image placeholders currently in the text
const placeholderRegex = /\[Image #\d+\]/g;
const currentPlaceholders = value.match(placeholderRegex) || [];
// Check if any images have been removed from the text
const validImages = images.filter(image => currentPlaceholders.includes(image.placeholder));
// Update images array only if any were removed
if (validImages.length < images.length) {
console.log('🧹 Cleaning up removed images:', images.length, '->', validImages.length);
onImagesChange(validImages);
}
}, [value]); // Only depend on value, not images, to avoid recursion
// Calculate cursor position (Unicode character safe)
const getCursorPosition = () => {
const beforeCursor = value.slice(0, cursor);
const lines = beforeCursor.split('\n');
const row = lines.length - 1;
const col = Array.from(lines[lines.length - 1]).length;
return { row, col };
};
// Handle paste operation (images or text)
const handlePasteOperation = async () => {
if (isProcessingPaste)
return;
setIsProcessingPaste(true);
onProcessingStateChange?.(true);
try {
const clipboardManager = createClipboardManager();
// First check if there are images
const hasImage = await clipboardManager.hasImage();
if (hasImage) {
const imageBuffer = await clipboardManager.getImage();
if (imageBuffer) {
// Create image attachment
const imageAttachment = createImageAttachment(imageBuffer, images);
// Insert placeholder at cursor position
const { newText, newCursorPosition } = insertImagePlaceholder(value, cursor, imageAttachment.placeholder);
// First update image array, then update text, finally update cursor position
onImagesChange([...images, imageAttachment]);
// Use setTimeout to ensure image is added first, then update text
setTimeout(() => {
onChange(newText);
setCursor(newCursorPosition);
}, 0);
}
else {
console.log('\n❌ Cannot get clipboard image');
}
}
else {
// If no image, try to get text
const text = await clipboardManager.getText();
if (text) {
const beforeCursor = value.slice(0, cursor);
const afterCursor = value.slice(cursor);
const newValue = beforeCursor + text + afterCursor;
const newCursorPosition = cursor + text.length;
onChange(newValue);
setCursor(newCursorPosition);
}
else {
console.log('\n💡 Clipboard is empty or no pasteable content');
}
}
}
catch (error) {
console.error('\n❌ Paste operation failed:', error.message);
}
finally {
setIsProcessingPaste(false);
onProcessingStateChange?.(false);
}
};
// Render multi-line text with cursor and IME candidates
const renderTextWithCursor = () => {
const displayValue = value + composition; // Include candidate text
if (displayValue === '') {
const { row, col } = getCursorPosition();
const formattedPlaceholder = placeholder
.replace(/\t/g, ' ')
.replace(/ /g, ' ');
const placeholderChars = Array.from(formattedPlaceholder);
return (_jsxs(Text, { color: "gray", children: [placeholderChars.slice(0, col).join(''), _jsx(Text, { backgroundColor: "white", color: "black", children: placeholderChars[col] || ' ' }), placeholderChars.slice(col + 1).join('')] }));
}
const lines = displayValue.split('\n');
const actualCursor = cursor + composition.length;
const beforeCursor = displayValue.slice(0, actualCursor);
const cursorLines = beforeCursor.split('\n');
const cursorRow = cursorLines.length - 1;
const cursorCol = Array.from(cursorLines[cursorLines.length - 1]).length;
return lines.map((line, lineIndex) => {
// Format line text, maintain tabs and spaces
const formatLine = (text) => {
return text
.replace(/\t/g, ' ') // Convert tabs to 4 spaces for display
.replace(/ /g, ' '); // Ensure spaces are displayed correctly
};
if (lineIndex === cursorRow) {
// Current line, show blinking cursor
const formattedLine = formatLine(line);
const lineChars = Array.from(formattedLine);
return (_jsxs(Text, { children: [lineChars.slice(0, cursorCol).join(''), composition && (_jsx(Text, { color: "yellow", backgroundColor: "gray", children: composition })), _jsx(Text, { backgroundColor: "white", color: "black", children: lineChars[cursorCol] || ' ' }), lineChars.slice(cursorCol + 1).join('')] }, lineIndex));
}
else {
// Other lines, display normally, maintain formatting
const formattedLine = formatLine(line);
return (_jsx(Text, { children: formattedLine || ' ' }, lineIndex));
}
});
};
useInput((input, key) => {
if (key.ctrl && input === 'c') {
process.exit(0);
}
// Handle copy/paste (Cmd+V on macOS, Ctrl+V on others)
if ((key.meta && input === 'v') || (key.ctrl && input === 'v')) {
handlePasteOperation();
return;
}
// Handle ESC key
if (key.escape) {
return;
}
// Handle Emacs-style cursor movement shortcuts
if (key.ctrl) {
switch (input) {
case 'f': // Ctrl+F - move right one character
if (cursor < value.length) {
const afterCursor = value.slice(cursor);
const charArray = Array.from(afterCursor);
const nextChar = charArray[0] || '';
setCursor(cursor + nextChar.length);
}
return;
case 'b': // Ctrl+B - move left one character
if (cursor > 0) {
const beforeCursor = value.slice(0, cursor);
const charArray = Array.from(beforeCursor);
setCursor(charArray.slice(0, -1).join('').length);
}
return;
case 'p': // Ctrl+P - move up one line
const beforeCursor = value.slice(0, cursor);
const lines = beforeCursor.split('\n');
if (lines.length > 1) {
const currentLinePos = Array.from(lines[lines.length - 1]).length;
const prevLine = lines[lines.length - 2];
const prevLineLength = Array.from(prevLine).length;
const newPos = Math.min(currentLinePos, prevLineLength);
const newLineStart = beforeCursor.lastIndexOf('\n', beforeCursor.lastIndexOf('\n') - 1) + 1;
const newCursorPos = newLineStart + Array.from(prevLine.slice(0, newPos)).join('').length;
setCursor(newCursorPos);
}
return;
case 'n': // Ctrl+N - move down one line
const remainingText = value.slice(cursor);
const nextNewlineIndex = remainingText.indexOf('\n');
if (nextNewlineIndex !== -1) {
const beforeCursorForDown = value.slice(0, cursor);
const linesForDown = beforeCursorForDown.split('\n');
const currentLinePosForDown = Array.from(linesForDown[linesForDown.length - 1]).length;
const nextLineStart = cursor + nextNewlineIndex + 1;
const nextLineEnd = value.indexOf('\n', nextLineStart);
const nextLine = value.slice(nextLineStart, nextLineEnd === -1 ? undefined : nextLineEnd);
const nextLineLength = Array.from(nextLine).length;
const newPos = Math.min(currentLinePosForDown, nextLineLength);
setCursor(nextLineStart + Array.from(nextLine.slice(0, newPos)).join('').length);
}
return;
case 'a': // Ctrl+A - move to line beginning
const beforeCursorForLineStart = value.slice(0, cursor);
const linesForLineStart = beforeCursorForLineStart.split('\n');
const currentLineStart = beforeCursorForLineStart.lastIndexOf('\n') + 1;
setCursor(currentLineStart);
return;
case 'e': // Ctrl+E - move to line end
const afterCursorForLineEnd = value.slice(cursor);
const nextNewlineForLineEnd = afterCursorForLineEnd.indexOf('\n');
if (nextNewlineForLineEnd === -1) {
// Last line, move to text end
setCursor(value.length);
}
else {
// Move to current line end
setCursor(cursor + nextNewlineForLineEnd);
}
return;
case 'l': // Ctrl+L - clear input
onChange('');
setCursor(0);
return;
}
}
// Handle Enter key - double click to submit, Shift+Enter for newline
if (key.return) {
// Shift+Enter forces newline
if (key.shift) {
const newValue = value.slice(0, cursor) + '\n' + value.slice(cursor);
onChange(newValue);
setCursor(cursor + 1);
return;
}
// Double Enter to submit
const now = Date.now();
if (now - lastEnterTime < 500) { // Double click within 500ms
if (value.trim()) {
// Filter images to only include those referenced in the text
const placeholderRegex = /\[Image #\d+\]/g;
const currentPlaceholders = value.match(placeholderRegex) || [];
const validImages = images.filter(image => currentPlaceholders.includes(image.placeholder));
onSubmit(value.trim(), validImages.length > 0 ? validImages : undefined);
}
return;
}
setLastEnterTime(now);
// Single Enter for newline
const newValue = value.slice(0, cursor) + '\n' + value.slice(cursor);
onChange(newValue);
setCursor(cursor + 1);
return;
}
// Handle backspace/delete
if (key.backspace || key.delete) {
if (cursor > 0) {
// Correctly handle Unicode characters (including Chinese)
const beforeCursor = value.slice(0, cursor);
const charArray = Array.from(beforeCursor);
const newBeforeCursor = charArray.slice(0, -1).join('');
const newValue = newBeforeCursor + value.slice(cursor);
onChange(newValue);
setCursor(newBeforeCursor.length);
}
return;
}
// Handle arrow keys
if (key.leftArrow) {
if (cursor > 0) {
// Correctly handle Unicode character boundaries
const beforeCursor = value.slice(0, cursor);
const charArray = Array.from(beforeCursor);
setCursor(charArray.slice(0, -1).join('').length);
}
return;
}
if (key.rightArrow) {
if (cursor < value.length) {
// Correctly handle Unicode character boundaries
const afterCursor = value.slice(cursor);
const charArray = Array.from(afterCursor);
const nextChar = charArray[0] || '';
setCursor(cursor + nextChar.length);
}
return;
}
if (key.upArrow) {
// Move up to previous line
const beforeCursor = value.slice(0, cursor);
const lines = beforeCursor.split('\n');
if (lines.length > 1) {
const currentLinePos = Array.from(lines[lines.length - 1]).length;
const prevLine = lines[lines.length - 2];
const prevLineLength = Array.from(prevLine).length;
const newPos = Math.min(currentLinePos, prevLineLength);
// Calculate new cursor position
const newLineStart = beforeCursor.lastIndexOf('\n', beforeCursor.lastIndexOf('\n') - 1) + 1;
const newCursorPos = newLineStart + Array.from(prevLine.slice(0, newPos)).join('').length;
setCursor(newCursorPos);
}
return;
}
if (key.downArrow) {
// Move down to next line
const remainingText = value.slice(cursor);
const nextNewlineIndex = remainingText.indexOf('\n');
if (nextNewlineIndex !== -1) {
const beforeCursor = value.slice(0, cursor);
const lines = beforeCursor.split('\n');
const currentLinePos = Array.from(lines[lines.length - 1]).length;
const nextLineStart = cursor + nextNewlineIndex + 1;
const nextLineEnd = value.indexOf('\n', nextLineStart);
const nextLine = value.slice(nextLineStart, nextLineEnd === -1 ? undefined : nextLineEnd);
const nextLineLength = Array.from(nextLine).length;
const newPos = Math.min(currentLinePos, nextLineLength);
setCursor(nextLineStart + Array.from(nextLine.slice(0, newPos)).join('').length);
}
return;
}
// Handle printable characters (including Chinese and special characters)
if (input && input.length > 0 && !key.ctrl && !key.meta) {
// Handle pasted multi-line text, maintain original formatting
let processedInput = input;
// If input contains newlines, it might be pasted text
if (input.includes('\n') || input.includes('\r')) {
// Maintain newlines, normalize to \n
processedInput = input.replace(/\r\n/g, '\n').replace(/\r/g, '\n');
}
// Maintain tabs and multiple spaces
const newValue = value.slice(0, cursor) + processedInput + value.slice(cursor);
onChange(newValue);
setCursor(cursor + processedInput.length);
}
});
return (_jsx(Box, { flexDirection: "column", children: renderTextWithCursor() }));
};
//# sourceMappingURL=MultiLineTextInput.js.map