zames_pro 2.64.0 → 2.65.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/CHANGELOG.md +46 -1
- package/README.md +16 -11
- package/dist/backlog.js +20 -0
- package/dist/browser.js +11 -0
- package/dist/commands.js +81 -0
- package/dist/config-menu.js +71 -33
- package/dist/i18n/common.js +362 -0
- package/dist/i18n/config.js +199 -0
- package/dist/i18n/deepseek.js +194 -0
- package/dist/i18n/doctor.js +127 -0
- package/dist/i18n/help.js +552 -0
- package/dist/i18n/mcp.js +41 -0
- package/dist/i18n/prompt.js +20 -0
- package/dist/i18n/self-review.js +198 -0
- package/dist/i18n/spinner.js +36 -0
- package/dist/i18n.js +21 -1724
- package/dist/index.js +39 -503
- package/dist/input/layout.js +292 -0
- package/dist/input.js +37 -290
- package/dist/run-task.js +451 -0
- package/package.json +2 -2
- package/scripts/render-demo.mjs +153 -0
- package/logo.jpg +0 -0
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
|
|
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,
|
|
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
|
-
|
|
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:
|
|
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
|
-
//
|
|
1088
|
-
//
|
|
1089
|
-
//
|
|
1090
|
-
//
|
|
1091
|
-
|
|
1092
|
-
|
|
1093
|
-
|
|
1094
|
-
|
|
1095
|
-
|
|
1096
|
-
|
|
1097
|
-
|
|
1098
|
-
|
|
1099
|
-
|
|
1100
|
-
|
|
1101
|
-
|
|
1102
|
-
|
|
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.
|
|
3535
|
-
|
|
3536
|
-
|
|
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,
|