zames_pro 2.2.1 → 2.4.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 +36 -1
- package/dist/input.js +227 -7
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -181,6 +181,10 @@ ${theme.bold('Опции CLI:')}
|
|
|
181
181
|
|
|
182
182
|
${theme.bold('Пока агент работает:')}
|
|
183
183
|
печать + Enter поставить сообщение в очередь (уйдёт после текущей задачи)
|
|
184
|
+
↑ / ↓ история введённых сообщений
|
|
185
|
+
Ctrl+← / Ctrl+→ перемещение по словам
|
|
186
|
+
/ + Tab подсказка и автодополнение slash-команд
|
|
187
|
+
Ctrl+J новая строка (Shift+Enter в терминалах с поддержкой)
|
|
184
188
|
Esc, Ctrl+C прервать текущую генерацию
|
|
185
189
|
|
|
186
190
|
${theme.bold('Обычные команды:')}
|
|
@@ -226,6 +230,34 @@ ${theme.bold('Файлы:')}
|
|
|
226
230
|
function dirLabel(p) {
|
|
227
231
|
return path.basename(p) || p;
|
|
228
232
|
}
|
|
233
|
+
// Список slash-команд для автодополнения при вводе «/» (Tab — дополнить).
|
|
234
|
+
const SLASH_COMMANDS = [
|
|
235
|
+
{ name: '/help', description: 'справка по командам и опциям' },
|
|
236
|
+
{ name: '/new', description: 'новый чат (сброс контекста)' },
|
|
237
|
+
{ name: '/clear', description: 'то же, что /new' },
|
|
238
|
+
{ name: '/sessions', description: 'список сохранённых сессий' },
|
|
239
|
+
{ name: '/chats', description: 'последние чаты DeepSeek' },
|
|
240
|
+
{ name: '/resume', description: 'открыть чат №n из /chats' },
|
|
241
|
+
{ name: '/resume-id', description: 'восстановить сессию по полному id' },
|
|
242
|
+
{ name: '/chat', description: 'показать текущий chat id' },
|
|
243
|
+
{ name: '/cd', description: 'сменить рабочую директорию' },
|
|
244
|
+
{ name: '/pwd', description: 'текущая рабочая директория' },
|
|
245
|
+
{ name: '/status', description: 'состояние сессии' },
|
|
246
|
+
{ name: '/reload', description: 'перечитать модули логики без перезапуска' },
|
|
247
|
+
{ name: '/undo', description: 'откатить последнюю запись/правку' },
|
|
248
|
+
{ name: '/undo-list', description: 'список того, что можно откатить' },
|
|
249
|
+
{ name: '/transcript', description: 'путь к файлу транскрипта' },
|
|
250
|
+
{ name: '/config', description: 'показать текущий конфиг' },
|
|
251
|
+
{ name: '/debug-dom', description: 'сохранить HTML страницы (отладка)' },
|
|
252
|
+
{ name: '/self-review', description: 'снапшот src/ и запуск ревью' },
|
|
253
|
+
{ name: '/self-fix', description: 'продолжить в существующем снапшоте' },
|
|
254
|
+
{ name: '/self-done', description: 'выйти из режима ревью' },
|
|
255
|
+
{ name: '/self-list', description: 'список снапшотов' },
|
|
256
|
+
{ name: '/self-diff', description: 'различия текущего src/ и снапшота' },
|
|
257
|
+
{ name: '/self-apply', description: 'применить снапшот к src/ (с бэкапом)' },
|
|
258
|
+
{ name: '/exit', description: 'выход' },
|
|
259
|
+
{ name: '/quit', description: 'выход' },
|
|
260
|
+
];
|
|
229
261
|
// Временные файлы агента (одноразовые скрипты и т.п.) складываем в
|
|
230
262
|
// <проект>/tmp — эта папка в .gitignore и очищается при каждом запуске.
|
|
231
263
|
const TMP_DIR = path.join(__dirname, '..', 'tmp');
|
|
@@ -839,7 +871,10 @@ async function main() {
|
|
|
839
871
|
return theme.prompt('❯ ') + tail + theme.dim(' › ');
|
|
840
872
|
};
|
|
841
873
|
if (process.stdin.isTTY && process.stdout.isTTY) {
|
|
842
|
-
const ed = new LineEditor({
|
|
874
|
+
const ed = new LineEditor({
|
|
875
|
+
prompt: buildPrompt(),
|
|
876
|
+
commands: SLASH_COMMANDS,
|
|
877
|
+
});
|
|
843
878
|
editor = ed;
|
|
844
879
|
ed.onSubmit = (text) => {
|
|
845
880
|
pendingQueue.push(text);
|
package/dist/input.js
CHANGED
|
@@ -9,7 +9,14 @@ 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
|
+
// ввода стрелки двигают по строкам, как в обычном терминале);
|
|
18
|
+
// * подсказки slash-команд: при вводе «/» под строкой показываются
|
|
19
|
+
// подходящие команды с описанием, Tab — дополнить.
|
|
13
20
|
//
|
|
14
21
|
// Все управляющие символы собираются из кодов, чтобы в файле не было
|
|
15
22
|
// «сырых» ESC/CR/LF в строковых литералах (см. AGENTS.md).
|
|
@@ -22,7 +29,7 @@ const PASTE_END = ESC + '[201~';
|
|
|
22
29
|
// Ширину выравниваем по максимуму (3), чтобы подсказка не смещалась.
|
|
23
30
|
const DOTS = ['', '.', '..', '...'];
|
|
24
31
|
const DOTS_PAD = ' ';
|
|
25
|
-
const HINT = theme.dim(' · Enter — отправить, Ctrl
|
|
32
|
+
const HINT = theme.dim(' · Enter — отправить, ↑/↓ — история, Ctrl+←/→ — по словам, Esc — стоп');
|
|
26
33
|
function safeJson(v) {
|
|
27
34
|
try {
|
|
28
35
|
return JSON.stringify(v);
|
|
@@ -64,14 +71,29 @@ export function layoutInput(promptStr, buf, cursor, cols) {
|
|
|
64
71
|
const first = rows.length === 0;
|
|
65
72
|
const prefix = first ? promptStr : pad;
|
|
66
73
|
const avail = Math.max(1, width - promptW);
|
|
74
|
+
const start = i;
|
|
75
|
+
// Собираем очередную визуальную строку. Сначала набираем столько
|
|
76
|
+
// символов, сколько влезает (count < avail). Если упёрлись в ширину
|
|
77
|
+
// и следующий символ — не конец строки, откатываемся к последнему
|
|
78
|
+
// пробелу, чтобы не разрывать слово посередине (word-wrap).
|
|
67
79
|
let text = '';
|
|
68
80
|
let count = 0;
|
|
69
|
-
|
|
81
|
+
let lastSpace = -1; // индекс пробела в text (по Array.from)
|
|
70
82
|
while (i < chars.length && chars[i] !== NL && count < avail) {
|
|
83
|
+
if (chars[i] === ' ')
|
|
84
|
+
lastSpace = count;
|
|
71
85
|
text += chars[i];
|
|
72
86
|
i++;
|
|
73
87
|
count++;
|
|
74
88
|
}
|
|
89
|
+
const atLineEnd = i >= chars.length || chars[i] === NL;
|
|
90
|
+
if (!atLineEnd && count >= avail && lastSpace > 0) {
|
|
91
|
+
// Переносим «хвост» строки на следующую визуальную строку.
|
|
92
|
+
const textChars = Array.from(text);
|
|
93
|
+
const tailLen = textChars.length - lastSpace;
|
|
94
|
+
i -= tailLen;
|
|
95
|
+
text = textChars.slice(0, lastSpace).join('');
|
|
96
|
+
}
|
|
75
97
|
if (i < chars.length && chars[i] === NL) {
|
|
76
98
|
i++;
|
|
77
99
|
rows.push({ prefix, text, start });
|
|
@@ -117,7 +139,14 @@ export class LineEditor {
|
|
|
117
139
|
_thinkBase;
|
|
118
140
|
_wasRaw;
|
|
119
141
|
_onData;
|
|
120
|
-
|
|
142
|
+
// История отправленных сообщений (для стрелок вверх/вниз).
|
|
143
|
+
history;
|
|
144
|
+
_histIndex;
|
|
145
|
+
_histDraft;
|
|
146
|
+
// Подсказки slash-команд (показываются при вводе «/»).
|
|
147
|
+
slashCommands;
|
|
148
|
+
_suggestCount;
|
|
149
|
+
constructor({ prompt = '> ', commands = [] } = {}) {
|
|
121
150
|
this.promptStr = prompt;
|
|
122
151
|
this.buf = '';
|
|
123
152
|
this.cursor = 0;
|
|
@@ -135,6 +164,45 @@ export class LineEditor {
|
|
|
135
164
|
this._thinkBase = '';
|
|
136
165
|
this._wasRaw = false;
|
|
137
166
|
this._onData = (b) => this._handle(b);
|
|
167
|
+
this.history = [];
|
|
168
|
+
this._histIndex = 0;
|
|
169
|
+
this._histDraft = '';
|
|
170
|
+
this.slashCommands = commands;
|
|
171
|
+
this._suggestCount = 0;
|
|
172
|
+
}
|
|
173
|
+
// Список подсказок для текущего ввода. Показываем, только когда строка
|
|
174
|
+
// начинается с «/» и это одно слово (без пробелов/переводов строк) — как
|
|
175
|
+
// автодополнение команд в Claude Code.
|
|
176
|
+
_suggestions() {
|
|
177
|
+
const b = this.buf;
|
|
178
|
+
if (!b.startsWith('/'))
|
|
179
|
+
return [];
|
|
180
|
+
if (b.includes(NL) || b.includes(' '))
|
|
181
|
+
return [];
|
|
182
|
+
const q = b.toLowerCase();
|
|
183
|
+
return this.slashCommands.filter((c) => c.name.toLowerCase().startsWith(q));
|
|
184
|
+
}
|
|
185
|
+
// Tab: дополнить команду до общего префикса; если совпадение одно —
|
|
186
|
+
// подставить его целиком и добавить пробел.
|
|
187
|
+
_completeCommand() {
|
|
188
|
+
const sugg = this._suggestions();
|
|
189
|
+
if (!sugg.length)
|
|
190
|
+
return;
|
|
191
|
+
if (sugg.length === 1) {
|
|
192
|
+
this.buf = sugg[0].name + ' ';
|
|
193
|
+
this.cursor = Array.from(this.buf).length;
|
|
194
|
+
return;
|
|
195
|
+
}
|
|
196
|
+
let prefix = sugg[0].name;
|
|
197
|
+
for (const c of sugg) {
|
|
198
|
+
while (!c.name.toLowerCase().startsWith(prefix.toLowerCase())) {
|
|
199
|
+
prefix = prefix.slice(0, -1);
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
if (prefix.length > this.buf.length) {
|
|
203
|
+
this.buf = prefix;
|
|
204
|
+
this.cursor = Array.from(this.buf).length;
|
|
205
|
+
}
|
|
138
206
|
}
|
|
139
207
|
start() {
|
|
140
208
|
const stdin = process.stdin;
|
|
@@ -187,9 +255,29 @@ export class LineEditor {
|
|
|
187
255
|
}
|
|
188
256
|
const lay = layoutInput(this.promptStr, this.buf, this.cursor, cols);
|
|
189
257
|
out += lay.rows.map((r) => r.prefix + r.text).join(NL);
|
|
258
|
+
// Подсказки slash-команд рисуем НИЖЕ строки ввода. Курсор потом
|
|
259
|
+
// возвращаем вверх, на строку ввода, поэтому cursorRowFromTop не меняется.
|
|
260
|
+
const sugg = this._suggestions();
|
|
261
|
+
const shown = sugg.slice(0, 8);
|
|
262
|
+
this._suggestCount = shown.length;
|
|
263
|
+
if (shown.length) {
|
|
264
|
+
const maxName = Math.max(...shown.map((c) => c.name.length));
|
|
265
|
+
const lines = shown.map((c) => {
|
|
266
|
+
const name = theme.prompt(c.name.padEnd(maxName));
|
|
267
|
+
const desc = theme.dim(' ' + c.description);
|
|
268
|
+
return ' ' + name + desc;
|
|
269
|
+
});
|
|
270
|
+
out += NL + lines.join(NL);
|
|
271
|
+
const hidden = sugg.length - shown.length;
|
|
272
|
+
if (hidden > 0)
|
|
273
|
+
out += NL + theme.dim(' …ещё ' + hidden);
|
|
274
|
+
}
|
|
190
275
|
process.stdout.write(out);
|
|
191
276
|
const lastRow = lay.rows.length - 1;
|
|
192
|
-
|
|
277
|
+
// Курсор вверх: сначала на строку ввода внутри lay, затем ещё на
|
|
278
|
+
// строки подсказок (если они есть) — курсор должен стоять на вводе.
|
|
279
|
+
const linesBelow = shown.length ? shown.length + (sugg.length > shown.length ? 1 : 0) : 0;
|
|
280
|
+
const up = lastRow - lay.cursorRow + linesBelow;
|
|
193
281
|
if (up > 0)
|
|
194
282
|
process.stdout.write(ESC + '[' + up + 'A');
|
|
195
283
|
process.stdout.write(CR);
|
|
@@ -278,6 +366,12 @@ export class LineEditor {
|
|
|
278
366
|
this._render();
|
|
279
367
|
return;
|
|
280
368
|
}
|
|
369
|
+
// В историю — только непустые и не дублирующие прошлое сообщение.
|
|
370
|
+
if (text.trim() && this.history[this.history.length - 1] !== text) {
|
|
371
|
+
this.history.push(text);
|
|
372
|
+
}
|
|
373
|
+
this._histIndex = this.history.length;
|
|
374
|
+
this._histDraft = '';
|
|
281
375
|
this.buf = '';
|
|
282
376
|
this.cursor = 0;
|
|
283
377
|
this.pendingText = null;
|
|
@@ -331,6 +425,19 @@ export class LineEditor {
|
|
|
331
425
|
i++;
|
|
332
426
|
this.cursor = i;
|
|
333
427
|
}
|
|
428
|
+
// Курсор на первой/последней ВИЗУАЛЬНОЙ строке (с учётом переноса).
|
|
429
|
+
// Нужно, чтобы Up/Down работали как история на краях ввода, а внутри —
|
|
430
|
+
// как перемещение по строкам, как в обычном терминале.
|
|
431
|
+
_onFirstVisualLine() {
|
|
432
|
+
const cols = process.stdout.columns || 80;
|
|
433
|
+
const lay = layoutInput(this.promptStr, this.buf, this.cursor, cols);
|
|
434
|
+
return lay.cursorRow === 0;
|
|
435
|
+
}
|
|
436
|
+
_onLastVisualLine() {
|
|
437
|
+
const cols = process.stdout.columns || 80;
|
|
438
|
+
const lay = layoutInput(this.promptStr, this.buf, this.cursor, cols);
|
|
439
|
+
return lay.cursorRow === lay.rows.length - 1;
|
|
440
|
+
}
|
|
334
441
|
_up() {
|
|
335
442
|
const chars = Array.from(this.buf);
|
|
336
443
|
let start = this.cursor;
|
|
@@ -362,6 +469,64 @@ export class LineEditor {
|
|
|
362
469
|
nextEnd++;
|
|
363
470
|
this.cursor = nextStart + Math.min(col, nextEnd - nextStart);
|
|
364
471
|
}
|
|
472
|
+
// Перемещение на слово назад (Ctrl+Left / Alt+B): пропускаем пробелы
|
|
473
|
+
// слева, затем идём до начала слова.
|
|
474
|
+
_wordLeft() {
|
|
475
|
+
const chars = Array.from(this.buf);
|
|
476
|
+
let i = this.cursor;
|
|
477
|
+
while (i > 0 && /\s/.test(chars[i - 1]))
|
|
478
|
+
i--;
|
|
479
|
+
while (i > 0 && !/\s/.test(chars[i - 1]))
|
|
480
|
+
i--;
|
|
481
|
+
this.cursor = i;
|
|
482
|
+
}
|
|
483
|
+
// Перемещение на слово вперёд (Ctrl+Right / Alt+F).
|
|
484
|
+
_wordRight() {
|
|
485
|
+
const chars = Array.from(this.buf);
|
|
486
|
+
let i = this.cursor;
|
|
487
|
+
while (i < chars.length && /\s/.test(chars[i]))
|
|
488
|
+
i++;
|
|
489
|
+
while (i < chars.length && !/\s/.test(chars[i]))
|
|
490
|
+
i++;
|
|
491
|
+
this.cursor = i;
|
|
492
|
+
}
|
|
493
|
+
// Удаление слова слева (Ctrl+W / Alt+Backspace).
|
|
494
|
+
_deleteWordLeft() {
|
|
495
|
+
const chars = Array.from(this.buf);
|
|
496
|
+
let i = this.cursor;
|
|
497
|
+
while (i > 0 && /\s/.test(chars[i - 1]))
|
|
498
|
+
i--;
|
|
499
|
+
while (i > 0 && !/\s/.test(chars[i - 1]))
|
|
500
|
+
i--;
|
|
501
|
+
chars.splice(i, this.cursor - i);
|
|
502
|
+
this.buf = chars.join('');
|
|
503
|
+
this.cursor = i;
|
|
504
|
+
}
|
|
505
|
+
// ---------- история ----------
|
|
506
|
+
_historyUp() {
|
|
507
|
+
if (!this.history.length)
|
|
508
|
+
return;
|
|
509
|
+
if (this._histIndex === this.history.length)
|
|
510
|
+
this._histDraft = this.buf;
|
|
511
|
+
if (this._histIndex > 0) {
|
|
512
|
+
this._histIndex--;
|
|
513
|
+
this.buf = this.history[this._histIndex];
|
|
514
|
+
this.cursor = Array.from(this.buf).length;
|
|
515
|
+
}
|
|
516
|
+
}
|
|
517
|
+
_historyDown() {
|
|
518
|
+
if (!this.history.length)
|
|
519
|
+
return;
|
|
520
|
+
if (this._histIndex < this.history.length - 1) {
|
|
521
|
+
this._histIndex++;
|
|
522
|
+
this.buf = this.history[this._histIndex];
|
|
523
|
+
}
|
|
524
|
+
else {
|
|
525
|
+
this._histIndex = this.history.length;
|
|
526
|
+
this.buf = this._histDraft;
|
|
527
|
+
}
|
|
528
|
+
this.cursor = Array.from(this.buf).length;
|
|
529
|
+
}
|
|
365
530
|
_handle(data) {
|
|
366
531
|
let s = data.toString('utf-8');
|
|
367
532
|
if (!this._inPaste && s === ESC) {
|
|
@@ -446,18 +611,53 @@ export class LineEditor {
|
|
|
446
611
|
this._render();
|
|
447
612
|
continue;
|
|
448
613
|
}
|
|
614
|
+
if (code === 9) {
|
|
615
|
+
this._completeCommand();
|
|
616
|
+
this._render();
|
|
617
|
+
continue;
|
|
618
|
+
}
|
|
449
619
|
if (code === 21) {
|
|
450
620
|
this.buf = '';
|
|
451
621
|
this.cursor = 0;
|
|
452
622
|
this._render();
|
|
453
623
|
continue;
|
|
454
624
|
}
|
|
625
|
+
if (code === 23) {
|
|
626
|
+
this._deleteWordLeft();
|
|
627
|
+
this._render();
|
|
628
|
+
continue;
|
|
629
|
+
}
|
|
630
|
+
if (code === 11) {
|
|
631
|
+
// Ctrl+K — удалить от курсора до конца строки.
|
|
632
|
+
const arr = Array.from(this.buf);
|
|
633
|
+
let e = this.cursor;
|
|
634
|
+
while (e < arr.length && arr[e] !== NL)
|
|
635
|
+
e++;
|
|
636
|
+
arr.splice(this.cursor, e - this.cursor);
|
|
637
|
+
this.buf = arr.join('');
|
|
638
|
+
this._render();
|
|
639
|
+
continue;
|
|
640
|
+
}
|
|
455
641
|
if (code === 127 || code === 8) {
|
|
456
642
|
this._backspace();
|
|
457
643
|
this._render();
|
|
458
644
|
continue;
|
|
459
645
|
}
|
|
460
646
|
if (code === 27) {
|
|
647
|
+
// Ctrl+Left / Ctrl+Right (xterm: ESC [1;5D / ESC [1;5C;
|
|
648
|
+
// некоторые терминалы: ESC [5D / ESC [5C).
|
|
649
|
+
if (s.startsWith('[1;5D') || s.startsWith('[5D')) {
|
|
650
|
+
this._wordLeft();
|
|
651
|
+
s = s.slice(s.startsWith('[1;5D') ? 5 : 3);
|
|
652
|
+
this._render();
|
|
653
|
+
continue;
|
|
654
|
+
}
|
|
655
|
+
if (s.startsWith('[1;5C') || s.startsWith('[5C')) {
|
|
656
|
+
this._wordRight();
|
|
657
|
+
s = s.slice(s.startsWith('[1;5C') ? 5 : 3);
|
|
658
|
+
this._render();
|
|
659
|
+
continue;
|
|
660
|
+
}
|
|
461
661
|
if (s.startsWith('[D')) {
|
|
462
662
|
this._left();
|
|
463
663
|
s = s.slice(2);
|
|
@@ -471,13 +671,20 @@ export class LineEditor {
|
|
|
471
671
|
continue;
|
|
472
672
|
}
|
|
473
673
|
if (s.startsWith('[A')) {
|
|
474
|
-
|
|
674
|
+
// Вверх: на первой визуальной строке — история, иначе — строка выше.
|
|
675
|
+
if (this._onFirstVisualLine())
|
|
676
|
+
this._historyUp();
|
|
677
|
+
else
|
|
678
|
+
this._up();
|
|
475
679
|
s = s.slice(2);
|
|
476
680
|
this._render();
|
|
477
681
|
continue;
|
|
478
682
|
}
|
|
479
683
|
if (s.startsWith('[B')) {
|
|
480
|
-
this.
|
|
684
|
+
if (this._onLastVisualLine())
|
|
685
|
+
this._historyDown();
|
|
686
|
+
else
|
|
687
|
+
this._down();
|
|
481
688
|
s = s.slice(2);
|
|
482
689
|
this._render();
|
|
483
690
|
continue;
|
|
@@ -500,6 +707,19 @@ export class LineEditor {
|
|
|
500
707
|
this._render();
|
|
501
708
|
continue;
|
|
502
709
|
}
|
|
710
|
+
// Alt+B / Alt+F — перемещение по словам.
|
|
711
|
+
if (s.startsWith('b') || s.startsWith('B')) {
|
|
712
|
+
this._wordLeft();
|
|
713
|
+
s = s.slice(1);
|
|
714
|
+
this._render();
|
|
715
|
+
continue;
|
|
716
|
+
}
|
|
717
|
+
if (s.startsWith('f') || s.startsWith('F')) {
|
|
718
|
+
this._wordRight();
|
|
719
|
+
s = s.slice(1);
|
|
720
|
+
this._render();
|
|
721
|
+
continue;
|
|
722
|
+
}
|
|
503
723
|
let j = 0;
|
|
504
724
|
while (j < s.length && !/[A-Za-z~]/.test(s[j]))
|
|
505
725
|
j++;
|