zames_pro 2.32.0 → 2.35.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/agent-loop.js +4 -1
- package/dist/browser.js +32 -1
- package/dist/commands.js +114 -4
- package/dist/i18n.js +96 -5
- package/dist/index.js +348 -30
- package/dist/input.js +98 -41
- package/dist/scheduler.js +185 -0
- package/dist/theme.js +1 -0
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -8,7 +8,7 @@ import { DeepSeekBrowser } from './browser.js';
|
|
|
8
8
|
import { createTools } from './tools.js';
|
|
9
9
|
import { runAgentLoop } from './agent-loop.js';
|
|
10
10
|
import { createSpinner } from './spinner.js';
|
|
11
|
-
import { LineEditor, expandPastes, pasteReplacement, } from './input.js';
|
|
11
|
+
import { LineEditor, expandPastes, pasteReplacement, formatCompactTokens, } from './input.js';
|
|
12
12
|
import { parseImagePaste, extForMime, saveToTemp, guessMime, isImageName, formatSize, looksLikeFilePath, readClipboardImageDetailed, sniffMime, readWindowsClipboardFiles, hasClipboardTool, } from './attachments.js';
|
|
13
13
|
import { loadConfig, DEFAULTS, CONFIG_PATHS, ZAMES_HOME, CONFIG_SCHEMA, getByPath, validateConfigValue, writeConfigValue, resetConfigValue, } from './config.js';
|
|
14
14
|
import { runConfigMenu } from './config-menu.js';
|
|
@@ -16,8 +16,9 @@ import { translate, normalizeLocale, localeDisplayName, isLocale, } from './i18n
|
|
|
16
16
|
import { Transcript } from './transcript.js';
|
|
17
17
|
import { UndoStore } from './undo.js';
|
|
18
18
|
import { selfReview, selfDiff, selfApply, selfList } from './self-review.js';
|
|
19
|
-
import { formatDiff, diffGitArgs, parseTranscript, summarizeTranscript, renderCost, formatExport, defaultExportPath, renderDoctor, renderPermissions, resolveExtraDir, buildReviewPrompt, trimRestoredMessages, RESTORED_HISTORY_LIMIT, mergeMessages, isSlashCommand, ctrlCEscalation, } from './commands.js';
|
|
19
|
+
import { formatDiff, diffGitArgs, parseTranscript, summarizeTranscript, renderCost, formatExport, defaultExportPath, renderDoctor, renderPermissions, resolveExtraDir, buildReviewPrompt, formatDuration, trimRestoredMessages, RESTORED_HISTORY_LIMIT, mergeMessages, isSlashCommand, parseQueueCommand, parseLiveToggle, parseGoalCommand, isLiveConfigCommand, withGoal, formatQueueList, ctrlCEscalation, } from './commands.js';
|
|
20
20
|
import { performCompact } from './compact.js';
|
|
21
|
+
import { Scheduler, parseInterval, formatInterval, parseCron, } from './scheduler.js';
|
|
21
22
|
import { renderMarkdown } from './markdown.js';
|
|
22
23
|
import { closeWeb } from './web.js';
|
|
23
24
|
import { saveSession, loadLastSession, listSessions, sessionsDir, } from './sessions.js';
|
|
@@ -108,6 +109,36 @@ function loadLastChat(workdir = '') {
|
|
|
108
109
|
const s = loadLastSession(workdir);
|
|
109
110
|
return s ? s.id : null;
|
|
110
111
|
}
|
|
112
|
+
// Persistent per-project session goal (set by /goal). Kept OUT of the session
|
|
113
|
+
// JSON (a session == a DeepSeek chat) because the goal is a property of the
|
|
114
|
+
// PROJECT, not of one chat: it must survive /new and /compact. Best-effort —
|
|
115
|
+
// a missing/unreadable file simply means "no goal".
|
|
116
|
+
function goalFilePath(workdir) {
|
|
117
|
+
return path.join(workdir, '.zames-goal');
|
|
118
|
+
}
|
|
119
|
+
async function loadGoal(workdir) {
|
|
120
|
+
try {
|
|
121
|
+
const txt = await fs.readFile(goalFilePath(workdir), 'utf-8');
|
|
122
|
+
const trimmed = txt.trim();
|
|
123
|
+
return trimmed || null;
|
|
124
|
+
}
|
|
125
|
+
catch {
|
|
126
|
+
return null;
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
async function saveGoal(workdir, goal) {
|
|
130
|
+
try {
|
|
131
|
+
if (goal && goal.trim()) {
|
|
132
|
+
await fs.writeFile(goalFilePath(workdir), goal.trim() + '\n', 'utf-8');
|
|
133
|
+
}
|
|
134
|
+
else {
|
|
135
|
+
await fs.rm(goalFilePath(workdir), { force: true });
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
catch {
|
|
139
|
+
// best-effort: a failed write must not break the command
|
|
140
|
+
}
|
|
141
|
+
}
|
|
111
142
|
// ---------- hot reload ----------
|
|
112
143
|
// We keep references to the logic modules in the mod object. The /reload
|
|
113
144
|
// command re-reads them via a dynamic import with a timestamp query — Node
|
|
@@ -287,6 +318,12 @@ ${theme.bold(t('help.commands'))}
|
|
|
287
318
|
${t('help.cmd.permissions')}
|
|
288
319
|
${t('help.cmd.add_dir')}
|
|
289
320
|
${t('help.cmd.review')}
|
|
321
|
+
${t('help.cmd.goal')}
|
|
322
|
+
${t('help.cmd.loop')}
|
|
323
|
+
${t('help.cmd.cron')}
|
|
324
|
+
${t('help.cmd.jobs')}
|
|
325
|
+
${t('help.cmd.thinking')}
|
|
326
|
+
${t('help.cmd.web')}
|
|
290
327
|
${t('help.cmd.compact')}
|
|
291
328
|
${t('help.cmd.queue')}
|
|
292
329
|
${t('help.cmd.config')}
|
|
@@ -344,6 +381,12 @@ const SLASH_COMMANDS = [
|
|
|
344
381
|
{ name: '/permissions', key: 'help.cmd.permissions' },
|
|
345
382
|
{ name: '/add-dir', key: 'help.cmd.add_dir' },
|
|
346
383
|
{ name: '/review', key: 'help.cmd.review' },
|
|
384
|
+
{ name: '/goal', key: 'help.cmd.goal' },
|
|
385
|
+
{ name: '/loop', key: 'help.cmd.loop' },
|
|
386
|
+
{ name: '/cron', key: 'help.cmd.cron' },
|
|
387
|
+
{ name: '/jobs', key: 'help.cmd.jobs' },
|
|
388
|
+
{ name: '/thinking', key: 'help.cmd.thinking' },
|
|
389
|
+
{ name: '/web', key: 'help.cmd.web' },
|
|
347
390
|
{ name: '/compact', key: 'help.cmd.compact' },
|
|
348
391
|
{ name: '/queue', key: 'help.cmd.queue' },
|
|
349
392
|
{ name: '/config', key: 'help.cmd.config' },
|
|
@@ -935,7 +978,7 @@ async function printRestoredHistory(browser, ui, chatId = null) {
|
|
|
935
978
|
}
|
|
936
979
|
// ---------- task runner ----------
|
|
937
980
|
async function runTask(browser, tools, taskText, workdir, opts, attachments = []) {
|
|
938
|
-
const { transcript, freshChat, sendSystemPrompt, queue = [], ui: editor, onChatReady, onAutoCompact = null, autoCompactPct = 95, contextLimit = 1_000_000, getTokenUsage = null, } = opts;
|
|
981
|
+
const { transcript, freshChat, sendSystemPrompt, queue = [], ui: editor, onChatReady, onAutoCompact = null, autoCompactPct = 95, contextLimit = 1_000_000, getTokenUsage = null, goal = null, } = opts;
|
|
939
982
|
// In TTY mode the UI is a LineEditor: it owns the input (queue, Esc,
|
|
940
983
|
// Ctrl+C) and draws the status ABOVE the permanent input line. In non-TTY
|
|
941
984
|
// mode (pipes) — a regular spinner + watchInput.
|
|
@@ -984,15 +1027,20 @@ async function runTask(browser, tools, taskText, workdir, opts, attachments = []
|
|
|
984
1027
|
// the whole pre-send phase (chat creation, throttle wait) with no
|
|
985
1028
|
// generation in flight.
|
|
986
1029
|
while (true) {
|
|
987
|
-
// T7: per-task summary (duration + number of tool calls).
|
|
988
|
-
// via the UI callback and reports ONE
|
|
989
|
-
// operator sees how long it took and how much work happened.
|
|
1030
|
+
// T7: per-task summary (duration + number of tool calls + tokens).
|
|
1031
|
+
// Counts tools via the UI callback and reports ONE line after the task,
|
|
1032
|
+
// so the operator sees how long it took and how much work happened.
|
|
990
1033
|
const taskStart = Date.now();
|
|
1034
|
+
// Token delta for THIS task: accumulated_token_usage is cumulative for
|
|
1035
|
+
// the whole chat, so the per-task spend is after - before. Null when the
|
|
1036
|
+
// counter is unknown (fresh chat / no answer yet); the token part is then
|
|
1037
|
+
// omitted from the summary.
|
|
1038
|
+
const tokensBefore = getTokenUsage ? getTokenUsage() : null;
|
|
991
1039
|
let taskTools = 0;
|
|
992
1040
|
const outcome = await mod.runAgentLoop({
|
|
993
1041
|
browser,
|
|
994
1042
|
tools,
|
|
995
|
-
task: next.text,
|
|
1043
|
+
task: withGoal(goal, next.text),
|
|
996
1044
|
workdir,
|
|
997
1045
|
maxIterations: maxIter,
|
|
998
1046
|
freshChat: next.freshChat,
|
|
@@ -1001,6 +1049,10 @@ async function runTask(browser, tools, taskText, workdir, opts, attachments = []
|
|
|
1001
1049
|
transcript,
|
|
1002
1050
|
onThinking: () => ui.thinking(),
|
|
1003
1051
|
onSendPause: (seconds) => ui.sendPause(seconds),
|
|
1052
|
+
onSendState: (state) => {
|
|
1053
|
+
if (editor)
|
|
1054
|
+
editor.setSendState(state);
|
|
1055
|
+
},
|
|
1004
1056
|
onNotice: (msg) => ui.warning(msg),
|
|
1005
1057
|
onToolCall: (name, toolArgs) => {
|
|
1006
1058
|
taskTools++;
|
|
@@ -1034,12 +1086,30 @@ async function runTask(browser, tools, taskText, workdir, opts, attachments = []
|
|
|
1034
1086
|
ui.warning(outcome);
|
|
1035
1087
|
transcript?.log('agent_no_answer', { outcome });
|
|
1036
1088
|
}
|
|
1037
|
-
// T7: one
|
|
1038
|
-
// even on an abort, so the operator sees what happened.
|
|
1039
|
-
|
|
1040
|
-
|
|
1041
|
-
|
|
1042
|
-
|
|
1089
|
+
// T7: one summary line per task (duration + tool calls + tokens spent).
|
|
1090
|
+
// Printed even on an abort, so the operator sees what happened. Duration
|
|
1091
|
+
// is human-readable ("45s", "1m 20s", "1h 12m 45s") with LOCALIZED unit
|
|
1092
|
+
// labels, instead of a raw seconds count like "3129.2s". The token part
|
|
1093
|
+
// is the DELTA of the cumulative counter for THIS task; it is omitted
|
|
1094
|
+
// when the counter is unknown.
|
|
1095
|
+
const tokensAfter = getTokenUsage ? getTokenUsage() : null;
|
|
1096
|
+
const tokenDelta = typeof tokensBefore === 'number' && typeof tokensAfter === 'number'
|
|
1097
|
+
? Math.max(0, tokensAfter - tokensBefore)
|
|
1098
|
+
: null;
|
|
1099
|
+
const summaryParts = [
|
|
1100
|
+
t('task_sum.dur', {
|
|
1101
|
+
dur: formatDuration(Date.now() - taskStart, {
|
|
1102
|
+
h: t('dur.h'),
|
|
1103
|
+
m: t('dur.m'),
|
|
1104
|
+
s: t('dur.s'),
|
|
1105
|
+
}),
|
|
1106
|
+
}),
|
|
1107
|
+
t('task_sum.tools', { n: String(taskTools) }),
|
|
1108
|
+
];
|
|
1109
|
+
if (tokenDelta !== null) {
|
|
1110
|
+
summaryParts.push(t('task_sum.tokens', { n: formatCompactTokens(tokenDelta) }));
|
|
1111
|
+
}
|
|
1112
|
+
const summary = theme.taskSummary('· ' + summaryParts.join(' · '));
|
|
1043
1113
|
if (editor)
|
|
1044
1114
|
editor.printAbove(summary);
|
|
1045
1115
|
else
|
|
@@ -1101,6 +1171,7 @@ async function runTask(browser, tools, taskText, workdir, opts, attachments = []
|
|
|
1101
1171
|
// start a stale spinner.
|
|
1102
1172
|
browser.onSendStart = null;
|
|
1103
1173
|
browser.onSendPause = null;
|
|
1174
|
+
browser.onSendState = null;
|
|
1104
1175
|
browser.onNotice = null;
|
|
1105
1176
|
}
|
|
1106
1177
|
}
|
|
@@ -1253,11 +1324,53 @@ async function main() {
|
|
|
1253
1324
|
let lastChats = [];
|
|
1254
1325
|
let currentChatId = null;
|
|
1255
1326
|
let running = true;
|
|
1327
|
+
// Long-lived session goal (set via /goal). Prepended to every task message so
|
|
1328
|
+
// the model keeps the big picture across many turns. Persisted per working
|
|
1329
|
+
// directory in <project>/.zames-goal (best-effort); restored right away.
|
|
1330
|
+
let sessionGoal = await loadGoal(currentWorkdir);
|
|
1331
|
+
if (sessionGoal) {
|
|
1332
|
+
console.log(theme.system(t('goal.loaded', { goal: sessionGoal })));
|
|
1333
|
+
}
|
|
1334
|
+
// Scheduled tasks (/loop, /cron). A pure Scheduler holds the jobs; a
|
|
1335
|
+
// 1-second ticker enqueues a due job's task into the SAME message queue the
|
|
1336
|
+
// operator uses, so it runs when the agent is free (never mid-generation,
|
|
1337
|
+
// never bypassing the send throttle). Started lazily on the first job.
|
|
1338
|
+
const scheduler = new Scheduler();
|
|
1339
|
+
let scheduleTimer = null;
|
|
1340
|
+
const startScheduleTicker = () => {
|
|
1341
|
+
if (scheduleTimer)
|
|
1342
|
+
return;
|
|
1343
|
+
scheduleTimer = setInterval(() => {
|
|
1344
|
+
const due = scheduler.due(Date.now());
|
|
1345
|
+
for (const job of due) {
|
|
1346
|
+
pendingQueue.push({ text: job.task });
|
|
1347
|
+
transcript.log('scheduled_fire', { id: job.id, task: job.task });
|
|
1348
|
+
if (editor) {
|
|
1349
|
+
editor.printAbove(theme.system(t('sched.fired', { id: String(job.id), task: job.task })));
|
|
1350
|
+
}
|
|
1351
|
+
else {
|
|
1352
|
+
console.log(theme.system(t('sched.fired', { id: String(job.id), task: job.task })));
|
|
1353
|
+
}
|
|
1354
|
+
// Wake a waiting main loop so a queued job runs even when idle.
|
|
1355
|
+
if (waiter) {
|
|
1356
|
+
const r = waiter;
|
|
1357
|
+
waiter = null;
|
|
1358
|
+
r(pendingQueue.shift() ?? null);
|
|
1359
|
+
}
|
|
1360
|
+
}
|
|
1361
|
+
}, 1000);
|
|
1362
|
+
if (scheduleTimer.unref)
|
|
1363
|
+
scheduleTimer.unref();
|
|
1364
|
+
};
|
|
1256
1365
|
// Messages the user typed while the agent worked. runTask takes them one
|
|
1257
1366
|
// by one after the current task finishes.
|
|
1258
1367
|
const pendingQueue = [];
|
|
1259
1368
|
// Show the "no clipboard image" hint only once per session.
|
|
1260
1369
|
let clipboardWarned = false;
|
|
1370
|
+
// Show the "message is queued" hint once per session (the first time the
|
|
1371
|
+
// operator sends while a task runs), so the queue is understood without
|
|
1372
|
+
// spamming the hint on every queued message.
|
|
1373
|
+
let queueHintShown = false;
|
|
1261
1374
|
// Timestamp of the last Ctrl+C while the agent was busy. Two presses
|
|
1262
1375
|
// within 2s escalate from "abort the running tool" to "stop the whole run".
|
|
1263
1376
|
let lastCtrlCAt = 0;
|
|
@@ -1353,6 +1466,9 @@ async function main() {
|
|
|
1353
1466
|
// The editor pulls the number on every render, so it follows the live
|
|
1354
1467
|
// DeepSeek counter (accumulated_token_usage) without a polling timer.
|
|
1355
1468
|
ed.onContextQuery = () => browser.getLastTokenUsage();
|
|
1469
|
+
// Queue badge: show "⧗N" while messages are waiting to be sent after the
|
|
1470
|
+
// current task. Pulled on every render (like the context counter).
|
|
1471
|
+
ed.onQueueQuery = () => pendingQueue.length;
|
|
1356
1472
|
// Use the configured context window for the fill percentage/color.
|
|
1357
1473
|
ed.setContextLimit(config.ui.contextLimit);
|
|
1358
1474
|
// Toggle icons (🧠 deep thinking, 🌐 web search) before the context
|
|
@@ -1451,6 +1567,49 @@ async function main() {
|
|
|
1451
1567
|
return att;
|
|
1452
1568
|
};
|
|
1453
1569
|
ed.onSubmit = (text, items) => {
|
|
1570
|
+
// While the agent is busy the main command loop is blocked on
|
|
1571
|
+
// `await runTask()`, so ONLY commands that make sense without the main
|
|
1572
|
+
// loop are handled here (a queued slash-command would run too late).
|
|
1573
|
+
if (ed.busy) {
|
|
1574
|
+
const q = parseQueueCommand(text);
|
|
1575
|
+
if (q) {
|
|
1576
|
+
if (q.sub === 'clear') {
|
|
1577
|
+
const n = pendingQueue.length;
|
|
1578
|
+
pendingQueue.length = 0;
|
|
1579
|
+
ed.printAbove(theme.system(t('msg.queue_cleared') + ' (' + n + ')'));
|
|
1580
|
+
}
|
|
1581
|
+
else if (!pendingQueue.length) {
|
|
1582
|
+
ed.printAbove(theme.dim(t('msg.queue_empty')));
|
|
1583
|
+
}
|
|
1584
|
+
else {
|
|
1585
|
+
ed.printAbove(theme.system(t('msg.queue_title')));
|
|
1586
|
+
for (const line of formatQueueList(pendingQueue)) {
|
|
1587
|
+
ed.printAbove(theme.assistant(line));
|
|
1588
|
+
}
|
|
1589
|
+
ed.printAbove(theme.dim(t('msg.queue_cleared_hint')));
|
|
1590
|
+
}
|
|
1591
|
+
return;
|
|
1592
|
+
}
|
|
1593
|
+
// Toggle Deep thinking / Smart search LIVE. This only flips the
|
|
1594
|
+
// browser's DESIRED state and the status icons; it does not touch the
|
|
1595
|
+
// in-flight generation, so it is safe mid-run. The actual DeepSeek
|
|
1596
|
+
// toggles are applied on the next send by _applyToggles().
|
|
1597
|
+
const tg = parseLiveToggle(text);
|
|
1598
|
+
if (tg) {
|
|
1599
|
+
applyLiveToggle(tg);
|
|
1600
|
+
return;
|
|
1601
|
+
}
|
|
1602
|
+
// `/config <sub>` text commands (list/get/set/reset/lang) are safe
|
|
1603
|
+
// mid-run: they only touch config values, never the chat. The
|
|
1604
|
+
// interactive menu (`/config` bare or `menu`) is NOT intercepted —
|
|
1605
|
+
// it pauses the editor and would fight the running task, so it stays
|
|
1606
|
+
// queued. `setConfigRuntime` already applies hot values (locale,
|
|
1607
|
+
// toggles, context limit) to the live objects immediately.
|
|
1608
|
+
if (isLiveConfigCommand(text)) {
|
|
1609
|
+
void handleConfigCommand(text);
|
|
1610
|
+
return;
|
|
1611
|
+
}
|
|
1612
|
+
}
|
|
1454
1613
|
const attachments = (items || []).map((a) => ({
|
|
1455
1614
|
path: a.path,
|
|
1456
1615
|
name: a.name,
|
|
@@ -1458,6 +1617,14 @@ async function main() {
|
|
|
1458
1617
|
}));
|
|
1459
1618
|
const msg = { text, attachments };
|
|
1460
1619
|
pendingQueue.push(msg);
|
|
1620
|
+
// The operator typed this WHILE the agent was working, so the message is
|
|
1621
|
+
// queued and only sent after the current task. Explain that once per
|
|
1622
|
+
// session (via the editor's own status area) so it is clear nothing was
|
|
1623
|
+
// lost; later queued messages stay as the compact msg.queued banner only.
|
|
1624
|
+
if (ed.busy && !queueHintShown) {
|
|
1625
|
+
queueHintShown = true;
|
|
1626
|
+
ed.printAbove(theme.dim(t('msg.queued_hint')));
|
|
1627
|
+
}
|
|
1461
1628
|
if (waiter) {
|
|
1462
1629
|
const r = waiter;
|
|
1463
1630
|
waiter = null;
|
|
@@ -1577,6 +1744,40 @@ async function main() {
|
|
|
1577
1744
|
}
|
|
1578
1745
|
}
|
|
1579
1746
|
}
|
|
1747
|
+
// Apply `/thinking` / `/web` immediately: flip the browser's DESIRED state
|
|
1748
|
+
// and the status icons, and persist to config (so it survives a restart).
|
|
1749
|
+
// The DeepSeek toggles themselves are clicked on the next send by
|
|
1750
|
+
// _applyToggles(), so this is safe even while a generation is in flight.
|
|
1751
|
+
function applyLiveToggle(cmd) {
|
|
1752
|
+
const b = browser;
|
|
1753
|
+
const cur = cmd.kind === 'thinking' ? b.deepThinking : b.webSearch;
|
|
1754
|
+
const next = cmd.mode === 'toggle' ? !cur : cmd.mode === 'on';
|
|
1755
|
+
if (cmd.kind === 'thinking')
|
|
1756
|
+
b.deepThinking = next;
|
|
1757
|
+
else
|
|
1758
|
+
b.webSearch = next;
|
|
1759
|
+
b.setToggleState({
|
|
1760
|
+
deepThinking: b.deepThinking,
|
|
1761
|
+
webSearch: b.webSearch,
|
|
1762
|
+
});
|
|
1763
|
+
const cfgPath = cmd.kind === 'thinking' ? 'browser.deepThinking' : 'browser.webSearch';
|
|
1764
|
+
try {
|
|
1765
|
+
writeConfigValue('home', cfgPath, String(next));
|
|
1766
|
+
setConfigRuntime(cfgPath, next);
|
|
1767
|
+
}
|
|
1768
|
+
catch {
|
|
1769
|
+
// A failed persist must not break the toggle itself.
|
|
1770
|
+
}
|
|
1771
|
+
if (editor)
|
|
1772
|
+
editor.refreshStatus();
|
|
1773
|
+
const label = cmd.kind === 'thinking' ? t('toggle.thinking') : t('toggle.search');
|
|
1774
|
+
const stateText = next ? t('common.on') : t('common.off');
|
|
1775
|
+
const line = t('toggle.set', { name: label, state: stateText });
|
|
1776
|
+
if (editor)
|
|
1777
|
+
editor.printAbove(theme.system(line));
|
|
1778
|
+
else
|
|
1779
|
+
console.log(theme.system(line));
|
|
1780
|
+
}
|
|
1580
1781
|
function configSetRaw(field, raw) {
|
|
1581
1782
|
const value = validateConfigValue(field, raw);
|
|
1582
1783
|
writeConfigValue('home', field.path, raw);
|
|
@@ -2126,37 +2327,145 @@ async function main() {
|
|
|
2126
2327
|
console.log(theme.system(currentWorkdir));
|
|
2127
2328
|
continue;
|
|
2128
2329
|
}
|
|
2330
|
+
if (lower === '/goal' || lower.startsWith('/goal ')) {
|
|
2331
|
+
const g = parseGoalCommand(trimmed);
|
|
2332
|
+
if (g) {
|
|
2333
|
+
if (g.sub === 'clear') {
|
|
2334
|
+
sessionGoal = null;
|
|
2335
|
+
await saveGoal(currentWorkdir, null);
|
|
2336
|
+
console.log(theme.system(t('goal.cleared')));
|
|
2337
|
+
}
|
|
2338
|
+
else if (g.sub === 'show') {
|
|
2339
|
+
console.log(sessionGoal
|
|
2340
|
+
? theme.system(t('goal.current', { goal: sessionGoal }))
|
|
2341
|
+
: theme.dim(t('goal.none')));
|
|
2342
|
+
}
|
|
2343
|
+
else {
|
|
2344
|
+
sessionGoal = g.goal;
|
|
2345
|
+
await saveGoal(currentWorkdir, g.goal);
|
|
2346
|
+
console.log(theme.system(t('goal.set', { goal: g.goal })));
|
|
2347
|
+
}
|
|
2348
|
+
continue;
|
|
2349
|
+
}
|
|
2350
|
+
}
|
|
2351
|
+
if (lower === '/loop' || lower.startsWith('/loop ')) {
|
|
2352
|
+
const rest = trimmed.slice('/loop'.length).trim();
|
|
2353
|
+
const sp = rest.indexOf(' ');
|
|
2354
|
+
const intervalStr = sp === -1 ? rest : rest.slice(0, sp);
|
|
2355
|
+
const taskStr = sp === -1 ? '' : rest.slice(sp + 1).trim();
|
|
2356
|
+
const ms = parseInterval(intervalStr);
|
|
2357
|
+
if (!ms || !taskStr) {
|
|
2358
|
+
console.error(theme.error(t('sched.loop_usage')));
|
|
2359
|
+
continue;
|
|
2360
|
+
}
|
|
2361
|
+
const job = scheduler.addLoop(ms, taskStr, Date.now());
|
|
2362
|
+
startScheduleTicker();
|
|
2363
|
+
console.log(theme.system(t('sched.added', {
|
|
2364
|
+
id: String(job.id),
|
|
2365
|
+
when: formatInterval(ms),
|
|
2366
|
+
task: taskStr,
|
|
2367
|
+
})));
|
|
2368
|
+
continue;
|
|
2369
|
+
}
|
|
2370
|
+
if (lower === '/cron' || lower.startsWith('/cron ')) {
|
|
2371
|
+
const rest = trimmed.slice('/cron'.length).trim();
|
|
2372
|
+
// A cron expression is exactly 5 whitespace-separated fields; the task
|
|
2373
|
+
// is everything after them.
|
|
2374
|
+
const parts = rest.split(/\s+/);
|
|
2375
|
+
if (parts.length < 6) {
|
|
2376
|
+
console.error(theme.error(t('sched.cron_usage')));
|
|
2377
|
+
continue;
|
|
2378
|
+
}
|
|
2379
|
+
const cronExpr = parts.slice(0, 5).join(' ');
|
|
2380
|
+
const taskStr = parts.slice(5).join(' ').trim();
|
|
2381
|
+
if (!parseCron(cronExpr) || !taskStr) {
|
|
2382
|
+
console.error(theme.error(t('sched.cron_usage')));
|
|
2383
|
+
continue;
|
|
2384
|
+
}
|
|
2385
|
+
const job = scheduler.addCron(cronExpr, taskStr, Date.now());
|
|
2386
|
+
if (!job) {
|
|
2387
|
+
console.error(theme.error(t('sched.cron_usage')));
|
|
2388
|
+
continue;
|
|
2389
|
+
}
|
|
2390
|
+
startScheduleTicker();
|
|
2391
|
+
const nextIn = formatInterval(Math.max(0, job.nextAt - Date.now()));
|
|
2392
|
+
console.log(theme.system(t('sched.added', {
|
|
2393
|
+
id: String(job.id),
|
|
2394
|
+
when: cronExpr + ' (in ~' + nextIn + ')',
|
|
2395
|
+
task: taskStr,
|
|
2396
|
+
})));
|
|
2397
|
+
continue;
|
|
2398
|
+
}
|
|
2399
|
+
if (lower === '/jobs' || lower.startsWith('/jobs ')) {
|
|
2400
|
+
const sub = trimmed.slice('/jobs'.length).trim().toLowerCase();
|
|
2401
|
+
if (sub === 'clear') {
|
|
2402
|
+
const n = scheduler.clear();
|
|
2403
|
+
if (scheduleTimer) {
|
|
2404
|
+
clearInterval(scheduleTimer);
|
|
2405
|
+
scheduleTimer = null;
|
|
2406
|
+
}
|
|
2407
|
+
console.log(theme.system(t('sched.cleared', { n: String(n) })));
|
|
2408
|
+
continue;
|
|
2409
|
+
}
|
|
2410
|
+
if (sub.startsWith('rm ') || sub.startsWith('remove ')) {
|
|
2411
|
+
const id = Number(sub.replace(/^(rm|remove)\s+/, ''));
|
|
2412
|
+
if (!Number.isInteger(id) || !scheduler.remove(id)) {
|
|
2413
|
+
console.error(theme.error(t('sched.rm_usage')));
|
|
2414
|
+
continue;
|
|
2415
|
+
}
|
|
2416
|
+
if (!scheduler.count() && scheduleTimer) {
|
|
2417
|
+
clearInterval(scheduleTimer);
|
|
2418
|
+
scheduleTimer = null;
|
|
2419
|
+
}
|
|
2420
|
+
console.log(theme.system(t('sched.removed', { id: String(id) })));
|
|
2421
|
+
continue;
|
|
2422
|
+
}
|
|
2423
|
+
const jobs = scheduler.list();
|
|
2424
|
+
if (!jobs.length) {
|
|
2425
|
+
console.log(theme.dim(t('sched.none')));
|
|
2426
|
+
continue;
|
|
2427
|
+
}
|
|
2428
|
+
console.log(theme.system(t('sched.title')));
|
|
2429
|
+
for (const j of jobs) {
|
|
2430
|
+
const when = j.kind === 'loop'
|
|
2431
|
+
? 'every ' + formatInterval(j.intervalMs || 0)
|
|
2432
|
+
: j.cron || '';
|
|
2433
|
+
const nextIn = formatInterval(Math.max(0, j.nextAt - Date.now()));
|
|
2434
|
+
console.log(theme.assistant(' #' + j.id + ' [' + when + ', next ~' + nextIn + '] ' + j.task));
|
|
2435
|
+
}
|
|
2436
|
+
console.log(theme.dim(t('sched.remove_hint')));
|
|
2437
|
+
continue;
|
|
2438
|
+
}
|
|
2129
2439
|
if (lower === '/queue' || lower.startsWith('/queue ')) {
|
|
2130
|
-
const
|
|
2131
|
-
if (
|
|
2440
|
+
const q = parseQueueCommand(trimmed);
|
|
2441
|
+
if (!q) {
|
|
2442
|
+
console.error(theme.error(t('msg.queue_usage')));
|
|
2443
|
+
continue;
|
|
2444
|
+
}
|
|
2445
|
+
if (q.sub === 'clear') {
|
|
2132
2446
|
const n = pendingQueue.length;
|
|
2133
2447
|
pendingQueue.length = 0;
|
|
2134
2448
|
console.log(theme.system(t('msg.queue_cleared') + ' (' + n + ')'));
|
|
2135
2449
|
continue;
|
|
2136
2450
|
}
|
|
2137
|
-
if (sub && sub !== 'list' && sub !== 'ls') {
|
|
2138
|
-
console.error(theme.error(t('msg.queue_usage')));
|
|
2139
|
-
continue;
|
|
2140
|
-
}
|
|
2141
2451
|
if (!pendingQueue.length) {
|
|
2142
2452
|
console.log(theme.dim(t('msg.queue_empty')));
|
|
2143
2453
|
continue;
|
|
2144
2454
|
}
|
|
2145
2455
|
console.log(theme.system(t('msg.queue_title')));
|
|
2146
|
-
|
|
2147
|
-
|
|
2148
|
-
|
|
2149
|
-
.trim();
|
|
2150
|
-
const shown = one.length > 80 ? one.slice(0, 80) + ' …' : one;
|
|
2151
|
-
const att = m.attachments?.length
|
|
2152
|
-
? ' [+' + m.attachments.length + ']'
|
|
2153
|
-
: '';
|
|
2154
|
-
console.log(theme.system(' ') +
|
|
2155
|
-
theme.assistant(t('msg.queue_item', { n: i + 1, text: shown + att })));
|
|
2156
|
-
});
|
|
2456
|
+
for (const line of formatQueueList(pendingQueue)) {
|
|
2457
|
+
console.log(theme.assistant(line));
|
|
2458
|
+
}
|
|
2157
2459
|
console.log(theme.dim(t('msg.queue_cleared_hint')));
|
|
2158
2460
|
continue;
|
|
2159
2461
|
}
|
|
2462
|
+
if (lower.startsWith('/thinking') || lower.startsWith('/web')) {
|
|
2463
|
+
const tg = parseLiveToggle(trimmed);
|
|
2464
|
+
if (tg) {
|
|
2465
|
+
applyLiveToggle(tg);
|
|
2466
|
+
continue;
|
|
2467
|
+
}
|
|
2468
|
+
}
|
|
2160
2469
|
if (lower === '/mcp') {
|
|
2161
2470
|
if (!mcpPool) {
|
|
2162
2471
|
console.log(theme.dim(t('mcp.none')));
|
|
@@ -2347,6 +2656,11 @@ async function main() {
|
|
|
2347
2656
|
console.log(theme.system(t('status.max_iter', { v: maxIter })));
|
|
2348
2657
|
console.log(theme.system(t('status.headless', { v: headless ? yes : no })));
|
|
2349
2658
|
console.log(theme.system(t('status.debug', { v: debug ? yes : no })));
|
|
2659
|
+
const statusToggles = browser.getToggleStatesSync();
|
|
2660
|
+
console.log(theme.system(t('status.toggles', {
|
|
2661
|
+
think: statusToggles.deepThinking ? yes : no,
|
|
2662
|
+
search: statusToggles.webSearch ? yes : no,
|
|
2663
|
+
})));
|
|
2350
2664
|
console.log(theme.system(t('status.undo', {
|
|
2351
2665
|
v: config.undo.enabled ? t('common.on') : t('common.off'),
|
|
2352
2666
|
})));
|
|
@@ -2466,6 +2780,7 @@ async function main() {
|
|
|
2466
2780
|
? (await runGit('git remote', currentWorkdir, 5000)).includes('origin')
|
|
2467
2781
|
: undefined,
|
|
2468
2782
|
minSendIntervalMs: config.browser.minSendIntervalMs,
|
|
2783
|
+
sshRemote: !!(process.env.SSH_CONNECTION || process.env.SSH_TTY),
|
|
2469
2784
|
})));
|
|
2470
2785
|
continue;
|
|
2471
2786
|
}
|
|
@@ -2695,6 +3010,7 @@ async function main() {
|
|
|
2695
3010
|
autoCompactPct: config.browser.autoCompactPct,
|
|
2696
3011
|
contextLimit: config.ui.contextLimit,
|
|
2697
3012
|
getTokenUsage: () => browser.getLastTokenUsage(),
|
|
3013
|
+
goal: sessionGoal,
|
|
2698
3014
|
}, inputAttachments);
|
|
2699
3015
|
}
|
|
2700
3016
|
finally {
|
|
@@ -2710,6 +3026,8 @@ async function main() {
|
|
|
2710
3026
|
}
|
|
2711
3027
|
if (editor)
|
|
2712
3028
|
editor.dispose();
|
|
3029
|
+
if (scheduleTimer)
|
|
3030
|
+
clearInterval(scheduleTimer);
|
|
2713
3031
|
await browser.close().catch(() => { });
|
|
2714
3032
|
await mod.closeWeb().catch(() => { });
|
|
2715
3033
|
if (mcpPool)
|