zames_pro 1.1.1 → 1.2.1
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 +5 -0
- package/package.json +1 -1
- package/src/agent-loop.js +99 -12
- package/src/browser.js +22 -0
- package/src/config.js +1 -0
- package/src/index.js +403 -109
- package/src/input.js +447 -0
- package/src/sessions.js +110 -0
- package/src/spinner.js +60 -2
- 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
|
@@ -8,11 +8,18 @@ import { DeepSeekBrowser } from './browser.js'
|
|
|
8
8
|
import { createTools } from './tools.js'
|
|
9
9
|
import { runAgentLoop } from './agent-loop.js'
|
|
10
10
|
import { createSpinner } from './spinner.js'
|
|
11
|
+
import { LineEditor } from './input.js'
|
|
11
12
|
import { loadConfig, CONFIG_PATHS, ZAMES_HOME } from './config.js'
|
|
12
13
|
import { Transcript } from './transcript.js'
|
|
13
14
|
import { UndoStore } from './undo.js'
|
|
14
15
|
import { selfReview, selfDiff, selfApply, selfList } from './self-review.js'
|
|
15
16
|
import { closeWeb } from './web.js'
|
|
17
|
+
import {
|
|
18
|
+
saveSession,
|
|
19
|
+
loadLastSession,
|
|
20
|
+
listSessions,
|
|
21
|
+
sessionsDir,
|
|
22
|
+
} from './sessions.js'
|
|
16
23
|
|
|
17
24
|
const __dirname = path.dirname(fileURLToPath(import.meta.url))
|
|
18
25
|
|
|
@@ -59,40 +66,26 @@ const chatIdArg = getArg('--chat', null)
|
|
|
59
66
|
// переотправляется (он уже есть в начале чата). Флаг --resend-prompt
|
|
60
67
|
// заставляет дослать его заново — например, если промпт обновился.
|
|
61
68
|
const resendPrompt = hasFlag('--resend-prompt')
|
|
62
|
-
// --new-chat: намеренно начать с чистого чата, игнорируя
|
|
63
|
-
// --resume-last:
|
|
64
|
-
// По умолчанию
|
|
65
|
-
|
|
69
|
+
// --new-chat: намеренно начать с чистого чата, игнорируя сохранённую сессию.
|
|
70
|
+
// --resume-last: оставлен для совместимости (восстановление и так по умолчанию).
|
|
71
|
+
// По умолчанию последняя сессия для рабочей директории восстанавливается.
|
|
72
|
+
const newChatFlag = hasFlag('--new-chat')
|
|
66
73
|
const resumeLastFlag = hasFlag('--resume-last')
|
|
67
74
|
|
|
68
|
-
// ----------
|
|
69
|
-
//
|
|
70
|
-
//
|
|
71
|
-
//
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
+
// ---------- session persistence ----------
|
|
76
|
+
// Сессии (чаты DeepSeek) храним в ~/.zames/.sessions, чтобы они переживали
|
|
77
|
+
// перезапуск процесса. При старте из той же рабочей директории последняя
|
|
78
|
+
// сессия восстанавливается автоматически (если не передан --new-chat).
|
|
79
|
+
// Раньше здесь был один файл last-chat.json, который терялся при смене
|
|
80
|
+
// проекта и не давал списка сессий для восстановления.
|
|
81
|
+
function saveLastChat(id, workdir = '', title = '') {
|
|
75
82
|
if (!id) return
|
|
76
|
-
|
|
77
|
-
await fs.mkdir(ZAMES_HOME, { recursive: true })
|
|
78
|
-
await fs.writeFile(
|
|
79
|
-
LAST_CHAT_FILE,
|
|
80
|
-
JSON.stringify({ id, updatedAt: new Date().toISOString() }, null, 2),
|
|
81
|
-
'utf-8',
|
|
82
|
-
)
|
|
83
|
-
} catch (e) {
|
|
84
|
-
if (debug) console.error('last-chat: не удалось сохранить:', e.message)
|
|
85
|
-
}
|
|
83
|
+
saveSession({ id, workdir, title })
|
|
86
84
|
}
|
|
87
85
|
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
const data = JSON.parse(raw)
|
|
92
|
-
return data && typeof data.id === 'string' ? data.id : null
|
|
93
|
-
} catch {
|
|
94
|
-
return null
|
|
95
|
-
}
|
|
86
|
+
function loadLastChat(workdir = '') {
|
|
87
|
+
const s = loadLastSession(workdir)
|
|
88
|
+
return s ? s.id : null
|
|
96
89
|
}
|
|
97
90
|
|
|
98
91
|
|
|
@@ -189,8 +182,8 @@ ${theme.bold('Опции CLI:')}
|
|
|
189
182
|
--dir <path> рабочая директория агента
|
|
190
183
|
--task <text> задача одной строкой
|
|
191
184
|
--chat <id> продолжить существующий чат по id
|
|
192
|
-
--resume-last вернуться в последний сохранённый чат
|
|
193
|
-
--new-chat начать новый
|
|
185
|
+
--resume-last вернуться в последний сохранённый чат (по умолчанию)
|
|
186
|
+
--new-chat начать новый чат, не восстанавливать сессию
|
|
194
187
|
--resend-prompt дослать system-prompt в существующий чат
|
|
195
188
|
--max-iter <n> лимит итераций (по умолчанию ${config.maxIterations})
|
|
196
189
|
--headless браузер без UI
|
|
@@ -200,8 +193,14 @@ ${theme.bold('Опции CLI:')}
|
|
|
200
193
|
--version, -v показать версию
|
|
201
194
|
--help, -h эта справка
|
|
202
195
|
|
|
196
|
+
${theme.bold('Пока агент работает:')}
|
|
197
|
+
печать + Enter поставить сообщение в очередь (уйдёт после текущей задачи)
|
|
198
|
+
Esc, Ctrl+C прервать текущую генерацию
|
|
199
|
+
|
|
203
200
|
${theme.bold('Обычные команды:')}
|
|
204
201
|
/new, /clear новый чат (сброс контекста)
|
|
202
|
+
/sessions список сохранённых сессий (~/.zames/.sessions)
|
|
203
|
+
/resume-id <id> восстановить сессию по полному id
|
|
205
204
|
/chats список последних чатов DeepSeek
|
|
206
205
|
/resume <n> открыть чат №n из /chats
|
|
207
206
|
/chat показать текущий chat id
|
|
@@ -230,6 +229,7 @@ ${theme.bold('Самообзор (отладка агента):')}
|
|
|
230
229
|
${theme.bold('Файлы:')}
|
|
231
230
|
Логи: ${config.transcript.dir}
|
|
232
231
|
Undo: ~/.zames/undo
|
|
232
|
+
Сессии: ${sessionsDir()}
|
|
233
233
|
Профиль: ~/.zames/profile
|
|
234
234
|
Снапшоты: ~/.zames/snapshots
|
|
235
235
|
Временные: <проект>/tmp (в .gitignore, чистится при запуске)
|
|
@@ -464,29 +464,143 @@ async function promptOnce(question) {
|
|
|
464
464
|
}
|
|
465
465
|
|
|
466
466
|
// Слежение за клавиатурой во время работы агента.
|
|
467
|
-
//
|
|
468
|
-
|
|
467
|
+
//
|
|
468
|
+
// Терминал остаётся живым, пока агент думает:
|
|
469
|
+
// - Esc (или Ctrl+C) — прервать текущую генерацию (клик Stop в браузере);
|
|
470
|
+
// - набор текста + Enter — поставить сообщение в очередь; оно уйдёт
|
|
471
|
+
// агенту сразу после того, как текущая задача завершится
|
|
472
|
+
// (как «отправить во время генерации» в веб-версии DeepSeek).
|
|
473
|
+
//
|
|
474
|
+
// Ввод буферизуется без построчного редактора: набранный текст отображается
|
|
475
|
+
// в строке спиннера через onChange -> ui.setPending(). Enter отправляет буфер
|
|
476
|
+
// в onQueue, пустой Enter игнорируется. Поддержаны Backspace, Ctrl+U, Esc-
|
|
477
|
+
// последовательности (стрелки/Home/End/Delete игнорируются) и bracketed paste.
|
|
478
|
+
function watchInput({ onEscape, onChange, onQueue } = {}) {
|
|
469
479
|
const stdin = process.stdin
|
|
470
|
-
if (!stdin.isTTY) return () => {}
|
|
480
|
+
if (!stdin.isTTY || !stdin.setRawMode) return () => {}
|
|
481
|
+
|
|
482
|
+
// Управляющие символы собираем из кодов: в этом файле нельзя писать
|
|
483
|
+
// «сырые» ESC/CR/LF в строковых литералах (см. AGENTS.md).
|
|
484
|
+
const ESC = String.fromCharCode(27)
|
|
485
|
+
const CSI = String.fromCharCode(91)
|
|
486
|
+
const CR = String.fromCharCode(13)
|
|
487
|
+
const LF = String.fromCharCode(10)
|
|
488
|
+
|
|
471
489
|
const wasRaw = stdin.isRaw
|
|
472
|
-
|
|
490
|
+
stdin.setRawMode(true)
|
|
473
491
|
stdin.resume()
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
492
|
+
process.stdout.write(ESC + '[?2004h')
|
|
493
|
+
|
|
494
|
+
let buf = ''
|
|
495
|
+
let inPaste = false
|
|
496
|
+
const PASTE_START = ESC + '[200~'
|
|
497
|
+
const PASTE_END = ESC + '[201~'
|
|
498
|
+
const CSI_RE = new RegExp('^' + CSI + '[0-9;]*[A-Za-z~]')
|
|
499
|
+
|
|
500
|
+
const emitChange = () => {
|
|
501
|
+
if (onChange) onChange(buf)
|
|
502
|
+
}
|
|
503
|
+
|
|
504
|
+
// Вставленный текст: переводы строк внутри многострочной вставки
|
|
505
|
+
// трактуем как пробелы — сообщение уходит одной строкой.
|
|
506
|
+
const insert = (text) => {
|
|
507
|
+
buf += text
|
|
508
|
+
.split(CR + LF).join(' ')
|
|
509
|
+
.split(CR).join(' ')
|
|
510
|
+
.split(LF).join(' ')
|
|
511
|
+
}
|
|
512
|
+
|
|
513
|
+
function onData(data) {
|
|
514
|
+
let s = data.toString('utf-8')
|
|
515
|
+
|
|
516
|
+
// Одиночный Esc — прервать генерацию. Стрелки приходят целым чанком
|
|
517
|
+
// и сюда не попадают.
|
|
518
|
+
if (!inPaste && s === ESC) {
|
|
519
|
+
if (onEscape) onEscape()
|
|
520
|
+
return
|
|
521
|
+
}
|
|
522
|
+
|
|
523
|
+
while (s.length) {
|
|
524
|
+
if (inPaste) {
|
|
525
|
+
const end = s.indexOf(PASTE_END)
|
|
526
|
+
if (end === -1) {
|
|
527
|
+
insert(s)
|
|
528
|
+
s = ''
|
|
529
|
+
} else {
|
|
530
|
+
insert(s.slice(0, end))
|
|
531
|
+
s = s.slice(end + PASTE_END.length)
|
|
532
|
+
inPaste = false
|
|
533
|
+
}
|
|
534
|
+
emitChange()
|
|
535
|
+
continue
|
|
536
|
+
}
|
|
537
|
+
|
|
538
|
+
const start = s.indexOf(PASTE_START)
|
|
539
|
+
if (start !== -1) {
|
|
540
|
+
const before = s.slice(0, start)
|
|
541
|
+
s = s.slice(start + PASTE_START.length)
|
|
542
|
+
inPaste = true
|
|
543
|
+
if (before) {
|
|
544
|
+
insert(before)
|
|
545
|
+
emitChange()
|
|
546
|
+
}
|
|
547
|
+
continue
|
|
548
|
+
}
|
|
549
|
+
|
|
550
|
+
const ch = s[0]
|
|
551
|
+
const code = s.charCodeAt(0)
|
|
552
|
+
s = s.slice(1)
|
|
553
|
+
|
|
554
|
+
if (ch === CR || ch === LF) {
|
|
555
|
+
const text = buf.trim()
|
|
556
|
+
buf = ''
|
|
557
|
+
emitChange()
|
|
558
|
+
if (text && onQueue) onQueue(text)
|
|
559
|
+
continue
|
|
560
|
+
}
|
|
561
|
+
if (code === 3) {
|
|
562
|
+
// Ctrl+C — как Esc: прервать генерацию.
|
|
563
|
+
if (onEscape) onEscape()
|
|
564
|
+
continue
|
|
565
|
+
}
|
|
566
|
+
if (code === 21) {
|
|
567
|
+
// Ctrl+U — очистить набранное.
|
|
568
|
+
buf = ''
|
|
569
|
+
emitChange()
|
|
570
|
+
continue
|
|
571
|
+
}
|
|
572
|
+
if (code === 127 || code === 8) {
|
|
573
|
+
// Backspace.
|
|
574
|
+
if (buf) {
|
|
575
|
+
buf = buf.slice(0, -1)
|
|
576
|
+
emitChange()
|
|
577
|
+
}
|
|
578
|
+
continue
|
|
579
|
+
}
|
|
580
|
+
if (ch === ESC) {
|
|
581
|
+
// Escape-последовательность (стрелки, Home/End, Delete) — пропускаем.
|
|
582
|
+
const m = s.match(CSI_RE)
|
|
583
|
+
if (m) s = s.slice(m[0].length)
|
|
584
|
+
continue
|
|
585
|
+
}
|
|
586
|
+
if (code < 32) continue // прочие управляющие — игнорируем
|
|
587
|
+
|
|
588
|
+
buf += ch
|
|
589
|
+
emitChange()
|
|
477
590
|
}
|
|
478
591
|
}
|
|
592
|
+
|
|
593
|
+
const handler = (data) => onData(data)
|
|
479
594
|
stdin.on('data', handler)
|
|
480
595
|
return () => {
|
|
481
596
|
stdin.removeListener('data', handler)
|
|
597
|
+
process.stdout.write(ESC + '[?2004l')
|
|
482
598
|
if (stdin.setRawMode) stdin.setRawMode(wasRaw || false)
|
|
483
599
|
}
|
|
484
600
|
}
|
|
485
601
|
|
|
486
602
|
// ---------- workdir resolution ----------
|
|
487
603
|
|
|
488
|
-
// ---------- workdir resolution ----------
|
|
489
|
-
|
|
490
604
|
// Агент работает в директории, из которой его запустили (process.cwd()).
|
|
491
605
|
// Это корень sandbox: инструменты не могут выходить выше него.
|
|
492
606
|
async function resolveWorkdir() {
|
|
@@ -502,47 +616,75 @@ async function resolveWorkdir() {
|
|
|
502
616
|
// ---------- task runner ----------
|
|
503
617
|
|
|
504
618
|
async function runTask(browser, tools, taskText, workdir, opts) {
|
|
505
|
-
const { transcript, freshChat, sendSystemPrompt } = opts
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
const stopWatching =
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
619
|
+
const { transcript, freshChat, sendSystemPrompt, queue = [], ui: editor } = opts
|
|
620
|
+
|
|
621
|
+
// В TTY-режиме UI — это LineEditor: он владеет вводом (очередь, Esc,
|
|
622
|
+
// Ctrl+C) и рисует статус НАД постоянной строкой ввода. В не-TTY режиме
|
|
623
|
+
// (пайпы) — обычный спиннер + watchInput.
|
|
624
|
+
const ui = editor || mod.createSpinner()
|
|
625
|
+
const stopWatching = editor
|
|
626
|
+
? () => {}
|
|
627
|
+
: watchInput({
|
|
628
|
+
onEscape: () => {
|
|
629
|
+
ui.stop()
|
|
630
|
+
console.error(theme.warn('⏹ Esc — прерываю генерацию...'))
|
|
631
|
+
browser.stopGeneration().catch(() => {})
|
|
632
|
+
},
|
|
633
|
+
onChange: (text) => ui.setPending(text),
|
|
634
|
+
onQueue: (text) => {
|
|
635
|
+
queue.push(text)
|
|
636
|
+
ui.setPending(null)
|
|
637
|
+
ui.stop()
|
|
638
|
+
console.log(
|
|
639
|
+
theme.user('📨 В очередь (' + queue.length + '): ') + theme.assistant(text),
|
|
640
|
+
)
|
|
641
|
+
ui.thinking()
|
|
642
|
+
},
|
|
643
|
+
})
|
|
518
644
|
|
|
519
645
|
try {
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
646
|
+
let next = { task: taskText, freshChat, sendSystemPrompt }
|
|
647
|
+
|
|
648
|
+
// Выполняем задачу, затем — всё, что пользователь успел напечатать за
|
|
649
|
+
// время работы. Очередь может пополняться прямо во время дренажа.
|
|
650
|
+
while (true) {
|
|
651
|
+
ui.thinking()
|
|
652
|
+
await mod.runAgentLoop({
|
|
653
|
+
browser,
|
|
654
|
+
tools,
|
|
655
|
+
task: next.task,
|
|
656
|
+
workdir,
|
|
657
|
+
maxIterations: maxIter,
|
|
658
|
+
freshChat: next.freshChat,
|
|
659
|
+
sendSystemPrompt: next.sendSystemPrompt,
|
|
660
|
+
transcript,
|
|
661
|
+
onThinking: () => ui.thinking(),
|
|
662
|
+
onToolCall: (name, toolArgs) => ui.toolCall(name, toolArgs),
|
|
663
|
+
onToolResult: (result) => ui.toolResult(result),
|
|
664
|
+
onAssistantMessage: (msg) => ui.assistant(msg),
|
|
665
|
+
debugLog: debug,
|
|
666
|
+
})
|
|
667
|
+
|
|
668
|
+
if (!queue.length) break
|
|
669
|
+
|
|
670
|
+
const queued = queue.shift()
|
|
671
|
+
ui.stop()
|
|
672
|
+
if (editor) {
|
|
673
|
+
editor.printAbove(theme.user('▶ Из очереди: ') + theme.assistant(queued))
|
|
674
|
+
} else {
|
|
675
|
+
console.log(theme.user('▶ Из очереди: ') + theme.assistant(queued))
|
|
676
|
+
}
|
|
677
|
+
transcript?.log('queued_task', { task: queued })
|
|
678
|
+
next = { task: queued, freshChat: false, sendSystemPrompt: false }
|
|
679
|
+
}
|
|
538
680
|
} catch (e) {
|
|
539
681
|
ui.stop()
|
|
540
|
-
console.error(theme.error('
|
|
682
|
+
console.error(theme.error(String.fromCharCode(10) + '✖ Ошибка агента:'), e.message)
|
|
541
683
|
if (debug) console.error(e.stack)
|
|
542
684
|
transcript?.log('agent_error', { error: e.message })
|
|
543
685
|
} finally {
|
|
544
686
|
stopWatching()
|
|
545
|
-
|
|
687
|
+
ui.stop()
|
|
546
688
|
}
|
|
547
689
|
}
|
|
548
690
|
|
|
@@ -624,13 +766,15 @@ async function main() {
|
|
|
624
766
|
let freshChat = true
|
|
625
767
|
let sendSystemPrompt = true
|
|
626
768
|
|
|
627
|
-
// По умолчанию
|
|
628
|
-
//
|
|
629
|
-
// --resume-last вернуться в последний сохранённый чат
|
|
769
|
+
// По умолчанию восстанавливаем последнюю сессию для этой директории.
|
|
770
|
+
// Начать с чистого листа — --new-chat.
|
|
630
771
|
let resumeId = chatIdArg
|
|
631
|
-
if (!resumeId &&
|
|
632
|
-
|
|
633
|
-
if (
|
|
772
|
+
if (!resumeId && !newChatFlag) {
|
|
773
|
+
const last = loadLastSession(currentWorkdir)
|
|
774
|
+
if (last && last.id) {
|
|
775
|
+
resumeId = last.id
|
|
776
|
+
console.log(theme.system(`Восстанавливаю сессию ${resumeId}...`))
|
|
777
|
+
}
|
|
634
778
|
}
|
|
635
779
|
|
|
636
780
|
if (resumeId) {
|
|
@@ -639,6 +783,7 @@ async function main() {
|
|
|
639
783
|
await browser.openChat(resumeId)
|
|
640
784
|
freshChat = false
|
|
641
785
|
sendSystemPrompt = resendPrompt
|
|
786
|
+
saveLastChat(resumeId, currentWorkdir)
|
|
642
787
|
} catch (e) {
|
|
643
788
|
console.error(theme.error(`Не удалось открыть чат: ${e.message}`))
|
|
644
789
|
}
|
|
@@ -659,7 +804,12 @@ async function main() {
|
|
|
659
804
|
|
|
660
805
|
console.log(
|
|
661
806
|
theme.system(
|
|
662
|
-
'Интерактивный режим. Введите задачу. Команды — /help. Выход — /exit
|
|
807
|
+
'Интерактивный режим. Введите задачу. Команды — /help. Выход — /exit.',
|
|
808
|
+
),
|
|
809
|
+
)
|
|
810
|
+
console.log(
|
|
811
|
+
theme.system(
|
|
812
|
+
'Пока агент работает, можно печатать следующее сообщение — оно уйдёт в очередь (Enter — отправить, Esc — прервать).',
|
|
663
813
|
),
|
|
664
814
|
)
|
|
665
815
|
|
|
@@ -669,6 +819,10 @@ async function main() {
|
|
|
669
819
|
let currentChatId = null
|
|
670
820
|
let running = true
|
|
671
821
|
|
|
822
|
+
// Сообщения, набранные пользователем, пока агент работал. runTask
|
|
823
|
+
// забирает их по одному после завершения текущей задачи.
|
|
824
|
+
const pendingQueue = []
|
|
825
|
+
|
|
672
826
|
// ---------- review mode state ----------
|
|
673
827
|
// null — обычный режим.
|
|
674
828
|
// { snapDir, snapName, originalWorkdir } — мы внутри снапшота, чат уже
|
|
@@ -686,13 +840,23 @@ async function main() {
|
|
|
686
840
|
process.exit(0)
|
|
687
841
|
})
|
|
688
842
|
|
|
689
|
-
//
|
|
843
|
+
// Восстановление сессии при старте. По умолчанию возвращаемся в последний
|
|
844
|
+
// чат для этой рабочей директории (сессии лежат в ~/.zames/.sessions),
|
|
845
|
+
// чтобы контекст не терялся после перезапуска. Явно начать с чистого листа
|
|
846
|
+
// можно флагом --new-chat.
|
|
690
847
|
// --chat <id> открыть конкретный чат
|
|
691
|
-
// --
|
|
848
|
+
// --new-chat не восстанавливать, начать новый чат
|
|
692
849
|
let resumeId = chatIdArg
|
|
693
|
-
if (!resumeId &&
|
|
694
|
-
|
|
695
|
-
if (
|
|
850
|
+
if (!resumeId && !newChatFlag) {
|
|
851
|
+
const last = loadLastSession(currentWorkdir)
|
|
852
|
+
if (last && last.id) {
|
|
853
|
+
resumeId = last.id
|
|
854
|
+
console.log(
|
|
855
|
+
theme.system(
|
|
856
|
+
`Восстанавливаю сессию ${last.id}${last.title ? ` (${last.title})` : ''}...`,
|
|
857
|
+
),
|
|
858
|
+
)
|
|
859
|
+
}
|
|
696
860
|
}
|
|
697
861
|
|
|
698
862
|
if (resumeId) {
|
|
@@ -702,25 +866,105 @@ async function main() {
|
|
|
702
866
|
currentChatId = resumeId
|
|
703
867
|
freshChatNext = false
|
|
704
868
|
sendSystemPromptNext = resendPrompt
|
|
705
|
-
|
|
869
|
+
saveLastChat(resumeId, currentWorkdir)
|
|
706
870
|
console.log(theme.system(`Чат открыт: ${resumeId}\n`))
|
|
707
871
|
} catch (e) {
|
|
708
872
|
console.error(theme.error(`Не удалось открыть чат: ${e.message}`))
|
|
709
873
|
}
|
|
710
874
|
}
|
|
711
875
|
|
|
876
|
+
// ---------- ввод: постоянная строка внизу + статус сверху ----------
|
|
877
|
+
// В TTY используем LineEditor: он владеет вводом всё время, показывает
|
|
878
|
+
// статус над строкой ввода и печатает ответы агента ВЫШЕ неё, поэтому
|
|
879
|
+
// набранный текст никогда не затирается выводом. В не-TTY (пайп) —
|
|
880
|
+
// старый promptOnce.
|
|
881
|
+
let editor = null
|
|
882
|
+
let waiter = null
|
|
883
|
+
const takeInput = () => {
|
|
884
|
+
if (pendingQueue.length) return Promise.resolve(pendingQueue.shift())
|
|
885
|
+
return new Promise((resolve) => {
|
|
886
|
+
waiter = resolve
|
|
887
|
+
})
|
|
888
|
+
}
|
|
889
|
+
|
|
890
|
+
const buildPrompt = () => {
|
|
891
|
+
let tail
|
|
892
|
+
if (reviewMode) {
|
|
893
|
+
tail = theme.warn('REVIEW') + theme.dim(':') + theme.dir(reviewMode.snapName)
|
|
894
|
+
} else {
|
|
895
|
+
tail = theme.dir(dirLabel(currentWorkdir))
|
|
896
|
+
}
|
|
897
|
+
return theme.prompt('❯ ') + tail + theme.dim(' › ')
|
|
898
|
+
}
|
|
899
|
+
|
|
900
|
+
if (process.stdin.isTTY && process.stdout.isTTY) {
|
|
901
|
+
editor = new LineEditor({ prompt: buildPrompt() })
|
|
902
|
+
editor.onSubmit = (text) => {
|
|
903
|
+
pendingQueue.push(text)
|
|
904
|
+
if (waiter) {
|
|
905
|
+
const r = waiter
|
|
906
|
+
waiter = null
|
|
907
|
+
r(pendingQueue.shift())
|
|
908
|
+
}
|
|
909
|
+
}
|
|
910
|
+
editor.onEscape = () => {
|
|
911
|
+
if (editor.busy) {
|
|
912
|
+
editor.printAbove(theme.warn('⏹ Esc — прерываю генерацию...'))
|
|
913
|
+
browser.stopGeneration().catch(() => {})
|
|
914
|
+
}
|
|
915
|
+
}
|
|
916
|
+
editor.onCtrlC = () => {
|
|
917
|
+
if (editor.busy) {
|
|
918
|
+
editor.printAbove(theme.warn('⏹ Ctrl+C — прерываю генерацию...'))
|
|
919
|
+
browser.stopGeneration().catch(() => {})
|
|
920
|
+
} else {
|
|
921
|
+
// Не заняты — выходим. Будим takeInput(), чтобы цикл завершился.
|
|
922
|
+
running = false
|
|
923
|
+
if (waiter) {
|
|
924
|
+
const r = waiter
|
|
925
|
+
waiter = null
|
|
926
|
+
r(null)
|
|
927
|
+
}
|
|
928
|
+
}
|
|
929
|
+
}
|
|
930
|
+
editor.start()
|
|
931
|
+
|
|
932
|
+
// Весь вывод команд (console.log/error) должен идти ВЫШЕ строки ввода,
|
|
933
|
+
// иначе он затирает набираемый текст. Пока редактор активен, заворачиваем
|
|
934
|
+
// оба потока в editor.printAbove.
|
|
935
|
+
const origLog = console.log.bind(console)
|
|
936
|
+
const origErr = console.error.bind(console)
|
|
937
|
+
const fmt = (a) =>
|
|
938
|
+
typeof a === 'string' ? a : (() => {
|
|
939
|
+
try {
|
|
940
|
+
return JSON.stringify(a)
|
|
941
|
+
} catch {
|
|
942
|
+
return String(a)
|
|
943
|
+
}
|
|
944
|
+
})()
|
|
945
|
+
console.log = (...a) => editor.printAbove(a.map(fmt).join(' '))
|
|
946
|
+
console.error = (...a) => editor.printAbove(a.map(fmt).join(' '))
|
|
947
|
+
// Сохраняем на случай отладки.
|
|
948
|
+
editor._origLog = origLog
|
|
949
|
+
editor._origErr = origErr
|
|
950
|
+
}
|
|
951
|
+
|
|
712
952
|
while (running) {
|
|
713
953
|
let input
|
|
714
954
|
try {
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
tail =
|
|
719
|
-
theme.warn('REVIEW') + theme.dim(':') + theme.dir(reviewMode.snapName)
|
|
955
|
+
if (editor) {
|
|
956
|
+
editor.setPrompt(buildPrompt())
|
|
957
|
+
input = await takeInput()
|
|
720
958
|
} else {
|
|
721
|
-
tail
|
|
959
|
+
let tail
|
|
960
|
+
if (reviewMode) {
|
|
961
|
+
tail =
|
|
962
|
+
theme.warn('REVIEW') + theme.dim(':') + theme.dir(reviewMode.snapName)
|
|
963
|
+
} else {
|
|
964
|
+
tail = theme.dir(dirLabel(currentWorkdir))
|
|
965
|
+
}
|
|
966
|
+
input = await promptOnce(theme.prompt('❯ ') + tail + theme.dim(' › '))
|
|
722
967
|
}
|
|
723
|
-
input = await promptOnce(theme.prompt('❯ ') + tail + theme.dim(' › '))
|
|
724
968
|
} catch {
|
|
725
969
|
break
|
|
726
970
|
}
|
|
@@ -744,7 +988,7 @@ async function main() {
|
|
|
744
988
|
freshChatNext = false
|
|
745
989
|
sendSystemPromptNext = true
|
|
746
990
|
currentChatId = await browser.getCurrentChatId()
|
|
747
|
-
|
|
991
|
+
saveLastChat(currentChatId, currentWorkdir)
|
|
748
992
|
transcript.log('new_chat')
|
|
749
993
|
console.log(theme.system('Новый чат.\n'))
|
|
750
994
|
} catch (e) {
|
|
@@ -784,7 +1028,7 @@ async function main() {
|
|
|
784
1028
|
freshChatNext = false
|
|
785
1029
|
sendSystemPromptNext = false
|
|
786
1030
|
currentChatId = await browser.getCurrentChatId()
|
|
787
|
-
|
|
1031
|
+
saveLastChat(currentChatId, currentWorkdir)
|
|
788
1032
|
|
|
789
1033
|
console.log(
|
|
790
1034
|
theme.user(
|
|
@@ -852,10 +1096,6 @@ async function main() {
|
|
|
852
1096
|
sendSystemPrompt: false,
|
|
853
1097
|
transcript,
|
|
854
1098
|
onThinking: () => ui.thinking(),
|
|
855
|
-
onAssistantThought: (text) => {
|
|
856
|
-
ui.stop()
|
|
857
|
-
console.log(theme.system('\n💭 ' + text.slice(0, 1200) + '\n'))
|
|
858
|
-
},
|
|
859
1099
|
onToolCall: (name, toolArgs) => ui.toolCall(name, toolArgs),
|
|
860
1100
|
onToolResult: (r) => ui.toolResult(r),
|
|
861
1101
|
onAssistantMessage: (m) => {
|
|
@@ -873,7 +1113,7 @@ async function main() {
|
|
|
873
1113
|
freshChatNext = false
|
|
874
1114
|
sendSystemPromptNext = false
|
|
875
1115
|
currentChatId = await browser.getCurrentChatId()
|
|
876
|
-
|
|
1116
|
+
saveLastChat(currentChatId, currentWorkdir)
|
|
877
1117
|
|
|
878
1118
|
console.log(
|
|
879
1119
|
theme.user(
|
|
@@ -943,7 +1183,7 @@ async function main() {
|
|
|
943
1183
|
// ---------- Обычные команды ----------
|
|
944
1184
|
|
|
945
1185
|
if (lower === '/chats') {
|
|
946
|
-
const spin = mod.createSpinner()
|
|
1186
|
+
const spin = editor || mod.createSpinner()
|
|
947
1187
|
spin.thinking()
|
|
948
1188
|
try {
|
|
949
1189
|
lastChats = await browser.listChats(30)
|
|
@@ -1000,7 +1240,7 @@ async function main() {
|
|
|
1000
1240
|
currentChatId = pick.id
|
|
1001
1241
|
freshChatNext = false
|
|
1002
1242
|
sendSystemPromptNext = resendPrompt
|
|
1003
|
-
|
|
1243
|
+
saveLastChat(pick.id, currentWorkdir, pick.title)
|
|
1004
1244
|
transcript.log('resume_chat', { id: pick.id, title: pick.title })
|
|
1005
1245
|
console.log(
|
|
1006
1246
|
theme.assistant(`Чат открыт.`) +
|
|
@@ -1031,6 +1271,51 @@ async function main() {
|
|
|
1031
1271
|
continue
|
|
1032
1272
|
}
|
|
1033
1273
|
|
|
1274
|
+
if (lower === '/sessions' || lower === '/session') {
|
|
1275
|
+
const all = listSessions()
|
|
1276
|
+
console.log(theme.system(`Папка сессий: ${sessionsDir()}`))
|
|
1277
|
+
if (!all.length) {
|
|
1278
|
+
console.log(
|
|
1279
|
+
theme.system(
|
|
1280
|
+
'Сохранённых сессий нет. Они появятся после первой задачи/чата.',
|
|
1281
|
+
),
|
|
1282
|
+
)
|
|
1283
|
+
} else {
|
|
1284
|
+
all.forEach((s, i) => {
|
|
1285
|
+
const n = String(i + 1).padStart(2, ' ')
|
|
1286
|
+
const mark = s.id === currentChatId ? theme.user(' *') : ''
|
|
1287
|
+
const title = s.title ? ` ${s.title}` : ''
|
|
1288
|
+
const wd = s.workdir ? theme.dim(` [${dirLabel(s.workdir)}]`) : ''
|
|
1289
|
+
console.log(` ${theme.user(n)}. ${s.id.slice(0, 8)}…${title}${wd}${mark}`)
|
|
1290
|
+
})
|
|
1291
|
+
console.log(
|
|
1292
|
+
theme.system('Восстановить: /resume-id <id> (полный id) или /resume <n> после /chats.'),
|
|
1293
|
+
)
|
|
1294
|
+
}
|
|
1295
|
+
continue
|
|
1296
|
+
}
|
|
1297
|
+
|
|
1298
|
+
if (lower.startsWith('/resume-id ')) {
|
|
1299
|
+
const id = trimmed.slice('/resume-id'.length).trim()
|
|
1300
|
+
if (!id) {
|
|
1301
|
+
console.error(theme.error('Использование: /resume-id <chat id>'))
|
|
1302
|
+
continue
|
|
1303
|
+
}
|
|
1304
|
+
try {
|
|
1305
|
+
console.log(theme.system(`Открываю чат ${id}...`))
|
|
1306
|
+
await browser.openChat(id)
|
|
1307
|
+
currentChatId = id
|
|
1308
|
+
freshChatNext = false
|
|
1309
|
+
sendSystemPromptNext = resendPrompt
|
|
1310
|
+
saveLastChat(id, currentWorkdir)
|
|
1311
|
+
transcript.log('resume_chat', { id })
|
|
1312
|
+
console.log(theme.assistant('Чат открыт.') + theme.system(String.fromCharCode(10)))
|
|
1313
|
+
} catch (e) {
|
|
1314
|
+
console.error(theme.error('Не удалось открыть чат:'), e.message)
|
|
1315
|
+
}
|
|
1316
|
+
continue
|
|
1317
|
+
}
|
|
1318
|
+
|
|
1034
1319
|
if (lower === '/pwd') {
|
|
1035
1320
|
console.log(theme.system(currentWorkdir))
|
|
1036
1321
|
continue
|
|
@@ -1081,8 +1366,9 @@ async function main() {
|
|
|
1081
1366
|
theme.system(`Resend prompt (--resend-prompt): ${resendPrompt ? 'да' : 'нет'}`),
|
|
1082
1367
|
)
|
|
1083
1368
|
console.log(
|
|
1084
|
-
theme.system(`Last chat: ${
|
|
1369
|
+
theme.system(`Last chat: ${loadLastChat(currentWorkdir) || 'нет'}`),
|
|
1085
1370
|
)
|
|
1371
|
+
console.log(theme.system(`Сессии: ${sessionsDir()}`))
|
|
1086
1372
|
console.log(
|
|
1087
1373
|
theme.system(`Dev mode (auto-reload): ${devMode ? 'вкл' : 'выкл'}`),
|
|
1088
1374
|
)
|
|
@@ -1195,20 +1481,28 @@ async function main() {
|
|
|
1195
1481
|
transcript.log('user_task', { task: trimmed, workdir: currentWorkdir })
|
|
1196
1482
|
|
|
1197
1483
|
const tools = mod.createTools(currentWorkdir, { undo })
|
|
1198
|
-
|
|
1199
|
-
|
|
1200
|
-
|
|
1201
|
-
|
|
1202
|
-
|
|
1484
|
+
if (editor) editor.busy = true
|
|
1485
|
+
try {
|
|
1486
|
+
await runTask(browser, tools, trimmed, currentWorkdir, {
|
|
1487
|
+
transcript,
|
|
1488
|
+
freshChat: freshChatNext,
|
|
1489
|
+
sendSystemPrompt: sendSystemPromptNext,
|
|
1490
|
+
queue: pendingQueue,
|
|
1491
|
+
ui: editor || null,
|
|
1492
|
+
})
|
|
1493
|
+
} finally {
|
|
1494
|
+
if (editor) editor.busy = false
|
|
1495
|
+
}
|
|
1203
1496
|
|
|
1204
1497
|
freshChatNext = false
|
|
1205
1498
|
sendSystemPromptNext = false
|
|
1206
1499
|
if (!currentChatId) {
|
|
1207
1500
|
currentChatId = await browser.getCurrentChatId()
|
|
1208
1501
|
}
|
|
1209
|
-
|
|
1502
|
+
saveLastChat(currentChatId, currentWorkdir)
|
|
1210
1503
|
}
|
|
1211
1504
|
|
|
1505
|
+
if (editor) editor.dispose()
|
|
1212
1506
|
await browser.close().catch(() => {})
|
|
1213
1507
|
await mod.closeWeb().catch(() => {})
|
|
1214
1508
|
transcript.close()
|