zames_pro 2.5.1 → 2.7.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 +32 -0
- package/dist/agent-loop.js +224 -49
- package/dist/browser.js +41 -42
- package/dist/config-menu.js +231 -0
- package/dist/config.js +143 -0
- package/dist/gitTools.js +4 -4
- package/dist/i18n.js +280 -0
- package/dist/index.js +548 -319
- package/dist/input.js +137 -67
- 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 +30 -52
- package/dist/system-prompt.js +31 -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 +14 -14
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -8,7 +8,9 @@ import { createTools } from './tools.js';
|
|
|
8
8
|
import { runAgentLoop } from './agent-loop.js';
|
|
9
9
|
import { createSpinner } from './spinner.js';
|
|
10
10
|
import { LineEditor } from './input.js';
|
|
11
|
-
import { loadConfig, CONFIG_PATHS, ZAMES_HOME } from './config.js';
|
|
11
|
+
import { loadConfig, DEFAULTS, CONFIG_PATHS, ZAMES_HOME, CONFIG_SCHEMA, getByPath, validateConfigValue, writeConfigValue, resetConfigValue, } from './config.js';
|
|
12
|
+
import { runConfigMenu } from './config-menu.js';
|
|
13
|
+
import { translate, normalizeLocale, localeDisplayName, isLocale, } from './i18n.js';
|
|
12
14
|
import { Transcript } from './transcript.js';
|
|
13
15
|
import { UndoStore } from './undo.js';
|
|
14
16
|
import { selfReview, selfDiff, selfApply, selfList } from './self-review.js';
|
|
@@ -39,6 +41,13 @@ function getPositional() {
|
|
|
39
41
|
return positional;
|
|
40
42
|
}
|
|
41
43
|
const config = loadConfig();
|
|
44
|
+
// Current interface/agent language. Changed by the /config lang <ru|en> command.
|
|
45
|
+
let currentLocale = isLocale(config.ui?.locale)
|
|
46
|
+
? config.ui.locale
|
|
47
|
+
: 'ru';
|
|
48
|
+
// Translation: reads currentLocale at call time, so a language change takes
|
|
49
|
+
// effect immediately, without a restart (for lines printed afterwards).
|
|
50
|
+
const t = (key, params) => translate(currentLocale)(key, params);
|
|
42
51
|
const headless = hasFlag('--headless') || config.headless;
|
|
43
52
|
const debug = hasFlag('--debug') || config.debug;
|
|
44
53
|
const calibrate = hasFlag('--calibrate');
|
|
@@ -47,21 +56,21 @@ const maxIter = Number(getArg('--max-iter', String(config.maxIterations))) ||
|
|
|
47
56
|
const positional = getPositional();
|
|
48
57
|
const task = getArg('--task', positional.join(' ').trim() || null);
|
|
49
58
|
const chatIdArg = getArg('--chat', null);
|
|
50
|
-
//
|
|
51
|
-
//
|
|
52
|
-
//
|
|
59
|
+
// When resuming an existing chat, the system-prompt is by default NOT
|
|
60
|
+
// resent (it is already at the start of the chat). The --resend-prompt flag
|
|
61
|
+
// forces resending it — for example, if the prompt was updated.
|
|
53
62
|
const resendPrompt = hasFlag('--resend-prompt');
|
|
54
|
-
//
|
|
55
|
-
// --resume-last:
|
|
56
|
-
// --new-chat:
|
|
63
|
+
// By default a NEW chat is started on launch (context is not carried over).
|
|
64
|
+
// --resume-last: return to the last session for the working directory.
|
|
65
|
+
// --new-chat: kept for compatibility — this is the default behavior anyway.
|
|
57
66
|
const newChatFlag = hasFlag('--new-chat');
|
|
58
67
|
const resumeLastFlag = hasFlag('--resume-last');
|
|
59
68
|
// ---------- session persistence ----------
|
|
60
|
-
//
|
|
61
|
-
//
|
|
62
|
-
// --chat <id>, /resume <n>).
|
|
63
|
-
//
|
|
64
|
-
//
|
|
69
|
+
// We store sessions (DeepSeek chats) in ~/.zames/.sessions so they survive
|
|
70
|
+
// a restart and are available for explicit restore (--resume-last,
|
|
71
|
+
// --chat <id>, /resume <n>). They are NOT brought up automatically on launch.
|
|
72
|
+
// Previously there was a single last-chat.json file that got lost when the
|
|
73
|
+
// project changed and gave no list of sessions to restore.
|
|
65
74
|
function saveLastChat(id, workdir = '', title = '') {
|
|
66
75
|
if (!id)
|
|
67
76
|
return;
|
|
@@ -72,13 +81,13 @@ function loadLastChat(workdir = '') {
|
|
|
72
81
|
return s ? s.id : null;
|
|
73
82
|
}
|
|
74
83
|
// ---------- hot reload ----------
|
|
75
|
-
//
|
|
76
|
-
//
|
|
77
|
-
//
|
|
78
|
-
//
|
|
79
|
-
//
|
|
80
|
-
//
|
|
81
|
-
//
|
|
84
|
+
// We keep references to the logic modules in the mod object. The /reload
|
|
85
|
+
// command re-reads them via a dynamic import with a timestamp query — Node
|
|
86
|
+
// caches ESM by URL, so such an import returns a fresh version of the module.
|
|
87
|
+
// The browser, chat and current state are NOT restarted: only the logic is updated.
|
|
88
|
+
// In dev mode tsx loads the .ts sources from src/, and in the built dist — .js.
|
|
89
|
+
// A dynamic import with the ?t= query bypasses tsx's extension rewriting,
|
|
90
|
+
// so we pick the extension ourselves — from the actual file of the current module.
|
|
82
91
|
const SRC_EXT = /[.]ts$/.test(new URL(import.meta.url).pathname) ? '.ts' : '.js';
|
|
83
92
|
const RELOADABLE = [
|
|
84
93
|
'tools',
|
|
@@ -142,19 +151,19 @@ async function reloadModules() {
|
|
|
142
151
|
mod.createSpinner = pick('spinner', 'createSpinner');
|
|
143
152
|
return { count: loaded.size, errors };
|
|
144
153
|
}
|
|
145
|
-
//
|
|
154
|
+
// Initial load so that mod.buildSystemPrompt and the rest are populated.
|
|
146
155
|
await reloadModules();
|
|
147
|
-
// Dev
|
|
148
|
-
//
|
|
149
|
-
//
|
|
156
|
+
// Dev mode: auto-reload of the logic modules before each task.
|
|
157
|
+
// Enabled by the --dev flag (`npm run dev` sets it) or config.hotReload === true.
|
|
158
|
+
// In normal mode (npm start, global zames) auto-reload is off.
|
|
150
159
|
const devMode = hasFlag('--dev') || config.hotReload === true;
|
|
151
|
-
//
|
|
160
|
+
// Safe auto-reload: on a load error we keep the previous working modules.
|
|
152
161
|
async function autoReload() {
|
|
153
162
|
if (!devMode)
|
|
154
163
|
return;
|
|
155
164
|
const { errors } = await reloadModules();
|
|
156
165
|
if (errors.length) {
|
|
157
|
-
console.error(theme.warn('
|
|
166
|
+
console.error(theme.warn(t('reload.auto_partial')));
|
|
158
167
|
for (const e of errors)
|
|
159
168
|
console.error(theme.warn(' ' + e));
|
|
160
169
|
}
|
|
@@ -162,104 +171,113 @@ async function autoReload() {
|
|
|
162
171
|
// ---------- helpers ----------
|
|
163
172
|
function printHelp() {
|
|
164
173
|
console.log(`
|
|
165
|
-
${theme.bold('zames')} —
|
|
174
|
+
${theme.bold('zames')} — ${t('app.tagline')}
|
|
166
175
|
|
|
167
|
-
${theme.bold('
|
|
168
|
-
--dir <path>
|
|
169
|
-
--task <text>
|
|
170
|
-
--chat <id>
|
|
171
|
-
--resume-last
|
|
172
|
-
--new-chat
|
|
173
|
-
--resend-prompt
|
|
174
|
-
--max-iter <n>
|
|
175
|
-
--headless
|
|
176
|
-
--debug
|
|
177
|
-
--calibrate
|
|
178
|
-
--dev
|
|
179
|
-
--version, -v
|
|
180
|
-
--help, -h
|
|
176
|
+
${theme.bold(t('help.options'))}
|
|
177
|
+
--dir <path> ${t('help.opt.dir')}
|
|
178
|
+
--task <text> ${t('help.opt.task')}
|
|
179
|
+
--chat <id> ${t('help.opt.chat')}
|
|
180
|
+
--resume-last ${t('help.opt.resume_last')}
|
|
181
|
+
--new-chat ${t('help.opt.new_chat')}
|
|
182
|
+
--resend-prompt ${t('help.opt.resend_prompt')}
|
|
183
|
+
--max-iter <n> ${t('help.opt.max_iter', { n: config.maxIterations })}
|
|
184
|
+
--headless ${t('help.opt.headless')}
|
|
185
|
+
--debug ${t('help.opt.debug')}
|
|
186
|
+
--calibrate ${t('help.opt.calibrate')}
|
|
187
|
+
--dev ${t('help.opt.dev')}
|
|
188
|
+
--version, -v ${t('help.opt.version')}
|
|
189
|
+
--help, -h ${t('help.opt.help')}
|
|
181
190
|
|
|
182
|
-
${theme.bold('
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
191
|
+
${theme.bold(t('help.while_working'))}
|
|
192
|
+
${t('help.key.queue')}
|
|
193
|
+
${t('help.key.history')}
|
|
194
|
+
${t('help.key.words')}
|
|
195
|
+
${t('help.key.slash')}
|
|
196
|
+
${t('help.key.newline')}
|
|
197
|
+
${t('help.key.backslash')}
|
|
198
|
+
${t('help.key.esc')}
|
|
189
199
|
|
|
190
|
-
${theme.bold('
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
200
|
+
${theme.bold(t('help.commands'))}
|
|
201
|
+
${t('help.cmd.new')}
|
|
202
|
+
${t('help.cmd.sessions')}
|
|
203
|
+
${t('help.cmd.resume_id')}
|
|
204
|
+
${t('help.cmd.chats')}
|
|
205
|
+
${t('help.cmd.resume')}
|
|
206
|
+
${t('help.cmd.chat')}
|
|
207
|
+
${t('help.cmd.cd')}
|
|
208
|
+
${t('help.cmd.pwd')}
|
|
209
|
+
${t('help.cmd.status')}
|
|
210
|
+
${t('help.cmd.reload')}
|
|
211
|
+
${t('help.cmd.undo')}
|
|
212
|
+
${t('help.cmd.undo_list')}
|
|
213
|
+
${t('help.cmd.transcript')}
|
|
214
|
+
${t('help.cmd.config')}
|
|
215
|
+
${t('help.cmd.lang')}
|
|
216
|
+
${t('help.cmd.debug_dom')}
|
|
217
|
+
${t('help.cmd.help')}
|
|
218
|
+
${t('help.cmd.exit')}
|
|
208
219
|
|
|
209
|
-
${theme.bold(
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
/self-diff <name> различия между текущим src/ и снапшотом
|
|
217
|
-
/self-apply <name> применить снапшот к src/ (с бэкапом)
|
|
220
|
+
${theme.bold(t('help.self_review'))}
|
|
221
|
+
${t('help.self.review')}
|
|
222
|
+
${t('help.self.fix')}
|
|
223
|
+
${t('help.self.done')}
|
|
224
|
+
${t('help.self.list')}
|
|
225
|
+
${t('help.self.diff')}
|
|
226
|
+
${t('help.self.apply')}
|
|
218
227
|
|
|
219
|
-
${theme.bold('
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
228
|
+
${theme.bold(t('help.files'))}
|
|
229
|
+
${t('help.files.logs')} ${config.transcript.dir}
|
|
230
|
+
${t('help.files.undo')} ~/.zames/undo
|
|
231
|
+
${t('help.files.sessions')} ${sessionsDir()}
|
|
232
|
+
${t('help.files.profile')} ~/.zames/profile
|
|
233
|
+
${t('help.files.snapshots')} ~/.zames/snapshots
|
|
234
|
+
${t('help.files.tmp')} <project>/tmp (.gitignore, cleaned on start)
|
|
235
|
+
${t('help.files.config')} ${CONFIG_PATHS.HOME_CONFIG}
|
|
227
236
|
${CONFIG_PATHS.PROJECT_CONFIG}
|
|
228
237
|
`);
|
|
229
238
|
}
|
|
230
239
|
function dirLabel(p) {
|
|
231
240
|
return path.basename(p) || p;
|
|
232
241
|
}
|
|
233
|
-
//
|
|
242
|
+
// List of slash commands for completion when you type «/» (Tab — complete).
|
|
243
|
+
// The description is localized by the help.cmd.* key at display time — see
|
|
244
|
+
// buildSlashCommands().
|
|
234
245
|
const SLASH_COMMANDS = [
|
|
235
|
-
{ name: '/help',
|
|
236
|
-
{ name: '/new',
|
|
237
|
-
{ name: '/clear',
|
|
238
|
-
{ name: '/sessions',
|
|
239
|
-
{ name: '/chats',
|
|
240
|
-
{ name: '/resume',
|
|
241
|
-
{ name: '/resume-id',
|
|
242
|
-
{ name: '/chat',
|
|
243
|
-
{ name: '/cd',
|
|
244
|
-
{ name: '/pwd',
|
|
245
|
-
{ name: '/status',
|
|
246
|
-
{ name: '/reload',
|
|
247
|
-
{ name: '/undo',
|
|
248
|
-
{ name: '/undo-list',
|
|
249
|
-
{ name: '/transcript',
|
|
250
|
-
{ name: '/config',
|
|
251
|
-
{ name: '/debug-dom',
|
|
252
|
-
{ name: '/self-review',
|
|
253
|
-
{ name: '/self-fix',
|
|
254
|
-
{ name: '/self-done',
|
|
255
|
-
{ name: '/self-list',
|
|
256
|
-
{ name: '/self-diff',
|
|
257
|
-
{ name: '/self-apply',
|
|
258
|
-
{ name: '/exit',
|
|
259
|
-
{ name: '/quit',
|
|
246
|
+
{ name: '/help', key: 'help.cmd.help' },
|
|
247
|
+
{ name: '/new', key: 'help.cmd.new' },
|
|
248
|
+
{ name: '/clear', key: 'help.cmd.new' },
|
|
249
|
+
{ name: '/sessions', key: 'help.cmd.sessions' },
|
|
250
|
+
{ name: '/chats', key: 'help.cmd.chats' },
|
|
251
|
+
{ name: '/resume', key: 'help.cmd.resume' },
|
|
252
|
+
{ name: '/resume-id', key: 'help.cmd.resume_id' },
|
|
253
|
+
{ name: '/chat', key: 'help.cmd.chat' },
|
|
254
|
+
{ name: '/cd', key: 'help.cmd.cd' },
|
|
255
|
+
{ name: '/pwd', key: 'help.cmd.pwd' },
|
|
256
|
+
{ name: '/status', key: 'help.cmd.status' },
|
|
257
|
+
{ name: '/reload', key: 'help.cmd.reload' },
|
|
258
|
+
{ name: '/undo', key: 'help.cmd.undo' },
|
|
259
|
+
{ name: '/undo-list', key: 'help.cmd.undo_list' },
|
|
260
|
+
{ name: '/transcript', key: 'help.cmd.transcript' },
|
|
261
|
+
{ name: '/config', key: 'help.cmd.config' },
|
|
262
|
+
{ name: '/debug-dom', key: 'help.cmd.debug_dom' },
|
|
263
|
+
{ name: '/self-review', key: 'help.self.review' },
|
|
264
|
+
{ name: '/self-fix', key: 'help.self.fix' },
|
|
265
|
+
{ name: '/self-done', key: 'help.self.done' },
|
|
266
|
+
{ name: '/self-list', key: 'help.self.list' },
|
|
267
|
+
{ name: '/self-diff', key: 'help.self.diff' },
|
|
268
|
+
{ name: '/self-apply', key: 'help.self.apply' },
|
|
269
|
+
{ name: '/exit', key: 'help.cmd.exit' },
|
|
270
|
+
{ name: '/quit', key: 'help.cmd.exit' },
|
|
260
271
|
];
|
|
261
|
-
//
|
|
262
|
-
|
|
272
|
+
// Slash-command descriptions in the current language (for LineEditor hints).
|
|
273
|
+
function buildSlashCommands() {
|
|
274
|
+
return SLASH_COMMANDS.map((c) => ({
|
|
275
|
+
name: c.name,
|
|
276
|
+
description: t(c.key),
|
|
277
|
+
}));
|
|
278
|
+
}
|
|
279
|
+
// We put the agent's temporary files (one-off scripts, etc.) in
|
|
280
|
+
// <project>/tmp — this folder is in .gitignore and is cleaned on every launch.
|
|
263
281
|
const TMP_DIR = path.join(__dirname, '..', 'tmp');
|
|
264
282
|
async function cleanTmpDir() {
|
|
265
283
|
try {
|
|
@@ -271,23 +289,22 @@ async function cleanTmpDir() {
|
|
|
271
289
|
console.error('tmp: не удалось очистить:', e.message);
|
|
272
290
|
}
|
|
273
291
|
}
|
|
274
|
-
//
|
|
275
|
-
// Ctrl+Shift+V,
|
|
292
|
+
// Terminal line input with correct paste handling (Shift+Insert,
|
|
293
|
+
// Ctrl+Shift+V, right mouse button, etc.).
|
|
276
294
|
//
|
|
277
|
-
//
|
|
278
|
-
// 1) readline
|
|
279
|
-
//
|
|
280
|
-
//
|
|
281
|
-
//
|
|
282
|
-
//
|
|
283
|
-
//
|
|
284
|
-
//
|
|
285
|
-
//
|
|
286
|
-
// не считается отправкой.
|
|
295
|
+
// Why a custom reader instead of readline:
|
|
296
|
+
// 1) readline submits the line on the FIRST newline. When pasting
|
|
297
|
+
// multiline text this caused immediate submission and only the first
|
|
298
|
+
// line went to the chat. Here newlines inside a paste are replaced with
|
|
299
|
+
// spaces, and submission happens only on a single Enter press.
|
|
300
|
+
// 2) We enable bracketed paste mode (\x1b[?2004h): the terminal wraps the
|
|
301
|
+
// pasted text in the markers \x1b[200~ … \x1b[201~, so we know for sure
|
|
302
|
+
// that this is a paste, not keyboard typing, and Enter inside it is not
|
|
303
|
+
// treated as submission.
|
|
287
304
|
async function promptOnce(question) {
|
|
288
305
|
const stdin = process.stdin;
|
|
289
306
|
const stdout = process.stdout;
|
|
290
|
-
//
|
|
307
|
+
// Non-TTY (pipe, redirect): read everything up to EOF as a single string.
|
|
291
308
|
if (!stdin.isTTY || !stdin.setRawMode) {
|
|
292
309
|
const chunks = [];
|
|
293
310
|
return await new Promise((resolve) => {
|
|
@@ -305,7 +322,7 @@ async function promptOnce(question) {
|
|
|
305
322
|
const wasRaw = stdin.isRaw;
|
|
306
323
|
stdin.setRawMode(true);
|
|
307
324
|
stdin.resume();
|
|
308
|
-
//
|
|
325
|
+
// We enable/disable bracketed paste in pairs.
|
|
309
326
|
stdout.write('\x1b[?2004h');
|
|
310
327
|
stdout.write(question);
|
|
311
328
|
let line = '';
|
|
@@ -314,11 +331,11 @@ async function promptOnce(question) {
|
|
|
314
331
|
const PASTE_START = '\x1b[200~';
|
|
315
332
|
const PASTE_END = '\x1b[201~';
|
|
316
333
|
const redraw = () => {
|
|
317
|
-
//
|
|
334
|
+
// Return to the start of the line, erase and print again.
|
|
318
335
|
stdout.write(String.fromCharCode(13));
|
|
319
336
|
stdout.write('\x1b[K');
|
|
320
337
|
stdout.write(question + line);
|
|
321
|
-
//
|
|
338
|
+
// Put the cursor in the right position.
|
|
322
339
|
const back = line.length - cursor;
|
|
323
340
|
if (back > 0)
|
|
324
341
|
stdout.write('\x1b[' + back + 'D');
|
|
@@ -334,9 +351,9 @@ async function promptOnce(question) {
|
|
|
334
351
|
resolve(value);
|
|
335
352
|
};
|
|
336
353
|
const insertText = (text) => {
|
|
337
|
-
//
|
|
338
|
-
//
|
|
339
|
-
//
|
|
354
|
+
// Normalize newlines: they come from a multiline paste but mean
|
|
355
|
+
// "submit". Inside the message we replace them with a space so the
|
|
356
|
+
// whole paste goes as ONE message.
|
|
340
357
|
const clean = text
|
|
341
358
|
.replace(/\r\n/g, ' ')
|
|
342
359
|
.replace(/\r/g, ' ')
|
|
@@ -346,11 +363,11 @@ async function promptOnce(question) {
|
|
|
346
363
|
};
|
|
347
364
|
const onData = (buf) => {
|
|
348
365
|
let s = buf.toString('utf-8');
|
|
349
|
-
// Fallback
|
|
350
|
-
//
|
|
351
|
-
//
|
|
352
|
-
//
|
|
353
|
-
//
|
|
366
|
+
// Fallback for terminals without bracketed paste: if the whole chunk is
|
|
367
|
+
// a "bare" newline (one byte), then Enter was pressed → submit.
|
|
368
|
+
// If newlines came TOGETHER with other text in one chunk — that's a
|
|
369
|
+
// paste; such newlines don't submit the message but are replaced with
|
|
370
|
+
// spaces (see insertText).
|
|
354
371
|
if (!inPaste && (s === '\r' || s === '\n')) {
|
|
355
372
|
return finish(line, true);
|
|
356
373
|
}
|
|
@@ -371,14 +388,14 @@ async function promptOnce(question) {
|
|
|
371
388
|
}
|
|
372
389
|
const start = s.indexOf(PASTE_START);
|
|
373
390
|
if (start !== -1) {
|
|
374
|
-
//
|
|
391
|
+
// Everything before the marker is processed as regular input.
|
|
375
392
|
const before = s.slice(0, start);
|
|
376
393
|
s = s.slice(start + PASTE_START.length);
|
|
377
394
|
inPaste = true;
|
|
378
395
|
if (before) {
|
|
379
396
|
for (const ch of before) {
|
|
380
397
|
if (ch === '\r' || ch === '\n') {
|
|
381
|
-
/*
|
|
398
|
+
/* inside a paste — skip */
|
|
382
399
|
}
|
|
383
400
|
else
|
|
384
401
|
insertText(ch);
|
|
@@ -391,22 +408,22 @@ async function promptOnce(question) {
|
|
|
391
408
|
const code = s.charCodeAt(0);
|
|
392
409
|
s = s.slice(1);
|
|
393
410
|
if (ch === '\r' || ch === '\n') {
|
|
394
|
-
//
|
|
395
|
-
// bracketed paste):
|
|
411
|
+
// A newline inside a chunk with other text (paste without
|
|
412
|
+
// bracketed paste): don't submit, insert a space instead.
|
|
396
413
|
insertText(' ');
|
|
397
414
|
redraw();
|
|
398
415
|
continue;
|
|
399
416
|
}
|
|
400
417
|
if (code === 3) {
|
|
401
|
-
// Ctrl+C —
|
|
418
|
+
// Ctrl+C — abort input.
|
|
402
419
|
return finish('', true);
|
|
403
420
|
}
|
|
404
421
|
if (code === 4) {
|
|
405
|
-
// Ctrl+D —
|
|
422
|
+
// Ctrl+D — like submitting an empty line.
|
|
406
423
|
return finish(line, true);
|
|
407
424
|
}
|
|
408
425
|
if (code === 21) {
|
|
409
|
-
// Ctrl+U —
|
|
426
|
+
// Ctrl+U — erase the line.
|
|
410
427
|
line = '';
|
|
411
428
|
cursor = 0;
|
|
412
429
|
redraw();
|
|
@@ -422,7 +439,7 @@ async function promptOnce(question) {
|
|
|
422
439
|
continue;
|
|
423
440
|
}
|
|
424
441
|
if (ch === '\x1b') {
|
|
425
|
-
// Escape
|
|
442
|
+
// Escape sequences (arrows, Home/End, Delete…).
|
|
426
443
|
const rest = s;
|
|
427
444
|
if (rest.startsWith('[D')) {
|
|
428
445
|
if (cursor > 0)
|
|
@@ -459,14 +476,14 @@ async function promptOnce(question) {
|
|
|
459
476
|
s = s.slice(3);
|
|
460
477
|
continue;
|
|
461
478
|
}
|
|
462
|
-
//
|
|
479
|
+
// Other ESC sequences are skipped up to a letter/tilde.
|
|
463
480
|
const m = s.match(/^\[[0-9;]*[A-Za-z~]/);
|
|
464
481
|
if (m)
|
|
465
482
|
s = s.slice(m[0].length);
|
|
466
483
|
continue;
|
|
467
484
|
}
|
|
468
485
|
if (code < 32)
|
|
469
|
-
continue; //
|
|
486
|
+
continue; // other control characters are ignored
|
|
470
487
|
insertText(ch);
|
|
471
488
|
redraw();
|
|
472
489
|
}
|
|
@@ -474,24 +491,24 @@ async function promptOnce(question) {
|
|
|
474
491
|
stdin.on('data', onData);
|
|
475
492
|
});
|
|
476
493
|
}
|
|
477
|
-
//
|
|
494
|
+
// Keyboard monitoring while the agent works.
|
|
478
495
|
//
|
|
479
|
-
//
|
|
480
|
-
// - Esc (
|
|
481
|
-
// -
|
|
482
|
-
//
|
|
483
|
-
//
|
|
496
|
+
// The terminal stays live while the agent thinks:
|
|
497
|
+
// - Esc (or Ctrl+C) — abort the current generation (a Stop click in the browser);
|
|
498
|
+
// - typing + Enter — queue a message; it goes to the agent right after
|
|
499
|
+
// the current task finishes (like "send during generation" in the
|
|
500
|
+
// DeepSeek web version).
|
|
484
501
|
//
|
|
485
|
-
//
|
|
486
|
-
//
|
|
487
|
-
//
|
|
488
|
-
//
|
|
502
|
+
// Input is buffered without a line editor: the typed text is shown in the
|
|
503
|
+
// spinner line via onChange -> ui.setPending(). Enter sends the buffer to
|
|
504
|
+
// onQueue, an empty Enter is ignored. Backspace, Ctrl+U, Esc sequences
|
|
505
|
+
// (arrows/Home/End/Delete are ignored) and bracketed paste are supported.
|
|
489
506
|
function watchInput({ onEscape, onChange, onQueue, } = {}) {
|
|
490
507
|
const stdin = process.stdin;
|
|
491
508
|
if (!stdin.isTTY || !stdin.setRawMode)
|
|
492
509
|
return () => { };
|
|
493
|
-
//
|
|
494
|
-
//
|
|
510
|
+
// Control characters are assembled from codes: this file must not contain
|
|
511
|
+
// "raw" ESC/CR/LF in string literals (see AGENTS.md).
|
|
495
512
|
const ESC = String.fromCharCode(27);
|
|
496
513
|
const CSI = String.fromCharCode(91);
|
|
497
514
|
const CR = String.fromCharCode(13);
|
|
@@ -509,8 +526,8 @@ function watchInput({ onEscape, onChange, onQueue, } = {}) {
|
|
|
509
526
|
if (onChange)
|
|
510
527
|
onChange(buf);
|
|
511
528
|
};
|
|
512
|
-
//
|
|
513
|
-
//
|
|
529
|
+
// Pasted text: newlines inside a multiline paste are treated as spaces —
|
|
530
|
+
// the message goes as a single line.
|
|
514
531
|
const insert = (text) => {
|
|
515
532
|
buf += text
|
|
516
533
|
.split(CR + LF)
|
|
@@ -522,8 +539,8 @@ function watchInput({ onEscape, onChange, onQueue, } = {}) {
|
|
|
522
539
|
};
|
|
523
540
|
function onData(data) {
|
|
524
541
|
let s = data.toString('utf-8');
|
|
525
|
-
//
|
|
526
|
-
//
|
|
542
|
+
// A single Esc — abort generation. Arrows come as a whole chunk and
|
|
543
|
+
// don't reach here.
|
|
527
544
|
if (!inPaste && s === ESC) {
|
|
528
545
|
if (onEscape)
|
|
529
546
|
onEscape();
|
|
@@ -567,13 +584,13 @@ function watchInput({ onEscape, onChange, onQueue, } = {}) {
|
|
|
567
584
|
continue;
|
|
568
585
|
}
|
|
569
586
|
if (code === 3) {
|
|
570
|
-
// Ctrl+C —
|
|
587
|
+
// Ctrl+C — like Esc: abort generation.
|
|
571
588
|
if (onEscape)
|
|
572
589
|
onEscape();
|
|
573
590
|
continue;
|
|
574
591
|
}
|
|
575
592
|
if (code === 21) {
|
|
576
|
-
// Ctrl+U —
|
|
593
|
+
// Ctrl+U — clear what was typed.
|
|
577
594
|
buf = '';
|
|
578
595
|
emitChange();
|
|
579
596
|
continue;
|
|
@@ -587,14 +604,14 @@ function watchInput({ onEscape, onChange, onQueue, } = {}) {
|
|
|
587
604
|
continue;
|
|
588
605
|
}
|
|
589
606
|
if (ch === ESC) {
|
|
590
|
-
// Escape
|
|
607
|
+
// Escape sequence (arrows, Home/End, Delete) — skip.
|
|
591
608
|
const m = s.match(CSI_RE);
|
|
592
609
|
if (m)
|
|
593
610
|
s = s.slice(m[0].length);
|
|
594
611
|
continue;
|
|
595
612
|
}
|
|
596
613
|
if (code < 32)
|
|
597
|
-
continue; //
|
|
614
|
+
continue; // other control characters — ignore
|
|
598
615
|
buf += ch;
|
|
599
616
|
emitChange();
|
|
600
617
|
}
|
|
@@ -609,8 +626,8 @@ function watchInput({ onEscape, onChange, onQueue, } = {}) {
|
|
|
609
626
|
};
|
|
610
627
|
}
|
|
611
628
|
// ---------- workdir resolution ----------
|
|
612
|
-
//
|
|
613
|
-
//
|
|
629
|
+
// The agent works in the directory it was launched from (process.cwd()).
|
|
630
|
+
// This is the sandbox root: tools cannot go above it.
|
|
614
631
|
async function resolveWorkdir() {
|
|
615
632
|
const explicitDir = getArg('--dir', null);
|
|
616
633
|
const dir = explicitDir ? path.resolve(explicitDir) : process.cwd();
|
|
@@ -623,19 +640,19 @@ async function resolveWorkdir() {
|
|
|
623
640
|
// ---------- task runner ----------
|
|
624
641
|
async function runTask(browser, tools, taskText, workdir, opts) {
|
|
625
642
|
const { transcript, freshChat, sendSystemPrompt, queue = [], ui: editor, onChatReady, } = opts;
|
|
626
|
-
//
|
|
627
|
-
// Ctrl+C)
|
|
628
|
-
// (
|
|
629
|
-
//
|
|
643
|
+
// In TTY mode the UI is a LineEditor: it owns the input (queue, Esc,
|
|
644
|
+
// Ctrl+C) and draws the status ABOVE the permanent input line. In non-TTY
|
|
645
|
+
// mode (pipes) — a regular spinner + watchInput.
|
|
646
|
+
// A new task from the prompt — we reset the "stop" from the previous abort.
|
|
630
647
|
browser._stopped = false;
|
|
631
648
|
browser._abort = false;
|
|
632
|
-
const ui = editor || mod.createSpinner();
|
|
649
|
+
const ui = editor || mod.createSpinner(currentLocale);
|
|
633
650
|
const stopWatching = editor
|
|
634
651
|
? () => { }
|
|
635
652
|
: watchInput({
|
|
636
653
|
onEscape: () => {
|
|
637
654
|
ui.stop();
|
|
638
|
-
console.error(theme.warn('
|
|
655
|
+
console.error(theme.warn(t('msg.abort_gen_short')));
|
|
639
656
|
browser.stopGeneration().catch(() => { });
|
|
640
657
|
},
|
|
641
658
|
onChange: (text) => ui.setPending(text),
|
|
@@ -643,15 +660,15 @@ async function runTask(browser, tools, taskText, workdir, opts) {
|
|
|
643
660
|
queue.push(text);
|
|
644
661
|
ui.setPending(null);
|
|
645
662
|
ui.stop();
|
|
646
|
-
console.log(theme.user('
|
|
663
|
+
console.log(theme.user(t('msg.queued', { n: queue.length })) +
|
|
647
664
|
theme.assistant(text));
|
|
648
665
|
ui.thinking();
|
|
649
666
|
},
|
|
650
667
|
});
|
|
651
668
|
try {
|
|
652
669
|
let next = { task: taskText, freshChat, sendSystemPrompt };
|
|
653
|
-
//
|
|
654
|
-
//
|
|
670
|
+
// Execute the task, then everything the user managed to type while it
|
|
671
|
+
// ran. The queue may be replenished right during draining.
|
|
655
672
|
while (true) {
|
|
656
673
|
ui.thinking();
|
|
657
674
|
await mod.runAgentLoop({
|
|
@@ -667,11 +684,13 @@ async function runTask(browser, tools, taskText, workdir, opts) {
|
|
|
667
684
|
onToolCall: (name, toolArgs) => ui.toolCall(name, toolArgs),
|
|
668
685
|
onToolResult: (result) => ui.toolResult(result),
|
|
669
686
|
onAssistantMessage: (msg) => ui.assistant(msg),
|
|
687
|
+
onWarning: (msg) => ui.warning(msg),
|
|
670
688
|
onChatReady,
|
|
671
689
|
debugLog: debug,
|
|
690
|
+
locale: currentLocale,
|
|
672
691
|
});
|
|
673
|
-
//
|
|
674
|
-
//
|
|
692
|
+
// Aborted (Esc/Ctrl+C) — we don't start the next tasks from the queue
|
|
693
|
+
// and clear it, so "stop" really stops everything.
|
|
675
694
|
if (browser._stopped) {
|
|
676
695
|
queue.length = 0;
|
|
677
696
|
break;
|
|
@@ -681,10 +700,10 @@ async function runTask(browser, tools, taskText, workdir, opts) {
|
|
|
681
700
|
const queued = queue.shift() ?? '';
|
|
682
701
|
ui.stop();
|
|
683
702
|
if (editor) {
|
|
684
|
-
editor.printAbove(theme.user('
|
|
703
|
+
editor.printAbove(theme.user(t('msg.from_queue')) + theme.assistant(queued));
|
|
685
704
|
}
|
|
686
705
|
else {
|
|
687
|
-
console.log(theme.user('
|
|
706
|
+
console.log(theme.user(t('msg.from_queue')) + theme.assistant(queued));
|
|
688
707
|
}
|
|
689
708
|
transcript?.log('queued_task', { task: queued });
|
|
690
709
|
next = { task: queued, freshChat: false, sendSystemPrompt: false };
|
|
@@ -692,7 +711,7 @@ async function runTask(browser, tools, taskText, workdir, opts) {
|
|
|
692
711
|
}
|
|
693
712
|
catch (e) {
|
|
694
713
|
ui.stop();
|
|
695
|
-
console.error(theme.error(String.fromCharCode(10) + '
|
|
714
|
+
console.error(theme.error(String.fromCharCode(10) + t('msg.agent_error')), e.message);
|
|
696
715
|
if (debug)
|
|
697
716
|
console.error(e.stack);
|
|
698
717
|
transcript?.log('agent_error', { error: e.message });
|
|
@@ -714,28 +733,28 @@ async function main() {
|
|
|
714
733
|
return;
|
|
715
734
|
}
|
|
716
735
|
if (calibrate) {
|
|
717
|
-
console.log(theme.warn('
|
|
736
|
+
console.log(theme.warn(t('calibrate')));
|
|
718
737
|
}
|
|
719
738
|
await cleanTmpDir();
|
|
720
739
|
let currentWorkdir;
|
|
721
740
|
let sandboxRoot;
|
|
722
741
|
try {
|
|
723
742
|
currentWorkdir = await resolveWorkdir();
|
|
724
|
-
//
|
|
743
|
+
// Sandbox root: the agent cannot go above the launch directory.
|
|
725
744
|
sandboxRoot = currentWorkdir;
|
|
726
745
|
}
|
|
727
746
|
catch (e) {
|
|
728
|
-
console.error(theme.error('
|
|
747
|
+
console.error(theme.error(t('msg.workdir_error')), e.message);
|
|
729
748
|
process.exit(1);
|
|
730
749
|
}
|
|
731
|
-
console.log(theme.system(
|
|
750
|
+
console.log(theme.system(t('msg.working_dir', { v: currentWorkdir })));
|
|
732
751
|
const transcript = new Transcript({
|
|
733
752
|
dir: config.transcript.dir,
|
|
734
753
|
enabled: config.transcript.enabled,
|
|
735
754
|
sessionName: dirLabel(currentWorkdir),
|
|
736
755
|
});
|
|
737
756
|
if (transcript.file) {
|
|
738
|
-
console.log(theme.system(
|
|
757
|
+
console.log(theme.system(t('msg.transcript', { v: transcript.file })));
|
|
739
758
|
}
|
|
740
759
|
const undo = new UndoStore(config.undo);
|
|
741
760
|
const browser = new DeepSeekBrowser({
|
|
@@ -744,7 +763,7 @@ async function main() {
|
|
|
744
763
|
channel: config.browserChannel,
|
|
745
764
|
...config.browser,
|
|
746
765
|
});
|
|
747
|
-
const bootSpinner = mod.createSpinner();
|
|
766
|
+
const bootSpinner = mod.createSpinner(currentLocale);
|
|
748
767
|
bootSpinner.thinking();
|
|
749
768
|
try {
|
|
750
769
|
await browser.launch();
|
|
@@ -753,38 +772,38 @@ async function main() {
|
|
|
753
772
|
}
|
|
754
773
|
catch (e) {
|
|
755
774
|
bootSpinner.stop();
|
|
756
|
-
console.error(theme.error('
|
|
775
|
+
console.error(theme.error(t('msg.browser_error')), e.message);
|
|
757
776
|
if (debug)
|
|
758
777
|
console.error(e.stack);
|
|
759
778
|
await browser.close().catch(() => { });
|
|
760
779
|
transcript.close();
|
|
761
780
|
process.exit(1);
|
|
762
781
|
}
|
|
763
|
-
//
|
|
782
|
+
// One-shot mode
|
|
764
783
|
if (task) {
|
|
765
784
|
const tools = mod.createTools(currentWorkdir, { undo });
|
|
766
785
|
let freshChat = true;
|
|
767
786
|
let sendSystemPrompt = true;
|
|
768
|
-
//
|
|
769
|
-
//
|
|
787
|
+
// By default we start a new chat. To continue a previous session —
|
|
788
|
+
// explicitly: --chat <id> or --resume-last.
|
|
770
789
|
let resumeId = chatIdArg;
|
|
771
790
|
if (!resumeId && resumeLastFlag && !newChatFlag) {
|
|
772
791
|
const last = loadLastSession(currentWorkdir);
|
|
773
792
|
if (last && last.id) {
|
|
774
793
|
resumeId = last.id;
|
|
775
|
-
console.log(theme.system(
|
|
794
|
+
console.log(theme.system(t('msg.resuming', { id: resumeId })));
|
|
776
795
|
}
|
|
777
796
|
}
|
|
778
797
|
if (resumeId) {
|
|
779
798
|
try {
|
|
780
|
-
console.log(theme.system(
|
|
799
|
+
console.log(theme.system(t('msg.opening_chat', { id: resumeId })));
|
|
781
800
|
await browser.openChat(resumeId);
|
|
782
801
|
freshChat = false;
|
|
783
802
|
sendSystemPrompt = resendPrompt;
|
|
784
803
|
saveLastChat(resumeId, currentWorkdir);
|
|
785
804
|
}
|
|
786
805
|
catch (e) {
|
|
787
|
-
console.error(theme.error(
|
|
806
|
+
console.error(theme.error(t('msg.open_chat_error', { v: e.message })));
|
|
788
807
|
}
|
|
789
808
|
}
|
|
790
809
|
await autoReload();
|
|
@@ -802,36 +821,37 @@ async function main() {
|
|
|
802
821
|
transcript.close();
|
|
803
822
|
return;
|
|
804
823
|
}
|
|
805
|
-
console.log(theme.system('
|
|
806
|
-
console.log(theme.system('
|
|
824
|
+
console.log(theme.system(t('msg.interactive')));
|
|
825
|
+
console.log(theme.system(t('msg.queue_hint')));
|
|
807
826
|
let freshChatNext = true;
|
|
808
827
|
let sendSystemPromptNext = true;
|
|
809
828
|
let lastChats = [];
|
|
810
829
|
let currentChatId = null;
|
|
811
830
|
let running = true;
|
|
812
|
-
//
|
|
813
|
-
//
|
|
831
|
+
// Messages the user typed while the agent worked. runTask takes them one
|
|
832
|
+
// by one after the current task finishes.
|
|
814
833
|
const pendingQueue = [];
|
|
815
834
|
// ---------- review mode state ----------
|
|
816
|
-
// null —
|
|
817
|
-
// { snapDir, snapName, originalWorkdir } —
|
|
818
|
-
//
|
|
835
|
+
// null — normal mode.
|
|
836
|
+
// { snapDir, snapName, originalWorkdir } — we're inside a snapshot, the chat
|
|
837
|
+
// is already initialized with the review prompt, the user can just type "fix...".
|
|
819
838
|
let reviewMode = null;
|
|
820
839
|
process.on('SIGINT', async () => {
|
|
821
840
|
running = false;
|
|
822
841
|
await browser.close().catch(() => { });
|
|
823
|
-
//
|
|
824
|
-
//
|
|
842
|
+
// Close the lazy headless browser from web.js, otherwise it would stay
|
|
843
|
+
// as a separate process after the agent exits.
|
|
825
844
|
await mod.closeWeb().catch(() => { });
|
|
826
845
|
transcript.close();
|
|
827
|
-
console.log(theme.system('
|
|
846
|
+
console.log(theme.system(String.fromCharCode(10) + t('msg.bye')));
|
|
828
847
|
process.exit(0);
|
|
829
848
|
});
|
|
830
|
-
//
|
|
831
|
-
//
|
|
832
|
-
//
|
|
833
|
-
// --
|
|
834
|
-
// --
|
|
849
|
+
// By default we start a NEW chat on launch: the context of a previous
|
|
850
|
+
// session is not carried over automatically. To continue a previous
|
|
851
|
+
// session explicitly:
|
|
852
|
+
// --chat <id> open a specific chat
|
|
853
|
+
// --resume-last return to the last chat for this working directory
|
|
854
|
+
// --new-chat kept for compatibility (this is the default behavior)
|
|
835
855
|
let resumeId = chatIdArg;
|
|
836
856
|
if (!resumeId && resumeLastFlag && !newChatFlag) {
|
|
837
857
|
const last = loadLastSession(currentWorkdir);
|
|
@@ -842,23 +862,23 @@ async function main() {
|
|
|
842
862
|
}
|
|
843
863
|
if (resumeId) {
|
|
844
864
|
try {
|
|
845
|
-
console.log(theme.system(
|
|
865
|
+
console.log(theme.system(t('msg.opening_chat', { id: resumeId })));
|
|
846
866
|
await browser.openChat(resumeId);
|
|
847
867
|
currentChatId = resumeId;
|
|
848
868
|
freshChatNext = false;
|
|
849
869
|
sendSystemPromptNext = resendPrompt;
|
|
850
870
|
saveLastChat(resumeId, currentWorkdir);
|
|
851
|
-
console.log(theme.system(
|
|
871
|
+
console.log(theme.system(t('msg.chat_opened') + ' ' + resumeId + String.fromCharCode(10)));
|
|
852
872
|
}
|
|
853
873
|
catch (e) {
|
|
854
|
-
console.error(theme.error(
|
|
874
|
+
console.error(theme.error(t('msg.open_chat_error', { v: e.message })));
|
|
855
875
|
}
|
|
856
876
|
}
|
|
857
|
-
// ----------
|
|
858
|
-
//
|
|
859
|
-
//
|
|
860
|
-
//
|
|
861
|
-
//
|
|
877
|
+
// ---------- input: permanent line below + status above ----------
|
|
878
|
+
// In TTY we use LineEditor: it owns the input all the time, shows the
|
|
879
|
+
// status above the input line and prints the agent's answers ABOVE it, so
|
|
880
|
+
// the typed text is never overwritten by output. In non-TTY (pipe) —
|
|
881
|
+
// the old promptOnce.
|
|
862
882
|
let editor = null;
|
|
863
883
|
let waiter = null;
|
|
864
884
|
const takeInput = () => {
|
|
@@ -872,7 +892,7 @@ async function main() {
|
|
|
872
892
|
let tail;
|
|
873
893
|
if (reviewMode) {
|
|
874
894
|
tail =
|
|
875
|
-
theme.warn('
|
|
895
|
+
theme.warn(t('prompt.review')) + theme.dim(':') + theme.dir(reviewMode.snapName);
|
|
876
896
|
}
|
|
877
897
|
else {
|
|
878
898
|
tail = theme.dir(dirLabel(currentWorkdir));
|
|
@@ -882,7 +902,7 @@ async function main() {
|
|
|
882
902
|
if (process.stdin.isTTY && process.stdout.isTTY) {
|
|
883
903
|
const ed = new LineEditor({
|
|
884
904
|
prompt: buildPrompt(),
|
|
885
|
-
commands:
|
|
905
|
+
commands: buildSlashCommands(),
|
|
886
906
|
});
|
|
887
907
|
editor = ed;
|
|
888
908
|
ed.onSubmit = (text) => {
|
|
@@ -895,17 +915,17 @@ async function main() {
|
|
|
895
915
|
};
|
|
896
916
|
ed.onEscape = () => {
|
|
897
917
|
if (ed.busy) {
|
|
898
|
-
ed.printAbove(theme.warn('
|
|
918
|
+
ed.printAbove(theme.warn(t('msg.abort_gen_short')));
|
|
899
919
|
browser.stopGeneration().catch(() => { });
|
|
900
920
|
}
|
|
901
921
|
};
|
|
902
922
|
ed.onCtrlC = () => {
|
|
903
923
|
if (ed.busy) {
|
|
904
|
-
ed.printAbove(theme.warn('
|
|
924
|
+
ed.printAbove(theme.warn(t('msg.abort_ctrlc_short')));
|
|
905
925
|
browser.stopGeneration().catch(() => { });
|
|
906
926
|
}
|
|
907
927
|
else {
|
|
908
|
-
//
|
|
928
|
+
// Not busy — exit. We wake takeInput() so the loop finishes.
|
|
909
929
|
running = false;
|
|
910
930
|
if (waiter) {
|
|
911
931
|
const r = waiter;
|
|
@@ -915,9 +935,9 @@ async function main() {
|
|
|
915
935
|
}
|
|
916
936
|
};
|
|
917
937
|
ed.start();
|
|
918
|
-
//
|
|
919
|
-
//
|
|
920
|
-
//
|
|
938
|
+
// All command output (console.log/error) must go ABOVE the input line,
|
|
939
|
+
// otherwise it overwrites the text being typed. While the editor is
|
|
940
|
+
// active, we wrap both streams in ed.printAbove.
|
|
921
941
|
const origLog = console.log.bind(console);
|
|
922
942
|
const origErr = console.error.bind(console);
|
|
923
943
|
const fmt = (a) => typeof a === 'string'
|
|
@@ -932,11 +952,211 @@ async function main() {
|
|
|
932
952
|
})();
|
|
933
953
|
console.log = (...a) => ed.printAbove(a.map(fmt).join(' '));
|
|
934
954
|
console.error = (...a) => ed.printAbove(a.map(fmt).join(' '));
|
|
935
|
-
//
|
|
955
|
+
// Save for debugging.
|
|
936
956
|
const edAny = ed;
|
|
937
957
|
edAny._origLog = origLog;
|
|
938
958
|
edAny._origErr = origErr;
|
|
939
959
|
}
|
|
960
|
+
// ---------- /config ----------
|
|
961
|
+
// View and edit settings without a restart. Values are validated against
|
|
962
|
+
// CONFIG_SCHEMA (src/config.ts) and written to the project .zamesrc.json.
|
|
963
|
+
// The language (ui.locale) is applied immediately: we change currentLocale,
|
|
964
|
+
// rebuild the editor hints and update config.ui.locale (agent-loop reads it
|
|
965
|
+
// on the next task).
|
|
966
|
+
//
|
|
967
|
+
// With no arguments in an interactive terminal a menu opens (arrows,
|
|
968
|
+
// Enter — edit, d — reset, q — quit). There are also text subcommands
|
|
969
|
+
// (list/get/set/reset/lang/path) — for scripts and non-TTY.
|
|
970
|
+
function setConfigRuntime(path, value) {
|
|
971
|
+
const segs = path.split('.');
|
|
972
|
+
let obj = config;
|
|
973
|
+
for (let i = 0; i < segs.length - 1; i++) {
|
|
974
|
+
obj = obj[segs[i]];
|
|
975
|
+
}
|
|
976
|
+
obj[segs[segs.length - 1]] = value;
|
|
977
|
+
if (path === 'ui.locale' && isLocale(value)) {
|
|
978
|
+
currentLocale = value;
|
|
979
|
+
config.ui.locale = value;
|
|
980
|
+
if (editor) {
|
|
981
|
+
editor.setCommands(buildSlashCommands());
|
|
982
|
+
editor.setPrompt(buildPrompt());
|
|
983
|
+
}
|
|
984
|
+
}
|
|
985
|
+
}
|
|
986
|
+
function configSetRaw(field, raw) {
|
|
987
|
+
const value = validateConfigValue(field, raw);
|
|
988
|
+
writeConfigValue('project', field.path, raw);
|
|
989
|
+
setConfigRuntime(field.path, value);
|
|
990
|
+
}
|
|
991
|
+
function configResetField(field) {
|
|
992
|
+
resetConfigValue('project', field.path);
|
|
993
|
+
// Reset the runtime value to the default.
|
|
994
|
+
const def = getByPath(DEFAULTS, field.path);
|
|
995
|
+
setConfigRuntime(field.path, def);
|
|
996
|
+
}
|
|
997
|
+
function configLabel(field) {
|
|
998
|
+
return t(field.labelKey);
|
|
999
|
+
}
|
|
1000
|
+
function configShowList() {
|
|
1001
|
+
console.log(theme.system(t('cfg.title')));
|
|
1002
|
+
let lastGroup = '';
|
|
1003
|
+
for (const f of CONFIG_SCHEMA) {
|
|
1004
|
+
const g = t(f.groupKey);
|
|
1005
|
+
if (g !== lastGroup) {
|
|
1006
|
+
console.log(theme.system('\n ' + g));
|
|
1007
|
+
lastGroup = g;
|
|
1008
|
+
}
|
|
1009
|
+
const cur = getByPath(config, f.path);
|
|
1010
|
+
const shown = cur === undefined
|
|
1011
|
+
? t('cfg.menu.default')
|
|
1012
|
+
: typeof cur === 'boolean'
|
|
1013
|
+
? cur
|
|
1014
|
+
? t('common.on')
|
|
1015
|
+
: t('common.off')
|
|
1016
|
+
: JSON.stringify(cur);
|
|
1017
|
+
const extra = f.values ? ' [' + f.values.join('|') + ']' : '';
|
|
1018
|
+
console.log(' ' +
|
|
1019
|
+
theme.user(f.path) +
|
|
1020
|
+
theme.dim(' = ') +
|
|
1021
|
+
theme.assistant(shown) +
|
|
1022
|
+
theme.dim(extra) +
|
|
1023
|
+
theme.dim(' # ' + configLabel(f)));
|
|
1024
|
+
}
|
|
1025
|
+
console.log(theme.dim('\n' + t('cfg.usage')));
|
|
1026
|
+
}
|
|
1027
|
+
async function configOpenMenu() {
|
|
1028
|
+
if (!(process.stdin.isTTY && process.stdout.isTTY)) {
|
|
1029
|
+
configShowList();
|
|
1030
|
+
return;
|
|
1031
|
+
}
|
|
1032
|
+
// The menu draws directly to stdout and reads keys itself. So that its
|
|
1033
|
+
// output doesn't overlap the LineEditor's permanent input line, we "pause"
|
|
1034
|
+
// the editor for the duration of the menu and restore it afterwards.
|
|
1035
|
+
if (editor)
|
|
1036
|
+
editor.pause();
|
|
1037
|
+
try {
|
|
1038
|
+
await runConfigMenu({
|
|
1039
|
+
fields: CONFIG_SCHEMA,
|
|
1040
|
+
t,
|
|
1041
|
+
get: (path) => getByPath(config, path),
|
|
1042
|
+
set: (field, raw) => configSetRaw(field, raw),
|
|
1043
|
+
reset: (field) => configResetField(field),
|
|
1044
|
+
});
|
|
1045
|
+
}
|
|
1046
|
+
catch (e) {
|
|
1047
|
+
console.error(theme.error(String(e.message || e)));
|
|
1048
|
+
}
|
|
1049
|
+
finally {
|
|
1050
|
+
if (editor)
|
|
1051
|
+
editor.resume();
|
|
1052
|
+
}
|
|
1053
|
+
}
|
|
1054
|
+
async function handleConfigCommand(input) {
|
|
1055
|
+
const parts = input.trim().split(/\s+/);
|
|
1056
|
+
const sub = (parts[1] || '').toLowerCase();
|
|
1057
|
+
// With no subcommand — the menu (or a text list in non-TTY).
|
|
1058
|
+
if (!sub || sub === 'menu' || sub === 'ui') {
|
|
1059
|
+
await configOpenMenu();
|
|
1060
|
+
return;
|
|
1061
|
+
}
|
|
1062
|
+
if (sub === 'list' || sub === 'show' || sub === 'ls') {
|
|
1063
|
+
configShowList();
|
|
1064
|
+
return;
|
|
1065
|
+
}
|
|
1066
|
+
if (sub === 'path' || sub === 'paths') {
|
|
1067
|
+
console.log(theme.system(t('cfg.paths', {
|
|
1068
|
+
global: CONFIG_PATHS.HOME_CONFIG,
|
|
1069
|
+
project: CONFIG_PATHS.PROJECT_CONFIG,
|
|
1070
|
+
})));
|
|
1071
|
+
return;
|
|
1072
|
+
}
|
|
1073
|
+
// /config lang <ru|en> — quick access to ui.locale
|
|
1074
|
+
if (sub === 'lang' || sub === 'language' || sub === 'язык') {
|
|
1075
|
+
const val = parts[2];
|
|
1076
|
+
if (!val) {
|
|
1077
|
+
console.log(theme.system(t('status.locale', { v: localeDisplayName(currentLocale) })));
|
|
1078
|
+
console.log(theme.dim(t('cfg.lang_usage')));
|
|
1079
|
+
return;
|
|
1080
|
+
}
|
|
1081
|
+
const field = CONFIG_SCHEMA.find((f) => f.path === 'ui.locale');
|
|
1082
|
+
const loc = normalizeLocale(val);
|
|
1083
|
+
try {
|
|
1084
|
+
configSetRaw(field, loc);
|
|
1085
|
+
}
|
|
1086
|
+
catch (e) {
|
|
1087
|
+
console.error(theme.error(t('cfg.write_error', { v: e.message })));
|
|
1088
|
+
return;
|
|
1089
|
+
}
|
|
1090
|
+
console.log(theme.assistant(t('cfg.lang_set', { v: localeDisplayName(loc) })));
|
|
1091
|
+
return;
|
|
1092
|
+
}
|
|
1093
|
+
if (sub === 'get') {
|
|
1094
|
+
const key = parts[2];
|
|
1095
|
+
if (!key) {
|
|
1096
|
+
console.log(theme.dim(t('cfg.usage')));
|
|
1097
|
+
return;
|
|
1098
|
+
}
|
|
1099
|
+
const field = CONFIG_SCHEMA.find((f) => f.path === key);
|
|
1100
|
+
if (!field) {
|
|
1101
|
+
console.error(theme.error(t('cfg.unknown_key', { v: key })));
|
|
1102
|
+
return;
|
|
1103
|
+
}
|
|
1104
|
+
const cur = getByPath(config, key);
|
|
1105
|
+
console.log(theme.system(t('cfg.value', {
|
|
1106
|
+
v: key,
|
|
1107
|
+
value: cur === undefined ? t('cfg.menu.default') : JSON.stringify(cur),
|
|
1108
|
+
})));
|
|
1109
|
+
return;
|
|
1110
|
+
}
|
|
1111
|
+
if (sub === 'set') {
|
|
1112
|
+
const key = parts[2];
|
|
1113
|
+
const raw = parts.slice(3).join(' ');
|
|
1114
|
+
const field = CONFIG_SCHEMA.find((f) => f.path === key);
|
|
1115
|
+
if (!field) {
|
|
1116
|
+
console.error(theme.error(t('cfg.unknown_key', { v: key || '' })));
|
|
1117
|
+
return;
|
|
1118
|
+
}
|
|
1119
|
+
if (!raw) {
|
|
1120
|
+
console.log(theme.dim(t('cfg.usage')));
|
|
1121
|
+
return;
|
|
1122
|
+
}
|
|
1123
|
+
try {
|
|
1124
|
+
configSetRaw(field, raw);
|
|
1125
|
+
}
|
|
1126
|
+
catch {
|
|
1127
|
+
console.error(theme.error(t('cfg.bad_value', {
|
|
1128
|
+
v: key,
|
|
1129
|
+
type: field.values ? field.values.join('|') : field.type,
|
|
1130
|
+
})));
|
|
1131
|
+
return;
|
|
1132
|
+
}
|
|
1133
|
+
console.log(theme.assistant(t('cfg.saved', {
|
|
1134
|
+
v: key,
|
|
1135
|
+
value: JSON.stringify(getByPath(config, key)),
|
|
1136
|
+
})));
|
|
1137
|
+
return;
|
|
1138
|
+
}
|
|
1139
|
+
if (sub === 'reset' || sub === 'unset') {
|
|
1140
|
+
const key = parts[2];
|
|
1141
|
+
const field = CONFIG_SCHEMA.find((f) => f.path === key);
|
|
1142
|
+
if (!field) {
|
|
1143
|
+
console.error(theme.error(t('cfg.unknown_key', { v: key || '' })));
|
|
1144
|
+
return;
|
|
1145
|
+
}
|
|
1146
|
+
try {
|
|
1147
|
+
configResetField(field);
|
|
1148
|
+
}
|
|
1149
|
+
catch (e) {
|
|
1150
|
+
console.error(theme.error(t('cfg.write_error', { v: e.message })));
|
|
1151
|
+
return;
|
|
1152
|
+
}
|
|
1153
|
+
console.log(theme.assistant(t('cfg.reset', { v: key })));
|
|
1154
|
+
return;
|
|
1155
|
+
}
|
|
1156
|
+
// Unknown subcommand — show the list.
|
|
1157
|
+
console.error(theme.error(t('cfg.unknown_key', { v: sub })));
|
|
1158
|
+
configShowList();
|
|
1159
|
+
}
|
|
940
1160
|
while (running) {
|
|
941
1161
|
let input;
|
|
942
1162
|
try {
|
|
@@ -948,7 +1168,7 @@ async function main() {
|
|
|
948
1168
|
let tail;
|
|
949
1169
|
if (reviewMode) {
|
|
950
1170
|
tail =
|
|
951
|
-
theme.warn('
|
|
1171
|
+
theme.warn(t('prompt.review')) +
|
|
952
1172
|
theme.dim(':') +
|
|
953
1173
|
theme.dir(reviewMode.snapName);
|
|
954
1174
|
}
|
|
@@ -972,7 +1192,7 @@ async function main() {
|
|
|
972
1192
|
continue;
|
|
973
1193
|
}
|
|
974
1194
|
if (['/new', '/clear', 'new'].includes(lower)) {
|
|
975
|
-
console.log(theme.system('
|
|
1195
|
+
console.log(theme.system(t('msg.new_chat')));
|
|
976
1196
|
try {
|
|
977
1197
|
await browser.newChat();
|
|
978
1198
|
freshChatNext = false;
|
|
@@ -980,17 +1200,17 @@ async function main() {
|
|
|
980
1200
|
currentChatId = await browser.getCurrentChatId();
|
|
981
1201
|
saveLastChat(currentChatId, currentWorkdir);
|
|
982
1202
|
transcript.log('new_chat');
|
|
983
|
-
console.log(theme.system('
|
|
1203
|
+
console.log(theme.system(t('msg.new_chat_ok') + String.fromCharCode(10)));
|
|
984
1204
|
}
|
|
985
1205
|
catch (e) {
|
|
986
|
-
console.error(theme.error('
|
|
1206
|
+
console.error(theme.error(t('msg.new_chat_error', { v: e.message })), e.message);
|
|
987
1207
|
}
|
|
988
1208
|
continue;
|
|
989
1209
|
}
|
|
990
|
-
// ----------
|
|
1210
|
+
// ---------- Self-review ----------
|
|
991
1211
|
if (lower === '/self-review' || lower.startsWith('/self-review ')) {
|
|
992
1212
|
const focus = trimmed.slice('/self-review'.length).trim();
|
|
993
|
-
//
|
|
1213
|
+
// Remember where to return
|
|
994
1214
|
const originalWorkdir = reviewMode
|
|
995
1215
|
? reviewMode.originalWorkdir
|
|
996
1216
|
: currentWorkdir;
|
|
@@ -1001,10 +1221,10 @@ async function main() {
|
|
|
1001
1221
|
focus: focus || undefined,
|
|
1002
1222
|
transcript,
|
|
1003
1223
|
});
|
|
1004
|
-
//
|
|
1005
|
-
// -
|
|
1006
|
-
// -
|
|
1007
|
-
//
|
|
1224
|
+
// Switch to review mode:
|
|
1225
|
+
// - working directory = snapshot
|
|
1226
|
+
// - we do NOT reset the chat — inside selfReview a fresh chat was
|
|
1227
|
+
// already created and the review prompt sent, we continue in it
|
|
1008
1228
|
reviewMode = {
|
|
1009
1229
|
snapDir: result.snapDir,
|
|
1010
1230
|
snapName: path.basename(result.snapDir),
|
|
@@ -1015,16 +1235,10 @@ async function main() {
|
|
|
1015
1235
|
sendSystemPromptNext = false;
|
|
1016
1236
|
currentChatId = await browser.getCurrentChatId();
|
|
1017
1237
|
saveLastChat(currentChatId, currentWorkdir);
|
|
1018
|
-
console.log(theme.user('
|
|
1019
|
-
' «исправь ошибки»\n' +
|
|
1020
|
-
' «доработай обработку ошибок в ask()»\n' +
|
|
1021
|
-
' «покажи, что не так с undo»\n' +
|
|
1022
|
-
'Выйти: /self-done. Применить: /self-apply ' +
|
|
1023
|
-
reviewMode.snapName +
|
|
1024
|
-
'\n'));
|
|
1238
|
+
console.log(theme.user(t('self.review_hint', { name: reviewMode.snapName })));
|
|
1025
1239
|
}
|
|
1026
1240
|
catch (e) {
|
|
1027
|
-
console.error(theme.error('
|
|
1241
|
+
console.error(theme.error(t('self.review_failed')), e.message);
|
|
1028
1242
|
if (debug)
|
|
1029
1243
|
console.error(e.stack);
|
|
1030
1244
|
}
|
|
@@ -1033,7 +1247,7 @@ async function main() {
|
|
|
1033
1247
|
if (lower === '/self-fix' || lower.startsWith('/self-fix ')) {
|
|
1034
1248
|
const rest = trimmed.slice('/self-fix'.length).trim();
|
|
1035
1249
|
if (!rest) {
|
|
1036
|
-
console.error(theme.error('
|
|
1250
|
+
console.error(theme.error(t('self.fix_usage')));
|
|
1037
1251
|
continue;
|
|
1038
1252
|
}
|
|
1039
1253
|
const sp = rest.indexOf(' ');
|
|
@@ -1042,26 +1256,26 @@ async function main() {
|
|
|
1042
1256
|
const snapRoot = path.join(ZAMES_HOME, 'snapshots', name);
|
|
1043
1257
|
const stat = await fs.stat(snapRoot).catch(() => null);
|
|
1044
1258
|
if (!stat || !stat.isDirectory()) {
|
|
1045
|
-
console.error(theme.error(
|
|
1259
|
+
console.error(theme.error(t('self.snapshot_not_found', { v: snapRoot })));
|
|
1046
1260
|
continue;
|
|
1047
1261
|
}
|
|
1048
1262
|
const originalWorkdir = reviewMode
|
|
1049
1263
|
? reviewMode.originalWorkdir
|
|
1050
1264
|
: currentWorkdir;
|
|
1051
|
-
//
|
|
1265
|
+
// Fresh chat + review prompt for this snapshot
|
|
1052
1266
|
try {
|
|
1053
1267
|
const { runAgentLoop: ral } = await import('./agent-loop.js');
|
|
1054
|
-
const { buildSystemPrompt } = await import('./system-prompt.js');
|
|
1055
1268
|
const tools = mod.createTools(snapRoot, { undo: null });
|
|
1056
1269
|
await browser.newChat();
|
|
1057
1270
|
const sysPrompt = mod.buildSystemPrompt({
|
|
1058
1271
|
workdir: snapRoot,
|
|
1059
1272
|
tools,
|
|
1273
|
+
locale: currentLocale,
|
|
1060
1274
|
});
|
|
1061
|
-
console.log(theme.system('
|
|
1275
|
+
console.log(theme.system(t('self.init_review_chat')));
|
|
1062
1276
|
await browser.ask(sysPrompt, { timeout: 60_000 });
|
|
1063
1277
|
if (focus) {
|
|
1064
|
-
const ui = mod.createSpinner();
|
|
1278
|
+
const ui = mod.createSpinner(currentLocale);
|
|
1065
1279
|
ui.thinking();
|
|
1066
1280
|
await ral({
|
|
1067
1281
|
browser,
|
|
@@ -1078,6 +1292,8 @@ async function main() {
|
|
|
1078
1292
|
onAssistantMessage: (m) => {
|
|
1079
1293
|
ui.assistant(m);
|
|
1080
1294
|
},
|
|
1295
|
+
onWarning: (m) => ui.warning(m),
|
|
1296
|
+
locale: currentLocale,
|
|
1081
1297
|
});
|
|
1082
1298
|
}
|
|
1083
1299
|
reviewMode = {
|
|
@@ -1090,25 +1306,25 @@ async function main() {
|
|
|
1090
1306
|
sendSystemPromptNext = false;
|
|
1091
1307
|
currentChatId = await browser.getCurrentChatId();
|
|
1092
1308
|
saveLastChat(currentChatId, currentWorkdir);
|
|
1093
|
-
console.log(theme.user(
|
|
1309
|
+
console.log(theme.user(t('self.fix_hint', { name })));
|
|
1094
1310
|
}
|
|
1095
1311
|
catch (e) {
|
|
1096
|
-
console.error(theme.error('
|
|
1312
|
+
console.error(theme.error(t('self.enter_failed')), e.message);
|
|
1097
1313
|
}
|
|
1098
1314
|
continue;
|
|
1099
1315
|
}
|
|
1100
1316
|
if (lower === '/self-done') {
|
|
1101
1317
|
if (!reviewMode) {
|
|
1102
|
-
console.log(theme.system('
|
|
1318
|
+
console.log(theme.system(t('self.not_in_review')));
|
|
1103
1319
|
continue;
|
|
1104
1320
|
}
|
|
1105
1321
|
const back = reviewMode.originalWorkdir;
|
|
1106
1322
|
reviewMode = null;
|
|
1107
1323
|
currentWorkdir = back;
|
|
1108
|
-
//
|
|
1324
|
+
// Since the chat is busy with the review context, we'll create a new one for regular work
|
|
1109
1325
|
freshChatNext = true;
|
|
1110
1326
|
sendSystemPromptNext = true;
|
|
1111
|
-
console.log(theme.system(
|
|
1327
|
+
console.log(theme.system(t('self.done_hint', { v: back })));
|
|
1112
1328
|
continue;
|
|
1113
1329
|
}
|
|
1114
1330
|
if (lower === '/self-list') {
|
|
@@ -1123,7 +1339,7 @@ async function main() {
|
|
|
1123
1339
|
if (lower === '/self-diff' || lower.startsWith('/self-diff ')) {
|
|
1124
1340
|
const name = trimmed.slice('/self-diff'.length).trim();
|
|
1125
1341
|
if (!name) {
|
|
1126
|
-
console.error(theme.error('
|
|
1342
|
+
console.error(theme.error(t('self.diff_usage')));
|
|
1127
1343
|
continue;
|
|
1128
1344
|
}
|
|
1129
1345
|
try {
|
|
@@ -1137,7 +1353,7 @@ async function main() {
|
|
|
1137
1353
|
if (lower === '/self-apply' || lower.startsWith('/self-apply ')) {
|
|
1138
1354
|
const name = trimmed.slice('/self-apply'.length).trim();
|
|
1139
1355
|
if (!name) {
|
|
1140
|
-
console.error(theme.error('
|
|
1356
|
+
console.error(theme.error(t('self.apply_usage')));
|
|
1141
1357
|
continue;
|
|
1142
1358
|
}
|
|
1143
1359
|
try {
|
|
@@ -1148,9 +1364,9 @@ async function main() {
|
|
|
1148
1364
|
}
|
|
1149
1365
|
continue;
|
|
1150
1366
|
}
|
|
1151
|
-
// ----------
|
|
1367
|
+
// ---------- Regular commands ----------
|
|
1152
1368
|
if (lower === '/chats') {
|
|
1153
|
-
const spin = editor || mod.createSpinner();
|
|
1369
|
+
const spin = editor || mod.createSpinner(currentLocale);
|
|
1154
1370
|
spin.thinking();
|
|
1155
1371
|
try {
|
|
1156
1372
|
lastChats = await browser.listChats(30);
|
|
@@ -1159,41 +1375,41 @@ async function main() {
|
|
|
1159
1375
|
console.log(theme.system('Чатов не найдено. Возможно, сайдбар свёрнут или селекторы устарели.'));
|
|
1160
1376
|
}
|
|
1161
1377
|
else {
|
|
1162
|
-
console.log(theme.system('
|
|
1378
|
+
console.log(theme.system(t('chats.recent')));
|
|
1163
1379
|
lastChats.forEach((c, i) => {
|
|
1164
1380
|
const n = String(i + 1).padStart(2, ' ');
|
|
1165
1381
|
console.log(` ${theme.user(n)}. ${c.title} ${theme.system('(' + c.id.slice(0, 8) + '…)')}`);
|
|
1166
1382
|
});
|
|
1167
|
-
console.log(theme.system('
|
|
1383
|
+
console.log(theme.system(t('chats.use_resume')));
|
|
1168
1384
|
}
|
|
1169
1385
|
}
|
|
1170
1386
|
catch (e) {
|
|
1171
1387
|
spin.stop();
|
|
1172
|
-
console.error(theme.error('
|
|
1388
|
+
console.error(theme.error(t('chats.fetch_error')), e.message);
|
|
1173
1389
|
}
|
|
1174
1390
|
continue;
|
|
1175
1391
|
}
|
|
1176
1392
|
if (lower === '/resume' || lower.startsWith('/resume ')) {
|
|
1177
1393
|
const arg = trimmed.slice(7).trim();
|
|
1178
1394
|
if (!arg) {
|
|
1179
|
-
console.error(theme.error('
|
|
1395
|
+
console.error(theme.error(t('chats.resume_usage')));
|
|
1180
1396
|
continue;
|
|
1181
1397
|
}
|
|
1182
1398
|
const n = Number(arg);
|
|
1183
1399
|
if (!Number.isFinite(n) || n < 1) {
|
|
1184
|
-
console.error(theme.error('
|
|
1400
|
+
console.error(theme.error(t('chats.need_number')));
|
|
1185
1401
|
continue;
|
|
1186
1402
|
}
|
|
1187
1403
|
if (!lastChats.length) {
|
|
1188
|
-
console.error(theme.error('
|
|
1404
|
+
console.error(theme.error(t('chats.run_chats_first')));
|
|
1189
1405
|
continue;
|
|
1190
1406
|
}
|
|
1191
1407
|
const pick = lastChats[n - 1];
|
|
1192
1408
|
if (!pick) {
|
|
1193
|
-
console.error(theme.error(
|
|
1409
|
+
console.error(theme.error(t('chats.no_n', { n, total: lastChats.length })));
|
|
1194
1410
|
continue;
|
|
1195
1411
|
}
|
|
1196
|
-
console.log(theme.system(
|
|
1412
|
+
console.log(theme.system(t('chats.opening', { v: pick.title })));
|
|
1197
1413
|
try {
|
|
1198
1414
|
await browser.openChat(pick.id);
|
|
1199
1415
|
currentChatId = pick.id;
|
|
@@ -1201,32 +1417,32 @@ async function main() {
|
|
|
1201
1417
|
sendSystemPromptNext = resendPrompt;
|
|
1202
1418
|
saveLastChat(pick.id, currentWorkdir, pick.title);
|
|
1203
1419
|
transcript.log('resume_chat', { id: pick.id, title: pick.title });
|
|
1204
|
-
console.log(theme.assistant(
|
|
1420
|
+
console.log(theme.assistant(t('msg.chat_opened')) +
|
|
1205
1421
|
theme.system(resendPrompt
|
|
1206
1422
|
? ' Системный промпт будет переслан на следующей задаче.\n'
|
|
1207
1423
|
: ' Контекст чата сохранён. Системный промпт не пересылается (--resend-prompt чтобы дослать).\n'));
|
|
1208
1424
|
}
|
|
1209
1425
|
catch (e) {
|
|
1210
|
-
console.error(theme.error('
|
|
1426
|
+
console.error(theme.error(t('msg.open_chat_error', { v: '' })), e.message);
|
|
1211
1427
|
}
|
|
1212
1428
|
continue;
|
|
1213
1429
|
}
|
|
1214
1430
|
if (lower === '/chat') {
|
|
1215
1431
|
if (currentChatId) {
|
|
1216
|
-
console.log(theme.system(
|
|
1432
|
+
console.log(theme.system(t('chats.current_id', { v: currentChatId })));
|
|
1217
1433
|
console.log(theme.system(`URL: https://chat.deepseek.com/a/chat/s/${currentChatId}`));
|
|
1218
1434
|
}
|
|
1219
1435
|
else {
|
|
1220
1436
|
const id = await browser.getCurrentChatId();
|
|
1221
|
-
console.log(theme.system(id ?
|
|
1437
|
+
console.log(theme.system(id ? t('chats.current_id', { v: id }) : t('chats.not_created')));
|
|
1222
1438
|
}
|
|
1223
1439
|
continue;
|
|
1224
1440
|
}
|
|
1225
1441
|
if (lower === '/sessions' || lower === '/session') {
|
|
1226
1442
|
const all = listSessions();
|
|
1227
|
-
console.log(theme.system(
|
|
1443
|
+
console.log(theme.system(t('sessions.dir', { v: sessionsDir() })));
|
|
1228
1444
|
if (!all.length) {
|
|
1229
|
-
console.log(theme.system('
|
|
1445
|
+
console.log(theme.system(t('sessions.none')));
|
|
1230
1446
|
}
|
|
1231
1447
|
else {
|
|
1232
1448
|
all.forEach((s, i) => {
|
|
@@ -1236,18 +1452,18 @@ async function main() {
|
|
|
1236
1452
|
const wd = s.workdir ? theme.dim(` [${dirLabel(s.workdir)}]`) : '';
|
|
1237
1453
|
console.log(` ${theme.user(n)}. ${s.id.slice(0, 8)}…${title}${wd}${mark}`);
|
|
1238
1454
|
});
|
|
1239
|
-
console.log(theme.system('
|
|
1455
|
+
console.log(theme.system(t('sessions.restore_hint')));
|
|
1240
1456
|
}
|
|
1241
1457
|
continue;
|
|
1242
1458
|
}
|
|
1243
1459
|
if (lower.startsWith('/resume-id ')) {
|
|
1244
1460
|
const id = trimmed.slice('/resume-id'.length).trim();
|
|
1245
1461
|
if (!id) {
|
|
1246
|
-
console.error(theme.error('
|
|
1462
|
+
console.error(theme.error(t('sessions.resume_id_usage')));
|
|
1247
1463
|
continue;
|
|
1248
1464
|
}
|
|
1249
1465
|
try {
|
|
1250
|
-
console.log(theme.system(
|
|
1466
|
+
console.log(theme.system(t('msg.opening_chat', { id })));
|
|
1251
1467
|
await browser.openChat(id);
|
|
1252
1468
|
currentChatId = id;
|
|
1253
1469
|
freshChatNext = false;
|
|
@@ -1258,7 +1474,7 @@ async function main() {
|
|
|
1258
1474
|
theme.system(String.fromCharCode(10)));
|
|
1259
1475
|
}
|
|
1260
1476
|
catch (e) {
|
|
1261
|
-
console.error(theme.error('
|
|
1477
|
+
console.error(theme.error(t('msg.open_chat_error', { v: '' })), e.message);
|
|
1262
1478
|
}
|
|
1263
1479
|
continue;
|
|
1264
1480
|
}
|
|
@@ -1267,11 +1483,11 @@ async function main() {
|
|
|
1267
1483
|
continue;
|
|
1268
1484
|
}
|
|
1269
1485
|
if (lower === '/reload') {
|
|
1270
|
-
console.log(theme.system('
|
|
1486
|
+
console.log(theme.system(t('msg.reload_start')));
|
|
1271
1487
|
try {
|
|
1272
1488
|
const { count, errors } = await reloadModules();
|
|
1273
1489
|
if (errors.length) {
|
|
1274
|
-
console.error(theme.error('
|
|
1490
|
+
console.error(theme.error(t('msg.reload_partial')));
|
|
1275
1491
|
for (const e of errors)
|
|
1276
1492
|
console.error(theme.error(' ' + e));
|
|
1277
1493
|
}
|
|
@@ -1280,59 +1496,72 @@ async function main() {
|
|
|
1280
1496
|
}
|
|
1281
1497
|
}
|
|
1282
1498
|
catch (e) {
|
|
1283
|
-
console.error(theme.error('
|
|
1499
|
+
console.error(theme.error(t('msg.reload_error')), e.message);
|
|
1284
1500
|
}
|
|
1285
1501
|
continue;
|
|
1286
1502
|
}
|
|
1287
1503
|
if (lower === '/status') {
|
|
1288
|
-
|
|
1289
|
-
|
|
1504
|
+
const yes = t('common.yes');
|
|
1505
|
+
const no = t('common.no');
|
|
1506
|
+
console.log(theme.system(t('status.workdir', { v: currentWorkdir })));
|
|
1507
|
+
console.log(theme.system(t('status.review_mode', {
|
|
1508
|
+
v: reviewMode ? reviewMode.snapName : t('status.review_none'),
|
|
1509
|
+
})));
|
|
1290
1510
|
if (reviewMode) {
|
|
1291
|
-
console.log(theme.system(
|
|
1292
|
-
}
|
|
1293
|
-
console.log(theme.system(
|
|
1294
|
-
console.log(theme.system(
|
|
1295
|
-
console.log(theme.system(
|
|
1296
|
-
console.log(theme.system(
|
|
1297
|
-
console.log(theme.system(
|
|
1298
|
-
|
|
1299
|
-
|
|
1300
|
-
console.log(theme.system(
|
|
1301
|
-
console.log(theme.system(
|
|
1302
|
-
|
|
1303
|
-
|
|
1304
|
-
console.log(theme.system(
|
|
1511
|
+
console.log(theme.system(t('status.orig_dir', { v: reviewMode.originalWorkdir })));
|
|
1512
|
+
}
|
|
1513
|
+
console.log(theme.system(t('status.chat', { v: currentChatId || t('common.none') })));
|
|
1514
|
+
console.log(theme.system(t('status.fresh_next', { v: freshChatNext ? yes : no })));
|
|
1515
|
+
console.log(theme.system(t('status.prompt_next', { v: sendSystemPromptNext ? yes : no })));
|
|
1516
|
+
console.log(theme.system(t('status.resend', { v: resendPrompt ? yes : no })));
|
|
1517
|
+
console.log(theme.system(t('status.last_chat', {
|
|
1518
|
+
v: loadLastChat(currentWorkdir) || t('common.none'),
|
|
1519
|
+
})));
|
|
1520
|
+
console.log(theme.system(t('status.sessions', { v: sessionsDir() })));
|
|
1521
|
+
console.log(theme.system(t('status.dev', {
|
|
1522
|
+
v: devMode ? t('common.on') : t('common.off'),
|
|
1523
|
+
})));
|
|
1524
|
+
console.log(theme.system(t('status.max_iter', { v: maxIter })));
|
|
1525
|
+
console.log(theme.system(t('status.headless', { v: headless ? yes : no })));
|
|
1526
|
+
console.log(theme.system(t('status.debug', { v: debug ? yes : no })));
|
|
1527
|
+
console.log(theme.system(t('status.undo', {
|
|
1528
|
+
v: config.undo.enabled ? t('common.on') : t('common.off'),
|
|
1529
|
+
})));
|
|
1530
|
+
console.log(theme.system(t('status.transcript', {
|
|
1531
|
+
v: transcript.file || t('common.off'),
|
|
1532
|
+
})));
|
|
1533
|
+
console.log(theme.system(t('status.locale', { v: localeDisplayName(currentLocale) })));
|
|
1305
1534
|
continue;
|
|
1306
1535
|
}
|
|
1307
|
-
if (lower === '/config') {
|
|
1308
|
-
|
|
1536
|
+
if (lower === '/config' || lower.startsWith('/config ')) {
|
|
1537
|
+
await handleConfigCommand(trimmed);
|
|
1309
1538
|
continue;
|
|
1310
1539
|
}
|
|
1311
1540
|
if (lower === '/transcript') {
|
|
1312
|
-
console.log(theme.system(transcript.file ||
|
|
1541
|
+
console.log(theme.system(transcript.file || t('common.off')));
|
|
1313
1542
|
continue;
|
|
1314
1543
|
}
|
|
1315
1544
|
if (lower === '/undo') {
|
|
1316
1545
|
const result = await undo.undoLast();
|
|
1317
1546
|
if (result.ok && result.record) {
|
|
1318
|
-
console.log(theme.assistant(
|
|
1319
|
-
theme.system(result.record.existed ? '
|
|
1547
|
+
console.log(theme.assistant(t('undo.reverted', { v: result.record.originalPath })) +
|
|
1548
|
+
theme.system(result.record.existed ? t('undo.restored') : t('undo.deleted')));
|
|
1320
1549
|
transcript.log('undo', { path: result.record.originalPath });
|
|
1321
1550
|
}
|
|
1322
1551
|
else {
|
|
1323
|
-
console.error(theme.error(
|
|
1552
|
+
console.error(theme.error(t('undo.failed', { v: String(result.reason ?? '') })));
|
|
1324
1553
|
}
|
|
1325
1554
|
continue;
|
|
1326
1555
|
}
|
|
1327
1556
|
if (lower === '/undo-list' || lower === '/history') {
|
|
1328
1557
|
const list = await undo.list(10);
|
|
1329
1558
|
if (!list.length) {
|
|
1330
|
-
console.log(theme.system('
|
|
1559
|
+
console.log(theme.system(t('undo.empty')));
|
|
1331
1560
|
}
|
|
1332
1561
|
else {
|
|
1333
1562
|
for (const r of list) {
|
|
1334
1563
|
const stamp = new Date(r.stamp).toLocaleString();
|
|
1335
|
-
const flag = r.existed ? '
|
|
1564
|
+
const flag = r.existed ? t('undo.changed') : t('undo.created');
|
|
1336
1565
|
console.log(theme.system(`${stamp} [${flag}] ${r.originalPath}`));
|
|
1337
1566
|
}
|
|
1338
1567
|
}
|
|
@@ -1343,12 +1572,12 @@ async function main() {
|
|
|
1343
1572
|
const file = path.join(config.transcript.dir, `dom-${stamp}.html`);
|
|
1344
1573
|
try {
|
|
1345
1574
|
const result = await browser.dumpDom(file);
|
|
1346
|
-
console.log(theme.assistant(
|
|
1347
|
-
console.log(theme.system('
|
|
1575
|
+
console.log(theme.assistant(t('dom.saved', { v: result.file })));
|
|
1576
|
+
console.log(theme.system(t('dom.selectors')));
|
|
1348
1577
|
console.log(JSON.stringify(result.selectors, null, 2));
|
|
1349
1578
|
}
|
|
1350
1579
|
catch (e) {
|
|
1351
|
-
console.error(theme.error('
|
|
1580
|
+
console.error(theme.error(t('dom.save_error')), e.message);
|
|
1352
1581
|
}
|
|
1353
1582
|
continue;
|
|
1354
1583
|
}
|
|
@@ -1360,38 +1589,38 @@ async function main() {
|
|
|
1360
1589
|
: sandboxRoot;
|
|
1361
1590
|
const rel = path.relative(sandboxRoot, newDir);
|
|
1362
1591
|
if (rel.startsWith('..') || path.isAbsolute(rel)) {
|
|
1363
|
-
console.error(theme.error('
|
|
1592
|
+
console.error(theme.error(t('cd.outside', { v: sandboxRoot })));
|
|
1364
1593
|
continue;
|
|
1365
1594
|
}
|
|
1366
1595
|
const stat = await fs.stat(newDir).catch(() => null);
|
|
1367
1596
|
if (!stat || !stat.isDirectory()) {
|
|
1368
|
-
console.error(theme.error('
|
|
1597
|
+
console.error(theme.error(t('cd.not_dir', { v: newDir })));
|
|
1369
1598
|
continue;
|
|
1370
1599
|
}
|
|
1371
1600
|
if (newDir === currentWorkdir) {
|
|
1372
|
-
console.log(theme.system('
|
|
1601
|
+
console.log(theme.system(t('cd.already')));
|
|
1373
1602
|
continue;
|
|
1374
1603
|
}
|
|
1375
1604
|
if (reviewMode) {
|
|
1376
|
-
console.log(theme.system('
|
|
1605
|
+
console.log(theme.system(t('cd.left_review')));
|
|
1377
1606
|
reviewMode = null;
|
|
1378
1607
|
}
|
|
1379
1608
|
currentWorkdir = newDir;
|
|
1380
1609
|
freshChatNext = true;
|
|
1381
1610
|
sendSystemPromptNext = true;
|
|
1382
|
-
console.log(theme.system('
|
|
1611
|
+
console.log(theme.system(t('cd.changed', { v: newDir })));
|
|
1383
1612
|
}
|
|
1384
1613
|
catch (e) {
|
|
1385
|
-
console.error(theme.error('
|
|
1614
|
+
console.error(theme.error(t('cd.failed', { v: e.message })));
|
|
1386
1615
|
}
|
|
1387
1616
|
continue;
|
|
1388
1617
|
}
|
|
1389
1618
|
if (lower.startsWith('/')) {
|
|
1390
|
-
console.error(theme.error(
|
|
1619
|
+
console.error(theme.error(t('msg.unknown_cmd', { v: trimmed })));
|
|
1391
1620
|
continue;
|
|
1392
1621
|
}
|
|
1393
|
-
// ----
|
|
1394
|
-
// Dev
|
|
1622
|
+
// ---- Regular task (including in review mode) ----
|
|
1623
|
+
// Dev mode: pick up fresh logic modules before the task.
|
|
1395
1624
|
await autoReload();
|
|
1396
1625
|
transcript.log('user_task', { task: trimmed, workdir: currentWorkdir });
|
|
1397
1626
|
const tools = mod.createTools(currentWorkdir, { undo });
|
|
@@ -1405,9 +1634,9 @@ async function main() {
|
|
|
1405
1634
|
queue: pendingQueue,
|
|
1406
1635
|
ui: editor || null,
|
|
1407
1636
|
onChatReady: (chatId) => {
|
|
1408
|
-
//
|
|
1409
|
-
//
|
|
1410
|
-
// ~/.zames/.sessions
|
|
1637
|
+
// Save the session right at the start of the dialog, without waiting
|
|
1638
|
+
// for the task to finish. Otherwise a long/aborted task would not
|
|
1639
|
+
// get the chat into ~/.zames/.sessions and it would be lost after a restart.
|
|
1411
1640
|
if (chatId) {
|
|
1412
1641
|
currentChatId = chatId;
|
|
1413
1642
|
saveLastChat(chatId, currentWorkdir);
|
|
@@ -1433,7 +1662,7 @@ async function main() {
|
|
|
1433
1662
|
transcript.close();
|
|
1434
1663
|
}
|
|
1435
1664
|
main().catch((e) => {
|
|
1436
|
-
console.error(theme.error('
|
|
1665
|
+
console.error(theme.error(t('msg.critical')), e.message);
|
|
1437
1666
|
if (debug)
|
|
1438
1667
|
console.error(e.stack);
|
|
1439
1668
|
process.exit(1);
|