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/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). Counts tools
988
- // via the UI callback and reports ONE dim line after the task, so the
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 dim summary line per task (duration + tool calls). Printed
1038
- // even on an abort, so the operator sees what happened.
1039
- const summary = theme.dim(t('msg.task_summary', {
1040
- sec: ((Date.now() - taskStart) / 1000).toFixed(1),
1041
- tools: String(taskTools),
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 sub = trimmed.slice('/queue'.length).trim().toLowerCase();
2131
- if (sub === 'clear' || sub === 'c' || sub === 'clean') {
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
- pendingQueue.forEach((m, i) => {
2147
- const one = String(m.text || '')
2148
- .replace(/\s+/g, ' ')
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)