zames_pro 2.89.0 → 2.90.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.
Files changed (49) hide show
  1. package/CHANGELOG.md +53 -0
  2. package/README.md +32 -585
  3. package/dist/agent-loop.js +47 -4
  4. package/dist/ask-retry.js +49 -0
  5. package/dist/ask-state.js +80 -0
  6. package/dist/browser-base.js +69 -0
  7. package/dist/browser.js +170 -41
  8. package/dist/command-registry.js +58 -0
  9. package/dist/commands.js +25 -2
  10. package/dist/completion-check.js +77 -0
  11. package/dist/config-schema.js +8 -0
  12. package/dist/config.js +14 -0
  13. package/dist/dev-commands.js +27 -0
  14. package/dist/dev-watch.js +96 -0
  15. package/dist/first-run.js +80 -0
  16. package/dist/glm-browser.js +133 -32
  17. package/dist/i18n/common.js +6 -3
  18. package/dist/i18n/config.js +16 -0
  19. package/dist/i18n/wizard.js +25 -0
  20. package/dist/i18n.js +6 -1
  21. package/dist/index.js +1289 -919
  22. package/dist/info-commands.js +61 -0
  23. package/dist/input/layout.js +38 -1
  24. package/dist/input.js +2 -2
  25. package/dist/password-store.js +48 -0
  26. package/dist/prompt-budget.js +23 -0
  27. package/dist/provider-errors.js +53 -0
  28. package/dist/provider-fallback.js +46 -0
  29. package/dist/qwen-browser.js +155 -39
  30. package/dist/run-task.js +2 -1
  31. package/dist/scheduler-commands.js +41 -0
  32. package/dist/send-slot.js +36 -0
  33. package/dist/session-commands.js +42 -0
  34. package/dist/spinner.js +3 -3
  35. package/dist/swallow.js +45 -0
  36. package/dist/system-prompt.js +2 -2
  37. package/dist/tree-commands.js +36 -0
  38. package/dist/tui/FullScreenApp.js +13 -4
  39. package/dist/tui/InputLine.js +20 -3
  40. package/dist/tui/LogView.js +11 -5
  41. package/dist/tui/StatusBar.js +4 -1
  42. package/dist/tui/TuiApp.js +11 -2
  43. package/dist/tui/TuiEditor.js +21 -4
  44. package/dist/tui/layout.js +14 -1
  45. package/dist/tui/store.js +2 -1
  46. package/dist/tui/ui-colors.js +2 -0
  47. package/dist/width.js +10 -5
  48. package/llms-full.txt +16 -292
  49. package/package.json +2 -2
@@ -15,6 +15,7 @@
15
15
  // answer -> SSE choices[].delta.content where phase != thinking_summary
16
16
  // The site sits behind Alibaba's WAF, which is exactly why we drive a REAL
17
17
  // browser: the page solves its own JS challenge and carries the tokens.
18
+ import { noteSwallowed } from './swallow.js';
18
19
  import fs from 'fs/promises';
19
20
  import path from 'path';
20
21
  import os from 'os';
@@ -57,6 +58,8 @@ function capNetBody(body) {
57
58
  }
58
59
  export class QwenBrowser {
59
60
  providerName = 'qwen';
61
+ /** The chat a send must join (set by the agent loop; see types.ts). */
62
+ resumeChatId = null;
60
63
  headless;
61
64
  debug;
62
65
  channel;
@@ -136,6 +139,8 @@ export class QwenBrowser {
136
139
  // askRetries.
137
140
  maxContextLimitRetries;
138
141
  onContextLimit;
142
+ /** N8: switch to an authorized fallback provider (see agent-loop). */
143
+ onProviderExhausted = null;
139
144
  constructor({ headless = false, debug = false, channel = null, answerTimeoutMs = 180000, askRetries = 3, stabilityChecks = 2, stabilityDelayMs = 400, minSendIntervalMs = 15000, rateLimitWaitMs = 300000, maxRateLimitRetries = 6, maxServerBusyRetries = 5, serverBusyWaitMs = 3000, maxIncompleteRetries = 4, incompleteWaitMs = 2000, maxContextLimitRetries = 1, onContextLimit, startWaitMs = 30000, sendButtonWaitMs = 8000, model = '', deepThinking = false, webSearch = true, auth, locale, onAuthSave, onLoginPrompt, loginTimeoutMs = 600000, userAgent, } = {}) {
140
145
  this.headless = headless;
141
146
  this.debug = debug;
@@ -212,7 +217,9 @@ export class QwenBrowser {
212
217
  this.onNotice(text);
213
218
  return;
214
219
  }
215
- catch { }
220
+ catch (e) {
221
+ noteSwallowed('_notice', e, this.debug);
222
+ }
216
223
  }
217
224
  console.error(text);
218
225
  }
@@ -221,14 +228,18 @@ export class QwenBrowser {
221
228
  try {
222
229
  this.onSendState(state);
223
230
  }
224
- catch { }
231
+ catch (e) {
232
+ noteSwallowed('_emitSendState', e, this.debug);
233
+ }
225
234
  }
226
235
  }
227
236
  async _setBusyTitle(busy) {
228
237
  try {
229
238
  await this.page.evaluate(setBusyTitleInDom, busy);
230
239
  }
231
- catch { }
240
+ catch (e) {
241
+ noteSwallowed('_setBusyTitle', e, this.debug);
242
+ }
232
243
  }
233
244
  sendIntervalMs() {
234
245
  return this.minSendIntervalMs;
@@ -307,6 +318,16 @@ export class QwenBrowser {
307
318
  // usually already usable, and every later step (_awaitInput, waitForLogin)
308
319
  // polls. Bailing out here turned a transient slowness into a hard startup
309
320
  // failure.
321
+ // A relaunch lands on the BASE url: re-open the dialogue the operator was
322
+ // in, or a pending resume id, instead of silently starting a new chat.
323
+ const back = this.resumeChatId || (await this.getCurrentChatId().catch(() => null));
324
+ if (back) {
325
+ const ok = await this.openChat(back).catch(() => false);
326
+ if (ok) {
327
+ await this._dismissOverlays();
328
+ return;
329
+ }
330
+ }
310
331
  await this.page
311
332
  .goto(CHAT_URL, { waitUntil: 'domcontentloaded' })
312
333
  .catch(() => { });
@@ -317,7 +338,9 @@ export class QwenBrowser {
317
338
  if (this.context)
318
339
  await this.context.close();
319
340
  }
320
- catch { }
341
+ catch (e) {
342
+ noteSwallowed('restart', e, this.debug);
343
+ }
321
344
  await this._launchOnce();
322
345
  await this._fixHeadlessUserAgent();
323
346
  }
@@ -357,7 +380,9 @@ export class QwenBrowser {
357
380
  await this.page.waitForTimeout(400);
358
381
  }
359
382
  }
360
- catch { }
383
+ catch (e) {
384
+ noteSwallowed('_dismissOverlays', e, this.debug);
385
+ }
361
386
  }
362
387
  async close() {
363
388
  // A session cookie (expires = -1) is dropped when the context closes, so a
@@ -383,12 +408,16 @@ export class QwenBrowser {
383
408
  }
384
409
  }
385
410
  }
386
- catch { }
411
+ catch (e) {
412
+ noteSwallowed('close', e, this.debug);
413
+ }
387
414
  try {
388
415
  if (this.context)
389
416
  await this.context.close();
390
417
  }
391
- catch { }
418
+ catch (e) {
419
+ noteSwallowed('close', e, this.debug);
420
+ }
392
421
  }
393
422
  _installNetHook() {
394
423
  const pg = this.page;
@@ -412,7 +441,9 @@ export class QwenBrowser {
412
441
  if (h['authorization'])
413
442
  this._apiAuth = h['authorization'];
414
443
  }
415
- catch { }
444
+ catch (e) {
445
+ noteSwallowed('_installNetHook', e, this.debug);
446
+ }
416
447
  });
417
448
  }
418
449
  async _onResponse(resp) {
@@ -484,7 +515,9 @@ export class QwenBrowser {
484
515
  this._netCaptureAt = Date.now();
485
516
  }
486
517
  }
487
- catch { }
518
+ catch (e) {
519
+ noteSwallowed('_onResponse', e, this.debug);
520
+ }
488
521
  }
489
522
  // Are we signed in? Detected from the PAGE, the same way a human sees it: a
490
523
  // signed-out visitor has the header "Log in" / "Sign up" buttons, a signed-in
@@ -522,7 +555,9 @@ export class QwenBrowser {
522
555
  return true;
523
556
  }
524
557
  }
525
- catch { }
558
+ catch (e) {
559
+ noteSwallowed('isLoggedIn', e, this.debug);
560
+ }
526
561
  await this.page.waitForTimeout(500);
527
562
  }
528
563
  return false;
@@ -598,7 +633,9 @@ export class QwenBrowser {
598
633
  .catch(() => { });
599
634
  }
600
635
  }
601
- catch { }
636
+ catch (e) {
637
+ noteSwallowed('_browserLogin', e, this.debug);
638
+ }
602
639
  },
603
640
  captureCredentials: async () => {
604
641
  try {
@@ -610,7 +647,9 @@ export class QwenBrowser {
610
647
  try {
611
648
  this.onAuthSave?.(user, '');
612
649
  }
613
- catch { }
650
+ catch (e) {
651
+ noteSwallowed('_browserLogin', e, this.debug);
652
+ }
614
653
  });
615
654
  await this.page.evaluate(() => {
616
655
  const w = window;
@@ -627,11 +666,15 @@ export class QwenBrowser {
627
666
  if (fn && e)
628
667
  fn(e.value || '');
629
668
  }
630
- catch { }
669
+ catch (e) {
670
+ noteSwallowed('_browserLogin', e, this.debug);
671
+ }
631
672
  }, true);
632
673
  });
633
674
  }
634
- catch { }
675
+ catch (e) {
676
+ noteSwallowed('_browserLogin', e, this.debug);
677
+ }
635
678
  },
636
679
  persistSession: () => this._persistSessionIfNeeded(),
637
680
  notice: (key) => this._notice(this._t(key)),
@@ -659,12 +702,16 @@ export class QwenBrowser {
659
702
  if (session.length)
660
703
  this._carryCookies = session;
661
704
  }
662
- catch { }
705
+ catch (e) {
706
+ noteSwallowed('relaunchSafe', e, this.debug);
707
+ }
663
708
  try {
664
709
  if (this.context)
665
710
  await this.context.close();
666
711
  }
667
- catch { }
712
+ catch (e) {
713
+ noteSwallowed('relaunchSafe', e, this.debug);
714
+ }
668
715
  const deadline = Date.now() + 15000;
669
716
  while (Date.now() < deadline) {
670
717
  if (!(await profileLooksLocked(QWEN_USER_DATA_DIR)))
@@ -736,7 +783,9 @@ export class QwenBrowser {
736
783
  await email.fill(this.auth.username).catch(() => { });
737
784
  }
738
785
  }
739
- catch { }
786
+ catch (e) {
787
+ noteSwallowed('waitForLogin', e, this.debug);
788
+ }
740
789
  },
741
790
  captureCredentials: async () => { },
742
791
  persistSession: () => this._persistSessionIfNeeded(),
@@ -807,7 +856,9 @@ export class QwenBrowser {
807
856
  try {
808
857
  this.onAuthSave?.(this.auth.username, this.auth.password);
809
858
  }
810
- catch { }
859
+ catch (e) {
860
+ noteSwallowed('_autoLogin', e, this.debug);
861
+ }
811
862
  }
812
863
  return ok;
813
864
  }
@@ -894,7 +945,9 @@ export class QwenBrowser {
894
945
  try {
895
946
  this.onAuthSave(addr, '');
896
947
  }
897
- catch { }
948
+ catch (e) {
949
+ noteSwallowed('_promptAndLogin', e, this.debug);
950
+ }
898
951
  }
899
952
  }
900
953
  else {
@@ -925,7 +978,9 @@ export class QwenBrowser {
925
978
  await this.page.waitForTimeout(2500);
926
979
  }
927
980
  }
928
- catch { }
981
+ catch (e) {
982
+ noteSwallowed('_openLoginPage', e, this.debug);
983
+ }
929
984
  }
930
985
  async _fillEmailAndContinue(email) {
931
986
  const input = this.page
@@ -958,7 +1013,9 @@ export class QwenBrowser {
958
1013
  await this.page.waitForTimeout(1500);
959
1014
  }
960
1015
  }
961
- catch { }
1016
+ catch (e) {
1017
+ noteSwallowed('_switchToPasswordForm', e, this.debug);
1018
+ }
962
1019
  }
963
1020
  // Submit the one-time code. Qwen renders EITHER six single-character boxes
964
1021
  // OR one code input; handle both. Best-effort (the exact markup may change).
@@ -992,7 +1049,9 @@ export class QwenBrowser {
992
1049
  await submit.click({ timeout: 3000 }).catch(() => { });
993
1050
  }
994
1051
  }
995
- catch { }
1052
+ catch (e) {
1053
+ noteSwallowed('_submitCode', e, this.debug);
1054
+ }
996
1055
  }
997
1056
  async _waitLoggedIn(timeoutMs) {
998
1057
  const deadline = Date.now() + timeoutMs;
@@ -1011,7 +1070,9 @@ export class QwenBrowser {
1011
1070
  await btn.click({ timeout: 2500 });
1012
1071
  clicked = true;
1013
1072
  }
1014
- catch { }
1073
+ catch (e) {
1074
+ noteSwallowed('newChat', e, this.debug);
1075
+ }
1015
1076
  let ok = false;
1016
1077
  if (clicked) {
1017
1078
  const deadline = Date.now() + 5000;
@@ -1050,7 +1111,9 @@ export class QwenBrowser {
1050
1111
  if (await loc.isVisible({ timeout }).catch(() => false))
1051
1112
  return loc;
1052
1113
  }
1053
- catch { }
1114
+ catch (e) {
1115
+ noteSwallowed('_findVisible', e, this.debug);
1116
+ }
1054
1117
  }
1055
1118
  return null;
1056
1119
  }
@@ -1088,7 +1151,9 @@ export class QwenBrowser {
1088
1151
  this._lastHistoryError = res.error;
1089
1152
  }
1090
1153
  }
1091
- catch { }
1154
+ catch (e) {
1155
+ noteSwallowed('listChats', e, this.debug);
1156
+ }
1092
1157
  // Fallback: scrape the sidebar (works for a guest with a rendered list).
1093
1158
  await this._ensureSidebarOpen();
1094
1159
  try {
@@ -1112,7 +1177,9 @@ export class QwenBrowser {
1112
1177
  await this.page.waitForTimeout(500);
1113
1178
  return;
1114
1179
  }
1115
- catch { }
1180
+ catch (e) {
1181
+ noteSwallowed('_ensureSidebarOpen', e, this.debug);
1182
+ }
1116
1183
  }
1117
1184
  }
1118
1185
  async openChat(id) {
@@ -1321,14 +1388,18 @@ export class QwenBrowser {
1321
1388
  }
1322
1389
  }
1323
1390
  }
1324
- catch { }
1391
+ catch (e) {
1392
+ noteSwallowed('stopGeneration', e, this.debug);
1393
+ }
1325
1394
  const btn = await this._findVisible(STOP_SELECTORS, 500);
1326
1395
  if (btn) {
1327
1396
  try {
1328
1397
  await btn.click({ timeout: 1500 });
1329
1398
  return true;
1330
1399
  }
1331
- catch { }
1400
+ catch (e) {
1401
+ noteSwallowed('stopGeneration', e, this.debug);
1402
+ }
1332
1403
  }
1333
1404
  const clicked = await this.page
1334
1405
  .evaluate(clickStopInDom, 'send-button|stop-button')
@@ -1339,7 +1410,9 @@ export class QwenBrowser {
1339
1410
  await this.page.keyboard.press('Escape');
1340
1411
  return true;
1341
1412
  }
1342
- catch { }
1413
+ catch (e) {
1414
+ noteSwallowed('stopGeneration', e, this.debug);
1415
+ }
1343
1416
  return false;
1344
1417
  }
1345
1418
  getToggleStatesSync() {
@@ -1400,7 +1473,9 @@ export class QwenBrowser {
1400
1473
  if (ok)
1401
1474
  this._appliedThinking = this.deepThinking ? 1 : 0;
1402
1475
  }
1403
- catch { }
1476
+ catch (e) {
1477
+ noteSwallowed('_applyThinkingMode', e, this.debug);
1478
+ }
1404
1479
  }
1405
1480
  // Enable web search via the "+" (mode) menu item, reading its selected state
1406
1481
  // first so a manual choice is not flipped back. The item shows a check/active
@@ -1449,7 +1524,9 @@ export class QwenBrowser {
1449
1524
  // Close the menu (the send button lives behind it).
1450
1525
  await this.page.keyboard.press('Escape').catch(() => { });
1451
1526
  }
1452
- catch { }
1527
+ catch (e) {
1528
+ noteSwallowed('_applyWebSearch', e, this.debug);
1529
+ }
1453
1530
  }
1454
1531
  // Click the first visible menu option whose label matches `labelRe`.
1455
1532
  async _clickMenuOption(labelRe) {
@@ -1516,7 +1593,9 @@ export class QwenBrowser {
1516
1593
  await this.page.keyboard.press('Escape').catch(() => { });
1517
1594
  }
1518
1595
  }
1519
- catch { }
1596
+ catch (e) {
1597
+ noteSwallowed('_applyModel', e, this.debug);
1598
+ }
1520
1599
  }
1521
1600
  async dumpDom(filePath) {
1522
1601
  if (!this.page)
@@ -1574,7 +1653,9 @@ export class QwenBrowser {
1574
1653
  try {
1575
1654
  this.onSendPause(secs);
1576
1655
  }
1577
- catch { }
1656
+ catch (e) {
1657
+ noteSwallowed('_waitForSendSlot', e, this.debug);
1658
+ }
1578
1659
  }
1579
1660
  };
1580
1661
  report(gap);
@@ -1617,6 +1698,8 @@ export class QwenBrowser {
1617
1698
  // The exhausted-context recovery is a COMPACTION (a new chat), not a
1618
1699
  // resend, so it has its own budget.
1619
1700
  let contextLimitRetries = 0;
1701
+ // N8: the fallback switch is tried ONCE per ask().
1702
+ let fallbackAttempted = false;
1620
1703
  this._askInFlight++;
1621
1704
  try {
1622
1705
  while (attempt < this.askRetries) {
@@ -1725,6 +1808,27 @@ export class QwenBrowser {
1725
1808
  })));
1726
1809
  }
1727
1810
  }
1811
+ // N8: the active provider exhausted its budgets. Give the injected
1812
+ // hook ONE chance to continue on an AUTHORIZED fallback provider; it
1813
+ // switches the backend and the SAME prompt is resent there.
1814
+ if (!fallbackAttempted && this.onProviderExhausted) {
1815
+ fallbackAttempted = true;
1816
+ let switched = false;
1817
+ try {
1818
+ switched = await this.onProviderExhausted(lastErr);
1819
+ }
1820
+ catch (e) {
1821
+ noteSwallowed('_onProviderExhausted', e, this.debug);
1822
+ }
1823
+ if (switched) {
1824
+ attempt = 0;
1825
+ rateLimitRetries = 0;
1826
+ serverBusyRetries = 0;
1827
+ incompleteRetries = 0;
1828
+ contextLimitRetries = 0;
1829
+ continue;
1830
+ }
1831
+ }
1728
1832
  this._notice(theme.warn(this._t('qwen.ask_retry', {
1729
1833
  attempt: attempt + 1,
1730
1834
  max: this.askRetries,
@@ -1777,7 +1881,9 @@ export class QwenBrowser {
1777
1881
  await this._awaitUploadPreview(files.map((f) => f.name), 5000);
1778
1882
  return failed;
1779
1883
  }
1780
- catch { }
1884
+ catch (e) {
1885
+ noteSwallowed('_attachFiles', e, this.debug);
1886
+ }
1781
1887
  // Fallback: set the files directly on the hidden <input type=file>.
1782
1888
  for (const sel of ATTACH_SELECTORS) {
1783
1889
  const el = this.page.locator(sel).last();
@@ -1788,7 +1894,9 @@ export class QwenBrowser {
1788
1894
  await this._awaitUploadPreview(files.map((f) => f.name), 3000);
1789
1895
  return failed;
1790
1896
  }
1791
- catch { }
1897
+ catch (e) {
1898
+ noteSwallowed('_attachFiles', e, this.debug);
1899
+ }
1792
1900
  }
1793
1901
  this._notice(this._t('qwen.attach_no_input'));
1794
1902
  for (const f of files)
@@ -1807,7 +1915,9 @@ export class QwenBrowser {
1807
1915
  await input.fill(text, { timeout: 5000 });
1808
1916
  return;
1809
1917
  }
1810
- catch { }
1918
+ catch (e) {
1919
+ noteSwallowed('_setInputText', e, this.debug);
1920
+ }
1811
1921
  }
1812
1922
  // contenteditable fallback: paste so a big block is inserted in one go.
1813
1923
  await input.click({ timeout: 3000 }).catch(() => { });
@@ -1847,7 +1957,9 @@ export class QwenBrowser {
1847
1957
  await this._awaitInput(10_000);
1848
1958
  input = await this._findVisible(INPUT_SELECTORS, 3000);
1849
1959
  }
1850
- catch { }
1960
+ catch (e) {
1961
+ noteSwallowed('_askOnce', e, this.debug);
1962
+ }
1851
1963
  }
1852
1964
  if (!input)
1853
1965
  throw new Error(this._t('qwen.input_missing'));
@@ -1859,7 +1971,9 @@ export class QwenBrowser {
1859
1971
  try {
1860
1972
  this.onSendStart();
1861
1973
  }
1862
- catch { }
1974
+ catch (e) {
1975
+ noteSwallowed('_askOnce', e, this.debug);
1976
+ }
1863
1977
  }
1864
1978
  this._emitSendState('generating');
1865
1979
  void this._setBusyTitle(true);
@@ -1918,7 +2032,9 @@ export class QwenBrowser {
1918
2032
  clicked = true;
1919
2033
  break;
1920
2034
  }
1921
- catch { }
2035
+ catch (e) {
2036
+ noteSwallowed('_askOnce', e, this.debug);
2037
+ }
1922
2038
  }
1923
2039
  // (2) Enter fallback — ONLY when no send button was clicked this round
1924
2040
  // (a build that sends solely on Enter, or a missing button). This is
package/dist/run-task.js CHANGED
@@ -284,7 +284,7 @@ async function evaluateGoal(browser, condition, config, transcript) {
284
284
  }
285
285
  export async function runTask(deps, browser, tools, taskText, workdir, opts, attachments = []) {
286
286
  const { t, locale, debug, maxIter, config, runAgentLoop, createSpinner } = deps;
287
- const { transcript, freshChat, sendSystemPrompt, queue = [], ui: editor, onChatReady, onAutoCompact = null, onContextLimit = null, autoCompactPct = 95, contextLimit = 1_000_000, getTokenUsage = null, goal = null, goalIsCondition = false, onGoalCleared = null, goalMaxTurns = GOAL_MAX_TURNS, goalQuery = null, todosQuery = null, selfImprovement = false, onSubagent = null, onAskUser = undefined, } = opts;
287
+ const { transcript, freshChat, sendSystemPrompt, queue = [], ui: editor, onChatReady, onAutoCompact = null, onContextLimit = null, onProviderExhausted = null, autoCompactPct = 95, contextLimit = 1_000_000, getTokenUsage = null, goal = null, goalIsCondition = false, onGoalCleared = null, goalMaxTurns = GOAL_MAX_TURNS, goalQuery = null, todosQuery = null, selfImprovement = false, onSubagent = null, onAskUser = undefined, } = opts;
288
288
  // The ACTIVE goal for the duration of this run. It is mutable because a
289
289
  // completion goal can be cleared mid-run (verdict MET/IMPOSSIBLE, or the
290
290
  // operator runs `/goal clear` while the task is in flight). `goalQuery`
@@ -412,6 +412,7 @@ export async function runTask(deps, browser, tools, taskText, workdir, opts, att
412
412
  // loop awaits it.
413
413
  onAutoCompact,
414
414
  onContextLimit,
415
+ onProviderExhausted,
415
416
  autoCompactPct,
416
417
  contextLimit,
417
418
  getTokenUsage,
@@ -0,0 +1,41 @@
1
+ // N2 group (d): the SCHEDULER and CONTEXT commands — /compact, /goal, /loop,
2
+ // /cron, /jobs. They all mutate long-lived session state (the chat, the goal,
3
+ // the scheduler's timers), so the state is INJECTED. The registry owns the
4
+ // names and the argument text; the handlers stay thin wrappers over the
5
+ // existing implementations.
6
+ //
7
+ // `/goal` is special: setting a condition FALLS THROUGH to the task path (the
8
+ // condition itself becomes the turn), so its handler returns a directive the
9
+ // caller turns into `taskOverride` instead of finishing the loop.
10
+ /** The directive a `/goal <condition>` turn carries into the task path. */
11
+ export const GOAL_TASK_OVERRIDE = 'Work toward this completion goal until it is satisfied. ' +
12
+ 'Do not call respond with a final summary until the condition is met.';
13
+ /**
14
+ * Build the group. `/goal` cannot be a plain fire-and-forget handler: the
15
+ * caller needs to know whether a directive was set, so the returned list is
16
+ * paired with `goalDirective()` below.
17
+ */
18
+ export function createSchedulerCommands(deps) {
19
+ return [
20
+ { name: 'compact', handler: () => deps.compact() },
21
+ {
22
+ name: 'goal',
23
+ handler: async (ctx) => {
24
+ const res = await deps.goal(ctx.input);
25
+ // 'set' is signalled through the shared state; see goalDirective().
26
+ void res;
27
+ },
28
+ },
29
+ { name: 'loop', handler: (ctx) => deps.loop(ctx.rest) },
30
+ { name: 'cron', handler: (ctx) => deps.cron(ctx.rest) },
31
+ { name: 'jobs', handler: (ctx) => deps.jobs(ctx.rest) },
32
+ ];
33
+ }
34
+ /**
35
+ * After dispatching group (d), the caller asks this: a freshly SET goal must
36
+ * turn the condition itself into the next task instead of returning to the
37
+ * prompt (Claude Code parity).
38
+ */
39
+ export function goalDirective(deps, wasSet) {
40
+ return wasSet && deps.currentGoal() ? GOAL_TASK_OVERRIDE : null;
41
+ }
@@ -0,0 +1,36 @@
1
+ // N1 stage 3: the SEND SLOT — the pause before a send that keeps the chat's
2
+ // rate limit happy.
3
+ //
4
+ // Every provider had its own `_waitForSendSlot()` with the same shape (skip a
5
+ // non-agent send, compute the remaining gap, report the animated countdown,
6
+ // mark the state 'paused', sleep interruptibly) and each computed the gap its
7
+ // own way (DeepSeek: interval + thinking bonus; Qwen: interval; GLM: the
8
+ // adaptive sendRuntime). The MECHANICS are shared here; the provider injects
9
+ // the gap and the plumbing.
10
+ /**
11
+ * Wait for the send slot. Returns true when the wait was INTERRUPTED (the
12
+ * caller must then abort the send).
13
+ */
14
+ export async function waitForSendSlot(agent, deps) {
15
+ if (!agent)
16
+ return false;
17
+ const gap = deps.gapMs();
18
+ if (gap <= 0)
19
+ return false;
20
+ const report = (leftMs) => {
21
+ const secs = Math.max(0, Math.ceil(leftMs / 1000));
22
+ if (deps.onPause) {
23
+ try {
24
+ deps.onPause(secs);
25
+ }
26
+ catch { }
27
+ }
28
+ else if (deps.onPauseText) {
29
+ deps.onPauseText(secs);
30
+ }
31
+ };
32
+ report(gap);
33
+ if (deps.emitState)
34
+ deps.emitState('paused');
35
+ return await deps.sleep(gap, report);
36
+ }
@@ -0,0 +1,42 @@
1
+ // N2 group (b): the SESSION and CHAT commands — /new (/clear), /sessions,
2
+ // /chats, /last, /resume-id. The handlers drive the live browser and the
3
+ // session store, so all of that is INJECTED. The ROUTING (a command name, its
4
+ // arguments, the aliases) is the registry's job and is unit-tested.
5
+ import { theme } from './theme.js';
6
+ export function createSessionCommands(deps) {
7
+ return [
8
+ {
9
+ name: 'new',
10
+ aliases: ['clear'],
11
+ handler: () => deps.newChat(),
12
+ },
13
+ {
14
+ name: 'sessions',
15
+ aliases: ['session'],
16
+ handler: (ctx) => deps.sessions(ctx.rest.toLowerCase()),
17
+ },
18
+ {
19
+ name: 'chats',
20
+ handler: (ctx) => deps.chats(ctx.rest.toLowerCase()),
21
+ },
22
+ {
23
+ name: 'last',
24
+ handler: () => deps.last(),
25
+ },
26
+ {
27
+ name: 'resume-id',
28
+ handler: async (ctx) => {
29
+ const id = deps.parseResumeId(ctx.rest);
30
+ if (!id) {
31
+ deps.printResumeUsage();
32
+ return;
33
+ }
34
+ await deps.resumeId(id);
35
+ },
36
+ },
37
+ ];
38
+ }
39
+ /** The `<id>` usage line, localized (shared by the registry handler and tests). */
40
+ export function resumeIdUsage(t) {
41
+ return theme.error(t('sessions.resume_id_usage'));
42
+ }
package/dist/spinner.js CHANGED
@@ -1,12 +1,12 @@
1
1
  import ora from 'ora';
2
2
  import { theme, divider } from './theme.js';
3
3
  import { renderMarkdown } from './markdown.js';
4
- import { translate } from './i18n.js';
4
+ import { translate, DEFAULT_LOCALE } from './i18n.js';
5
5
  import { truncateToWidth, wrapToWidth } from './input/layout.js';
6
6
  import { contentWidth } from './width.js';
7
7
  // Phrases for the "agent is working" spinner. Chosen in random order.
8
8
  // The text comes from i18n via the key 'spinner.phrases' (strings separated by "|").
9
- export function randomThinkingPhrase(locale = 'ru') {
9
+ export function randomThinkingPhrase(locale = DEFAULT_LOCALE) {
10
10
  const raw = translate(locale)('spinner.phrases');
11
11
  const phrases = raw.split('|').filter((s) => s.trim().length);
12
12
  if (!phrases.length)
@@ -28,7 +28,7 @@ export function renderDots(n) {
28
28
  const d = DOTS[n] ?? '';
29
29
  return theme.brown(d + DOTS_PAD.slice(d.length));
30
30
  }
31
- export function createSpinner(locale = 'ru') {
31
+ export function createSpinner(locale = DEFAULT_LOCALE) {
32
32
  // Set by the caller (index.ts) to the same todos summary the LineEditor
33
33
  // shows, so the non-TTY path has parity.
34
34
  let onTasksQuery = null;