zames_pro 2.64.1 → 2.65.1

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/index.js CHANGED
@@ -10,7 +10,7 @@ import { createTools } from './tools.js';
10
10
  import { createTodoStore, getTodos, setTodos, resetTodos, renderTodos, } from './extraTools.js';
11
11
  import { runAgentLoop } from './agent-loop.js';
12
12
  import { createSpinner } from './spinner.js';
13
- import { LineEditor, expandPastes, pasteReplacement, formatCompactTokens, } from './input.js';
13
+ import { LineEditor } from './input.js';
14
14
  import { parseImagePaste, extForMime, saveToTemp, guessMime, formatSize, readClipboardImageDetailed, sniffMime, readWindowsClipboardFiles, hasClipboardTool, writeClipboardText, contentHash, } from './attachments.js';
15
15
  import { loadConfig, DEFAULTS, CONFIG_PATHS, ZAMES_HOME, CONFIG_SCHEMA, getByPath, validateConfigValue, writeConfigValue, resetConfigValue, } from './config.js';
16
16
  import { runConfigMenu } from './config-menu.js';
@@ -19,11 +19,13 @@ import { Transcript } from './transcript.js';
19
19
  import { UndoStore } from './undo.js';
20
20
  import { CheckpointStore, pickRecord as pickCheckpoint } from './checkpoint.js';
21
21
  import { selfReview, selfDiff, selfApply, selfList } from './self-review.js';
22
- import { formatDiff, formatDiffStat, formatContextSources, diffGitArgs, parseTranscript, summarizeTranscript, renderCost, formatExport, defaultExportPath, renderDoctor, resolveExtraDir, buildReviewPrompt, isDevOnlyCommand, parseBacklogItems, nextBacklogItem, buildImprovePrompt, formatDuration, formatRelativeTime, trimRestoredMessages, RESTORED_HISTORY_LIMIT, mergeMessages, hasQueuedJob, isSlashCommand, parseQueueCommand, parseLiveToggle, parseGoalCommand, isLiveConfigCommand, withGoal, formatQueueList, ctrlCEscalation, expandCommandArgs, missingCommandArgs, } from './commands.js';
22
+ import { formatDiff, formatDiffStat, formatContextSources, diffGitArgs, parseTranscript, summarizeTranscript, renderCost, formatExport, defaultExportPath, renderDoctor, resolveExtraDir, buildReviewPrompt, isDevOnlyCommand, parseBacklogItems, nextBacklogItem, buildImprovePrompt, formatRelativeTime, trimRestoredMessages, RESTORED_HISTORY_LIMIT, hasQueuedJob, buildSlashCommandHints, parseQueueCommand, parseLiveToggle, parseGoalCommand, isLiveConfigCommand, formatQueueList, ctrlCEscalation, expandCommandArgs, missingCommandArgs, } from './commands.js';
23
23
  import { performCompact } from './compact.js';
24
24
  import { Scheduler, parseInterval, formatInterval, formatJobLine, parseCron, } from './scheduler.js';
25
25
  import { renderMarkdown, setAnswerWidth } from './markdown.js';
26
26
  import { resolveAttachPath, inlineAtRefs } from './attach-refs.js';
27
+ import { parseBacklogNote } from './backlog.js';
28
+ import { runTask } from './run-task.js';
27
29
  import { closeWeb } from './web.js';
28
30
  import { assertCommandInsideRoot, runShell } from './shell.js';
29
31
  import { saveSession, loadLastSession, readSession, listSessions, sessionsDir, loadHistory, saveHistory, } from './sessions.js';
@@ -461,73 +463,6 @@ ${theme.dim(t('help.toggles_legend'))}
461
463
  function dirLabel(p) {
462
464
  return path.basename(p) || p;
463
465
  }
464
- // List of slash commands for completion when you type «/» (Tab — complete).
465
- // The description is localized by the help.cmd.* key at display time — see
466
- // buildSlashCommands().
467
- //
468
- // Dev-only commands (backlog-driven self-improvement, self-review snapshots)
469
- // are filtered out of the hints and rejected by the main loop unless the
470
- // operator is in dev mode — a regular package install must never advertise
471
- // them, and a muscle-memory `/improve` must not edit the user's BACKLOG.md.
472
- // The list itself lives in commands.ts (`isDevOnlyCommand`), one source.
473
- const SLASH_COMMANDS = [
474
- { name: '/help', key: 'help.cmd.help' },
475
- { name: '/new', key: 'help.cmd.new' },
476
- { name: '/clear', key: 'help.cmd.new' },
477
- { name: '/sessions', key: 'help.cmd.sessions' },
478
- { name: '/chats', key: 'help.cmd.chats' },
479
- { name: '/resume', key: 'help.cmd.resume' },
480
- { name: '/resume-id', key: 'help.cmd.resume_id' },
481
- { name: '/last', key: 'help.cmd.last' },
482
- { name: '/retry', key: 'help.cmd.retry' },
483
- { name: '/rename', key: 'help.cmd.rename' },
484
- { name: '/context', key: 'help.cmd.context' },
485
- { name: '/copy', key: 'help.cmd.copy' },
486
- { name: '/chat', key: 'help.cmd.chat' },
487
- { name: '/cd', key: 'help.cmd.cd' },
488
- { name: '/pwd', key: 'help.cmd.pwd' },
489
- { name: '/status', key: 'help.cmd.status' },
490
- { name: '/reload', key: 'help.cmd.reload' },
491
- { name: '/undo', key: 'help.cmd.undo' },
492
- { name: '/undo-list', key: 'help.cmd.undo_list' },
493
- { name: '/rewind', key: 'help.cmd.rewind' },
494
- { name: '/rewind-list', key: 'help.cmd.rewind_list' },
495
- { name: '/transcript', key: 'help.cmd.transcript' },
496
- { name: '/diff', key: 'help.cmd.diff' },
497
- { name: '/diffstat', key: 'help.cmd.diffstat' },
498
- { name: '/cost', key: 'help.cmd.cost' },
499
- { name: '/export', key: 'help.cmd.export' },
500
- { name: '/doctor', key: 'help.cmd.doctor' },
501
- { name: '/add-dir', key: 'help.cmd.add_dir' },
502
- { name: '/review', key: 'help.cmd.review' },
503
- { name: '/improve', key: 'help.cmd.improve' },
504
- { name: '/backlog', key: 'help.cmd.backlog' },
505
- { name: '/goal', key: 'help.cmd.goal' },
506
- { name: '/loop', key: 'help.cmd.loop' },
507
- { name: '/cron', key: 'help.cmd.cron' },
508
- { name: '/jobs', key: 'help.cmd.jobs' },
509
- { name: '/thinking', key: 'help.cmd.thinking' },
510
- { name: '/web', key: 'help.cmd.web' },
511
- { name: '/plan', key: 'help.cmd.plan' },
512
- { name: '/compact', key: 'help.cmd.compact' },
513
- { name: '/queue', key: 'help.cmd.queue' },
514
- { name: '/tasks', key: 'help.cmd.tasks' },
515
- { name: '/config', key: 'help.cmd.config' },
516
- { name: '/skills', key: 'help.cmd.skills' },
517
- { name: '/memory', key: 'help.cmd.memory' },
518
- { name: '/remember', key: 'help.cmd.remember' },
519
- { name: '/init', key: 'help.cmd.init' },
520
- { name: '/mcp', key: 'help.cmd.mcp' },
521
- { name: '/debug-dom', key: 'help.cmd.debug_dom' },
522
- { name: '/self-review', key: 'help.self.review' },
523
- { name: '/self-fix', key: 'help.self.fix' },
524
- { name: '/self-done', key: 'help.self.done' },
525
- { name: '/self-list', key: 'help.self.list' },
526
- { name: '/self-diff', key: 'help.self.diff' },
527
- { name: '/self-apply', key: 'help.self.apply' },
528
- { name: '/exit', key: 'help.cmd.exit' },
529
- { name: '/quit', key: 'help.cmd.exit' },
530
- ];
531
466
  // Dynamically discovered skills and custom commands for the current workdir.
532
467
  // Filled in by refreshDynamicCommands() and shown in the «/» hint list and
533
468
  // help. Skills are surfaced as /skill-name entries (user-invokable only).
@@ -610,19 +545,11 @@ async function expandSlashTarget(workdir, name, rest) {
610
545
  }
611
546
  // Slash-command descriptions in the current language (for LineEditor hints).
612
547
  // Dev-only commands are filtered out unless the operator is in dev mode, so a
613
- // regular install never advertises (or accepts) them.
548
+ // regular install never advertises (or accepts) them. The pure assembly lives
549
+ // in commands.ts (buildSlashCommandHints); here we only supply the runtime
550
+ // data (dev flag, translator, dynamic skills/commands).
614
551
  function buildSlashCommands() {
615
- const base = SLASH_COMMANDS.filter((c) => devMode || !isDevOnlyCommand(c.name)).map((c) => ({
616
- name: c.name,
617
- description: t(c.key),
618
- }));
619
- const known = new Set(base.map((c) => c.name.toLowerCase()));
620
- for (const d of dynamicCommands) {
621
- if (known.has(d.name.toLowerCase()))
622
- continue;
623
- base.push(d);
624
- }
625
- return base;
552
+ return buildSlashCommandHints({ devMode, t, dynamic: dynamicCommands });
626
553
  }
627
554
  // We put the agent's temporary files (one-off scripts, etc.) in
628
555
  // <project>/tmp — this folder is in .gitignore and is cleaned on every launch.
@@ -634,7 +561,7 @@ async function cleanTmpDir() {
634
561
  }
635
562
  catch (e) {
636
563
  if (debug)
637
- console.error('tmp: не удалось очистить:', e.message);
564
+ console.error('tmp: cleanup failed:', e.message);
638
565
  }
639
566
  }
640
567
  // Terminal line input with correct paste handling (Shift+Insert,
@@ -839,157 +766,6 @@ async function promptOnce(question) {
839
766
  stdin.on('data', onData);
840
767
  });
841
768
  }
842
- // Keyboard monitoring while the agent works.
843
- //
844
- // The terminal stays live while the agent thinks:
845
- // - Esc (or Ctrl+C) — abort the current generation (a Stop click in the browser);
846
- // - typing + Enter — queue a message; it goes to the agent right after
847
- // the current task finishes (like "send during generation" in the
848
- // DeepSeek web version).
849
- //
850
- // Input is buffered without a line editor: the typed text is shown in the
851
- // spinner line via onChange -> ui.setPending(). Enter sends the buffer to
852
- // onQueue, an empty Enter is ignored. Backspace, Ctrl+U, Esc sequences
853
- // (arrows/Home/End/Delete are ignored) and bracketed paste are supported.
854
- function watchInput({ onEscape, onChange, onQueue, } = {}) {
855
- const stdin = process.stdin;
856
- if (!stdin.isTTY || !stdin.setRawMode)
857
- return () => { };
858
- // Control characters are assembled from codes: this file must not contain
859
- // "raw" ESC/CR/LF in string literals (see AGENTS.md).
860
- const ESC = String.fromCharCode(27);
861
- const CSI = String.fromCharCode(91);
862
- const CR = String.fromCharCode(13);
863
- const LF = String.fromCharCode(10);
864
- const wasRaw = stdin.isRaw;
865
- stdin.setRawMode(true);
866
- stdin.resume();
867
- process.stdout.write(ESC + '[?2004h');
868
- let buf = '';
869
- let inPaste = false;
870
- let pasteBuf = '';
871
- const pastes = [];
872
- const PASTE_START = ESC + '[200~';
873
- const PASTE_END = ESC + '[201~';
874
- const CSI_RE = new RegExp('^' + CSI + '[0-9;]*[A-Za-z~]');
875
- const emitChange = () => {
876
- if (onChange)
877
- onChange(buf);
878
- };
879
- // Pasted text: small pastes (1–2 lines) are flattened into one line;
880
- // large ones (3+ lines) are collapsed into "[Pasted lines#N]" (the original
881
- // text is expanded back when the message is queued).
882
- const insert = (text) => {
883
- buf += text
884
- .split(CR + LF)
885
- .join(' ')
886
- .split(CR)
887
- .join(' ')
888
- .split(LF)
889
- .join(' ');
890
- };
891
- const insertPaste = (raw) => {
892
- const rep = pasteReplacement(raw);
893
- if (!rep) {
894
- insert(raw);
895
- return;
896
- }
897
- pastes.push(rep);
898
- buf += rep.marker;
899
- };
900
- function onData(data) {
901
- let s = data.toString('utf-8');
902
- // A single Esc — abort generation. Arrows come as a whole chunk and
903
- // don't reach here.
904
- if (!inPaste && s === ESC) {
905
- if (onEscape)
906
- onEscape();
907
- return;
908
- }
909
- while (s.length) {
910
- if (inPaste) {
911
- const end = s.indexOf(PASTE_END);
912
- if (end === -1) {
913
- pasteBuf += s;
914
- s = '';
915
- }
916
- else {
917
- pasteBuf += s.slice(0, end);
918
- s = s.slice(end + PASTE_END.length);
919
- inPaste = false;
920
- insertPaste(pasteBuf);
921
- pasteBuf = '';
922
- }
923
- emitChange();
924
- continue;
925
- }
926
- const start = s.indexOf(PASTE_START);
927
- if (start !== -1) {
928
- const before = s.slice(0, start);
929
- s = s.slice(start + PASTE_START.length);
930
- inPaste = true;
931
- pasteBuf = '';
932
- if (before) {
933
- insert(before);
934
- emitChange();
935
- }
936
- continue;
937
- }
938
- const ch = s[0];
939
- const code = s.charCodeAt(0);
940
- s = s.slice(1);
941
- if (ch === CR || ch === LF) {
942
- const text = expandPastes(pastes, buf).trim();
943
- buf = '';
944
- pastes.length = 0;
945
- emitChange();
946
- if (text && onQueue)
947
- onQueue(text);
948
- continue;
949
- }
950
- if (code === 3) {
951
- // Ctrl+C — like Esc: abort generation.
952
- if (onEscape)
953
- onEscape();
954
- continue;
955
- }
956
- if (code === 21) {
957
- // Ctrl+U — clear what was typed.
958
- buf = '';
959
- pastes.length = 0;
960
- emitChange();
961
- continue;
962
- }
963
- if (code === 127 || code === 8) {
964
- // Backspace.
965
- if (buf) {
966
- buf = buf.slice(0, -1);
967
- emitChange();
968
- }
969
- continue;
970
- }
971
- if (ch === ESC) {
972
- // Escape sequence (arrows, Home/End, Delete) — skip.
973
- const m = s.match(CSI_RE);
974
- if (m)
975
- s = s.slice(m[0].length);
976
- continue;
977
- }
978
- if (code < 32)
979
- continue; // other control characters — ignore
980
- buf += ch;
981
- emitChange();
982
- }
983
- }
984
- const handler = (data) => onData(data);
985
- stdin.on('data', handler);
986
- return () => {
987
- stdin.removeListener('data', handler);
988
- process.stdout.write(ESC + '[?2004l');
989
- if (stdin.setRawMode)
990
- stdin.setRawMode(wasRaw || false);
991
- };
992
- }
993
769
  // ---------- workdir resolution ----------
994
770
  // The agent works in the directory it was launched from (process.cwd()).
995
771
  // This is the sandbox root: tools cannot go above it.
@@ -1084,252 +860,23 @@ async function printRestoredHistory(browser, ui, chatId = null) {
1084
860
  out(theme.dim(t('chats.history_truncated', { n: String(RESTORED_HISTORY_LIMIT) })));
1085
861
  }
1086
862
  }
1087
- // ---------- task runner ----------
1088
- // Ask the operator to approve a tool call flagged by an `ask` permission rule
1089
- // (C1). While the LineEditor owns the terminal it must be paused first, or the
1090
- // confirmation prompt and the input line fight for the same keys. A non-TTY run
1091
- // has no way to answer, so it DENIES (never silently allows a guarded call).
1092
- async function askOperatorConfirm(editor, question) {
1093
- if (!(process.stdin.isTTY && process.stdout.isTTY))
1094
- return false;
1095
- if (editor)
1096
- editor.pause();
1097
- try {
1098
- const a = await promptOnce(question + ' [y/N] ');
1099
- return /^(y|yes|д|да)$/i.test(a.trim());
1100
- }
1101
- catch {
1102
- return false;
1103
- }
1104
- finally {
1105
- if (editor)
1106
- editor.resume();
1107
- }
1108
- }
1109
- async function runTask(browser, tools, taskText, workdir, opts, attachments = []) {
1110
- const { transcript, freshChat, sendSystemPrompt, queue = [], ui: editor, onChatReady, onAutoCompact = null, autoCompactPct = 95, contextLimit = 1_000_000, getTokenUsage = null, goal = null, todosQuery = null, selfImprovement = false, } = opts;
1111
- // In TTY mode the UI is a LineEditor: it owns the input (queue, Esc,
1112
- // Ctrl+C) and draws the status ABOVE the permanent input line. In non-TTY
1113
- // mode (pipes) — a regular spinner + watchInput.
1114
- // A new task from the prompt — we reset the "stop" from the previous abort.
1115
- browser._stopped = false;
1116
- browser._abort = false;
1117
- const ui = editor || mod.createSpinner(currentLocale);
1118
- // Non-TTY parity: the ora spinner shows the same "tasks: 2/5" badge the
1119
- // LineEditor shows (the editor gets it via onTasksQuery in main()).
1120
- if (!editor && todosQuery && 'onTasksQuery' in ui) {
1121
- ;
1122
- ui.onTasksQuery = todosQuery;
1123
- }
1124
- // Mark the editor busy for the WHOLE task, not only for /init and /self-fix.
1125
- // Esc / Ctrl+C abort the current generation only while busy; without this a
1126
- // long-running tool (Bash, npm, MCP) could not be interrupted — Esc did
1127
- // nothing. Cleared in the finally below.
1128
- if (editor)
1129
- editor.busy = true;
1130
- const stopWatching = editor
1131
- ? () => { }
1132
- : watchInput({
1133
- onEscape: () => {
1134
- ui.stop();
1135
- console.error(theme.warn(t('msg.abort_gen_short')));
1136
- browser.stopGeneration().catch(() => { });
1137
- },
1138
- onChange: (text) => ui.setPending(text),
1139
- onQueue: (text) => {
1140
- queue.push({ text });
1141
- ui.setPending(null);
1142
- ui.stop();
1143
- console.log(theme.user(t('msg.queued', { n: queue.length })) +
1144
- theme.assistant(text));
1145
- ui.thinking();
1146
- },
1147
- });
1148
- try {
1149
- let next = {
1150
- text: taskText,
1151
- attachments,
1152
- freshChat,
1153
- sendSystemPrompt,
1154
- };
1155
- // Execute the task, then everything the user managed to type while it
1156
- // ran. The queue may be replenished right during draining.
1157
- //
1158
- // NOTE: we do NOT call ui.thinking() here. runAgentLoop() fires
1159
- // onThinking() right before the actual browser send (after the send
1160
- // pause, system-prompt, etc.), so the spinner only appears when a
1161
- // generation really starts. Calling it here made the spinner run for
1162
- // the whole pre-send phase (chat creation, throttle wait) with no
1163
- // generation in flight.
1164
- while (true) {
1165
- // T7: per-task summary (duration + number of tool calls + tokens).
1166
- // Counts tools via the UI callback and reports ONE line after the task,
1167
- // so the operator sees how long it took and how much work happened.
1168
- const taskStart = Date.now();
1169
- // Token delta for THIS task: accumulated_token_usage is cumulative for
1170
- // the whole chat, so the per-task spend is after - before. Null when the
1171
- // counter is unknown (fresh chat / no answer yet); the token part is then
1172
- // omitted from the summary.
1173
- const tokensBefore = getTokenUsage ? getTokenUsage() : null;
1174
- let taskTools = 0;
1175
- const outcome = await mod.runAgentLoop({
1176
- browser,
1177
- tools,
1178
- task: withGoal(goal, next.text),
1179
- workdir,
1180
- maxIterations: maxIter,
1181
- freshChat: next.freshChat,
1182
- sendSystemPrompt: next.sendSystemPrompt,
1183
- attachments: next.attachments || [],
1184
- transcript,
1185
- onThinking: () => ui.thinking(),
1186
- onSendPause: (seconds) => ui.sendPause(seconds),
1187
- onSendState: (state) => {
1188
- if (editor)
1189
- editor.setSendState(state);
1190
- },
1191
- onNotice: (msg) => ui.warning(msg),
1192
- onToolCall: (name, toolArgs) => {
1193
- taskTools++;
1194
- ui.toolCall(name, toolArgs);
1195
- },
1196
- onToolResult: (result) => ui.toolResult(result),
1197
- onAssistantMessage: (msg) => {
1198
- // Remember the last answer so /copy can put it on the clipboard
1199
- // without re-reading the chat. Wrapped: a UI failure is swallowed
1200
- // by agent-loop anyway.
1201
- lastAssistantMessage = msg;
1202
- opts.onAssistantFinal?.(msg);
1203
- ui.assistant(msg);
1204
- },
1205
- onWarning: (msg) => ui.warning(msg),
1206
- onAskPermission: async (info) => {
1207
- // C1: a rule with action "ask". Show the reason and wait for the
1208
- // operator. In non-TTY this denies (see askOperatorConfirm).
1209
- ui.warning(t('perm.ask', { tool: info.tool, reason: info.reason }));
1210
- const ok = await askOperatorConfirm(editor || null, t('perm.confirm'));
1211
- ui.warning(ok ? t('perm.allowed') : t('perm.denied'));
1212
- transcript?.log('permission_ask', {
1213
- tool: info.tool,
1214
- reason: info.reason,
1215
- allowed: ok,
1216
- });
1217
- return ok;
1218
- },
1219
- onChatReady,
1220
- debugLog: debug,
1221
- locale: currentLocale,
1222
- askDeadlineMs: config.browser.askDeadlineMs,
1223
- maxAfterToolRetries: config.browser.maxAfterToolRetries,
1224
- // Auto-compact between tool calls when the context nears the
1225
- // window limit. The callback is provided by the caller (runTask) so it
1226
- // can refresh the outer currentChatId. It runs in the SAME execution
1227
- // context (this task owns the browser send loop) and the loop awaits
1228
- // it, so it serializes with the throttle.
1229
- onAutoCompact,
1230
- autoCompactPct,
1231
- contextLimit,
1232
- getTokenUsage,
1233
- selfImprovement,
1234
- });
1235
- // The loop may end WITHOUT a model answer: an exhausted iteration
1236
- // limit or an ask() watchdog (the model stopped responding). In that
1237
- // case no assistant message was shown, and the operator saw the run
1238
- // just "stop" after a tool call with no explanation. Surface it.
1239
- if (outcome &&
1240
- (outcome.startsWith('Iteration limit reached') ||
1241
- outcome.startsWith('ask() watchdog'))) {
1242
- ui.warning(outcome);
1243
- transcript?.log('agent_no_answer', { outcome });
1244
- }
1245
- // T7: one summary line per task (duration + tool calls + tokens spent).
1246
- // Printed even on an abort, so the operator sees what happened. Duration
1247
- // is human-readable ("45s", "1m 20s", "1h 12m 45s") with LOCALIZED unit
1248
- // labels, instead of a raw seconds count like "3129.2s". The token part
1249
- // is the DELTA of the cumulative counter for THIS task; it is omitted
1250
- // when the counter is unknown.
1251
- const tokensAfter = getTokenUsage ? getTokenUsage() : null;
1252
- const tokenDelta = typeof tokensBefore === 'number' && typeof tokensAfter === 'number'
1253
- ? Math.max(0, tokensAfter - tokensBefore)
1254
- : null;
1255
- const summaryParts = [
1256
- t('task_sum.dur', {
1257
- dur: formatDuration(Date.now() - taskStart, {
1258
- h: t('dur.h'),
1259
- m: t('dur.m'),
1260
- s: t('dur.s'),
1261
- }),
1262
- }),
1263
- t('task_sum.tools', { n: String(taskTools) }),
1264
- ];
1265
- if (tokenDelta !== null) {
1266
- summaryParts.push(t('task_sum.tokens', { n: formatCompactTokens(tokenDelta) }));
1267
- }
1268
- const summary = theme.taskSummary('· ' + summaryParts.join(' · '));
1269
- if (editor)
1270
- editor.printAbove(summary);
1271
- else
1272
- console.log(summary);
1273
- // Aborted (Esc/Ctrl+C) — we don't start the next tasks from the queue
1274
- // and clear it, so "stop" really stops everything.
1275
- if (browser._stopped) {
1276
- queue.length = 0;
1277
- break;
1278
- }
1279
- if (!queue.length)
1280
- break;
1281
- // Merge the LEADING plain-text messages into ONE batch (fewer sends ->
1282
- // less rate-limit risk and the operator's thoughts arrive together). We
1283
- // stop at the first slash-command: it must NOT be sent as a task — it is
1284
- // handled by the main loop, so we leave it (and anything after it) in
1285
- // the queue and break out of this task's drain loop.
1286
- let batchLen = 0;
1287
- while (batchLen < queue.length && !isSlashCommand(queue[batchLen].text)) {
1288
- batchLen++;
1289
- }
1290
- if (batchLen === 0)
1291
- break;
1292
- const batch = queue.splice(0, batchLen);
1293
- const merged = mergeMessages(batch);
1294
- if (!merged.text.trim())
1295
- break;
1296
- ui.stop();
1297
- const banner = batch.length === 1
1298
- ? theme.user(t('msg.from_queue')) + theme.assistant(batch[0].text)
1299
- : theme.user(t('msg.from_queue_batch', { n: batch.length })) +
1300
- theme.assistant(t('msg.batch_joined', { n: batch.length }));
1301
- if (editor)
1302
- editor.printAbove(banner);
1303
- else
1304
- console.log(banner);
1305
- transcript?.log('queued_task', { task: merged.text, count: batch.length });
1306
- next = {
1307
- text: merged.text,
1308
- attachments: merged.attachments,
1309
- freshChat: false,
1310
- sendSystemPrompt: false,
1311
- };
1312
- }
1313
- }
1314
- catch (e) {
1315
- ui.stop();
1316
- console.error(theme.error(String.fromCharCode(10) + t('msg.agent_error')), e.message);
1317
- if (debug)
1318
- console.error(e.stack);
1319
- transcript?.log('agent_error', { error: e.message });
1320
- }
1321
- finally {
1322
- stopWatching();
1323
- ui.stop();
1324
- if (editor)
1325
- editor.busy = false;
1326
- // Detach the send hooks so a later browser.ask() outside this task cannot
1327
- // start a stale spinner.
1328
- browser.onSendStart = null;
1329
- browser.onSendPause = null;
1330
- browser.onSendState = null;
1331
- browser.onNotice = null;
1332
- }
863
+ // Build the explicit dependency bag for runTask() (src/run-task.ts). The
864
+ // main loop owns the browser/queue/config state, so every call site passes
865
+ // the SAME collaborators here instead of runTask reaching into module
866
+ // globals.
867
+ function runTaskDeps() {
868
+ return {
869
+ t,
870
+ locale: currentLocale,
871
+ debug,
872
+ maxIter,
873
+ config,
874
+ runAgentLoop: mod.runAgentLoop,
875
+ createSpinner: mod.createSpinner,
876
+ onAssistantMessage: (msg) => {
877
+ lastAssistantMessage = msg;
878
+ },
879
+ };
1333
880
  }
1334
881
  // ---------- main ----------
1335
882
  async function main() {
@@ -1464,7 +1011,7 @@ async function main() {
1464
1011
  }
1465
1012
  }
1466
1013
  await autoReload();
1467
- await runTask(browser, tools, task, currentWorkdir, {
1014
+ await runTask(runTaskDeps(), browser, tools, task, currentWorkdir, {
1468
1015
  transcript,
1469
1016
  freshChat,
1470
1017
  sendSystemPrompt,
@@ -1974,6 +1521,11 @@ async function main() {
1974
1521
  if (path === 'ui.locale' && isLocale(value)) {
1975
1522
  currentLocale = value;
1976
1523
  config.ui.locale = value;
1524
+ // The browser (and its startup spinner) are created ONCE, so their own
1525
+ // notices must be told about the new language explicitly — otherwise a
1526
+ // DeepSeek-side message printed later would still come out in the OLD
1527
+ // language until a restart.
1528
+ browser.setLocale(value);
1977
1529
  if (editor) {
1978
1530
  editor.setLocale(value);
1979
1531
  editor.setCommands(buildSlashCommands());
@@ -3255,7 +2807,7 @@ async function main() {
3255
2807
  if (editor)
3256
2808
  editor.busy = true;
3257
2809
  try {
3258
- await runTask(browser, tools, lastTaskText, currentWorkdir, {
2810
+ await runTask(runTaskDeps(), browser, tools, lastTaskText, currentWorkdir, {
3259
2811
  transcript,
3260
2812
  freshChat: false,
3261
2813
  sendSystemPrompt: false,
@@ -3465,7 +3017,7 @@ async function main() {
3465
3017
  if (editor)
3466
3018
  editor.busy = true;
3467
3019
  try {
3468
- await runTask(browser, improveTools, buildImprovePrompt(item), currentWorkdir, {
3020
+ await runTask(runTaskDeps(), browser, improveTools, buildImprovePrompt(item), currentWorkdir, {
3469
3021
  transcript,
3470
3022
  freshChat: false,
3471
3023
  sendSystemPrompt: false,
@@ -3531,25 +3083,9 @@ async function main() {
3531
3083
  }
3532
3084
  // Deterministic append: no model round-trip is needed to record a
3533
3085
  // one-line note, and a pure helper cannot invent a different id than
3534
- // the one the operator is shown. An optional leading `P0..P3` sets the
3535
- // priority; the first line is the title, the rest becomes the body.
3536
- let priority = 'P2';
3537
- let body = note;
3538
- for (const p of ['P0', 'P1', 'P2', 'P3']) {
3539
- if (note.toUpperCase().startsWith(p + ' ')) {
3540
- priority = p;
3541
- body = note.slice(p.length + 1);
3542
- break;
3543
- }
3544
- }
3545
- const bodyLines = body.split(String.fromCharCode(10));
3546
- const title = bodyLines[0];
3547
- const rest = bodyLines.slice(1).join(String.fromCharCode(10)).trim();
3548
- const res = mod.appendBacklogItem(backlogText, {
3549
- title,
3550
- priority,
3551
- note: rest || undefined,
3552
- });
3086
+ // the one the operator is shown.
3087
+ const parsed = parseBacklogNote(note);
3088
+ const res = mod.appendBacklogItem(backlogText, parsed);
3553
3089
  await fs.writeFile(backlogFile, res.text, 'utf-8');
3554
3090
  console.log(theme.system(t('backlog.adding', { v: res.id })));
3555
3091
  continue;
@@ -3566,7 +3102,7 @@ async function main() {
3566
3102
  if (editor)
3567
3103
  editor.busy = true;
3568
3104
  try {
3569
- await runTask(browser, reviewTools, reviewTask, currentWorkdir, {
3105
+ await runTask(runTaskDeps(), browser, reviewTools, reviewTask, currentWorkdir, {
3570
3106
  transcript,
3571
3107
  freshChat: false,
3572
3108
  sendSystemPrompt: false,
@@ -3821,7 +3357,7 @@ async function main() {
3821
3357
  if (editor)
3822
3358
  editor.busy = true;
3823
3359
  try {
3824
- await runTask(browser, tools, finalTaskText, currentWorkdir, {
3360
+ await runTask(runTaskDeps(), browser, tools, finalTaskText, currentWorkdir, {
3825
3361
  transcript,
3826
3362
  freshChat: freshChatNext,
3827
3363
  sendSystemPrompt: sendSystemPromptNext,