zames_pro 2.2.1 → 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 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+J — новая строка, Esc — стоп');
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
- const start = i;
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
- this._up();
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._down();
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++;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "zames_pro",
3
- "version": "2.2.1",
3
+ "version": "2.3.0",
4
4
  "description": "Terminal coding agent over chat.deepseek.com via Playwright",
5
5
  "type": "module",
6
6
  "bin": {