zames_pro 2.16.0 → 2.18.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');
@@ -185,6 +186,78 @@ export function resolveExtraDir(input, workdir) {
185
186
  }
186
187
  return { path: abs };
187
188
  }
189
+ /** How many characters of the dialogue are printed by default. */
190
+ export const RESTORED_HISTORY_LIMIT = 20;
191
+ /**
192
+ * Is this message worth showing to the operator as part of the dialogue?
193
+ *
194
+ * The chat history contains a lot of protocol noise that is meaningless in
195
+ * the terminal: the system-prompt (a huge REQUEST), tool results
196
+ * ("Tool result for ..."), the agent's raw tool-calls (JSON / DSML), the
197
+ * "You stopped after a tool result..." nudges, and the agent's <system> notes.
198
+ * We keep only real user turns and real assistant answers.
199
+ */
200
+ export function isDisplayableMessage(m) {
201
+ const text = String((m && m.text) || '').trim();
202
+ if (!text)
203
+ return false;
204
+ if (text.length > 100_000)
205
+ return false; // the system-prompt / a giant blob
206
+ if (m.role === 'user') {
207
+ // Tool output, the system-prompt and the corrective nudges are not the
208
+ // operator's words.
209
+ if (/^Tool result for /.test(text))
210
+ return false;
211
+ if (/^You are a coding agent running in a terminal/.test(text))
212
+ return false;
213
+ if (/^You stopped after a tool result/.test(text))
214
+ return false;
215
+ if (/^\[system\]/.test(text))
216
+ return false;
217
+ if (/^The user ran \//.test(text))
218
+ return false;
219
+ return true;
220
+ }
221
+ // Assistant: a tool-call (JSON or DSML) is not an answer to show. The
222
+ // DSML markers use FULL-WIDTH vertical bars (||DSML), so the regex must
223
+ // match them, not the ASCII pipe.
224
+ if (/DSML/i.test(text))
225
+ return false;
226
+ if (/^<system>/i.test(text))
227
+ return false;
228
+ if (/"tool"\s*:/.test(text.slice(0, 600))) {
229
+ return false;
230
+ }
231
+ if (/^\s*[\[{]/.test(text) && /"args"\s*:/.test(text.slice(0, 600))) {
232
+ return false;
233
+ }
234
+ return true;
235
+ }
236
+ /**
237
+ * Keep only the last `limit` messages and drop empties/service noise. The
238
+ * source (network history or DOM) may contain protocol messages; they are
239
+ * filtered out here so the terminal output stays readable.
240
+ */
241
+ export function trimRestoredMessages(messages, limit = RESTORED_HISTORY_LIMIT) {
242
+ const clean = (messages || []).filter((m) => m && typeof m.text === 'string' && isDisplayableMessage(m));
243
+ if (limit > 0 && clean.length > limit)
244
+ return clean.slice(clean.length - limit);
245
+ return clean;
246
+ }
247
+ /**
248
+ * Render the restored dialogue as plain text lines: "❯ ..." for the operator,
249
+ * "● ..." for the agent. Markdown rendering is done by the caller (it needs
250
+ * the terminal width); this helper is pure and unit-tested.
251
+ */
252
+ export function formatRestoredHistory(messages, opts = {}) {
253
+ const list = trimRestoredMessages(messages, opts.limit ?? RESTORED_HISTORY_LIMIT);
254
+ const out = [];
255
+ for (const m of list) {
256
+ const marker = m.role === 'user' ? '❯ ' : '● ';
257
+ out.push(marker + m.text.trim());
258
+ }
259
+ return out.join(NL + NL);
260
+ }
188
261
  // ---------- /review ----------
189
262
  export function buildReviewPrompt(focus, hasStaged = false) {
190
263
  const scope = hasStaged ? 'staged' : 'uncommitted';
@@ -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: {
@@ -43,6 +45,11 @@ export const DEFAULTS = {
43
45
  maxRateLimitRetries: 6,
44
46
  deepThinking: false,
45
47
  webSearch: true,
48
+ auth: {
49
+ username: '',
50
+ password: '',
51
+ saveSession: true,
52
+ },
46
53
  },
47
54
  ui: {
48
55
  locale: DEFAULT_LOCALE,
@@ -81,6 +88,9 @@ export const CONFIG_SCHEMA = [
81
88
  { path: 'ui.locale', type: 'enum', values: ['ru', 'en'], labelKey: 'cfg.f.ui_locale', groupKey: 'cfg.group.ui' },
82
89
  { path: 'maxIterations', type: 'number', min: 0, max: 100000, labelKey: 'cfg.f.maxIterations', groupKey: 'cfg.group.agent' },
83
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' },
84
94
  { path: 'debug', type: 'boolean', labelKey: 'cfg.f.debug', groupKey: 'cfg.group.agent' },
85
95
  { path: 'hotReload', type: 'boolean', labelKey: 'cfg.f.hotReload', groupKey: 'cfg.group.agent' },
86
96
  { path: 'confirmation.write', type: 'boolean', labelKey: 'cfg.f.confirmation_write', groupKey: 'cfg.group.confirmation' },
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}' },
@@ -205,6 +209,83 @@ const CATALOG = {
205
209
  'msg.reload_error': { ru: 'Ошибка reload:', en: 'Reload error:' },
206
210
  'msg.abort_gen_short': { ru: '⏹ Esc — прерываю генерацию...', en: '⏹ Esc — aborting generation...' },
207
211
  'msg.abort_ctrlc_short': { ru: '⏹ Ctrl+C — прерываю генерацию...', en: '⏹ Ctrl+C — aborting generation...' },
212
+ // ---------- DeepSeek-side problems (what is happening right now) ----------
213
+ // The operator must understand WHY the agent is waiting or stopped: a rate
214
+ // limit, a server hiccup, a refused send, a login problem. These strings are
215
+ // printed around the moment they happen (browser.ts / agent-loop.ts), not as
216
+ // a bare error at the end of the run.
217
+ 'ds.rate_limit_wait': {
218
+ ru: '⏳ DeepSeek: «слишком часто». Жду {min} мин ({attempt}/{max}) и повторю отправку...',
219
+ en: '⏳ DeepSeek: "too frequent". Waiting {min} min ({attempt}/{max}), then resending...',
220
+ },
221
+ 'ds.rate_limit_give_up': {
222
+ ru: '✖ DeepSeek не принял сообщение после {attempt} пауз по {min} мин. Подожди и попробуй снова.',
223
+ en: '✖ DeepSeek refused the message after {attempt} waits of {min} min. Wait and try again.',
224
+ },
225
+ 'ds.server_busy_wait': {
226
+ ru: '⏳ DeepSeek: сервер занят. Жду {sec}с ({attempt}/{max}) и повторю...',
227
+ en: '⏳ DeepSeek: server busy. Waiting {sec}s ({attempt}/{max}), then retrying...',
228
+ },
229
+ 'ds.server_busy_give_up': {
230
+ ru: '✖ DeepSeek: сервер не ответил после {attempt} повторов. Попробуй позже.',
231
+ en: '✖ DeepSeek: the server did not respond after {attempt} retries. Try again later.',
232
+ },
233
+ 'ds.ask_retry': {
234
+ ru: '⚠ Ответ не получен ({attempt}/{max}): {error} — повторяю...',
235
+ en: '⚠ No answer yet ({attempt}/{max}): {error} — retrying...',
236
+ },
237
+ 'ds.ask_restart_browser': {
238
+ ru: '⚠ Браузер потерял страницу — перезапускаю и вхожу заново...',
239
+ en: '⚠ The browser lost the page — restarting and signing in again...',
240
+ },
241
+ 'ds.ask_restart_failed': {
242
+ ru: '⚠ Не удалось перезапустить браузер: {error}',
243
+ en: '⚠ Could not restart the browser: {error}',
244
+ },
245
+ 'ds.ask_failed': {
246
+ ru: '✖ Не удалось получить ответ от DeepSeek после {max} попыток: {error}',
247
+ en: '✖ Could not get an answer from DeepSeek after {max} attempts: {error}',
248
+ },
249
+ 'ds.input_missing': {
250
+ ru: '✖ Не найдено поле ввода на странице DeepSeek. Запусти /debug-dom и поправь INPUT_SELECTORS.',
251
+ en: '✖ The DeepSeek input field was not found. Run /debug-dom and fix INPUT_SELECTORS.',
252
+ },
253
+ 'ds.input_partial': {
254
+ ru: '✖ Не удалось вставить текст в поле ввода целиком ({got} из {want} символов). Сообщение не отправлено, чтобы не отправить обрезанный текст. Попробуй ещё раз или разбей сообщение.',
255
+ en: '✖ Could not paste the whole text into the input ({got} of {want} chars). The message was not sent to avoid sending a truncated text. Retry or split the message.',
256
+ },
257
+ 'ds.send_no_start': {
258
+ ru: '✖ Ответ не начал генерироваться за 35с даже после повторной отправки. Проверь чат DeepSeek вручную (возможно, кнопка отправки не нажимается или сессия разлогинилась).',
259
+ en: '✖ The answer did not start generating within 35s even after resending. Check the DeepSeek chat manually (the send button may not be clickable or the session may have expired).',
260
+ },
261
+ 'ds.send_no_new_answer': {
262
+ ru: '✖ Новый ответ не получен — на странице остался прежний текст. Возможно, сообщение не отправилось. Проверь чат DeepSeek вручную.',
263
+ en: '✖ No new answer received — the page still shows the previous text. The message may not have been sent. Check the DeepSeek chat manually.',
264
+ },
265
+ 'ds.answer_timeout': {
266
+ ru: '⏳ DeepSeek не ответил за {sec}с — повторяю запрос...',
267
+ en: '⏳ DeepSeek did not answer within {sec}s — retrying the request...',
268
+ },
269
+ 'ds.answer_timeout_give_up': {
270
+ ru: '✖ DeepSeek перестал отвечать, лимит повторов исчерпан. Модель не дала ответа — проверь чат DeepSeek вручную.',
271
+ en: '✖ DeepSeek stopped responding, retry limit exhausted. The model gave no answer — check the DeepSeek chat manually.',
272
+ },
273
+ 'ds.tool_retry': {
274
+ ru: '⏳ Агент не распознал ответ модели — прошу продолжить ({attempt}/{max})...',
275
+ en: '⏳ The agent did not recognize the model answer — asking it to continue ({attempt}/{max})...',
276
+ },
277
+ 'ds.stalled': {
278
+ ru: '⚠ Агент остановился, не завершив задачу (модель перестала вызывать инструменты). Проверь чат DeepSeek — задача может быть не выполнена.',
279
+ en: '⚠ The agent stopped before finishing (the model stopped calling tools). Check the DeepSeek chat — the task may be incomplete.',
280
+ },
281
+ 'ds.not_launched': {
282
+ ru: '✖ Браузер не запущен.',
283
+ en: '✖ The browser is not launched.',
284
+ },
285
+ 'ds.open_chat_failed': {
286
+ ru: '✖ Не удалось открыть чат {id}: {error}',
287
+ en: '✖ Could not open chat {id}: {error}',
288
+ },
208
289
  // ---------- self-review ----------
209
290
  'self.review_failed': { ru: 'Самообзор провалился:', en: 'Self-review failed:' },
210
291
  'self.fix_usage': { ru: 'Использование: /self-fix <name> [фокус]', en: 'Usage: /self-fix <name> [focus]' },
@@ -234,6 +315,12 @@ const CATALOG = {
234
315
  'chats.prompt_will_resend': { ru: ' Системный промпт будет переслан на следующей задаче.\n', en: ' System prompt will be resent on the next task.\n' },
235
316
  'chats.current_id': { ru: 'Текущий chat id: {v}', en: 'Current chat id: {v}' },
236
317
  'chats.not_created': { ru: 'Чат ещё не создан.', en: 'No chat created yet.' },
318
+ 'chats.history_title': { ru: 'Диалог чата:', en: 'Chat dialogue:' },
319
+ 'chats.history_empty': { ru: 'Диалог пуст или не удалось прочитать сообщения.', en: 'The dialogue is empty or the messages could not be read.' },
320
+ 'chats.history_service_only': { ru: 'В этом чате нет пользовательских реплик — только служебные сообщения агента (вызовы инструментов).', en: 'This chat has no user turns — only the agent service messages (tool calls).' },
321
+ 'chats.history_truncated': { ru: '… показаны последние {n} сообщений.', en: '… showing the last {n} messages.' },
322
+ 'chats.history_you': { ru: 'Вы', en: 'You' },
323
+ 'chats.history_agent': { ru: 'Агент', en: 'Agent' },
237
324
  'sessions.dir': { ru: 'Папка сессий: {v}', en: 'Sessions dir: {v}' },
238
325
  'sessions.none': { ru: 'Сохранённых сессий нет. Они появятся после первой задачи/чата.', en: 'No saved sessions. They appear after the first task/chat.' },
239
326
  'sessions.restore_hint': { ru: 'Восстановить: /resume-id <id> (полный id) или /resume <n> после /chats.', en: 'Restore: /resume-id <id> (full id) or /resume <n> after /chats.' },
@@ -295,6 +382,37 @@ const CATALOG = {
295
382
  'cfg.f.browser_maxRateLimitRetries': { ru: 'Число повторов при лимите', en: 'Rate-limit retry count' },
296
383
  'cfg.f.browser_deepThinking': { ru: 'Глубокое мышление (долго; размышления не выводятся)', en: 'Deep thinking (slow; reasoning is hidden)' },
297
384
  'cfg.f.browser_webSearch': { ru: 'Умный поиск в интернете', en: 'Smart web search' },
385
+ 'cfg.f.browser_authUsername': { ru: 'Логин DeepSeek для авто-входа', en: 'DeepSeek login for auto sign-in' },
386
+ 'cfg.f.browser_authPassword': { ru: 'Пароль DeepSeek для авто-входа', en: 'DeepSeek password for auto sign-in' },
387
+ 'cfg.f.browser_authSaveSession': { ru: 'Сохранять сессию входа в профиле', en: 'Persist the login session in the profile' },
388
+ // ---------- login / auth ----------
389
+ 'auth.need_login': { ru: '🔐 Требуется вход в DeepSeek.', en: '🔐 DeepSeek sign-in required.' },
390
+ 'auth.prompt_login': { ru: 'Логин DeepSeek (телефон/email): ', en: 'DeepSeek login (phone/email): ' },
391
+ 'auth.prompt_password': { ru: 'Пароль DeepSeek: ', en: 'DeepSeek password: ' },
392
+ 'auth.prompt_password_saved': {
393
+ ru: 'Пароль DeepSeek (Enter — использовать сохранённый): ',
394
+ en: 'DeepSeek password (Enter — use the saved one): ',
395
+ },
396
+ 'auth.auto_login': { ru: 'Авто-вход в DeepSeek...', en: 'Signing in to DeepSeek...' },
397
+ 'auth.auto_login_ok': { ru: 'Вход выполнен.', en: 'Signed in.' },
398
+ 'auth.auto_login_failed': { ru: 'Авто-вход не удался: {v}', en: 'Auto sign-in failed: {v}' },
399
+ 'auth.login_rejected': { ru: 'DeepSeek отклонил вход: {v}', en: 'DeepSeek rejected the sign-in: {v}' },
400
+ 'auth.form_not_found': {
401
+ ru: 'Форма входа не распознана. Запустите с --headed и войдите вручную.',
402
+ en: 'Login form not recognized. Run with --headed and sign in manually.',
403
+ },
404
+ 'auth.no_reason': {
405
+ ru: 'причина не определена (возможно, неверный логин/пароль или капча)',
406
+ en: 'no reason detected (possibly wrong login/password or a captcha)',
407
+ },
408
+ 'auth.manual_hint': { ru: 'Войдите в DeepSeek в открытом браузере, затем нажмите Enter.', en: 'Sign in to DeepSeek in the open browser, then press Enter.' },
409
+ 'auth.manual_hint_headless': {
410
+ ru: 'Войдите в DeepSeek: откройте https://chat.deepseek.com/ в своём браузере. Если включён headless — задайте логин/пароль через /config или запустите с --headed.',
411
+ 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.',
412
+ },
413
+ 'auth.session_saved': { ru: 'Сессия DeepSeek сохранена.', en: 'DeepSeek session saved.' },
414
+ 'auth.session_restored': { ru: 'Сессия DeepSeek восстановлена.', en: 'DeepSeek session restored.' },
415
+ 'auth.enter_to_continue': { ru: 'Нажмите Enter, чтобы продолжить...', en: 'Press Enter to continue...' },
298
416
  'cfg.menu.title': { ru: '⚙ Настройки — выбери параметр', en: '⚙ Settings — choose a parameter' },
299
417
  'cfg.menu.hint': { ru: '↑/↓ — выбор, Enter — изменить, q/Esc — выйти', en: '↑/↓ select, Enter edit, q/Esc quit' },
300
418
  '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';
@@ -15,10 +16,22 @@ import { translate, normalizeLocale, localeDisplayName, isLocale, } from './i18n
15
16
  import { Transcript } from './transcript.js';
16
17
  import { UndoStore } from './undo.js';
17
18
  import { selfReview, selfDiff, selfApply, selfList } from './self-review.js';
18
- import { formatDiff, diffGitArgs, parseTranscript, summarizeTranscript, renderCost, formatExport, defaultExportPath, renderDoctor, renderPermissions, resolveExtraDir, buildReviewPrompt, } from './commands.js';
19
+ import { formatDiff, diffGitArgs, parseTranscript, summarizeTranscript, renderCost, formatExport, defaultExportPath, renderDoctor, renderPermissions, resolveExtraDir, buildReviewPrompt, trimRestoredMessages, RESTORED_HISTORY_LIMIT, } from './commands.js';
20
+ import { renderMarkdown } from './markdown.js';
19
21
  import { closeWeb } from './web.js';
20
22
  import { saveSession, loadLastSession, listSessions, sessionsDir, } from './sessions.js';
21
23
  const __dirname = path.dirname(fileURLToPath(import.meta.url));
24
+ const NL = String.fromCharCode(10);
25
+ // True when a DeepSeek session/credentials were stored earlier (auth.json).
26
+ // Used by /doctor to report that auto re-login is ready.
27
+ function authMarkerExists() {
28
+ try {
29
+ return existsSync(path.join(ZAMES_HOME, 'auth.json'));
30
+ }
31
+ catch {
32
+ return false;
33
+ }
34
+ }
22
35
  // ---------- CLI parsing ----------
23
36
  const args = process.argv.slice(2);
24
37
  function getArg(flag, fallback = null) {
@@ -50,7 +63,11 @@ let currentLocale = isLocale(config.ui?.locale)
50
63
  // Translation: reads currentLocale at call time, so a language change takes
51
64
  // effect immediately, without a restart (for lines printed afterwards).
52
65
  const t = (key, params) => translate(currentLocale)(key, params);
53
- const headless = hasFlag('--headless') || config.headless;
66
+ // Headless is the default. --headed forces a visible window (useful for the
67
+ // first sign-in or debugging the DOM). --headless keeps it explicit.
68
+ const headless = hasFlag('--headed')
69
+ ? false
70
+ : hasFlag('--headless') || config.headless;
54
71
  const debug = hasFlag('--debug') || config.debug;
55
72
  const calibrate = hasFlag('--calibrate');
56
73
  const maxIterArg = getArg('--max-iter', null);
@@ -189,6 +206,7 @@ ${theme.bold(t('help.options'))}
189
206
  --resend-prompt ${t('help.opt.resend_prompt')}
190
207
  --max-iter <n> ${t('help.opt.max_iter', { n: config.maxIterations })}
191
208
  --headless ${t('help.opt.headless')}
209
+ --headed ${t('help.opt.headed')}
192
210
  --debug ${t('help.opt.debug')}
193
211
  --calibrate ${t('help.opt.calibrate')}
194
212
  --dev ${t('help.opt.dev')}
@@ -772,6 +790,68 @@ async function resolveWorkdir() {
772
790
  }
773
791
  return dir;
774
792
  }
793
+ // ---------- restored dialogue ----------
794
+ /**
795
+ * Print the dialogue of the just-opened chat into the terminal. Called by
796
+ * /resume, /resume-id and on startup when a previous session is restored, so
797
+ * the operator sees the context instead of a bare "Chat opened.".
798
+ *
799
+ * Best-effort: a DOM-scraping failure must never break the restore.
800
+ */
801
+ async function printRestoredHistory(browser, ui, chatId = null) {
802
+ // Prefer DeepSeek's own history endpoint (complete, unvirtualized); fall
803
+ // back to scraping the rendered DOM when the request fails.
804
+ let all = [];
805
+ let fetchCount = -1;
806
+ let domCount = -1;
807
+ if (chatId && browser.fetchChatMessages) {
808
+ all = await browser.fetchChatMessages(chatId).catch(() => []);
809
+ fetchCount = all.length;
810
+ }
811
+ if (!all.length && browser.readChatMessages) {
812
+ all = await browser.readChatMessages().catch(() => []);
813
+ domCount = all.length;
814
+ }
815
+ const messages = trimRestoredMessages(all);
816
+ // Always surface the diagnostic line when the history could not be turned
817
+ // into anything printable — otherwise "the dialogue is empty" is a dead end
818
+ // (was the fetch blocked? did the chat really have no user turns?).
819
+ if (!all.length || (debug && !messages.length)) {
820
+ console.log(theme.dim(`[history] chat=${chatId || '-'} fetch=${fetchCount} dom=${domCount} raw=${all.length} shown=${messages.length} auth=${browser._apiAuth ? 'yes' : 'no'} err=${browser._lastHistoryError || '-'}`));
821
+ }
822
+ if (!all.length) {
823
+ console.log(theme.system(t('chats.history_empty')));
824
+ return;
825
+ }
826
+ if (!messages.length) {
827
+ // The history exists but contains only service/protocol messages (e.g.
828
+ // a chat where only the system-prompt and tool-calls were stored).
829
+ console.log(theme.system(t('chats.history_service_only', { n: String(all.length) })));
830
+ return;
831
+ }
832
+ const out = (text) => {
833
+ if (ui)
834
+ ui.printAbove(text);
835
+ else
836
+ console.log(text);
837
+ };
838
+ out(theme.system(t('chats.history_title')));
839
+ for (const m of messages) {
840
+ if (m.role === 'user') {
841
+ out(theme.user('❯ ' + t('chats.history_you') + ': ') + m.text.trim());
842
+ }
843
+ else {
844
+ out(NL +
845
+ theme.assistant('● ' + t('chats.history_agent') + ':') +
846
+ NL +
847
+ renderMarkdown(m.text.trim()) +
848
+ NL);
849
+ }
850
+ }
851
+ if (all.length > messages.length) {
852
+ out(theme.dim(t('chats.history_truncated', { n: String(RESTORED_HISTORY_LIMIT) })));
853
+ }
854
+ }
775
855
  // ---------- task runner ----------
776
856
  async function runTask(browser, tools, taskText, workdir, opts, attachments = []) {
777
857
  const { transcript, freshChat, sendSystemPrompt, queue = [], ui: editor, onChatReady, } = opts;
@@ -828,6 +908,7 @@ async function runTask(browser, tools, taskText, workdir, opts, attachments = []
828
908
  attachments: next.attachments || [],
829
909
  transcript,
830
910
  onThinking: () => ui.thinking(),
911
+ onSendPause: (seconds) => ui.sendPause(seconds),
831
912
  onToolCall: (name, toolArgs) => ui.toolCall(name, toolArgs),
832
913
  onToolResult: (result) => ui.toolResult(result),
833
914
  onAssistantMessage: (msg) => ui.assistant(msg),
@@ -881,9 +962,10 @@ async function runTask(browser, tools, taskText, workdir, opts, attachments = []
881
962
  finally {
882
963
  stopWatching();
883
964
  ui.stop();
884
- // Detach the send hook so a later browser.ask() outside this task cannot
965
+ // Detach the send hooks so a later browser.ask() outside this task cannot
885
966
  // start a stale spinner.
886
967
  browser.onSendStart = null;
968
+ browser.onSendPause = null;
887
969
  }
888
970
  }
889
971
  // ---------- main ----------
@@ -944,6 +1026,18 @@ async function main() {
944
1026
  debug,
945
1027
  channel: config.browserChannel,
946
1028
  ...config.browser,
1029
+ locale: currentLocale,
1030
+ // Persist credentials the operator typed in the terminal so the next
1031
+ // launch can re-login silently after a logout.
1032
+ onAuthSave: (username, password) => {
1033
+ config.browser.auth.username = username;
1034
+ config.browser.auth.password = password;
1035
+ try {
1036
+ writeConfigValue('home', 'browser.auth.username', username);
1037
+ writeConfigValue('home', 'browser.auth.password', password);
1038
+ }
1039
+ catch { }
1040
+ },
947
1041
  });
948
1042
  const bootSpinner = mod.createSpinner(currentLocale);
949
1043
  bootSpinner.thinking();
@@ -1061,6 +1155,7 @@ async function main() {
1061
1155
  sendSystemPromptNext = resendPrompt;
1062
1156
  saveLastChat(resumeId, currentWorkdir);
1063
1157
  console.log(theme.system(t('msg.chat_opened') + ' ' + resumeId + String.fromCharCode(10)));
1158
+ await printRestoredHistory(browser, null, resumeId);
1064
1159
  }
1065
1160
  catch (e) {
1066
1161
  console.error(theme.error(t('msg.open_chat_error', { v: e.message })));
@@ -1096,6 +1191,7 @@ async function main() {
1096
1191
  const ed = new LineEditor({
1097
1192
  prompt: buildPrompt(),
1098
1193
  commands: buildSlashCommands(),
1194
+ locale: currentLocale,
1099
1195
  });
1100
1196
  editor = ed;
1101
1197
  ed.setTmpDir(TMP_DIR);
@@ -1259,6 +1355,7 @@ async function main() {
1259
1355
  currentLocale = value;
1260
1356
  config.ui.locale = value;
1261
1357
  if (editor) {
1358
+ editor.setLocale(value);
1262
1359
  editor.setCommands(buildSlashCommands());
1263
1360
  editor.setPrompt(buildPrompt());
1264
1361
  }
@@ -1304,7 +1401,9 @@ async function main() {
1304
1401
  ? cur
1305
1402
  ? t('common.on')
1306
1403
  : t('common.off')
1307
- : JSON.stringify(cur);
1404
+ : /password/i.test(f.path) && String(cur).length > 0
1405
+ ? '********'
1406
+ : JSON.stringify(cur);
1308
1407
  const extra = f.values ? ' [' + f.values.join('|') + ']' : '';
1309
1408
  console.log(' ' +
1310
1409
  theme.user(f.path) +
@@ -1395,7 +1494,11 @@ async function main() {
1395
1494
  const cur = getByPath(config, key);
1396
1495
  console.log(theme.system(t('cfg.value', {
1397
1496
  v: key,
1398
- value: cur === undefined ? t('cfg.menu.default') : JSON.stringify(cur),
1497
+ value: cur === undefined
1498
+ ? t('cfg.menu.default')
1499
+ : /password/i.test(key) && String(cur).length > 0
1500
+ ? '********'
1501
+ : JSON.stringify(cur),
1399
1502
  })));
1400
1503
  return;
1401
1504
  }
@@ -1736,6 +1839,7 @@ async function main() {
1736
1839
  theme.system(resendPrompt
1737
1840
  ? ' Системный промпт будет переслан на следующей задаче.\n'
1738
1841
  : ' Контекст чата сохранён. Системный промпт не пересылается (--resend-prompt чтобы дослать).\n'));
1842
+ await printRestoredHistory(browser, editor, pick.id);
1739
1843
  }
1740
1844
  catch (e) {
1741
1845
  console.error(theme.error(t('msg.open_chat_error', { v: '' })), e.message);
@@ -1793,6 +1897,7 @@ async function main() {
1793
1897
  transcript.log('resume_chat', { id });
1794
1898
  console.log(theme.assistant('Чат открыт.') +
1795
1899
  theme.system(String.fromCharCode(10)));
1900
+ await printRestoredHistory(browser, editor, id);
1796
1901
  }
1797
1902
  catch (e) {
1798
1903
  console.error(theme.error(t('msg.open_chat_error', { v: '' })), e.message);
@@ -2101,6 +2206,7 @@ async function main() {
2101
2206
  mcpServers: mcpStatus.servers.length,
2102
2207
  mcpTools: mcpStatus.toolCount,
2103
2208
  transcriptOk: !!transcript.file,
2209
+ authSaved: authMarkerExists(),
2104
2210
  })));
2105
2211
  continue;
2106
2212
  }