zames_pro 2.15.5 → 2.17.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 +39 -1
- package/dist/agent-loop.js +5 -1
- package/dist/browser.js +600 -17
- package/dist/commands.js +1 -0
- package/dist/config-menu.js +4 -0
- package/dist/config.js +15 -1
- package/dist/i18n.js +38 -1
- package/dist/index.js +53 -4
- package/dist/input.js +51 -9
- package/dist/net-capture.js +32 -4
- package/dist/spinner.js +39 -6
- package/package.json +1 -1
package/dist/commands.js
CHANGED
|
@@ -153,6 +153,7 @@ export function renderDoctor(d) {
|
|
|
153
153
|
row(d.gitOk, 'git', d.gitOk ? 'repo (' + (d.gitBranch || 'detached') + ')' : 'not a repository');
|
|
154
154
|
row(d.configOk, 'config', d.configOk ? 'loaded' : 'error: ' + (d.configError || 'unknown'));
|
|
155
155
|
row(true, 'browser', d.browserChannel ? d.browserChannel : 'bundled chromium');
|
|
156
|
+
row(!!d.authSaved, 'auth', d.authSaved ? 'session saved (auto re-login ready)' : 'no saved session');
|
|
156
157
|
row(!!d.clipboardTool, 'clipboard', d.clipboardTool || 'no tool found (image paste disabled)');
|
|
157
158
|
row(true, 'mcp', d.mcpServers + ' server(s), ' + d.mcpTools + ' tool(s)');
|
|
158
159
|
row(d.transcriptOk, 'transcript', d.transcriptOk ? 'on' : 'off');
|
package/dist/config-menu.js
CHANGED
|
@@ -19,6 +19,10 @@ function displayValue(field, value, t) {
|
|
|
19
19
|
return t('cfg.menu.default');
|
|
20
20
|
if (typeof value === 'boolean')
|
|
21
21
|
return value ? t('common.on') : t('common.off');
|
|
22
|
+
// Never print a password in clear text: show a fixed mask when set.
|
|
23
|
+
if (/password/i.test(field.path) && String(value).length > 0) {
|
|
24
|
+
return '********';
|
|
25
|
+
}
|
|
22
26
|
return String(value);
|
|
23
27
|
}
|
|
24
28
|
function groupLabel(field, t) {
|
package/dist/config.js
CHANGED
|
@@ -7,7 +7,9 @@ const HOME_CONFIG = path.join(ZAMES_HOME, 'config.json');
|
|
|
7
7
|
const PROJECT_CONFIG = path.join(process.cwd(), '.zamesrc.json');
|
|
8
8
|
export const DEFAULTS = {
|
|
9
9
|
maxIterations: 0,
|
|
10
|
-
|
|
10
|
+
// Headless by default: the browser runs without a visible window. Set
|
|
11
|
+
// headless: false (or pass --headed) to watch/debug the DeepSeek page.
|
|
12
|
+
headless: true,
|
|
11
13
|
debug: false,
|
|
12
14
|
browserChannel: null,
|
|
13
15
|
confirmation: {
|
|
@@ -41,6 +43,13 @@ export const DEFAULTS = {
|
|
|
41
43
|
minSendIntervalMs: 15000,
|
|
42
44
|
rateLimitWaitMs: 300000,
|
|
43
45
|
maxRateLimitRetries: 6,
|
|
46
|
+
deepThinking: false,
|
|
47
|
+
webSearch: true,
|
|
48
|
+
auth: {
|
|
49
|
+
username: '',
|
|
50
|
+
password: '',
|
|
51
|
+
saveSession: true,
|
|
52
|
+
},
|
|
44
53
|
},
|
|
45
54
|
ui: {
|
|
46
55
|
locale: DEFAULT_LOCALE,
|
|
@@ -79,6 +88,9 @@ export const CONFIG_SCHEMA = [
|
|
|
79
88
|
{ path: 'ui.locale', type: 'enum', values: ['ru', 'en'], labelKey: 'cfg.f.ui_locale', groupKey: 'cfg.group.ui' },
|
|
80
89
|
{ path: 'maxIterations', type: 'number', min: 0, max: 100000, labelKey: 'cfg.f.maxIterations', groupKey: 'cfg.group.agent' },
|
|
81
90
|
{ path: 'headless', type: 'boolean', labelKey: 'cfg.f.headless', groupKey: 'cfg.group.agent' },
|
|
91
|
+
{ path: 'browser.auth.username', type: 'string', labelKey: 'cfg.f.browser_authUsername', groupKey: 'cfg.group.browser' },
|
|
92
|
+
{ path: 'browser.auth.password', type: 'string', labelKey: 'cfg.f.browser_authPassword', groupKey: 'cfg.group.browser' },
|
|
93
|
+
{ path: 'browser.auth.saveSession', type: 'boolean', labelKey: 'cfg.f.browser_authSaveSession', groupKey: 'cfg.group.browser' },
|
|
82
94
|
{ path: 'debug', type: 'boolean', labelKey: 'cfg.f.debug', groupKey: 'cfg.group.agent' },
|
|
83
95
|
{ path: 'hotReload', type: 'boolean', labelKey: 'cfg.f.hotReload', groupKey: 'cfg.group.agent' },
|
|
84
96
|
{ path: 'confirmation.write', type: 'boolean', labelKey: 'cfg.f.confirmation_write', groupKey: 'cfg.group.confirmation' },
|
|
@@ -94,6 +106,8 @@ export const CONFIG_SCHEMA = [
|
|
|
94
106
|
{ path: 'browser.minSendIntervalMs', type: 'number', min: 0, max: 600000, labelKey: 'cfg.f.browser_minSendIntervalMs', groupKey: 'cfg.group.browser' },
|
|
95
107
|
{ path: 'browser.rateLimitWaitMs', type: 'number', min: 0, max: 3600000, labelKey: 'cfg.f.browser_rateLimitWaitMs', groupKey: 'cfg.group.browser' },
|
|
96
108
|
{ path: 'browser.maxRateLimitRetries', type: 'number', min: 0, max: 100, labelKey: 'cfg.f.browser_maxRateLimitRetries', groupKey: 'cfg.group.browser' },
|
|
109
|
+
{ path: 'browser.deepThinking', type: 'boolean', labelKey: 'cfg.f.browser_deepThinking', groupKey: 'cfg.group.browser' },
|
|
110
|
+
{ path: 'browser.webSearch', type: 'boolean', labelKey: 'cfg.f.browser_webSearch', groupKey: 'cfg.group.browser' },
|
|
97
111
|
];
|
|
98
112
|
export function getConfigField(path) {
|
|
99
113
|
return CONFIG_SCHEMA.find((f) => f.path === path);
|
package/dist/i18n.js
CHANGED
|
@@ -46,7 +46,8 @@ const CATALOG = {
|
|
|
46
46
|
'help.opt.new_chat': { ru: 'начать новый чат (по умолчанию)', en: 'start a new chat (default)' },
|
|
47
47
|
'help.opt.resend_prompt': { ru: 'дослать system-prompt в существующий чат', en: 'resend system-prompt into an existing chat' },
|
|
48
48
|
'help.opt.max_iter': { ru: 'лимит итераций, 0 = без лимита ({n})', en: 'iteration limit, 0 = unlimited ({n})' },
|
|
49
|
-
'help.opt.headless': { ru: 'браузер без UI', en: 'headless browser' },
|
|
49
|
+
'help.opt.headless': { ru: 'браузер без UI (по умолчанию)', en: 'headless browser (default)' },
|
|
50
|
+
'help.opt.headed': { ru: 'показать окно браузера (отладка/вход)', en: 'show the browser window (debug/sign-in)' },
|
|
50
51
|
'help.opt.debug': { ru: 'подробный лог', en: 'verbose log' },
|
|
51
52
|
'help.opt.calibrate': { ru: 'режим калибровки селекторов', en: 'selector calibration mode' },
|
|
52
53
|
'help.opt.dev': { ru: 'режим разработки: авто-перечитывание модулей', en: 'dev mode: auto-reload modules' },
|
|
@@ -132,6 +133,9 @@ const CATALOG = {
|
|
|
132
133
|
en: 'Mixing the slurry…|Whipping it to a smooth paste…|Stirring logic into it…|Shaking it into a cocktail…|Whisking it around…|Blending it with caffeine…|Kneading dough out of it…|Whipping it into foam…|Tossing it with a spatula…|Kneading clay out of it…|Homogenizing it into concrete…|Shaking it until clear…|Blending it into a smoothie…|Mixing cement out of it…|Whipping porridge out of it…|Stirring it with hope…|Swirling the sediment…|Running the blender over it…|Mixing it beyond recognition…|Mixing mortar out of it…|Whipping a cocktail of it…|Stirring it with the deadline spatula…|Whipping foam out of it…|Homogenizing it evenly…|Tumbling it in a cement mixer…|Beating it into a uniform mass…|Stirring it until clear…|Mixing it from whatever is around…',
|
|
133
134
|
},
|
|
134
135
|
'spinner.hint': { ru: 'Esc — стоп', en: 'Esc — stop' },
|
|
136
|
+
'spinner.pause': { ru: '⏳ пауза {n}с перед отправкой', en: '⏳ pause {n}s before send' },
|
|
137
|
+
'editor.more': { ru: '…ещё {n}', en: '…{n} more' },
|
|
138
|
+
'editor.answer': { ru: '● Ответ', en: '● Answer' },
|
|
135
139
|
// ---------- status ----------
|
|
136
140
|
'status.workdir': { ru: 'Рабочая директория: {v}', en: 'Working directory: {v}' },
|
|
137
141
|
'status.review_mode': { ru: 'Режим ревью: {v}', en: 'Review mode: {v}' },
|
|
@@ -293,6 +297,39 @@ const CATALOG = {
|
|
|
293
297
|
'cfg.f.browser_minSendIntervalMs': { ru: 'Мин. пауза между отправками, мс', en: 'Min pause between sends, ms' },
|
|
294
298
|
'cfg.f.browser_rateLimitWaitMs': { ru: 'Пауза при лимите частоты, мс', en: 'Wait on rate limit, ms' },
|
|
295
299
|
'cfg.f.browser_maxRateLimitRetries': { ru: 'Число повторов при лимите', en: 'Rate-limit retry count' },
|
|
300
|
+
'cfg.f.browser_deepThinking': { ru: 'Глубокое мышление (долго; размышления не выводятся)', en: 'Deep thinking (slow; reasoning is hidden)' },
|
|
301
|
+
'cfg.f.browser_webSearch': { ru: 'Умный поиск в интернете', en: 'Smart web search' },
|
|
302
|
+
'cfg.f.browser_authUsername': { ru: 'Логин DeepSeek для авто-входа', en: 'DeepSeek login for auto sign-in' },
|
|
303
|
+
'cfg.f.browser_authPassword': { ru: 'Пароль DeepSeek для авто-входа', en: 'DeepSeek password for auto sign-in' },
|
|
304
|
+
'cfg.f.browser_authSaveSession': { ru: 'Сохранять сессию входа в профиле', en: 'Persist the login session in the profile' },
|
|
305
|
+
// ---------- login / auth ----------
|
|
306
|
+
'auth.need_login': { ru: '🔐 Требуется вход в DeepSeek.', en: '🔐 DeepSeek sign-in required.' },
|
|
307
|
+
'auth.prompt_login': { ru: 'Логин DeepSeek (телефон/email): ', en: 'DeepSeek login (phone/email): ' },
|
|
308
|
+
'auth.prompt_password': { ru: 'Пароль DeepSeek: ', en: 'DeepSeek password: ' },
|
|
309
|
+
'auth.prompt_password_saved': {
|
|
310
|
+
ru: 'Пароль DeepSeek (Enter — использовать сохранённый): ',
|
|
311
|
+
en: 'DeepSeek password (Enter — use the saved one): ',
|
|
312
|
+
},
|
|
313
|
+
'auth.auto_login': { ru: 'Авто-вход в DeepSeek...', en: 'Signing in to DeepSeek...' },
|
|
314
|
+
'auth.auto_login_ok': { ru: 'Вход выполнен.', en: 'Signed in.' },
|
|
315
|
+
'auth.auto_login_failed': { ru: 'Авто-вход не удался: {v}', en: 'Auto sign-in failed: {v}' },
|
|
316
|
+
'auth.login_rejected': { ru: 'DeepSeek отклонил вход: {v}', en: 'DeepSeek rejected the sign-in: {v}' },
|
|
317
|
+
'auth.form_not_found': {
|
|
318
|
+
ru: 'Форма входа не распознана. Запустите с --headed и войдите вручную.',
|
|
319
|
+
en: 'Login form not recognized. Run with --headed and sign in manually.',
|
|
320
|
+
},
|
|
321
|
+
'auth.no_reason': {
|
|
322
|
+
ru: 'причина не определена (возможно, неверный логин/пароль или капча)',
|
|
323
|
+
en: 'no reason detected (possibly wrong login/password or a captcha)',
|
|
324
|
+
},
|
|
325
|
+
'auth.manual_hint': { ru: 'Войдите в DeepSeek в открытом браузере, затем нажмите Enter.', en: 'Sign in to DeepSeek in the open browser, then press Enter.' },
|
|
326
|
+
'auth.manual_hint_headless': {
|
|
327
|
+
ru: 'Войдите в DeepSeek: откройте https://chat.deepseek.com/ в своём браузере. Если включён headless — задайте логин/пароль через /config или запустите с --headed.',
|
|
328
|
+
en: 'Sign in to DeepSeek: open https://chat.deepseek.com/ in your browser. If headless is on — set the login/password via /config or run with --headed.',
|
|
329
|
+
},
|
|
330
|
+
'auth.session_saved': { ru: 'Сессия DeepSeek сохранена.', en: 'DeepSeek session saved.' },
|
|
331
|
+
'auth.session_restored': { ru: 'Сессия DeepSeek восстановлена.', en: 'DeepSeek session restored.' },
|
|
332
|
+
'auth.enter_to_continue': { ru: 'Нажмите Enter, чтобы продолжить...', en: 'Press Enter to continue...' },
|
|
296
333
|
'cfg.menu.title': { ru: '⚙ Настройки — выбери параметр', en: '⚙ Settings — choose a parameter' },
|
|
297
334
|
'cfg.menu.hint': { ru: '↑/↓ — выбор, Enter — изменить, q/Esc — выйти', en: '↑/↓ select, Enter edit, q/Esc quit' },
|
|
298
335
|
'cfg.menu.edit_hint': { ru: 'Введи значение и Enter (пусто — отмена)', en: 'Enter a value and press Enter (empty — cancel)' },
|
package/dist/index.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import path from 'path';
|
|
3
3
|
import fs from 'fs/promises';
|
|
4
|
+
import { existsSync } from 'fs';
|
|
4
5
|
import { fileURLToPath } from 'url';
|
|
5
6
|
import { theme } from './theme.js';
|
|
6
7
|
import { DeepSeekBrowser } from './browser.js';
|
|
@@ -19,6 +20,16 @@ import { formatDiff, diffGitArgs, parseTranscript, summarizeTranscript, renderCo
|
|
|
19
20
|
import { closeWeb } from './web.js';
|
|
20
21
|
import { saveSession, loadLastSession, listSessions, sessionsDir, } from './sessions.js';
|
|
21
22
|
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
23
|
+
// True when a DeepSeek session/credentials were stored earlier (auth.json).
|
|
24
|
+
// Used by /doctor to report that auto re-login is ready.
|
|
25
|
+
function authMarkerExists() {
|
|
26
|
+
try {
|
|
27
|
+
return existsSync(path.join(ZAMES_HOME, 'auth.json'));
|
|
28
|
+
}
|
|
29
|
+
catch {
|
|
30
|
+
return false;
|
|
31
|
+
}
|
|
32
|
+
}
|
|
22
33
|
// ---------- CLI parsing ----------
|
|
23
34
|
const args = process.argv.slice(2);
|
|
24
35
|
function getArg(flag, fallback = null) {
|
|
@@ -50,7 +61,11 @@ let currentLocale = isLocale(config.ui?.locale)
|
|
|
50
61
|
// Translation: reads currentLocale at call time, so a language change takes
|
|
51
62
|
// effect immediately, without a restart (for lines printed afterwards).
|
|
52
63
|
const t = (key, params) => translate(currentLocale)(key, params);
|
|
53
|
-
|
|
64
|
+
// Headless is the default. --headed forces a visible window (useful for the
|
|
65
|
+
// first sign-in or debugging the DOM). --headless keeps it explicit.
|
|
66
|
+
const headless = hasFlag('--headed')
|
|
67
|
+
? false
|
|
68
|
+
: hasFlag('--headless') || config.headless;
|
|
54
69
|
const debug = hasFlag('--debug') || config.debug;
|
|
55
70
|
const calibrate = hasFlag('--calibrate');
|
|
56
71
|
const maxIterArg = getArg('--max-iter', null);
|
|
@@ -189,6 +204,7 @@ ${theme.bold(t('help.options'))}
|
|
|
189
204
|
--resend-prompt ${t('help.opt.resend_prompt')}
|
|
190
205
|
--max-iter <n> ${t('help.opt.max_iter', { n: config.maxIterations })}
|
|
191
206
|
--headless ${t('help.opt.headless')}
|
|
207
|
+
--headed ${t('help.opt.headed')}
|
|
192
208
|
--debug ${t('help.opt.debug')}
|
|
193
209
|
--calibrate ${t('help.opt.calibrate')}
|
|
194
210
|
--dev ${t('help.opt.dev')}
|
|
@@ -828,6 +844,7 @@ async function runTask(browser, tools, taskText, workdir, opts, attachments = []
|
|
|
828
844
|
attachments: next.attachments || [],
|
|
829
845
|
transcript,
|
|
830
846
|
onThinking: () => ui.thinking(),
|
|
847
|
+
onSendPause: (seconds) => ui.sendPause(seconds),
|
|
831
848
|
onToolCall: (name, toolArgs) => ui.toolCall(name, toolArgs),
|
|
832
849
|
onToolResult: (result) => ui.toolResult(result),
|
|
833
850
|
onAssistantMessage: (msg) => ui.assistant(msg),
|
|
@@ -881,9 +898,10 @@ async function runTask(browser, tools, taskText, workdir, opts, attachments = []
|
|
|
881
898
|
finally {
|
|
882
899
|
stopWatching();
|
|
883
900
|
ui.stop();
|
|
884
|
-
// Detach the send
|
|
901
|
+
// Detach the send hooks so a later browser.ask() outside this task cannot
|
|
885
902
|
// start a stale spinner.
|
|
886
903
|
browser.onSendStart = null;
|
|
904
|
+
browser.onSendPause = null;
|
|
887
905
|
}
|
|
888
906
|
}
|
|
889
907
|
// ---------- main ----------
|
|
@@ -944,6 +962,18 @@ async function main() {
|
|
|
944
962
|
debug,
|
|
945
963
|
channel: config.browserChannel,
|
|
946
964
|
...config.browser,
|
|
965
|
+
locale: currentLocale,
|
|
966
|
+
// Persist credentials the operator typed in the terminal so the next
|
|
967
|
+
// launch can re-login silently after a logout.
|
|
968
|
+
onAuthSave: (username, password) => {
|
|
969
|
+
config.browser.auth.username = username;
|
|
970
|
+
config.browser.auth.password = password;
|
|
971
|
+
try {
|
|
972
|
+
writeConfigValue('home', 'browser.auth.username', username);
|
|
973
|
+
writeConfigValue('home', 'browser.auth.password', password);
|
|
974
|
+
}
|
|
975
|
+
catch { }
|
|
976
|
+
},
|
|
947
977
|
});
|
|
948
978
|
const bootSpinner = mod.createSpinner(currentLocale);
|
|
949
979
|
bootSpinner.thinking();
|
|
@@ -1096,6 +1126,7 @@ async function main() {
|
|
|
1096
1126
|
const ed = new LineEditor({
|
|
1097
1127
|
prompt: buildPrompt(),
|
|
1098
1128
|
commands: buildSlashCommands(),
|
|
1129
|
+
locale: currentLocale,
|
|
1099
1130
|
});
|
|
1100
1131
|
editor = ed;
|
|
1101
1132
|
ed.setTmpDir(TMP_DIR);
|
|
@@ -1259,10 +1290,21 @@ async function main() {
|
|
|
1259
1290
|
currentLocale = value;
|
|
1260
1291
|
config.ui.locale = value;
|
|
1261
1292
|
if (editor) {
|
|
1293
|
+
editor.setLocale(value);
|
|
1262
1294
|
editor.setCommands(buildSlashCommands());
|
|
1263
1295
|
editor.setPrompt(buildPrompt());
|
|
1264
1296
|
}
|
|
1265
1297
|
}
|
|
1298
|
+
// Reflect browser toggles on the LIVE browser object. The browser is
|
|
1299
|
+
// created once at startup, so without this a /config set browser.*
|
|
1300
|
+
// change would only apply after a restart.
|
|
1301
|
+
if (path === 'browser.deepThinking' || path === 'browser.webSearch') {
|
|
1302
|
+
const b = browser;
|
|
1303
|
+
if (path === 'browser.deepThinking')
|
|
1304
|
+
b.deepThinking = !!value;
|
|
1305
|
+
else
|
|
1306
|
+
b.webSearch = !!value;
|
|
1307
|
+
}
|
|
1266
1308
|
}
|
|
1267
1309
|
function configSetRaw(field, raw) {
|
|
1268
1310
|
const value = validateConfigValue(field, raw);
|
|
@@ -1294,7 +1336,9 @@ async function main() {
|
|
|
1294
1336
|
? cur
|
|
1295
1337
|
? t('common.on')
|
|
1296
1338
|
: t('common.off')
|
|
1297
|
-
:
|
|
1339
|
+
: /password/i.test(f.path) && String(cur).length > 0
|
|
1340
|
+
? '********'
|
|
1341
|
+
: JSON.stringify(cur);
|
|
1298
1342
|
const extra = f.values ? ' [' + f.values.join('|') + ']' : '';
|
|
1299
1343
|
console.log(' ' +
|
|
1300
1344
|
theme.user(f.path) +
|
|
@@ -1385,7 +1429,11 @@ async function main() {
|
|
|
1385
1429
|
const cur = getByPath(config, key);
|
|
1386
1430
|
console.log(theme.system(t('cfg.value', {
|
|
1387
1431
|
v: key,
|
|
1388
|
-
value: cur === undefined
|
|
1432
|
+
value: cur === undefined
|
|
1433
|
+
? t('cfg.menu.default')
|
|
1434
|
+
: /password/i.test(key) && String(cur).length > 0
|
|
1435
|
+
? '********'
|
|
1436
|
+
: JSON.stringify(cur),
|
|
1389
1437
|
})));
|
|
1390
1438
|
return;
|
|
1391
1439
|
}
|
|
@@ -2091,6 +2139,7 @@ async function main() {
|
|
|
2091
2139
|
mcpServers: mcpStatus.servers.length,
|
|
2092
2140
|
mcpTools: mcpStatus.toolCount,
|
|
2093
2141
|
transcriptOk: !!transcript.file,
|
|
2142
|
+
authSaved: authMarkerExists(),
|
|
2094
2143
|
})));
|
|
2095
2144
|
continue;
|
|
2096
2145
|
}
|
package/dist/input.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { theme } from './theme.js';
|
|
2
2
|
import { renderMarkdown } from './markdown.js';
|
|
3
3
|
import { randomThinkingPhrase, stripEllipsis } from './spinner.js';
|
|
4
|
+
import { translate } from './i18n.js';
|
|
4
5
|
import { AttachmentStore, parseImagePaste, looksLikeFilePath, } from './attachments.js';
|
|
5
6
|
// A permanent input line at the bottom of the terminal + a status/output area above it.
|
|
6
7
|
//
|
|
@@ -70,7 +71,6 @@ export function expandPastes(pastes, text) {
|
|
|
70
71
|
// We align the width to the maximum (3) so the hint doesn't shift.
|
|
71
72
|
const DOTS = ['', '.', '..', '...'];
|
|
72
73
|
const DOTS_PAD = ' ';
|
|
73
|
-
const HINT = theme.dim(' · Esc — стоп');
|
|
74
74
|
function safeJson(v) {
|
|
75
75
|
try {
|
|
76
76
|
return JSON.stringify(v);
|
|
@@ -184,6 +184,9 @@ export class LineEditor {
|
|
|
184
184
|
onCtrlC;
|
|
185
185
|
_inPaste;
|
|
186
186
|
_dotTimer;
|
|
187
|
+
// True while the dot-animation timer is running. Lets sendPause() update
|
|
188
|
+
// only the base text (remaining seconds) without restarting the timer.
|
|
189
|
+
_animating;
|
|
187
190
|
_dotPhase;
|
|
188
191
|
_thinkBase;
|
|
189
192
|
_wasRaw;
|
|
@@ -212,7 +215,11 @@ export class LineEditor {
|
|
|
212
215
|
// user could type and send a message mid-operation, and it would be queued
|
|
213
216
|
// and sent right after the operation, breaking the restored session.
|
|
214
217
|
locked;
|
|
215
|
-
|
|
218
|
+
// Interface language for the editor's own labels (hint, answer marker,
|
|
219
|
+
// pause status). Everything the OPERATOR sees must be localized.
|
|
220
|
+
locale;
|
|
221
|
+
constructor({ prompt = '> ', commands = [], locale = 'ru' } = {}) {
|
|
222
|
+
this.locale = locale;
|
|
216
223
|
this.promptStr = prompt;
|
|
217
224
|
this.buf = '';
|
|
218
225
|
this.cursor = 0;
|
|
@@ -226,6 +233,7 @@ export class LineEditor {
|
|
|
226
233
|
this.onCtrlC = null;
|
|
227
234
|
this._inPaste = false;
|
|
228
235
|
this._dotTimer = null;
|
|
236
|
+
this._animating = false;
|
|
229
237
|
this._dotPhase = 0;
|
|
230
238
|
this._thinkBase = '';
|
|
231
239
|
this._wasRaw = false;
|
|
@@ -362,6 +370,11 @@ export class LineEditor {
|
|
|
362
370
|
this.promptStr = str;
|
|
363
371
|
this._render();
|
|
364
372
|
}
|
|
373
|
+
// Update the interface language (labels: hint, answer marker, pause).
|
|
374
|
+
setLocale(locale) {
|
|
375
|
+
this.locale = locale;
|
|
376
|
+
this._render();
|
|
377
|
+
}
|
|
365
378
|
// Update slash-command descriptions (interface language change).
|
|
366
379
|
setCommands(commands) {
|
|
367
380
|
this.slashCommands = commands;
|
|
@@ -412,7 +425,7 @@ export class LineEditor {
|
|
|
412
425
|
out += NL + lines.join(NL);
|
|
413
426
|
const hidden = sugg.length - shown.length;
|
|
414
427
|
if (hidden > 0)
|
|
415
|
-
out += NL + theme.dim('
|
|
428
|
+
out += NL + theme.dim(' ' + translate(this.locale)('editor.more', { n: hidden }));
|
|
416
429
|
}
|
|
417
430
|
process.stdout.write(out);
|
|
418
431
|
const lastRow = lay.rows.length - 1;
|
|
@@ -449,36 +462,65 @@ export class LineEditor {
|
|
|
449
462
|
clearInterval(this._dotTimer);
|
|
450
463
|
this._dotTimer = null;
|
|
451
464
|
}
|
|
465
|
+
this._animating = false;
|
|
452
466
|
}
|
|
453
467
|
_dots(n) {
|
|
454
468
|
// The dots are the same color as the base and of fixed width — otherwise
|
|
455
469
|
// the hint on the right "jumps" when the animation phase changes.
|
|
456
470
|
return theme.brown(DOTS[n] + DOTS_PAD.slice(DOTS[n].length));
|
|
457
471
|
}
|
|
472
|
+
// Status-line hint ("Esc — stop"), localized.
|
|
473
|
+
_hint() {
|
|
474
|
+
return theme.dim(' · ' + translate(this.locale)('spinner.hint'));
|
|
475
|
+
}
|
|
458
476
|
_startThinking() {
|
|
459
477
|
if (this.pendingText) {
|
|
460
|
-
this.setStatus(theme.prompt('✎ ') + this.pendingText +
|
|
478
|
+
this.setStatus(theme.prompt('✎ ') + this.pendingText + this._hint());
|
|
461
479
|
return;
|
|
462
480
|
}
|
|
463
|
-
this.
|
|
481
|
+
this._startAnimated(randomThinkingPhrase());
|
|
482
|
+
}
|
|
483
|
+
// Animated status: a brown base text plus a growing "running" dot sequence.
|
|
484
|
+
// Shared by the thinking spinner and the send-pause indicator, so the pause
|
|
485
|
+
// is animated too (previously the pause was a static console line and the
|
|
486
|
+
// dots stayed frozen).
|
|
487
|
+
_startAnimated(baseText) {
|
|
488
|
+
this._thinkBase = theme.brown(stripEllipsis(baseText));
|
|
464
489
|
this._dotPhase = 0;
|
|
465
|
-
this.setStatus(this._thinkBase + this._dots(0) +
|
|
490
|
+
this.setStatus(this._thinkBase + this._dots(0) + this._hint());
|
|
466
491
|
this._stopDots();
|
|
467
492
|
this._dotTimer = setInterval(() => {
|
|
468
493
|
this._dotPhase = (this._dotPhase + 1) % DOTS.length;
|
|
469
|
-
this.setStatus(this._thinkBase + this._dots(this._dotPhase) +
|
|
494
|
+
this.setStatus(this._thinkBase + this._dots(this._dotPhase) + this._hint());
|
|
470
495
|
}, 400);
|
|
496
|
+
this._animating = true;
|
|
471
497
|
if (this._dotTimer.unref)
|
|
472
498
|
this._dotTimer.unref();
|
|
473
499
|
}
|
|
474
500
|
thinking() {
|
|
475
501
|
this._startThinking();
|
|
476
502
|
}
|
|
503
|
+
// The agent is waiting out the send-interval pause before a real send.
|
|
504
|
+
// Show it as an ANIMATED status with the remaining seconds. The browser
|
|
505
|
+
// calls this repeatedly (once per second), so we update only the base text
|
|
506
|
+
// and keep the running dot timer — otherwise the dots would restart from
|
|
507
|
+
// zero on every update and look frozen on a single dot.
|
|
508
|
+
sendPause(seconds) {
|
|
509
|
+
if (this.pendingText)
|
|
510
|
+
return;
|
|
511
|
+
const label = translate(this.locale)('spinner.pause', { n: seconds });
|
|
512
|
+
if (this._animating && this._dotTimer) {
|
|
513
|
+
this._thinkBase = theme.brown(stripEllipsis(label));
|
|
514
|
+
this.setStatus(this._thinkBase + this._dots(this._dotPhase) + this._hint());
|
|
515
|
+
return;
|
|
516
|
+
}
|
|
517
|
+
this._startAnimated(label);
|
|
518
|
+
}
|
|
477
519
|
setPending(text) {
|
|
478
520
|
this.pendingText = text && String(text).length ? String(text) : null;
|
|
479
521
|
if (this.pendingText) {
|
|
480
522
|
this._stopDots();
|
|
481
|
-
this.setStatus(theme.prompt('✎ ') + this.pendingText +
|
|
523
|
+
this.setStatus(theme.prompt('✎ ') + this.pendingText + this._hint());
|
|
482
524
|
}
|
|
483
525
|
else {
|
|
484
526
|
this._startThinking();
|
|
@@ -500,7 +542,7 @@ export class LineEditor {
|
|
|
500
542
|
assistant(msg) {
|
|
501
543
|
this.stop();
|
|
502
544
|
const rendered = renderMarkdown(msg);
|
|
503
|
-
this.printAbove(NL + theme.assistant('
|
|
545
|
+
this.printAbove(NL + theme.assistant(translate(this.locale)('editor.answer')) + NL + rendered + NL + theme.dim('─'.repeat(60)));
|
|
504
546
|
}
|
|
505
547
|
// Warning to the operator (e.g. the agent stopped suspiciously).
|
|
506
548
|
// Printed above the input line without overwriting it.
|
package/dist/net-capture.js
CHANGED
|
@@ -43,6 +43,12 @@ function parseDataLines(body) {
|
|
|
43
43
|
// incremental APPEND chunks response/fragments/-1/content.
|
|
44
44
|
export function extractFromSse(body) {
|
|
45
45
|
let out = '';
|
|
46
|
+
// Type of the CURRENT last fragment. When deep thinking is on, the
|
|
47
|
+
// stream first fills a THINK fragment (the reasoning) and only then a
|
|
48
|
+
// RESPONSE fragment (the answer), and BOTH use the same APPEND path
|
|
49
|
+
// response/fragments/-1/content. Without tracking this, the reasoning was
|
|
50
|
+
// concatenated into the answer and leaked to the terminal.
|
|
51
|
+
let lastType = '';
|
|
46
52
|
for (const obj of parseDataLines(body)) {
|
|
47
53
|
if (obj == null || typeof obj !== 'object')
|
|
48
54
|
continue;
|
|
@@ -58,21 +64,43 @@ export function extractFromSse(body) {
|
|
|
58
64
|
? v.response
|
|
59
65
|
: undefined;
|
|
60
66
|
if (resp && Array.isArray(resp.fragments)) {
|
|
61
|
-
|
|
67
|
+
const frags = resp.fragments;
|
|
68
|
+
for (const fr of frags) {
|
|
62
69
|
if (fr && fr.type === 'RESPONSE' && typeof fr.content === 'string') {
|
|
63
70
|
out += fr.content;
|
|
64
71
|
}
|
|
65
72
|
}
|
|
73
|
+
const lastFrag = frags[frags.length - 1];
|
|
74
|
+
if (lastFrag && typeof lastFrag.type === 'string')
|
|
75
|
+
lastType = lastFrag.type;
|
|
76
|
+
continue;
|
|
77
|
+
}
|
|
78
|
+
// A NEW fragment is appended to response/fragments[]. The LAST fragment
|
|
79
|
+
// decides what the following content chunks belong to (THINK vs
|
|
80
|
+
// RESPONSE).
|
|
81
|
+
if (o.p === 'response/fragments' && Array.isArray(o.v)) {
|
|
82
|
+
const frags = o.v;
|
|
83
|
+
for (const fr of frags) {
|
|
84
|
+
if (fr && fr.type === 'RESPONSE' && typeof fr.content === 'string') {
|
|
85
|
+
out += fr.content;
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
const lastFrag = frags[frags.length - 1];
|
|
89
|
+
if (lastFrag && typeof lastFrag.type === 'string')
|
|
90
|
+
lastType = lastFrag.type;
|
|
66
91
|
continue;
|
|
67
92
|
}
|
|
68
93
|
// Incremental APPEND: p='response/fragments/-1/content' -> v=string;
|
|
69
|
-
// subsequent chunks come with a single v field.
|
|
94
|
+
// subsequent chunks come with a single v field. Append ONLY when the
|
|
95
|
+
// current fragment is RESPONSE; a THINK fragment is the reasoning.
|
|
70
96
|
if (o.p === 'response/fragments/-1/content' && typeof o.v === 'string') {
|
|
71
|
-
|
|
97
|
+
if (lastType === '' || lastType === 'RESPONSE')
|
|
98
|
+
out += o.v;
|
|
72
99
|
continue;
|
|
73
100
|
}
|
|
74
101
|
if (o.p === undefined && typeof o.v === 'string') {
|
|
75
|
-
|
|
102
|
+
if (lastType === '' || lastType === 'RESPONSE')
|
|
103
|
+
out += o.v;
|
|
76
104
|
}
|
|
77
105
|
}
|
|
78
106
|
return out;
|
package/dist/spinner.js
CHANGED
|
@@ -20,6 +20,12 @@ export function createSpinner(locale = 'ru') {
|
|
|
20
20
|
let dotTimer = null;
|
|
21
21
|
let dotPhase = 0;
|
|
22
22
|
let pending = null;
|
|
23
|
+
// Current animated base text and its dot renderer. Kept in variables (not
|
|
24
|
+
// captured in the interval closure) so sendPause() can update the text
|
|
25
|
+
// without restarting the dot animation — otherwise the dots would reset to
|
|
26
|
+
// zero every second and look frozen on a single dot.
|
|
27
|
+
let animBase = '';
|
|
28
|
+
let animating = false;
|
|
23
29
|
// Dot animation: start from an empty string (0 dots), then grow.
|
|
24
30
|
// We align the width to the maximum (3) so the hint doesn't shift.
|
|
25
31
|
const DOTS = ['', '.', '..', '...'];
|
|
@@ -35,19 +41,23 @@ export function createSpinner(locale = 'ru') {
|
|
|
35
41
|
clearInterval(dotTimer);
|
|
36
42
|
dotTimer = null;
|
|
37
43
|
}
|
|
44
|
+
animating = false;
|
|
38
45
|
if (spinner)
|
|
39
46
|
spinner.stop();
|
|
40
47
|
};
|
|
41
48
|
// Hint in the status line: while the agent works the terminal is live,
|
|
42
49
|
// so you can type the next message. Without it this is not obvious.
|
|
43
50
|
const HINT = theme.dim(' · ' + translate(locale)('spinner.hint'));
|
|
44
|
-
//
|
|
45
|
-
|
|
46
|
-
|
|
51
|
+
// Run the animated status line: a brown base text plus a growing/shrinking
|
|
52
|
+
// "running" dot sequence. Shared by the thinking spinner and the send-pause
|
|
53
|
+
// indicator, so the pause is animated too (before, it was a static line and
|
|
54
|
+
// the dots did not move — the operator saw a frozen spinner).
|
|
55
|
+
const startAnimated = (baseText) => {
|
|
56
|
+
animBase = theme.brown(stripEllipsis(baseText));
|
|
47
57
|
// The dots are the same color as the base and of fixed width — otherwise
|
|
48
58
|
// the hint on the right "jumps" when the animation phase changes.
|
|
49
59
|
const dots = (n) => theme.brown(DOTS[n] + DOTS_PAD.slice(DOTS[n].length));
|
|
50
|
-
start(
|
|
60
|
+
start(animBase + dots(0) + HINT);
|
|
51
61
|
dotPhase = 0;
|
|
52
62
|
if (dotTimer)
|
|
53
63
|
clearInterval(dotTimer);
|
|
@@ -55,11 +65,16 @@ export function createSpinner(locale = 'ru') {
|
|
|
55
65
|
if (!spinner)
|
|
56
66
|
return;
|
|
57
67
|
dotPhase = (dotPhase + 1) % DOTS.length;
|
|
58
|
-
spinner.text =
|
|
68
|
+
spinner.text = animBase + dots(dotPhase) + HINT;
|
|
59
69
|
}, 400);
|
|
70
|
+
animating = true;
|
|
60
71
|
if (dotTimer.unref)
|
|
61
72
|
dotTimer.unref();
|
|
62
73
|
};
|
|
74
|
+
// Start the animated status: brown text + "running" dots.
|
|
75
|
+
const startThinking = () => {
|
|
76
|
+
startAnimated(randomThinkingPhrase(locale));
|
|
77
|
+
};
|
|
63
78
|
// Show the typed but not yet sent text instead of the spinner.
|
|
64
79
|
// Lets you type a message right while the agent is working.
|
|
65
80
|
const showPending = () => {
|
|
@@ -67,6 +82,7 @@ export function createSpinner(locale = 'ru') {
|
|
|
67
82
|
clearInterval(dotTimer);
|
|
68
83
|
dotTimer = null;
|
|
69
84
|
}
|
|
85
|
+
animating = false;
|
|
70
86
|
if (!spinner)
|
|
71
87
|
spinner = ora('');
|
|
72
88
|
spinner.start();
|
|
@@ -80,6 +96,23 @@ export function createSpinner(locale = 'ru') {
|
|
|
80
96
|
}
|
|
81
97
|
startThinking();
|
|
82
98
|
},
|
|
99
|
+
// The agent is waiting out the send-interval pause before a real send.
|
|
100
|
+
// Show it as an ANIMATED status line with the remaining seconds, so the
|
|
101
|
+
// spinner keeps moving during the pause (previously a static console line
|
|
102
|
+
// was printed and the dots stayed frozen).
|
|
103
|
+
sendPause: (seconds) => {
|
|
104
|
+
if (pending)
|
|
105
|
+
return;
|
|
106
|
+
const label = translate(locale)('spinner.pause', { n: seconds });
|
|
107
|
+
// Update only the base text while the dot animation is already running,
|
|
108
|
+
// so the countdown refreshes without resetting the dots.
|
|
109
|
+
if (animating && dotTimer && spinner) {
|
|
110
|
+
animBase = theme.brown(stripEllipsis(label));
|
|
111
|
+
spinner.text = animBase + theme.brown(DOTS[dotPhase] + DOTS_PAD.slice(DOTS[dotPhase].length)) + HINT;
|
|
112
|
+
return;
|
|
113
|
+
}
|
|
114
|
+
startAnimated(label);
|
|
115
|
+
},
|
|
83
116
|
// The text the user types while the agent is working.
|
|
84
117
|
// Empty string / null — restore the regular spinner.
|
|
85
118
|
setPending: (text) => {
|
|
@@ -106,7 +139,7 @@ export function createSpinner(locale = 'ru') {
|
|
|
106
139
|
const rendered = renderMarkdown(msg);
|
|
107
140
|
// Model answer marker: helps visually separate it from the user's input
|
|
108
141
|
// (which is highlighted by the prompt with a golden arrow).
|
|
109
|
-
console.log(NL + theme.assistant('
|
|
142
|
+
console.log(NL + theme.assistant(translate(locale)('editor.answer')) + NL);
|
|
110
143
|
console.log(rendered);
|
|
111
144
|
console.log(theme.dim('─'.repeat(60)) + NL);
|
|
112
145
|
},
|