zames_pro 2.2.0 → 2.3.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.
- package/dist/index.js +3 -0
- package/dist/input.js +159 -5
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -181,6 +181,9 @@ ${theme.bold('Опции CLI:')}
|
|
|
181
181
|
|
|
182
182
|
${theme.bold('Пока агент работает:')}
|
|
183
183
|
печать + Enter поставить сообщение в очередь (уйдёт после текущей задачи)
|
|
184
|
+
↑ / ↓ история введённых сообщений
|
|
185
|
+
Ctrl+← / Ctrl+→ перемещение по словам
|
|
186
|
+
Ctrl+J новая строка (Shift+Enter в терминалах с поддержкой)
|
|
184
187
|
Esc, Ctrl+C прервать текущую генерацию
|
|
185
188
|
|
|
186
189
|
${theme.bold('Обычные команды:')}
|
package/dist/input.js
CHANGED
|
@@ -9,7 +9,12 @@ import { randomThinkingPhrase, stripEllipsis } from './spinner.js';
|
|
|
9
9
|
// * перерисовка учитывает перенос по ширине терминала, поэтому нет
|
|
10
10
|
// дублирования строк при многострочном вводе;
|
|
11
11
|
// * многострочный ввод: Enter отправляет, Ctrl+J (и Shift+Enter в
|
|
12
|
-
// терминалах с расширенным протоколом) вставляют перевод
|
|
12
|
+
// терминалах с расширенным протоколом) вставляют перевод строки;
|
|
13
|
+
// * перенос длинных строк по словам (слово не рвётся посередине);
|
|
14
|
+
// * перемещение по словам: Ctrl+←/→ (и Alt+B/Alt+F), удаление слова
|
|
15
|
+
// Ctrl+W; Ctrl+K — удалить до конца строки;
|
|
16
|
+
// * история введённых сообщений: ↑/↓ на краях ввода (внутри многострочного
|
|
17
|
+
// ввода стрелки двигают по строкам, как в обычном терминале).
|
|
13
18
|
//
|
|
14
19
|
// Все управляющие символы собираются из кодов, чтобы в файле не было
|
|
15
20
|
// «сырых» ESC/CR/LF в строковых литералах (см. AGENTS.md).
|
|
@@ -22,7 +27,7 @@ const PASTE_END = ESC + '[201~';
|
|
|
22
27
|
// Ширину выравниваем по максимуму (3), чтобы подсказка не смещалась.
|
|
23
28
|
const DOTS = ['', '.', '..', '...'];
|
|
24
29
|
const DOTS_PAD = ' ';
|
|
25
|
-
const HINT = theme.dim(' · Enter — отправить, Ctrl
|
|
30
|
+
const HINT = theme.dim(' · Enter — отправить, ↑/↓ — история, Ctrl+←/→ — по словам, Esc — стоп');
|
|
26
31
|
function safeJson(v) {
|
|
27
32
|
try {
|
|
28
33
|
return JSON.stringify(v);
|
|
@@ -64,14 +69,29 @@ export function layoutInput(promptStr, buf, cursor, cols) {
|
|
|
64
69
|
const first = rows.length === 0;
|
|
65
70
|
const prefix = first ? promptStr : pad;
|
|
66
71
|
const avail = Math.max(1, width - promptW);
|
|
72
|
+
const start = i;
|
|
73
|
+
// Собираем очередную визуальную строку. Сначала набираем столько
|
|
74
|
+
// символов, сколько влезает (count < avail). Если упёрлись в ширину
|
|
75
|
+
// и следующий символ — не конец строки, откатываемся к последнему
|
|
76
|
+
// пробелу, чтобы не разрывать слово посередине (word-wrap).
|
|
67
77
|
let text = '';
|
|
68
78
|
let count = 0;
|
|
69
|
-
|
|
79
|
+
let lastSpace = -1; // индекс пробела в text (по Array.from)
|
|
70
80
|
while (i < chars.length && chars[i] !== NL && count < avail) {
|
|
81
|
+
if (chars[i] === ' ')
|
|
82
|
+
lastSpace = count;
|
|
71
83
|
text += chars[i];
|
|
72
84
|
i++;
|
|
73
85
|
count++;
|
|
74
86
|
}
|
|
87
|
+
const atLineEnd = i >= chars.length || chars[i] === NL;
|
|
88
|
+
if (!atLineEnd && count >= avail && lastSpace > 0) {
|
|
89
|
+
// Переносим «хвост» строки на следующую визуальную строку.
|
|
90
|
+
const textChars = Array.from(text);
|
|
91
|
+
const tailLen = textChars.length - lastSpace;
|
|
92
|
+
i -= tailLen;
|
|
93
|
+
text = textChars.slice(0, lastSpace).join('');
|
|
94
|
+
}
|
|
75
95
|
if (i < chars.length && chars[i] === NL) {
|
|
76
96
|
i++;
|
|
77
97
|
rows.push({ prefix, text, start });
|
|
@@ -117,6 +137,10 @@ export class LineEditor {
|
|
|
117
137
|
_thinkBase;
|
|
118
138
|
_wasRaw;
|
|
119
139
|
_onData;
|
|
140
|
+
// История отправленных сообщений (для стрелок вверх/вниз).
|
|
141
|
+
history;
|
|
142
|
+
_histIndex;
|
|
143
|
+
_histDraft;
|
|
120
144
|
constructor({ prompt = '> ' } = {}) {
|
|
121
145
|
this.promptStr = prompt;
|
|
122
146
|
this.buf = '';
|
|
@@ -135,6 +159,9 @@ export class LineEditor {
|
|
|
135
159
|
this._thinkBase = '';
|
|
136
160
|
this._wasRaw = false;
|
|
137
161
|
this._onData = (b) => this._handle(b);
|
|
162
|
+
this.history = [];
|
|
163
|
+
this._histIndex = 0;
|
|
164
|
+
this._histDraft = '';
|
|
138
165
|
}
|
|
139
166
|
start() {
|
|
140
167
|
const stdin = process.stdin;
|
|
@@ -278,6 +305,12 @@ export class LineEditor {
|
|
|
278
305
|
this._render();
|
|
279
306
|
return;
|
|
280
307
|
}
|
|
308
|
+
// В историю — только непустые и не дублирующие прошлое сообщение.
|
|
309
|
+
if (text.trim() && this.history[this.history.length - 1] !== text) {
|
|
310
|
+
this.history.push(text);
|
|
311
|
+
}
|
|
312
|
+
this._histIndex = this.history.length;
|
|
313
|
+
this._histDraft = '';
|
|
281
314
|
this.buf = '';
|
|
282
315
|
this.cursor = 0;
|
|
283
316
|
this.pendingText = null;
|
|
@@ -331,6 +364,19 @@ export class LineEditor {
|
|
|
331
364
|
i++;
|
|
332
365
|
this.cursor = i;
|
|
333
366
|
}
|
|
367
|
+
// Курсор на первой/последней ВИЗУАЛЬНОЙ строке (с учётом переноса).
|
|
368
|
+
// Нужно, чтобы Up/Down работали как история на краях ввода, а внутри —
|
|
369
|
+
// как перемещение по строкам, как в обычном терминале.
|
|
370
|
+
_onFirstVisualLine() {
|
|
371
|
+
const cols = process.stdout.columns || 80;
|
|
372
|
+
const lay = layoutInput(this.promptStr, this.buf, this.cursor, cols);
|
|
373
|
+
return lay.cursorRow === 0;
|
|
374
|
+
}
|
|
375
|
+
_onLastVisualLine() {
|
|
376
|
+
const cols = process.stdout.columns || 80;
|
|
377
|
+
const lay = layoutInput(this.promptStr, this.buf, this.cursor, cols);
|
|
378
|
+
return lay.cursorRow === lay.rows.length - 1;
|
|
379
|
+
}
|
|
334
380
|
_up() {
|
|
335
381
|
const chars = Array.from(this.buf);
|
|
336
382
|
let start = this.cursor;
|
|
@@ -362,6 +408,64 @@ export class LineEditor {
|
|
|
362
408
|
nextEnd++;
|
|
363
409
|
this.cursor = nextStart + Math.min(col, nextEnd - nextStart);
|
|
364
410
|
}
|
|
411
|
+
// Перемещение на слово назад (Ctrl+Left / Alt+B): пропускаем пробелы
|
|
412
|
+
// слева, затем идём до начала слова.
|
|
413
|
+
_wordLeft() {
|
|
414
|
+
const chars = Array.from(this.buf);
|
|
415
|
+
let i = this.cursor;
|
|
416
|
+
while (i > 0 && /\s/.test(chars[i - 1]))
|
|
417
|
+
i--;
|
|
418
|
+
while (i > 0 && !/\s/.test(chars[i - 1]))
|
|
419
|
+
i--;
|
|
420
|
+
this.cursor = i;
|
|
421
|
+
}
|
|
422
|
+
// Перемещение на слово вперёд (Ctrl+Right / Alt+F).
|
|
423
|
+
_wordRight() {
|
|
424
|
+
const chars = Array.from(this.buf);
|
|
425
|
+
let i = this.cursor;
|
|
426
|
+
while (i < chars.length && /\s/.test(chars[i]))
|
|
427
|
+
i++;
|
|
428
|
+
while (i < chars.length && !/\s/.test(chars[i]))
|
|
429
|
+
i++;
|
|
430
|
+
this.cursor = i;
|
|
431
|
+
}
|
|
432
|
+
// Удаление слова слева (Ctrl+W / Alt+Backspace).
|
|
433
|
+
_deleteWordLeft() {
|
|
434
|
+
const chars = Array.from(this.buf);
|
|
435
|
+
let i = this.cursor;
|
|
436
|
+
while (i > 0 && /\s/.test(chars[i - 1]))
|
|
437
|
+
i--;
|
|
438
|
+
while (i > 0 && !/\s/.test(chars[i - 1]))
|
|
439
|
+
i--;
|
|
440
|
+
chars.splice(i, this.cursor - i);
|
|
441
|
+
this.buf = chars.join('');
|
|
442
|
+
this.cursor = i;
|
|
443
|
+
}
|
|
444
|
+
// ---------- история ----------
|
|
445
|
+
_historyUp() {
|
|
446
|
+
if (!this.history.length)
|
|
447
|
+
return;
|
|
448
|
+
if (this._histIndex === this.history.length)
|
|
449
|
+
this._histDraft = this.buf;
|
|
450
|
+
if (this._histIndex > 0) {
|
|
451
|
+
this._histIndex--;
|
|
452
|
+
this.buf = this.history[this._histIndex];
|
|
453
|
+
this.cursor = Array.from(this.buf).length;
|
|
454
|
+
}
|
|
455
|
+
}
|
|
456
|
+
_historyDown() {
|
|
457
|
+
if (!this.history.length)
|
|
458
|
+
return;
|
|
459
|
+
if (this._histIndex < this.history.length - 1) {
|
|
460
|
+
this._histIndex++;
|
|
461
|
+
this.buf = this.history[this._histIndex];
|
|
462
|
+
}
|
|
463
|
+
else {
|
|
464
|
+
this._histIndex = this.history.length;
|
|
465
|
+
this.buf = this._histDraft;
|
|
466
|
+
}
|
|
467
|
+
this.cursor = Array.from(this.buf).length;
|
|
468
|
+
}
|
|
365
469
|
_handle(data) {
|
|
366
470
|
let s = data.toString('utf-8');
|
|
367
471
|
if (!this._inPaste && s === ESC) {
|
|
@@ -452,12 +556,42 @@ export class LineEditor {
|
|
|
452
556
|
this._render();
|
|
453
557
|
continue;
|
|
454
558
|
}
|
|
559
|
+
if (code === 23) {
|
|
560
|
+
this._deleteWordLeft();
|
|
561
|
+
this._render();
|
|
562
|
+
continue;
|
|
563
|
+
}
|
|
564
|
+
if (code === 11) {
|
|
565
|
+
// Ctrl+K — удалить от курсора до конца строки.
|
|
566
|
+
const arr = Array.from(this.buf);
|
|
567
|
+
let e = this.cursor;
|
|
568
|
+
while (e < arr.length && arr[e] !== NL)
|
|
569
|
+
e++;
|
|
570
|
+
arr.splice(this.cursor, e - this.cursor);
|
|
571
|
+
this.buf = arr.join('');
|
|
572
|
+
this._render();
|
|
573
|
+
continue;
|
|
574
|
+
}
|
|
455
575
|
if (code === 127 || code === 8) {
|
|
456
576
|
this._backspace();
|
|
457
577
|
this._render();
|
|
458
578
|
continue;
|
|
459
579
|
}
|
|
460
580
|
if (code === 27) {
|
|
581
|
+
// Ctrl+Left / Ctrl+Right (xterm: ESC [1;5D / ESC [1;5C;
|
|
582
|
+
// некоторые терминалы: ESC [5D / ESC [5C).
|
|
583
|
+
if (s.startsWith('[1;5D') || s.startsWith('[5D')) {
|
|
584
|
+
this._wordLeft();
|
|
585
|
+
s = s.slice(s.startsWith('[1;5D') ? 5 : 3);
|
|
586
|
+
this._render();
|
|
587
|
+
continue;
|
|
588
|
+
}
|
|
589
|
+
if (s.startsWith('[1;5C') || s.startsWith('[5C')) {
|
|
590
|
+
this._wordRight();
|
|
591
|
+
s = s.slice(s.startsWith('[1;5C') ? 5 : 3);
|
|
592
|
+
this._render();
|
|
593
|
+
continue;
|
|
594
|
+
}
|
|
461
595
|
if (s.startsWith('[D')) {
|
|
462
596
|
this._left();
|
|
463
597
|
s = s.slice(2);
|
|
@@ -471,13 +605,20 @@ export class LineEditor {
|
|
|
471
605
|
continue;
|
|
472
606
|
}
|
|
473
607
|
if (s.startsWith('[A')) {
|
|
474
|
-
|
|
608
|
+
// Вверх: на первой визуальной строке — история, иначе — строка выше.
|
|
609
|
+
if (this._onFirstVisualLine())
|
|
610
|
+
this._historyUp();
|
|
611
|
+
else
|
|
612
|
+
this._up();
|
|
475
613
|
s = s.slice(2);
|
|
476
614
|
this._render();
|
|
477
615
|
continue;
|
|
478
616
|
}
|
|
479
617
|
if (s.startsWith('[B')) {
|
|
480
|
-
this.
|
|
618
|
+
if (this._onLastVisualLine())
|
|
619
|
+
this._historyDown();
|
|
620
|
+
else
|
|
621
|
+
this._down();
|
|
481
622
|
s = s.slice(2);
|
|
482
623
|
this._render();
|
|
483
624
|
continue;
|
|
@@ -500,6 +641,19 @@ export class LineEditor {
|
|
|
500
641
|
this._render();
|
|
501
642
|
continue;
|
|
502
643
|
}
|
|
644
|
+
// Alt+B / Alt+F — перемещение по словам.
|
|
645
|
+
if (s.startsWith('b') || s.startsWith('B')) {
|
|
646
|
+
this._wordLeft();
|
|
647
|
+
s = s.slice(1);
|
|
648
|
+
this._render();
|
|
649
|
+
continue;
|
|
650
|
+
}
|
|
651
|
+
if (s.startsWith('f') || s.startsWith('F')) {
|
|
652
|
+
this._wordRight();
|
|
653
|
+
s = s.slice(1);
|
|
654
|
+
this._render();
|
|
655
|
+
continue;
|
|
656
|
+
}
|
|
503
657
|
let j = 0;
|
|
504
658
|
while (j < s.length && !/[A-Za-z~]/.test(s[j]))
|
|
505
659
|
j++;
|