@semalt-ai/code 1.7.0 → 1.8.0

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.
@@ -0,0 +1,1176 @@
1
+ 'use strict';
2
+
3
+ const EventEmitter = require('events');
4
+ const readline = require('readline');
5
+
6
+ const { RST, DIM, FG_CYAN, FG_GREEN, FG_YELLOW } = require('./ansi');
7
+
8
+ const SLASH_CMDS = [
9
+ '/help','/file','/new','/model','/models','/shell','/compact',
10
+ '/clear','/approve','/config','/history','/login','/whoami','/logout','/chats',
11
+ ];
12
+
13
+ // ─── Key sequence parser ──────────────────────────────────────────────────────
14
+
15
+ function parseKeySequence(buf) {
16
+ const b = Buffer.isBuffer(buf) ? buf : Buffer.from(buf);
17
+ if (!b.length) return null;
18
+ const b0 = b[0];
19
+
20
+ const SINGLE = {
21
+ 0x7f:'backspace', 0x08:'backspace', 0x0d:'enter', 0x0a:'enter',
22
+ 0x09:'tab', 0x01:'ctrl+a', 0x02:'ctrl+b', 0x05:'ctrl+e',
23
+ 0x06:'ctrl+f', 0x07:'ctrl+g', 0x0b:'ctrl+k', 0x0e:'ctrl+n',
24
+ 0x10:'ctrl+p', 0x12:'ctrl+r', 0x14:'ctrl+t', 0x15:'ctrl+u',
25
+ 0x17:'ctrl+w', 0x0c:'ctrl+l', 0x03:'ctrl+c', 0x04:'ctrl+d', 0x0f:'ctrl+o',
26
+ };
27
+ if (SINGLE[b0]) return { key: SINGLE[b0], len: 1 };
28
+
29
+ if (b0 !== 0x1b) return null; // printable — caller handles
30
+
31
+ const INCOMPLETE = { key: null, len: 0, incomplete: true };
32
+ if (b.length === 1) return { key: 'escape', len: 1, incomplete: true };
33
+
34
+ const b1 = b[1];
35
+ if (b1 === 0x62) return { key: 'alt+b', len: 2 };
36
+ if (b1 === 0x66) return { key: 'alt+f', len: 2 };
37
+ if (b1 === 0x0d) return { key: 'shift+enter', len: 2 }; // ESC+CR (\x1b\r)
38
+
39
+ if (b1 === 0x5b) { // CSI '['
40
+ if (b.length < 3) return INCOMPLETE;
41
+ const b2 = b[2];
42
+ if (b2 === 0x41) return { key: 'up', len: 3 };
43
+ if (b2 === 0x42) return { key: 'down', len: 3 };
44
+ if (b2 === 0x43) return { key: 'right', len: 3 };
45
+ if (b2 === 0x44) return { key: 'left', len: 3 };
46
+ if (b2 === 0x48) return { key: 'home', len: 3 };
47
+ if (b2 === 0x46) return { key: 'end', len: 3 };
48
+
49
+ if (b2 === 0x33 || b2 === 0x35 || b2 === 0x36) {
50
+ if (b.length < 4) return INCOMPLETE;
51
+ if (b[3] === 0x7e) {
52
+ if (b2 === 0x33) return { key: 'delete', len: 4 };
53
+ if (b2 === 0x35) return { key: 'pageup', len: 4 };
54
+ if (b2 === 0x36) return { key: 'pagedown', len: 4 };
55
+ }
56
+ }
57
+
58
+ if (b2 === 0x31) { // [1...
59
+ if (b.length < 4) return INCOMPLETE;
60
+ if (b[3] === 0x3b) { // [1;...
61
+ if (b.length < 6) return INCOMPLETE;
62
+ const mod = b[4], fin = b[5];
63
+ if (mod === 0x35 && fin === 0x43) return { key: 'ctrl+right', len: 6 };
64
+ if (mod === 0x35 && fin === 0x44) return { key: 'ctrl+left', len: 6 };
65
+ if (mod === 0x32 && fin === 0x41) return { key: 'shift+up', len: 6 };
66
+ if (mod === 0x32 && fin === 0x42) return { key: 'shift+down', len: 6 };
67
+ }
68
+ // [13;2u — kitty Shift+Enter
69
+ if (b[3] === 0x33 && b.length >= 7 &&
70
+ b[4] === 0x3b && b[5] === 0x32 && b[6] === 0x75)
71
+ return { key: 'shift+enter', len: 7 };
72
+ }
73
+
74
+ // [200~ / [201~ — bracketed paste start/end
75
+ if (b2 === 0x32 && b.length >= 6 && b[3] === 0x30 && b[5] === 0x7e) {
76
+ if (b[4] === 0x30) return { key: 'paste-start', len: 6 };
77
+ if (b[4] === 0x31) return { key: 'paste-end', len: 6 };
78
+ }
79
+
80
+ // [27;2;13~ — xterm modifyOtherKeys Shift+Enter
81
+ if (b2 === 0x32 && b.length >= 10 &&
82
+ b[3]===0x37 && b[4]===0x3b && b[5]===0x32 && b[6]===0x3b &&
83
+ b[7]===0x31 && b[8]===0x33 && b[9]===0x7e)
84
+ return { key: 'shift+enter', len: 10 };
85
+
86
+ let j = 3;
87
+ while (j < b.length && (b[j] < 0x40 || b[j] > 0x7e)) j++;
88
+ if (j >= b.length) return INCOMPLETE;
89
+ return { key: 'unknown', len: j + 1 };
90
+ }
91
+
92
+ if (b1 === 0x4f) { // SS3 'O'
93
+ if (b.length < 3) return INCOMPLETE;
94
+ const b2 = b[2];
95
+ if (b2 === 0x41) return { key: 'up', len: 3 };
96
+ if (b2 === 0x42) return { key: 'down', len: 3 };
97
+ if (b2 === 0x43) return { key: 'right', len: 3 };
98
+ if (b2 === 0x44) return { key: 'left', len: 3 };
99
+ if (b2 === 0x48) return { key: 'home', len: 3 };
100
+ if (b2 === 0x46) return { key: 'end', len: 3 };
101
+ return { key: 'unknown', len: 3 };
102
+ }
103
+
104
+ return { key: 'unknown', len: 2 };
105
+ }
106
+
107
+ // ─── InputField ───────────────────────────────────────────────────────────────
108
+
109
+ class InputField extends EventEmitter {
110
+ constructor(layout, chatHistory) {
111
+ super();
112
+ this._layout = layout;
113
+ this._chatHistory = chatHistory;
114
+ this._chars = [];
115
+ this._cursor = 0;
116
+ this._disabled = false;
117
+ this._inputHeight = 1;
118
+ this._hint = '';
119
+ this._tabCycleIdx = 0;
120
+ this._tabMatches = [];
121
+ this._history = [];
122
+ this._historyIdx = -1;
123
+ this._draft = '';
124
+ this._selectCapture = null;
125
+ this._blink = true;
126
+ this._idle = false;
127
+ this._idleTimer = null;
128
+ this._cursorHidden = false;
129
+ this._blinkTimer = setInterval(() => {
130
+ if (this._idle || this._disabled) return;
131
+ this._blink = !this._blink;
132
+ if (!this._pasteMode) this._render();
133
+ }, 500);
134
+ this._escBuf = null;
135
+ this._escTimer = null;
136
+ this._utf8Buf = null;
137
+ this._pasteMode = false;
138
+ this._pasteStartIdx = null;
139
+ this._abbreviatedRanges = [];
140
+ this._pasteCounter = 0;
141
+ this._syntheticPaste = false;
142
+ this._navCapture = null;
143
+ this._navSearchMode = false;
144
+ this._navSearchQuery = '';
145
+
146
+ // History search state
147
+ this._searchMode = false;
148
+ this._searchQuery = '';
149
+ this._searchMatchIdx = -1;
150
+ this._searchMatchType = null;
151
+ this._searchExtraItems = [];
152
+ this._savedChars = null;
153
+ this._savedCursor = 0;
154
+
155
+ this._onData = (chunk) => this._handleData(chunk);
156
+ if (process.stdin.isTTY) {
157
+ process.stdin.setRawMode(true);
158
+ process.stdin.resume();
159
+ process.stdout.write('\x1b[?2004h'); // bracketed paste
160
+ process.stdout.write('\x1b[>4;2m'); // xterm modifyOtherKeys level 2
161
+ process.stdout.write('\x1b[>1u'); // kitty keyboard protocol
162
+ process.stdin.on('data', this._onData);
163
+ this._idleTimer = setTimeout(() => this._goIdle(), 0);
164
+ }
165
+ }
166
+
167
+ // ── Public API ───────────────────────────────────────────────────────────────
168
+
169
+ setDisabled(val) {
170
+ this._disabled = !!val;
171
+ if (this._disabled) {
172
+ this._goIdle();
173
+ } else {
174
+ this._goActive();
175
+ }
176
+ this._renderHints();
177
+ this._render();
178
+ }
179
+ getValue() { return this._chars.join(''); }
180
+ onSubmit(cb) { this.on('submit', cb); }
181
+
182
+ captureSelect(menu) {
183
+ return new Promise((resolve) => {
184
+ this._selectCapture = { menu, resolve };
185
+ });
186
+ }
187
+
188
+ hideCursor() {
189
+ if (!this._cursorHidden) {
190
+ this._cursorHidden = true;
191
+ process.stdout.write('\x1b[?25l');
192
+ }
193
+ }
194
+
195
+ showCursor() {
196
+ if (this._cursorHidden) {
197
+ this._cursorHidden = false;
198
+ process.stdout.write('\x1b[?25h');
199
+ }
200
+ }
201
+
202
+ captureNavigation(handler) {
203
+ this._navCapture = handler;
204
+ this._navSearchMode = false;
205
+ this._navSearchQuery = '';
206
+ this._deleteLine();
207
+ this._renderHints();
208
+ this._render();
209
+ this.hideCursor();
210
+ }
211
+
212
+ releaseNavigation() {
213
+ this._navCapture = null;
214
+ this._navSearchMode = false;
215
+ this._navSearchQuery = '';
216
+ this.showCursor();
217
+ this._renderHints();
218
+ this._render();
219
+ }
220
+
221
+ // Stop all periodic stdout writes so the terminal viewport can scroll freely.
222
+ _goIdle() {
223
+ this._idle = true;
224
+ clearTimeout(this._idleTimer);
225
+ this._idleTimer = null;
226
+ this.hideCursor();
227
+ this.emit('idle');
228
+ }
229
+
230
+ // Resume periodic writes and restart the idle countdown.
231
+ _goActive() {
232
+ const wasIdle = this._idle;
233
+ this._idle = false;
234
+ clearTimeout(this._idleTimer);
235
+ this._idleTimer = setTimeout(() => this._goIdle(), 0);
236
+ if (wasIdle) {
237
+ this.showCursor();
238
+ this.emit('active');
239
+ }
240
+ }
241
+
242
+ setSearchItems(items) {
243
+ this._searchExtraItems = Array.isArray(items) ? items : [];
244
+ }
245
+
246
+ suspend() {
247
+ if (process.stdin.isTTY) process.stdin.removeListener('data', this._onData);
248
+ }
249
+ resume() {
250
+ if (process.stdin.isTTY) {
251
+ process.stdin.resume();
252
+ process.stdin.removeListener('data', this._onData);
253
+ process.stdin.on('data', this._onData);
254
+ }
255
+ this._renderHints();
256
+ this._render();
257
+ }
258
+
259
+ // ── Line buffer operations ───────────────────────────────────────────────────
260
+
261
+ _insertChar(ch) {
262
+ if (!this._pasteMode) this._rangeShiftInsert(this._cursor);
263
+ this._chars.splice(this._cursor, 0, ch);
264
+ this._cursor++;
265
+ }
266
+
267
+ _backspace() {
268
+ if (this._cursor <= 0) return;
269
+ const ri = this._abbreviatedRanges.findIndex(r => r.end === this._cursor);
270
+ if (ri >= 0) {
271
+ const r = this._abbreviatedRanges[ri];
272
+ const len = r.end - r.start;
273
+ this._chars.splice(r.start, len);
274
+ this._cursor = r.start;
275
+ this._abbreviatedRanges.splice(ri, 1);
276
+ for (const o of this._abbreviatedRanges) {
277
+ if (o.start >= r.end) { o.start -= len; o.end -= len; }
278
+ }
279
+ return;
280
+ }
281
+ this._rangeShiftDelete(this._cursor - 1, 1);
282
+ this._chars.splice(this._cursor - 1, 1);
283
+ this._cursor--;
284
+ }
285
+
286
+ _deleteForward() {
287
+ if (this._cursor >= this._chars.length) return;
288
+ const ri = this._abbreviatedRanges.findIndex(r => r.start === this._cursor);
289
+ if (ri >= 0) {
290
+ const r = this._abbreviatedRanges[ri];
291
+ const len = r.end - r.start;
292
+ this._chars.splice(r.start, len);
293
+ this._abbreviatedRanges.splice(ri, 1);
294
+ for (const o of this._abbreviatedRanges) {
295
+ if (o.start >= r.end) { o.start -= len; o.end -= len; }
296
+ }
297
+ return;
298
+ }
299
+ this._rangeShiftDelete(this._cursor, 1);
300
+ this._chars.splice(this._cursor, 1);
301
+ }
302
+
303
+ _deleteWordBefore() {
304
+ let pos = this._cursor;
305
+ while (pos > 0 && this._chars[pos - 1] === ' ') pos--;
306
+ while (pos > 0 && this._chars[pos - 1] !== ' ') pos--;
307
+ const len = this._cursor - pos;
308
+ if (!len) return;
309
+ this._rangeShiftDelete(pos, len);
310
+ this._chars.splice(pos, len);
311
+ this._cursor = pos;
312
+ }
313
+
314
+ _deleteWordForward() {
315
+ let pos = this._cursor;
316
+ while (pos < this._chars.length && this._chars[pos] !== ' ') pos++;
317
+ while (pos < this._chars.length && this._chars[pos] === ' ') pos++;
318
+ const len = pos - this._cursor;
319
+ if (!len) return;
320
+ this._rangeShiftDelete(this._cursor, len);
321
+ this._chars.splice(this._cursor, len);
322
+ }
323
+
324
+ _deleteToEnd() {
325
+ const len = this._chars.length - this._cursor;
326
+ if (len > 0) this._rangeShiftDelete(this._cursor, len);
327
+ this._chars = this._chars.slice(0, this._cursor);
328
+ }
329
+
330
+ _deleteLine() {
331
+ this._abbreviatedRanges = [];
332
+ this._pasteCounter = 0;
333
+ this._chars = [];
334
+ this._cursor = 0;
335
+ }
336
+
337
+ _moveCursorLeft() {
338
+ if (this._cursor <= 0) return;
339
+ const newPos = this._cursor - 1;
340
+ for (const r of this._abbreviatedRanges) {
341
+ if (newPos > r.start && newPos < r.end) { this._cursor = r.start; return; }
342
+ }
343
+ this._cursor = newPos;
344
+ }
345
+
346
+ _moveCursorRight() {
347
+ if (this._cursor >= this._chars.length) return;
348
+ const newPos = this._cursor + 1;
349
+ for (const r of this._abbreviatedRanges) {
350
+ if (newPos > r.start && newPos < r.end) { this._cursor = r.end; return; }
351
+ }
352
+ this._cursor = newPos;
353
+ }
354
+
355
+ _moveToStart() { this._cursor = 0; }
356
+ _moveToEnd() { this._cursor = this._chars.length; }
357
+
358
+ _jumpWordForward() {
359
+ let pos = this._cursor;
360
+ while (pos < this._chars.length && this._chars[pos] === ' ') pos++;
361
+ while (pos < this._chars.length && this._chars[pos] !== ' ') pos++;
362
+ this._cursor = pos;
363
+ this._normalizeCursor();
364
+ }
365
+
366
+ _jumpWordBackward() {
367
+ let pos = this._cursor;
368
+ while (pos > 0 && this._chars[pos - 1] === ' ') pos--;
369
+ while (pos > 0 && this._chars[pos - 1] !== ' ') pos--;
370
+ this._cursor = pos;
371
+ this._normalizeCursor();
372
+ }
373
+
374
+ _transposeChars() {
375
+ if (this._cursor <= 0 || this._chars.length < 2) return;
376
+ const pos = this._cursor >= this._chars.length ? this._chars.length - 1 : this._cursor;
377
+ const tmp = this._chars[pos - 1];
378
+ this._chars[pos - 1] = this._chars[pos];
379
+ this._chars[pos] = tmp;
380
+ if (this._cursor < this._chars.length) this._cursor++;
381
+ }
382
+
383
+ _setValue(str) {
384
+ this._abbreviatedRanges = [];
385
+ this._chars = Array.from(str);
386
+ this._cursor = this._chars.length;
387
+ }
388
+
389
+ // ── Abbreviated-range helpers ────────────────────────────────────────────────
390
+
391
+ _rangeShiftInsert(idx) {
392
+ const newRanges = [];
393
+ for (const r of this._abbreviatedRanges) {
394
+ if (idx > r.start && idx < r.end) { /* insert inside range — invalidate */ }
395
+ else if (idx <= r.start) newRanges.push({ start: r.start + 1, end: r.end + 1, label: r.label });
396
+ else newRanges.push(r);
397
+ }
398
+ this._abbreviatedRanges = newRanges;
399
+ }
400
+
401
+ _rangeShiftDelete(delStart, delLen) {
402
+ const delEnd = delStart + delLen;
403
+ const newRanges = [];
404
+ for (const r of this._abbreviatedRanges) {
405
+ if (r.end <= delStart) newRanges.push(r);
406
+ else if (r.start >= delEnd) newRanges.push({ start: r.start - delLen, end: r.end - delLen, label: r.label });
407
+ // else overlaps deletion — invalidate
408
+ }
409
+ this._abbreviatedRanges = newRanges;
410
+ }
411
+
412
+ _normalizeCursor() {
413
+ for (const r of this._abbreviatedRanges) {
414
+ if (this._cursor > r.start && this._cursor < r.end) {
415
+ const toStart = this._cursor - r.start;
416
+ const toEnd = r.end - this._cursor;
417
+ this._cursor = toStart <= toEnd ? r.start : r.end;
418
+ break;
419
+ }
420
+ }
421
+ }
422
+
423
+ _checkAndAbbreviatePaste() {
424
+ if (this._pasteStartIdx == null) return;
425
+ const start = this._pasteStartIdx;
426
+ const end = this._cursor;
427
+ this._pasteStartIdx = null;
428
+ const pasteLen = end - start;
429
+ if (pasteLen <= 0) return;
430
+
431
+ const newRanges = [];
432
+ for (const r of this._abbreviatedRanges) {
433
+ if (r.end <= start) newRanges.push(r);
434
+ else if (r.start >= start) newRanges.push({ start: r.start + pasteLen, end: r.end + pasteLen, label: r.label });
435
+ }
436
+ this._abbreviatedRanges = newRanges;
437
+
438
+ const pastedChars = this._chars.slice(start, end);
439
+ const pastedStr = pastedChars.join('');
440
+ const lineCount = (pastedStr.match(/\n/g) || []).length + 1;
441
+ const isLarge = lineCount >= 2 || pastedStr.length >= 80;
442
+ if (!isLarge) return;
443
+
444
+ this._pasteCounter++;
445
+ const label = `[Pasted text #${this._pasteCounter}]`;
446
+ this._abbreviatedRanges.push({ start, end, label });
447
+ }
448
+
449
+ _abbreviateIfLarge() {
450
+ const str = this._chars.join('');
451
+ const lineCount = (str.match(/\n/g) || []).length + 1;
452
+ if (lineCount < 2 && str.length < 80) return;
453
+ this._pasteCounter++;
454
+ const label = `[Pasted text #${this._pasteCounter}]`;
455
+ this._abbreviatedRanges = [{ start: 0, end: this._chars.length, label }];
456
+ }
457
+
458
+ _getDisplayContent() {
459
+ if (!this._abbreviatedRanges.length) {
460
+ return { chars: this._chars, cursor: this._cursor };
461
+ }
462
+ const sorted = [...this._abbreviatedRanges].sort((a, b) => a.start - b.start);
463
+ const displayChars = [];
464
+ let displayCursor = -1;
465
+ let i = 0;
466
+ let ri = 0;
467
+
468
+ while (i < this._chars.length) {
469
+ const r = sorted[ri];
470
+ if (r && i === r.start) {
471
+ if (this._cursor >= r.start && this._cursor < r.end && displayCursor === -1) {
472
+ displayCursor = displayChars.length;
473
+ }
474
+ for (const ch of r.label) displayChars.push(ch);
475
+ if (this._cursor === r.end && displayCursor === -1) {
476
+ displayCursor = displayChars.length;
477
+ }
478
+ i = r.end;
479
+ ri++;
480
+ } else {
481
+ if (i === this._cursor && displayCursor === -1) displayCursor = displayChars.length;
482
+ displayChars.push(this._chars[i]);
483
+ i++;
484
+ }
485
+ }
486
+ if (displayCursor === -1) displayCursor = displayChars.length;
487
+ return { chars: displayChars, cursor: displayCursor };
488
+ }
489
+
490
+ // ── Autocomplete hint ────────────────────────────────────────────────────────
491
+
492
+ _clearHint() { this._hint = ''; this._tabMatches = []; this._tabCycleIdx = 0; }
493
+
494
+ _getFileMatches(partial) {
495
+ const path = require('path');
496
+ const fs = require('fs');
497
+ let dir, prefix;
498
+ if (partial.includes('/')) {
499
+ if (partial.endsWith('/')) { dir = partial; prefix = ''; }
500
+ else { dir = path.dirname(partial); prefix = path.basename(partial); }
501
+ } else {
502
+ dir = '.'; prefix = partial;
503
+ }
504
+ let entries;
505
+ try { entries = fs.readdirSync(dir || '.').sort(); } catch { return []; }
506
+ const matches = [];
507
+ for (const entry of entries) {
508
+ if (prefix && !entry.startsWith(prefix)) continue;
509
+ if (entry.startsWith('.') && !prefix.startsWith('.')) continue;
510
+ const full = dir === '.' ? entry : path.join(dir, entry).replace(/\\/g, '/');
511
+ let isDir = false;
512
+ try { isDir = fs.statSync(full).isDirectory(); } catch {}
513
+ matches.push(isDir ? full + '/' : full);
514
+ }
515
+ return matches;
516
+ }
517
+
518
+ // ── History ──────────────────────────────────────────────────────────────────
519
+
520
+ _histPrev() {
521
+ if (!this._history.length) return;
522
+ if (this._historyIdx === -1) this._draft = this.getValue();
523
+ this._historyIdx = Math.min(this._historyIdx + 1, this._history.length - 1);
524
+ this._setValue(this._history[this._historyIdx]);
525
+ this._abbreviateIfLarge();
526
+ this._render();
527
+ }
528
+
529
+ _histNext() {
530
+ if (this._historyIdx === -1) return;
531
+ this._historyIdx--;
532
+ if (this._historyIdx === -1) this._setValue(this._draft);
533
+ else this._setValue(this._history[this._historyIdx]);
534
+ this._abbreviateIfLarge();
535
+ this._render();
536
+ }
537
+
538
+ // ── History search (ctrl+r) ──────────────────────────────────────────────────
539
+
540
+ _enterSearchMode() {
541
+ this._savedChars = this._chars.slice();
542
+ this._savedCursor = this._cursor;
543
+ this._searchMode = true;
544
+ this._searchQuery = '';
545
+ this._searchMatchIdx = -1;
546
+ this._abbreviatedRanges = [];
547
+ this._chars = [];
548
+ this._cursor = 0;
549
+ this._renderHints();
550
+ this._render();
551
+ }
552
+
553
+ _exitSearchMode(accept) {
554
+ this._searchMode = false;
555
+ if (!accept) {
556
+ this._chars = this._savedChars || [];
557
+ this._cursor = this._savedCursor;
558
+ this._abbreviatedRanges = [];
559
+ }
560
+ this._savedChars = null;
561
+ this._renderHints();
562
+ this._render();
563
+ }
564
+
565
+ _getSearchSources() {
566
+ const sources = [];
567
+ for (const text of this._history)
568
+ sources.push({ type: 'history', text });
569
+ for (const item of this._searchExtraItems)
570
+ sources.push(item);
571
+ for (const cmd of SLASH_CMDS)
572
+ sources.push({ type: 'command', text: cmd });
573
+ return sources;
574
+ }
575
+
576
+ _doSearch(fromIdx) {
577
+ const q = this._searchQuery;
578
+ if (!q) {
579
+ this._searchMatchIdx = -1;
580
+ this._searchMatchType = null;
581
+ this._chars = [];
582
+ this._cursor = 0;
583
+ return;
584
+ }
585
+ const qLow = q.toLowerCase();
586
+ const sources = this._getSearchSources();
587
+ const start = fromIdx !== undefined ? fromIdx : 0;
588
+ for (let i = start; i < sources.length; i++) {
589
+ if (sources[i].text.toLowerCase().includes(qLow)) {
590
+ this._searchMatchIdx = i;
591
+ this._searchMatchType = sources[i].type;
592
+ this._abbreviatedRanges = [];
593
+ this._chars = Array.from(sources[i].text);
594
+ this._cursor = this._chars.length;
595
+ return;
596
+ }
597
+ }
598
+ // no match — keep query visible
599
+ this._searchMatchIdx = -1;
600
+ this._searchMatchType = null;
601
+ this._chars = Array.from(q);
602
+ this._cursor = this._chars.length;
603
+ }
604
+
605
+ _searchNext() {
606
+ const nextIdx = this._searchMatchIdx >= 0 ? this._searchMatchIdx + 1 : 0;
607
+ this._doSearch(nextIdx);
608
+ this._render();
609
+ }
610
+
611
+ // ── Tab autocomplete ─────────────────────────────────────────────────────────
612
+
613
+ _handleTab() {
614
+ const val = this.getValue();
615
+
616
+ if (val.startsWith('/file ')) {
617
+ const partial = val.slice(6);
618
+ if (!this._tabMatches.length) {
619
+ this._tabMatches = this._getFileMatches(partial);
620
+ this._tabCycleIdx = 0;
621
+ } else {
622
+ this._tabCycleIdx = (this._tabCycleIdx + 1) % this._tabMatches.length;
623
+ }
624
+ if (!this._tabMatches.length) { this._render(); return; }
625
+ const match = this._tabMatches[this._tabCycleIdx];
626
+ this._setValue('/file ' + match);
627
+ this._hint = this._tabMatches.length > 1
628
+ ? ` [${this._tabCycleIdx + 1}/${this._tabMatches.length}]`
629
+ : '';
630
+ this._render();
631
+ return;
632
+ }
633
+
634
+ if (!val.startsWith('/')) return;
635
+ const matches = SLASH_CMDS.filter(c => c.startsWith(val));
636
+ if (!matches.length) return;
637
+ if (matches.length === 1) {
638
+ this._setValue(matches[0] + ' ');
639
+ this._clearHint();
640
+ } else {
641
+ if (!this._tabMatches.length) { this._tabMatches = matches; this._tabCycleIdx = 0; }
642
+ else { this._tabCycleIdx = (this._tabCycleIdx + 1) % this._tabMatches.length; }
643
+ this._hint = this._tabMatches[this._tabCycleIdx].slice(val.length);
644
+ }
645
+ this._render();
646
+ }
647
+
648
+ // ── Key dispatch ─────────────────────────────────────────────────────────────
649
+
650
+ _handleKey(key) {
651
+ if (this._navCapture) {
652
+ if (this._navSearchMode) {
653
+ if (key === 'escape' || key === 'ctrl+g') {
654
+ this._navSearchMode = false;
655
+ this._navSearchQuery = '';
656
+ this._navCapture('search:');
657
+ this._renderHints();
658
+ } else if (key === 'backspace') {
659
+ this._navSearchQuery = this._navSearchQuery.slice(0, -1);
660
+ this._navCapture('search:' + this._navSearchQuery);
661
+ this._renderHints();
662
+ } else if (key === 'ctrl+u') {
663
+ this._navSearchQuery = '';
664
+ this._navCapture('search:');
665
+ this._renderHints();
666
+ } else if (key === 'enter') {
667
+ this._navSearchMode = false;
668
+ this._navCapture('select');
669
+ this._renderHints();
670
+ } else if (key === 'up' || key === 'ctrl+p') {
671
+ this._navCapture('prev');
672
+ } else if (key === 'down' || key === 'ctrl+n') {
673
+ this._navCapture('next');
674
+ } else if (key === 'ctrl+c' || key === 'ctrl+d') {
675
+ this._navSearchMode = false;
676
+ this._navSearchQuery = '';
677
+ this._navCapture('cancel');
678
+ }
679
+ } else {
680
+ if (key === 'up' || key === 'ctrl+p') this._navCapture('prev');
681
+ else if (key === 'down' || key === 'ctrl+n') this._navCapture('next');
682
+ else if (key === 'enter') this._navCapture('select');
683
+ else if (key === 'escape' || key === 'ctrl+c' || key === 'ctrl+d') this._navCapture('cancel');
684
+ else if (key === 'ctrl+r') {
685
+ this._navSearchMode = true;
686
+ this._navSearchQuery = '';
687
+ this._navCapture('search:');
688
+ this._renderHints();
689
+ }
690
+ }
691
+ return;
692
+ }
693
+
694
+ if (this._selectCapture) {
695
+ const { menu, resolve } = this._selectCapture;
696
+ if (key === 'up' || key === 'ctrl+p') { menu.moveUp(); this._chatHistory.invalidateCache(); }
697
+ else if (key === 'down' || key === 'ctrl+n') { menu.moveDown(); this._chatHistory.invalidateCache(); }
698
+ else if (key === 'enter') { this._selectCapture = null; resolve(menu.current()); }
699
+ else if (key === 'ctrl+c' || key === 'ctrl+d' || key === 'escape') {
700
+ this._selectCapture = null;
701
+ resolve(null);
702
+ }
703
+ return;
704
+ }
705
+
706
+ // ── History search mode ──────────────────────────────────────────────────
707
+ if (this._searchMode) {
708
+ switch (key) {
709
+ case 'ctrl+r':
710
+ this._searchNext();
711
+ return;
712
+ case 'escape':
713
+ case 'ctrl+g':
714
+ this._exitSearchMode(false);
715
+ return;
716
+ case 'enter': {
717
+ const accepted = this.getValue();
718
+ this._exitSearchMode(true);
719
+ if (accepted) {
720
+ this._historyIdx = -1;
721
+ this._draft = '';
722
+ this._render();
723
+ this._history.unshift(accepted);
724
+ if (this._history.length > 100) this._history.pop();
725
+ this.emit('submit', accepted);
726
+ }
727
+ return;
728
+ }
729
+ case 'tab': {
730
+ this._exitSearchMode(true);
731
+ return;
732
+ }
733
+ case 'backspace':
734
+ if (this._searchQuery.length > 0) {
735
+ this._searchQuery = this._searchQuery.slice(0, -1);
736
+ this._searchMatchIdx = -1;
737
+ this._doSearch(0);
738
+ this._render();
739
+ }
740
+ return;
741
+ case 'ctrl+u':
742
+ this._searchQuery = '';
743
+ this._searchMatchIdx = -1;
744
+ this._chars = [];
745
+ this._cursor = 0;
746
+ this._render();
747
+ return;
748
+ default:
749
+ return;
750
+ }
751
+ }
752
+
753
+ if (key !== 'tab') this._clearHint();
754
+
755
+ switch (key) {
756
+ case 'shift+enter': this._insertChar('\n'); this._render(); break;
757
+ case 'enter': {
758
+ const val = this.getValue().trim();
759
+ this._chars = []; this._cursor = 0;
760
+ this._abbreviatedRanges = [];
761
+ this._pasteCounter = 0;
762
+ this._historyIdx = -1; this._draft = '';
763
+ this._clearHint();
764
+ this._render();
765
+ if (val) {
766
+ this._history.unshift(val);
767
+ if (this._history.length > 100) this._history.pop();
768
+ this.emit('submit', val);
769
+ }
770
+ break;
771
+ }
772
+ case 'backspace': this._backspace(); this._render(); break;
773
+ case 'delete': this._deleteForward(); this._render(); break;
774
+ case 'tab': this._handleTab(); break;
775
+ case 'up':
776
+ case 'ctrl+p': this._histPrev(); break;
777
+ case 'down':
778
+ case 'ctrl+n': this._histNext(); break;
779
+ case 'left':
780
+ case 'ctrl+b': this._moveCursorLeft(); this._render(); break;
781
+ case 'right':
782
+ case 'ctrl+f': this._moveCursorRight(); this._render(); break;
783
+ case 'home':
784
+ case 'ctrl+a': this._moveToStart(); this._render(); break;
785
+ case 'end':
786
+ case 'ctrl+e': this._moveToEnd(); this._render(); break;
787
+ case 'ctrl+k': this._deleteToEnd(); this._render(); break;
788
+ case 'ctrl+u': this._deleteLine(); this._render(); break;
789
+ case 'ctrl+w': this._deleteWordBefore(); this._render(); break;
790
+ case 'ctrl+t': this._transposeChars(); this._render(); break;
791
+ case 'ctrl+l': this._chatHistory.clearMessages(); break;
792
+ case 'ctrl+r': this._enterSearchMode(); break;
793
+ case 'ctrl+o': if (this._navCapture) this._navCapture('expand'); else this.emit('expand'); break;
794
+ case 'ctrl+g':
795
+ case 'ctrl+c':
796
+ case 'ctrl+d': this.emit('interrupt'); break;
797
+ case 'ctrl+right':
798
+ case 'alt+f': this._jumpWordForward(); this._render(); break;
799
+ case 'ctrl+left':
800
+ case 'alt+b': this._jumpWordBackward(); this._render(); break;
801
+ case 'pageup':
802
+ case 'shift+up': this._chatHistory.scrollUp(Math.max(1, this._layout.historyRows - 2)); break;
803
+ case 'pagedown':
804
+ case 'shift+down': this._chatHistory.scrollDown(Math.max(1, this._layout.historyRows - 2)); break;
805
+ case 'paste-start':
806
+ this._pasteMode = true;
807
+ this._pasteStartIdx = this._cursor;
808
+ break;
809
+ case 'paste-end':
810
+ this._syntheticPaste = false;
811
+ this._pasteMode = false;
812
+ this._checkAndAbbreviatePaste();
813
+ this._render();
814
+ break;
815
+ case 'escape': this._render(); break;
816
+ }
817
+ }
818
+
819
+ // ── Raw stdin byte handler ───────────────────────────────────────────────────
820
+
821
+ _handleData(chunk) {
822
+ let buf;
823
+ const incoming = Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk);
824
+ if (this._escBuf) {
825
+ buf = Buffer.concat([this._escBuf, incoming]);
826
+ this._escBuf = null;
827
+ if (this._escTimer) { clearTimeout(this._escTimer); this._escTimer = null; }
828
+ } else {
829
+ buf = incoming;
830
+ }
831
+ if (this._utf8Buf) {
832
+ buf = Buffer.concat([this._utf8Buf, buf]);
833
+ this._utf8Buf = null;
834
+ }
835
+
836
+ if (this._selectCapture) { this._processBuf(buf, false); return; }
837
+ if (this._navCapture) { this._processBuf(buf, false); return; }
838
+ if (this._disabled) {
839
+ if (buf[0] === 0x03 || buf[0] === 0x04) {
840
+ this.emit('interrupt');
841
+ } else if (buf[0] === 0x0f) {
842
+ this.emit('expand');
843
+ } else if (buf[0] === 0x1b && buf.length === 1) {
844
+ // Bare ESC while agent is running: buffer and confirm after 20ms so that
845
+ // escape sequences (arrow keys, etc.) arriving as separate bytes are ignored.
846
+ this._escBuf = buf;
847
+ this._escTimer = setTimeout(() => {
848
+ const pending = this._escBuf;
849
+ this._escBuf = null; this._escTimer = null;
850
+ if (pending && pending.length === 1 && this._disabled) this.emit('abort');
851
+ }, 20);
852
+ }
853
+ return;
854
+ }
855
+ this._goActive();
856
+ this._processBuf(buf, false);
857
+ }
858
+
859
+ _processBuf(buf, force) {
860
+ // Detect unbracketed paste: buffer has a newline with content on both sides.
861
+ // Terminals that don't send \x1b[200~/\x1b[201~ would otherwise fire Enter on
862
+ // each newline inside pasted text, submitting only the first line.
863
+ if (!this._pasteMode && !force && !this._syntheticPaste &&
864
+ !this._selectCapture && !this._navCapture && !this._searchMode &&
865
+ buf[0] !== 0x1b) {
866
+ const crIdx = buf.indexOf(0x0d);
867
+ const nlIdx = buf.indexOf(0x0a);
868
+ const nlPos = crIdx === -1 ? nlIdx : (nlIdx === -1 ? crIdx : Math.min(crIdx, nlIdx));
869
+ if (nlPos > 0 && nlPos < buf.length - 1) {
870
+ this._pasteMode = true;
871
+ this._pasteStartIdx = this._cursor;
872
+ this._syntheticPaste = true;
873
+ }
874
+ }
875
+
876
+ let i = 0;
877
+ while (i < buf.length) {
878
+ const b = buf[i];
879
+
880
+ if (b === 0x1b) {
881
+ const result = parseKeySequence(buf.slice(i));
882
+ if (result && result.incomplete && !force) {
883
+ this._escBuf = buf.slice(i);
884
+ this._escTimer = setTimeout(() => {
885
+ const pending = this._escBuf;
886
+ this._escBuf = null; this._escTimer = null;
887
+ if (!pending) return;
888
+ if (pending.length === 1) { this._handleKey('escape'); this._render(); }
889
+ else this._processBuf(pending, true);
890
+ }, 20);
891
+ return;
892
+ }
893
+ if (result && result.key && result.key !== 'unknown') this._handleKey(result.key);
894
+ i += (result && result.len > 0) ? result.len : 1;
895
+ continue;
896
+ }
897
+
898
+ if (b < 0x20 || b === 0x7f) {
899
+ if (this._pasteMode && (b === 0x0d || b === 0x0a)) {
900
+ if (!(b === 0x0a && i > 0 && buf[i - 1] === 0x0d)) {
901
+ this._insertChar('\n');
902
+ this._clearHint();
903
+ // render deferred to paste-end
904
+ }
905
+ } else {
906
+ const result = parseKeySequence(buf.slice(i, i + 1));
907
+ if (result && result.key) this._handleKey(result.key);
908
+ }
909
+ i++;
910
+ continue;
911
+ }
912
+
913
+ // Printable char — in nav capture mode, dispatch n/p/s/c or append to search query
914
+ if (this._navCapture) {
915
+ const _b0nc = buf[i];
916
+ if (_b0nc >= 0xC0 && _b0nc < 0xFE) {
917
+ const _el = _b0nc >= 0xF0 ? 4 : _b0nc >= 0xE0 ? 3 : 2;
918
+ if (buf.length - i < _el) { this._utf8Buf = buf.slice(i); return; }
919
+ }
920
+ try {
921
+ const str = buf.slice(i).toString('utf8');
922
+ const cp = str.codePointAt(0);
923
+ if (cp !== undefined && cp >= 0x20) {
924
+ const ch = String.fromCodePoint(cp);
925
+ const byteLen = Buffer.byteLength(ch, 'utf8');
926
+ if (this._navSearchMode) {
927
+ this._navSearchQuery += ch;
928
+ this._navCapture('search:' + this._navSearchQuery);
929
+ this._renderHints();
930
+ } else {
931
+ const chLower = ch.toLowerCase();
932
+ if (chLower === 'n') this._navCapture('next');
933
+ else if (chLower === 'p') this._navCapture('prev');
934
+ else if (chLower === 's' || chLower === 'y') this._navCapture('select');
935
+ else if (chLower === 'c') this._navCapture('cancel');
936
+ }
937
+ i += byteLen;
938
+ } else { i++; }
939
+ } catch { i++; }
940
+ continue;
941
+ }
942
+
943
+ // Printable char — in search mode, append to query
944
+ if (this._searchMode) {
945
+ const _b0sm = buf[i];
946
+ if (_b0sm >= 0xC0 && _b0sm < 0xFE) {
947
+ const _el = _b0sm >= 0xF0 ? 4 : _b0sm >= 0xE0 ? 3 : 2;
948
+ if (buf.length - i < _el) { this._utf8Buf = buf.slice(i); return; }
949
+ }
950
+ try {
951
+ const str = buf.slice(i).toString('utf8');
952
+ const cp = str.codePointAt(0);
953
+ if (cp !== undefined && cp >= 0x20) {
954
+ const ch = String.fromCodePoint(cp);
955
+ this._searchQuery += ch;
956
+ this._searchMatchIdx = -1;
957
+ this._doSearch(0);
958
+ this._render();
959
+ i += Buffer.byteLength(ch, 'utf8');
960
+ } else { i++; }
961
+ } catch { i++; }
962
+ continue;
963
+ }
964
+
965
+ {
966
+ const _b0 = buf[i];
967
+ if (_b0 >= 0xC0 && _b0 < 0xFE) {
968
+ const _el = _b0 >= 0xF0 ? 4 : _b0 >= 0xE0 ? 3 : 2;
969
+ if (buf.length - i < _el) { this._utf8Buf = buf.slice(i); return; }
970
+ }
971
+ try {
972
+ const str = buf.slice(i).toString('utf8');
973
+ const cp = str.codePointAt(0);
974
+ if (cp !== undefined && cp >= 0x20) {
975
+ const ch = String.fromCodePoint(cp);
976
+ this._insertChar(ch);
977
+ if (!this._pasteMode) {
978
+ this._clearHint();
979
+ this._render();
980
+ }
981
+ i += Buffer.byteLength(ch, 'utf8');
982
+ } else { i++; }
983
+ } catch { i++; }
984
+ }
985
+ }
986
+
987
+ if (this._syntheticPaste) {
988
+ this._syntheticPaste = false;
989
+ if (this._pasteMode) {
990
+ this._pasteMode = false;
991
+ this._checkAndAbbreviatePaste();
992
+ this._render();
993
+ }
994
+ }
995
+ }
996
+
997
+ // ── Hints row ────────────────────────────────────────────────────────────────
998
+
999
+ _renderHints() {
1000
+ const layout = this._layout;
1001
+ if (!layout || !layout.hintsRow) return;
1002
+ const row = layout.hintsRow;
1003
+ const cols = layout.cols;
1004
+
1005
+ let text;
1006
+ if (this._searchMode) {
1007
+ text = `${FG_YELLOW}ctrl+r${RST}${DIM} next match ${RST}${FG_YELLOW}esc${RST}${DIM} cancel search${RST}`;
1008
+ } else if (this._disabled) {
1009
+ text = `${FG_YELLOW}esc${RST}${DIM} interrupt${RST}`;
1010
+ } else if (this._navCapture) {
1011
+ if (this._navSearchMode) {
1012
+ text = `${FG_YELLOW}search:${RST} ${FG_CYAN}'${this._navSearchQuery}'${RST} ${DIM}↑↓ navigate enter select esc clear${RST}`;
1013
+ } else {
1014
+ text = `${FG_CYAN}↑↓${RST}${DIM}/${RST}${FG_CYAN}n/p${RST}${DIM} navigate ${RST}${FG_CYAN}s${RST}${DIM}/${RST}${FG_CYAN}enter${RST}${DIM} select ${RST}${FG_CYAN}ctrl+r${RST}${DIM} search ${RST}${FG_CYAN}c${RST}${DIM}/${RST}${FG_CYAN}esc${RST}${DIM} cancel${RST}`;
1015
+ }
1016
+ } else {
1017
+ text = `${DIM}↑↓ history ${RST}${FG_CYAN}ctrl+r${RST}${DIM} search ${RST}${FG_CYAN}ctrl+c${RST}${DIM} cancel${RST}`;
1018
+ }
1019
+
1020
+ // Use DECSC/DECRC so the cursor is restored after drawing (keeps streaming
1021
+ // tokens anchored at historyRows when the blink timer calls this).
1022
+ process.stdout.write(`\x1b7\x1b[${row};1H\x1b[2K${text}`);
1023
+ const visLen = text.replace(/\x1b\[[^m]*m/g, '').length;
1024
+ if (visLen < cols) process.stdout.write(' '.repeat(cols - visLen));
1025
+ process.stdout.write('\x1b8');
1026
+ }
1027
+
1028
+ // ── Render ───────────────────────────────────────────────────────────────────
1029
+
1030
+ _render() {
1031
+ const layout = this._layout;
1032
+ const cols = layout.cols;
1033
+
1034
+ if (this._disabled) {
1035
+ if (this._inputHeight !== 1) {
1036
+ this._inputHeight = 1;
1037
+ layout.setInputHeight(1);
1038
+ }
1039
+ const baseRow = layout.inputRow;
1040
+ // DECSC/DECRC keeps the cursor in the history scroll region so streaming
1041
+ // tokens written after this blink-timer redraw land at the right row.
1042
+ process.stdout.write(`\x1b7\x1b[${baseRow};1H\x1b[2K${DIM}›${RST}`);
1043
+ process.stdout.write(' '.repeat(Math.max(0, cols - 1)));
1044
+ process.stdout.write('\x1b8');
1045
+ return;
1046
+ }
1047
+
1048
+ if (this._navCapture) {
1049
+ if (this._inputHeight !== 1) {
1050
+ this._inputHeight = 1;
1051
+ layout.setInputHeight(1);
1052
+ }
1053
+ const baseRow = layout.inputRow;
1054
+ process.stdout.write(`\x1b[${baseRow};1H\x1b[2K${DIM}›${RST}`);
1055
+ process.stdout.write(' '.repeat(Math.max(0, cols - 1)));
1056
+ return;
1057
+ }
1058
+
1059
+ // ── Search mode rendering ────────────────────────────────────────────────
1060
+ if (this._searchMode) {
1061
+ if (this._inputHeight !== 1) {
1062
+ this._inputHeight = 1;
1063
+ layout.setInputHeight(1);
1064
+ }
1065
+ const baseRow = layout.inputRow;
1066
+ const noMatch = this._searchQuery && this._searchMatchIdx === -1;
1067
+ const queryColor = noMatch ? '\x1b[38;5;203m' : FG_CYAN;
1068
+ const prefix = `${DIM}(search)${RST} ${queryColor}'${this._searchQuery}'${RST}${DIM}:${RST} `;
1069
+ const matched = this._chars.join('');
1070
+
1071
+ // Type label
1072
+ const typeLabel = this._searchMatchType
1073
+ ? `${DIM}[${this._searchMatchType}]${RST} `
1074
+ : '';
1075
+
1076
+ // Highlight the matching part (case-insensitive)
1077
+ let displayMatch;
1078
+ const matchedLow = matched.toLowerCase();
1079
+ const qLow = this._searchQuery.toLowerCase();
1080
+ if (this._searchQuery && matchedLow.includes(qLow)) {
1081
+ const idx = matchedLow.indexOf(qLow);
1082
+ const before = matched.slice(0, idx);
1083
+ const hit = matched.slice(idx, idx + this._searchQuery.length);
1084
+ const after = matched.slice(idx + this._searchQuery.length);
1085
+ displayMatch = `${before}${FG_GREEN}${hit}${RST}${after}`;
1086
+ } else {
1087
+ displayMatch = matched;
1088
+ }
1089
+
1090
+ const line = prefix + typeLabel + displayMatch;
1091
+ process.stdout.write(`\x1b[${baseRow};1H\x1b[2K${line}`);
1092
+ const visLen = line.replace(/\x1b\[[^m]*m/g, '').length;
1093
+ if (visLen < cols) process.stdout.write(' '.repeat(cols - visLen));
1094
+ return;
1095
+ }
1096
+
1097
+ // ── Normal rendering ─────────────────────────────────────────────────────
1098
+ const { chars: displayChars, cursor: displayCursor } = this._getDisplayContent();
1099
+
1100
+ const newlineCount = displayChars.filter(c => c === '\n').length;
1101
+ const needed = Math.min(5, newlineCount + 1);
1102
+ if (needed !== this._inputHeight) {
1103
+ this._inputHeight = needed;
1104
+ layout.setInputHeight(needed);
1105
+ }
1106
+
1107
+ const baseRow = layout.inputRow;
1108
+
1109
+ for (let r = baseRow; r < baseRow + this._inputHeight; r++) {
1110
+ process.stdout.write(`\x1b[${r};1H\x1b[2K`);
1111
+ }
1112
+
1113
+ const PREFIX = '\x1b[36m › \x1b[0m';
1114
+ const PREFIX_LEN = 3;
1115
+
1116
+ const lines = [[]];
1117
+ for (const ch of displayChars) {
1118
+ if (ch === '\n') lines.push([]);
1119
+ else lines[lines.length - 1].push(ch);
1120
+ }
1121
+
1122
+ let cursorLine = 0, cursorCol = 0, pos = 0;
1123
+ for (let li = 0; li < lines.length; li++) {
1124
+ if (displayCursor <= pos + lines[li].length) {
1125
+ cursorLine = li;
1126
+ cursorCol = displayCursor - pos;
1127
+ break;
1128
+ }
1129
+ pos += lines[li].length + 1;
1130
+ }
1131
+
1132
+ const renderCount = Math.min(lines.length, this._inputHeight);
1133
+ for (let li = 0; li < renderCount; li++) {
1134
+ const r = baseRow + li;
1135
+ const lineChars = lines[li];
1136
+
1137
+ let lineStr;
1138
+ if (li === cursorLine) {
1139
+ const leftStr = lineChars.slice(0, cursorCol).join('');
1140
+ const cursorCh = cursorCol < lineChars.length ? lineChars[cursorCol] : ' ';
1141
+ const afterStr = lineChars.slice(cursorCol + (cursorCol < lineChars.length ? 1 : 0)).join('');
1142
+ const cursorEsc = this._blink
1143
+ ? `\x1b[7m${cursorCh}\x1b[27m`
1144
+ : `\x1b[4m${cursorCh}\x1b[24m`;
1145
+ lineStr = leftStr + cursorEsc + afterStr;
1146
+ } else {
1147
+ lineStr = lineChars.join('');
1148
+ }
1149
+
1150
+ const leadIn = li === 0 ? PREFIX : ' '.repeat(PREFIX_LEN);
1151
+ const hintStr = (li === 0 && this._hint) ? `${DIM}${this._hint}${RST}` : '';
1152
+ const visLen = PREFIX_LEN + lineChars.length + (li === 0 ? this._hint.length : 0);
1153
+ const pad = visLen < cols ? ' '.repeat(cols - visLen) : '';
1154
+
1155
+ process.stdout.write(`\x1b[${r};1H` + leadIn + lineStr + hintStr + pad);
1156
+ }
1157
+ }
1158
+
1159
+ // ── Destroy ──────────────────────────────────────────────────────────────────
1160
+
1161
+ destroy() {
1162
+ if (this._blinkTimer) { clearInterval(this._blinkTimer); this._blinkTimer = null; }
1163
+ if (this._escTimer) { clearTimeout(this._escTimer); this._escTimer = null; }
1164
+ if (this._idleTimer) { clearTimeout(this._idleTimer); this._idleTimer = null; }
1165
+ if (process.stdin.isTTY) {
1166
+ process.stdin.removeListener('data', this._onData);
1167
+ this._cursorHidden = true; this.showCursor(); // restore cursor visibility
1168
+ process.stdout.write('\x1b[<u'); // pop kitty keyboard protocol
1169
+ process.stdout.write('\x1b[>4;0m'); // reset modifyOtherKeys
1170
+ process.stdout.write('\x1b[?2004l'); // disable bracketed paste
1171
+ process.stdin.setRawMode(false); // restore cooked mode so mouse works again
1172
+ }
1173
+ }
1174
+ }
1175
+
1176
+ module.exports = { InputField, parseKeySequence, SLASH_CMDS };