@undefineds.co/linx 0.3.22 → 0.3.24

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.
@@ -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';
@@ -41,6 +41,11 @@ const COMPACT_STATUS_LINE_TOKENS = [
41
41
  'context-remaining',
42
42
  'current-dir',
43
43
  ];
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';
44
49
  /** Module-level reference to interactive for footer mode state (set during bootstrap). */
45
50
  let _linxFooterInteractive = null;
46
51
  export function bootstrapLinxInteractiveMode(runtime, options = {}) {
@@ -554,6 +559,9 @@ function parseLinxGlobalCommand(input) {
554
559
  : input.slice('/status-line'.length).trim();
555
560
  return { action: 'statusline', args: splitInteractiveCommandArgs(body) };
556
561
  }
562
+ if (input === '/update' || input === '/upgrade') {
563
+ return { action: 'update' };
564
+ }
557
565
  if (input === '/rewind') {
558
566
  return { action: 'rewind-select' };
559
567
  }
@@ -679,6 +687,10 @@ async function handleLinxGlobalCommand(interactive, runtime, command) {
679
687
  await handleInteractiveStatusLineCommand(interactive, command.args);
680
688
  return;
681
689
  }
690
+ if (command.action === 'update') {
691
+ await checkAndShowLinxUpdate(interactive, { manual: true });
692
+ return;
693
+ }
682
694
  if (command.action === 'rewind-select') {
683
695
  await handleInteractiveRewindSelector(interactive, runtime);
684
696
  return;
@@ -695,53 +707,133 @@ async function handleInteractiveStatusLineCommand(interactive, args) {
695
707
  return;
696
708
  }
697
709
  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?.();
710
+ if (typeof interactive.showSelector === 'function') {
711
+ await showInteractiveStatusLineMultiSelect(interactive);
701
712
  return;
702
713
  }
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
- });
714
+ if (typeof interactive.showExtensionSelector === 'function') {
715
+ await showInteractiveStatusLineFallbackSelector(interactive);
716
716
  return;
717
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
- });
718
+ {
719
+ interactive.showStatus?.(`${summary} · Use /statusline set <tokens...>, /statusline tokens, /statusline colors <on|off>, or /statusline reset.`);
720
+ interactive.ui?.requestRender?.();
723
721
  return;
724
722
  }
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;
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
+ }
732
764
  }
733
- if (choice === 'Show available tokens') {
734
- showInteractiveStatusLineTokens(interactive);
735
- 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;
736
814
  }
737
- if (choice === 'Reset to default') {
738
- resetLinxStatusLineConfig();
739
- finishInteractiveStatusLineUpdate(interactive, `Status line reset to default: ${DEFAULT_STATUS_LINE_TOKENS.join(', ')}`);
740
- return;
815
+ const token = choice.replace(/^[✓○]\s*/u, '').trim();
816
+ if (!token) {
817
+ return null;
741
818
  }
742
- if (choice === 'Configure tokens manually') {
743
- 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;
744
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
+ });
745
837
  }
746
838
  function handleInteractiveStatusLineArgs(interactive, args) {
747
839
  const action = args[0]?.toLowerCase();
@@ -779,20 +871,6 @@ function handleInteractiveStatusLineArgs(interactive, args) {
779
871
  interactive.showError?.(`${message}. Use /statusline tokens to list valid tokens.`);
780
872
  }
781
873
  }
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
874
  function writeInteractiveStatusLineConfig(interactive, patch) {
797
875
  writeLinxStatusLineConfigPatch({
798
876
  ...(patch.statusLine ? { statusLine: patch.statusLine } : {}),
@@ -1159,6 +1237,178 @@ function refreshInteractiveTranscriptFromSessionManager(interactive) {
1159
1237
  interactive?.showWarning?.(`Rewind transcript refresh failed: ${message}`);
1160
1238
  }
1161
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
+ }
1162
1412
  function collectRewindUserMessages(_session, sessionManager) {
1163
1413
  return getActiveSessionBranch(sessionManager)
1164
1414
  .filter((entry) => entry?.type === 'message' && entry.message?.role === 'user')
@@ -1520,6 +1770,10 @@ const LINX_INTERACTIVE_SLASH_COMMANDS = [
1520
1770
  { value: 'reset', description: 'Restore default status line tokens' },
1521
1771
  ]),
1522
1772
  },
1773
+ {
1774
+ name: 'update',
1775
+ description: 'check for a LinX CLI update and install from the TUI',
1776
+ },
1523
1777
  {
1524
1778
  name: 'ai',
1525
1779
  argumentHint: 'connect <provider>',