zames_pro 2.6.0 → 2.8.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 +21 -0
- package/dist/agent-loop.js +141 -83
- package/dist/attachments.js +267 -0
- package/dist/browser.js +139 -50
- package/dist/config-menu.js +25 -25
- package/dist/config.js +5 -5
- package/dist/gitTools.js +4 -4
- package/dist/i18n.js +31 -7
- package/dist/index.js +295 -168
- package/dist/input.js +237 -83
- package/dist/markdown.js +1 -1
- package/dist/net-capture.js +20 -20
- package/dist/self-review.js +16 -16
- package/dist/sessions.js +11 -11
- package/dist/spinner.js +20 -16
- package/dist/system-prompt.js +29 -2
- package/dist/theme.js +26 -26
- package/dist/tools.js +9 -9
- package/dist/transcript.js +2 -2
- package/dist/types.js +2 -2
- package/dist/web.js +16 -16
- package/dist/xml-toolcall.js +89 -14
- package/package.json +1 -1
- package/scripts/postinstall.mjs +78 -51
package/dist/index.js
CHANGED
|
@@ -7,7 +7,8 @@ import { DeepSeekBrowser } from './browser.js';
|
|
|
7
7
|
import { createTools } from './tools.js';
|
|
8
8
|
import { runAgentLoop } from './agent-loop.js';
|
|
9
9
|
import { createSpinner } from './spinner.js';
|
|
10
|
-
import { LineEditor } from './input.js';
|
|
10
|
+
import { LineEditor, expandPastes, pasteReplacement } from './input.js';
|
|
11
|
+
import { parseImagePaste, extForMime, saveToTemp, guessMime, isImageName, formatSize, looksLikeFilePath, readClipboardImageDetailed, sniffMime, } from './attachments.js';
|
|
11
12
|
import { loadConfig, DEFAULTS, CONFIG_PATHS, ZAMES_HOME, CONFIG_SCHEMA, getByPath, validateConfigValue, writeConfigValue, resetConfigValue, } from './config.js';
|
|
12
13
|
import { runConfigMenu } from './config-menu.js';
|
|
13
14
|
import { translate, normalizeLocale, localeDisplayName, isLocale, } from './i18n.js';
|
|
@@ -41,12 +42,12 @@ function getPositional() {
|
|
|
41
42
|
return positional;
|
|
42
43
|
}
|
|
43
44
|
const config = loadConfig();
|
|
44
|
-
//
|
|
45
|
+
// Current interface/agent language. Changed by the /config lang <ru|en> command.
|
|
45
46
|
let currentLocale = isLocale(config.ui?.locale)
|
|
46
47
|
? config.ui.locale
|
|
47
48
|
: 'ru';
|
|
48
|
-
//
|
|
49
|
-
//
|
|
49
|
+
// Translation: reads currentLocale at call time, so a language change takes
|
|
50
|
+
// effect immediately, without a restart (for lines printed afterwards).
|
|
50
51
|
const t = (key, params) => translate(currentLocale)(key, params);
|
|
51
52
|
const headless = hasFlag('--headless') || config.headless;
|
|
52
53
|
const debug = hasFlag('--debug') || config.debug;
|
|
@@ -56,21 +57,21 @@ const maxIter = Number(getArg('--max-iter', String(config.maxIterations))) ||
|
|
|
56
57
|
const positional = getPositional();
|
|
57
58
|
const task = getArg('--task', positional.join(' ').trim() || null);
|
|
58
59
|
const chatIdArg = getArg('--chat', null);
|
|
59
|
-
//
|
|
60
|
-
//
|
|
61
|
-
//
|
|
60
|
+
// When resuming an existing chat, the system-prompt is by default NOT
|
|
61
|
+
// resent (it is already at the start of the chat). The --resend-prompt flag
|
|
62
|
+
// forces resending it — for example, if the prompt was updated.
|
|
62
63
|
const resendPrompt = hasFlag('--resend-prompt');
|
|
63
|
-
//
|
|
64
|
-
// --resume-last:
|
|
65
|
-
// --new-chat:
|
|
64
|
+
// By default a NEW chat is started on launch (context is not carried over).
|
|
65
|
+
// --resume-last: return to the last session for the working directory.
|
|
66
|
+
// --new-chat: kept for compatibility — this is the default behavior anyway.
|
|
66
67
|
const newChatFlag = hasFlag('--new-chat');
|
|
67
68
|
const resumeLastFlag = hasFlag('--resume-last');
|
|
68
69
|
// ---------- session persistence ----------
|
|
69
|
-
//
|
|
70
|
-
//
|
|
71
|
-
// --chat <id>, /resume <n>).
|
|
72
|
-
//
|
|
73
|
-
//
|
|
70
|
+
// We store sessions (DeepSeek chats) in ~/.zames/.sessions so they survive
|
|
71
|
+
// a restart and are available for explicit restore (--resume-last,
|
|
72
|
+
// --chat <id>, /resume <n>). They are NOT brought up automatically on launch.
|
|
73
|
+
// Previously there was a single last-chat.json file that got lost when the
|
|
74
|
+
// project changed and gave no list of sessions to restore.
|
|
74
75
|
function saveLastChat(id, workdir = '', title = '') {
|
|
75
76
|
if (!id)
|
|
76
77
|
return;
|
|
@@ -81,13 +82,13 @@ function loadLastChat(workdir = '') {
|
|
|
81
82
|
return s ? s.id : null;
|
|
82
83
|
}
|
|
83
84
|
// ---------- hot reload ----------
|
|
84
|
-
//
|
|
85
|
-
//
|
|
86
|
-
//
|
|
87
|
-
//
|
|
88
|
-
//
|
|
89
|
-
//
|
|
90
|
-
//
|
|
85
|
+
// We keep references to the logic modules in the mod object. The /reload
|
|
86
|
+
// command re-reads them via a dynamic import with a timestamp query — Node
|
|
87
|
+
// caches ESM by URL, so such an import returns a fresh version of the module.
|
|
88
|
+
// The browser, chat and current state are NOT restarted: only the logic is updated.
|
|
89
|
+
// In dev mode tsx loads the .ts sources from src/, and in the built dist — .js.
|
|
90
|
+
// A dynamic import with the ?t= query bypasses tsx's extension rewriting,
|
|
91
|
+
// so we pick the extension ourselves — from the actual file of the current module.
|
|
91
92
|
const SRC_EXT = /[.]ts$/.test(new URL(import.meta.url).pathname) ? '.ts' : '.js';
|
|
92
93
|
const RELOADABLE = [
|
|
93
94
|
'tools',
|
|
@@ -151,13 +152,13 @@ async function reloadModules() {
|
|
|
151
152
|
mod.createSpinner = pick('spinner', 'createSpinner');
|
|
152
153
|
return { count: loaded.size, errors };
|
|
153
154
|
}
|
|
154
|
-
//
|
|
155
|
+
// Initial load so that mod.buildSystemPrompt and the rest are populated.
|
|
155
156
|
await reloadModules();
|
|
156
|
-
// Dev
|
|
157
|
-
//
|
|
158
|
-
//
|
|
157
|
+
// Dev mode: auto-reload of the logic modules before each task.
|
|
158
|
+
// Enabled by the --dev flag (`npm run dev` sets it) or config.hotReload === true.
|
|
159
|
+
// In normal mode (npm start, global zames) auto-reload is off.
|
|
159
160
|
const devMode = hasFlag('--dev') || config.hotReload === true;
|
|
160
|
-
//
|
|
161
|
+
// Safe auto-reload: on a load error we keep the previous working modules.
|
|
161
162
|
async function autoReload() {
|
|
162
163
|
if (!devMode)
|
|
163
164
|
return;
|
|
@@ -195,6 +196,7 @@ ${theme.bold(t('help.while_working'))}
|
|
|
195
196
|
${t('help.key.slash')}
|
|
196
197
|
${t('help.key.newline')}
|
|
197
198
|
${t('help.key.backslash')}
|
|
199
|
+
${t('help.key.attach')}
|
|
198
200
|
${t('help.key.esc')}
|
|
199
201
|
|
|
200
202
|
${theme.bold(t('help.commands'))}
|
|
@@ -239,8 +241,8 @@ ${theme.bold(t('help.files'))}
|
|
|
239
241
|
function dirLabel(p) {
|
|
240
242
|
return path.basename(p) || p;
|
|
241
243
|
}
|
|
242
|
-
//
|
|
243
|
-
//
|
|
244
|
+
// List of slash commands for completion when you type «/» (Tab — complete).
|
|
245
|
+
// The description is localized by the help.cmd.* key at display time — see
|
|
244
246
|
// buildSlashCommands().
|
|
245
247
|
const SLASH_COMMANDS = [
|
|
246
248
|
{ name: '/help', key: 'help.cmd.help' },
|
|
@@ -269,16 +271,37 @@ const SLASH_COMMANDS = [
|
|
|
269
271
|
{ name: '/exit', key: 'help.cmd.exit' },
|
|
270
272
|
{ name: '/quit', key: 'help.cmd.exit' },
|
|
271
273
|
];
|
|
272
|
-
//
|
|
274
|
+
// Slash-command descriptions in the current language (for LineEditor hints).
|
|
273
275
|
function buildSlashCommands() {
|
|
274
276
|
return SLASH_COMMANDS.map((c) => ({
|
|
275
277
|
name: c.name,
|
|
276
278
|
description: t(c.key),
|
|
277
279
|
}));
|
|
278
280
|
}
|
|
279
|
-
//
|
|
280
|
-
//
|
|
281
|
+
// We put the agent's temporary files (one-off scripts, etc.) in
|
|
282
|
+
// <project>/tmp — this folder is in .gitignore and is cleaned on every launch.
|
|
281
283
|
const TMP_DIR = path.join(__dirname, '..', 'tmp');
|
|
284
|
+
// Resolve a pasted string into a path to an existing file. Handles quoted
|
|
285
|
+
// paths (drag&drop from some file managers adds quotes) and paths relative to
|
|
286
|
+
// the working directory.
|
|
287
|
+
async function resolveAttachPath(workdir, raw) {
|
|
288
|
+
let s = String(raw || '').trim();
|
|
289
|
+
if (!s)
|
|
290
|
+
return null;
|
|
291
|
+
if ((s.startsWith('"') && s.endsWith('"')) || (s.startsWith("'") && s.endsWith("'"))) {
|
|
292
|
+
s = s.slice(1, -1);
|
|
293
|
+
}
|
|
294
|
+
s = s.replace(/\\ /g, ' ');
|
|
295
|
+
if (!looksLikeFilePath(s) && !isImageName(s))
|
|
296
|
+
return null;
|
|
297
|
+
const candidates = path.isAbsolute(s) ? [s] : [path.resolve(workdir, s)];
|
|
298
|
+
for (const c of candidates) {
|
|
299
|
+
const st = await fs.stat(c).catch(() => null);
|
|
300
|
+
if (st && st.isFile())
|
|
301
|
+
return c;
|
|
302
|
+
}
|
|
303
|
+
return null;
|
|
304
|
+
}
|
|
282
305
|
async function cleanTmpDir() {
|
|
283
306
|
try {
|
|
284
307
|
await fs.rm(TMP_DIR, { recursive: true, force: true });
|
|
@@ -289,23 +312,22 @@ async function cleanTmpDir() {
|
|
|
289
312
|
console.error('tmp: не удалось очистить:', e.message);
|
|
290
313
|
}
|
|
291
314
|
}
|
|
292
|
-
//
|
|
293
|
-
// Ctrl+Shift+V,
|
|
315
|
+
// Terminal line input with correct paste handling (Shift+Insert,
|
|
316
|
+
// Ctrl+Shift+V, right mouse button, etc.).
|
|
294
317
|
//
|
|
295
|
-
//
|
|
296
|
-
// 1) readline
|
|
297
|
-
//
|
|
298
|
-
//
|
|
299
|
-
//
|
|
300
|
-
//
|
|
301
|
-
//
|
|
302
|
-
//
|
|
303
|
-
//
|
|
304
|
-
// не считается отправкой.
|
|
318
|
+
// Why a custom reader instead of readline:
|
|
319
|
+
// 1) readline submits the line on the FIRST newline. When pasting
|
|
320
|
+
// multiline text this caused immediate submission and only the first
|
|
321
|
+
// line went to the chat. Here newlines inside a paste are replaced with
|
|
322
|
+
// spaces, and submission happens only on a single Enter press.
|
|
323
|
+
// 2) We enable bracketed paste mode (\x1b[?2004h): the terminal wraps the
|
|
324
|
+
// pasted text in the markers \x1b[200~ … \x1b[201~, so we know for sure
|
|
325
|
+
// that this is a paste, not keyboard typing, and Enter inside it is not
|
|
326
|
+
// treated as submission.
|
|
305
327
|
async function promptOnce(question) {
|
|
306
328
|
const stdin = process.stdin;
|
|
307
329
|
const stdout = process.stdout;
|
|
308
|
-
//
|
|
330
|
+
// Non-TTY (pipe, redirect): read everything up to EOF as a single string.
|
|
309
331
|
if (!stdin.isTTY || !stdin.setRawMode) {
|
|
310
332
|
const chunks = [];
|
|
311
333
|
return await new Promise((resolve) => {
|
|
@@ -323,7 +345,7 @@ async function promptOnce(question) {
|
|
|
323
345
|
const wasRaw = stdin.isRaw;
|
|
324
346
|
stdin.setRawMode(true);
|
|
325
347
|
stdin.resume();
|
|
326
|
-
//
|
|
348
|
+
// We enable/disable bracketed paste in pairs.
|
|
327
349
|
stdout.write('\x1b[?2004h');
|
|
328
350
|
stdout.write(question);
|
|
329
351
|
let line = '';
|
|
@@ -332,11 +354,11 @@ async function promptOnce(question) {
|
|
|
332
354
|
const PASTE_START = '\x1b[200~';
|
|
333
355
|
const PASTE_END = '\x1b[201~';
|
|
334
356
|
const redraw = () => {
|
|
335
|
-
//
|
|
357
|
+
// Return to the start of the line, erase and print again.
|
|
336
358
|
stdout.write(String.fromCharCode(13));
|
|
337
359
|
stdout.write('\x1b[K');
|
|
338
360
|
stdout.write(question + line);
|
|
339
|
-
//
|
|
361
|
+
// Put the cursor in the right position.
|
|
340
362
|
const back = line.length - cursor;
|
|
341
363
|
if (back > 0)
|
|
342
364
|
stdout.write('\x1b[' + back + 'D');
|
|
@@ -352,9 +374,9 @@ async function promptOnce(question) {
|
|
|
352
374
|
resolve(value);
|
|
353
375
|
};
|
|
354
376
|
const insertText = (text) => {
|
|
355
|
-
//
|
|
356
|
-
//
|
|
357
|
-
//
|
|
377
|
+
// Normalize newlines: they come from a multiline paste but mean
|
|
378
|
+
// "submit". Inside the message we replace them with a space so the
|
|
379
|
+
// whole paste goes as ONE message.
|
|
358
380
|
const clean = text
|
|
359
381
|
.replace(/\r\n/g, ' ')
|
|
360
382
|
.replace(/\r/g, ' ')
|
|
@@ -364,11 +386,11 @@ async function promptOnce(question) {
|
|
|
364
386
|
};
|
|
365
387
|
const onData = (buf) => {
|
|
366
388
|
let s = buf.toString('utf-8');
|
|
367
|
-
// Fallback
|
|
368
|
-
//
|
|
369
|
-
//
|
|
370
|
-
//
|
|
371
|
-
//
|
|
389
|
+
// Fallback for terminals without bracketed paste: if the whole chunk is
|
|
390
|
+
// a "bare" newline (one byte), then Enter was pressed → submit.
|
|
391
|
+
// If newlines came TOGETHER with other text in one chunk — that's a
|
|
392
|
+
// paste; such newlines don't submit the message but are replaced with
|
|
393
|
+
// spaces (see insertText).
|
|
372
394
|
if (!inPaste && (s === '\r' || s === '\n')) {
|
|
373
395
|
return finish(line, true);
|
|
374
396
|
}
|
|
@@ -389,14 +411,14 @@ async function promptOnce(question) {
|
|
|
389
411
|
}
|
|
390
412
|
const start = s.indexOf(PASTE_START);
|
|
391
413
|
if (start !== -1) {
|
|
392
|
-
//
|
|
414
|
+
// Everything before the marker is processed as regular input.
|
|
393
415
|
const before = s.slice(0, start);
|
|
394
416
|
s = s.slice(start + PASTE_START.length);
|
|
395
417
|
inPaste = true;
|
|
396
418
|
if (before) {
|
|
397
419
|
for (const ch of before) {
|
|
398
420
|
if (ch === '\r' || ch === '\n') {
|
|
399
|
-
/*
|
|
421
|
+
/* inside a paste — skip */
|
|
400
422
|
}
|
|
401
423
|
else
|
|
402
424
|
insertText(ch);
|
|
@@ -409,22 +431,22 @@ async function promptOnce(question) {
|
|
|
409
431
|
const code = s.charCodeAt(0);
|
|
410
432
|
s = s.slice(1);
|
|
411
433
|
if (ch === '\r' || ch === '\n') {
|
|
412
|
-
//
|
|
413
|
-
// bracketed paste):
|
|
434
|
+
// A newline inside a chunk with other text (paste without
|
|
435
|
+
// bracketed paste): don't submit, insert a space instead.
|
|
414
436
|
insertText(' ');
|
|
415
437
|
redraw();
|
|
416
438
|
continue;
|
|
417
439
|
}
|
|
418
440
|
if (code === 3) {
|
|
419
|
-
// Ctrl+C —
|
|
441
|
+
// Ctrl+C — abort input.
|
|
420
442
|
return finish('', true);
|
|
421
443
|
}
|
|
422
444
|
if (code === 4) {
|
|
423
|
-
// Ctrl+D —
|
|
445
|
+
// Ctrl+D — like submitting an empty line.
|
|
424
446
|
return finish(line, true);
|
|
425
447
|
}
|
|
426
448
|
if (code === 21) {
|
|
427
|
-
// Ctrl+U —
|
|
449
|
+
// Ctrl+U — erase the line.
|
|
428
450
|
line = '';
|
|
429
451
|
cursor = 0;
|
|
430
452
|
redraw();
|
|
@@ -440,7 +462,7 @@ async function promptOnce(question) {
|
|
|
440
462
|
continue;
|
|
441
463
|
}
|
|
442
464
|
if (ch === '\x1b') {
|
|
443
|
-
// Escape
|
|
465
|
+
// Escape sequences (arrows, Home/End, Delete…).
|
|
444
466
|
const rest = s;
|
|
445
467
|
if (rest.startsWith('[D')) {
|
|
446
468
|
if (cursor > 0)
|
|
@@ -477,14 +499,14 @@ async function promptOnce(question) {
|
|
|
477
499
|
s = s.slice(3);
|
|
478
500
|
continue;
|
|
479
501
|
}
|
|
480
|
-
//
|
|
502
|
+
// Other ESC sequences are skipped up to a letter/tilde.
|
|
481
503
|
const m = s.match(/^\[[0-9;]*[A-Za-z~]/);
|
|
482
504
|
if (m)
|
|
483
505
|
s = s.slice(m[0].length);
|
|
484
506
|
continue;
|
|
485
507
|
}
|
|
486
508
|
if (code < 32)
|
|
487
|
-
continue; //
|
|
509
|
+
continue; // other control characters are ignored
|
|
488
510
|
insertText(ch);
|
|
489
511
|
redraw();
|
|
490
512
|
}
|
|
@@ -492,24 +514,24 @@ async function promptOnce(question) {
|
|
|
492
514
|
stdin.on('data', onData);
|
|
493
515
|
});
|
|
494
516
|
}
|
|
495
|
-
//
|
|
517
|
+
// Keyboard monitoring while the agent works.
|
|
496
518
|
//
|
|
497
|
-
//
|
|
498
|
-
// - Esc (
|
|
499
|
-
// -
|
|
500
|
-
//
|
|
501
|
-
//
|
|
519
|
+
// The terminal stays live while the agent thinks:
|
|
520
|
+
// - Esc (or Ctrl+C) — abort the current generation (a Stop click in the browser);
|
|
521
|
+
// - typing + Enter — queue a message; it goes to the agent right after
|
|
522
|
+
// the current task finishes (like "send during generation" in the
|
|
523
|
+
// DeepSeek web version).
|
|
502
524
|
//
|
|
503
|
-
//
|
|
504
|
-
//
|
|
505
|
-
//
|
|
506
|
-
//
|
|
525
|
+
// Input is buffered without a line editor: the typed text is shown in the
|
|
526
|
+
// spinner line via onChange -> ui.setPending(). Enter sends the buffer to
|
|
527
|
+
// onQueue, an empty Enter is ignored. Backspace, Ctrl+U, Esc sequences
|
|
528
|
+
// (arrows/Home/End/Delete are ignored) and bracketed paste are supported.
|
|
507
529
|
function watchInput({ onEscape, onChange, onQueue, } = {}) {
|
|
508
530
|
const stdin = process.stdin;
|
|
509
531
|
if (!stdin.isTTY || !stdin.setRawMode)
|
|
510
532
|
return () => { };
|
|
511
|
-
//
|
|
512
|
-
//
|
|
533
|
+
// Control characters are assembled from codes: this file must not contain
|
|
534
|
+
// "raw" ESC/CR/LF in string literals (see AGENTS.md).
|
|
513
535
|
const ESC = String.fromCharCode(27);
|
|
514
536
|
const CSI = String.fromCharCode(91);
|
|
515
537
|
const CR = String.fromCharCode(13);
|
|
@@ -520,6 +542,8 @@ function watchInput({ onEscape, onChange, onQueue, } = {}) {
|
|
|
520
542
|
process.stdout.write(ESC + '[?2004h');
|
|
521
543
|
let buf = '';
|
|
522
544
|
let inPaste = false;
|
|
545
|
+
let pasteBuf = '';
|
|
546
|
+
const pastes = [];
|
|
523
547
|
const PASTE_START = ESC + '[200~';
|
|
524
548
|
const PASTE_END = ESC + '[201~';
|
|
525
549
|
const CSI_RE = new RegExp('^' + CSI + '[0-9;]*[A-Za-z~]');
|
|
@@ -527,8 +551,9 @@ function watchInput({ onEscape, onChange, onQueue, } = {}) {
|
|
|
527
551
|
if (onChange)
|
|
528
552
|
onChange(buf);
|
|
529
553
|
};
|
|
530
|
-
//
|
|
531
|
-
//
|
|
554
|
+
// Pasted text: small pastes (1–2 lines) are flattened into one line;
|
|
555
|
+
// large ones (3+ lines) are collapsed into "[Pasted lines#N]" (the original
|
|
556
|
+
// text is expanded back when the message is queued).
|
|
532
557
|
const insert = (text) => {
|
|
533
558
|
buf += text
|
|
534
559
|
.split(CR + LF)
|
|
@@ -538,10 +563,19 @@ function watchInput({ onEscape, onChange, onQueue, } = {}) {
|
|
|
538
563
|
.split(LF)
|
|
539
564
|
.join(' ');
|
|
540
565
|
};
|
|
566
|
+
const insertPaste = (raw) => {
|
|
567
|
+
const rep = pasteReplacement(raw);
|
|
568
|
+
if (!rep) {
|
|
569
|
+
insert(raw);
|
|
570
|
+
return;
|
|
571
|
+
}
|
|
572
|
+
pastes.push(rep);
|
|
573
|
+
buf += rep.marker;
|
|
574
|
+
};
|
|
541
575
|
function onData(data) {
|
|
542
576
|
let s = data.toString('utf-8');
|
|
543
|
-
//
|
|
544
|
-
//
|
|
577
|
+
// A single Esc — abort generation. Arrows come as a whole chunk and
|
|
578
|
+
// don't reach here.
|
|
545
579
|
if (!inPaste && s === ESC) {
|
|
546
580
|
if (onEscape)
|
|
547
581
|
onEscape();
|
|
@@ -551,13 +585,15 @@ function watchInput({ onEscape, onChange, onQueue, } = {}) {
|
|
|
551
585
|
if (inPaste) {
|
|
552
586
|
const end = s.indexOf(PASTE_END);
|
|
553
587
|
if (end === -1) {
|
|
554
|
-
|
|
588
|
+
pasteBuf += s;
|
|
555
589
|
s = '';
|
|
556
590
|
}
|
|
557
591
|
else {
|
|
558
|
-
|
|
592
|
+
pasteBuf += s.slice(0, end);
|
|
559
593
|
s = s.slice(end + PASTE_END.length);
|
|
560
594
|
inPaste = false;
|
|
595
|
+
insertPaste(pasteBuf);
|
|
596
|
+
pasteBuf = '';
|
|
561
597
|
}
|
|
562
598
|
emitChange();
|
|
563
599
|
continue;
|
|
@@ -567,6 +603,7 @@ function watchInput({ onEscape, onChange, onQueue, } = {}) {
|
|
|
567
603
|
const before = s.slice(0, start);
|
|
568
604
|
s = s.slice(start + PASTE_START.length);
|
|
569
605
|
inPaste = true;
|
|
606
|
+
pasteBuf = '';
|
|
570
607
|
if (before) {
|
|
571
608
|
insert(before);
|
|
572
609
|
emitChange();
|
|
@@ -577,22 +614,24 @@ function watchInput({ onEscape, onChange, onQueue, } = {}) {
|
|
|
577
614
|
const code = s.charCodeAt(0);
|
|
578
615
|
s = s.slice(1);
|
|
579
616
|
if (ch === CR || ch === LF) {
|
|
580
|
-
const text = buf.trim();
|
|
617
|
+
const text = expandPastes(pastes, buf).trim();
|
|
581
618
|
buf = '';
|
|
619
|
+
pastes.length = 0;
|
|
582
620
|
emitChange();
|
|
583
621
|
if (text && onQueue)
|
|
584
622
|
onQueue(text);
|
|
585
623
|
continue;
|
|
586
624
|
}
|
|
587
625
|
if (code === 3) {
|
|
588
|
-
// Ctrl+C —
|
|
626
|
+
// Ctrl+C — like Esc: abort generation.
|
|
589
627
|
if (onEscape)
|
|
590
628
|
onEscape();
|
|
591
629
|
continue;
|
|
592
630
|
}
|
|
593
631
|
if (code === 21) {
|
|
594
|
-
// Ctrl+U —
|
|
632
|
+
// Ctrl+U — clear what was typed.
|
|
595
633
|
buf = '';
|
|
634
|
+
pastes.length = 0;
|
|
596
635
|
emitChange();
|
|
597
636
|
continue;
|
|
598
637
|
}
|
|
@@ -605,14 +644,14 @@ function watchInput({ onEscape, onChange, onQueue, } = {}) {
|
|
|
605
644
|
continue;
|
|
606
645
|
}
|
|
607
646
|
if (ch === ESC) {
|
|
608
|
-
// Escape
|
|
647
|
+
// Escape sequence (arrows, Home/End, Delete) — skip.
|
|
609
648
|
const m = s.match(CSI_RE);
|
|
610
649
|
if (m)
|
|
611
650
|
s = s.slice(m[0].length);
|
|
612
651
|
continue;
|
|
613
652
|
}
|
|
614
653
|
if (code < 32)
|
|
615
|
-
continue; //
|
|
654
|
+
continue; // other control characters — ignore
|
|
616
655
|
buf += ch;
|
|
617
656
|
emitChange();
|
|
618
657
|
}
|
|
@@ -627,8 +666,8 @@ function watchInput({ onEscape, onChange, onQueue, } = {}) {
|
|
|
627
666
|
};
|
|
628
667
|
}
|
|
629
668
|
// ---------- workdir resolution ----------
|
|
630
|
-
//
|
|
631
|
-
//
|
|
669
|
+
// The agent works in the directory it was launched from (process.cwd()).
|
|
670
|
+
// This is the sandbox root: tools cannot go above it.
|
|
632
671
|
async function resolveWorkdir() {
|
|
633
672
|
const explicitDir = getArg('--dir', null);
|
|
634
673
|
const dir = explicitDir ? path.resolve(explicitDir) : process.cwd();
|
|
@@ -639,12 +678,12 @@ async function resolveWorkdir() {
|
|
|
639
678
|
return dir;
|
|
640
679
|
}
|
|
641
680
|
// ---------- task runner ----------
|
|
642
|
-
async function runTask(browser, tools, taskText, workdir, opts) {
|
|
681
|
+
async function runTask(browser, tools, taskText, workdir, opts, attachments = []) {
|
|
643
682
|
const { transcript, freshChat, sendSystemPrompt, queue = [], ui: editor, onChatReady, } = opts;
|
|
644
|
-
//
|
|
645
|
-
// Ctrl+C)
|
|
646
|
-
// (
|
|
647
|
-
//
|
|
683
|
+
// In TTY mode the UI is a LineEditor: it owns the input (queue, Esc,
|
|
684
|
+
// Ctrl+C) and draws the status ABOVE the permanent input line. In non-TTY
|
|
685
|
+
// mode (pipes) — a regular spinner + watchInput.
|
|
686
|
+
// A new task from the prompt — we reset the "stop" from the previous abort.
|
|
648
687
|
browser._stopped = false;
|
|
649
688
|
browser._abort = false;
|
|
650
689
|
const ui = editor || mod.createSpinner(currentLocale);
|
|
@@ -658,7 +697,7 @@ async function runTask(browser, tools, taskText, workdir, opts) {
|
|
|
658
697
|
},
|
|
659
698
|
onChange: (text) => ui.setPending(text),
|
|
660
699
|
onQueue: (text) => {
|
|
661
|
-
queue.push(text);
|
|
700
|
+
queue.push({ text });
|
|
662
701
|
ui.setPending(null);
|
|
663
702
|
ui.stop();
|
|
664
703
|
console.log(theme.user(t('msg.queued', { n: queue.length })) +
|
|
@@ -667,46 +706,58 @@ async function runTask(browser, tools, taskText, workdir, opts) {
|
|
|
667
706
|
},
|
|
668
707
|
});
|
|
669
708
|
try {
|
|
670
|
-
let next = {
|
|
671
|
-
|
|
672
|
-
|
|
709
|
+
let next = {
|
|
710
|
+
text: taskText,
|
|
711
|
+
attachments,
|
|
712
|
+
freshChat,
|
|
713
|
+
sendSystemPrompt,
|
|
714
|
+
};
|
|
715
|
+
// Execute the task, then everything the user managed to type while it
|
|
716
|
+
// ran. The queue may be replenished right during draining.
|
|
673
717
|
while (true) {
|
|
674
718
|
ui.thinking();
|
|
675
719
|
await mod.runAgentLoop({
|
|
676
720
|
browser,
|
|
677
721
|
tools,
|
|
678
|
-
task: next.
|
|
722
|
+
task: next.text,
|
|
679
723
|
workdir,
|
|
680
724
|
maxIterations: maxIter,
|
|
681
725
|
freshChat: next.freshChat,
|
|
682
726
|
sendSystemPrompt: next.sendSystemPrompt,
|
|
727
|
+
attachments: next.attachments || [],
|
|
683
728
|
transcript,
|
|
684
729
|
onThinking: () => ui.thinking(),
|
|
685
730
|
onToolCall: (name, toolArgs) => ui.toolCall(name, toolArgs),
|
|
686
731
|
onToolResult: (result) => ui.toolResult(result),
|
|
687
732
|
onAssistantMessage: (msg) => ui.assistant(msg),
|
|
733
|
+
onWarning: (msg) => ui.warning(msg),
|
|
688
734
|
onChatReady,
|
|
689
735
|
debugLog: debug,
|
|
690
736
|
locale: currentLocale,
|
|
691
737
|
});
|
|
692
|
-
//
|
|
693
|
-
//
|
|
738
|
+
// Aborted (Esc/Ctrl+C) — we don't start the next tasks from the queue
|
|
739
|
+
// and clear it, so "stop" really stops everything.
|
|
694
740
|
if (browser._stopped) {
|
|
695
741
|
queue.length = 0;
|
|
696
742
|
break;
|
|
697
743
|
}
|
|
698
744
|
if (!queue.length)
|
|
699
745
|
break;
|
|
700
|
-
const queued = queue.shift() ?? '';
|
|
746
|
+
const queued = queue.shift() ?? { text: '' };
|
|
701
747
|
ui.stop();
|
|
702
748
|
if (editor) {
|
|
703
|
-
editor.printAbove(theme.user(t('msg.from_queue')) + theme.assistant(queued));
|
|
749
|
+
editor.printAbove(theme.user(t('msg.from_queue')) + theme.assistant(queued.text));
|
|
704
750
|
}
|
|
705
751
|
else {
|
|
706
|
-
console.log(theme.user(t('msg.from_queue')) + theme.assistant(queued));
|
|
707
|
-
}
|
|
708
|
-
transcript?.log('queued_task', { task: queued });
|
|
709
|
-
next = {
|
|
752
|
+
console.log(theme.user(t('msg.from_queue')) + theme.assistant(queued.text));
|
|
753
|
+
}
|
|
754
|
+
transcript?.log('queued_task', { task: queued.text });
|
|
755
|
+
next = {
|
|
756
|
+
text: queued.text,
|
|
757
|
+
attachments: queued.attachments,
|
|
758
|
+
freshChat: false,
|
|
759
|
+
sendSystemPrompt: false,
|
|
760
|
+
};
|
|
710
761
|
}
|
|
711
762
|
}
|
|
712
763
|
catch (e) {
|
|
@@ -740,7 +791,7 @@ async function main() {
|
|
|
740
791
|
let sandboxRoot;
|
|
741
792
|
try {
|
|
742
793
|
currentWorkdir = await resolveWorkdir();
|
|
743
|
-
//
|
|
794
|
+
// Sandbox root: the agent cannot go above the launch directory.
|
|
744
795
|
sandboxRoot = currentWorkdir;
|
|
745
796
|
}
|
|
746
797
|
catch (e) {
|
|
@@ -779,13 +830,13 @@ async function main() {
|
|
|
779
830
|
transcript.close();
|
|
780
831
|
process.exit(1);
|
|
781
832
|
}
|
|
782
|
-
//
|
|
833
|
+
// One-shot mode
|
|
783
834
|
if (task) {
|
|
784
835
|
const tools = mod.createTools(currentWorkdir, { undo });
|
|
785
836
|
let freshChat = true;
|
|
786
837
|
let sendSystemPrompt = true;
|
|
787
|
-
//
|
|
788
|
-
//
|
|
838
|
+
// By default we start a new chat. To continue a previous session —
|
|
839
|
+
// explicitly: --chat <id> or --resume-last.
|
|
789
840
|
let resumeId = chatIdArg;
|
|
790
841
|
if (!resumeId && resumeLastFlag && !newChatFlag) {
|
|
791
842
|
const last = loadLastSession(currentWorkdir);
|
|
@@ -828,29 +879,34 @@ async function main() {
|
|
|
828
879
|
let lastChats = [];
|
|
829
880
|
let currentChatId = null;
|
|
830
881
|
let running = true;
|
|
831
|
-
//
|
|
832
|
-
//
|
|
882
|
+
// Messages the user typed while the agent worked. runTask takes them one
|
|
883
|
+
// by one after the current task finishes.
|
|
833
884
|
const pendingQueue = [];
|
|
885
|
+
// Attachments for the next submit (filled by the editor's onAttachments).
|
|
886
|
+
let pendingAttachments = [];
|
|
887
|
+
// Show the "no clipboard image" hint only once per session.
|
|
888
|
+
let clipboardWarned = false;
|
|
834
889
|
// ---------- review mode state ----------
|
|
835
|
-
// null —
|
|
836
|
-
// { snapDir, snapName, originalWorkdir } —
|
|
837
|
-
//
|
|
890
|
+
// null — normal mode.
|
|
891
|
+
// { snapDir, snapName, originalWorkdir } — we're inside a snapshot, the chat
|
|
892
|
+
// is already initialized with the review prompt, the user can just type "fix...".
|
|
838
893
|
let reviewMode = null;
|
|
839
894
|
process.on('SIGINT', async () => {
|
|
840
895
|
running = false;
|
|
841
896
|
await browser.close().catch(() => { });
|
|
842
|
-
//
|
|
843
|
-
//
|
|
897
|
+
// Close the lazy headless browser from web.js, otherwise it would stay
|
|
898
|
+
// as a separate process after the agent exits.
|
|
844
899
|
await mod.closeWeb().catch(() => { });
|
|
845
900
|
transcript.close();
|
|
846
901
|
console.log(theme.system(String.fromCharCode(10) + t('msg.bye')));
|
|
847
902
|
process.exit(0);
|
|
848
903
|
});
|
|
849
|
-
//
|
|
850
|
-
//
|
|
851
|
-
//
|
|
852
|
-
// --
|
|
853
|
-
// --
|
|
904
|
+
// By default we start a NEW chat on launch: the context of a previous
|
|
905
|
+
// session is not carried over automatically. To continue a previous
|
|
906
|
+
// session explicitly:
|
|
907
|
+
// --chat <id> open a specific chat
|
|
908
|
+
// --resume-last return to the last chat for this working directory
|
|
909
|
+
// --new-chat kept for compatibility (this is the default behavior)
|
|
854
910
|
let resumeId = chatIdArg;
|
|
855
911
|
if (!resumeId && resumeLastFlag && !newChatFlag) {
|
|
856
912
|
const last = loadLastSession(currentWorkdir);
|
|
@@ -873,11 +929,11 @@ async function main() {
|
|
|
873
929
|
console.error(theme.error(t('msg.open_chat_error', { v: e.message })));
|
|
874
930
|
}
|
|
875
931
|
}
|
|
876
|
-
// ----------
|
|
877
|
-
//
|
|
878
|
-
//
|
|
879
|
-
//
|
|
880
|
-
//
|
|
932
|
+
// ---------- input: permanent line below + status above ----------
|
|
933
|
+
// In TTY we use LineEditor: it owns the input all the time, shows the
|
|
934
|
+
// status above the input line and prints the agent's answers ABOVE it, so
|
|
935
|
+
// the typed text is never overwritten by output. In non-TTY (pipe) —
|
|
936
|
+
// the old promptOnce.
|
|
881
937
|
let editor = null;
|
|
882
938
|
let waiter = null;
|
|
883
939
|
const takeInput = () => {
|
|
@@ -904,8 +960,74 @@ async function main() {
|
|
|
904
960
|
commands: buildSlashCommands(),
|
|
905
961
|
});
|
|
906
962
|
editor = ed;
|
|
907
|
-
ed.
|
|
908
|
-
|
|
963
|
+
ed.setTmpDir(TMP_DIR);
|
|
964
|
+
ed.onAttach = async (raw) => {
|
|
965
|
+
// Case 1: the paste is the image data itself (data URL / base64 blob).
|
|
966
|
+
const image = parseImagePaste(raw);
|
|
967
|
+
if (image) {
|
|
968
|
+
const name = 'paste' + extForMime(image.mime);
|
|
969
|
+
const p = await saveToTemp(TMP_DIR, name, image.data);
|
|
970
|
+
const att = ed.attachments.add({
|
|
971
|
+
path: p,
|
|
972
|
+
name,
|
|
973
|
+
mime: image.mime,
|
|
974
|
+
size: image.data.length,
|
|
975
|
+
});
|
|
976
|
+
ed.printAbove(theme.system(t('msg.attached_image', { marker: att.marker, size: formatSize(image.data.length) })));
|
|
977
|
+
return att;
|
|
978
|
+
}
|
|
979
|
+
// Case 2: the paste is a path to a local file (drag&drop or copy path).
|
|
980
|
+
const filePath = await resolveAttachPath(currentWorkdir, raw);
|
|
981
|
+
if (!filePath)
|
|
982
|
+
return null;
|
|
983
|
+
const data = await fs.readFile(filePath).catch(() => null);
|
|
984
|
+
if (!data)
|
|
985
|
+
return null;
|
|
986
|
+
const name = path.basename(filePath);
|
|
987
|
+
const mime = guessMime(name);
|
|
988
|
+
const att = ed.attachments.add({
|
|
989
|
+
path: filePath,
|
|
990
|
+
name,
|
|
991
|
+
mime,
|
|
992
|
+
size: data.length,
|
|
993
|
+
});
|
|
994
|
+
ed.printAbove(theme.system(t('msg.attached_file', { marker: att.marker, name, size: formatSize(data.length) })));
|
|
995
|
+
return att;
|
|
996
|
+
};
|
|
997
|
+
ed.onClipboard = async () => {
|
|
998
|
+
const res = readClipboardImageDetailed();
|
|
999
|
+
if (!res.data || !res.data.length) {
|
|
1000
|
+
if (!clipboardWarned) {
|
|
1001
|
+
clipboardWarned = true;
|
|
1002
|
+
ed.printAbove(theme.warn(t('msg.clip_empty', { via: res.via }) +
|
|
1003
|
+
String.fromCharCode(10) +
|
|
1004
|
+
t('msg.clip_hint')));
|
|
1005
|
+
}
|
|
1006
|
+
return null;
|
|
1007
|
+
}
|
|
1008
|
+
const mime = sniffMime(res.data) || 'image/png';
|
|
1009
|
+
const name = 'clipboard-' + Date.now() + (extForMime(mime) || '.png');
|
|
1010
|
+
const p = await saveToTemp(TMP_DIR, name, res.data);
|
|
1011
|
+
const att = ed.attachments.add({
|
|
1012
|
+
path: p,
|
|
1013
|
+
name,
|
|
1014
|
+
mime,
|
|
1015
|
+
size: res.data.length,
|
|
1016
|
+
});
|
|
1017
|
+
ed.printAbove(theme.system(t('msg.attached_image', {
|
|
1018
|
+
marker: att.marker,
|
|
1019
|
+
size: formatSize(res.data.length),
|
|
1020
|
+
}) + theme.dim(' (' + res.via + ')')));
|
|
1021
|
+
return att;
|
|
1022
|
+
};
|
|
1023
|
+
ed.onSubmit = (text, items) => {
|
|
1024
|
+
const attachments = (items || []).map((a) => ({
|
|
1025
|
+
path: a.path,
|
|
1026
|
+
name: a.name,
|
|
1027
|
+
mime: a.mime,
|
|
1028
|
+
}));
|
|
1029
|
+
const msg = { text, attachments };
|
|
1030
|
+
pendingQueue.push(msg);
|
|
909
1031
|
if (waiter) {
|
|
910
1032
|
const r = waiter;
|
|
911
1033
|
waiter = null;
|
|
@@ -924,7 +1046,7 @@ async function main() {
|
|
|
924
1046
|
browser.stopGeneration().catch(() => { });
|
|
925
1047
|
}
|
|
926
1048
|
else {
|
|
927
|
-
//
|
|
1049
|
+
// Not busy — exit. We wake takeInput() so the loop finishes.
|
|
928
1050
|
running = false;
|
|
929
1051
|
if (waiter) {
|
|
930
1052
|
const r = waiter;
|
|
@@ -934,9 +1056,9 @@ async function main() {
|
|
|
934
1056
|
}
|
|
935
1057
|
};
|
|
936
1058
|
ed.start();
|
|
937
|
-
//
|
|
938
|
-
//
|
|
939
|
-
//
|
|
1059
|
+
// All command output (console.log/error) must go ABOVE the input line,
|
|
1060
|
+
// otherwise it overwrites the text being typed. While the editor is
|
|
1061
|
+
// active, we wrap both streams in ed.printAbove.
|
|
940
1062
|
const origLog = console.log.bind(console);
|
|
941
1063
|
const origErr = console.error.bind(console);
|
|
942
1064
|
const fmt = (a) => typeof a === 'string'
|
|
@@ -951,21 +1073,21 @@ async function main() {
|
|
|
951
1073
|
})();
|
|
952
1074
|
console.log = (...a) => ed.printAbove(a.map(fmt).join(' '));
|
|
953
1075
|
console.error = (...a) => ed.printAbove(a.map(fmt).join(' '));
|
|
954
|
-
//
|
|
1076
|
+
// Save for debugging.
|
|
955
1077
|
const edAny = ed;
|
|
956
1078
|
edAny._origLog = origLog;
|
|
957
1079
|
edAny._origErr = origErr;
|
|
958
1080
|
}
|
|
959
1081
|
// ---------- /config ----------
|
|
960
|
-
//
|
|
961
|
-
// CONFIG_SCHEMA (src/config.ts)
|
|
962
|
-
//
|
|
963
|
-
//
|
|
964
|
-
//
|
|
1082
|
+
// View and edit settings without a restart. Values are validated against
|
|
1083
|
+
// CONFIG_SCHEMA (src/config.ts) and written to the project .zamesrc.json.
|
|
1084
|
+
// The language (ui.locale) is applied immediately: we change currentLocale,
|
|
1085
|
+
// rebuild the editor hints and update config.ui.locale (agent-loop reads it
|
|
1086
|
+
// on the next task).
|
|
965
1087
|
//
|
|
966
|
-
//
|
|
967
|
-
// Enter —
|
|
968
|
-
// (list/get/set/reset/lang/path) —
|
|
1088
|
+
// With no arguments in an interactive terminal a menu opens (arrows,
|
|
1089
|
+
// Enter — edit, d — reset, q — quit). There are also text subcommands
|
|
1090
|
+
// (list/get/set/reset/lang/path) — for scripts and non-TTY.
|
|
969
1091
|
function setConfigRuntime(path, value) {
|
|
970
1092
|
const segs = path.split('.');
|
|
971
1093
|
let obj = config;
|
|
@@ -989,7 +1111,7 @@ async function main() {
|
|
|
989
1111
|
}
|
|
990
1112
|
function configResetField(field) {
|
|
991
1113
|
resetConfigValue('project', field.path);
|
|
992
|
-
//
|
|
1114
|
+
// Reset the runtime value to the default.
|
|
993
1115
|
const def = getByPath(DEFAULTS, field.path);
|
|
994
1116
|
setConfigRuntime(field.path, def);
|
|
995
1117
|
}
|
|
@@ -1028,9 +1150,9 @@ async function main() {
|
|
|
1028
1150
|
configShowList();
|
|
1029
1151
|
return;
|
|
1030
1152
|
}
|
|
1031
|
-
//
|
|
1032
|
-
//
|
|
1033
|
-
//
|
|
1153
|
+
// The menu draws directly to stdout and reads keys itself. So that its
|
|
1154
|
+
// output doesn't overlap the LineEditor's permanent input line, we "pause"
|
|
1155
|
+
// the editor for the duration of the menu and restore it afterwards.
|
|
1034
1156
|
if (editor)
|
|
1035
1157
|
editor.pause();
|
|
1036
1158
|
try {
|
|
@@ -1053,7 +1175,7 @@ async function main() {
|
|
|
1053
1175
|
async function handleConfigCommand(input) {
|
|
1054
1176
|
const parts = input.trim().split(/\s+/);
|
|
1055
1177
|
const sub = (parts[1] || '').toLowerCase();
|
|
1056
|
-
//
|
|
1178
|
+
// With no subcommand — the menu (or a text list in non-TTY).
|
|
1057
1179
|
if (!sub || sub === 'menu' || sub === 'ui') {
|
|
1058
1180
|
await configOpenMenu();
|
|
1059
1181
|
return;
|
|
@@ -1069,7 +1191,7 @@ async function main() {
|
|
|
1069
1191
|
})));
|
|
1070
1192
|
return;
|
|
1071
1193
|
}
|
|
1072
|
-
// /config lang <ru|en> —
|
|
1194
|
+
// /config lang <ru|en> — quick access to ui.locale
|
|
1073
1195
|
if (sub === 'lang' || sub === 'language' || sub === 'язык') {
|
|
1074
1196
|
const val = parts[2];
|
|
1075
1197
|
if (!val) {
|
|
@@ -1152,16 +1274,19 @@ async function main() {
|
|
|
1152
1274
|
console.log(theme.assistant(t('cfg.reset', { v: key })));
|
|
1153
1275
|
return;
|
|
1154
1276
|
}
|
|
1155
|
-
//
|
|
1277
|
+
// Unknown subcommand — show the list.
|
|
1156
1278
|
console.error(theme.error(t('cfg.unknown_key', { v: sub })));
|
|
1157
1279
|
configShowList();
|
|
1158
1280
|
}
|
|
1159
1281
|
while (running) {
|
|
1160
1282
|
let input;
|
|
1283
|
+
let inputAttachments = [];
|
|
1161
1284
|
try {
|
|
1162
1285
|
if (editor) {
|
|
1163
1286
|
editor.setPrompt(buildPrompt());
|
|
1164
|
-
|
|
1287
|
+
const msg = await takeInput();
|
|
1288
|
+
input = msg ? msg.text : null;
|
|
1289
|
+
inputAttachments = msg?.attachments || [];
|
|
1165
1290
|
}
|
|
1166
1291
|
else {
|
|
1167
1292
|
let tail;
|
|
@@ -1206,10 +1331,10 @@ async function main() {
|
|
|
1206
1331
|
}
|
|
1207
1332
|
continue;
|
|
1208
1333
|
}
|
|
1209
|
-
// ----------
|
|
1334
|
+
// ---------- Self-review ----------
|
|
1210
1335
|
if (lower === '/self-review' || lower.startsWith('/self-review ')) {
|
|
1211
1336
|
const focus = trimmed.slice('/self-review'.length).trim();
|
|
1212
|
-
//
|
|
1337
|
+
// Remember where to return
|
|
1213
1338
|
const originalWorkdir = reviewMode
|
|
1214
1339
|
? reviewMode.originalWorkdir
|
|
1215
1340
|
: currentWorkdir;
|
|
@@ -1220,10 +1345,10 @@ async function main() {
|
|
|
1220
1345
|
focus: focus || undefined,
|
|
1221
1346
|
transcript,
|
|
1222
1347
|
});
|
|
1223
|
-
//
|
|
1224
|
-
// -
|
|
1225
|
-
// -
|
|
1226
|
-
//
|
|
1348
|
+
// Switch to review mode:
|
|
1349
|
+
// - working directory = snapshot
|
|
1350
|
+
// - we do NOT reset the chat — inside selfReview a fresh chat was
|
|
1351
|
+
// already created and the review prompt sent, we continue in it
|
|
1227
1352
|
reviewMode = {
|
|
1228
1353
|
snapDir: result.snapDir,
|
|
1229
1354
|
snapName: path.basename(result.snapDir),
|
|
@@ -1261,7 +1386,7 @@ async function main() {
|
|
|
1261
1386
|
const originalWorkdir = reviewMode
|
|
1262
1387
|
? reviewMode.originalWorkdir
|
|
1263
1388
|
: currentWorkdir;
|
|
1264
|
-
//
|
|
1389
|
+
// Fresh chat + review prompt for this snapshot
|
|
1265
1390
|
try {
|
|
1266
1391
|
const { runAgentLoop: ral } = await import('./agent-loop.js');
|
|
1267
1392
|
const tools = mod.createTools(snapRoot, { undo: null });
|
|
@@ -1291,6 +1416,8 @@ async function main() {
|
|
|
1291
1416
|
onAssistantMessage: (m) => {
|
|
1292
1417
|
ui.assistant(m);
|
|
1293
1418
|
},
|
|
1419
|
+
onWarning: (m) => ui.warning(m),
|
|
1420
|
+
locale: currentLocale,
|
|
1294
1421
|
});
|
|
1295
1422
|
}
|
|
1296
1423
|
reviewMode = {
|
|
@@ -1318,7 +1445,7 @@ async function main() {
|
|
|
1318
1445
|
const back = reviewMode.originalWorkdir;
|
|
1319
1446
|
reviewMode = null;
|
|
1320
1447
|
currentWorkdir = back;
|
|
1321
|
-
//
|
|
1448
|
+
// Since the chat is busy with the review context, we'll create a new one for regular work
|
|
1322
1449
|
freshChatNext = true;
|
|
1323
1450
|
sendSystemPromptNext = true;
|
|
1324
1451
|
console.log(theme.system(t('self.done_hint', { v: back })));
|
|
@@ -1361,7 +1488,7 @@ async function main() {
|
|
|
1361
1488
|
}
|
|
1362
1489
|
continue;
|
|
1363
1490
|
}
|
|
1364
|
-
// ----------
|
|
1491
|
+
// ---------- Regular commands ----------
|
|
1365
1492
|
if (lower === '/chats') {
|
|
1366
1493
|
const spin = editor || mod.createSpinner(currentLocale);
|
|
1367
1494
|
spin.thinking();
|
|
@@ -1616,8 +1743,8 @@ async function main() {
|
|
|
1616
1743
|
console.error(theme.error(t('msg.unknown_cmd', { v: trimmed })));
|
|
1617
1744
|
continue;
|
|
1618
1745
|
}
|
|
1619
|
-
// ----
|
|
1620
|
-
// Dev
|
|
1746
|
+
// ---- Regular task (including in review mode) ----
|
|
1747
|
+
// Dev mode: pick up fresh logic modules before the task.
|
|
1621
1748
|
await autoReload();
|
|
1622
1749
|
transcript.log('user_task', { task: trimmed, workdir: currentWorkdir });
|
|
1623
1750
|
const tools = mod.createTools(currentWorkdir, { undo });
|
|
@@ -1631,15 +1758,15 @@ async function main() {
|
|
|
1631
1758
|
queue: pendingQueue,
|
|
1632
1759
|
ui: editor || null,
|
|
1633
1760
|
onChatReady: (chatId) => {
|
|
1634
|
-
//
|
|
1635
|
-
//
|
|
1636
|
-
// ~/.zames/.sessions
|
|
1761
|
+
// Save the session right at the start of the dialog, without waiting
|
|
1762
|
+
// for the task to finish. Otherwise a long/aborted task would not
|
|
1763
|
+
// get the chat into ~/.zames/.sessions and it would be lost after a restart.
|
|
1637
1764
|
if (chatId) {
|
|
1638
1765
|
currentChatId = chatId;
|
|
1639
1766
|
saveLastChat(chatId, currentWorkdir);
|
|
1640
1767
|
}
|
|
1641
1768
|
},
|
|
1642
|
-
});
|
|
1769
|
+
}, inputAttachments);
|
|
1643
1770
|
}
|
|
1644
1771
|
finally {
|
|
1645
1772
|
if (editor)
|