zames_pro 1.1.0 → 1.2.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/README.md +27 -37
- package/logo.png +0 -0
- package/package.json +1 -1
- package/src/agent-loop.js +28 -0
- package/src/browser.js +22 -0
- package/src/config.js +1 -0
- package/src/index.js +503 -72
- package/src/input.js +447 -0
- package/src/spinner.js +97 -1
- package/src/system-prompt.js +39 -5
- package/src/theme.js +1 -0
- package/src/xml-toolcall.js +69 -0
package/src/index.js
CHANGED
|
@@ -2,13 +2,13 @@
|
|
|
2
2
|
import path from 'path'
|
|
3
3
|
import fs from 'fs/promises'
|
|
4
4
|
import { fileURLToPath } from 'url'
|
|
5
|
-
import * as readlinePromises from 'readline/promises'
|
|
6
5
|
import { theme } from './theme.js'
|
|
7
6
|
|
|
8
7
|
import { DeepSeekBrowser } from './browser.js'
|
|
9
8
|
import { createTools } from './tools.js'
|
|
10
9
|
import { runAgentLoop } from './agent-loop.js'
|
|
11
10
|
import { createSpinner } from './spinner.js'
|
|
11
|
+
import { LineEditor } from './input.js'
|
|
12
12
|
import { loadConfig, CONFIG_PATHS, ZAMES_HOME } from './config.js'
|
|
13
13
|
import { Transcript } from './transcript.js'
|
|
14
14
|
import { UndoStore } from './undo.js'
|
|
@@ -201,6 +201,10 @@ ${theme.bold('Опции CLI:')}
|
|
|
201
201
|
--version, -v показать версию
|
|
202
202
|
--help, -h эта справка
|
|
203
203
|
|
|
204
|
+
${theme.bold('Пока агент работает:')}
|
|
205
|
+
печать + Enter поставить сообщение в очередь (уйдёт после текущей задачи)
|
|
206
|
+
Esc, Ctrl+C прервать текущую генерацию
|
|
207
|
+
|
|
204
208
|
${theme.bold('Обычные команды:')}
|
|
205
209
|
/new, /clear новый чат (сброс контекста)
|
|
206
210
|
/chats список последних чатов DeepSeek
|
|
@@ -256,46 +260,352 @@ async function cleanTmpDir() {
|
|
|
256
260
|
}
|
|
257
261
|
}
|
|
258
262
|
|
|
263
|
+
// Ввод строки в терминале с корректной обработкой вставки (Shift+Insert,
|
|
264
|
+
// Ctrl+Shift+V, правая кнопка мыши и т.п.).
|
|
265
|
+
//
|
|
266
|
+
// Зачем свой ридер вместо readline:
|
|
267
|
+
// 1) readline отправляет строку на ПЕРВОМ переводе строки. При вставке
|
|
268
|
+
// многострочного текста это приводило к немедленной отправке и к тому,
|
|
269
|
+
// что в чат уходила только первая строка. Здесь переводы строк внутри
|
|
270
|
+
// вставки заменяются на пробелы, а отправка происходит только по
|
|
271
|
+
// одиночному нажатию Enter.
|
|
272
|
+
// 2) Включаем bracketed paste mode (\x1b[?2004h): терминал оборачивает
|
|
273
|
+
// вставленный текст в маркеры \x1b[200~ … \x1b[201~, поэтому мы точно
|
|
274
|
+
// знаем, что это вставка, а не набор с клавиатуры, и Enter внутри неё
|
|
275
|
+
// не считается отправкой.
|
|
259
276
|
async function promptOnce(question) {
|
|
260
|
-
process.stdin
|
|
261
|
-
|
|
262
|
-
|
|
277
|
+
const stdin = process.stdin
|
|
278
|
+
const stdout = process.stdout
|
|
279
|
+
|
|
280
|
+
// Не-TTY (пайп, редирект): читаем всё до EOF одной строкой.
|
|
281
|
+
if (!stdin.isTTY || !stdin.setRawMode) {
|
|
282
|
+
const chunks = []
|
|
283
|
+
return await new Promise((resolve) => {
|
|
284
|
+
const onData = (b) => chunks.push(b)
|
|
285
|
+
const onEnd = () => {
|
|
286
|
+
stdin.removeListener('data', onData)
|
|
287
|
+
stdin.removeListener('end', onEnd)
|
|
288
|
+
resolve(Buffer.concat(chunks).toString('utf-8'))
|
|
289
|
+
}
|
|
290
|
+
stdin.on('data', onData)
|
|
291
|
+
stdin.on('end', onEnd)
|
|
292
|
+
stdin.resume()
|
|
293
|
+
})
|
|
263
294
|
}
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
295
|
+
|
|
296
|
+
const wasRaw = stdin.isRaw
|
|
297
|
+
stdin.setRawMode(true)
|
|
298
|
+
stdin.resume()
|
|
299
|
+
|
|
300
|
+
// Bracketed paste включаем/выключаем парно.
|
|
301
|
+
stdout.write('\x1b[?2004h')
|
|
302
|
+
stdout.write(question)
|
|
303
|
+
|
|
304
|
+
let line = ''
|
|
305
|
+
let cursor = 0
|
|
306
|
+
let inPaste = false
|
|
307
|
+
const PASTE_START = '\x1b[200~'
|
|
308
|
+
const PASTE_END = '\x1b[201~'
|
|
309
|
+
|
|
310
|
+
const redraw = () => {
|
|
311
|
+
// Возвращаемся в начало строки, стираем и печатаем заново.
|
|
312
|
+
stdout.write(String.fromCharCode(13))
|
|
313
|
+
stdout.write('\x1b[K')
|
|
314
|
+
stdout.write(question + line)
|
|
315
|
+
// Ставим курсор в нужную позицию.
|
|
316
|
+
const back = line.length - cursor
|
|
317
|
+
if (back > 0) stdout.write('\x1b[' + back + 'D')
|
|
272
318
|
}
|
|
319
|
+
|
|
320
|
+
return await new Promise((resolve) => {
|
|
321
|
+
const finish = (value, submit) => {
|
|
322
|
+
stdin.removeListener('data', onData)
|
|
323
|
+
stdout.write('\x1b[?2004l')
|
|
324
|
+
if (stdin.setRawMode) stdin.setRawMode(wasRaw || false)
|
|
325
|
+
if (submit) stdout.write(String.fromCharCode(10))
|
|
326
|
+
resolve(value)
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
const insertText = (text) => {
|
|
330
|
+
// Нормализуем переводы строк: они приходят от многострочной вставки,
|
|
331
|
+
// но означают «отправить». Внутри сообщения заменяем на пробел, чтобы
|
|
332
|
+
// вся вставка ушла ОДНИМ сообщением.
|
|
333
|
+
const clean = text
|
|
334
|
+
.replace(/\r\n/g, ' ')
|
|
335
|
+
.replace(/\r/g, ' ')
|
|
336
|
+
.replace(/\n/g, ' ')
|
|
337
|
+
line = line.slice(0, cursor) + clean + line.slice(cursor)
|
|
338
|
+
cursor += clean.length
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
const onData = (buf) => {
|
|
342
|
+
let s = buf.toString('utf-8')
|
|
343
|
+
|
|
344
|
+
// Fallback для терминалов без bracketed paste: если весь чанк — это
|
|
345
|
+
// «голый» перевод строки (один байт), значит нажали Enter → отправляем.
|
|
346
|
+
// Если переводы строк пришли ВМЕСТЕ с другим текстом в одном чанке —
|
|
347
|
+
// это вставка; такие переводы строк не отправляют сообщение, а
|
|
348
|
+
// заменяются на пробелы (см. insertText).
|
|
349
|
+
if (!inPaste && (s === '\r' || s === '\n')) {
|
|
350
|
+
return finish(line, true)
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
while (s.length) {
|
|
354
|
+
if (inPaste) {
|
|
355
|
+
const end = s.indexOf(PASTE_END)
|
|
356
|
+
if (end === -1) {
|
|
357
|
+
insertText(s)
|
|
358
|
+
s = ''
|
|
359
|
+
} else {
|
|
360
|
+
insertText(s.slice(0, end))
|
|
361
|
+
s = s.slice(end + PASTE_END.length)
|
|
362
|
+
inPaste = false
|
|
363
|
+
}
|
|
364
|
+
redraw()
|
|
365
|
+
continue
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
const start = s.indexOf(PASTE_START)
|
|
369
|
+
if (start !== -1) {
|
|
370
|
+
// Всё до маркера обрабатываем как обычный ввод.
|
|
371
|
+
const before = s.slice(0, start)
|
|
372
|
+
s = s.slice(start + PASTE_START.length)
|
|
373
|
+
inPaste = true
|
|
374
|
+
if (before) {
|
|
375
|
+
for (const ch of before) {
|
|
376
|
+
if (ch === '\r' || ch === '\n') { /* внутри вставки — пропускаем */ }
|
|
377
|
+
else insertText(ch)
|
|
378
|
+
}
|
|
379
|
+
redraw()
|
|
380
|
+
}
|
|
381
|
+
continue
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
const ch = s[0]
|
|
385
|
+
const code = s.charCodeAt(0)
|
|
386
|
+
s = s.slice(1)
|
|
387
|
+
|
|
388
|
+
if (ch === '\r' || ch === '\n') {
|
|
389
|
+
// Перевод строки внутри чанка с другим текстом (вставка без
|
|
390
|
+
// bracketed paste): не отправляем, а вставляем пробел.
|
|
391
|
+
insertText(' ')
|
|
392
|
+
redraw()
|
|
393
|
+
continue
|
|
394
|
+
}
|
|
395
|
+
if (code === 3) {
|
|
396
|
+
// Ctrl+C — прерываем ввод.
|
|
397
|
+
return finish('', true)
|
|
398
|
+
}
|
|
399
|
+
if (code === 4) {
|
|
400
|
+
// Ctrl+D — как отправка пустой строки.
|
|
401
|
+
return finish(line, true)
|
|
402
|
+
}
|
|
403
|
+
if (code === 21) {
|
|
404
|
+
// Ctrl+U — стереть строку.
|
|
405
|
+
line = ''
|
|
406
|
+
cursor = 0
|
|
407
|
+
redraw()
|
|
408
|
+
continue
|
|
409
|
+
}
|
|
410
|
+
if (code === 127 || code === 8) {
|
|
411
|
+
// Backspace.
|
|
412
|
+
if (cursor > 0) {
|
|
413
|
+
line = line.slice(0, cursor - 1) + line.slice(cursor)
|
|
414
|
+
cursor--
|
|
415
|
+
redraw()
|
|
416
|
+
}
|
|
417
|
+
continue
|
|
418
|
+
}
|
|
419
|
+
if (ch === '\x1b') {
|
|
420
|
+
// Escape-последовательности (стрелки, Home/End, Delete…).
|
|
421
|
+
const rest = s
|
|
422
|
+
if (rest.startsWith('[D')) {
|
|
423
|
+
if (cursor > 0) cursor--
|
|
424
|
+
s = s.slice(2)
|
|
425
|
+
redraw()
|
|
426
|
+
continue
|
|
427
|
+
}
|
|
428
|
+
if (rest.startsWith('[C')) {
|
|
429
|
+
if (cursor < line.length) cursor++
|
|
430
|
+
s = s.slice(2)
|
|
431
|
+
redraw()
|
|
432
|
+
continue
|
|
433
|
+
}
|
|
434
|
+
if (rest.startsWith('[H') || rest.startsWith('[1~')) {
|
|
435
|
+
cursor = 0
|
|
436
|
+
s = s.slice(rest.startsWith('[1~') ? 3 : 2)
|
|
437
|
+
redraw()
|
|
438
|
+
continue
|
|
439
|
+
}
|
|
440
|
+
if (rest.startsWith('[F') || rest.startsWith('[4~')) {
|
|
441
|
+
cursor = line.length
|
|
442
|
+
s = s.slice(rest.startsWith('[4~') ? 3 : 2)
|
|
443
|
+
redraw()
|
|
444
|
+
continue
|
|
445
|
+
}
|
|
446
|
+
if (rest.startsWith('[3~')) {
|
|
447
|
+
// Delete.
|
|
448
|
+
if (cursor < line.length) {
|
|
449
|
+
line = line.slice(0, cursor) + line.slice(cursor + 1)
|
|
450
|
+
redraw()
|
|
451
|
+
}
|
|
452
|
+
s = s.slice(3)
|
|
453
|
+
continue
|
|
454
|
+
}
|
|
455
|
+
// Прочие ESC-последовательности пропускаем до буквы/тильды.
|
|
456
|
+
const m = s.match(/^\[[0-9;]*[A-Za-z~]/)
|
|
457
|
+
if (m) s = s.slice(m[0].length)
|
|
458
|
+
continue
|
|
459
|
+
}
|
|
460
|
+
if (code < 32) continue // прочие управляющие символы игнорируем
|
|
461
|
+
|
|
462
|
+
insertText(ch)
|
|
463
|
+
redraw()
|
|
464
|
+
}
|
|
465
|
+
}
|
|
466
|
+
|
|
467
|
+
stdin.on('data', onData)
|
|
468
|
+
})
|
|
273
469
|
}
|
|
274
470
|
|
|
275
471
|
// Слежение за клавиатурой во время работы агента.
|
|
276
|
-
//
|
|
277
|
-
|
|
472
|
+
//
|
|
473
|
+
// Терминал остаётся живым, пока агент думает:
|
|
474
|
+
// - Esc (или Ctrl+C) — прервать текущую генерацию (клик Stop в браузере);
|
|
475
|
+
// - набор текста + Enter — поставить сообщение в очередь; оно уйдёт
|
|
476
|
+
// агенту сразу после того, как текущая задача завершится
|
|
477
|
+
// (как «отправить во время генерации» в веб-версии DeepSeek).
|
|
478
|
+
//
|
|
479
|
+
// Ввод буферизуется без построчного редактора: набранный текст отображается
|
|
480
|
+
// в строке спиннера через onChange -> ui.setPending(). Enter отправляет буфер
|
|
481
|
+
// в onQueue, пустой Enter игнорируется. Поддержаны Backspace, Ctrl+U, Esc-
|
|
482
|
+
// последовательности (стрелки/Home/End/Delete игнорируются) и bracketed paste.
|
|
483
|
+
function watchInput({ onEscape, onChange, onQueue } = {}) {
|
|
278
484
|
const stdin = process.stdin
|
|
279
|
-
if (!stdin.isTTY) return () => {}
|
|
485
|
+
if (!stdin.isTTY || !stdin.setRawMode) return () => {}
|
|
486
|
+
|
|
487
|
+
// Управляющие символы собираем из кодов: в этом файле нельзя писать
|
|
488
|
+
// «сырые» ESC/CR/LF в строковых литералах (см. AGENTS.md).
|
|
489
|
+
const ESC = String.fromCharCode(27)
|
|
490
|
+
const CSI = String.fromCharCode(91)
|
|
491
|
+
const CR = String.fromCharCode(13)
|
|
492
|
+
const LF = String.fromCharCode(10)
|
|
493
|
+
|
|
280
494
|
const wasRaw = stdin.isRaw
|
|
281
|
-
|
|
495
|
+
stdin.setRawMode(true)
|
|
282
496
|
stdin.resume()
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
497
|
+
process.stdout.write(ESC + '[?2004h')
|
|
498
|
+
|
|
499
|
+
let buf = ''
|
|
500
|
+
let inPaste = false
|
|
501
|
+
const PASTE_START = ESC + '[200~'
|
|
502
|
+
const PASTE_END = ESC + '[201~'
|
|
503
|
+
const CSI_RE = new RegExp('^' + CSI + '[0-9;]*[A-Za-z~]')
|
|
504
|
+
|
|
505
|
+
const emitChange = () => {
|
|
506
|
+
if (onChange) onChange(buf)
|
|
507
|
+
}
|
|
508
|
+
|
|
509
|
+
// Вставленный текст: переводы строк внутри многострочной вставки
|
|
510
|
+
// трактуем как пробелы — сообщение уходит одной строкой.
|
|
511
|
+
const insert = (text) => {
|
|
512
|
+
buf += text
|
|
513
|
+
.split(CR + LF).join(' ')
|
|
514
|
+
.split(CR).join(' ')
|
|
515
|
+
.split(LF).join(' ')
|
|
516
|
+
}
|
|
517
|
+
|
|
518
|
+
function onData(data) {
|
|
519
|
+
let s = data.toString('utf-8')
|
|
520
|
+
|
|
521
|
+
// Одиночный Esc — прервать генерацию. Стрелки приходят целым чанком
|
|
522
|
+
// и сюда не попадают.
|
|
523
|
+
if (!inPaste && s === ESC) {
|
|
524
|
+
if (onEscape) onEscape()
|
|
525
|
+
return
|
|
526
|
+
}
|
|
527
|
+
|
|
528
|
+
while (s.length) {
|
|
529
|
+
if (inPaste) {
|
|
530
|
+
const end = s.indexOf(PASTE_END)
|
|
531
|
+
if (end === -1) {
|
|
532
|
+
insert(s)
|
|
533
|
+
s = ''
|
|
534
|
+
} else {
|
|
535
|
+
insert(s.slice(0, end))
|
|
536
|
+
s = s.slice(end + PASTE_END.length)
|
|
537
|
+
inPaste = false
|
|
538
|
+
}
|
|
539
|
+
emitChange()
|
|
540
|
+
continue
|
|
541
|
+
}
|
|
542
|
+
|
|
543
|
+
const start = s.indexOf(PASTE_START)
|
|
544
|
+
if (start !== -1) {
|
|
545
|
+
const before = s.slice(0, start)
|
|
546
|
+
s = s.slice(start + PASTE_START.length)
|
|
547
|
+
inPaste = true
|
|
548
|
+
if (before) {
|
|
549
|
+
insert(before)
|
|
550
|
+
emitChange()
|
|
551
|
+
}
|
|
552
|
+
continue
|
|
553
|
+
}
|
|
554
|
+
|
|
555
|
+
const ch = s[0]
|
|
556
|
+
const code = s.charCodeAt(0)
|
|
557
|
+
s = s.slice(1)
|
|
558
|
+
|
|
559
|
+
if (ch === CR || ch === LF) {
|
|
560
|
+
const text = buf.trim()
|
|
561
|
+
buf = ''
|
|
562
|
+
emitChange()
|
|
563
|
+
if (text && onQueue) onQueue(text)
|
|
564
|
+
continue
|
|
565
|
+
}
|
|
566
|
+
if (code === 3) {
|
|
567
|
+
// Ctrl+C — как Esc: прервать генерацию.
|
|
568
|
+
if (onEscape) onEscape()
|
|
569
|
+
continue
|
|
570
|
+
}
|
|
571
|
+
if (code === 21) {
|
|
572
|
+
// Ctrl+U — очистить набранное.
|
|
573
|
+
buf = ''
|
|
574
|
+
emitChange()
|
|
575
|
+
continue
|
|
576
|
+
}
|
|
577
|
+
if (code === 127 || code === 8) {
|
|
578
|
+
// Backspace.
|
|
579
|
+
if (buf) {
|
|
580
|
+
buf = buf.slice(0, -1)
|
|
581
|
+
emitChange()
|
|
582
|
+
}
|
|
583
|
+
continue
|
|
584
|
+
}
|
|
585
|
+
if (ch === ESC) {
|
|
586
|
+
// Escape-последовательность (стрелки, Home/End, Delete) — пропускаем.
|
|
587
|
+
const m = s.match(CSI_RE)
|
|
588
|
+
if (m) s = s.slice(m[0].length)
|
|
589
|
+
continue
|
|
590
|
+
}
|
|
591
|
+
if (code < 32) continue // прочие управляющие — игнорируем
|
|
592
|
+
|
|
593
|
+
buf += ch
|
|
594
|
+
emitChange()
|
|
286
595
|
}
|
|
287
596
|
}
|
|
597
|
+
|
|
598
|
+
const handler = (data) => onData(data)
|
|
288
599
|
stdin.on('data', handler)
|
|
289
600
|
return () => {
|
|
290
601
|
stdin.removeListener('data', handler)
|
|
602
|
+
process.stdout.write(ESC + '[?2004l')
|
|
291
603
|
if (stdin.setRawMode) stdin.setRawMode(wasRaw || false)
|
|
292
604
|
}
|
|
293
605
|
}
|
|
294
606
|
|
|
295
607
|
// ---------- workdir resolution ----------
|
|
296
608
|
|
|
297
|
-
// ---------- workdir resolution ----------
|
|
298
|
-
|
|
299
609
|
// Агент работает в директории, из которой его запустили (process.cwd()).
|
|
300
610
|
// Это корень sandbox: инструменты не могут выходить выше него.
|
|
301
611
|
async function resolveWorkdir() {
|
|
@@ -311,47 +621,75 @@ async function resolveWorkdir() {
|
|
|
311
621
|
// ---------- task runner ----------
|
|
312
622
|
|
|
313
623
|
async function runTask(browser, tools, taskText, workdir, opts) {
|
|
314
|
-
const { transcript, freshChat, sendSystemPrompt } = opts
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
const stopWatching =
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
624
|
+
const { transcript, freshChat, sendSystemPrompt, queue = [], ui: editor } = opts
|
|
625
|
+
|
|
626
|
+
// В TTY-режиме UI — это LineEditor: он владеет вводом (очередь, Esc,
|
|
627
|
+
// Ctrl+C) и рисует статус НАД постоянной строкой ввода. В не-TTY режиме
|
|
628
|
+
// (пайпы) — обычный спиннер + watchInput.
|
|
629
|
+
const ui = editor || mod.createSpinner()
|
|
630
|
+
const stopWatching = editor
|
|
631
|
+
? () => {}
|
|
632
|
+
: watchInput({
|
|
633
|
+
onEscape: () => {
|
|
634
|
+
ui.stop()
|
|
635
|
+
console.error(theme.warn('⏹ Esc — прерываю генерацию...'))
|
|
636
|
+
browser.stopGeneration().catch(() => {})
|
|
637
|
+
},
|
|
638
|
+
onChange: (text) => ui.setPending(text),
|
|
639
|
+
onQueue: (text) => {
|
|
640
|
+
queue.push(text)
|
|
641
|
+
ui.setPending(null)
|
|
642
|
+
ui.stop()
|
|
643
|
+
console.log(
|
|
644
|
+
theme.user('📨 В очередь (' + queue.length + '): ') + theme.assistant(text),
|
|
645
|
+
)
|
|
646
|
+
ui.thinking()
|
|
647
|
+
},
|
|
648
|
+
})
|
|
327
649
|
|
|
328
650
|
try {
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
651
|
+
let next = { task: taskText, freshChat, sendSystemPrompt }
|
|
652
|
+
|
|
653
|
+
// Выполняем задачу, затем — всё, что пользователь успел напечатать за
|
|
654
|
+
// время работы. Очередь может пополняться прямо во время дренажа.
|
|
655
|
+
while (true) {
|
|
656
|
+
ui.thinking()
|
|
657
|
+
await mod.runAgentLoop({
|
|
658
|
+
browser,
|
|
659
|
+
tools,
|
|
660
|
+
task: next.task,
|
|
661
|
+
workdir,
|
|
662
|
+
maxIterations: maxIter,
|
|
663
|
+
freshChat: next.freshChat,
|
|
664
|
+
sendSystemPrompt: next.sendSystemPrompt,
|
|
665
|
+
transcript,
|
|
666
|
+
onThinking: () => ui.thinking(),
|
|
667
|
+
onToolCall: (name, toolArgs) => ui.toolCall(name, toolArgs),
|
|
668
|
+
onToolResult: (result) => ui.toolResult(result),
|
|
669
|
+
onAssistantMessage: (msg) => ui.assistant(msg),
|
|
670
|
+
debugLog: debug,
|
|
671
|
+
})
|
|
672
|
+
|
|
673
|
+
if (!queue.length) break
|
|
674
|
+
|
|
675
|
+
const queued = queue.shift()
|
|
676
|
+
ui.stop()
|
|
677
|
+
if (editor) {
|
|
678
|
+
editor.printAbove(theme.user('▶ Из очереди: ') + theme.assistant(queued))
|
|
679
|
+
} else {
|
|
680
|
+
console.log(theme.user('▶ Из очереди: ') + theme.assistant(queued))
|
|
681
|
+
}
|
|
682
|
+
transcript?.log('queued_task', { task: queued })
|
|
683
|
+
next = { task: queued, freshChat: false, sendSystemPrompt: false }
|
|
684
|
+
}
|
|
347
685
|
} catch (e) {
|
|
348
686
|
ui.stop()
|
|
349
|
-
console.error(theme.error('
|
|
687
|
+
console.error(theme.error(String.fromCharCode(10) + '✖ Ошибка агента:'), e.message)
|
|
350
688
|
if (debug) console.error(e.stack)
|
|
351
689
|
transcript?.log('agent_error', { error: e.message })
|
|
352
690
|
} finally {
|
|
353
691
|
stopWatching()
|
|
354
|
-
|
|
692
|
+
ui.stop()
|
|
355
693
|
}
|
|
356
694
|
}
|
|
357
695
|
|
|
@@ -468,7 +806,12 @@ async function main() {
|
|
|
468
806
|
|
|
469
807
|
console.log(
|
|
470
808
|
theme.system(
|
|
471
|
-
'Интерактивный режим. Введите задачу. Команды — /help. Выход — /exit
|
|
809
|
+
'Интерактивный режим. Введите задачу. Команды — /help. Выход — /exit.',
|
|
810
|
+
),
|
|
811
|
+
)
|
|
812
|
+
console.log(
|
|
813
|
+
theme.system(
|
|
814
|
+
'Пока агент работает, можно печатать следующее сообщение — оно уйдёт в очередь (Enter — отправить, Esc — прервать).',
|
|
472
815
|
),
|
|
473
816
|
)
|
|
474
817
|
|
|
@@ -478,6 +821,10 @@ async function main() {
|
|
|
478
821
|
let currentChatId = null
|
|
479
822
|
let running = true
|
|
480
823
|
|
|
824
|
+
// Сообщения, набранные пользователем, пока агент работал. runTask
|
|
825
|
+
// забирает их по одному после завершения текущей задачи.
|
|
826
|
+
const pendingQueue = []
|
|
827
|
+
|
|
481
828
|
// ---------- review mode state ----------
|
|
482
829
|
// null — обычный режим.
|
|
483
830
|
// { snapDir, snapName, originalWorkdir } — мы внутри снапшота, чат уже
|
|
@@ -518,18 +865,98 @@ async function main() {
|
|
|
518
865
|
}
|
|
519
866
|
}
|
|
520
867
|
|
|
868
|
+
// ---------- ввод: постоянная строка внизу + статус сверху ----------
|
|
869
|
+
// В TTY используем LineEditor: он владеет вводом всё время, показывает
|
|
870
|
+
// статус над строкой ввода и печатает ответы агента ВЫШЕ неё, поэтому
|
|
871
|
+
// набранный текст никогда не затирается выводом. В не-TTY (пайп) —
|
|
872
|
+
// старый promptOnce.
|
|
873
|
+
let editor = null
|
|
874
|
+
let waiter = null
|
|
875
|
+
const takeInput = () => {
|
|
876
|
+
if (pendingQueue.length) return Promise.resolve(pendingQueue.shift())
|
|
877
|
+
return new Promise((resolve) => {
|
|
878
|
+
waiter = resolve
|
|
879
|
+
})
|
|
880
|
+
}
|
|
881
|
+
|
|
882
|
+
const buildPrompt = () => {
|
|
883
|
+
let tail
|
|
884
|
+
if (reviewMode) {
|
|
885
|
+
tail = theme.warn('REVIEW') + theme.dim(':') + theme.dir(reviewMode.snapName)
|
|
886
|
+
} else {
|
|
887
|
+
tail = theme.dir(dirLabel(currentWorkdir))
|
|
888
|
+
}
|
|
889
|
+
return theme.prompt('❯ ') + tail + theme.dim(' › ')
|
|
890
|
+
}
|
|
891
|
+
|
|
892
|
+
if (process.stdin.isTTY && process.stdout.isTTY) {
|
|
893
|
+
editor = new LineEditor({ prompt: buildPrompt() })
|
|
894
|
+
editor.onSubmit = (text) => {
|
|
895
|
+
pendingQueue.push(text)
|
|
896
|
+
if (waiter) {
|
|
897
|
+
const r = waiter
|
|
898
|
+
waiter = null
|
|
899
|
+
r(pendingQueue.shift())
|
|
900
|
+
}
|
|
901
|
+
}
|
|
902
|
+
editor.onEscape = () => {
|
|
903
|
+
if (editor.busy) {
|
|
904
|
+
editor.printAbove(theme.warn('⏹ Esc — прерываю генерацию...'))
|
|
905
|
+
browser.stopGeneration().catch(() => {})
|
|
906
|
+
}
|
|
907
|
+
}
|
|
908
|
+
editor.onCtrlC = () => {
|
|
909
|
+
if (editor.busy) {
|
|
910
|
+
editor.printAbove(theme.warn('⏹ Ctrl+C — прерываю генерацию...'))
|
|
911
|
+
browser.stopGeneration().catch(() => {})
|
|
912
|
+
} else {
|
|
913
|
+
// Не заняты — выходим. Будим takeInput(), чтобы цикл завершился.
|
|
914
|
+
running = false
|
|
915
|
+
if (waiter) {
|
|
916
|
+
const r = waiter
|
|
917
|
+
waiter = null
|
|
918
|
+
r(null)
|
|
919
|
+
}
|
|
920
|
+
}
|
|
921
|
+
}
|
|
922
|
+
editor.start()
|
|
923
|
+
|
|
924
|
+
// Весь вывод команд (console.log/error) должен идти ВЫШЕ строки ввода,
|
|
925
|
+
// иначе он затирает набираемый текст. Пока редактор активен, заворачиваем
|
|
926
|
+
// оба потока в editor.printAbove.
|
|
927
|
+
const origLog = console.log.bind(console)
|
|
928
|
+
const origErr = console.error.bind(console)
|
|
929
|
+
const fmt = (a) =>
|
|
930
|
+
typeof a === 'string' ? a : (() => {
|
|
931
|
+
try {
|
|
932
|
+
return JSON.stringify(a)
|
|
933
|
+
} catch {
|
|
934
|
+
return String(a)
|
|
935
|
+
}
|
|
936
|
+
})()
|
|
937
|
+
console.log = (...a) => editor.printAbove(a.map(fmt).join(' '))
|
|
938
|
+
console.error = (...a) => editor.printAbove(a.map(fmt).join(' '))
|
|
939
|
+
// Сохраняем на случай отладки.
|
|
940
|
+
editor._origLog = origLog
|
|
941
|
+
editor._origErr = origErr
|
|
942
|
+
}
|
|
943
|
+
|
|
521
944
|
while (running) {
|
|
522
945
|
let input
|
|
523
946
|
try {
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
tail =
|
|
528
|
-
theme.warn('REVIEW') + theme.dim(':') + theme.dir(reviewMode.snapName)
|
|
947
|
+
if (editor) {
|
|
948
|
+
editor.setPrompt(buildPrompt())
|
|
949
|
+
input = await takeInput()
|
|
529
950
|
} else {
|
|
530
|
-
tail
|
|
951
|
+
let tail
|
|
952
|
+
if (reviewMode) {
|
|
953
|
+
tail =
|
|
954
|
+
theme.warn('REVIEW') + theme.dim(':') + theme.dir(reviewMode.snapName)
|
|
955
|
+
} else {
|
|
956
|
+
tail = theme.dir(dirLabel(currentWorkdir))
|
|
957
|
+
}
|
|
958
|
+
input = await promptOnce(theme.prompt('❯ ') + tail + theme.dim(' › '))
|
|
531
959
|
}
|
|
532
|
-
input = await promptOnce(theme.prompt('❯ ') + tail + theme.dim(' › '))
|
|
533
960
|
} catch {
|
|
534
961
|
break
|
|
535
962
|
}
|
|
@@ -661,10 +1088,6 @@ async function main() {
|
|
|
661
1088
|
sendSystemPrompt: false,
|
|
662
1089
|
transcript,
|
|
663
1090
|
onThinking: () => ui.thinking(),
|
|
664
|
-
onAssistantThought: (text) => {
|
|
665
|
-
ui.stop()
|
|
666
|
-
console.log(theme.system('\n💭 ' + text.slice(0, 1200) + '\n'))
|
|
667
|
-
},
|
|
668
1091
|
onToolCall: (name, toolArgs) => ui.toolCall(name, toolArgs),
|
|
669
1092
|
onToolResult: (r) => ui.toolResult(r),
|
|
670
1093
|
onAssistantMessage: (m) => {
|
|
@@ -752,7 +1175,7 @@ async function main() {
|
|
|
752
1175
|
// ---------- Обычные команды ----------
|
|
753
1176
|
|
|
754
1177
|
if (lower === '/chats') {
|
|
755
|
-
const spin = mod.createSpinner()
|
|
1178
|
+
const spin = editor || mod.createSpinner()
|
|
756
1179
|
spin.thinking()
|
|
757
1180
|
try {
|
|
758
1181
|
lastChats = await browser.listChats(30)
|
|
@@ -1004,11 +1427,18 @@ async function main() {
|
|
|
1004
1427
|
transcript.log('user_task', { task: trimmed, workdir: currentWorkdir })
|
|
1005
1428
|
|
|
1006
1429
|
const tools = mod.createTools(currentWorkdir, { undo })
|
|
1007
|
-
|
|
1008
|
-
|
|
1009
|
-
|
|
1010
|
-
|
|
1011
|
-
|
|
1430
|
+
if (editor) editor.busy = true
|
|
1431
|
+
try {
|
|
1432
|
+
await runTask(browser, tools, trimmed, currentWorkdir, {
|
|
1433
|
+
transcript,
|
|
1434
|
+
freshChat: freshChatNext,
|
|
1435
|
+
sendSystemPrompt: sendSystemPromptNext,
|
|
1436
|
+
queue: pendingQueue,
|
|
1437
|
+
ui: editor || null,
|
|
1438
|
+
})
|
|
1439
|
+
} finally {
|
|
1440
|
+
if (editor) editor.busy = false
|
|
1441
|
+
}
|
|
1012
1442
|
|
|
1013
1443
|
freshChatNext = false
|
|
1014
1444
|
sendSystemPromptNext = false
|
|
@@ -1018,6 +1448,7 @@ async function main() {
|
|
|
1018
1448
|
await saveLastChat(currentChatId)
|
|
1019
1449
|
}
|
|
1020
1450
|
|
|
1451
|
+
if (editor) editor.dispose()
|
|
1021
1452
|
await browser.close().catch(() => {})
|
|
1022
1453
|
await mod.closeWeb().catch(() => {})
|
|
1023
1454
|
transcript.close()
|