zames_pro 2.20.0 → 2.21.2

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.
@@ -47,7 +47,7 @@ export async function runAgentLoop({ browser, tools, task, workdir, maxIteration
47
47
  id = await browser.getCurrentChatId();
48
48
  }
49
49
  catch {
50
- id = null;
50
+ // keep id = null
51
51
  }
52
52
  if (id && id !== lastReportedChatId) {
53
53
  lastReportedChatId = id;
@@ -56,7 +56,7 @@ export async function runAgentLoop({ browser, tools, task, workdir, maxIteration
56
56
  };
57
57
  await reportChat();
58
58
  if (sendSystemPrompt) {
59
- let gitText = null;
59
+ let gitText;
60
60
  try {
61
61
  const ctx = await getGitContext(workdir);
62
62
  gitText = formatGitContext(ctx);
@@ -64,7 +64,7 @@ export async function runAgentLoop({ browser, tools, task, workdir, maxIteration
64
64
  catch (e) {
65
65
  gitText = `(git context error: ${e.message})`;
66
66
  }
67
- let context = null;
67
+ let context;
68
68
  try {
69
69
  context = await loadProjectContext(workdir);
70
70
  }
@@ -267,7 +267,9 @@ export async function runAgentLoop({ browser, tools, task, workdir, maxIteration
267
267
  // A stale answer is discarded only when it does NOT parse as a tool
268
268
  // call (see wdStale above); an echo of a call that never ran is a real
269
269
  // call and must be executed, not dropped.
270
- if (!isFirst && (wdEmpty || wdStale || wdNoCall) && watchdogRetries < MAX_WATCHDOG_RETRIES) {
270
+ if (!isFirst &&
271
+ (wdEmpty || wdStale || wdNoCall) &&
272
+ watchdogRetries < MAX_WATCHDOG_RETRIES) {
271
273
  watchdogRetries++;
272
274
  transcript?.log('watchdog_nudge', {
273
275
  attempt: watchdogRetries,
@@ -372,7 +374,8 @@ export async function runAgentLoop({ browser, tools, task, workdir, maxIteration
372
374
  // README…" or "Let me run the tests…" and goes silent. If this is taken
373
375
  // as final, the agent stalls without doing the work. We ask it to
374
376
  // continue and to actually call a tool this time (or respond if truly done).
375
- if (looksLikeUnfinishedWork(trimmed) && looksDoneRetries < MAX_LOOKSDONE_RETRIES) {
377
+ if (looksLikeUnfinishedWork(trimmed) &&
378
+ looksDoneRetries < MAX_LOOKSDONE_RETRIES) {
376
379
  looksDoneRetries++;
377
380
  unparsedRetries++;
378
381
  transcript?.log('unfinished_retry', {
@@ -454,9 +457,12 @@ export async function runAgentLoop({ browser, tools, task, workdir, maxIteration
454
457
  // a READABLE report, but say explicitly that the task may be incomplete:
455
458
  // never let a reasoning paragraph masquerade as a finished result.
456
459
  const lastText = (rawResponse || '').trim();
457
- return (lastText
458
- ? lastText + String.fromCharCode(10) + String.fromCharCode(10) + '(The model stopped calling tools before finishing. The task may be incomplete - check the DeepSeek chat.)'
459
- : 'The model stopped calling tools before finishing the task.');
460
+ return lastText
461
+ ? lastText +
462
+ String.fromCharCode(10) +
463
+ String.fromCharCode(10) +
464
+ '(The model stopped calling tools before finishing. The task may be incomplete - check the DeepSeek chat.)'
465
+ : 'The model stopped calling tools before finishing the task.';
460
466
  }
461
467
  const suspiciousFinal = responseLooksLikeToolCall(rawResponse) ||
462
468
  looksLikeUnfinishedWork((rawResponse || '').trim()) ||
@@ -608,8 +614,8 @@ export function responseLooksLikeToolCall(rawResponse) {
608
614
  // argument key (args/command/path/...). We require the opening bracket at
609
615
  // the start (after spaces/prefix) so we don't catch ordinary prose with
610
616
  // colons like "path: ...".
611
- /^\s*[\[\{]/.test(raw) &&
612
- /["']?(?:tool|args|command|path|old_string|content|content_base64)["']?\s*:/.test(raw) ||
617
+ (/^\s*[\[\{]/.test(raw) &&
618
+ /["']?(?:tool|args|command|path|old_string|content|content_base64)["']?\s*:/.test(raw)) ||
613
619
  /<\s*\|?\s*(DSML|invoke|parameter)/i.test(raw) ||
614
620
  /^\s*\[?\s*\{[^}]*$/.test(raw.trim()));
615
621
  }
@@ -839,7 +845,7 @@ function parseArgsPermissive(str) {
839
845
  break;
840
846
  if (str[i] !== '"')
841
847
  return null;
842
- let keyEnd = str.indexOf('"', i + 1);
848
+ const keyEnd = str.indexOf('"', i + 1);
843
849
  if (keyEnd === -1)
844
850
  return null;
845
851
  const key = str.slice(i + 1, keyEnd);
@@ -994,7 +1000,9 @@ function parseToolCallPermissive(text) {
994
1000
  if (braceIdx === -1)
995
1001
  return null;
996
1002
  const endBrace = findMatching(text, braceIdx, '{', '}');
997
- const objText = endBrace === -1 ? text.slice(braceIdx) : text.slice(braceIdx, endBrace + 1);
1003
+ const objText = endBrace === -1
1004
+ ? text.slice(braceIdx)
1005
+ : text.slice(braceIdx, endBrace + 1);
998
1006
  const inlineArgs = parseArgsPermissive(objText) || parseArgsGreedy(objText);
999
1007
  if (!inlineArgs)
1000
1008
  return null;
@@ -4,14 +4,23 @@ import os from 'os';
4
4
  import { execFileSync } from 'child_process';
5
5
  import { extractPathToken } from './path-token.js';
6
6
  export { extractPathToken };
7
- export const IMAGE_EXTS = ['.png', '.jpg', '.jpeg', '.gif', '.webp', '.bmp', '.svg', '.ico'];
7
+ export const IMAGE_EXTS = [
8
+ '.png',
9
+ '.jpg',
10
+ '.jpeg',
11
+ '.gif',
12
+ '.webp',
13
+ '.bmp',
14
+ '.svg',
15
+ '.ico',
16
+ ];
8
17
  export const MAX_ATTACHMENT_BYTES = 20 * 1024 * 1024;
9
18
  function sanitize(name) {
10
- return String(name || '')
19
+ return (String(name || '')
11
20
  .replace(/[\u0000-\u001f\u007f]/g, '')
12
21
  .replace(/[\\/:*?"<>|]/g, '_')
13
22
  .replace(/\s+/g, '_')
14
- .slice(0, 80) || 'file';
23
+ .slice(0, 80) || 'file');
15
24
  }
16
25
  export function guessMime(name) {
17
26
  const ext = path.extname(String(name || '')).toLowerCase();
@@ -74,7 +83,10 @@ export function parseDataUrl(text) {
74
83
  if (!m)
75
84
  return null;
76
85
  try {
77
- return { mime: m[1] || 'application/octet-stream', data: Buffer.from(m[2].replace(/\s+/g, ''), 'base64') };
86
+ return {
87
+ mime: m[1] || 'application/octet-stream',
88
+ data: Buffer.from(m[2].replace(/\s+/g, ''), 'base64'),
89
+ };
78
90
  }
79
91
  catch {
80
92
  return null;
@@ -189,7 +201,9 @@ export function hasClipboardTool() {
189
201
  if (process.platform === 'win32' || process.platform === 'darwin')
190
202
  return true;
191
203
  if (process.platform === 'linux') {
192
- return Boolean(tryCommand('xclip', ['-version']) || tryCommand('xsel', ['--version']) || tryCommand('wl-paste', ['--version']));
204
+ return Boolean(tryCommand('xclip', ['-version']) ||
205
+ tryCommand('xsel', ['--version']) ||
206
+ tryCommand('wl-paste', ['--version']));
193
207
  }
194
208
  return false;
195
209
  }
@@ -238,7 +252,9 @@ const PS_GET_FILE_DROP = 'Add-Type -AssemblyName System.Windows.Forms; ' +
238
252
  'if($d -and $d.GetDataPresent([System.Windows.Forms.DataFormats]::FileDrop)){' +
239
253
  '$f=$d.GetData([System.Windows.Forms.DataFormats]::FileDrop); $f -join [Environment]::NewLine}';
240
254
  export async function readClipboardImageDetailed() {
241
- if (process.platform === 'linux' || process.platform === 'freebsd' || process.platform === 'openbsd') {
255
+ if (process.platform === 'linux' ||
256
+ process.platform === 'freebsd' ||
257
+ process.platform === 'openbsd') {
242
258
  const wayland = process.env.WAYLAND_DISPLAY;
243
259
  const x11 = process.env.DISPLAY;
244
260
  const attempts = [];
@@ -249,14 +265,34 @@ export async function readClipboardImageDetailed() {
249
265
  return { data: win, via: 'powershell.exe' };
250
266
  }
251
267
  if (wayland || !x11)
252
- attempts.push({ bin: 'wl-paste', args: ['--type', 'image/png', '--no-newline'], via: 'wl-paste' });
268
+ attempts.push({
269
+ bin: 'wl-paste',
270
+ args: ['--type', 'image/png', '--no-newline'],
271
+ via: 'wl-paste',
272
+ });
253
273
  if (x11 || !wayland) {
254
- attempts.push({ bin: 'xclip', args: ['-selection', 'clipboard', '-t', 'image/png', '-o'], via: 'xclip' });
255
- attempts.push({ bin: 'xsel', args: ['--clipboard', '--output'], via: 'xsel' });
274
+ attempts.push({
275
+ bin: 'xclip',
276
+ args: ['-selection', 'clipboard', '-t', 'image/png', '-o'],
277
+ via: 'xclip',
278
+ });
279
+ attempts.push({
280
+ bin: 'xsel',
281
+ args: ['--clipboard', '--output'],
282
+ via: 'xsel',
283
+ });
256
284
  }
257
285
  // Even if the session env is missing, try the other family as a fallback.
258
- attempts.push({ bin: 'wl-paste', args: ['--type', 'image/png', '--no-newline'], via: 'wl-paste' });
259
- attempts.push({ bin: 'xclip', args: ['-selection', 'clipboard', '-t', 'image/png', '-o'], via: 'xclip' });
286
+ attempts.push({
287
+ bin: 'wl-paste',
288
+ args: ['--type', 'image/png', '--no-newline'],
289
+ via: 'wl-paste',
290
+ });
291
+ attempts.push({
292
+ bin: 'xclip',
293
+ args: ['-selection', 'clipboard', '-t', 'image/png', '-o'],
294
+ via: 'xclip',
295
+ });
260
296
  const tried = [];
261
297
  for (const a of attempts) {
262
298
  if (tried.includes(a.via))
@@ -272,7 +308,10 @@ export async function readClipboardImageDetailed() {
272
308
  }
273
309
  if (process.platform === 'darwin') {
274
310
  const data = tryCommand('pngpaste', ['-']);
275
- return { data: data && data.length ? data : null, via: data ? 'pngpaste' : 'no-tool:pngpaste' };
311
+ return {
312
+ data: data && data.length ? data : null,
313
+ via: data ? 'pngpaste' : 'no-tool:pngpaste',
314
+ };
276
315
  }
277
316
  if (process.platform === 'win32') {
278
317
  // PowerShell reads the clipboard image and writes raw PNG bytes to stdout.
@@ -281,8 +320,16 @@ export async function readClipboardImageDetailed() {
281
320
  'if($img){$ms=New-Object System.IO.MemoryStream; ' +
282
321
  '$img.Save($ms,[System.Drawing.Imaging.ImageFormat]::Png); ' +
283
322
  '[Console]::OpenStandardOutput().Write($ms.ToArray(),0,$ms.Length)}';
284
- const data = tryCommand('powershell', ['-NoProfile', '-NonInteractive', '-Command', ps]);
285
- return { data: data && data.length ? data : null, via: data ? 'powershell' : 'no-tool:powershell' };
323
+ const data = tryCommand('powershell', [
324
+ '-NoProfile',
325
+ '-NonInteractive',
326
+ '-Command',
327
+ ps,
328
+ ]);
329
+ return {
330
+ data: data && data.length ? data : null,
331
+ via: data ? 'powershell' : 'no-tool:powershell',
332
+ };
286
333
  }
287
334
  return { data: null, via: 'unsupported-platform' };
288
335
  }
@@ -329,7 +376,13 @@ export async function findFileByName(workdir, name) {
329
376
  export function readWindowsClipboardFiles() {
330
377
  if (!isWsl())
331
378
  return [];
332
- const out = tryCommand('powershell.exe', ['-STA', '-NoProfile', '-NonInteractive', '-Command', PS_GET_FILE_DROP]);
379
+ const out = tryCommand('powershell.exe', [
380
+ '-STA',
381
+ '-NoProfile',
382
+ '-NonInteractive',
383
+ '-Command',
384
+ PS_GET_FILE_DROP,
385
+ ]);
333
386
  if (!out)
334
387
  return [];
335
388
  const text = out.toString('utf-8');
@@ -360,10 +413,20 @@ export function winPathToWsl(winPath) {
360
413
  export async function readWindowsClipboardImage() {
361
414
  if (!isWsl())
362
415
  return null;
363
- const out = tryCommand('powershell.exe', ['-STA', '-NoProfile', '-NonInteractive', '-EncodedCommand', PS_IMAGE_ENCODED]);
416
+ const out = tryCommand('powershell.exe', [
417
+ '-STA',
418
+ '-NoProfile',
419
+ '-NonInteractive',
420
+ '-EncodedCommand',
421
+ PS_IMAGE_ENCODED,
422
+ ]);
364
423
  if (!out)
365
424
  return null;
366
- const first = out.toString('utf-8').trim().split(String.fromCharCode(10))[0].trim();
425
+ const first = out
426
+ .toString('utf-8')
427
+ .trim()
428
+ .split(String.fromCharCode(10))[0]
429
+ .trim();
367
430
  if (!first)
368
431
  return null;
369
432
  const wslPath = winPathToWsl(first);
package/dist/browser.js CHANGED
@@ -165,7 +165,15 @@ const STOP_SELECTORS = [
165
165
  // Otherwise the agent takes a status (Reading...) for an answer and breaks parsing.
166
166
  const STATUS_RE = /^(reading|thinking|searching|analyzing|generating|stop|остановить|читаю|думаю|поиск|анализ)[\s.…]*$/i;
167
167
  // DeepSeek's answer when the rate limit is exceeded.
168
- const RATE_LIMIT_RE = /(messages? too frequent|too many requests|rate limit|слишком часто|повторите позже|try again later)/i;
168
+ //
169
+ // IMPORTANT: the generic "try again later" / "повторите позже" tail is NOT
170
+ // part of this pattern. DeepSeek appends it to MANY toasts ("Server busy.
171
+ // Try again later.", "Service unavailable. Try again later."), so matching it
172
+ // here classified every server hiccup as a RATE LIMIT and sent the agent into
173
+ // a 5-minute wait (and, after the finish-loop stopped reading toasts every
174
+ // tick, looked like "the agent hung"). The rate limit is recognized by its
175
+ // specific wording only.
176
+ const RATE_LIMIT_RE = /(messages? too frequent|too many requests|rate limit|слишком часто|сообщени[яе] слишком част)/i;
169
177
  // Transient server-side hiccups (DeepSeek overloaded / hiccup). Unlike the
170
178
  // rate limit, these usually clear in seconds, so we retry quickly instead of
171
179
  // waiting minutes. "Server busy", 503, "temporarily unavailable", etc.
@@ -415,7 +423,7 @@ export class DeepSeekBrowser {
415
423
  try {
416
424
  this.context = await chromium.launchPersistentContext(USER_DATA_DIR, options);
417
425
  }
418
- catch (e2) {
426
+ catch {
419
427
  if (this.debug)
420
428
  console.error('profile: сношу целиком');
421
429
  await nukeProfile();
@@ -501,7 +509,13 @@ export class DeepSeekBrowser {
501
509
  await this.context.close();
502
510
  }
503
511
  catch { }
504
- await this.launch();
512
+ // A restart must REUSE the existing profile. `launch()` would call
513
+ // killStaleChrome() and clean Singleton files, which can race with a
514
+ // sibling agent on the same profile; here we just relaunch the persistent
515
+ // context in place. A fresh window appears only because the old context was
516
+ // actually closed — no second window is created while one is alive.
517
+ await this._launchOnce();
518
+ await this._fixHeadlessUserAgent();
505
519
  }
506
520
  async waitForLogin() {
507
521
  // 1) Already signed in (session cookie in the persistent profile)?
@@ -1096,7 +1110,11 @@ export class DeepSeekBrowser {
1096
1110
  })));
1097
1111
  // Interruptible: Esc/Ctrl+C must cancel this long wait too,
1098
1112
  // otherwise "stop" stays frozen for up to 5 minutes.
1099
- const aborted = await this._sleepInterruptible(this.rateLimitWaitMs);
1113
+ // The wait is ANIMATED through onSendPause (a live countdown), so the
1114
+ // operator sees the agent is waiting on the rate limit instead of a
1115
+ // frozen spinner. `_lastSentAt` is bumped so the countdown renders.
1116
+ const waitSec = Math.ceil(this.rateLimitWaitMs / 1000);
1117
+ const aborted = await this._waitRateLimit(waitSec);
1100
1118
  if (aborted)
1101
1119
  return '(прервано пользователем)';
1102
1120
  continue;
@@ -1129,7 +1147,12 @@ export class DeepSeekBrowser {
1129
1147
  max: this.askRetries,
1130
1148
  error: e.message,
1131
1149
  })));
1132
- if (/closed|crash|Target page|browser/i.test(e.message)) {
1150
+ // Only a genuinely LOST page justifies a restart. "Target page,
1151
+ // context or browser has been closed" means the page is gone for good;
1152
+ // a bare "browser" mention (e.g. a selector error containing the word)
1153
+ // must NOT tear the window down — that was the "a new window opened"
1154
+ // surprise during a resumed chat. We re-open the SAME context instead.
1155
+ if (/target page, context or browser has been closed|page.*has been closed/i.test(e.message)) {
1133
1156
  console.error(theme.warn(this._t('ds.ask_restart_browser')));
1134
1157
  try {
1135
1158
  await this.restart();
@@ -1276,6 +1299,22 @@ export class DeepSeekBrowser {
1276
1299
  // Wait for the upload to finish (the attach preview to appear).
1277
1300
  await this.page.waitForTimeout(2000);
1278
1301
  }
1302
+ // Wait out a rate-limit pause, showing an animated countdown through
1303
+ // onSendPause so the operator sees the agent is alive (not frozen). Returns
1304
+ // true when the wait was cut short by Esc/Ctrl+C.
1305
+ async _waitRateLimit(totalSec) {
1306
+ const tick = (leftMs) => {
1307
+ const secs = Math.max(0, Math.ceil(leftMs / 1000));
1308
+ if (this.onSendPause) {
1309
+ try {
1310
+ this.onSendPause(secs);
1311
+ }
1312
+ catch { }
1313
+ }
1314
+ };
1315
+ tick(totalSec * 1000);
1316
+ return await this._sleepInterruptible(totalSec * 1000, tick);
1317
+ }
1279
1318
  // Pause between sends. Applied ONLY to agent messages
1280
1319
  // (tool-result, system-prompt) so we don't hit the rate limit.
1281
1320
  // User input is sent without delay.
@@ -1560,20 +1599,24 @@ export class DeepSeekBrowser {
1560
1599
  const deadline = Date.now() + timeout;
1561
1600
  let last = '';
1562
1601
  let stable = 0;
1563
- let tick = 0;
1564
1602
  while (Date.now() < deadline) {
1565
1603
  if (this._abort) {
1566
1604
  return last || '(прервано пользователем)';
1567
1605
  }
1568
- // We check the limit toast not every tick but about once per 5 ticks,
1569
- // so we don't poke the DOM unnecessarily.
1570
- if (tick++ % 5 === 0) {
1606
+ // Rate-limit / server-busy toast. The old code only read the toasts
1607
+ // when `tick % 5 === 0` (once per ~2s), and the throw on the rate limit
1608
+ // made the server-busy branch unreachable. A toast that appears and
1609
+ // disappears between those checks was missed entirely, so the run
1610
+ // waited out the whole deadline and threw ds.send_no_new_answer. We now
1611
+ // read the toast on EVERY tick (a cheap, selector-scoped evaluate) and
1612
+ // classify both conditions independently.
1613
+ {
1571
1614
  const pageText = await this._readPageText();
1572
1615
  if (isRateLimitText(pageText)) {
1573
1616
  throw new RateLimitError(pageText.slice(0, 300));
1574
- if (isServerBusyText(pageText)) {
1575
- throw new ServerBusyError(pageText.slice(0, 300));
1576
- }
1617
+ }
1618
+ if (isServerBusyText(pageText)) {
1619
+ throw new ServerBusyError(pageText.slice(0, 300));
1577
1620
  }
1578
1621
  }
1579
1622
  const netFresh = !!this._netCapture && this._netCaptureAt >= this._lastSentAt;
package/dist/commands.js CHANGED
@@ -7,7 +7,10 @@ const CR = String.fromCharCode(13);
7
7
  // ---------- /diff ----------
8
8
  export function formatDiff(diffText, opts = {}) {
9
9
  const maxLines = opts.maxLines ?? 200;
10
- const trimmed = String(diffText ?? '').split(CR + NL).join(NL).trimEnd();
10
+ const trimmed = String(diffText ?? '')
11
+ .split(CR + NL)
12
+ .join(NL)
13
+ .trimEnd();
11
14
  if (!trimmed)
12
15
  return '(no changes)';
13
16
  const lines = trimmed.split(NL);
@@ -17,7 +20,9 @@ export function formatDiff(diffText, opts = {}) {
17
20
  const rest = lines.length - maxLines;
18
21
  return (head +
19
22
  NL +
20
- '... [' + rest + ' more lines, use Bash git diff for the full output]');
23
+ '... [' +
24
+ rest +
25
+ ' more lines, use Bash git diff for the full output]');
21
26
  }
22
27
  export function diffGitArgs(staged = false) {
23
28
  return staged ? 'git diff --staged' : 'git diff';
@@ -66,7 +66,9 @@ export function runConfigMenu(opts) {
66
66
  }
67
67
  const selected = i === cursor;
68
68
  const marker = selected ? theme.prompt('\u276f ') : ' ';
69
- const label = selected ? theme.prompt(f.labelKey ? t(f.labelKey) : f.path) : t(f.labelKey || f.path);
69
+ const label = selected
70
+ ? theme.prompt(f.labelKey ? t(f.labelKey) : f.path)
71
+ : t(f.labelKey || f.path);
70
72
  const val = displayValue(f, get(f.path), t);
71
73
  const valText = selected ? theme.assistant(val) : theme.dim(val);
72
74
  const pathText = selected ? theme.dim(' ' + f.path) : '';
package/dist/config.js CHANGED
@@ -85,29 +85,163 @@ function deepMerge(target, source) {
85
85
  }
86
86
  }
87
87
  export const CONFIG_SCHEMA = [
88
- { path: 'ui.locale', type: 'enum', values: ['ru', 'en'], labelKey: 'cfg.f.ui_locale', groupKey: 'cfg.group.ui' },
89
- { path: 'maxIterations', type: 'number', min: 0, max: 100000, labelKey: 'cfg.f.maxIterations', groupKey: 'cfg.group.agent' },
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' },
94
- { path: 'debug', type: 'boolean', labelKey: 'cfg.f.debug', groupKey: 'cfg.group.agent' },
95
- { path: 'hotReload', type: 'boolean', labelKey: 'cfg.f.hotReload', groupKey: 'cfg.group.agent' },
96
- { path: 'confirmation.write', type: 'boolean', labelKey: 'cfg.f.confirmation_write', groupKey: 'cfg.group.confirmation' },
97
- { path: 'confirmation.edit', type: 'boolean', labelKey: 'cfg.f.confirmation_edit', groupKey: 'cfg.group.confirmation' },
98
- { path: 'confirmation.bash', type: 'boolean', labelKey: 'cfg.f.confirmation_bash', groupKey: 'cfg.group.confirmation' },
99
- { path: 'undo.enabled', type: 'boolean', labelKey: 'cfg.f.undo_enabled', groupKey: 'cfg.group.undo' },
100
- { path: 'undo.maxBackups', type: 'number', min: 1, max: 100000, labelKey: 'cfg.f.undo_maxBackups', groupKey: 'cfg.group.undo' },
101
- { path: 'transcript.enabled', type: 'boolean', labelKey: 'cfg.f.transcript_enabled', groupKey: 'cfg.group.transcript' },
102
- { path: 'browser.answerTimeoutMs', type: 'number', min: 1000, max: 3600000, labelKey: 'cfg.f.browser_answerTimeoutMs', groupKey: 'cfg.group.browser' },
103
- { path: 'browser.askRetries', type: 'number', min: 1, max: 100, labelKey: 'cfg.f.browser_askRetries', groupKey: 'cfg.group.browser' },
104
- { path: 'browser.stabilityChecks', type: 'number', min: 1, max: 100, labelKey: 'cfg.f.browser_stabilityChecks', groupKey: 'cfg.group.browser' },
105
- { path: 'browser.stabilityDelayMs', type: 'number', min: 0, max: 60000, labelKey: 'cfg.f.browser_stabilityDelayMs', groupKey: 'cfg.group.browser' },
106
- { path: 'browser.minSendIntervalMs', type: 'number', min: 0, max: 600000, labelKey: 'cfg.f.browser_minSendIntervalMs', groupKey: 'cfg.group.browser' },
107
- { path: 'browser.rateLimitWaitMs', type: 'number', min: 0, max: 3600000, labelKey: 'cfg.f.browser_rateLimitWaitMs', groupKey: 'cfg.group.browser' },
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' },
88
+ {
89
+ path: 'ui.locale',
90
+ type: 'enum',
91
+ values: ['ru', 'en'],
92
+ labelKey: 'cfg.f.ui_locale',
93
+ groupKey: 'cfg.group.ui',
94
+ },
95
+ {
96
+ path: 'maxIterations',
97
+ type: 'number',
98
+ min: 0,
99
+ max: 100000,
100
+ labelKey: 'cfg.f.maxIterations',
101
+ groupKey: 'cfg.group.agent',
102
+ },
103
+ {
104
+ path: 'headless',
105
+ type: 'boolean',
106
+ labelKey: 'cfg.f.headless',
107
+ groupKey: 'cfg.group.agent',
108
+ },
109
+ {
110
+ path: 'browser.auth.username',
111
+ type: 'string',
112
+ labelKey: 'cfg.f.browser_authUsername',
113
+ groupKey: 'cfg.group.browser',
114
+ },
115
+ {
116
+ path: 'browser.auth.password',
117
+ type: 'string',
118
+ labelKey: 'cfg.f.browser_authPassword',
119
+ groupKey: 'cfg.group.browser',
120
+ },
121
+ {
122
+ path: 'browser.auth.saveSession',
123
+ type: 'boolean',
124
+ labelKey: 'cfg.f.browser_authSaveSession',
125
+ groupKey: 'cfg.group.browser',
126
+ },
127
+ {
128
+ path: 'debug',
129
+ type: 'boolean',
130
+ labelKey: 'cfg.f.debug',
131
+ groupKey: 'cfg.group.agent',
132
+ },
133
+ {
134
+ path: 'hotReload',
135
+ type: 'boolean',
136
+ labelKey: 'cfg.f.hotReload',
137
+ groupKey: 'cfg.group.agent',
138
+ },
139
+ {
140
+ path: 'confirmation.write',
141
+ type: 'boolean',
142
+ labelKey: 'cfg.f.confirmation_write',
143
+ groupKey: 'cfg.group.confirmation',
144
+ },
145
+ {
146
+ path: 'confirmation.edit',
147
+ type: 'boolean',
148
+ labelKey: 'cfg.f.confirmation_edit',
149
+ groupKey: 'cfg.group.confirmation',
150
+ },
151
+ {
152
+ path: 'confirmation.bash',
153
+ type: 'boolean',
154
+ labelKey: 'cfg.f.confirmation_bash',
155
+ groupKey: 'cfg.group.confirmation',
156
+ },
157
+ {
158
+ path: 'undo.enabled',
159
+ type: 'boolean',
160
+ labelKey: 'cfg.f.undo_enabled',
161
+ groupKey: 'cfg.group.undo',
162
+ },
163
+ {
164
+ path: 'undo.maxBackups',
165
+ type: 'number',
166
+ min: 1,
167
+ max: 100000,
168
+ labelKey: 'cfg.f.undo_maxBackups',
169
+ groupKey: 'cfg.group.undo',
170
+ },
171
+ {
172
+ path: 'transcript.enabled',
173
+ type: 'boolean',
174
+ labelKey: 'cfg.f.transcript_enabled',
175
+ groupKey: 'cfg.group.transcript',
176
+ },
177
+ {
178
+ path: 'browser.answerTimeoutMs',
179
+ type: 'number',
180
+ min: 1000,
181
+ max: 3600000,
182
+ labelKey: 'cfg.f.browser_answerTimeoutMs',
183
+ groupKey: 'cfg.group.browser',
184
+ },
185
+ {
186
+ path: 'browser.askRetries',
187
+ type: 'number',
188
+ min: 1,
189
+ max: 100,
190
+ labelKey: 'cfg.f.browser_askRetries',
191
+ groupKey: 'cfg.group.browser',
192
+ },
193
+ {
194
+ path: 'browser.stabilityChecks',
195
+ type: 'number',
196
+ min: 1,
197
+ max: 100,
198
+ labelKey: 'cfg.f.browser_stabilityChecks',
199
+ groupKey: 'cfg.group.browser',
200
+ },
201
+ {
202
+ path: 'browser.stabilityDelayMs',
203
+ type: 'number',
204
+ min: 0,
205
+ max: 60000,
206
+ labelKey: 'cfg.f.browser_stabilityDelayMs',
207
+ groupKey: 'cfg.group.browser',
208
+ },
209
+ {
210
+ path: 'browser.minSendIntervalMs',
211
+ type: 'number',
212
+ min: 0,
213
+ max: 600000,
214
+ labelKey: 'cfg.f.browser_minSendIntervalMs',
215
+ groupKey: 'cfg.group.browser',
216
+ },
217
+ {
218
+ path: 'browser.rateLimitWaitMs',
219
+ type: 'number',
220
+ min: 0,
221
+ max: 3600000,
222
+ labelKey: 'cfg.f.browser_rateLimitWaitMs',
223
+ groupKey: 'cfg.group.browser',
224
+ },
225
+ {
226
+ path: 'browser.maxRateLimitRetries',
227
+ type: 'number',
228
+ min: 0,
229
+ max: 100,
230
+ labelKey: 'cfg.f.browser_maxRateLimitRetries',
231
+ groupKey: 'cfg.group.browser',
232
+ },
233
+ {
234
+ path: 'browser.deepThinking',
235
+ type: 'boolean',
236
+ labelKey: 'cfg.f.browser_deepThinking',
237
+ groupKey: 'cfg.group.browser',
238
+ },
239
+ {
240
+ path: 'browser.webSearch',
241
+ type: 'boolean',
242
+ labelKey: 'cfg.f.browser_webSearch',
243
+ groupKey: 'cfg.group.browser',
244
+ },
111
245
  ];
112
246
  export function getConfigField(path) {
113
247
  return CONFIG_SCHEMA.find((f) => f.path === path);