mixdog 0.9.140 → 0.9.142
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSES/MIT.txt +4 -0
- package/NOTICE.md +12 -46
- package/package.json +1 -1
- package/scripts/manifest-cache-key.mjs +44 -0
- package/scripts/native-binary-arch.mjs +93 -0
- package/scripts/native-binary-arch.test.mjs +75 -0
- package/scripts/native-tool-download.mjs +118 -0
- package/src/runtime/agent/orchestrator/providers/gemini.mjs +1 -1
- package/src/runtime/agent/orchestrator/providers/grok-oauth-tokens.mjs +2 -2
- package/src/runtime/agent/orchestrator/providers/openai-compat-stream.mjs +2 -2
- package/src/runtime/agent/orchestrator/providers/openai-ws-pool.mjs +1 -1
- package/src/runtime/agent/orchestrator/providers/openai-ws-stream.mjs +1 -1
- package/src/runtime/agent/orchestrator/providers/retry-classifier.mjs +12 -12
- package/src/runtime/agent/orchestrator/session/compact/file-reattach.mjs +2 -2
- package/src/runtime/agent/orchestrator/session/compact/runner.mjs +1 -1
- package/src/runtime/agent/orchestrator/session/manager/runtime-liveness.mjs +1 -1
- package/src/runtime/agent/orchestrator/tools/builtin/bash-tool.mjs +1 -1
- package/src/runtime/agent/orchestrator/tools/builtin/read-constants.mjs +2 -3
- package/src/runtime/agent/orchestrator/tools/builtin/read-image-resize.mjs +6 -7
- package/src/runtime/agent/orchestrator/tools/builtin/read-single-tool.mjs +2 -3
- package/src/runtime/agent/orchestrator/tools/builtin/read-special-files.mjs +1 -1
- package/src/runtime/agent/orchestrator/tools/builtin/read-tool.mjs +3 -3
- package/src/runtime/agent/orchestrator/tools/shell-command.mjs +2 -2
- package/src/runtime/agent/orchestrator/tools/shell-exec-output.mjs +2 -2
- package/src/runtime/agent/orchestrator/tools/shell-policy.mjs +1 -1
- package/src/runtime/channels/lib/parent-bridge.mjs +2 -2
- package/src/runtime/channels/lib/runtime-paths.mjs +1 -1
- package/src/standalone/agent-tool/shard-spread.mjs +5 -3
- package/src/standalone/agent-tool/shard-spread.test.mjs +16 -1
- package/src/tui/App.jsx +3 -3
- package/src/tui/app/transcript-window.mjs +4 -5
- package/src/tui/app/use-mouse-input.mjs +2 -2
- package/src/tui/app/use-transcript-scroll.mjs +37 -29
- package/src/tui/components/tool-output-format.mjs +2 -2
- package/src/tui/dist/index.mjs +21 -31
- package/src/tui/themes/teal.mjs +1 -1
- package/vendor/ink/build/ink.js +7 -85
- package/vendor/ink/build/output.js +7 -9
- package/vendor/ink/build/selection-rect.js +80 -0
- package/vendor/ink/build/termio-keypress.js +110 -79
- package/vendor/ink/build/_baseline-termio-keypress.js +0 -240
- package/vendor/ink/build/_baseline-termio-tokenize.js +0 -253
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* termio-keypress.js — chunked keypress pipeline built on the termio tokenizer.
|
|
3
3
|
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
* /
|
|
4
|
+
* Tokens arrive as text runs and raw escape sequences; this layer decides what
|
|
5
|
+
* each one MEANS: bracketed-paste accumulation, terminal-response replies,
|
|
6
|
+
* SGR/X10 mouse events, orphaned mouse tails, and ordinary keypresses.
|
|
7
7
|
*
|
|
8
8
|
* It emits a flat list of typed events:
|
|
9
9
|
* { kind: 'key', sequence, isPasted, ... } normal keypress or paste
|
|
@@ -148,93 +148,124 @@ export const INITIAL_STATE = {
|
|
|
148
148
|
pasteBuffer: '',
|
|
149
149
|
};
|
|
150
150
|
|
|
151
|
+
// An orphaned mouse tail: the ESC was flushed on its own as a lone Escape (a
|
|
152
|
+
// heavy render blocked the event loop past the pending-ESC timer), so the
|
|
153
|
+
// continuation reached us as plain text. The X10 Cb slot is narrowed to the
|
|
154
|
+
// wheel range so ordinary typing like `[MAX]` batched into one read is not
|
|
155
|
+
// mistaken for a click.
|
|
156
|
+
// eslint-disable-next-line no-control-regex
|
|
157
|
+
const ORPHAN_SGR_TAIL_RE = /^\[<\d+;\d+;\d+[Mm]$/;
|
|
158
|
+
// eslint-disable-next-line no-control-regex
|
|
159
|
+
const ORPHAN_X10_TAIL_RE = /^\[M[\x60-\x7f][\x20-\uffff]{2}$/;
|
|
160
|
+
|
|
161
|
+
/** A non-paste sequence token becomes a response, a mouse event, or a key. */
|
|
162
|
+
function eventForSequence(sequence) {
|
|
163
|
+
const response = parseTerminalResponse(sequence);
|
|
164
|
+
if (response) return { kind: 'response', sequence, response };
|
|
165
|
+
return parseMouseEvent(sequence) ?? asKey(sequence);
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
/** Restore the ESC an orphaned mouse tail lost, or null when it is real text. */
|
|
169
|
+
function eventForOrphanTail(text) {
|
|
170
|
+
if (!ORPHAN_SGR_TAIL_RE.test(text) && !ORPHAN_X10_TAIL_RE.test(text)) return null;
|
|
171
|
+
const restored = `\x1b${text}`;
|
|
172
|
+
return parseMouseEvent(restored) ?? asKey(restored);
|
|
173
|
+
}
|
|
174
|
+
|
|
151
175
|
/**
|
|
152
|
-
*
|
|
153
|
-
*
|
|
154
|
-
*
|
|
176
|
+
* Cut a text run at every DEL/BS byte. A held backspace arrives as one chunk
|
|
177
|
+
* (`\x7f\x7f\x7f`) and must land as three key events, not one garbage key.
|
|
178
|
+
* Carriage returns and tabs stay inside their run.
|
|
155
179
|
*/
|
|
156
|
-
|
|
157
|
-
const
|
|
158
|
-
|
|
180
|
+
function splitEditingBytes(text) {
|
|
181
|
+
const pieces = [];
|
|
182
|
+
let run = '';
|
|
183
|
+
for (const character of text) {
|
|
184
|
+
if (character !== '\x7f' && character !== '\x08') {
|
|
185
|
+
run += character;
|
|
186
|
+
continue;
|
|
187
|
+
}
|
|
188
|
+
if (run) pieces.push(run);
|
|
189
|
+
pieces.push(character);
|
|
190
|
+
run = '';
|
|
191
|
+
}
|
|
192
|
+
if (run) pieces.push(run);
|
|
193
|
+
return pieces;
|
|
194
|
+
}
|
|
159
195
|
|
|
196
|
+
/** Bracketed-paste accumulator: everything between the markers is literal. */
|
|
197
|
+
function pasteAccumulator(open, buffer) {
|
|
198
|
+
return {
|
|
199
|
+
open,
|
|
200
|
+
buffer,
|
|
201
|
+
start() {
|
|
202
|
+
this.open = true;
|
|
203
|
+
this.buffer = '';
|
|
204
|
+
},
|
|
205
|
+
absorb(text) {
|
|
206
|
+
this.buffer += text;
|
|
207
|
+
},
|
|
208
|
+
take() {
|
|
209
|
+
const content = this.buffer;
|
|
210
|
+
this.open = false;
|
|
211
|
+
this.buffer = '';
|
|
212
|
+
return content;
|
|
213
|
+
},
|
|
214
|
+
};
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
function asInputString(input) {
|
|
218
|
+
if (typeof input === 'string') return input;
|
|
219
|
+
return String(input ?? '');
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
/**
|
|
223
|
+
* Feed a chunk (or null to flush) through the tokenizer and turn the resulting
|
|
224
|
+
* tokens into typed events. Returns [events, newState]; the tokenizer instance
|
|
225
|
+
* rides on the returned state so incomplete sequences buffer across calls.
|
|
226
|
+
*/
|
|
227
|
+
export function parseMultipleKeypresses(prevState, input = '') {
|
|
228
|
+
const flushing = input === null;
|
|
160
229
|
const tokenizer = prevState._tokenizer ?? createTokenizer({ x10Mouse: true });
|
|
161
|
-
const tokens =
|
|
230
|
+
const tokens = flushing ? tokenizer.flush() : tokenizer.feed(asInputString(input));
|
|
162
231
|
|
|
163
|
-
const
|
|
164
|
-
|
|
165
|
-
let pasteBuffer = prevState.pasteBuffer;
|
|
232
|
+
const events = [];
|
|
233
|
+
const paste = pasteAccumulator(prevState.mode === 'IN_PASTE', prevState.pasteBuffer);
|
|
166
234
|
|
|
167
235
|
for (const token of tokens) {
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
} else if (inPaste) {
|
|
178
|
-
// Sequences inside paste are literal text.
|
|
179
|
-
pasteBuffer += token.value;
|
|
180
|
-
} else {
|
|
181
|
-
const response = parseTerminalResponse(token.value);
|
|
182
|
-
if (response) {
|
|
183
|
-
keys.push({ kind: 'response', sequence: token.value, response });
|
|
184
|
-
} else {
|
|
185
|
-
const mouse = parseMouseEvent(token.value);
|
|
186
|
-
keys.push(mouse ?? asKey(token.value));
|
|
187
|
-
}
|
|
188
|
-
}
|
|
189
|
-
} else if (token.type === 'text') {
|
|
190
|
-
if (inPaste) {
|
|
191
|
-
pasteBuffer += token.value;
|
|
192
|
-
} else if (
|
|
193
|
-
/^\[<\d+;\d+;\d+[Mm]$/.test(token.value) ||
|
|
194
|
-
/^\[M[\x60-\x7f][\x20-\uffff]{2}$/.test(token.value)
|
|
195
|
-
) {
|
|
196
|
-
// Orphaned SGR/X10 mouse tail (fullscreen only). A heavy render blocked
|
|
197
|
-
// the event loop past the pending-ESC flush timer, so the buffered ESC
|
|
198
|
-
// flushed as a lone Escape and the continuation `[<btn;col;rowM` arrived
|
|
199
|
-
// as text. Re-synthesize with the ESC prefix so the scroll/mouse event
|
|
200
|
-
// still fires. X10 Cb slot narrowed to wheel range [\x60-\x7f] so typed
|
|
201
|
-
// input like `[MAX]` batched into one read isn't dropped as a click.
|
|
202
|
-
const resynthesized = '\x1b' + token.value;
|
|
203
|
-
const mouse = parseMouseEvent(resynthesized);
|
|
204
|
-
keys.push(mouse ?? asKey(resynthesized));
|
|
205
|
-
} else {
|
|
206
|
-
// Split 0x7F (DEL) and 0x08 (BS) bytes into individual key events so
|
|
207
|
-
// held-backspace chunks (`\x7f\x7f\x7f`) don't parse as one garbage
|
|
208
|
-
// key. \r and \t are left intact — only backspace bytes are split.
|
|
209
|
-
let run = '';
|
|
210
|
-
for (const ch of token.value) {
|
|
211
|
-
if (ch === '\x7f' || ch === '\x08') {
|
|
212
|
-
if (run) {
|
|
213
|
-
keys.push(asKey(run));
|
|
214
|
-
run = '';
|
|
215
|
-
}
|
|
216
|
-
keys.push(asKey(ch));
|
|
217
|
-
} else {
|
|
218
|
-
run += ch;
|
|
219
|
-
}
|
|
220
|
-
}
|
|
221
|
-
if (run) keys.push(asKey(run));
|
|
222
|
-
}
|
|
236
|
+
const isText = token.type === 'text';
|
|
237
|
+
if (!isText && token.value === PASTE_START) {
|
|
238
|
+
paste.start();
|
|
239
|
+
continue;
|
|
240
|
+
}
|
|
241
|
+
if (!isText && token.value === PASTE_END) {
|
|
242
|
+
// An empty paste still emits a key so callers can react to it.
|
|
243
|
+
events.push(createPasteKey(paste.take()));
|
|
244
|
+
continue;
|
|
223
245
|
}
|
|
246
|
+
if (paste.open) {
|
|
247
|
+
paste.absorb(token.value);
|
|
248
|
+
continue;
|
|
249
|
+
}
|
|
250
|
+
if (!isText) {
|
|
251
|
+
events.push(eventForSequence(token.value));
|
|
252
|
+
continue;
|
|
253
|
+
}
|
|
254
|
+
const orphan = eventForOrphanTail(token.value);
|
|
255
|
+
if (orphan) {
|
|
256
|
+
events.push(orphan);
|
|
257
|
+
continue;
|
|
258
|
+
}
|
|
259
|
+
for (const piece of splitEditingBytes(token.value)) events.push(asKey(piece));
|
|
224
260
|
}
|
|
225
261
|
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
inPaste = false;
|
|
229
|
-
pasteBuffer = '';
|
|
230
|
-
}
|
|
262
|
+
// A paste left open at end-of-stream is delivered rather than dropped.
|
|
263
|
+
if (flushing && paste.open && paste.buffer) events.push(createPasteKey(paste.take()));
|
|
231
264
|
|
|
232
|
-
|
|
233
|
-
mode:
|
|
265
|
+
return [events, {
|
|
266
|
+
mode: paste.open ? 'IN_PASTE' : 'NORMAL',
|
|
234
267
|
incomplete: tokenizer.buffer(),
|
|
235
|
-
pasteBuffer,
|
|
268
|
+
pasteBuffer: paste.buffer,
|
|
236
269
|
_tokenizer: tokenizer,
|
|
237
|
-
};
|
|
238
|
-
|
|
239
|
-
return [keys, newState];
|
|
270
|
+
}];
|
|
240
271
|
}
|
|
@@ -1,240 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* termio-keypress.js — chunked keypress pipeline built on the termio tokenizer.
|
|
3
|
-
*
|
|
4
|
-
* Keypress parsing (parseMultipleKeypresses +
|
|
5
|
-
* paste IN_PASTE buffering + orphaned mouse-tail resynthesis + terminal-response
|
|
6
|
-
* / SGR-mouse recognition) to plain ESM JS.
|
|
7
|
-
*
|
|
8
|
-
* It emits a flat list of typed events:
|
|
9
|
-
* { kind: 'key', sequence, isPasted, ... } normal keypress or paste
|
|
10
|
-
* { kind: 'mouse', button, action, col, row, sequence } SGR click/drag
|
|
11
|
-
* { kind: 'response', sequence, response } terminal query reply
|
|
12
|
-
*
|
|
13
|
-
* Single-sequence keypress decoding is delegated to the existing kitty-aware
|
|
14
|
-
* parseKeypress (parse-keypress.js) so kitty/modifyOtherKeys handling is shared.
|
|
15
|
-
*/
|
|
16
|
-
import { createTokenizer, PASTE_START, PASTE_END } from './_baseline-termio-tokenize.js';
|
|
17
|
-
import parseKeypress from './parse-keypress.js';
|
|
18
|
-
|
|
19
|
-
// SGR mouse event: CSI < button ; col ; row M (press) or m (release).
|
|
20
|
-
// Button bit 0x40 = wheel, bit 0x20 = drag/motion.
|
|
21
|
-
// eslint-disable-next-line no-control-regex
|
|
22
|
-
const SGR_MOUSE_RE = /^\x1b\[<(\d+);(\d+);(\d+)([Mm])$/;
|
|
23
|
-
|
|
24
|
-
// Wheel button classification shared by SGR + X10 decoding. Button bit 0x40 =
|
|
25
|
-
// wheel; low bit selects direction (0=up, 1=down). Mask 0x43 checks wheel-flag
|
|
26
|
-
// + direction while ignoring modifier bits so modified wheel events still
|
|
27
|
-
// classify. The vendored parse-keypress.js (enquirer-based) has NO mouse
|
|
28
|
-
// branches, so wheel naming lives here.
|
|
29
|
-
function wheelName(s) {
|
|
30
|
-
let button;
|
|
31
|
-
const m = SGR_MOUSE_RE.exec(s);
|
|
32
|
-
if (m) {
|
|
33
|
-
button = parseInt(m[1], 10);
|
|
34
|
-
} else if (s.length === 6 && s.startsWith('\x1b[M')) {
|
|
35
|
-
// X10: CSI M + 3 bytes (Cb+32, Cx+32, Cy+32).
|
|
36
|
-
button = s.charCodeAt(3) - 32;
|
|
37
|
-
} else {
|
|
38
|
-
return null;
|
|
39
|
-
}
|
|
40
|
-
if ((button & 0x43) === 0x40) return 'wheelup';
|
|
41
|
-
if ((button & 0x43) === 0x41) return 'wheeldown';
|
|
42
|
-
return null;
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
// -- Terminal response patterns (inbound sequences from the terminal itself) --
|
|
46
|
-
// eslint-disable-next-line no-control-regex
|
|
47
|
-
const DECRPM_RE = /^\x1b\[\?(\d+);(\d+)\$y$/;
|
|
48
|
-
// eslint-disable-next-line no-control-regex
|
|
49
|
-
const DA1_RE = /^\x1b\[\?([\d;]*)c$/;
|
|
50
|
-
// eslint-disable-next-line no-control-regex
|
|
51
|
-
const DA2_RE = /^\x1b\[>([\d;]*)c$/;
|
|
52
|
-
// eslint-disable-next-line no-control-regex
|
|
53
|
-
const KITTY_FLAGS_RE = /^\x1b\[\?(\d+)u$/;
|
|
54
|
-
// eslint-disable-next-line no-control-regex
|
|
55
|
-
const CURSOR_POSITION_RE = /^\x1b\[\?(\d+);(\d+)R$/;
|
|
56
|
-
// eslint-disable-next-line no-control-regex
|
|
57
|
-
const OSC_RESPONSE_RE = /^\x1b\](\d+);(.*?)(?:\x07|\x1b\\)$/s;
|
|
58
|
-
// eslint-disable-next-line no-control-regex
|
|
59
|
-
const XTVERSION_RE = /^\x1bP>\|(.*?)(?:\x07|\x1b\\)$/s;
|
|
60
|
-
|
|
61
|
-
/** DECRPM status values (response to DECRQM). */
|
|
62
|
-
export const DECRPM_STATUS = {
|
|
63
|
-
NOT_RECOGNIZED: 0,
|
|
64
|
-
SET: 1,
|
|
65
|
-
RESET: 2,
|
|
66
|
-
PERMANENTLY_SET: 3,
|
|
67
|
-
PERMANENTLY_RESET: 4,
|
|
68
|
-
};
|
|
69
|
-
|
|
70
|
-
function splitNumericParams(params) {
|
|
71
|
-
if (!params) return [];
|
|
72
|
-
return params.split(';').map((p) => parseInt(p, 10));
|
|
73
|
-
}
|
|
74
|
-
|
|
75
|
-
/**
|
|
76
|
-
* Recognize a sequence token as a terminal response, or null if it should be
|
|
77
|
-
* treated as a keypress. These patterns are syntactically distinguishable from
|
|
78
|
-
* keyboard input (no physical key produces CSI ? ... c etc.).
|
|
79
|
-
*/
|
|
80
|
-
function parseTerminalResponse(s) {
|
|
81
|
-
if (s.startsWith('\x1b[')) {
|
|
82
|
-
let m;
|
|
83
|
-
if ((m = DECRPM_RE.exec(s))) {
|
|
84
|
-
return { type: 'decrpm', mode: parseInt(m[1], 10), status: parseInt(m[2], 10) };
|
|
85
|
-
}
|
|
86
|
-
if ((m = DA1_RE.exec(s))) return { type: 'da1', params: splitNumericParams(m[1]) };
|
|
87
|
-
if ((m = DA2_RE.exec(s))) return { type: 'da2', params: splitNumericParams(m[1]) };
|
|
88
|
-
if ((m = KITTY_FLAGS_RE.exec(s))) return { type: 'kittyKeyboard', flags: parseInt(m[1], 10) };
|
|
89
|
-
if ((m = CURSOR_POSITION_RE.exec(s))) {
|
|
90
|
-
return { type: 'cursorPosition', row: parseInt(m[1], 10), col: parseInt(m[2], 10) };
|
|
91
|
-
}
|
|
92
|
-
return null;
|
|
93
|
-
}
|
|
94
|
-
if (s.startsWith('\x1b]')) {
|
|
95
|
-
const m = OSC_RESPONSE_RE.exec(s);
|
|
96
|
-
if (m) return { type: 'osc', code: parseInt(m[1], 10), data: m[2] };
|
|
97
|
-
}
|
|
98
|
-
if (s.startsWith('\x1bP')) {
|
|
99
|
-
const m = XTVERSION_RE.exec(s);
|
|
100
|
-
if (m) return { type: 'xtversion', name: m[1] };
|
|
101
|
-
}
|
|
102
|
-
return null;
|
|
103
|
-
}
|
|
104
|
-
|
|
105
|
-
/**
|
|
106
|
-
* Parse an SGR mouse sequence into a ParsedMouse, or null if not a mouse event
|
|
107
|
-
* or if it's a wheel event (wheel stays a keypress for scroll routing).
|
|
108
|
-
*/
|
|
109
|
-
function parseMouseEvent(s) {
|
|
110
|
-
const match = SGR_MOUSE_RE.exec(s);
|
|
111
|
-
if (!match) return null;
|
|
112
|
-
const button = parseInt(match[1], 10);
|
|
113
|
-
// Wheel events (bit 6 set) stay as keys so the scroll path can route them.
|
|
114
|
-
if ((button & 0x40) !== 0) return null;
|
|
115
|
-
return {
|
|
116
|
-
kind: 'mouse',
|
|
117
|
-
button,
|
|
118
|
-
action: match[4] === 'M' ? 'press' : 'release',
|
|
119
|
-
col: parseInt(match[2], 10),
|
|
120
|
-
row: parseInt(match[3], 10),
|
|
121
|
-
sequence: s,
|
|
122
|
-
};
|
|
123
|
-
}
|
|
124
|
-
|
|
125
|
-
function createPasteKey(content) {
|
|
126
|
-
const key = parseKeypress('');
|
|
127
|
-
key.kind = 'key';
|
|
128
|
-
key.sequence = content;
|
|
129
|
-
key.raw = content;
|
|
130
|
-
key.isPasted = true;
|
|
131
|
-
return key;
|
|
132
|
-
}
|
|
133
|
-
|
|
134
|
-
function asKey(s) {
|
|
135
|
-
const key = parseKeypress(s);
|
|
136
|
-
key.kind = 'key';
|
|
137
|
-
if (key.isPasted === undefined) key.isPasted = false;
|
|
138
|
-
// Vendored parse-keypress.js has no mouse branches; classify wheel here so
|
|
139
|
-
// App.js can route wheel keys to the 'mouse' channel.
|
|
140
|
-
const wheel = wheelName(s);
|
|
141
|
-
if (wheel) key.name = wheel;
|
|
142
|
-
return key;
|
|
143
|
-
}
|
|
144
|
-
|
|
145
|
-
export const INITIAL_STATE = {
|
|
146
|
-
mode: 'NORMAL',
|
|
147
|
-
incomplete: '',
|
|
148
|
-
pasteBuffer: '',
|
|
149
|
-
};
|
|
150
|
-
|
|
151
|
-
/**
|
|
152
|
-
* Feed a chunk (or null to flush) through the tokenizer + paste/mouse/response
|
|
153
|
-
* classification. Returns [events, newState]. The tokenizer instance lives on
|
|
154
|
-
* the returned state so incomplete sequences buffer across calls.
|
|
155
|
-
*/
|
|
156
|
-
export function parseMultipleKeypresses(prevState, input = '') {
|
|
157
|
-
const isFlush = input === null;
|
|
158
|
-
const inputString = isFlush ? '' : typeof input === 'string' ? input : String(input ?? '');
|
|
159
|
-
|
|
160
|
-
const tokenizer = prevState._tokenizer ?? createTokenizer({ x10Mouse: true });
|
|
161
|
-
const tokens = isFlush ? tokenizer.flush() : tokenizer.feed(inputString);
|
|
162
|
-
|
|
163
|
-
const keys = [];
|
|
164
|
-
let inPaste = prevState.mode === 'IN_PASTE';
|
|
165
|
-
let pasteBuffer = prevState.pasteBuffer;
|
|
166
|
-
|
|
167
|
-
for (const token of tokens) {
|
|
168
|
-
if (token.type === 'sequence') {
|
|
169
|
-
if (token.value === PASTE_START) {
|
|
170
|
-
inPaste = true;
|
|
171
|
-
pasteBuffer = '';
|
|
172
|
-
} else if (token.value === PASTE_END) {
|
|
173
|
-
// Always emit a paste key, even for empty pastes.
|
|
174
|
-
keys.push(createPasteKey(pasteBuffer));
|
|
175
|
-
inPaste = false;
|
|
176
|
-
pasteBuffer = '';
|
|
177
|
-
} else if (inPaste) {
|
|
178
|
-
// Sequences inside paste are literal text.
|
|
179
|
-
pasteBuffer += token.value;
|
|
180
|
-
} else {
|
|
181
|
-
const response = parseTerminalResponse(token.value);
|
|
182
|
-
if (response) {
|
|
183
|
-
keys.push({ kind: 'response', sequence: token.value, response });
|
|
184
|
-
} else {
|
|
185
|
-
const mouse = parseMouseEvent(token.value);
|
|
186
|
-
keys.push(mouse ?? asKey(token.value));
|
|
187
|
-
}
|
|
188
|
-
}
|
|
189
|
-
} else if (token.type === 'text') {
|
|
190
|
-
if (inPaste) {
|
|
191
|
-
pasteBuffer += token.value;
|
|
192
|
-
} else if (
|
|
193
|
-
/^\[<\d+;\d+;\d+[Mm]$/.test(token.value) ||
|
|
194
|
-
/^\[M[\x60-\x7f][\x20-\uffff]{2}$/.test(token.value)
|
|
195
|
-
) {
|
|
196
|
-
// Orphaned SGR/X10 mouse tail (fullscreen only). A heavy render blocked
|
|
197
|
-
// the event loop past the pending-ESC flush timer, so the buffered ESC
|
|
198
|
-
// flushed as a lone Escape and the continuation `[<btn;col;rowM` arrived
|
|
199
|
-
// as text. Re-synthesize with the ESC prefix so the scroll/mouse event
|
|
200
|
-
// still fires. X10 Cb slot narrowed to wheel range [\x60-\x7f] so typed
|
|
201
|
-
// input like `[MAX]` batched into one read isn't dropped as a click.
|
|
202
|
-
const resynthesized = '\x1b' + token.value;
|
|
203
|
-
const mouse = parseMouseEvent(resynthesized);
|
|
204
|
-
keys.push(mouse ?? asKey(resynthesized));
|
|
205
|
-
} else {
|
|
206
|
-
// Split 0x7F (DEL) and 0x08 (BS) bytes into individual key events so
|
|
207
|
-
// held-backspace chunks (`\x7f\x7f\x7f`) don't parse as one garbage
|
|
208
|
-
// key. \r and \t are left intact — only backspace bytes are split.
|
|
209
|
-
let run = '';
|
|
210
|
-
for (const ch of token.value) {
|
|
211
|
-
if (ch === '\x7f' || ch === '\x08') {
|
|
212
|
-
if (run) {
|
|
213
|
-
keys.push(asKey(run));
|
|
214
|
-
run = '';
|
|
215
|
-
}
|
|
216
|
-
keys.push(asKey(ch));
|
|
217
|
-
} else {
|
|
218
|
-
run += ch;
|
|
219
|
-
}
|
|
220
|
-
}
|
|
221
|
-
if (run) keys.push(asKey(run));
|
|
222
|
-
}
|
|
223
|
-
}
|
|
224
|
-
}
|
|
225
|
-
|
|
226
|
-
if (isFlush && inPaste && pasteBuffer) {
|
|
227
|
-
keys.push(createPasteKey(pasteBuffer));
|
|
228
|
-
inPaste = false;
|
|
229
|
-
pasteBuffer = '';
|
|
230
|
-
}
|
|
231
|
-
|
|
232
|
-
const newState = {
|
|
233
|
-
mode: inPaste ? 'IN_PASTE' : 'NORMAL',
|
|
234
|
-
incomplete: tokenizer.buffer(),
|
|
235
|
-
pasteBuffer,
|
|
236
|
-
_tokenizer: tokenizer,
|
|
237
|
-
};
|
|
238
|
-
|
|
239
|
-
return [keys, newState];
|
|
240
|
-
}
|