@undefineds.co/linx 0.3.8 → 0.3.13

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 (42) hide show
  1. package/README.md +17 -0
  2. package/dist/generated/version.js +2 -2
  3. package/dist/generated/version.js.map +1 -1
  4. package/dist/index.js +13 -3
  5. package/dist/index.js.map +1 -1
  6. package/dist/lib/auto-mode/pod-approval.js +216 -2
  7. package/dist/lib/auto-mode/pod-approval.js.map +1 -1
  8. package/dist/lib/linx-status-line.js +335 -0
  9. package/dist/lib/linx-status-line.js.map +1 -0
  10. package/dist/lib/linx-tui-contract.js +3 -3
  11. package/dist/lib/linx-tui-contract.js.map +1 -1
  12. package/dist/lib/models.js +2 -2
  13. package/dist/lib/models.js.map +1 -1
  14. package/dist/lib/pi-adapter/auth.js +68 -0
  15. package/dist/lib/pi-adapter/auth.js.map +1 -0
  16. package/dist/lib/pi-adapter/interactive.js +326 -231
  17. package/dist/lib/pi-adapter/interactive.js.map +1 -1
  18. package/dist/lib/pi-adapter/pod-mirror.js +52 -2
  19. package/dist/lib/pi-adapter/pod-mirror.js.map +1 -1
  20. package/dist/lib/pi-adapter/pod-tools.js +140 -0
  21. package/dist/lib/pi-adapter/pod-tools.js.map +1 -0
  22. package/dist/lib/pi-adapter/runtime.js +14 -4
  23. package/dist/lib/pi-adapter/runtime.js.map +1 -1
  24. package/dist/lib/pi-adapter/stream.js +24 -1
  25. package/dist/lib/pi-adapter/stream.js.map +1 -1
  26. package/dist/lib/status-line-command.js +108 -0
  27. package/dist/lib/status-line-command.js.map +1 -0
  28. package/dist/lib/symphony/pod-projection.js +15 -7
  29. package/dist/lib/symphony/pod-projection.js.map +1 -1
  30. package/dist/lib/symphony-command.js +20 -21
  31. package/dist/lib/symphony-command.js.map +1 -1
  32. package/dist/skills/symphony/SKILL.md +69 -10
  33. package/dist/skills/xpod-cli/SKILL.md +70 -0
  34. package/package.json +9 -3
  35. package/vendor/agent-runtime/dist/client-inbox-subscription.d.ts +56 -0
  36. package/vendor/agent-runtime/dist/client-inbox-subscription.js +93 -0
  37. package/vendor/agent-runtime/dist/index.d.ts +1 -0
  38. package/vendor/agent-runtime/dist/index.js +1 -0
  39. package/vendor/agent-runtime/dist/reconciler.d.ts +60 -1
  40. package/vendor/agent-runtime/dist/reconciler.js +148 -4
  41. package/vendor/agent-runtime/dist/thread-reconciler-controller.d.ts +2 -1
  42. package/vendor/agent-runtime/dist/thread-reconciler-controller.js +4 -0
@@ -13,9 +13,10 @@ import { installPodStatusOutputFilter } from './pod-status-output.js';
13
13
  import { createPodBackedExtensionUiContext } from './pod-approval.js';
14
14
  import { DEFAULT_SECRETARY_CHAT_ID, secretaryChatUri, secretaryThreadUri } from './pod-mirror-mapping.js';
15
15
  import { getSecretaryAutoInputController } from './auto-input-controller.js';
16
- import { createSymphonyIdeaRecord, listSymphonyIssues, listSymphonySessions, writeSymphonyIdea, } from '../symphony/archive.js';
17
- import { listOpenSymphonyIssuesFromPod, listRecentSymphonyReportsFromPod, listRunningSymphonyWorkersFromPod, mirrorSymphonyProjectionJsonLdFromPod, persistSymphonyIdeaToPod, persistSymphonyProjectionToPod, } from '../symphony/pod-projection.js';
16
+ import { createSymphonyIdeaRecord, listSymphonyIssues, listSymphonySessions, } from '../symphony/archive.js';
17
+ import { listOpenSymphonyIssuesFromPod, listRecentSymphonyReportsFromPod, listRunningSymphonyWorkersFromPod, mirrorSymphonyProjectionJsonLdFromPod, persistSymphonyIdeaToPod, persistSymphonyControlStateToPod, } from '../symphony/pod-projection.js';
18
18
  import { getSessionControlManager, installSessionControlRuntimeEventBridge, } from './session-control.js';
19
+ import { buildLinxFooterStatusLine, calculateSessionUsage, DEFAULT_STATUS_LINE_TOKENS, formatTokenCount, LINX_STATUS_LINE_TOKEN_NAMES, parseLinxStatusLineColorArg, parseLinxStatusLineTokenArgs, readLinxStatusLineConfig, resetLinxStatusLineConfig, writeLinxStatusLineConfigPatch, } from '../linx-status-line.js';
19
20
  let footerPatched = false;
20
21
  let assistantMessagePatched = false;
21
22
  let linxResumeOutputStyleRestore = null;
@@ -27,6 +28,21 @@ const BACKEND_OWNED_SLASH_COMMANDS = new Set([
27
28
  ]);
28
29
  const SYMPHONY_STATUS_POD_TIMEOUT_MS = 1_200;
29
30
  const DEFAULT_SYMPHONY_WORKER_SUPERVISOR_INTERVAL_MS = 10 * 60 * 1000;
31
+ const CODEX_STYLE_STATUS_LINE_TOKENS = [
32
+ 'model-with-reasoning',
33
+ 'git-branch',
34
+ 'context-remaining',
35
+ 'total-input-tokens',
36
+ 'total-output-tokens',
37
+ 'current-dir',
38
+ ];
39
+ const COMPACT_STATUS_LINE_TOKENS = [
40
+ 'model-with-reasoning',
41
+ 'context-remaining',
42
+ 'current-dir',
43
+ ];
44
+ /** Module-level reference to interactive for footer mode state (set during bootstrap). */
45
+ let _linxFooterInteractive = null;
30
46
  export function bootstrapLinxInteractiveMode(runtime, options = {}) {
31
47
  installLinxResumeOutputStyle();
32
48
  patchPiFooter();
@@ -37,6 +53,7 @@ export function bootstrapLinxInteractiveMode(runtime, options = {}) {
37
53
  interactive.runtime = runtime;
38
54
  interactive.__autoEnabled = runtime?.autoEnabled === true;
39
55
  interactive.__linxSymphonyModeEnabled = runtime?.symphonyEnabled === true;
56
+ _linxFooterInteractive = interactive;
40
57
  if (options.onSymphonyControlChange) {
41
58
  ;
42
59
  interactive.__linxOnSymphonyControlChange = options.onSymphonyControlChange;
@@ -124,7 +141,7 @@ export function installLinxRestoredAutoStartup(interactive, runtime, sessionCont
124
141
  controller.start({ scheduleImmediately: true });
125
142
  interactive.showStatus?.([
126
143
  'Auto restored from the previous session.',
127
- '托管中 · Secretary 自动输入 · Ctrl+C 接管 · /auto off',
144
+ 'auto · Ctrl+C or /auto off to hand control back',
128
145
  ].join('\n'));
129
146
  interactive.ui?.requestRender?.();
130
147
  }
@@ -528,6 +545,15 @@ function parseLinxGlobalCommand(input) {
528
545
  if (input.startsWith('/ai connect ')) {
529
546
  return { action: 'ai-connect', ...parseInteractiveAiConnectArgs(input.slice('/ai connect'.length).trim()) };
530
547
  }
548
+ if (input === '/statusline' || input === '/status-line') {
549
+ return { action: 'statusline', args: [] };
550
+ }
551
+ if (input.startsWith('/statusline ') || input.startsWith('/status-line ')) {
552
+ const body = input.startsWith('/statusline ')
553
+ ? input.slice('/statusline'.length).trim()
554
+ : input.slice('/status-line'.length).trim();
555
+ return { action: 'statusline', args: splitInteractiveCommandArgs(body) };
556
+ }
531
557
  if (input === '/rewind') {
532
558
  return { action: 'rewind-select' };
533
559
  }
@@ -649,6 +675,10 @@ async function handleLinxGlobalCommand(interactive, runtime, command) {
649
675
  await handleInteractiveAiConnectCommand(interactive, runtime, command);
650
676
  return;
651
677
  }
678
+ if (command.action === 'statusline') {
679
+ await handleInteractiveStatusLineCommand(interactive, command.args);
680
+ return;
681
+ }
652
682
  if (command.action === 'rewind-select') {
653
683
  await handleInteractiveRewindSelector(interactive, runtime);
654
684
  return;
@@ -659,6 +689,130 @@ async function handleLinxGlobalCommand(interactive, runtime, command) {
659
689
  }
660
690
  await changeInteractiveCwd(interactive, runtime, command.target);
661
691
  }
692
+ async function handleInteractiveStatusLineCommand(interactive, args) {
693
+ if (args.length > 0) {
694
+ handleInteractiveStatusLineArgs(interactive, args);
695
+ return;
696
+ }
697
+ const summary = formatInteractiveStatusLineSummary();
698
+ if (typeof interactive.showExtensionSelector !== 'function') {
699
+ interactive.showStatus?.(`${summary} · Use /statusline set <tokens...>, /statusline tokens, /statusline colors <on|off>, or /statusline reset.`);
700
+ interactive.ui?.requestRender?.();
701
+ return;
702
+ }
703
+ const choice = await interactive.showExtensionSelector(`Status line\n${summary}`, [
704
+ 'Use Codex-style preset',
705
+ 'Use compact preset',
706
+ 'Configure tokens manually',
707
+ 'Toggle colors',
708
+ 'Show available tokens',
709
+ 'Reset to default',
710
+ ]);
711
+ if (choice === 'Use Codex-style preset') {
712
+ writeInteractiveStatusLineConfig(interactive, {
713
+ statusLine: CODEX_STYLE_STATUS_LINE_TOKENS,
714
+ message: 'Status line set to Codex-style preset.',
715
+ });
716
+ return;
717
+ }
718
+ if (choice === 'Use compact preset') {
719
+ writeInteractiveStatusLineConfig(interactive, {
720
+ statusLine: COMPACT_STATUS_LINE_TOKENS,
721
+ message: 'Status line set to compact preset.',
722
+ });
723
+ return;
724
+ }
725
+ if (choice === 'Toggle colors') {
726
+ const current = readLinxStatusLineConfig();
727
+ writeInteractiveStatusLineConfig(interactive, {
728
+ statusLineUseColors: !current.useColors,
729
+ message: `Status line colors ${current.useColors ? 'disabled' : 'enabled'}.`,
730
+ });
731
+ return;
732
+ }
733
+ if (choice === 'Show available tokens') {
734
+ showInteractiveStatusLineTokens(interactive);
735
+ return;
736
+ }
737
+ if (choice === 'Reset to default') {
738
+ resetLinxStatusLineConfig();
739
+ finishInteractiveStatusLineUpdate(interactive, `Status line reset to default: ${DEFAULT_STATUS_LINE_TOKENS.join(', ')}`);
740
+ return;
741
+ }
742
+ if (choice === 'Configure tokens manually') {
743
+ await promptInteractiveStatusLineTokens(interactive);
744
+ }
745
+ }
746
+ function handleInteractiveStatusLineArgs(interactive, args) {
747
+ const action = args[0]?.toLowerCase();
748
+ if (action === 'tokens' || action === 'list') {
749
+ showInteractiveStatusLineTokens(interactive);
750
+ return;
751
+ }
752
+ if (action === 'reset') {
753
+ resetLinxStatusLineConfig();
754
+ finishInteractiveStatusLineUpdate(interactive, `Status line reset to default: ${DEFAULT_STATUS_LINE_TOKENS.join(', ')}`);
755
+ return;
756
+ }
757
+ if (action === 'colors' || action === 'color') {
758
+ const value = parseLinxStatusLineColorArg(args[1]);
759
+ if (value === undefined) {
760
+ interactive.showError?.('Usage: /statusline colors <on|off>');
761
+ return;
762
+ }
763
+ writeInteractiveStatusLineConfig(interactive, {
764
+ statusLineUseColors: value,
765
+ message: `Status line colors ${value ? 'enabled' : 'disabled'}.`,
766
+ });
767
+ return;
768
+ }
769
+ const tokenArgs = action === 'set' ? args.slice(1) : args;
770
+ try {
771
+ const tokens = parseLinxStatusLineTokenArgs(tokenArgs);
772
+ writeInteractiveStatusLineConfig(interactive, {
773
+ statusLine: tokens,
774
+ message: `Status line updated: ${tokens.join(', ')}`,
775
+ });
776
+ }
777
+ catch (error) {
778
+ const message = error instanceof Error ? error.message : String(error);
779
+ interactive.showError?.(`${message}. Use /statusline tokens to list valid tokens.`);
780
+ }
781
+ }
782
+ async function promptInteractiveStatusLineTokens(interactive) {
783
+ if (typeof interactive.showExtensionInput !== 'function') {
784
+ interactive.showStatus?.(`Use /statusline set ${CODEX_STYLE_STATUS_LINE_TOKENS.join(' ')}`);
785
+ interactive.ui?.requestRender?.();
786
+ return;
787
+ }
788
+ const value = await interactive.showExtensionInput('Status line tokens', CODEX_STYLE_STATUS_LINE_TOKENS.join(' '));
789
+ if (typeof value !== 'string' || !value.trim()) {
790
+ interactive.showStatus?.('Status line unchanged.');
791
+ interactive.ui?.requestRender?.();
792
+ return;
793
+ }
794
+ handleInteractiveStatusLineArgs(interactive, ['set', ...splitInteractiveCommandArgs(value)]);
795
+ }
796
+ function writeInteractiveStatusLineConfig(interactive, patch) {
797
+ writeLinxStatusLineConfigPatch({
798
+ ...(patch.statusLine ? { statusLine: patch.statusLine } : {}),
799
+ ...(patch.statusLineUseColors !== undefined ? { statusLineUseColors: patch.statusLineUseColors } : {}),
800
+ });
801
+ finishInteractiveStatusLineUpdate(interactive, patch.message);
802
+ }
803
+ function finishInteractiveStatusLineUpdate(interactive, message) {
804
+ interactive.footer?.invalidate?.();
805
+ interactive.showStatus?.(message);
806
+ interactive.ui?.requestRender?.();
807
+ }
808
+ function showInteractiveStatusLineTokens(interactive) {
809
+ interactive.showStatus?.(`Status line tokens: ${LINX_STATUS_LINE_TOKEN_NAMES.join(', ')}`);
810
+ interactive.ui?.requestRender?.();
811
+ }
812
+ function formatInteractiveStatusLineSummary() {
813
+ const config = readLinxStatusLineConfig();
814
+ return `Current: ${config.tokens.join(', ')} · colors ${config.useColors ? 'on' : 'off'} · source ${config.tokenSource}`;
815
+ }
662
816
  async function handleInteractiveRewindSelector(interactive, runtime) {
663
817
  const session = resolveInteractiveSession(interactive, runtime);
664
818
  const sessionManager = resolveInteractiveSessionManager(interactive, runtime);
@@ -720,6 +874,7 @@ async function handleInteractiveRewindTurnsCommand(interactive, runtime, turns)
720
874
  }
721
875
  const cleanResult = materializeCleanRewindSession(sessionManager, result.targetLeafId, previousState);
722
876
  syncAgentStateFromSessionManager(session, sessionManager);
877
+ refreshInteractiveTranscriptFromSessionManager(interactive);
723
878
  await syncRewindProjection(interactive, runtime, {
724
879
  previousState,
725
880
  cleanResult,
@@ -748,6 +903,7 @@ async function rewindSessionManagerBeforeUserEntry(interactive, runtime, session
748
903
  moveSessionManagerLeaf(sessionManager, targetLeafId);
749
904
  const cleanResult = materializeCleanRewindSession(sessionManager, targetLeafId, previousState);
750
905
  syncAgentStateFromSessionManager(session, sessionManager);
906
+ refreshInteractiveTranscriptFromSessionManager(interactive);
751
907
  await syncRewindProjection(interactive, runtime, {
752
908
  previousState,
753
909
  cleanResult,
@@ -987,6 +1143,22 @@ function syncAgentStateFromSessionManager(session, sessionManager) {
987
1143
  session.agent.state.messages = context.messages;
988
1144
  }
989
1145
  }
1146
+ function refreshInteractiveTranscriptFromSessionManager(interactive) {
1147
+ try {
1148
+ if (typeof interactive?.rebuildChatFromMessages === 'function') {
1149
+ interactive.rebuildChatFromMessages();
1150
+ return;
1151
+ }
1152
+ if (typeof interactive?.renderInitialMessages === 'function') {
1153
+ interactive.chatContainer?.clear?.();
1154
+ interactive.renderInitialMessages();
1155
+ }
1156
+ }
1157
+ catch (error) {
1158
+ const message = error instanceof Error ? error.message : String(error);
1159
+ interactive?.showWarning?.(`Rewind transcript refresh failed: ${message}`);
1160
+ }
1161
+ }
990
1162
  function collectRewindUserMessages(_session, sessionManager) {
991
1163
  return getActiveSessionBranch(sessionManager)
992
1164
  .filter((entry) => entry?.type === 'message' && entry.message?.role === 'user')
@@ -1190,7 +1362,7 @@ function formatAutoModeChangeStatus(enabled) {
1190
1362
  'Auto is on.',
1191
1363
  'Auto on: Secretary drives the current session input loop.',
1192
1364
  'What changed: backend prompts and blocked approval/input requests go to Secretary first; Secretary answers in-policy and asks you only when blocked.',
1193
- 'User-visible state: the input bar shows托管中; Ctrl+C or /auto off hands control back to you.',
1365
+ 'User-visible state: the input bar shows auto; Ctrl+C or /auto off hands control back to you.',
1194
1366
  'Backend approval policy is unchanged.',
1195
1367
  ].join('\n')
1196
1368
  : [
@@ -1270,18 +1442,8 @@ export function installSymphonyCommand(interactive) {
1270
1442
  return;
1271
1443
  }
1272
1444
  if (this.__linxSymphonyModeEnabled && shouldProjectSymphonyInput(input)) {
1273
- const source = await resolveSymphonySourceContext(this);
1274
- const idea = await captureSymphonyIdeaIfNeeded(input, source);
1275
1445
  getSessionControlManager(this, this.runtime).recordUserMessage({ text: input });
1276
- if (shouldDispatchSymphonyWorkerInput(input)) {
1277
- await dispatchSymphonyWorkerFromInteractive(this, input, source);
1278
- return;
1279
- }
1280
- await originalSubmit(buildSymphonyDelegationPrompt(input, {
1281
- persistentMode: true,
1282
- ...(source ? { source } : {}),
1283
- ...(idea ? { idea } : {}),
1284
- }));
1446
+ await originalSubmit(renderSymphonySecretaryProjection(input));
1285
1447
  return;
1286
1448
  }
1287
1449
  await originalSubmit(text);
@@ -1347,6 +1509,17 @@ const LINX_INTERACTIVE_SLASH_COMMANDS = [
1347
1509
  name: 'rewind',
1348
1510
  description: 'select a user message and rewind the active branch before it',
1349
1511
  },
1512
+ {
1513
+ name: 'statusline',
1514
+ argumentHint: 'set|colors|tokens|reset',
1515
+ description: 'configure which items appear in the status line',
1516
+ getArgumentCompletions: (prefix) => completeStaticArguments(prefix, [
1517
+ { value: 'set', description: 'Set status line tokens' },
1518
+ { value: 'colors', description: 'Enable or disable status line colors' },
1519
+ { value: 'tokens', description: 'List available status line tokens' },
1520
+ { value: 'reset', description: 'Restore default status line tokens' },
1521
+ ]),
1522
+ },
1350
1523
  {
1351
1524
  name: 'ai',
1352
1525
  argumentHint: 'connect <provider>',
@@ -1356,11 +1529,11 @@ const LINX_INTERACTIVE_SLASH_COMMANDS = [
1356
1529
  {
1357
1530
  name: 'symphony',
1358
1531
  argumentHint: 'on|off|status',
1359
- description: 'switch chat peer between Secretary and backend worker',
1532
+ description: 'turn Secretary task handoff on/off, or show status',
1360
1533
  getArgumentCompletions: (prefix) => completeStaticArguments(prefix, [
1361
- { value: 'on', description: 'Chat with Secretary using Symphony orchestration skills' },
1362
- { value: 'off', description: 'Chat directly with the current worker/backend peer' },
1363
- { value: 'status', description: 'Show Symphony state and source conversation' },
1534
+ { value: 'on', description: 'Secretary can plan and hand off larger tasks' },
1535
+ { value: 'off', description: 'Return to direct chat' },
1536
+ { value: 'status', description: 'Show whether Symphony task handoff is enabled' },
1364
1537
  ]),
1365
1538
  },
1366
1539
  ];
@@ -1472,19 +1645,8 @@ async function handleSymphonyCommand(interactive, command) {
1472
1645
  }
1473
1646
  function formatSymphonyModeChangeStatus(enabled) {
1474
1647
  return enabled
1475
- ? [
1476
- 'Symphony on: you are now chatting with Secretary.',
1477
- 'What changed: following normal messages enter the Secretary control lane before worker/backend routing.',
1478
- 'Skills: issue triage, existing Issue lookup, create/update/ask decision, task split, worker dispatch, status/report tracking.',
1479
- 'Ordinary chat stays ordinary Message; only trackable work becomes Issue/Task/Delivery/Session.',
1480
- 'Use /symphony status to inspect workers, /symphony off to chat with the current worker/backend peer.',
1481
- ].join('\n')
1482
- : [
1483
- 'Symphony off: you are now chatting with the current worker/backend peer.',
1484
- 'What changed: following messages bypass Secretary Symphony triage and dispatch.',
1485
- 'Current Symphony dispatches started from this TUI were cancelled; archived workers remain inspectable with /symphony status.',
1486
- 'Use /symphony on to chat with Secretary again.',
1487
- ].join('\n');
1648
+ ? 'Symphony is on. I will keep ordinary chat ordinary and only plan or hand off real work.'
1649
+ : 'Symphony is off. Back to direct chat. Active handoffs from this window were stopped.';
1488
1650
  }
1489
1651
  function formatSymphonyUsage(input) {
1490
1652
  return [
@@ -1498,13 +1660,21 @@ function shouldProjectSymphonyInput(input) {
1498
1660
  && !input.startsWith('/')
1499
1661
  && !input.startsWith('!');
1500
1662
  }
1501
- function shouldDispatchSymphonyWorkerInput(input) {
1502
- const normalized = input.trim().toLowerCase();
1503
- if (!normalized) {
1504
- return false;
1505
- }
1506
- return /\b(delegate|dispatch|assign|worker|agent|task)\b/u.test(normalized)
1507
- || /(派工|派活|派发|委派|分派|交给.*(worker|agent|codex|claude|codebuddy|ai)|让.*(worker|agent|codex|claude|codebuddy|ai).*做|发一个任务|派出一个任务)/u.test(input);
1663
+ function renderSymphonySecretaryProjection(input) {
1664
+ return [
1665
+ '# AI Secretary Symphony request',
1666
+ '',
1667
+ 'Symphony is on: the user is chatting with Secretary, not directly with the worker/backend peer.',
1668
+ 'Treat the user message below as a Secretary-facing product message.',
1669
+ 'Decide whether it is ordinary chat, an Idea, a change to existing work, or delegable work.',
1670
+ 'Default response style: reply like normal chat.',
1671
+ 'Do not print internal Symphony binding, Issue/Task routing, worker selection, or report-style sections unless a visible state change or blocker must be surfaced.',
1672
+ 'If the message is ordinary chat or early exploration, answer directly and do not explain that it was not delegated.',
1673
+ 'If real delegation is needed, summarize the visible handoff result briefly after updating control state.',
1674
+ '',
1675
+ 'User message:',
1676
+ input,
1677
+ ].join('\n');
1508
1678
  }
1509
1679
  async function dispatchSymphonyWorkerFromInteractive(interactive, objective, source) {
1510
1680
  const backend = resolveSymphonyWorkerBackend(interactive, objective);
@@ -1522,18 +1692,9 @@ async function dispatchSymphonyWorkerFromInteractive(interactive, objective, sou
1522
1692
  const controller = new AbortController();
1523
1693
  const controllers = getInteractiveSymphonyDispatchControllers(interactive);
1524
1694
  controllers.add(controller);
1525
- interactive.showStatus?.([
1526
- 'Symphony dispatch started.',
1527
- `Worker backend: ${backend}`,
1528
- `Worker credentials: ${workerCredentialSource}`,
1529
- ...(agentRuntime ? [`Control runtime: ${formatSymphonyControlRuntime(agentRuntime)}`] : []),
1530
- ...(workerModel ? [`Worker model: ${workerModel}`] : []),
1531
- workerGoalMode
1532
- ? `Worker goal: on · supervisor interval=${formatSymphonySupervisorInterval(workerSupervisorIntervalMs)}`
1533
- : 'Worker goal: off',
1534
- 'Status: creating Issue / Task / Delivery and starting a quiet worker session.',
1535
- 'Use /symphony status to inspect running workers and reports.',
1536
- ].join('\n'));
1695
+ interactive.showStatus?.(`Symphony handoff started: ${backend}${workerModel ? ` · ${workerModel}` : ''}`
1696
+ + `${workerGoalMode ? ` · supervised every ${formatSymphonySupervisorInterval(workerSupervisorIntervalMs)}` : ''}.`
1697
+ + ' Use /symphony status for details.');
1537
1698
  interactive.ui?.requestRender?.();
1538
1699
  const run = typeof interactive.__linxRunSymphony === 'function'
1539
1700
  ? interactive.__linxRunSymphony
@@ -1624,8 +1785,8 @@ function createInteractiveSymphonyRuntime(interactive) {
1624
1785
  return {
1625
1786
  runAutoMode,
1626
1787
  listAutoModeSessions: listArchivedAutoModeSessions,
1627
- persistSymphonyProjectionToPod(plan, options) {
1628
- return persistSymphonyProjectionToPod(plan, {
1788
+ persistSymphonyControlStateToPod(plan, options) {
1789
+ return persistSymphonyControlStateToPod(plan, {
1629
1790
  ...options,
1630
1791
  runtime: projectionRuntime,
1631
1792
  });
@@ -1817,13 +1978,9 @@ function formatSymphonyDispatchResult(plan) {
1817
1978
  const delivery = worker?.delivery ?? plan.delivery;
1818
1979
  const lines = [
1819
1980
  plan.issue.status === 'resolved' && delivery.status === 'completed'
1820
- ? 'Symphony dispatch completed.'
1821
- : 'Symphony dispatch recorded.',
1822
- `Issue: ${plan.issue.title} (${formatSymphonyResourceTail(plan.issue.uri) ?? plan.issue.uri})`,
1823
- `Task: ${formatSymphonyResourceTail(worker?.task ?? plan.task) ?? worker?.task ?? plan.task}`,
1824
- `Delivery: ${delivery.status}${delivery.autoModeSessionId ? ` · runtime=${delivery.autoModeSessionId}` : ''}`,
1825
- `Worker session: ${session.status}${session.autoModeSessionId ? ` · runtime=${session.autoModeSessionId}` : ''}`,
1826
- 'Use /symphony status to inspect the Pod-projected worker report.',
1981
+ ? `Symphony handoff completed: ${plan.issue.title}.`
1982
+ : `Symphony handoff recorded: ${plan.issue.title}.`,
1983
+ 'Use /symphony status for details.',
1827
1984
  ];
1828
1985
  if (session.error) {
1829
1986
  lines.push(`Error: ${session.error}`);
@@ -1847,10 +2004,9 @@ async function captureSymphonyIdeaIfNeeded(input, source) {
1847
2004
  ...(affectedArea ? { affectedArea } : {}),
1848
2005
  };
1849
2006
  const idea = createSymphonyIdeaRecord(captureInput);
1850
- const persisted = await persistSymphonyIdeaToPod(idea)
1851
- .catch(() => null);
2007
+ const persisted = await persistSymphonyIdeaToPod(idea);
1852
2008
  if (!persisted) {
1853
- writeSymphonyIdea(idea);
2009
+ throw new Error('No active Pod session; Symphony Idea records must be written to Pod in LinX runtime.');
1854
2010
  }
1855
2011
  return {
1856
2012
  uri: idea.uri,
@@ -1859,7 +2015,10 @@ async function captureSymphonyIdeaIfNeeded(input, source) {
1859
2015
  commitment: idea.commitment,
1860
2016
  };
1861
2017
  }
1862
- catch {
2018
+ catch (error) {
2019
+ process.emitWarning(error instanceof Error
2020
+ ? new Error(`LinX Symphony Idea Pod write failed: ${error.message}`)
2021
+ : new Error(`LinX Symphony Idea Pod write failed: ${String(error)}`));
1863
2022
  return undefined;
1864
2023
  }
1865
2024
  }
@@ -1881,51 +2040,6 @@ function inferSymphonyIdeaAffectedArea(input) {
1881
2040
  }
1882
2041
  return undefined;
1883
2042
  }
1884
- function buildSymphonyDelegationPrompt(objective, options) {
1885
- const modeLine = options.persistentMode
1886
- ? 'Symphony is on: the user is chatting with Secretary in this LinX TUI session.'
1887
- : 'This is a chat-driven Symphony request from the LinX TUI.';
1888
- const sourceLines = options.source
1889
- ? [
1890
- '',
1891
- 'Source conversation resources:',
1892
- `Chat: ${options.source.chat}`,
1893
- `Thread: ${options.source.thread}`,
1894
- ...(options.source.sessionId ? [`Runtime session: ${options.source.sessionId}`] : []),
1895
- ]
1896
- : [];
1897
- const ideaLines = options.idea
1898
- ? [
1899
- '',
1900
- 'Captured Idea:',
1901
- `Idea: ${options.idea.uri}`,
1902
- `Summary: ${options.idea.summary}`,
1903
- `Commitment: ${options.idea.commitment}`,
1904
- `Status: ${options.idea.status}`,
1905
- ]
1906
- : [];
1907
- return [
1908
- 'AI Secretary Symphony request.',
1909
- modeLine,
1910
- ...sourceLines,
1911
- ...ideaLines,
1912
- '',
1913
- 'User objective:',
1914
- objective.trim(),
1915
- '',
1916
- 'Act as AI Secretary with Symphony skills enabled: issue triage, existing issue lookup, create/update/ask decision, task split, worker dispatch, and status/report tracking.',
1917
- 'Decide whether this objective should be delegated to backend workers through Symphony.',
1918
- 'Do not create an Issue for ordinary chat. Create or update an Issue only when the objective is a trackable work item.',
1919
- 'If this is an uncommitted fragment, keep or merge it as an Idea first; do not dispatch a worker until promotion gates are met.',
1920
- 'Before creating a new Issue, compare against existing open Issues. Update the existing Issue when it is clearly the same work item, and ask the user only when new-vs-existing is ambiguous.',
1921
- 'Every delegation must target a Chat resource. Use a personal AI contact chat when assigning to one worker, or a group chat when the work belongs in a shared room.',
1922
- 'Use the Source conversation resources only as provenance unless they are also the correct target chat.',
1923
- 'If delegation is appropriate, create or update the normal LinX work context, derive issue/task acceptance criteria from the objective and source context, and project the task to the selected backend worker.',
1924
- 'Ask the user only when acceptance, authority, credentials, or target selection cannot be safely inferred.',
1925
- 'If delegation is not appropriate, explain the reason and continue in this conversation.',
1926
- 'Keep the user-facing answer concise and show the next observable step.',
1927
- ].join('\n');
1928
- }
1929
2043
  async function formatSymphonyStatus(interactive) {
1930
2044
  const enabled = interactive.__linxSymphonyModeEnabled === true;
1931
2045
  const [source, workersRead, issuesRead, reportsRead] = await Promise.all([
@@ -1937,32 +2051,29 @@ async function formatSymphonyStatus(interactive) {
1937
2051
  const workers = workersRead.items;
1938
2052
  const issues = issuesRead.items;
1939
2053
  const reports = reportsRead.items;
1940
- const projectionErrors = Array.from(new Set([
2054
+ const controlStateErrors = Array.from(new Set([
1941
2055
  workersRead.error,
1942
2056
  issuesRead.error,
1943
2057
  reportsRead.error,
1944
2058
  ].filter((item) => Boolean(item))));
1945
- const projectionSources = new Set([workersRead.source, issuesRead.source, reportsRead.source]);
2059
+ const controlStateSources = new Set([workersRead.source, issuesRead.source, reportsRead.source]);
1946
2060
  const lines = [
1947
2061
  `Symphony is ${enabled ? 'on' : 'off'}.`,
1948
2062
  `Current chat peer: ${enabled ? 'Secretary' : 'worker/backend peer'}.`,
1949
2063
  `Open issues: ${issues.length}`,
1950
2064
  `Running workers: ${workers.length}`,
1951
2065
  `Recent reports: ${reports.length}`,
1952
- projectionErrors.length > 0
1953
- ? `Pod projection: unavailable (${formatSymphonyStatusError(projectionErrors[0])})`
1954
- : projectionSources.has('pod')
1955
- ? 'Pod projection: active.'
1956
- : 'Pod projection: local archive only.',
2066
+ controlStateErrors.length > 0
2067
+ ? `Pod control state: unavailable (${formatSymphonyStatusError(controlStateErrors[0])})`
2068
+ : controlStateSources.has('pod')
2069
+ ? 'Pod control state: active.'
2070
+ : 'Pod control state: portable local archive mode.',
1957
2071
  'Skills: issue triage, existing issue lookup, create/update/ask decision, task split, worker dispatch, status/report tracking.',
1958
2072
  'Delegation target: AI Secretary must choose a Chat resource before dispatch.',
1959
2073
  'Allowed targets: personal AI contact chat or group chat.',
1960
2074
  'Thread role: concrete work timeline under the selected Chat.',
1961
2075
  'Session role: backend runtime lifecycle only.',
1962
2076
  ];
1963
- if (projectionErrors.length > 0) {
1964
- lines.push('Fallback: showing local Symphony archive while Pod projection is unavailable.');
1965
- }
1966
2077
  for (const issue of issues.slice(0, 5)) {
1967
2078
  lines.push(` - ${formatSymphonyIssueStatus(issue)}`);
1968
2079
  }
@@ -2017,19 +2128,24 @@ async function withSymphonyStatusTimeout(interactive, label, task) {
2017
2128
  }
2018
2129
  }
2019
2130
  async function listOpenSymphonyIssues(interactive) {
2020
- const projectionRuntime = interactive?.__linxSymphonyPodProjectionRuntime;
2021
- let projectionError;
2131
+ const controlRuntime = interactive?.__linxSymphonyPodProjectionRuntime;
2022
2132
  try {
2023
- if (projectionRuntime?.issueResource) {
2024
- const podIssues = await withSymphonyStatusTimeout(interactive, 'Symphony Pod issue status', listOpenSymphonyIssuesFromPod({ runtime: projectionRuntime }));
2133
+ if (controlRuntime?.issueResource) {
2134
+ const podIssues = await withSymphonyStatusTimeout(interactive, 'Symphony Pod issue status', listOpenSymphonyIssuesFromPod({ runtime: controlRuntime }));
2025
2135
  if (podIssues) {
2026
2136
  return { items: podIssues, source: 'pod' };
2027
2137
  }
2028
2138
  }
2029
2139
  }
2030
2140
  catch (error) {
2031
- projectionError = error instanceof Error ? error.message : String(error);
2032
- // Fall back to local no-Pod archive below.
2141
+ return { items: [], source: 'none', error: error instanceof Error ? error.message : String(error) };
2142
+ }
2143
+ if (controlRuntime?.issueResource) {
2144
+ return {
2145
+ items: [],
2146
+ source: 'none',
2147
+ error: 'No active Pod session; Symphony control-plane state is Pod-authoritative.',
2148
+ };
2033
2149
  }
2034
2150
  try {
2035
2151
  const issues = typeof interactive?.__linxListSymphonyIssues === 'function'
@@ -2038,26 +2154,31 @@ async function listOpenSymphonyIssues(interactive) {
2038
2154
  return {
2039
2155
  items: issues.filter((issue) => issue.status !== 'closed' && issue.status !== 'resolved'),
2040
2156
  source: 'local',
2041
- ...(projectionError ? { error: projectionError } : {}),
2042
2157
  };
2043
2158
  }
2044
2159
  catch {
2045
- return { items: [], source: 'none', ...(projectionError ? { error: projectionError } : {}) };
2160
+ return { items: [], source: 'none' };
2046
2161
  }
2047
2162
  }
2048
2163
  async function listRunningSymphonyWorkers(interactive) {
2049
- const projectionRuntime = interactive?.__linxSymphonyPodProjectionRuntime;
2050
- let projectionError;
2164
+ const controlRuntime = interactive?.__linxSymphonyPodProjectionRuntime;
2051
2165
  try {
2052
- if (projectionRuntime?.sessionResource) {
2053
- const podWorkers = await withSymphonyStatusTimeout(interactive, 'Symphony Pod worker status', listRunningSymphonyWorkersFromPod({ runtime: projectionRuntime }));
2166
+ if (controlRuntime?.sessionResource) {
2167
+ const podWorkers = await withSymphonyStatusTimeout(interactive, 'Symphony Pod worker status', listRunningSymphonyWorkersFromPod({ runtime: controlRuntime }));
2054
2168
  if (podWorkers) {
2055
2169
  return { items: podWorkers, source: 'pod' };
2056
2170
  }
2057
2171
  }
2058
2172
  }
2059
2173
  catch (error) {
2060
- projectionError = error instanceof Error ? error.message : String(error);
2174
+ return { items: [], source: 'none', error: error instanceof Error ? error.message : String(error) };
2175
+ }
2176
+ if (controlRuntime?.sessionResource) {
2177
+ return {
2178
+ items: [],
2179
+ source: 'none',
2180
+ error: 'No active Pod session; Symphony control-plane state is Pod-authoritative.',
2181
+ };
2061
2182
  }
2062
2183
  try {
2063
2184
  if (typeof interactive?.__linxListSymphonySessions === 'function') {
@@ -2065,27 +2186,24 @@ async function listRunningSymphonyWorkers(interactive) {
2065
2186
  return {
2066
2187
  items: sessions.filter((session) => session.status === 'running'),
2067
2188
  source: 'local',
2068
- ...(projectionError ? { error: projectionError } : {}),
2069
2189
  };
2070
2190
  }
2071
2191
  return {
2072
2192
  items: listSymphonySessions()
2073
2193
  .filter((session) => session.status === 'running'),
2074
2194
  source: 'local',
2075
- ...(projectionError ? { error: projectionError } : {}),
2076
2195
  };
2077
2196
  }
2078
2197
  catch {
2079
- return { items: [], source: 'none', ...(projectionError ? { error: projectionError } : {}) };
2198
+ return { items: [], source: 'none' };
2080
2199
  }
2081
2200
  }
2082
2201
  async function listRecentSymphonyReports(interactive) {
2083
- const projectionRuntime = interactive?.__linxSymphonyPodProjectionRuntime;
2084
- let projectionError;
2202
+ const controlRuntime = interactive?.__linxSymphonyPodProjectionRuntime;
2085
2203
  try {
2086
- if (projectionRuntime?.deliveryResource) {
2204
+ if (controlRuntime?.deliveryResource) {
2087
2205
  const podReports = await withSymphonyStatusTimeout(interactive, 'Symphony Pod report status', listRecentSymphonyReportsFromPod({
2088
- runtime: projectionRuntime,
2206
+ runtime: controlRuntime,
2089
2207
  limit: 5,
2090
2208
  }));
2091
2209
  if (podReports) {
@@ -2094,8 +2212,14 @@ async function listRecentSymphonyReports(interactive) {
2094
2212
  }
2095
2213
  }
2096
2214
  catch (error) {
2097
- projectionError = error instanceof Error ? error.message : String(error);
2098
- // Fall back to local no-Pod archive below.
2215
+ return { items: [], source: 'none', error: error instanceof Error ? error.message : String(error) };
2216
+ }
2217
+ if (controlRuntime?.deliveryResource) {
2218
+ return {
2219
+ items: [],
2220
+ source: 'none',
2221
+ error: 'No active Pod session; Symphony control-plane state is Pod-authoritative.',
2222
+ };
2099
2223
  }
2100
2224
  try {
2101
2225
  const sessions = typeof interactive?.__linxListSymphonySessions === 'function'
@@ -2106,11 +2230,10 @@ async function listRecentSymphonyReports(interactive) {
2106
2230
  .filter((session) => session.status === 'completed' || session.status === 'failed')
2107
2231
  .slice(0, 5),
2108
2232
  source: 'local',
2109
- ...(projectionError ? { error: projectionError } : {}),
2110
2233
  };
2111
2234
  }
2112
2235
  catch {
2113
- return { items: [], source: 'none', ...(projectionError ? { error: projectionError } : {}) };
2236
+ return { items: [], source: 'none' };
2114
2237
  }
2115
2238
  }
2116
2239
  function formatSymphonyWorkerStatus(session) {
@@ -2433,19 +2556,37 @@ export function installLinxEscapeInterrupt(interactive) {
2433
2556
  let currentOnEscape = isLinxEscapeInterruptWrapper(initialOnEscape)
2434
2557
  ? undefined
2435
2558
  : initialOnEscape;
2559
+ let lastIdleEscapeTime = 0;
2436
2560
  const linxEscapeInterrupt = function linxEscapeInterrupt() {
2437
2561
  const session = interactive?.session;
2438
2562
  if (handBackAutoControlOnInterrupt(interactive)) {
2563
+ lastIdleEscapeTime = 0;
2439
2564
  return;
2440
2565
  }
2441
2566
  if (session?.isBashRunning && typeof session.abortBash === 'function') {
2567
+ lastIdleEscapeTime = 0;
2442
2568
  void session.abortBash();
2443
2569
  return;
2444
2570
  }
2445
2571
  if (isLinxSessionRunning(interactive) && typeof session?.abort === 'function') {
2572
+ lastIdleEscapeTime = 0;
2446
2573
  void session.abort();
2447
2574
  return;
2448
2575
  }
2576
+ if (shouldHandleLinxIdleDoubleEscape(interactive)) {
2577
+ const now = Date.now();
2578
+ if (now - lastIdleEscapeTime < 500) {
2579
+ lastIdleEscapeTime = 0;
2580
+ void openInteractiveRewindFromEscape(interactive);
2581
+ }
2582
+ else {
2583
+ lastIdleEscapeTime = now;
2584
+ interactive?.showStatus?.('Press Escape again to rewind this session.');
2585
+ interactive?.ui?.requestRender?.();
2586
+ }
2587
+ return;
2588
+ }
2589
+ lastIdleEscapeTime = 0;
2449
2590
  currentOnEscape?.call(editor);
2450
2591
  };
2451
2592
  Object.defineProperty(linxEscapeInterrupt, '__linxEscapeInterruptWrapper', {
@@ -2466,6 +2607,21 @@ export function installLinxEscapeInterrupt(interactive) {
2466
2607
  installLinxClearInterrupt(interactive, editor);
2467
2608
  editor.__linxEscapeInterruptInstalled = true;
2468
2609
  }
2610
+ function shouldHandleLinxIdleDoubleEscape(interactive) {
2611
+ if (typeof interactive?.editor?.getText !== 'function') {
2612
+ return false;
2613
+ }
2614
+ const text = String(interactive.editor.getText() ?? '');
2615
+ return text.trim().length === 0;
2616
+ }
2617
+ async function openInteractiveRewindFromEscape(interactive) {
2618
+ try {
2619
+ await handleInteractiveRewindSelector(interactive, interactive?.runtime);
2620
+ }
2621
+ catch (error) {
2622
+ interactive?.showError?.(error instanceof Error ? error.message : String(error));
2623
+ }
2624
+ }
2469
2625
  function isLinxEscapeInterruptWrapper(value) {
2470
2626
  return typeof value === 'function'
2471
2627
  && value.__linxEscapeInterruptWrapper === true;
@@ -2711,12 +2867,38 @@ function patchPiFooter() {
2711
2867
  if (Array.isArray(lines) && lines.length > 1 && typeof lines[1] === 'string') {
2712
2868
  const session = this.session;
2713
2869
  const autoCompactEnabled = this.autoCompactEnabled !== false;
2714
- lines[1] = buildLinxFooterStatusLine(session, width, autoCompactEnabled);
2870
+ const footerData = this.footerData;
2871
+ const modePrefix = buildLinxFooterModePrefix();
2872
+ const modeLen = visibleWidth(modePrefix);
2873
+ const bulletLen = modeLen > 0 ? 3 : 0;
2874
+ const statusWidth = Math.max(0, width - modeLen - bulletLen);
2875
+ lines[1] = buildLinxFooterStatusLine({
2876
+ session,
2877
+ width: statusWidth,
2878
+ autoCompactEnabled,
2879
+ footerData: footerData,
2880
+ });
2881
+ if (modePrefix) {
2882
+ lines[1] = modePrefix + ' • ' + lines[1];
2883
+ }
2715
2884
  }
2716
2885
  return lines;
2717
2886
  };
2718
2887
  footerPatched = true;
2719
2888
  }
2889
+ function buildLinxFooterModePrefix() {
2890
+ if (!_linxFooterInteractive)
2891
+ return '';
2892
+ const autoOn = _linxFooterInteractive.__autoEnabled === true;
2893
+ const symphonyOn = _linxFooterInteractive.__linxSymphonyModeEnabled === true;
2894
+ if (!autoOn && !symphonyOn)
2895
+ return '';
2896
+ if (autoOn && symphonyOn)
2897
+ return 'Symphony · Auto';
2898
+ if (autoOn)
2899
+ return 'Auto';
2900
+ return 'Symphony';
2901
+ }
2720
2902
  export function patchPiAssistantMessageRendering() {
2721
2903
  if (assistantMessagePatched) {
2722
2904
  return;
@@ -2747,91 +2929,4 @@ function isLinxHiddenAssistantContentPart(part) {
2747
2929
  function isRecord(value) {
2748
2930
  return typeof value === 'object' && value !== null;
2749
2931
  }
2750
- function buildLinxFooterStatusLine(session, width, autoCompactEnabled) {
2751
- const usage = calculateSessionUsage(session);
2752
- const state = session?.state ?? {};
2753
- const model = state.model ?? {};
2754
- const parts = [];
2755
- if (usage.input > 0) {
2756
- parts.push(`↑${formatTokenCount(usage.input)}`);
2757
- }
2758
- if (usage.output > 0) {
2759
- parts.push(`↓${formatTokenCount(usage.output)}`);
2760
- }
2761
- parts.push(formatContextUsage(session, model, autoCompactEnabled));
2762
- if (usage.cacheRate !== null) {
2763
- parts.push(`cache ${usage.cacheRate}%`);
2764
- }
2765
- parts.push(typeof model.id === 'string' && model.id ? model.id : 'no-model');
2766
- if (model.reasoning) {
2767
- const thinkingLevel = typeof state.thinkingLevel === 'string' && state.thinkingLevel
2768
- ? state.thinkingLevel
2769
- : 'off';
2770
- parts.push(thinkingLevel === 'off' ? 'thinking off' : thinkingLevel);
2771
- }
2772
- return fitFooterLine(parts.join(' • '), width);
2773
- }
2774
- function fitFooterLine(line, width) {
2775
- const truncated = truncateToWidth(line, width);
2776
- const visible = visibleWidth(truncated);
2777
- const padded = visible < width ? `${truncated}${' '.repeat(width - visible)}` : truncated;
2778
- return `\x1b[2m${padded}\x1b[22m`;
2779
- }
2780
- function calculateSessionUsage(session) {
2781
- const entries = session?.sessionManager?.getEntries?.();
2782
- if (!Array.isArray(entries)) {
2783
- return { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, cacheRate: null };
2784
- }
2785
- let input = 0;
2786
- let output = 0;
2787
- let cacheRead = 0;
2788
- let cacheWrite = 0;
2789
- for (const entry of entries) {
2790
- const message = entry?.type === 'message' ? entry.message : undefined;
2791
- if (message?.role !== 'assistant' || !message.usage) {
2792
- continue;
2793
- }
2794
- input += safeTokenCount(message.usage.input);
2795
- output += safeTokenCount(message.usage.output);
2796
- cacheRead += safeTokenCount(message.usage.cacheRead);
2797
- cacheWrite += safeTokenCount(message.usage.cacheWrite);
2798
- }
2799
- const totalPromptTokens = input + cacheRead + cacheWrite;
2800
- if (totalPromptTokens <= 0) {
2801
- return { input, output, cacheRead, cacheWrite, cacheRate: null };
2802
- }
2803
- return {
2804
- input,
2805
- output,
2806
- cacheRead,
2807
- cacheWrite,
2808
- cacheRate: Math.round((cacheRead / totalPromptTokens) * 100),
2809
- };
2810
- }
2811
- function formatContextUsage(session, model, autoCompactEnabled) {
2812
- const contextUsage = session?.getContextUsage?.();
2813
- const contextWindow = safeTokenCount(contextUsage?.contextWindow) || safeTokenCount(model.contextWindow);
2814
- const percent = typeof contextUsage?.percent === 'number' && Number.isFinite(contextUsage.percent)
2815
- ? `${contextUsage.percent.toFixed(1)}%`
2816
- : '?';
2817
- return `${percent}/${formatTokenCount(contextWindow)}${autoCompactEnabled ? ' (auto)' : ''}`;
2818
- }
2819
- function formatTokenCount(count) {
2820
- if (count < 1000) {
2821
- return count.toString();
2822
- }
2823
- if (count < 10000) {
2824
- return `${(count / 1000).toFixed(1)}k`;
2825
- }
2826
- if (count < 1000000) {
2827
- return `${Math.round(count / 1000)}k`;
2828
- }
2829
- if (count < 10000000) {
2830
- return `${(count / 1000000).toFixed(1)}M`;
2831
- }
2832
- return `${Math.round(count / 1000000)}M`;
2833
- }
2834
- function safeTokenCount(value) {
2835
- return typeof value === 'number' && Number.isFinite(value) && value > 0 ? value : 0;
2836
- }
2837
2932
  //# sourceMappingURL=interactive.js.map