Something went wrong. Try again.
fork of prime agent with changes i want but are not upstreamable probqbly (and some fixes)
Something went wrong. Try again.
9.9 kB · 399 lines
TypeScript
at main
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400/** * StdinBuffer buffers input and emits complete sequences. * * This is necessary because stdin data events can arrive in partial chunks, * especially for escape sequences like mouse events. Without buffering, * partial sequences can be misinterpreted as regular keypresses. * * For example, the mouse SGR sequence `\x1b[<35;20;5m` might arrive as: * - Event 1: `\x1b` * - Event 2: `[<35` * - Event 3: `;20;5m` * * The buffer accumulates these until a complete sequence is detected. * Call the `process()` method to feed input data. * * Based on code from OpenTUI (https://github.com/anomalyco/opentui) * MIT License - Copyright (c) 2025 opentui */
import { EventEmitter } from "events";
const ESC = "\x1b";const BRACKETED_PASTE_START = "\x1b[200~";const BRACKETED_PASTE_END = "\x1b[201~";
/** * Check if a string is a complete escape sequence or needs more data */function isCompleteSequence(data: string): "complete" | "incomplete" | "not-escape" { if (!data.startsWith(ESC)) { return "not-escape"; }
if (data.length === 1) { return "incomplete"; }
const afterEsc = data.slice(1);
if (afterEsc.startsWith("[")) { if (afterEsc.startsWith("[M")) { return data.length >= 6 ? "complete" : "incomplete"; } return isCompleteCsiSequence(data); }
if (afterEsc.startsWith("]")) { return isCompleteOscSequence(data); }
if (afterEsc.startsWith("P")) { return isCompleteDcsSequence(data); }
if (afterEsc.startsWith("_")) { return isCompleteApcSequence(data); }
if (afterEsc.startsWith("O")) { return afterEsc.length >= 2 ? "complete" : "incomplete"; }
if (afterEsc.length === 1) { return "complete"; }
return "complete";}
/** * Check if CSI sequence is complete * CSI sequences: ESC [ ... followed by a final byte (0x40-0x7E) */function isCompleteCsiSequence(data: string): "complete" | "incomplete" { if (!data.startsWith(`${ESC}[`)) { return "complete"; }
if (data.length < 3) { return "incomplete"; }
const payload = data.slice(2);
const lastChar = payload[payload.length - 1]; const lastCharCode = lastChar.charCodeAt(0);
if (lastCharCode >= 0x40 && lastCharCode <= 0x7e) { if (payload.startsWith("<")) { const mouseMatch = /^<\d+;\d+;\d+[Mm]$/.test(payload); if (mouseMatch) { return "complete"; } if (lastChar === "M" || lastChar === "m") { const parts = payload.slice(1, -1).split(";"); if (parts.length === 3 && parts.every((p) => /^\d+$/.test(p))) { return "complete"; } }
return "incomplete"; }
return "complete"; }
return "incomplete";}
/** * Check if OSC sequence is complete * OSC sequences: ESC ] ... ST (where ST is ESC \ or BEL) */function isCompleteOscSequence(data: string): "complete" | "incomplete" { if (!data.startsWith(`${ESC}]`)) { return "complete"; }
if (data.endsWith(`${ESC}\\`) || data.endsWith("\x07")) { return "complete"; }
return "incomplete";}
/** * Check if DCS (Device Control String) sequence is complete * DCS sequences: ESC P ... ST (where ST is ESC \) * Used for XTVersion responses like ESC P >| ... ESC \ */function isCompleteDcsSequence(data: string): "complete" | "incomplete" { if (!data.startsWith(`${ESC}P`)) { return "complete"; }
if (data.endsWith(`${ESC}\\`)) { return "complete"; }
return "incomplete";}
/** * Check if APC (Application Program Command) sequence is complete * APC sequences: ESC _ ... ST (where ST is ESC \) * Used for Kitty graphics responses like ESC _ G ... ESC \ */function isCompleteApcSequence(data: string): "complete" | "incomplete" { if (!data.startsWith(`${ESC}_`)) { return "complete"; }
if (data.endsWith(`${ESC}\\`)) { return "complete"; }
return "incomplete";}
/** * Split accumulated buffer into complete sequences */function parseUnmodifiedKittyPrintableCodepoint(sequence: string): number | undefined { const match = sequence.match(/^\x1b\[(\d+)(?::\d*)?(?::\d+)?u$/); if (!match) return undefined;
const codepoint = parseInt(match[1]!, 10); return codepoint >= 32 ? codepoint : undefined;}
function isRawMultilinePaste(data: string): boolean { if (data.includes(ESC)) return false; // A leading or trailing Enter alone is ordinary key input, not evidence of a multiline paste. return /[^\r\n][\r\n]+[^\r\n]/.test(data);}
function extractCompleteSequences(buffer: string): { sequences: string[]; remainder: string } { const sequences: string[] = []; let pos = 0;
while (pos < buffer.length) { const remaining = buffer.slice(pos);
if (remaining.startsWith(ESC)) { let seqEnd = 1; while (seqEnd <= remaining.length) { const candidate = remaining.slice(0, seqEnd); const status = isCompleteSequence(candidate);
if (status === "complete") { sequences.push(candidate); pos += seqEnd; break; } else if (status === "incomplete") { seqEnd++; } else { sequences.push(candidate); pos += seqEnd; break; } }
if (seqEnd > remaining.length) { return { sequences, remainder: remaining }; } } else { sequences.push(remaining[0]!); pos++; } }
return { sequences, remainder: "" };}
export type StdinBufferOptions = { /** * Maximum time to wait for sequence completion (default: 10ms) * After this time, the buffer is flushed even if incomplete */ timeout?: number;};
export type StdinBufferEventMap = { data: [string]; paste: [string];};
/** * Buffers stdin input and emits complete sequences via the 'data' event. * Handles partial escape sequences that arrive across multiple chunks. */export class StdinBuffer extends EventEmitter<StdinBufferEventMap> { private buffer: string = ""; private timeout: ReturnType<typeof setTimeout> | null = null; private readonly timeoutMs: number; private pasteMode: boolean = false; private pasteBuffer: string = ""; private pendingKittyPrintableCodepoint: number | undefined;
constructor(options: StdinBufferOptions = {}) { super(); this.timeoutMs = options.timeout ?? 10; }
public process(data: string | Buffer): void { if (this.timeout) { clearTimeout(this.timeout); this.timeout = null; }
// Handle high-byte conversion (for compatibility with parseKeypress) // If buffer has single byte > 127, convert to ESC + (byte - 128) let str: string; if (Buffer.isBuffer(data)) { if (data.length === 1 && data[0]! > 127) { const byte = data[0]! - 128; str = `\x1b${String.fromCharCode(byte)}`; } else { str = data.toString(); } } else { str = data; }
if (str.length === 0 && this.buffer.length === 0) { this.emitDataSequence(""); return; }
this.buffer += str;
if (this.pasteMode) { this.pasteBuffer += this.buffer; this.buffer = "";
const endIndex = this.pasteBuffer.indexOf(BRACKETED_PASTE_END); if (endIndex !== -1) { const pastedContent = this.pasteBuffer.slice(0, endIndex); const remaining = this.pasteBuffer.slice(endIndex + BRACKETED_PASTE_END.length);
this.pasteMode = false; this.pasteBuffer = ""; this.pendingKittyPrintableCodepoint = undefined;
this.emit("paste", pastedContent);
if (remaining.length > 0) { this.process(remaining); } } return; }
const startIndex = this.buffer.indexOf(BRACKETED_PASTE_START); if (startIndex !== -1) { if (startIndex > 0) { const beforePaste = this.buffer.slice(0, startIndex); const result = extractCompleteSequences(beforePaste); for (const sequence of result.sequences) { this.emitDataSequence(sequence); } }
this.pendingKittyPrintableCodepoint = undefined; this.buffer = this.buffer.slice(startIndex + BRACKETED_PASTE_START.length); this.pasteMode = true; this.pasteBuffer = this.buffer; this.buffer = "";
const endIndex = this.pasteBuffer.indexOf(BRACKETED_PASTE_END); if (endIndex !== -1) { const pastedContent = this.pasteBuffer.slice(0, endIndex); const remaining = this.pasteBuffer.slice(endIndex + BRACKETED_PASTE_END.length);
this.pasteMode = false; this.pasteBuffer = ""; this.pendingKittyPrintableCodepoint = undefined;
this.emit("paste", pastedContent);
if (remaining.length > 0) { this.process(remaining); } } return; }
if (isRawMultilinePaste(this.buffer)) { const pastedContent = this.buffer; this.buffer = ""; this.pendingKittyPrintableCodepoint = undefined; this.emit("paste", pastedContent); return; }
const result = extractCompleteSequences(this.buffer); this.buffer = result.remainder;
for (const sequence of result.sequences) { this.emitDataSequence(sequence); }
if (this.buffer.length > 0) { this.timeout = setTimeout(() => { const flushed = this.flush();
for (const sequence of flushed) { this.emitDataSequence(sequence); } }, this.timeoutMs); } }
private emitDataSequence(sequence: string): void { const rawCodepoint = sequence.length === 1 ? sequence.codePointAt(0) : undefined; if (rawCodepoint !== undefined && rawCodepoint === this.pendingKittyPrintableCodepoint) { this.pendingKittyPrintableCodepoint = undefined; return; }
this.pendingKittyPrintableCodepoint = parseUnmodifiedKittyPrintableCodepoint(sequence); this.emit("data", sequence); }
flush(): string[] { if (this.timeout) { clearTimeout(this.timeout); this.timeout = null; }
if (this.buffer.length === 0) { return []; }
const sequences = [this.buffer]; this.buffer = ""; this.pendingKittyPrintableCodepoint = undefined; return sequences; }
clear(): void { if (this.timeout) { clearTimeout(this.timeout); this.timeout = null; } this.buffer = ""; this.pasteMode = false; this.pasteBuffer = ""; this.pendingKittyPrintableCodepoint = undefined; }
getBuffer(): string { return this.buffer; }
destroy(): void { this.clear(); }}