@pellux/goodvibes-tui 0.18.20 → 0.19.0

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 (83) hide show
  1. package/CHANGELOG.md +154 -0
  2. package/README.md +1 -1
  3. package/docs/foundation-artifacts/operator-contract.json +1 -1
  4. package/package.json +7 -3
  5. package/src/core/conversation-rendering.ts +22 -6
  6. package/src/core/orchestrator.ts +1 -1
  7. package/src/input/commands/diff-runtime.ts +6 -5
  8. package/src/input/commands/guidance-runtime.ts +1 -1
  9. package/src/input/commands/health-runtime.ts +2 -2
  10. package/src/input/commands/local-setup-review.ts +1 -1
  11. package/src/input/commands/session-content.ts +1 -1
  12. package/src/input/commands/session.ts +0 -1
  13. package/src/input/commands/shell-core.ts +3 -2
  14. package/src/input/commands/skills-runtime.ts +2 -2
  15. package/src/input/commands/subscription-runtime.ts +4 -4
  16. package/src/input/feed-context-factory.ts +236 -0
  17. package/src/input/handler-feed.ts +44 -6
  18. package/src/input/handler-shortcuts.ts +138 -125
  19. package/src/input/handler.ts +119 -119
  20. package/src/input/keybindings.ts +30 -0
  21. package/src/input/panel-integration-actions.ts +2 -1
  22. package/src/input/settings-modal-types.ts +60 -0
  23. package/src/input/settings-modal.ts +83 -65
  24. package/src/panels/agent-inspector-panel.ts +10 -9
  25. package/src/panels/agent-logs-panel.ts +26 -6
  26. package/src/panels/approval-panel.ts +55 -82
  27. package/src/panels/automation-control-panel.ts +120 -161
  28. package/src/panels/base-panel.ts +108 -3
  29. package/src/panels/communication-panel.ts +69 -107
  30. package/src/panels/context-visualizer-panel.ts +2 -0
  31. package/src/panels/control-plane-panel.ts +117 -172
  32. package/src/panels/diff-panel.ts +2 -0
  33. package/src/panels/file-explorer-panel.ts +51 -31
  34. package/src/panels/file-preview-panel.ts +57 -35
  35. package/src/panels/git-panel.ts +12 -13
  36. package/src/panels/hooks-panel.ts +103 -138
  37. package/src/panels/incident-review-panel.ts +59 -109
  38. package/src/panels/knowledge-panel.ts +75 -107
  39. package/src/panels/local-auth-panel.ts +77 -93
  40. package/src/panels/marketplace-panel.ts +51 -69
  41. package/src/panels/mcp-panel.ts +110 -155
  42. package/src/panels/memory-panel.ts +90 -158
  43. package/src/panels/ops-control-panel.ts +51 -85
  44. package/src/panels/orchestration-panel.ts +70 -51
  45. package/src/panels/panel-list-panel.ts +5 -4
  46. package/src/panels/panel-manager.ts +25 -2
  47. package/src/panels/plan-dashboard-panel.ts +2 -0
  48. package/src/panels/plugins-panel.ts +37 -60
  49. package/src/panels/polish.ts +51 -2
  50. package/src/panels/provider-accounts-panel.ts +1 -0
  51. package/src/panels/provider-health-panel.ts +6 -8
  52. package/src/panels/routes-panel.ts +91 -141
  53. package/src/panels/schedule-panel.ts +7 -6
  54. package/src/panels/scrollable-list-panel.ts +64 -16
  55. package/src/panels/security-panel.ts +118 -152
  56. package/src/panels/services-panel.ts +63 -105
  57. package/src/panels/session-browser-panel.ts +19 -18
  58. package/src/panels/settings-sync-panel.ts +79 -123
  59. package/src/panels/skills-panel.ts +114 -230
  60. package/src/panels/subscription-panel.ts +64 -86
  61. package/src/panels/system-messages-panel.ts +147 -141
  62. package/src/panels/tasks-panel.ts +130 -179
  63. package/src/panels/token-budget-panel.ts +2 -0
  64. package/src/panels/watchers-panel.ts +89 -137
  65. package/src/panels/worktree-panel.ts +1 -0
  66. package/src/panels/wrfc-panel.ts +2 -0
  67. package/src/renderer/agent-detail-modal.ts +2 -2
  68. package/src/renderer/ansi-sanitize.ts +76 -0
  69. package/src/renderer/buffer.ts +23 -1
  70. package/src/renderer/diff.ts +8 -0
  71. package/src/renderer/help-overlay.ts +48 -28
  72. package/src/renderer/markdown.ts +3 -145
  73. package/src/renderer/settings-modal-helpers.ts +27 -0
  74. package/src/renderer/settings-modal.ts +18 -1
  75. package/src/renderer/status-glyphs.ts +21 -0
  76. package/src/renderer/status-token.ts +4 -8
  77. package/src/renderer/tool-call.ts +4 -3
  78. package/src/runtime/bootstrap-core.ts +1 -1
  79. package/src/runtime/bootstrap-hook-bridge.ts +1 -1
  80. package/src/runtime/bootstrap.ts +7 -8
  81. package/src/runtime/diagnostics/panels/policy.ts +2 -1
  82. package/src/shell/ui-openers.ts +1 -1
  83. package/src/version.ts +1 -1
@@ -26,6 +26,7 @@ import {
26
26
  isPanelSearchCommit,
27
27
  isPanelSearchPrintable,
28
28
  } from './search-focus.ts';
29
+ import { type ConfirmState, handleConfirmInput } from './confirm-state.ts';
29
30
 
30
31
  const C = {
31
32
  headerBg: '#1a1a2e',
@@ -79,7 +80,7 @@ function formatReturnContextLines(returnContext: SessionInfo['returnContext']):
79
80
  // ---------------------------------------------------------------------------
80
81
  // Confirmation state for deletion
81
82
  // ---------------------------------------------------------------------------
82
- type ConfirmState = { sessionName: string } | null;
83
+ // ConfirmState<string> subject holds the session name to delete
83
84
 
84
85
  export class SessionBrowserPanel extends BasePanel {
85
86
  private sessions: SessionInfo[] = [];
@@ -88,10 +89,10 @@ export class SessionBrowserPanel extends BasePanel {
88
89
  private searching = false; // true when user is actively typing a search
89
90
  private cursorIndex = 0;
90
91
  private scrollOffset = 0;
91
- private confirm: ConfirmState = null;
92
+ private confirm: ConfirmState<string> | null = null;
92
93
  private deleteError = '';
93
94
  private loadError = '';
94
- private refreshTimer: ReturnType<typeof setInterval> | null = null;
95
+ private refreshTimerId: ReturnType<typeof setInterval> | null = null;
95
96
 
96
97
  constructor(
97
98
  private readonly sessionManager: SessionBrowserQuery,
@@ -103,29 +104,29 @@ export class SessionBrowserPanel extends BasePanel {
103
104
  override onActivate(): void {
104
105
  super.onActivate();
105
106
  this._load();
106
- this.refreshTimer = setInterval(() => { this._load(); }, 5000);
107
+ this.refreshTimerId = this.registerTimer(setInterval(() => { this._load(); }, 5000));
107
108
  }
108
109
 
109
110
  override onDeactivate(): void {
110
- if (this.refreshTimer) { clearInterval(this.refreshTimer); this.refreshTimer = null; }
111
+ if (this.refreshTimerId !== null) { this.clearTimer(this.refreshTimerId); this.refreshTimerId = null; }
111
112
  this.searching = false;
112
113
  this.confirm = null;
113
114
  super.onDeactivate();
114
115
  }
115
116
 
116
117
  handleInput(key: string): boolean {
117
- // Confirmation dialog
118
- if (this.confirm) {
119
- if (key === 'y') {
120
- this._deleteConfirmed();
121
- return true;
122
- } else if (key === 'n' || key === 'escape') {
123
- this.confirm = null;
124
- this.markDirty();
125
- return true;
126
- }
118
+ // Confirmation dialog — use shared handleConfirmInput for y/n/Esc UX
119
+ const confirmResult = handleConfirmInput(this.confirm, key);
120
+ if (confirmResult === 'confirmed') {
121
+ this._deleteConfirmed();
122
+ return true;
123
+ }
124
+ if (confirmResult === 'cancelled') {
125
+ this.confirm = null;
126
+ this.markDirty();
127
127
  return true;
128
128
  }
129
+ if (confirmResult === 'absorbed') return true;
129
130
 
130
131
  // Search mode
131
132
  if (this.searching) {
@@ -200,7 +201,7 @@ export class SessionBrowserPanel extends BasePanel {
200
201
  {
201
202
  title: 'Confirmation',
202
203
  lines: [
203
- buildPanelLine(width, [[` Delete "${this.confirm.sessionName}"?`, DEFAULT_PANEL_PALETTE.warn]]),
204
+ buildPanelLine(width, [[` Delete "${this.confirm.subject}"?`, DEFAULT_PANEL_PALETTE.warn]]),
204
205
  buildPanelLine(width, [[' y', DEFAULT_PANEL_PALETTE.info], [' confirm delete', DEFAULT_PANEL_PALETTE.dim], [' n / Esc', DEFAULT_PANEL_PALETTE.info], [' cancel', DEFAULT_PANEL_PALETTE.dim]]),
205
206
  ],
206
207
  },
@@ -378,13 +379,13 @@ export class SessionBrowserPanel extends BasePanel {
378
379
  private _promptDelete(): void {
379
380
  const sess = this.filtered[this.cursorIndex];
380
381
  if (!sess) return;
381
- this.confirm = { sessionName: sess.name };
382
+ this.confirm = { subject: sess.name, label: sess.name };
382
383
  this.markDirty();
383
384
  }
384
385
 
385
386
  private _deleteConfirmed(): void {
386
387
  if (!this.confirm) return;
387
- const name = this.confirm.sessionName;
388
+ const name = this.confirm.subject;
388
389
  this.confirm = null;
389
390
  try {
390
391
  this.sessionManager.delete(name);
@@ -1,16 +1,14 @@
1
1
  import type { Line } from '../types/grid.ts';
2
- import { createEmptyLine } from '../types/grid.ts';
3
- import { BasePanel } from './base-panel.ts';
2
+ import { ScrollableListPanel } from './scrollable-list-panel.ts';
4
3
  import {
5
4
  buildDetailBlock,
6
5
  buildGuidanceLine,
7
6
  buildPanelListRow,
8
7
  buildPanelLine,
9
- buildPanelWorkspace,
8
+ buildStatusPill,
10
9
  buildSummaryBlock,
11
10
  DEFAULT_PANEL_PALETTE,
12
- resolvePrimaryScrollableSection,
13
- type PanelWorkspaceSection,
11
+ type PanelPalette,
14
12
  } from './polish.ts';
15
13
  import { getSettingsControlPlaneSnapshot } from '@pellux/goodvibes-sdk/platform/runtime/settings/control-plane';
16
14
  import type { ConfigManager } from '../config/index.ts';
@@ -24,141 +22,99 @@ const C = {
24
22
  error: '#ef4444',
25
23
  } as const;
26
24
 
27
- export class SettingsSyncPanel extends BasePanel {
28
- private selectedIndex = 0;
29
- private scrollOffset = 0;
25
+ type ResolvedEntry = ReturnType<typeof getSettingsControlPlaneSnapshot>['resolvedEntries'][number];
30
26
 
27
+ export class SettingsSyncPanel extends ScrollableListPanel<ResolvedEntry> {
31
28
  public constructor(private readonly configManager: ConfigManager) {
32
29
  super('settings-sync', 'Settings Sync', 'S', 'monitoring');
30
+ this.showSelectionGutter = true; // I5: non-color selection affordance
33
31
  }
34
32
 
35
- public handleInput(key: string): boolean {
36
- const total = getSettingsControlPlaneSnapshot(this.configManager).resolvedEntries.length;
37
- if (total <= 0) return false;
38
- if (key === 'up' || key === 'k') {
39
- this.selectedIndex = Math.max(0, this.selectedIndex - 1);
40
- this.markDirty();
41
- return true;
42
- }
43
- if (key === 'down' || key === 'j') {
44
- this.selectedIndex = Math.min(total - 1, this.selectedIndex + 1);
45
- this.markDirty();
46
- return true;
47
- }
48
- return false;
33
+ protected override getPalette(): PanelPalette {
34
+ return C;
35
+ }
36
+
37
+ protected getItems(): readonly ResolvedEntry[] {
38
+ return getSettingsControlPlaneSnapshot(this.configManager).resolvedEntries;
39
+ }
40
+
41
+ protected renderItem(entry: ResolvedEntry, _index: number, selected: boolean, width: number): Line {
42
+ return buildPanelListRow(width, [
43
+ { text: entry.key.padEnd(32), fg: C.value },
44
+ { text: ` ${entry.effectiveSource}`.padEnd(11), fg: entry.effectiveSource === 'managed' ? C.warn : entry.effectiveSource === 'synced' ? C.ok : entry.effectiveSource === 'local' ? C.info : C.dim },
45
+ { text: `${String(entry.effectiveValue)}`.slice(0, Math.max(0, width - 47)), fg: entry.locked ? C.warn : C.dim },
46
+ ], C, { selected });
47
+ }
48
+
49
+ protected override getEmptyStateMessage(): string {
50
+ return ' No resolved settings entries.';
49
51
  }
50
52
 
51
53
  public render(width: number, height: number): Line[] {
52
- this.needsRender = false;
53
54
  const snapshot = getSettingsControlPlaneSnapshot(this.configManager);
54
- const safeSelectedIndex = Math.max(0, Math.min(this.selectedIndex, Math.max(0, snapshot.resolvedEntries.length - 1)));
55
- this.selectedIndex = safeSelectedIndex;
55
+
56
56
  const postureLines: Line[] = [
57
- buildPanelLine(width, [[' resolved keys ', C.label], [String(snapshot.resolvedEntries.length), C.value], [' conflicts ', C.label], [String(snapshot.conflicts.length), snapshot.conflicts.length > 0 ? C.error : C.good], [' failures ', C.label], [String(snapshot.recentFailures.length), snapshot.recentFailures.length > 0 ? C.warn : C.good]]),
57
+ buildPanelLine(width, [[' resolved keys ', C.label], [String(snapshot.resolvedEntries.length), C.value], [' conflicts ', C.label], ...buildStatusPill(snapshot.conflicts.length > 0 ? 'bad' : 'good', String(snapshot.conflicts.length)), [' failures ', C.label], ...buildStatusPill(snapshot.recentFailures.length > 0 ? 'warn' : 'good', String(snapshot.recentFailures.length))]),
58
58
  buildPanelLine(width, [[' managed locks ', C.label], [String(snapshot.managedLockCount), snapshot.managedLockCount > 0 ? C.warn : C.dim], [' staged bundle ', C.label], [snapshot.stagedManagedBundle ? snapshot.stagedManagedBundle.profileName : 'none', snapshot.stagedManagedBundle ? C.info : C.dim]]),
59
59
  buildGuidanceLine(width, '/settingssync conflicts', 'review conflicting synced values before they silently shape effective configuration', C),
60
60
  buildGuidanceLine(width, '/managed review', 'inspect staged managed changes, risk posture, and rollback records', C),
61
61
  ];
62
- const prefixSections: PanelWorkspaceSection[] = [
63
- {
64
- lines: buildSummaryBlock(width, 'Settings posture', postureLines, C),
65
- },
66
- {
67
- title: 'Layers',
68
- lines: [
69
- buildPanelLine(width, [[' local typed config ', C.label], [String(snapshot.liveKeyCount), C.value], [' saved profiles ', C.label], [String(snapshot.profileCount), C.info], [' managed locks ', C.label], [String(snapshot.managedLockCount), snapshot.managedLockCount > 0 ? C.warn : C.dim]]),
70
- buildPanelLine(width, [[' effective local ', C.label], [String(snapshot.resolvedCounts.local), C.info], [' synced ', C.label], [String(snapshot.resolvedCounts.synced), snapshot.resolvedCounts.synced > 0 ? C.ok : C.dim], [' managed ', C.label], [String(snapshot.resolvedCounts.managed), snapshot.resolvedCounts.managed > 0 ? C.warn : C.dim]]),
71
- buildPanelLine(width, [[' last sync ', C.label], [snapshot.lastSync ? `${snapshot.lastSync.surface}/${snapshot.lastSync.direction}` : 'none', snapshot.lastSync ? C.ok : C.dim], [' when ', C.label], [snapshot.lastSync ? new Date(snapshot.lastSync.timestamp).toLocaleString() : 'n/a', C.dim]]),
72
- ],
73
- },
74
- {
75
- title: 'Staged Bundle',
76
- lines: snapshot.stagedManagedBundle
77
- ? [
78
- buildPanelLine(width, [[' profile ', C.label], [snapshot.stagedManagedBundle.profileName, C.value], [' risk ', C.label], [snapshot.stagedManagedBundle.risk, snapshot.stagedManagedBundle.risk === 'high' ? C.error : snapshot.stagedManagedBundle.risk === 'medium' ? C.warn : C.ok], [' changes ', C.label], [String(snapshot.stagedManagedBundle.changeCount), C.info]]),
79
- buildPanelLine(width, [[' path ', C.label], [snapshot.stagedManagedBundle.path.slice(0, Math.max(0, width - 9)), C.dim]]),
80
- ]
81
- : [buildPanelLine(width, [[' No staged managed settings bundle.', C.dim]])],
82
- },
83
- {
84
- title: 'Recent Events',
85
- lines: snapshot.recentEvents.length > 0
86
- ? snapshot.recentEvents.map((event) => buildPanelLine(width, [[` ${event.surface}/${event.direction}`.padEnd(18), C.info], [` ${event.detail}`.slice(0, Math.max(0, width - 20)), C.dim]]))
87
- : [buildPanelLine(width, [[' No sync or managed-setting events recorded yet.', C.dim]])],
88
- },
89
- {
90
- title: 'Managed Locks',
91
- lines: snapshot.managedLocks.length > 0
92
- ? snapshot.managedLocks.slice(0, 10).map((lock) => buildPanelLine(width, [[` ${lock.key}`.padEnd(30), C.value], [` source=${lock.source}`.padEnd(24), C.info], [` ${lock.reason}`.slice(0, Math.max(0, width - 56)), C.dim]]))
93
- : [buildPanelLine(width, [[' No managed locks are currently active.', C.dim]])],
94
- },
95
- {
96
- title: 'Failures',
97
- lines: snapshot.recentFailures.length > 0
98
- ? snapshot.recentFailures.map((failure) => buildPanelLine(width, [[` ${failure.surface}`.padEnd(10), C.error], [` ${failure.message}`.slice(0, Math.max(0, width - 12)), C.dim]]))
99
- : [buildPanelLine(width, [[' No recent sync or managed-setting failures.', C.dim]])],
100
- },
101
- {
102
- title: 'Conflicts',
103
- lines: snapshot.conflicts.length > 0
104
- ? snapshot.conflicts.map((conflict) => buildPanelLine(width, [[` ${conflict.key}`.padEnd(30), C.value], [` ${conflict.source}`.padEnd(10), C.warn], [` resolve: /settingssync resolve ${conflict.key} local|synced`.slice(0, Math.max(0, width - 42)), C.dim]]))
105
- : [buildPanelLine(width, [[' No settings conflicts detected.', C.dim]])],
106
- },
107
- {
108
- title: 'Rollback History',
109
- lines: snapshot.rollbackHistory.length > 0
110
- ? snapshot.rollbackHistory.map((entry) => buildPanelLine(width, [[` ${entry.token}`.padEnd(18), C.info], [` ${entry.profileName}`.padEnd(18), C.value], [` restored=${String(entry.restoredKeys.length).padEnd(4)}`, C.warn], [` ${new Date(entry.appliedAt).toLocaleString()}`.slice(0, Math.max(0, width - 46)), C.dim]]))
111
- : [buildPanelLine(width, [[' No managed rollback records yet.', C.dim]])],
112
- },
62
+
63
+ const headerLines: Line[] = [
64
+ ...buildSummaryBlock(width, 'Settings posture', postureLines, C),
65
+ buildPanelLine(width, [[' local typed config ', C.label], [String(snapshot.liveKeyCount), C.value], [' saved profiles ', C.label], [String(snapshot.profileCount), C.info], [' managed locks ', C.label], [String(snapshot.managedLockCount), snapshot.managedLockCount > 0 ? C.warn : C.dim]]),
66
+ buildPanelLine(width, [[' effective local ', C.label], [String(snapshot.resolvedCounts.local), C.info], [' synced ', C.label], [String(snapshot.resolvedCounts.synced), snapshot.resolvedCounts.synced > 0 ? C.ok : C.dim], [' managed ', C.label], [String(snapshot.resolvedCounts.managed), snapshot.resolvedCounts.managed > 0 ? C.warn : C.dim]]),
67
+ buildPanelLine(width, [[' last sync ', C.label], [snapshot.lastSync ? `${snapshot.lastSync.surface}/${snapshot.lastSync.direction}` : 'none', snapshot.lastSync ? C.ok : C.dim], [' when ', C.label], [snapshot.lastSync ? new Date(snapshot.lastSync.timestamp).toLocaleString() : 'n/a', C.dim]]),
68
+ // Staged Bundle
69
+ ...(snapshot.stagedManagedBundle
70
+ ? [
71
+ buildPanelLine(width, [[' profile ', C.label], [snapshot.stagedManagedBundle.profileName, C.value], [' risk ', C.label], [snapshot.stagedManagedBundle.risk, snapshot.stagedManagedBundle.risk === 'high' ? C.error : snapshot.stagedManagedBundle.risk === 'medium' ? C.warn : C.ok], [' changes ', C.label], [String(snapshot.stagedManagedBundle.changeCount), C.info]]),
72
+ buildPanelLine(width, [[' path ', C.label], [snapshot.stagedManagedBundle.path.slice(0, Math.max(0, width - 9)), C.dim]]),
73
+ ]
74
+ : [buildPanelLine(width, [[' No staged managed settings bundle.', C.dim]])]),
75
+ // Recent Events
76
+ ...(snapshot.recentEvents.length > 0
77
+ ? snapshot.recentEvents.map((event) => buildPanelLine(width, [[` ${event.surface}/${event.direction}`.padEnd(18), C.info], [` ${event.detail}`.slice(0, Math.max(0, width - 20)), C.dim]]))
78
+ : [buildPanelLine(width, [[' No sync or managed-setting events recorded yet.', C.dim]])]),
79
+ // Managed Locks
80
+ ...(snapshot.managedLocks.length > 0
81
+ ? snapshot.managedLocks.slice(0, 10).map((lock) => buildPanelLine(width, [[` ${lock.key}`.padEnd(30), C.value], [` source=${lock.source}`.padEnd(24), C.info], [` ${lock.reason}`.slice(0, Math.max(0, width - 56)), C.dim]]))
82
+ : [buildPanelLine(width, [[' No managed locks are currently active.', C.dim]])]),
83
+ // Failures
84
+ ...(snapshot.recentFailures.length > 0
85
+ ? snapshot.recentFailures.map((failure) => buildPanelLine(width, [[` ${failure.surface}`.padEnd(10), C.error], [` ${failure.message}`.slice(0, Math.max(0, width - 12)), C.dim]]))
86
+ : [buildPanelLine(width, [[' No recent sync or managed-setting failures.', C.dim]])]),
87
+ // Conflicts
88
+ ...(snapshot.conflicts.length > 0
89
+ ? snapshot.conflicts.map((conflict) => buildPanelLine(width, [[` ${conflict.key}`.padEnd(30), C.value], [` ${conflict.source}`.padEnd(10), C.warn], [` resolve: /settingssync resolve ${conflict.key} local|synced`.slice(0, Math.max(0, width - 42)), C.dim]]))
90
+ : [buildPanelLine(width, [[' No settings conflicts detected.', C.dim]])]),
91
+ // Rollback History
92
+ ...(snapshot.rollbackHistory.length > 0
93
+ ? snapshot.rollbackHistory.map((entry) => buildPanelLine(width, [[` ${entry.token}`.padEnd(18), C.info], [` ${entry.profileName}`.padEnd(18), C.value], [` restored=${String(entry.restoredKeys.length).padEnd(4)}`, C.warn], [` ${new Date(entry.appliedAt).toLocaleString()}`.slice(0, Math.max(0, width - 46)), C.dim]]))
94
+ : [buildPanelLine(width, [[' No managed rollback records yet.', C.dim]])]),
113
95
  ];
114
- const selected = snapshot.resolvedEntries[this.selectedIndex];
115
- const selectedSections = !selected ? [] as PanelWorkspaceSection[] : [{
116
- lines: buildDetailBlock(width, 'Selected setting', [
117
- buildPanelLine(width, [[' key ', C.label], [selected.key, C.value], [' category ', C.label], [selected.category, C.info]]),
118
- buildPanelLine(width, [[' effective ', C.label], [selected.effectiveSource, selected.effectiveSource === 'managed' ? C.warn : selected.effectiveSource === 'synced' ? C.ok : selected.effectiveSource === 'local' ? C.info : C.dim], [' locked ', C.label], [selected.locked ? 'yes' : 'no', selected.locked ? C.warn : C.dim], [' conflict ', C.label], [selected.conflict ? 'yes' : 'no', selected.conflict ? C.error : C.good]]),
119
- buildPanelLine(width, [[' source ', C.label], [(selected.sourceLabel ?? 'local/default').slice(0, Math.max(0, width - 10)), C.dim]]),
120
- buildPanelLine(width, [[' overrides ', C.label], [(selected.overriddenSources.length > 0 ? selected.overriddenSources.join(', ') : 'none').slice(0, Math.max(0, width - 13)), C.dim]]),
121
- buildPanelLine(width, [[' local ', C.label], [String(selected.localValue).slice(0, Math.max(0, width - 9)), C.dim]]),
122
- buildPanelLine(width, [[' synced ', C.label], [String(selected.syncedValue ?? '(unset)').slice(0, Math.max(0, width - 10)), C.ok]]),
123
- buildPanelLine(width, [[' managed ', C.label], [String(selected.managedValue ?? '(unset)').slice(0, Math.max(0, width - 11)), C.warn]]),
124
- ], C),
125
- }];
126
- const resolvedEntriesSection = resolvePrimaryScrollableSection(width, height, {
127
- intro: 'Local typed config, synced and managed layers, staged bundle review, conflicts, and control-plane failures.',
128
- footerLines: [buildPanelLine(width, [[' ↑/↓ browse /settingssync show <key> /settingssync resolve <key> <local|synced> /managed apply-staged [key...] ', C.dim]])],
129
- palette: C,
130
- beforeSections: prefixSections,
131
- section: {
132
- title: 'Resolved Entries',
133
- scrollableLines: snapshot.resolvedEntries.map((entry, absolute) => {
134
- return buildPanelListRow(width, [
135
- { text: entry.key.padEnd(32), fg: C.value },
136
- { text: ` ${entry.effectiveSource}`.padEnd(11), fg: entry.effectiveSource === 'managed' ? C.warn : entry.effectiveSource === 'synced' ? C.ok : entry.effectiveSource === 'local' ? C.info : C.dim },
137
- { text: `${String(entry.effectiveValue)}`.slice(0, Math.max(0, width - 47)), fg: entry.locked ? C.warn : C.dim },
138
- ], C, { selected: absolute === this.selectedIndex });
139
- }),
140
- selectedIndex: this.selectedIndex,
141
- scrollOffset: this.scrollOffset,
142
- guardRows: 1,
143
- minRows: 4,
144
- appendWindowSummary: { dimColor: C.dim },
145
- },
146
- afterSections: selectedSections,
147
- });
148
- this.scrollOffset = resolvedEntriesSection.scrollOffset;
149
- const sections: PanelWorkspaceSection[] = [
150
- ...prefixSections,
151
- resolvedEntriesSection.section,
152
- ...selectedSections,
96
+
97
+ this.clampSelection();
98
+ const selectedEntry = snapshot.resolvedEntries[this.selectedIndex];
99
+ const footerLines: Line[] = [
100
+ ...(selectedEntry
101
+ ? buildDetailBlock(width, 'Selected setting', [
102
+ buildPanelLine(width, [[' key ', C.label], [selectedEntry.key, C.value], [' category ', C.label], [selectedEntry.category, C.info]]),
103
+ buildPanelLine(width, [[' effective ', C.label], [selectedEntry.effectiveSource, selectedEntry.effectiveSource === 'managed' ? C.warn : selectedEntry.effectiveSource === 'synced' ? C.ok : selectedEntry.effectiveSource === 'local' ? C.info : C.dim], [' locked ', C.label], [selectedEntry.locked ? 'yes' : 'no', selectedEntry.locked ? C.warn : C.dim], [' conflict ', C.label], [selectedEntry.conflict ? 'yes' : 'no', selectedEntry.conflict ? C.error : C.good]]),
104
+ buildPanelLine(width, [[' source ', C.label], [(selectedEntry.sourceLabel ?? 'local/default').slice(0, Math.max(0, width - 10)), C.dim]]),
105
+ buildPanelLine(width, [[' overrides ', C.label], [(selectedEntry.overriddenSources.length > 0 ? selectedEntry.overriddenSources.join(', ') : 'none').slice(0, Math.max(0, width - 13)), C.dim]]),
106
+ buildPanelLine(width, [[' local ', C.label], [String(selectedEntry.localValue).slice(0, Math.max(0, width - 9)), C.dim]]),
107
+ buildPanelLine(width, [[' synced ', C.label], [String(selectedEntry.syncedValue ?? '(unset)').slice(0, Math.max(0, width - 10)), C.ok]]),
108
+ buildPanelLine(width, [[' managed ', C.label], [String(selectedEntry.managedValue ?? '(unset)').slice(0, Math.max(0, width - 11)), C.warn]]),
109
+ ], C)
110
+ : []),
111
+ buildPanelLine(width, [[' ↑/↓ browse /settingssync show <key> /settingssync resolve <key> <local|synced> /managed apply-staged [key...] ', C.dim]]),
153
112
  ];
154
- const lines = buildPanelWorkspace(width, height, {
113
+
114
+ return this.renderList(width, height, {
155
115
  title: 'Settings Sync',
156
- intro: 'Local typed config, synced and managed layers, staged bundle review, conflicts, and control-plane failures.',
157
- sections,
158
- footerLines: [buildPanelLine(width, [[' ↑/↓ browse /settingssync show <key> /settingssync resolve <key> <local|synced> /managed apply-staged [key...] ', C.dim]])],
159
- palette: C,
116
+ header: headerLines,
117
+ footer: footerLines,
160
118
  });
161
- while (lines.length < height) lines.push(createEmptyLine(width));
162
- return lines.slice(0, height);
163
119
  }
164
120
  }