pi-codex-marketplace 0.1.8 → 0.1.9

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.
@@ -7,7 +7,7 @@
7
7
  */
8
8
 
9
9
  import type { Theme } from '@earendil-works/pi-coding-agent';
10
- import { Key, matchesKey, type Component, type TUI } from '@earendil-works/pi-tui';
10
+ import { Key, matchesKey, wrapTextWithAnsi, type Component, type TUI } from '@earendil-works/pi-tui';
11
11
 
12
12
  import { checkGlobalPendingBarrier, type GlobalBarrierStatus } from '../../src/barrier/global-barrier.js';
13
13
  import { readBothStates } from '../../src/bridge-state/store.js';
@@ -34,9 +34,13 @@ import type { ValidationFinding } from '../../src/registration/findings.js';
34
34
  import type { AttemptReceipt } from '../../src/registration/receipt.js';
35
35
  import {
36
36
  fitTerminalLine,
37
- padTerminalLine,
38
37
  quoteTerminalText,
38
+ renderBadge,
39
+ renderPanel,
40
+ renderSelectableRow,
41
+ renderSideBySidePanels,
39
42
  } from './terminal-presentation.js';
43
+ import { attemptSummaryText, findingOutcomeText, uiText } from './ui-strings.js';
40
44
 
41
45
  export type LedgerSectionId =
42
46
  | 'observe'
@@ -283,14 +287,16 @@ function rail(scope: Scope, result: ReadResult, projectTrusted: boolean): Ledger
283
287
  ? 'indeterminate'
284
288
  : 'healthy';
285
289
  const healthText = health === 'healthy'
286
- ? result.status === 'missing' ? 'Healthy (empty initial state)' : 'Healthy'
290
+ ? result.status === 'missing'
291
+ ? uiText('ledger.rail.health.healthyEmpty')
292
+ : uiText('ledger.rail.health.healthy')
287
293
  : health === 'incompatible'
288
- ? `Incompatible: ${result.error ?? 'unknown schema'}`
289
- : `Persistence Indeterminate: ${result.error ?? 'unreadable Bridge State'}`;
294
+ ? uiText('ledger.rail.health.incompatible', { error: result.error ?? uiText('common.unknown') })
295
+ : uiText('ledger.rail.health.indeterminate', { error: result.error ?? uiText('common.unknown') });
290
296
  return {
291
297
  scope,
292
- label: scope === 'global' ? 'Global Scope' : 'Project Scope',
293
- revision: state?.stateRevision ?? 'unavailable',
298
+ label: scope === 'global' ? uiText('common.scope.global') : uiText('common.scope.project'),
299
+ revision: state?.stateRevision ?? uiText('ledger.revision.unavailable'),
294
300
  registrationCount: state?.registrations.length ?? 0,
295
301
  installationEnabledCount:
296
302
  state?.installations.filter((item) => item.installationState === 'enabled').length ?? 0,
@@ -300,8 +306,8 @@ function rail(scope: Scope, result: ReadResult, projectTrusted: boolean): Ledger
300
306
  health,
301
307
  healthText,
302
308
  trustText: scope === 'global'
303
- ? 'Project Trust: not applicable'
304
- : projectTrusted ? 'Project Trust: granted' : 'Project Trust: not granted',
309
+ ? uiText('ledger.rail.trust.notApplicable')
310
+ : projectTrusted ? uiText('ledger.rail.trust.granted') : uiText('ledger.rail.trust.notGranted'),
305
311
  };
306
312
  }
307
313
 
@@ -313,14 +319,15 @@ function availability(
313
319
  if (intent.scope === 'project' && snapshot.barrier.active) {
314
320
  return {
315
321
  enabled: false,
316
- disabledReason: `Global Pending Barrier: ${snapshot.barrier.reason ?? 'global recovery is required'}; ` +
317
- 'open Recovery & receipts and complete an eligible Global Recovery Action',
322
+ disabledReason: uiText('ledger.disabledReason.barrier', {
323
+ reason: snapshot.barrier.reason ?? uiText('ledger.barrier.defaultReason'),
324
+ }),
318
325
  };
319
326
  }
320
327
  if (intent.scope === 'project' && !snapshot.projectTrusted) {
321
328
  return {
322
329
  enabled: false,
323
- disabledReason: 'Project Trust is not granted; Project Scope mutation is unavailable',
330
+ disabledReason: uiText('ledger.disabledReason.trust'),
324
331
  };
325
332
  }
326
333
  const authority = intent.scope === 'global' ? snapshot.global : snapshot.project;
@@ -331,7 +338,7 @@ function availability(
331
338
  if (!chain?.receipts[0]?.validationSnapshot) {
332
339
  return {
333
340
  enabled: false,
334
- disabledReason: 'Pending Application has no bound Validation Snapshot; start a fresh validated Lifecycle Intent instead of replaying it',
341
+ disabledReason: uiText('ledger.disabledReason.retryNoSnapshot'),
335
342
  };
336
343
  }
337
344
  }
@@ -340,7 +347,9 @@ function availability(
340
347
  if (authority.status === 'incompatible') {
341
348
  return {
342
349
  enabled: false,
343
- disabledReason: `Bridge State schema is incompatible: ${authority.error ?? 'unknown schema'}; update the Bridge Package`,
350
+ disabledReason: uiText('ledger.disabledReason.incompatible', {
351
+ error: authority.error ?? uiText('common.unknown'),
352
+ }),
344
353
  };
345
354
  }
346
355
  if (snapshot.journals[intent.scope].isDegraded) return { enabled: true };
@@ -350,52 +359,56 @@ function availability(
350
359
  if (repairable) return { enabled: true };
351
360
  const activeConditions = snapshot.journals[intent.scope].activeChains.map((chain) =>
352
361
  chain.condition === 'pending-application'
353
- ? 'Pending Application'
362
+ ? uiText('ledger.condition.pending-application')
354
363
  : chain.condition === 'persistence-failed'
355
- ? 'Persistence Failed'
364
+ ? uiText('ledger.condition.persistence-failed')
356
365
  : chain.condition,
357
366
  );
358
367
  return {
359
368
  enabled: false,
360
369
  disabledReason: activeConditions.length > 0
361
- ? `State Repair is not eligible for ${activeConditions.join(', ')}; use the exact declared Recovery Action`
362
- : 'State Repair has no eligible Persistence Indeterminate recovery chain or unreadable state',
370
+ ? uiText('ledger.disabledReason.repairIneligible', { conditions: activeConditions.join(', ') })
371
+ : uiText('ledger.disabledReason.repairNothing'),
363
372
  };
364
373
  }
365
374
  if (authority.status === 'incompatible') {
366
375
  return {
367
376
  enabled: false,
368
- disabledReason: `Bridge State is incompatible: ${authority.error ?? 'unknown schema'}; update the Bridge Package before mutation`,
377
+ disabledReason: uiText('ledger.disabledReason.incompatibleMutation', {
378
+ error: authority.error ?? uiText('common.unknown'),
379
+ }),
369
380
  };
370
381
  }
371
382
  if (authority.status === 'corrupted') {
372
383
  return {
373
384
  enabled: false,
374
- disabledReason: `Persistence Indeterminate: ${authority.error ?? 'Bridge State is unreadable'}; use Repair State`,
385
+ disabledReason: uiText('ledger.disabledReason.corrupted', {
386
+ error: authority.error ?? uiText('ledger.disabledReason.corruptState'),
387
+ }),
375
388
  };
376
389
  }
377
390
  return { enabled: true };
378
391
  }
379
392
 
380
393
  const ACTION_LABELS: Record<LedgerActionId, string> = {
381
- 'observe-partitions': 'Inspect authority partitions',
382
- 'observe-effective-state': 'Inspect Effective State and Projected Skills',
383
- 'register-local': 'Register local Marketplace',
384
- 'register-git': 'Register Git Marketplace',
385
- 'refresh-registration': 'Refresh Marketplace',
386
- 'rebind-registration': 'Rebind Registration',
387
- 'remove-registration': 'Remove Registration',
388
- 'install-disabled': 'Install Disabled',
389
- 'install-and-enable': 'Install and Enable',
390
- 'enable-installation': 'Enable Installation',
391
- 'disable-installation': 'Disable Installation',
392
- 'remove-installation': 'Remove Installation',
393
- 'create-scope-override': 'Create Scope Override',
394
- 'remove-scope-override': 'Remove Scope Override',
395
- 'view-receipt-journal': 'View Receipt Journal',
396
- 'repair-state': 'Repair Bridge State',
397
- 'retry-application': 'Retry Runtime Application',
398
- 'inspect-receipt': 'Inspect Attempt Receipt',
394
+ 'observe-partitions': uiText('ledger.action.observe-partitions'),
395
+ 'observe-effective-state': uiText('ledger.action.observe-effective-state'),
396
+ 'register-local': uiText('ledger.action.register-local'),
397
+ 'register-git': uiText('ledger.action.register-git'),
398
+ 'refresh-registration': uiText('ledger.action.refresh-registration'),
399
+ 'rebind-registration': uiText('ledger.action.rebind-registration'),
400
+ 'remove-registration': uiText('ledger.action.remove-registration'),
401
+ 'install-disabled': uiText('ledger.action.install-disabled'),
402
+ 'install-and-enable': uiText('ledger.action.install-and-enable'),
403
+ 'enable-installation': uiText('ledger.action.enable-installation'),
404
+ 'disable-installation': uiText('ledger.action.disable-installation'),
405
+ 'remove-installation': uiText('ledger.action.remove-installation'),
406
+ 'create-scope-override': uiText('ledger.action.create-scope-override'),
407
+ 'remove-scope-override': uiText('ledger.action.remove-scope-override'),
408
+ 'view-receipt-journal': uiText('ledger.action.view-receipt-journal'),
409
+ 'repair-state': uiText('ledger.action.repair-state'),
410
+ 'retry-application': uiText('ledger.action.retry-application'),
411
+ 'inspect-receipt': uiText('ledger.action.inspect-receipt'),
399
412
  };
400
413
 
401
414
  function action(snapshot: BridgeLedgerSnapshot, intent: LedgerActionIntent): LedgerActionRow {
@@ -429,7 +442,9 @@ function scopeRegistrationRows(
429
442
  ): LedgerObjectRow[] {
430
443
  const createRow: LedgerObjectRow = {
431
444
  id: `registration-create:${scope}`,
432
- label: `${scope === 'global' ? 'Global' : 'Project'} registration actions`,
445
+ label: uiText('ledger.row.registrationActions', {
446
+ scopeWord: scope === 'global' ? uiText('common.scope.word.global') : uiText('common.scope.word.project'),
447
+ }),
433
448
  scope,
434
449
  targetKind: 'scope',
435
450
  targetId: scope,
@@ -441,7 +456,7 @@ function scopeRegistrationRows(
441
456
  const records = (state?.registrations ?? []).map((registration): LedgerObjectRow => ({
442
457
  id: `registration:${scope}:${registration.id}`,
443
458
  label: registrationName(registration),
444
- detail: `${registration.sourceKind ?? 'unknown source'} · ${registration.source ?? '(source unavailable)'}`,
459
+ detail: `${registration.sourceKind ?? uiText('ledger.row.registration.sourceUnknown')} · ${registration.source ?? uiText('ledger.row.registration.sourceUnavailable')}`,
445
460
  scope,
446
461
  targetKind: 'registration',
447
462
  targetId: registration.id,
@@ -463,13 +478,13 @@ function pluginRows(
463
478
  if (!('marketplaceEntryId' in entry)) {
464
479
  const findings = entry.findings.length > 0
465
480
  ? entry.findings.map((finding) =>
466
- `${finding.code} · ${finding.rule} · ${finding.outcome}`,
481
+ `${finding.code} · ${finding.rule} · ${findingOutcomeText(finding)}`,
467
482
  ).join('; ')
468
- : 'no Marketplace Entries were reported';
483
+ : uiText('ledger.row.diagnostic.noEntries');
469
484
  return {
470
485
  id: `marketplace-diagnostic:${scope}:${entry.registrationId}`,
471
486
  label: entry.name,
472
- detail: `Unavailable · ${findings}`,
487
+ detail: uiText('ledger.row.diagnostic.unavailable', { findings }),
473
488
  scope,
474
489
  targetKind: 'registration',
475
490
  targetId: entry.registrationId,
@@ -484,15 +499,21 @@ function pluginRows(
484
499
  installation.pluginId === compatiblePluginId);
485
500
  const skillDetail = entry.plugin?.skills.length
486
501
  ? entry.plugin.skills.map((skill) =>
487
- `${skill.name} ${skill.invocationPolicy} resources ${skill.resources.length > 0 ? skill.resources.join(', ') : 'none'}`,
502
+ uiText('ledger.row.skills.resources', {
503
+ name: skill.name,
504
+ policy: skill.invocationPolicy,
505
+ resources: skill.resources.length > 0 ? skill.resources.join(', ') : uiText('ledger.row.skills.noResources'),
506
+ }),
488
507
  ).join('; ')
489
- : 'skills none';
508
+ : uiText('ledger.row.skills.none');
490
509
  const unavailableReason = installed
491
- ? 'this Marketplace Entry already has a scope-local Installation'
510
+ ? uiText('ledger.row.install.alreadyInstalled')
492
511
  : entry.unavailableReason
493
512
  ?? (entry.classification === 'compatible' && !entry.validationSnapshot
494
- ? 'Validation Snapshot is unavailable; reopen Plugins after source inspection'
495
- : entry.classification === 'compatible' ? undefined : `${entry.classification} Marketplace Entry`);
513
+ ? uiText('ledger.row.install.snapshotMissing')
514
+ : entry.classification === 'compatible'
515
+ ? undefined
516
+ : uiText('ledger.row.install.classification', { classification: entry.classification }));
496
517
  const installAction = (
497
518
  actionId: 'install-disabled' | 'install-and-enable',
498
519
  desiredInstallationState: 'disabled' | 'enabled',
@@ -557,7 +578,7 @@ function createOverrideRow(
557
578
  return {
558
579
  id: `inherited:${kind}:${targetId}`,
559
580
  label,
560
- detail: `Inherited Global ${kind} · canonical target ${targetId}`,
581
+ detail: uiText('ledger.row.override.inherited', { kind, targetId }),
561
582
  scope: 'global',
562
583
  targetKind: kind,
563
584
  targetId,
@@ -597,8 +618,8 @@ function overrideRows(
597
618
  const canonicalOverrideId = `${override.kind}/${override.targetId}`;
598
619
  return {
599
620
  id: `scope-override:${canonicalOverrideId}`,
600
- label: `${override.kind} override`,
601
- detail: `Suppresses inherited ${override.targetId}`,
621
+ label: uiText('ledger.row.override.label', { kind: override.kind }),
622
+ detail: uiText('ledger.row.override.suppresses', { targetId: override.targetId }),
602
623
  scope: 'project',
603
624
  targetKind: 'scope-override',
604
625
  targetId: canonicalOverrideId,
@@ -619,7 +640,7 @@ function overrideRows(
619
640
  function receiptRow(receipt: AttemptReceipt, snapshot: BridgeLedgerSnapshot): LedgerObjectRow {
620
641
  return {
621
642
  id: `receipt:${receipt.scope}:${receipt.id}`,
622
- label: `${receipt.summary} · ${receipt.operation}`,
643
+ label: `${attemptSummaryText(receipt.summary)} · ${receipt.operation}`,
623
644
  detail: `${receipt.createdAt} · ${receipt.id}`,
624
645
  scope: receipt.scope,
625
646
  targetKind: 'receipt',
@@ -636,8 +657,13 @@ function retryApplicationRows(snapshot: BridgeLedgerSnapshot): LedgerObjectRow[]
636
657
  .filter((chain) => chain.condition === 'pending-application')
637
658
  .map((chain): LedgerObjectRow => ({
638
659
  id: `retry-application:${scope}:${chain.rootReceiptId}`,
639
- label: `${scope === 'global' ? 'Global' : 'Project'} Pending Application`,
640
- detail: `Active recovery chain ${chain.rootReceiptId} · revision ${chain.stateRevision}`,
660
+ label: uiText('ledger.row.retry.label', {
661
+ scopeWord: scope === 'global' ? uiText('common.scope.word.global') : uiText('common.scope.word.project'),
662
+ }),
663
+ detail: uiText('ledger.row.retry.detail', {
664
+ receiptId: chain.rootReceiptId,
665
+ revision: chain.stateRevision,
666
+ }),
641
667
  scope,
642
668
  targetKind: 'receipt',
643
669
  targetId: chain.rootReceiptId,
@@ -658,30 +684,35 @@ export function buildBridgeLedgerModel(snapshot: BridgeLedgerSnapshot): BridgeLe
658
684
  const effective = snapshot.effective;
659
685
  const observe: LedgerSection = {
660
686
  id: 'observe',
661
- label: 'Observe',
662
- description: 'Inspect authoritative partitions and the derived Effective State',
687
+ label: uiText('ledger.section.observe.label'),
688
+ description: uiText('ledger.section.observe.description'),
663
689
  rows: [
664
690
  {
665
691
  id: 'observe:partitions',
666
- label: 'Global / Project authority partitions',
667
- detail: `Global revision ${snapshot.global.state?.stateRevision ?? 'unavailable'} · ` +
668
- `Project revision ${snapshot.project.state?.stateRevision ?? 'unavailable'}`,
692
+ label: uiText('ledger.row.observe.partitions'),
693
+ detail: uiText('ledger.row.observe.partitionsDetail', {
694
+ global: snapshot.global.state?.stateRevision ?? uiText('ledger.revision.unavailable'),
695
+ project: snapshot.project.state?.stateRevision ?? uiText('ledger.revision.unavailable'),
696
+ }),
669
697
  actions: [action(snapshot, { actionId: 'observe-partitions', mode: 'read' })],
670
698
  },
671
699
  {
672
700
  id: 'observe:effective',
673
- label: 'Effective State and Projected Skills',
674
- detail: `registrations ${effective?.registrations.length ?? 0} · ` +
675
- `installations ${effective?.installations.length ?? 0} · ` +
676
- `suppressed ${effective?.suppressed.length ?? 0} · excluded ${effective?.excluded.length ?? 0}`,
701
+ label: uiText('ledger.row.observe.effective'),
702
+ detail: uiText('ledger.row.observe.effectiveDetail', {
703
+ registrations: effective?.registrations.length ?? 0,
704
+ installations: effective?.installations.length ?? 0,
705
+ suppressed: effective?.suppressed.length ?? 0,
706
+ excluded: effective?.excluded.length ?? 0,
707
+ }),
677
708
  actions: [action(snapshot, { actionId: 'observe-effective-state', mode: 'read' })],
678
709
  },
679
710
  ],
680
711
  };
681
712
  const sources: LedgerSection = {
682
713
  id: 'sources',
683
- label: 'Sources',
684
- description: 'Marketplace Registrations and source lifecycle actions',
714
+ label: uiText('ledger.section.sources.label'),
715
+ description: uiText('ledger.section.sources.description'),
685
716
  rows: [
686
717
  ...scopeRegistrationRows('global', globalState, snapshot),
687
718
  ...scopeRegistrationRows('project', projectState, snapshot),
@@ -690,8 +721,8 @@ export function buildBridgeLedgerModel(snapshot: BridgeLedgerSnapshot): BridgeLe
690
721
  let pluginRowCache: LedgerObjectRow[] | undefined;
691
722
  const plugins: LedgerSection = {
692
723
  id: 'plugins',
693
- label: 'Plugins',
694
- description: 'Compatible candidates and scope-local Installation state',
724
+ label: uiText('ledger.section.plugins.label'),
725
+ description: uiText('ledger.section.plugins.description'),
695
726
  get rows() {
696
727
  pluginRowCache ??= [
697
728
  ...pluginRows('global', globalState, snapshot),
@@ -702,22 +733,26 @@ export function buildBridgeLedgerModel(snapshot: BridgeLedgerSnapshot): BridgeLe
702
733
  };
703
734
  const inheritance: LedgerSection = {
704
735
  id: 'scope-inheritance',
705
- label: 'Scope & inheritance',
706
- description: 'Project Scope overrides suppress inherited Global records without mutating them',
736
+ label: uiText('ledger.section.scope-inheritance.label'),
737
+ description: uiText('ledger.section.scope-inheritance.description'),
707
738
  rows: overrideRows(globalState, projectState, snapshot),
708
739
  };
709
740
  const recovery: LedgerSection = {
710
741
  id: 'recovery-receipts',
711
- label: 'Recovery & receipts',
712
- description: 'Non-authoritative Attempt Receipt history and explicit State Repair',
742
+ label: uiText('ledger.section.recovery-receipts.label'),
743
+ description: uiText('ledger.section.recovery-receipts.description'),
713
744
  rows: [
714
745
  ...(['global', 'project'] as const).flatMap((scope): LedgerObjectRow[] => [
715
746
  {
716
747
  id: `journal:${scope}`,
717
- label: `${scope === 'global' ? 'Global' : 'Project'} Receipt Journal`,
718
- detail: `${snapshot.journals[scope].receipts.length} receipts · ` +
719
- `${snapshot.journals[scope].activeChains.length} active recovery chains · ` +
720
- `degraded ${snapshot.journals[scope].isDegraded ? 'yes' : 'no'}`,
748
+ label: uiText('ledger.row.journal.label', {
749
+ scopeWord: scope === 'global' ? uiText('common.scope.word.global') : uiText('common.scope.word.project'),
750
+ }),
751
+ detail: uiText('ledger.row.journal.detail', {
752
+ receipts: snapshot.journals[scope].receipts.length,
753
+ chains: snapshot.journals[scope].activeChains.length,
754
+ degraded: snapshot.journals[scope].isDegraded ? uiText('common.yes') : uiText('common.no'),
755
+ }),
721
756
  scope,
722
757
  targetKind: 'scope',
723
758
  targetId: scope,
@@ -725,7 +760,9 @@ export function buildBridgeLedgerModel(snapshot: BridgeLedgerSnapshot): BridgeLe
725
760
  },
726
761
  {
727
762
  id: `repair:${scope}`,
728
- label: `${scope === 'global' ? 'Global' : 'Project'} State Repair`,
763
+ label: uiText('ledger.row.repair.label', {
764
+ scopeWord: scope === 'global' ? uiText('common.scope.word.global') : uiText('common.scope.word.project'),
765
+ }),
729
766
  scope,
730
767
  targetKind: 'scope',
731
768
  targetId: scope,
@@ -747,8 +784,8 @@ export function buildBridgeLedgerModel(snapshot: BridgeLedgerSnapshot): BridgeLe
747
784
  barrier: {
748
785
  active: snapshot.barrier.active,
749
786
  text: snapshot.barrier.active
750
- ? snapshot.barrier.reason ?? 'global recovery is required'
751
- : 'Clear',
787
+ ? snapshot.barrier.reason ?? uiText('ledger.barrier.defaultReason')
788
+ : uiText('ledger.barrier.clear'),
752
789
  },
753
790
  effective: {
754
791
  registrationCount: effective?.registrations.length ?? 0,
@@ -762,6 +799,21 @@ export function buildBridgeLedgerModel(snapshot: BridgeLedgerSnapshot): BridgeLe
762
799
 
763
800
  // The custom component is added below the presentation model so callers can use the
764
801
  // pure buildBridgeLedgerModel seam independently of Pi's runtime objects.
802
+
803
+ /** Widths at or above this breakpoint render the two-column panel workspace. */
804
+ const WIDE_WORKSPACE_WIDTH = 96;
805
+
806
+ const HEALTH_BADGES: Record<LedgerHealth, { tone: 'success' | 'warning' | 'error'; labelId: Parameters<typeof uiText>[0] }> = {
807
+ healthy: { tone: 'success', labelId: 'ledger.badge.healthy' },
808
+ incompatible: { tone: 'error', labelId: 'ledger.badge.incompatible' },
809
+ indeterminate: { tone: 'warning', labelId: 'ledger.badge.indeterminate' },
810
+ };
811
+
812
+ const AVAILABILITY = {
813
+ ready: { icon: '\u25cf', token: 'success', word: uiText('ledger.availability.ready') },
814
+ blocked: { icon: '\u25cb', token: 'warning', word: uiText('ledger.availability.blocked') },
815
+ } as const;
816
+
765
817
  export class BridgeLedgerComponent implements Component {
766
818
  private readonly model: BridgeLedgerModel;
767
819
  private readonly theme: Theme;
@@ -771,7 +823,10 @@ export class BridgeLedgerComponent implements Component {
771
823
  private rowIndex = 0;
772
824
  private browseFocus: Scope = 'global';
773
825
  private helpVisible = false;
774
- private narrowDetail = false;
826
+ /** Single-column drill-down state for sub-96-column workspaces. */
827
+ private sectionDetail = false;
828
+ /** Expanded metadata layer for the selected entry; never consulted by dispatch. */
829
+ private metadataExpanded = false;
775
830
  private lastWidth = 80;
776
831
 
777
832
  constructor(
@@ -799,64 +854,75 @@ export class BridgeLedgerComponent implements Component {
799
854
  if (matchesKey(data, Key.escape)) {
800
855
  if (this.helpVisible) {
801
856
  this.helpVisible = false;
802
- this.tui.requestRender();
803
- } else if (this.lastWidth < 64 && this.narrowDetail) {
804
- this.narrowDetail = false;
857
+ } else if (this.metadataExpanded) {
858
+ this.metadataExpanded = false;
859
+ } else if (this.lastWidth < WIDE_WORKSPACE_WIDTH && this.sectionDetail) {
860
+ this.sectionDetail = false;
805
861
  this.rowIndex = 0;
806
- this.tui.requestRender();
807
862
  } else {
808
863
  this.onDone(undefined);
864
+ return;
809
865
  }
866
+ this.tui.requestRender();
810
867
  return;
811
868
  }
869
+ if (this.helpVisible) return;
870
+
812
871
  if (matchesKey(data, 'g') || matchesKey(data, 'p')) {
813
872
  const nextFocus: Scope = matchesKey(data, 'g') ? 'global' : 'project';
814
873
  if (nextFocus !== this.browseFocus) {
815
874
  this.browseFocus = nextFocus;
816
875
  this.rowIndex = 0;
876
+ this.metadataExpanded = false;
817
877
  this.tui.requestRender();
818
878
  }
819
879
  return;
820
880
  }
821
- if (this.helpVisible) return;
881
+ if (matchesKey(data, 'i')) {
882
+ this.metadataExpanded = !this.metadataExpanded;
883
+ this.tui.requestRender();
884
+ return;
885
+ }
822
886
 
823
- const narrowSections = this.lastWidth < 64 && !this.narrowDetail;
887
+ const wide = this.lastWidth >= WIDE_WORKSPACE_WIDTH;
824
888
  if (matchesKey(data, Key.right)) {
825
- if (this.lastWidth < 64) {
826
- if (!this.narrowDetail) {
827
- this.narrowDetail = true;
828
- this.rowIndex = 0;
829
- this.tui.requestRender();
830
- }
831
- } else {
889
+ if (!wide && !this.sectionDetail) {
890
+ this.sectionDetail = true;
891
+ this.rowIndex = 0;
892
+ this.metadataExpanded = false;
893
+ this.tui.requestRender();
894
+ } else if (wide) {
832
895
  this.moveSection(1);
833
896
  }
834
897
  return;
835
898
  }
836
899
  if (matchesKey(data, Key.left)) {
837
- if (this.lastWidth < 64 && this.narrowDetail) {
838
- this.narrowDetail = false;
900
+ if (!wide && this.sectionDetail) {
901
+ this.sectionDetail = false;
839
902
  this.rowIndex = 0;
903
+ this.metadataExpanded = false;
840
904
  this.tui.requestRender();
841
- } else if (this.lastWidth >= 64) {
905
+ } else if (wide) {
842
906
  this.moveSection(-1);
843
907
  }
844
908
  return;
845
909
  }
910
+ const listView = !wide && !this.sectionDetail;
846
911
  if (matchesKey(data, 'j') || matchesKey(data, Key.down)) {
847
- if (narrowSections) this.moveSection(1);
912
+ if (listView) this.moveSection(1);
848
913
  else this.moveRow(1);
849
914
  return;
850
915
  }
851
916
  if (matchesKey(data, 'k') || matchesKey(data, Key.up)) {
852
- if (narrowSections) this.moveSection(-1);
917
+ if (listView) this.moveSection(-1);
853
918
  else this.moveRow(-1);
854
919
  return;
855
920
  }
856
921
  if (matchesKey(data, Key.enter)) {
857
- if (narrowSections) {
858
- this.narrowDetail = true;
922
+ if (listView) {
923
+ this.sectionDetail = true;
859
924
  this.rowIndex = 0;
925
+ this.metadataExpanded = false;
860
926
  this.tui.requestRender();
861
927
  return;
862
928
  }
@@ -872,13 +938,20 @@ export class BridgeLedgerComponent implements Component {
872
938
 
873
939
  render(width: number): string[] {
874
940
  this.lastWidth = Math.max(1, Math.floor(width));
875
- const out = this.authorityHeader(this.lastWidth);
941
+ const out = [
942
+ this.fit('CODEX MARKETPLACE / BRIDGE LEDGER', this.lastWidth, 'accent'),
943
+ ...this.railPanels(this.lastWidth),
944
+ ];
876
945
  if (this.helpVisible) {
877
- out.push(...this.helpLines(this.lastWidth));
878
- } else if (this.lastWidth >= 64) {
946
+ out.push(...renderPanel(this.theme, {
947
+ title: uiText('ledger.panel.help'),
948
+ lines: this.helpLines().map((line) => this.fit(line, Math.max(1, this.lastWidth - 3), 'text')),
949
+ width: this.lastWidth,
950
+ }));
951
+ } else if (this.lastWidth >= WIDE_WORKSPACE_WIDTH) {
879
952
  out.push(...this.wideWorkspace(this.lastWidth));
880
953
  } else {
881
- out.push(...this.narrowWorkspace(this.lastWidth));
954
+ out.push(...this.drilldownWorkspace(this.lastWidth));
882
955
  }
883
956
  out.push(this.fit(this.statusText(), this.lastWidth, 'dim'));
884
957
  out.push(this.fit(this.keyHints(), this.lastWidth, 'dim'));
@@ -895,6 +968,7 @@ export class BridgeLedgerComponent implements Component {
895
968
  if (count === 0) return;
896
969
  this.sectionIndex = (this.sectionIndex + delta + count) % count;
897
970
  this.rowIndex = 0;
971
+ this.metadataExpanded = false;
898
972
  this.tui.requestRender();
899
973
  }
900
974
 
@@ -902,6 +976,7 @@ export class BridgeLedgerComponent implements Component {
902
976
  const count = this.actionEntries().length;
903
977
  if (count === 0) return;
904
978
  this.rowIndex = (this.rowIndex + delta + count) % count;
979
+ this.metadataExpanded = false;
905
980
  this.tui.requestRender();
906
981
  }
907
982
 
@@ -909,117 +984,209 @@ export class BridgeLedgerComponent implements Component {
909
984
  return fitTerminalLine(this.theme.fg(token, text), width);
910
985
  }
911
986
 
912
- private authorityHeader(width: number): string[] {
913
- const global = this.model.rails.global;
914
- const project = this.model.rails.project;
915
- const globalLine = width < 96
916
- ? `G rev ${quoteTerminalText(global.revision)} | reg ${global.registrationCount} | ` +
917
- `inst ${global.installationEnabledCount} on/${global.installationDisabledCount} off | health ${global.health}`
918
- : `G rev ${quoteTerminalText(global.revision)} | registrations ${global.registrationCount} | ` +
919
- `installations ${global.installationEnabledCount} enabled/${global.installationDisabledCount} disabled | ` +
920
- `health ${global.health}: ${quoteTerminalText(global.healthText)}`;
921
- const projectLine = width < 96
922
- ? `P rev ${quoteTerminalText(project.revision)} | reg ${project.registrationCount} | ` +
923
- `inst ${project.installationEnabledCount} on/${project.installationDisabledCount} off | ` +
924
- `ovr ${project.overrideCount} | health ${project.health}`
925
- : `P rev ${quoteTerminalText(project.revision)} | registrations ${project.registrationCount} | ` +
926
- `installations ${project.installationEnabledCount} enabled/${project.installationDisabledCount} disabled | ` +
927
- `overrides ${project.overrideCount} | health ${project.health}: ${quoteTerminalText(project.healthText)}`;
928
- const barrier = this.model.barrier.active
929
- ? `Barrier: ACTIVE ${quoteTerminalText(this.model.barrier.text)}`
930
- : 'Barrier: Clear';
931
- return [
932
- this.fit('CODEX MARKETPLACE / BRIDGE LEDGER', width, 'accent'),
933
- this.fit(globalLine, width, global.health === 'healthy' ? 'text' : 'error'),
934
- this.fit(projectLine, width, project.health === 'healthy' ? 'text' : 'error'),
935
- this.fit(project.trustText, width, this.model.projectTrusted ? 'success' : 'warning'),
936
- this.fit(barrier, width, this.model.barrier.active ? 'warning' : 'success'),
937
- ];
987
+ // --- Authority rails -----------------------------------------------------
988
+
989
+ private railPanels(width: number): string[] {
990
+ if (width < WIDE_WORKSPACE_WIDTH) {
991
+ return [
992
+ ...renderPanel(this.theme, {
993
+ title: this.model.rails.global.label,
994
+ lines: this.railContentLines('global', width - 3),
995
+ width,
996
+ borderToken: 'borderMuted',
997
+ }),
998
+ ...renderPanel(this.theme, {
999
+ title: this.model.rails.project.label,
1000
+ lines: this.railContentLines('project', width - 3),
1001
+ width,
1002
+ borderToken: 'borderAccent',
1003
+ }),
1004
+ ];
1005
+ }
1006
+ const leftWidth = Math.max(24, Math.floor((width - 2) / 2));
1007
+ const rightWidth = Math.max(24, width - 2 - leftWidth);
1008
+ return renderSideBySidePanels(this.theme, {
1009
+ left: {
1010
+ title: this.model.rails.global.label,
1011
+ lines: this.railContentLines('global', leftWidth - 3),
1012
+ width: leftWidth,
1013
+ borderToken: 'borderMuted',
1014
+ },
1015
+ right: {
1016
+ title: this.model.rails.project.label,
1017
+ lines: this.railContentLines('project', rightWidth - 3),
1018
+ width: rightWidth,
1019
+ borderToken: 'borderAccent',
1020
+ },
1021
+ totalWidth: width,
1022
+ });
938
1023
  }
939
1024
 
940
- private helpLines(width: number): string[] {
941
- return [
942
- this.fit('Help', width, 'accent'),
943
- this.fit('Up/Down or j/k: move selection', width),
944
- this.fit('Left/Right: change section in wide layout', width),
945
- this.fit('Enter: open section or activate available structured action', width),
946
- this.fit('g/p: browse Global/Project only; mutation authority remains explicit', width),
947
- this.fit('Esc: back/cancel | q or Ctrl-C: exit | ?: close help', width),
948
- ];
1025
+ private railBadge(scope: Scope): string {
1026
+ const rail = scope === 'global' ? this.model.rails.global : this.model.rails.project;
1027
+ const badges = [renderBadge(this.theme, HEALTH_BADGES[rail.health].tone, uiText(HEALTH_BADGES[rail.health].labelId))];
1028
+ if (scope === 'project') {
1029
+ badges.push(renderBadge(
1030
+ this.theme,
1031
+ this.model.projectTrusted ? 'success' : 'warning',
1032
+ this.model.projectTrusted ? uiText('ledger.badge.trustGranted') : uiText('ledger.badge.noTrust'),
1033
+ ));
1034
+ } else {
1035
+ badges.push(renderBadge(this.theme, this.model.barrier.active ? 'error' : 'success',
1036
+ this.model.barrier.active ? uiText('ledger.badge.barrierActive') : uiText('ledger.badge.barrierClear')));
1037
+ }
1038
+ return badges.join(' ');
949
1039
  }
950
1040
 
951
- private wideWorkspace(width: number): string[] {
952
- const separator = this.theme.fg('borderMuted', ' | ');
953
- const navWidth = Math.min(30, Math.max(20, Math.floor(width * 0.28)));
954
- const detailWidth = Math.max(1, width - navWidth - 3);
955
- const nav = [
956
- this.theme.fg('accent', 'Navigation'),
957
- ...this.model.sections.map((section, index) =>
958
- this.theme.fg(index === this.sectionIndex ? 'accent' : 'text',
959
- `${index === this.sectionIndex ? '>' : ' '} ${section.label}`)),
960
- ];
961
- const section = this.currentSection();
962
- const detail = [
963
- this.theme.fg('accent', section.label),
964
- this.theme.fg('dim', section.description),
965
- ...this.actionLines(section),
1041
+ private railContentLines(scope: Scope, width: number): string[] {
1042
+ const rail = scope === 'global' ? this.model.rails.global : this.model.rails.project;
1043
+ const marker = scope === 'global' ? 'G' : 'P';
1044
+ const fitDim = (text: string): string => this.fit(text, width, 'dim');
1045
+ const lines = [
1046
+ this.railBadge(scope),
1047
+ uiText('ledger.rail.revision', { marker, revision: quoteTerminalText(rail.revision) }),
1048
+ uiText('ledger.rail.registrations', { count: rail.registrationCount }),
1049
+ uiText('ledger.rail.installations', {
1050
+ enabled: rail.installationEnabledCount,
1051
+ disabled: rail.installationDisabledCount,
1052
+ }),
966
1053
  ];
967
- const height = Math.max(nav.length, detail.length);
968
- const lines: string[] = [];
969
- for (let index = 0; index < height; index++) {
970
- const left = padTerminalLine(nav[index] ?? '', navWidth);
971
- const right = fitTerminalLine(detail[index] ?? '', detailWidth);
972
- lines.push(fitTerminalLine(left + separator + right, width));
1054
+ if (scope === 'project') lines.push(uiText('ledger.rail.overrides', { count: rail.overrideCount }));
1055
+ if (rail.health !== 'healthy') lines.push(fitDim(quoteTerminalText(rail.healthText)));
1056
+ if (scope === 'project') lines.push(fitDim(rail.trustText));
1057
+ if (scope === 'global' && this.model.barrier.active) {
1058
+ lines.push(this.fit(
1059
+ uiText('ledger.rail.barrierReason', { reason: quoteTerminalText(this.model.barrier.text) }),
1060
+ width, 'warning',
1061
+ ));
973
1062
  }
974
1063
  return lines;
975
1064
  }
976
1065
 
977
- private narrowWorkspace(width: number): string[] {
978
- if (!this.narrowDetail) {
979
- return [
980
- this.fit('Sections', width, 'accent'),
981
- ...this.model.sections.map((section, index) =>
982
- this.fit(`${index === this.sectionIndex ? '>' : ' '} ${section.label}`, width,
983
- index === this.sectionIndex ? 'accent' : 'text')),
984
- ];
1066
+ // --- Workspaces ----------------------------------------------------------
1067
+
1068
+ private sectionNavLines(width: number): string[] {
1069
+ return this.model.sections.map((section, index) =>
1070
+ renderSelectableRow(this.theme, {
1071
+ selected: index === this.sectionIndex,
1072
+ text: this.theme.fg(index === this.sectionIndex ? 'accent' : 'text', section.label),
1073
+ width,
1074
+ }));
1075
+ }
1076
+
1077
+ private wideWorkspace(width: number): string[] {
1078
+ const navWidth = Math.min(34, Math.max(22, Math.floor(width * 0.28)));
1079
+ const detailWidth = Math.max(24, width - navWidth - 2);
1080
+ const section = this.currentSection();
1081
+ return renderSideBySidePanels(this.theme, {
1082
+ left: {
1083
+ title: uiText('ledger.panel.navigation'),
1084
+ lines: this.sectionNavLines(navWidth - 3),
1085
+ width: navWidth,
1086
+ borderToken: 'borderMuted',
1087
+ },
1088
+ right: {
1089
+ title: section.label,
1090
+ lines: [
1091
+ this.fit(section.description, detailWidth - 3, 'dim'),
1092
+ '',
1093
+ ...this.actionEntryLines(detailWidth - 3),
1094
+ ],
1095
+ width: detailWidth,
1096
+ borderToken: 'borderAccent',
1097
+ },
1098
+ totalWidth: width,
1099
+ });
1100
+ }
1101
+
1102
+ private drilldownWorkspace(width: number): string[] {
1103
+ if (!this.sectionDetail) {
1104
+ return renderPanel(this.theme, {
1105
+ title: uiText('ledger.panel.sections'),
1106
+ lines: this.sectionNavLines(Math.max(1, width - 3)),
1107
+ width,
1108
+ borderToken: 'borderAccent',
1109
+ });
985
1110
  }
986
1111
  const section = this.currentSection();
987
- return [
988
- this.fit(`Section: ${section.label}`, width, 'accent'),
989
- this.fit(section.description, width, 'dim'),
990
- ...this.actionLines(section).map((line) => fitTerminalLine(line, width)),
991
- ];
1112
+ return renderPanel(this.theme, {
1113
+ title: section.label,
1114
+ lines: [
1115
+ this.fit(section.description, Math.max(1, width - 3), 'dim'),
1116
+ '',
1117
+ ...this.actionEntryLines(Math.max(1, width - 3)),
1118
+ ],
1119
+ width,
1120
+ borderToken: 'borderAccent',
1121
+ });
992
1122
  }
993
1123
 
994
- private actionLines(section: LedgerSection): string[] {
995
- const rows = this.visibleRows(section);
996
- if (rows.length === 0) return [this.theme.fg('muted', 'No rows in this section')];
1124
+ // --- Action entries ------------------------------------------------------
1125
+
1126
+ /**
1127
+ * Renders every visible row in the panel visual language: availability is an
1128
+ * icon+token+word state, selection combines a background wash with a text
1129
+ * cursor, and structured field dumps live only in the expandable metadata layer.
1130
+ */
1131
+ private actionEntryLines(width: number): string[] {
1132
+ const rows = this.visibleRows();
1133
+ if (rows.length === 0) return [this.fit(uiText('ledger.rows.empty'), width, 'muted')];
997
1134
  let actionIndex = 0;
998
- return rows.flatMap((row) => {
1135
+ const lines: string[] = [];
1136
+ for (const row of rows) {
999
1137
  if (row.actions.length === 0) {
1000
- return [
1001
- this.theme.fg('warning', ` [Unavailable] ${quoteTerminalText(row.label)}`),
1002
- this.theme.fg('dim',
1003
- ` detail ${quoteTerminalText(row.detail ?? '(none)')} | target ${row.targetKind ?? 'none'} ` +
1004
- `${quoteTerminalText(row.targetId ?? '(none)')} | scope ${row.scope ?? 'none'} | actions none`),
1005
- ];
1138
+ lines.push(
1139
+ this.fit(`${AVAILABILITY.blocked.icon} ${uiText('ledger.entry.unavailableRow', { label: quoteTerminalText(row.label) })}`, width, 'warning'),
1140
+ this.fit(` ${quoteTerminalText(row.detail ?? uiText('ledger.entry.noFindings'))}`, width, 'dim'),
1141
+ );
1142
+ continue;
1006
1143
  }
1007
- return row.actions.flatMap((action) => {
1144
+ for (const action of row.actions) {
1008
1145
  const selected = actionIndex === this.rowIndex;
1009
1146
  actionIndex += 1;
1010
- const availabilityText = action.enabled
1011
- ? 'available'
1012
- : `disabled: ${quoteTerminalText(action.disabledReason ?? 'unavailable')}`;
1013
- return [
1014
- this.theme.fg(selected ? 'accent' : 'text',
1015
- `${selected ? '>' : ' '} [${availabilityText}] ${action.label} / ${quoteTerminalText(row.label)}`),
1016
- this.theme.fg('dim',
1017
- ` detail ${quoteTerminalText(row.detail ?? '(none)')} | target ${action.intent.targetKind ?? 'none'} ` +
1018
- `${quoteTerminalText(action.intent.targetId ?? '(none)')} | scope ${action.intent.scope ?? 'none'} | ` +
1019
- `mode ${action.intent.mode}`),
1020
- ];
1021
- });
1022
- });
1147
+ const availability = action.enabled ? AVAILABILITY.ready : AVAILABILITY.blocked;
1148
+ const text =
1149
+ this.theme.fg(availability.token, `${availability.icon} ${availability.word} ${action.label}`) +
1150
+ this.theme.fg('dim', ` \u00b7 ${quoteTerminalText(row.label)}`);
1151
+ lines.push(renderSelectableRow(this.theme, { selected, text, width }));
1152
+ if (!selected) continue;
1153
+ if (!action.enabled) {
1154
+ lines.push(...this.wrapContext(
1155
+ `\u21b3 ${uiText('ledger.entry.blocked', { reason: quoteTerminalText(action.disabledReason ?? uiText('common.unavailable')) })}`,
1156
+ width, 'warning', 4));
1157
+ }
1158
+ if (this.metadataExpanded) {
1159
+ const intent = action.intent;
1160
+ const meta = [
1161
+ uiText('ledger.meta.target', {
1162
+ kind: intent.targetKind ?? row.targetKind ?? uiText('common.none'),
1163
+ target: quoteTerminalText(intent.targetId ?? row.targetId ?? uiText('common.none')),
1164
+ }),
1165
+ uiText('ledger.meta.scope', { scope: intent.scope ?? row.scope ?? uiText('common.none') }),
1166
+ uiText('ledger.meta.mode', { mode: intent.mode }),
1167
+ ];
1168
+ if (row.detail !== undefined) meta.push(uiText('ledger.meta.detail', { detail: quoteTerminalText(row.detail) }));
1169
+ lines.push(...meta.flatMap((entry) => this.wrapContext(` ${entry}`, width, 'muted', 4)));
1170
+ }
1171
+ }
1172
+ }
1173
+ return lines;
1174
+ }
1175
+
1176
+ private wrapContext(text: string, width: number, token: Parameters<Theme['fg']>[0], maxLines: number): string[] {
1177
+ const wrapped = wrapTextWithAnsi(this.theme.fg(token, text), Math.max(1, width)).slice(0, maxLines);
1178
+ return wrapped.map((line) => fitTerminalLine(line, width));
1179
+ }
1180
+
1181
+ private helpLines(): string[] {
1182
+ return [
1183
+ uiText('ledger.help.move'),
1184
+ uiText('ledger.help.sections'),
1185
+ uiText('ledger.help.enter'),
1186
+ uiText('ledger.help.metadata'),
1187
+ uiText('ledger.help.browse'),
1188
+ uiText('ledger.help.close'),
1189
+ ];
1023
1190
  }
1024
1191
 
1025
1192
  private visibleRows(section = this.currentSection()): LedgerObjectRow[] {
@@ -1042,15 +1209,21 @@ export class BridgeLedgerComponent implements Component {
1042
1209
 
1043
1210
  private statusText(): string {
1044
1211
  const section = this.currentSection();
1045
- const pane = this.lastWidth < 64 && !this.narrowDetail ? 'sections' : 'actions';
1046
- return `Status: browsing ${this.browseFocus === 'global' ? 'G' : 'P'} | ${section.label} | ${pane}`;
1212
+ const pane = this.lastWidth >= WIDE_WORKSPACE_WIDTH || this.sectionDetail
1213
+ ? uiText('ledger.status.pane.actions')
1214
+ : uiText('ledger.status.pane.sections');
1215
+ return uiText('ledger.status.browsing', {
1216
+ marker: this.browseFocus === 'global' ? 'G' : 'P',
1217
+ section: section.label,
1218
+ pane,
1219
+ });
1047
1220
  }
1048
1221
 
1049
1222
  private keyHints(): string {
1050
- if (this.helpVisible) return 'Keys: ?/Esc close help | q/Ctrl-C exit | Esc/q cancel context';
1051
- if (this.lastWidth < 64 && !this.narrowDetail) {
1052
- return 'Keys: Esc/q cancel | Enter drill down | j/k move | g/p | ?';
1223
+ if (this.helpVisible) return uiText('ledger.keys.help');
1224
+ if (this.lastWidth < WIDE_WORKSPACE_WIDTH && !this.sectionDetail) {
1225
+ return uiText('ledger.keys.drilldown');
1053
1226
  }
1054
- return 'Keys: Esc/q cancel | Enter activate | j/k or arrows move | g/p browse | ? help';
1227
+ return uiText('ledger.keys.wide');
1055
1228
  }
1056
1229
  }