@undefineds.co/linx 0.3.20 → 0.3.23

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 (99) hide show
  1. package/dist/generated/version.js +1 -1
  2. package/dist/index.js +6 -1
  3. package/dist/index.js.map +1 -1
  4. package/dist/lib/auto-mode/pod-persistence.js +53 -3
  5. package/dist/lib/auto-mode/pod-persistence.js.map +1 -1
  6. package/dist/lib/auto-mode/secretary.js +2 -2
  7. package/dist/lib/auto-mode/secretary.js.map +1 -1
  8. package/dist/lib/chat-api.js +23 -61
  9. package/dist/lib/chat-api.js.map +1 -1
  10. package/dist/lib/codex-plugin/index.js +1 -0
  11. package/dist/lib/codex-plugin/index.js.map +1 -1
  12. package/dist/lib/codex-plugin/symphony-mcp.js +335 -0
  13. package/dist/lib/codex-plugin/symphony-mcp.js.map +1 -0
  14. package/dist/lib/linx-cloud-errors.js +0 -5
  15. package/dist/lib/linx-cloud-errors.js.map +1 -1
  16. package/dist/lib/linx-status-line.js +1 -8
  17. package/dist/lib/linx-status-line.js.map +1 -1
  18. package/dist/lib/linx-tui-contract.js +2 -1
  19. package/dist/lib/linx-tui-contract.js.map +1 -1
  20. package/dist/lib/models.js +3 -2
  21. package/dist/lib/models.js.map +1 -1
  22. package/dist/lib/pi-adapter/auth.js +68 -0
  23. package/dist/lib/pi-adapter/auth.js.map +1 -0
  24. package/dist/lib/pi-adapter/branding.js +67 -110
  25. package/dist/lib/pi-adapter/branding.js.map +1 -1
  26. package/dist/lib/pi-adapter/interactive.js +341 -101
  27. package/dist/lib/pi-adapter/interactive.js.map +1 -1
  28. package/dist/lib/pi-adapter/pod-mirror.js +38 -107
  29. package/dist/lib/pi-adapter/pod-mirror.js.map +1 -1
  30. package/dist/lib/pi-adapter/pod-native.js +2 -0
  31. package/dist/lib/pi-adapter/pod-native.js.map +1 -1
  32. package/dist/lib/pi-adapter/pod-tools.js +140 -0
  33. package/dist/lib/pi-adapter/pod-tools.js.map +1 -0
  34. package/dist/lib/pi-adapter/runtime.js +2 -12
  35. package/dist/lib/pi-adapter/runtime.js.map +1 -1
  36. package/dist/lib/pi-adapter/session.js +13 -17
  37. package/dist/lib/pi-adapter/session.js.map +1 -1
  38. package/dist/lib/pi-adapter/stream.js +2 -20
  39. package/dist/lib/pi-adapter/stream.js.map +1 -1
  40. package/dist/lib/pod-chat-store.js +53 -4
  41. package/dist/lib/pod-chat-store.js.map +1 -1
  42. package/dist/lib/resource-identity.js +2 -0
  43. package/dist/lib/resource-identity.js.map +1 -0
  44. package/dist/lib/status-line-command.js +2 -2
  45. package/dist/lib/status-line-command.js.map +1 -1
  46. package/dist/lib/symphony/archive.js +15 -37
  47. package/dist/lib/symphony/archive.js.map +1 -1
  48. package/dist/lib/symphony/pod-projection.js +189 -1346
  49. package/dist/lib/symphony/pod-projection.js.map +1 -1
  50. package/dist/lib/symphony-command.js +209 -109
  51. package/dist/lib/symphony-command.js.map +1 -1
  52. package/dist/plugins/linx-symphony-codex/.codex-plugin/plugin.json +38 -0
  53. package/dist/plugins/linx-symphony-codex/.mcp.json +10 -0
  54. package/dist/plugins/linx-symphony-codex/README.md +9 -0
  55. package/dist/plugins/linx-symphony-codex/hooks.json +60 -0
  56. package/dist/plugins/linx-symphony-codex/scripts/symphony-hook-events.mjs +119 -0
  57. package/dist/plugins/linx-symphony-codex/scripts/symphony-mcp.mjs +335 -0
  58. package/dist/plugins/linx-symphony-codex/skills/symphony/SKILL.md +791 -0
  59. package/dist/skills/symphony/SKILL.md +7 -0
  60. package/dist/skills/xpod-cli/SKILL.md +2 -13
  61. package/package.json +4 -4
  62. package/vendor/agent-runtime/dist/chat-reconciler.d.ts +33 -0
  63. package/vendor/agent-runtime/dist/chat-reconciler.js +108 -0
  64. package/vendor/agent-runtime/dist/index.d.ts +4 -1
  65. package/vendor/agent-runtime/dist/index.js +4 -1
  66. package/vendor/agent-runtime/dist/matrix-client.d.ts +149 -0
  67. package/vendor/agent-runtime/dist/matrix-client.js +220 -0
  68. package/vendor/agent-runtime/dist/pod-resource-identity.d.ts +17 -0
  69. package/vendor/agent-runtime/dist/pod-resource-identity.js +54 -0
  70. package/vendor/agent-runtime/dist/reconciler.d.ts +0 -11
  71. package/vendor/agent-runtime/dist/reconciler.js +5 -43
  72. package/vendor/agent-runtime/dist/symphony.d.ts +272 -27
  73. package/vendor/agent-runtime/dist/symphony.js +1268 -21
  74. package/vendor/agent-runtime/dist/workspace.d.ts +61 -0
  75. package/vendor/agent-runtime/dist/workspace.js +81 -0
  76. package/vendor/agent-runtime/package.json +5 -1
  77. package/vendor/stores/dist/current-pod-base.d.ts +2 -0
  78. package/vendor/stores/dist/current-pod-base.js +14 -0
  79. package/vendor/stores/dist/exact-records.d.ts +7 -0
  80. package/vendor/stores/dist/exact-records.js +87 -0
  81. package/vendor/stores/dist/index.d.ts +1 -0
  82. package/vendor/stores/dist/index.js +1 -0
  83. package/vendor/stores/dist/login.d.ts +51 -0
  84. package/vendor/stores/dist/login.js +195 -0
  85. package/vendor/stores/dist/pod-collection.d.ts +28 -0
  86. package/vendor/stores/dist/pod-collection.js +194 -0
  87. package/vendor/stores/dist/pod-write-guard.d.ts +5 -0
  88. package/vendor/stores/dist/pod-write-guard.js +133 -0
  89. package/vendor/stores/dist/symphony-control.d.ts +245 -0
  90. package/vendor/stores/dist/symphony-control.js +2175 -0
  91. package/vendor/stores/package.json +14 -0
  92. package/dist/lib/capture/persistence.js +0 -377
  93. package/dist/lib/capture/persistence.js.map +0 -1
  94. package/dist/lib/capture/tool.js +0 -242
  95. package/dist/lib/capture/tool.js.map +0 -1
  96. package/dist/skills/basic/SKILL.md +0 -46
  97. package/dist/skills/capture/SKILL.md +0 -165
  98. package/vendor/agent-runtime/dist/coordination.d.ts +0 -93
  99. package/vendor/agent-runtime/dist/coordination.js +0 -145
@@ -8,7 +8,7 @@ import { listArchivedAutoModeSessions, runAutoMode } from '../auto-mode/runner.j
8
8
  import { resolveAutoModeCommandRoute, } from '../../../vendor/agent-runtime/dist/auto-mode.js';
9
9
  import { getAIConfigProviderCatalog, getAIConfigProviderMetadata } from '../models.js';
10
10
  import { runSymphony } from '../symphony-command.js';
11
- import { applyLinxInteractiveBranding, requestLinxCloudLogin } from './branding.js';
11
+ import { applyLinxInteractiveBranding, checkAndShowLinxUpdate, requestLinxCloudLogin } from './branding.js';
12
12
  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';
@@ -16,8 +16,8 @@ import { getSecretaryAutoInputController } from './auto-input-controller.js';
16
16
  import { createSymphonyIdeaRecord, listSymphonyIssues, listSymphonySessions, } from '../symphony/archive.js';
17
17
  import { listOpenSymphonyIssuesFromPod, listRecentSymphonyReportsFromPod, listRunningSymphonyWorkersFromPod, mirrorSymphonyProjectionJsonLdFromPod, persistSymphonyIdeaToPod, persistSymphonyControlStateToPod, } from '../symphony/pod-projection.js';
18
18
  import { getSessionControlManager, installSessionControlRuntimeEventBridge, } from './session-control.js';
19
- import { buildLinxTuiStatusLine, calculateSessionUsage, DEFAULT_STATUS_LINE_TOKENS, formatTokenCount, LINX_STATUS_LINE_TOKEN_NAMES, parseLinxStatusLineColorArg, parseLinxStatusLineTokenArgs, readLinxStatusLineConfig, resetLinxStatusLineConfig, writeLinxStatusLineConfigPatch, } from '../linx-status-line.js';
20
- let statusLinePatched = false;
19
+ import { buildLinxFooterStatusLine, calculateSessionUsage, DEFAULT_STATUS_LINE_TOKENS, formatTokenCount, LINX_STATUS_LINE_TOKEN_NAMES, parseLinxStatusLineColorArg, parseLinxStatusLineTokenArgs, readLinxStatusLineConfig, resetLinxStatusLineConfig, writeLinxStatusLineConfigPatch, } from '../linx-status-line.js';
20
+ let footerPatched = false;
21
21
  let assistantMessagePatched = false;
22
22
  let linxResumeOutputStyleRestore = null;
23
23
  const BACKEND_OWNED_SLASH_COMMANDS = new Set([
@@ -29,7 +29,6 @@ const BACKEND_OWNED_SLASH_COMMANDS = new Set([
29
29
  const SYMPHONY_STATUS_POD_TIMEOUT_MS = 1_200;
30
30
  const DEFAULT_SYMPHONY_WORKER_SUPERVISOR_INTERVAL_MS = 10 * 60 * 1000;
31
31
  const CODEX_STYLE_STATUS_LINE_TOKENS = [
32
- 'mode',
33
32
  'model-with-reasoning',
34
33
  'git-branch',
35
34
  'context-remaining',
@@ -38,16 +37,20 @@ const CODEX_STYLE_STATUS_LINE_TOKENS = [
38
37
  'current-dir',
39
38
  ];
40
39
  const COMPACT_STATUS_LINE_TOKENS = [
41
- 'mode',
42
40
  'model-with-reasoning',
43
41
  'context-remaining',
44
42
  'current-dir',
45
43
  ];
46
- /** Module-level reference to interactive for status line mode state (set during bootstrap). */
47
- let _linxStatusLineInteractive = null;
44
+ const STATUS_LINE_CODEX_PRESET_OPTION = 'Preset: Codex-style';
45
+ const STATUS_LINE_COMPACT_PRESET_OPTION = 'Preset: Compact';
46
+ const STATUS_LINE_TOGGLE_COLORS_OPTION = 'Toggle colors';
47
+ const STATUS_LINE_RESET_OPTION = 'Reset to default';
48
+ const STATUS_LINE_DONE_OPTION = 'Done';
49
+ /** Module-level reference to interactive for footer mode state (set during bootstrap). */
50
+ let _linxFooterInteractive = null;
48
51
  export function bootstrapLinxInteractiveMode(runtime, options = {}) {
49
52
  installLinxResumeOutputStyle();
50
- patchPiStatusLine();
53
+ patchPiFooter();
51
54
  patchPiAssistantMessageRendering();
52
55
  const sessionCwd = runtime?.cwd || process.cwd();
53
56
  ensureInteractiveRuntimeHost(runtime);
@@ -55,7 +58,7 @@ export function bootstrapLinxInteractiveMode(runtime, options = {}) {
55
58
  interactive.runtime = runtime;
56
59
  interactive.__autoEnabled = runtime?.autoEnabled === true;
57
60
  interactive.__linxSymphonyModeEnabled = runtime?.symphonyEnabled === true;
58
- _linxStatusLineInteractive = interactive;
61
+ _linxFooterInteractive = interactive;
59
62
  if (options.onSymphonyControlChange) {
60
63
  ;
61
64
  interactive.__linxOnSymphonyControlChange = options.onSymphonyControlChange;
@@ -556,6 +559,9 @@ function parseLinxGlobalCommand(input) {
556
559
  : input.slice('/status-line'.length).trim();
557
560
  return { action: 'statusline', args: splitInteractiveCommandArgs(body) };
558
561
  }
562
+ if (input === '/update' || input === '/upgrade') {
563
+ return { action: 'update' };
564
+ }
559
565
  if (input === '/rewind') {
560
566
  return { action: 'rewind-select' };
561
567
  }
@@ -681,6 +687,10 @@ async function handleLinxGlobalCommand(interactive, runtime, command) {
681
687
  await handleInteractiveStatusLineCommand(interactive, command.args);
682
688
  return;
683
689
  }
690
+ if (command.action === 'update') {
691
+ await checkAndShowLinxUpdate(interactive, { manual: true });
692
+ return;
693
+ }
684
694
  if (command.action === 'rewind-select') {
685
695
  await handleInteractiveRewindSelector(interactive, runtime);
686
696
  return;
@@ -697,53 +707,133 @@ async function handleInteractiveStatusLineCommand(interactive, args) {
697
707
  return;
698
708
  }
699
709
  const summary = formatInteractiveStatusLineSummary();
700
- if (typeof interactive.showExtensionSelector !== 'function') {
701
- interactive.showStatus?.(`${summary} · Use /statusline set <tokens...>, /statusline tokens, /statusline colors <on|off>, or /statusline reset.`);
702
- interactive.ui?.requestRender?.();
710
+ if (typeof interactive.showSelector === 'function') {
711
+ await showInteractiveStatusLineMultiSelect(interactive);
703
712
  return;
704
713
  }
705
- const choice = await interactive.showExtensionSelector(`Status line\n${summary}`, [
706
- 'Use Codex-style preset',
707
- 'Use compact preset',
708
- 'Configure tokens manually',
709
- 'Toggle colors',
710
- 'Show available tokens',
711
- 'Reset to default',
712
- ]);
713
- if (choice === 'Use Codex-style preset') {
714
- writeInteractiveStatusLineConfig(interactive, {
715
- statusLine: CODEX_STYLE_STATUS_LINE_TOKENS,
716
- message: 'Status line set to Codex-style preset.',
717
- });
714
+ if (typeof interactive.showExtensionSelector === 'function') {
715
+ await showInteractiveStatusLineFallbackSelector(interactive);
718
716
  return;
719
717
  }
720
- if (choice === 'Use compact preset') {
721
- writeInteractiveStatusLineConfig(interactive, {
722
- statusLine: COMPACT_STATUS_LINE_TOKENS,
723
- message: 'Status line set to compact preset.',
724
- });
718
+ {
719
+ interactive.showStatus?.(`${summary} · Use /statusline set <tokens...>, /statusline tokens, /statusline colors <on|off>, or /statusline reset.`);
720
+ interactive.ui?.requestRender?.();
725
721
  return;
726
722
  }
727
- if (choice === 'Toggle colors') {
728
- const current = readLinxStatusLineConfig();
729
- writeInteractiveStatusLineConfig(interactive, {
730
- statusLineUseColors: !current.useColors,
731
- message: `Status line colors ${current.useColors ? 'disabled' : 'enabled'}.`,
732
- });
733
- return;
723
+ }
724
+ async function showInteractiveStatusLineFallbackSelector(interactive) {
725
+ while (true) {
726
+ const currentSummary = formatInteractiveStatusLineSummary();
727
+ const config = readLinxStatusLineConfig();
728
+ const options = buildInteractiveStatusLineOptions(config);
729
+ const choice = await interactive.showExtensionSelector(`Status line\n${currentSummary}`, options);
730
+ if (!choice || choice === STATUS_LINE_DONE_OPTION) {
731
+ return;
732
+ }
733
+ const token = parseInteractiveStatusLineTokenChoice(choice);
734
+ if (token) {
735
+ toggleInteractiveStatusLineToken(interactive, token);
736
+ continue;
737
+ }
738
+ if (choice === STATUS_LINE_CODEX_PRESET_OPTION) {
739
+ writeInteractiveStatusLineConfig(interactive, {
740
+ statusLine: CODEX_STYLE_STATUS_LINE_TOKENS,
741
+ message: 'Status line set to Codex-style preset.',
742
+ });
743
+ continue;
744
+ }
745
+ if (choice === STATUS_LINE_COMPACT_PRESET_OPTION) {
746
+ writeInteractiveStatusLineConfig(interactive, {
747
+ statusLine: COMPACT_STATUS_LINE_TOKENS,
748
+ message: 'Status line set to compact preset.',
749
+ });
750
+ continue;
751
+ }
752
+ if (choice === STATUS_LINE_TOGGLE_COLORS_OPTION) {
753
+ const current = readLinxStatusLineConfig();
754
+ writeInteractiveStatusLineConfig(interactive, {
755
+ statusLineUseColors: !current.useColors,
756
+ message: `Status line colors ${current.useColors ? 'disabled' : 'enabled'}.`,
757
+ });
758
+ continue;
759
+ }
760
+ if (choice === STATUS_LINE_RESET_OPTION) {
761
+ resetLinxStatusLineConfig();
762
+ finishInteractiveStatusLineUpdate(interactive, `Status line reset to default: ${DEFAULT_STATUS_LINE_TOKENS.join(', ')}`);
763
+ }
734
764
  }
735
- if (choice === 'Show available tokens') {
736
- showInteractiveStatusLineTokens(interactive);
737
- return;
765
+ }
766
+ async function showInteractiveStatusLineMultiSelect(interactive) {
767
+ await new Promise((resolvePromise, rejectPromise) => {
768
+ let resolved = false;
769
+ const resolveOnce = () => {
770
+ if (!resolved) {
771
+ resolved = true;
772
+ resolvePromise();
773
+ }
774
+ };
775
+ try {
776
+ interactive.showSelector((done) => {
777
+ const close = () => {
778
+ done();
779
+ resolveOnce();
780
+ };
781
+ const selector = new LinxStatusLineSelectorComponent(readLinxStatusLineConfig(), ({ tokens, useColors }) => {
782
+ writeInteractiveStatusLineConfig(interactive, {
783
+ statusLine: tokens,
784
+ statusLineUseColors: useColors,
785
+ message: `Status line updated: ${tokens.join(', ')}`,
786
+ });
787
+ close();
788
+ }, () => {
789
+ close();
790
+ interactive.ui?.requestRender?.();
791
+ });
792
+ return { component: selector, focus: selector.getList() };
793
+ });
794
+ }
795
+ catch (error) {
796
+ rejectPromise(error);
797
+ }
798
+ });
799
+ }
800
+ function buildInteractiveStatusLineOptions(config = readLinxStatusLineConfig()) {
801
+ const enabled = new Set(config.tokens);
802
+ return [
803
+ ...LINX_STATUS_LINE_TOKEN_NAMES.map((token) => `${enabled.has(token) ? '✓' : '○'} ${token}`),
804
+ STATUS_LINE_CODEX_PRESET_OPTION,
805
+ STATUS_LINE_COMPACT_PRESET_OPTION,
806
+ STATUS_LINE_TOGGLE_COLORS_OPTION,
807
+ STATUS_LINE_RESET_OPTION,
808
+ STATUS_LINE_DONE_OPTION,
809
+ ];
810
+ }
811
+ function parseInteractiveStatusLineTokenChoice(choice) {
812
+ if (typeof choice !== 'string') {
813
+ return null;
738
814
  }
739
- if (choice === 'Reset to default') {
740
- resetLinxStatusLineConfig();
741
- finishInteractiveStatusLineUpdate(interactive, `Status line reset to default: ${DEFAULT_STATUS_LINE_TOKENS.join(', ')}`);
742
- return;
815
+ const token = choice.replace(/^[✓○]\s*/u, '').trim();
816
+ if (!token) {
817
+ return null;
743
818
  }
744
- if (choice === 'Configure tokens manually') {
745
- await promptInteractiveStatusLineTokens(interactive);
819
+ return LINX_STATUS_LINE_TOKEN_NAMES.includes(token)
820
+ ? token
821
+ : null;
822
+ }
823
+ function toggleInteractiveStatusLineToken(interactive, token) {
824
+ const current = readLinxStatusLineConfig().tokens;
825
+ const exists = current.includes(token);
826
+ if (exists && current.length <= 1) {
827
+ interactive.showError?.('Status line needs at least one item.');
828
+ return;
746
829
  }
830
+ const next = exists
831
+ ? current.filter((item) => item !== token)
832
+ : [...current, token];
833
+ writeInteractiveStatusLineConfig(interactive, {
834
+ statusLine: next,
835
+ message: `Status line ${exists ? 'removed' : 'added'}: ${token}`,
836
+ });
747
837
  }
748
838
  function handleInteractiveStatusLineArgs(interactive, args) {
749
839
  const action = args[0]?.toLowerCase();
@@ -781,20 +871,6 @@ function handleInteractiveStatusLineArgs(interactive, args) {
781
871
  interactive.showError?.(`${message}. Use /statusline tokens to list valid tokens.`);
782
872
  }
783
873
  }
784
- async function promptInteractiveStatusLineTokens(interactive) {
785
- if (typeof interactive.showExtensionInput !== 'function') {
786
- interactive.showStatus?.(`Use /statusline set ${CODEX_STYLE_STATUS_LINE_TOKENS.join(' ')}`);
787
- interactive.ui?.requestRender?.();
788
- return;
789
- }
790
- const value = await interactive.showExtensionInput('Status line tokens', CODEX_STYLE_STATUS_LINE_TOKENS.join(' '));
791
- if (typeof value !== 'string' || !value.trim()) {
792
- interactive.showStatus?.('Status line unchanged.');
793
- interactive.ui?.requestRender?.();
794
- return;
795
- }
796
- handleInteractiveStatusLineArgs(interactive, ['set', ...splitInteractiveCommandArgs(value)]);
797
- }
798
874
  function writeInteractiveStatusLineConfig(interactive, patch) {
799
875
  writeLinxStatusLineConfigPatch({
800
876
  ...(patch.statusLine ? { statusLine: patch.statusLine } : {}),
@@ -891,15 +967,14 @@ async function handleInteractiveRewindTurnsCommand(interactive, runtime, turns)
891
967
  interactive.ui?.requestRender?.();
892
968
  }
893
969
  async function rewindSessionManagerBeforeUserEntry(interactive, runtime, session, sessionManager, entryId) {
894
- const previousBranch = getActiveSessionBranch(sessionManager);
895
- const entry = previousBranch.find((candidate) => String(candidate?.id) === entryId);
896
- if (!entry) {
897
- throw new Error('Cannot rewind: selected message is no longer in the active branch. Reopen /rewind and choose a current user turn.');
898
- }
899
- if (entry.type !== 'message' || entry.message?.role !== 'user') {
900
- throw new Error('Cannot rewind: selected message is not a user turn.');
970
+ const entry = typeof sessionManager?.getEntry === 'function'
971
+ ? sessionManager.getEntry(entryId)
972
+ : getActiveSessionBranch(sessionManager).find((candidate) => candidate?.id === entryId);
973
+ if (!entry || entry.type !== 'message' || entry.message?.role !== 'user') {
974
+ throw new Error('Cannot rewind: selected message is not a user turn in the active branch.');
901
975
  }
902
976
  const previousState = captureRewindSessionState(sessionManager);
977
+ const previousBranch = getActiveSessionBranch(sessionManager);
903
978
  await stopActiveSessionWorkForRewind(session);
904
979
  resetPendingAutoInputForRewind(interactive, runtime);
905
980
  const targetLeafId = typeof entry.parentId === 'string' && entry.parentId ? entry.parentId : null;
@@ -1162,6 +1237,178 @@ function refreshInteractiveTranscriptFromSessionManager(interactive) {
1162
1237
  interactive?.showWarning?.(`Rewind transcript refresh failed: ${message}`);
1163
1238
  }
1164
1239
  }
1240
+ class LinxMultiSelectList {
1241
+ getRows;
1242
+ onToggle;
1243
+ onAction;
1244
+ onCancel;
1245
+ selectedIndex = 0;
1246
+ constructor(getRows, onToggle, onAction, onCancel) {
1247
+ this.getRows = getRows;
1248
+ this.onToggle = onToggle;
1249
+ this.onAction = onAction;
1250
+ this.onCancel = onCancel;
1251
+ }
1252
+ invalidate() {
1253
+ // No cached render state.
1254
+ }
1255
+ render(width) {
1256
+ const rows = this.normalizedRows();
1257
+ if (rows.length === 0) {
1258
+ return [' No options'];
1259
+ }
1260
+ const lines = [];
1261
+ for (let index = 0; index < rows.length; index += 1) {
1262
+ const row = rows[index];
1263
+ const cursor = index === this.selectedIndex ? '> ' : ' ';
1264
+ const label = row.kind === 'item'
1265
+ ? `${row.selected ? '✓' : '○'} ${row.label}`
1266
+ : row.label;
1267
+ lines.push(`${cursor}${truncateToWidth(label, Math.max(1, width - 2))}`);
1268
+ if (row.description) {
1269
+ lines.push(` ${truncateToWidth(row.description, Math.max(1, width - 2))}`);
1270
+ }
1271
+ }
1272
+ lines.push('');
1273
+ lines.push('↑↓ navigate Enter toggles items/actions Escape/Ctrl+C cancel');
1274
+ return lines;
1275
+ }
1276
+ handleInput(keyData) {
1277
+ const rows = this.normalizedRows();
1278
+ if (rows.length === 0) {
1279
+ return;
1280
+ }
1281
+ const keybindings = getKeybindings();
1282
+ if (keybindings.matches(keyData, 'tui.select.up')) {
1283
+ this.selectedIndex = this.selectedIndex === 0 ? rows.length - 1 : this.selectedIndex - 1;
1284
+ return;
1285
+ }
1286
+ if (keybindings.matches(keyData, 'tui.select.down')) {
1287
+ this.selectedIndex = this.selectedIndex === rows.length - 1 ? 0 : this.selectedIndex + 1;
1288
+ return;
1289
+ }
1290
+ if (keybindings.matches(keyData, 'tui.select.confirm')) {
1291
+ const selected = rows[this.selectedIndex];
1292
+ if (selected?.kind === 'item') {
1293
+ this.onToggle(selected.id);
1294
+ }
1295
+ else if (selected?.kind === 'action') {
1296
+ this.onAction(selected.id);
1297
+ }
1298
+ return;
1299
+ }
1300
+ if (keybindings.matches(keyData, 'tui.select.cancel')) {
1301
+ this.onCancel();
1302
+ }
1303
+ }
1304
+ normalizedRows() {
1305
+ const rows = this.getRows();
1306
+ if (this.selectedIndex >= rows.length) {
1307
+ this.selectedIndex = Math.max(0, rows.length - 1);
1308
+ }
1309
+ return rows;
1310
+ }
1311
+ }
1312
+ class LinxStatusLineSelectorComponent extends Container {
1313
+ list;
1314
+ draftTokens;
1315
+ draftUseColors;
1316
+ notice = null;
1317
+ constructor(config, onCommit, onCancel) {
1318
+ super();
1319
+ this.draftTokens = [...config.tokens];
1320
+ this.draftUseColors = config.useColors;
1321
+ this.addChild(new Spacer(1));
1322
+ this.addChild(new Text('Status line', 1, 0));
1323
+ this.addChild(new Text('Select the items that appear in the bottom TUI status line.', 1, 0));
1324
+ this.addChild(new Text(`Current source: tokens ${config.tokenSource}, colors ${config.colorSource}.`, 1, 0));
1325
+ this.addChild(new Spacer(1));
1326
+ this.list = new LinxMultiSelectList(() => this.rows(), (id) => this.toggleToken(id), (id) => this.handleAction(id, onCommit), onCancel);
1327
+ this.addChild(this.list);
1328
+ }
1329
+ getList() {
1330
+ return this.list;
1331
+ }
1332
+ rows() {
1333
+ const enabled = new Set(this.draftTokens);
1334
+ const rows = LINX_STATUS_LINE_TOKEN_NAMES.map((token) => ({
1335
+ kind: 'item',
1336
+ id: token,
1337
+ label: token,
1338
+ selected: enabled.has(token),
1339
+ }));
1340
+ rows.push({
1341
+ kind: 'action',
1342
+ id: 'preset-codex',
1343
+ label: STATUS_LINE_CODEX_PRESET_OPTION,
1344
+ description: CODEX_STYLE_STATUS_LINE_TOKENS.join(', '),
1345
+ }, {
1346
+ kind: 'action',
1347
+ id: 'preset-compact',
1348
+ label: STATUS_LINE_COMPACT_PRESET_OPTION,
1349
+ description: COMPACT_STATUS_LINE_TOKENS.join(', '),
1350
+ }, {
1351
+ kind: 'action',
1352
+ id: 'toggle-colors',
1353
+ label: `${STATUS_LINE_TOGGLE_COLORS_OPTION}: ${this.draftUseColors ? 'on' : 'off'}`,
1354
+ }, {
1355
+ kind: 'action',
1356
+ id: 'reset',
1357
+ label: STATUS_LINE_RESET_OPTION,
1358
+ description: DEFAULT_STATUS_LINE_TOKENS.join(', '),
1359
+ }, {
1360
+ kind: 'action',
1361
+ id: 'done',
1362
+ label: STATUS_LINE_DONE_OPTION,
1363
+ description: this.notice ?? 'Save changes and close.',
1364
+ });
1365
+ return rows;
1366
+ }
1367
+ toggleToken(id) {
1368
+ const token = id;
1369
+ if (!LINX_STATUS_LINE_TOKEN_NAMES.includes(token)) {
1370
+ return;
1371
+ }
1372
+ const exists = this.draftTokens.includes(token);
1373
+ if (exists && this.draftTokens.length <= 1) {
1374
+ this.notice = 'Status line needs at least one item.';
1375
+ return;
1376
+ }
1377
+ this.draftTokens = exists
1378
+ ? this.draftTokens.filter((item) => item !== token)
1379
+ : [...this.draftTokens, token];
1380
+ this.notice = null;
1381
+ }
1382
+ handleAction(id, onCommit) {
1383
+ if (id === 'preset-codex') {
1384
+ this.draftTokens = [...CODEX_STYLE_STATUS_LINE_TOKENS];
1385
+ this.notice = 'Draft changed to Codex-style preset.';
1386
+ return;
1387
+ }
1388
+ if (id === 'preset-compact') {
1389
+ this.draftTokens = [...COMPACT_STATUS_LINE_TOKENS];
1390
+ this.notice = 'Draft changed to compact preset.';
1391
+ return;
1392
+ }
1393
+ if (id === 'toggle-colors') {
1394
+ this.draftUseColors = !this.draftUseColors;
1395
+ this.notice = `Draft colors ${this.draftUseColors ? 'enabled' : 'disabled'}.`;
1396
+ return;
1397
+ }
1398
+ if (id === 'reset') {
1399
+ this.draftTokens = [...DEFAULT_STATUS_LINE_TOKENS];
1400
+ this.draftUseColors = true;
1401
+ this.notice = 'Draft reset to default.';
1402
+ return;
1403
+ }
1404
+ if (id === 'done') {
1405
+ onCommit({
1406
+ tokens: this.draftTokens,
1407
+ useColors: this.draftUseColors,
1408
+ });
1409
+ }
1410
+ }
1411
+ }
1165
1412
  function collectRewindUserMessages(_session, sessionManager) {
1166
1413
  return getActiveSessionBranch(sessionManager)
1167
1414
  .filter((entry) => entry?.type === 'message' && entry.message?.role === 'user')
@@ -1523,6 +1770,10 @@ const LINX_INTERACTIVE_SLASH_COMMANDS = [
1523
1770
  { value: 'reset', description: 'Restore default status line tokens' },
1524
1771
  ]),
1525
1772
  },
1773
+ {
1774
+ name: 'update',
1775
+ description: 'check for a LinX CLI update and install from the TUI',
1776
+ },
1526
1777
  {
1527
1778
  name: 'ai',
1528
1779
  argumentHint: 'connect <provider>',
@@ -1673,12 +1924,7 @@ function renderSymphonySecretaryProjection(input) {
1673
1924
  'Default response style: reply like normal chat.',
1674
1925
  '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.',
1675
1926
  'If the message is ordinary chat or early exploration, answer directly and do not explain that it was not delegated.',
1676
- 'If real delegation is needed, update the Symphony control state before describing the visible handoff result.',
1677
- 'When you need to inspect or mutate Symphony Pod resources from the AI side, use the xpod CLI as the direct Pod tool surface.',
1678
- 'Prefer model-backed xpod obj commands for Idea, Issue, Task, Delivery, Run, RunStep, Report, Evidence, ApprovalRequest, InputRequest, and InboxNotification resources.',
1679
- 'xpod uses the same Solid authority as LinX inside the Agent Runtime; do not ask the model to handle tokens or client secrets.',
1680
- 'Before mutating Pod resources from tools, verify xpod auth status/whoami reports the same acting WebID/Pod root as the LinX session; stop on mismatch.',
1681
- 'Do not hand-patch TTL or guess Pod paths for modeled product resources; use xpod/model descriptors or inspect existing links first.',
1927
+ 'If real delegation is needed, summarize the visible handoff result briefly after updating control state.',
1682
1928
  '',
1683
1929
  'User message:',
1684
1930
  input,
@@ -1727,9 +1973,8 @@ async function dispatchSymphonyWorkerFromInteractive(interactive, objective, sou
1727
1973
  target: {
1728
1974
  source: 'active-session',
1729
1975
  backend,
1730
- contact: backend,
1731
- agent: backend,
1732
- label: backend,
1976
+ agent: `${backend}-worker`,
1977
+ label: `${backend} worker`,
1733
1978
  ...(source?.chat ? { chat: source.chat } : {}),
1734
1979
  ...(source?.thread ? { thread: source.thread } : {}),
1735
1980
  },
@@ -2078,8 +2323,8 @@ async function formatSymphonyStatus(interactive) {
2078
2323
  ? 'Pod control state: active.'
2079
2324
  : 'Pod control state: portable local archive mode.',
2080
2325
  'Skills: issue triage, existing issue lookup, create/update/ask decision, task split, worker dispatch, status/report tracking.',
2081
- 'Delegation target: AI Secretary chooses a Contact-backed worker target before dispatch.',
2082
- 'Allowed targets: personal AI contact chat or group chat; Chat participants must reference Contacts.',
2326
+ 'Delegation target: AI Secretary must choose a Chat resource before dispatch.',
2327
+ 'Allowed targets: personal AI contact chat or group chat.',
2083
2328
  'Thread role: concrete work timeline under the selected Chat.',
2084
2329
  'Session role: backend runtime lifecycle only.',
2085
2330
  ];
@@ -2247,7 +2492,6 @@ async function listRecentSymphonyReports(interactive) {
2247
2492
  }
2248
2493
  function formatSymphonyWorkerStatus(session) {
2249
2494
  const target = session.target?.label
2250
- ?? session.target?.contact
2251
2495
  ?? session.target?.agent
2252
2496
  ?? session.target?.chat
2253
2497
  ?? session.backend;
@@ -2261,10 +2505,8 @@ function formatSymphonyWorkerStatus(session) {
2261
2505
  function formatSymphonyReportStatus(report) {
2262
2506
  const status = report.status;
2263
2507
  const reportRecord = report;
2264
- const target = reportRecord.target?.label
2265
- ?? reportRecord.contact
2266
- ?? reportRecord.target?.contact
2267
- ?? reportRecord.agent
2508
+ const target = reportRecord.agent
2509
+ ?? reportRecord.target?.label
2268
2510
  ?? reportRecord.target?.agent
2269
2511
  ?? report.backend;
2270
2512
  const title = 'summary' in report && report.summary
@@ -2869,42 +3111,40 @@ function isPotentialPiResumeOutput(text) {
2869
3111
  function stripAnsi(text) {
2870
3112
  return text.replace(/\x1b\[[0-9;]*m/gu, '');
2871
3113
  }
2872
- function patchPiStatusLine() {
2873
- if (statusLinePatched) {
3114
+ function patchPiFooter() {
3115
+ if (footerPatched) {
2874
3116
  return;
2875
3117
  }
2876
3118
  const originalRender = FooterComponent.prototype.render;
2877
3119
  FooterComponent.prototype.render = function patchedRender(width) {
2878
3120
  const lines = originalRender.call(this, width);
2879
- if (Array.isArray(lines)) {
3121
+ if (Array.isArray(lines) && lines.length > 1 && typeof lines[1] === 'string') {
2880
3122
  const session = this.session;
2881
3123
  const autoCompactEnabled = this.autoCompactEnabled !== false;
2882
3124
  const footerData = this.footerData;
2883
- const statusLine = buildLinxTuiStatusLine({
3125
+ const modePrefix = buildLinxFooterModePrefix();
3126
+ const modeLen = visibleWidth(modePrefix);
3127
+ const bulletLen = modeLen > 0 ? 3 : 0;
3128
+ const statusWidth = Math.max(0, width - modeLen - bulletLen);
3129
+ lines[1] = buildLinxFooterStatusLine({
2884
3130
  session,
2885
- width,
3131
+ width: statusWidth,
2886
3132
  autoCompactEnabled,
2887
3133
  footerData: footerData,
2888
- modeLabel: buildLinxStatusLineModeLabel(),
2889
3134
  });
2890
- // Pi's footer renders cwd, stats, then optional extension notices. LinX
2891
- // owns only the logical TUI status line, so replace Pi's stats line and
2892
- // append the configurable status line after notices to keep it the
2893
- // bottom-most line.
2894
- if (lines.length > 1 && typeof lines[1] === 'string') {
2895
- lines.splice(1, 1);
3135
+ if (modePrefix) {
3136
+ lines[1] = modePrefix + ' ' + lines[1];
2896
3137
  }
2897
- lines.push(statusLine);
2898
3138
  }
2899
3139
  return lines;
2900
3140
  };
2901
- statusLinePatched = true;
3141
+ footerPatched = true;
2902
3142
  }
2903
- function buildLinxStatusLineModeLabel() {
2904
- if (!_linxStatusLineInteractive)
3143
+ function buildLinxFooterModePrefix() {
3144
+ if (!_linxFooterInteractive)
2905
3145
  return '';
2906
- const autoOn = _linxStatusLineInteractive.__autoEnabled === true;
2907
- const symphonyOn = _linxStatusLineInteractive.__linxSymphonyModeEnabled === true;
3146
+ const autoOn = _linxFooterInteractive.__autoEnabled === true;
3147
+ const symphonyOn = _linxFooterInteractive.__linxSymphonyModeEnabled === true;
2908
3148
  if (!autoOn && !symphonyOn)
2909
3149
  return '';
2910
3150
  if (autoOn && symphonyOn)