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/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');
@@ -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
- headless: false,
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
- const headless = hasFlag('--headless') || config.headless;
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 hook so a later browser.ask() outside this task cannot
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
- : JSON.stringify(cur);
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 ? t('cfg.menu.default') : JSON.stringify(cur),
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
- constructor({ prompt = '> ', commands = [] } = {}) {
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(' …ещё ' + hidden);
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 + HINT);
478
+ this.setStatus(theme.prompt('✎ ') + this.pendingText + this._hint());
461
479
  return;
462
480
  }
463
- this._thinkBase = theme.brown(stripEllipsis(randomThinkingPhrase()));
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) + HINT);
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) + HINT);
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 + HINT);
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('● Ответ') + NL + rendered + NL + theme.dim('─'.repeat(60)));
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.
@@ -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
- for (const fr of resp.fragments) {
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
- out += o.v;
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
- out += o.v;
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
- // Start the animated status: brown text + "running" dots.
45
- const startThinking = () => {
46
- const base = theme.brown(stripEllipsis(randomThinkingPhrase(locale)));
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(base + dots(0) + HINT);
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 = base + dots(dotPhase) + HINT;
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('● Ответ') + NL);
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
  },
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "zames_pro",
3
- "version": "2.15.5",
3
+ "version": "2.17.0",
4
4
  "description": "Terminal coding agent over chat.deepseek.com via Playwright",
5
5
  "type": "module",
6
6
  "bin": {