automagik-cli
Version:
Automagik CLI - A powerful command-line interface for interacting with Automagik Hive multi-agent AI systems
147 lines (146 loc) • 5.82 kB
JavaScript
/**
* useKeypress hook copied from gemini-cli
* Handles keyboard input with bracketed paste support
*/
import { useEffect, useRef } from 'react';
import { useStdin } from 'ink';
import readline from 'readline';
import { PassThrough } from 'stream';
/**
* A hook that listens for keypress events from stdin, providing a
* key object that mirrors the one from Node's `readline` module,
* adding a 'paste' flag for characters input as part of a bracketed
* paste (when enabled).
*
* Pastes are currently sent as a single key event where the full paste
* is in the sequence field.
*
* @param onKeypress - The callback function to execute on each keypress.
* @param options - Options to control the hook's behavior.
* @param options.isActive - Whether the hook should be actively listening for input.
*/
export function useKeypress(onKeypress, { isActive }) {
const { stdin, setRawMode } = useStdin();
const onKeypressRef = useRef(onKeypress);
useEffect(() => {
onKeypressRef.current = onKeypress;
}, [onKeypress]);
useEffect(() => {
if (!isActive || !stdin.isTTY) {
return;
}
setRawMode(true);
const keypressStream = new PassThrough();
let usePassthrough = false;
const nodeMajorVersion = parseInt(process.versions.node.split('.')[0], 10);
if (nodeMajorVersion < 20 ||
process.env['PASTE_WORKAROUND'] === '1' ||
process.env['PASTE_WORKAROUND'] === 'true') {
// Prior to node 20, node's built-in readline does not support bracketed
// paste mode. We hack by detecting it with our own handler.
usePassthrough = true;
}
let isPaste = false;
let pasteBuffer = Buffer.alloc(0);
const handleKeypress = (_, key) => {
if (key.name === 'paste-start') {
isPaste = true;
}
else if (key.name === 'paste-end') {
isPaste = false;
onKeypressRef.current({
name: '',
ctrl: false,
meta: false,
shift: false,
paste: true,
sequence: pasteBuffer.toString(),
});
pasteBuffer = Buffer.alloc(0);
}
else {
if (isPaste) {
pasteBuffer = Buffer.concat([pasteBuffer, Buffer.from(key.sequence)]);
}
else {
// Handle special keys
if (key.name === 'return' && key.sequence === '\x1B\r') {
key.meta = true;
}
onKeypressRef.current({ ...key, paste: isPaste });
}
}
};
const handleRawKeypress = (data) => {
const PASTE_MODE_PREFIX = Buffer.from('\x1B[200~');
const PASTE_MODE_SUFFIX = Buffer.from('\x1B[201~');
let pos = 0;
while (pos < data.length) {
const prefixPos = data.indexOf(PASTE_MODE_PREFIX, pos);
const suffixPos = data.indexOf(PASTE_MODE_SUFFIX, pos);
// Determine which marker comes first, if any.
const isPrefixNext = prefixPos !== -1 && (suffixPos === -1 || prefixPos < suffixPos);
const isSuffixNext = suffixPos !== -1 && (prefixPos === -1 || suffixPos < prefixPos);
let nextMarkerPos = -1;
let markerLength = 0;
if (isPrefixNext) {
nextMarkerPos = prefixPos;
}
else if (isSuffixNext) {
nextMarkerPos = suffixPos;
}
markerLength = PASTE_MODE_SUFFIX.length;
if (nextMarkerPos === -1) {
keypressStream.write(data.slice(pos));
return;
}
const nextData = data.slice(pos, nextMarkerPos);
if (nextData.length > 0) {
keypressStream.write(nextData);
}
const createPasteKeyEvent = (name) => ({
name,
ctrl: false,
meta: false,
shift: false,
paste: false,
sequence: '',
});
if (isPrefixNext) {
handleKeypress(undefined, createPasteKeyEvent('paste-start'));
}
else if (isSuffixNext) {
handleKeypress(undefined, createPasteKeyEvent('paste-end'));
}
pos = nextMarkerPos + markerLength;
}
};
let rl;
if (usePassthrough) {
rl = readline.createInterface({ input: keypressStream });
readline.emitKeypressEvents(keypressStream, rl);
keypressStream.on('keypress', handleKeypress);
stdin.on('data', handleRawKeypress);
}
else {
rl = readline.createInterface({ input: stdin });
readline.emitKeypressEvents(stdin, rl);
stdin.on('keypress', handleKeypress);
}
// Enable bracketed paste mode
process.stdout.write('\x1B[?2004h');
return () => {
// Disable bracketed paste mode
process.stdout.write('\x1B[?2004l');
if (usePassthrough) {
keypressStream.removeListener('keypress', handleKeypress);
stdin.removeListener('data', handleRawKeypress);
}
else {
stdin.removeListener('keypress', handleKeypress);
}
rl.close();
setRawMode(false);
};
}, [isActive, stdin, setRawMode]);
}