@pellux/goodvibes-tui 0.26.0 → 0.28.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 (148) hide show
  1. package/CHANGELOG.md +40 -0
  2. package/README.md +12 -7
  3. package/docs/foundation-artifacts/operator-contract.json +4 -1
  4. package/package.json +2 -2
  5. package/src/cli/bundle-command.ts +2 -1
  6. package/src/cli/service-posture.ts +2 -1
  7. package/src/core/context-usage.ts +53 -0
  8. package/src/core/conversation-rendering.ts +2 -2
  9. package/src/core/conversation.ts +53 -8
  10. package/src/core/session-recovery.ts +26 -18
  11. package/src/core/system-message-router.ts +42 -26
  12. package/src/core/turn-event-wiring.ts +13 -16
  13. package/src/daemon/handlers/calendar/caldav-client.ts +2 -1
  14. package/src/daemon/handlers/inbox/index.ts +2 -1
  15. package/src/daemon/handlers/inbox/poller.ts +2 -1
  16. package/src/daemon/handlers/inbox/providers/discord.ts +2 -1
  17. package/src/daemon/handlers/inbox/providers/email.ts +2 -1
  18. package/src/daemon/handlers/inbox/providers/route-util.ts +2 -1
  19. package/src/daemon/handlers/inbox/providers/slack.ts +2 -1
  20. package/src/daemon/handlers/triage/integration.ts +2 -1
  21. package/src/daemon/handlers/triage/pipeline.ts +2 -1
  22. package/src/daemon/handlers/triage/tagger/discord.ts +2 -1
  23. package/src/daemon/handlers/triage/tagger/imap.ts +4 -3
  24. package/src/export/gist-uploader.ts +3 -1
  25. package/src/input/command-registry.ts +1 -0
  26. package/src/input/commands/cloudflare-runtime.ts +2 -1
  27. package/src/input/commands/eval.ts +4 -3
  28. package/src/input/commands/guidance-runtime.ts +2 -4
  29. package/src/input/commands/health-runtime.ts +13 -7
  30. package/src/input/commands/knowledge.ts +2 -1
  31. package/src/input/commands/runtime-services.ts +40 -10
  32. package/src/input/handler-command-route.ts +1 -1
  33. package/src/input/handler-feed-routes.ts +61 -4
  34. package/src/input/handler-feed.ts +13 -0
  35. package/src/input/handler-interactions.ts +2 -1
  36. package/src/input/handler-modal-routes.ts +2 -1
  37. package/src/input/handler-onboarding-cloudflare.ts +2 -1
  38. package/src/input/handler-onboarding.ts +8 -8
  39. package/src/input/handler-picker-routes.ts +18 -13
  40. package/src/input/handler-ui-state.ts +5 -1
  41. package/src/input/keybindings.ts +5 -0
  42. package/src/input/model-picker.ts +5 -0
  43. package/src/input/panel-integration-actions.ts +10 -0
  44. package/src/input/tts-settings-actions.ts +2 -1
  45. package/src/main.ts +52 -60
  46. package/src/panels/agent-inspector-panel.ts +90 -15
  47. package/src/panels/agent-inspector-shared.ts +3 -3
  48. package/src/panels/agent-logs-panel.ts +149 -72
  49. package/src/panels/approval-panel.ts +146 -95
  50. package/src/panels/automation-control-panel.ts +24 -1
  51. package/src/panels/base-panel.ts +28 -3
  52. package/src/panels/builtin/agent.ts +3 -1
  53. package/src/panels/builtin/development.ts +2 -1
  54. package/src/panels/builtin/session.ts +1 -1
  55. package/src/panels/cockpit-panel.ts +24 -5
  56. package/src/panels/cockpit-read-model.ts +2 -1
  57. package/src/panels/communication-panel.ts +108 -43
  58. package/src/panels/context-visualizer-panel.ts +49 -15
  59. package/src/panels/control-plane-panel.ts +27 -1
  60. package/src/panels/cost-tracker-panel.ts +87 -65
  61. package/src/panels/debug-panel.ts +61 -37
  62. package/src/panels/diff-panel.ts +59 -30
  63. package/src/panels/docs-panel.ts +30 -24
  64. package/src/panels/eval-panel.ts +130 -81
  65. package/src/panels/expandable-list-panel.ts +189 -0
  66. package/src/panels/file-explorer-panel.ts +42 -42
  67. package/src/panels/file-preview-panel.ts +11 -3
  68. package/src/panels/forensics-panel.ts +27 -13
  69. package/src/panels/git-panel.ts +65 -84
  70. package/src/panels/hooks-panel.ts +36 -2
  71. package/src/panels/incident-review-panel.ts +31 -10
  72. package/src/panels/intelligence-panel.ts +152 -71
  73. package/src/panels/knowledge-graph-panel.ts +89 -27
  74. package/src/panels/local-auth-panel.ts +17 -11
  75. package/src/panels/marketplace-panel.ts +33 -13
  76. package/src/panels/memory-panel.ts +7 -5
  77. package/src/panels/ops-control-panel.ts +69 -7
  78. package/src/panels/ops-strategy-panel.ts +61 -43
  79. package/src/panels/orchestration-panel.ts +34 -4
  80. package/src/panels/panel-list-panel.ts +33 -8
  81. package/src/panels/panel-manager.ts +23 -4
  82. package/src/panels/plan-dashboard-panel.ts +183 -66
  83. package/src/panels/plugins-panel.ts +48 -10
  84. package/src/panels/policy-panel.ts +60 -23
  85. package/src/panels/polish-core.ts +157 -0
  86. package/src/panels/polish-tables.ts +258 -0
  87. package/src/panels/polish.ts +44 -145
  88. package/src/panels/project-planning-panel.ts +27 -4
  89. package/src/panels/provider-accounts-panel.ts +55 -18
  90. package/src/panels/provider-health-panel.ts +118 -87
  91. package/src/panels/provider-stats-panel.ts +47 -22
  92. package/src/panels/qr-panel.ts +23 -11
  93. package/src/panels/remote-panel.ts +12 -5
  94. package/src/panels/routes-panel.ts +27 -5
  95. package/src/panels/sandbox-panel.ts +62 -27
  96. package/src/panels/schedule-panel.ts +44 -24
  97. package/src/panels/scrollable-list-panel.ts +124 -10
  98. package/src/panels/security-panel.ts +72 -20
  99. package/src/panels/services-panel.ts +68 -22
  100. package/src/panels/session-browser-panel.ts +42 -26
  101. package/src/panels/session-maintenance.ts +2 -2
  102. package/src/panels/settings-sync-panel.ts +30 -15
  103. package/src/panels/skills-panel.ts +2 -1
  104. package/src/panels/subscription-panel.ts +21 -3
  105. package/src/panels/symbol-outline-panel.ts +40 -47
  106. package/src/panels/system-messages-panel.ts +60 -27
  107. package/src/panels/tasks-panel.ts +36 -14
  108. package/src/panels/thinking-panel.ts +32 -36
  109. package/src/panels/token-budget-panel.ts +80 -53
  110. package/src/panels/tool-inspector-panel.ts +37 -39
  111. package/src/panels/types.ts +25 -0
  112. package/src/panels/watchers-panel.ts +25 -5
  113. package/src/panels/work-plan-panel.ts +127 -35
  114. package/src/panels/worktree-panel.ts +65 -20
  115. package/src/panels/wrfc-panel-format.ts +133 -0
  116. package/src/panels/wrfc-panel.ts +53 -147
  117. package/src/renderer/agent-detail-modal.ts +2 -2
  118. package/src/renderer/compaction-preview.ts +2 -1
  119. package/src/renderer/compositor.ts +46 -43
  120. package/src/renderer/modal-utils.ts +0 -10
  121. package/src/renderer/model-picker-overlay.ts +9 -4
  122. package/src/renderer/model-workspace.ts +2 -3
  123. package/src/renderer/panel-composite.ts +7 -20
  124. package/src/renderer/panel-workspace-bar.ts +14 -8
  125. package/src/renderer/process-modal.ts +2 -2
  126. package/src/renderer/progress.ts +3 -2
  127. package/src/renderer/tab-strip.ts +148 -34
  128. package/src/renderer/ui-factory.ts +4 -6
  129. package/src/runtime/bootstrap-command-context.ts +3 -0
  130. package/src/runtime/bootstrap-command-parts.ts +3 -1
  131. package/src/runtime/bootstrap-core.ts +31 -31
  132. package/src/runtime/bootstrap-shell.ts +1 -0
  133. package/src/runtime/onboarding/apply.ts +4 -3
  134. package/src/runtime/onboarding/snapshot.ts +4 -3
  135. package/src/runtime/process-lifecycle.ts +195 -0
  136. package/src/runtime/services.ts +4 -1
  137. package/src/runtime/ui/model-picker/health-enrichment.ts +3 -0
  138. package/src/runtime/ui/model-picker/types.ts +4 -0
  139. package/src/runtime/wrfc-persistence.ts +20 -5
  140. package/src/shell/ui-openers.ts +3 -2
  141. package/src/utils/format-duration.ts +55 -0
  142. package/src/utils/format-number.ts +71 -0
  143. package/src/verification/live-verifier.ts +4 -3
  144. package/src/version.ts +1 -1
  145. package/src/core/context-auto-compact.ts +0 -110
  146. package/src/panels/panel-picker.ts +0 -106
  147. package/src/renderer/panel-picker-overlay.ts +0 -202
  148. package/src/renderer/panel-tab-bar.ts +0 -69
@@ -1,9 +1,13 @@
1
1
  import type { Line } from '../types/grid.ts';
2
2
  import { createEmptyLine } from '../types/grid.ts';
3
+ import { truncateDisplay } from '../utils/terminal-width.ts';
3
4
  import { ScrollableListPanel } from './scrollable-list-panel.ts';
4
5
  import type { PluginManagerObserver, PluginStatus } from '@pellux/goodvibes-sdk/platform/plugins';
5
6
  import {
7
+ buildDetailBlock,
6
8
  buildEmptyState,
9
+ buildKeyValueLine,
10
+ buildKeyboardHints,
7
11
  buildPanelLine,
8
12
  buildPanelWorkspace,
9
13
  DEFAULT_PANEL_PALETTE,
@@ -53,10 +57,18 @@ export class PluginsPanel extends ScrollableListPanel<PluginStatus> {
53
57
  public constructor(manager: PluginManagerObserver) {
54
58
  super('plugins', 'Plugins', 'P', 'monitoring');
55
59
  this.showSelectionGutter = true; // I5: non-color selection affordance
60
+ this.filterEnabled = true;
61
+ this.filterLabel = 'Filter plugins';
56
62
  this.manager = manager;
57
63
  this.unsub = manager.subscribe(() => this.markDirty());
58
64
  }
59
65
 
66
+ protected override filterMatches(plugin: PluginStatus, q: string): boolean {
67
+ return plugin.name.toLowerCase().includes(q)
68
+ || plugin.trustTier.toLowerCase().includes(q)
69
+ || plugin.version.toLowerCase().includes(q);
70
+ }
71
+
60
72
  public override onActivate(): void {
61
73
  super.onActivate();
62
74
  this.selectedIndex = 0;
@@ -122,15 +134,30 @@ export class PluginsPanel extends ScrollableListPanel<PluginStatus> {
122
134
  return workspace;
123
135
  }
124
136
 
137
+ // Provenance/error posture header — surface trust + quarantine pressure first.
138
+ const quarantined = plugins.filter((p) => p.quarantined).length;
139
+ const untrusted = plugins.filter((p) => p.trustTier === 'untrusted').length;
140
+ const active = plugins.filter((p) => p.active).length;
141
+ const headerLines: Line[] = [
142
+ buildKeyValueLine(width, [
143
+ { label: 'plugins', value: String(plugins.length), valueColor: C.info },
144
+ { label: 'active', value: String(active), valueColor: active > 0 ? C.ok : C.dim },
145
+ { label: 'untrusted', value: String(untrusted), valueColor: untrusted > 0 ? C.warn : C.dim },
146
+ { label: 'quarantined', value: String(quarantined), valueColor: quarantined > 0 ? C.error : C.dim },
147
+ ], C),
148
+ ];
149
+
125
150
  this.clampSelection();
126
151
  const selected = plugins[this.selectedIndex]!;
127
152
  const selectedCaps = this.manager.capabilities(selected.name);
128
153
  const trustRecord = this.manager.getTrustRecord(selected.name);
129
154
  const quarantineRecord = this.manager.getQuarantineRecord(selected.name);
130
- const detailLines: Line[] = [
155
+ const detailRows: Line[] = [
131
156
  buildPanelLine(width, [
132
157
  [' Plugin: ', C.label],
133
158
  [selected.name, C.value],
159
+ [' v', C.label],
160
+ [selected.version, C.dim],
134
161
  [' State: ', C.label],
135
162
  [statusLabel(selected), statusColor(selected)],
136
163
  [' Trust: ', C.label],
@@ -138,12 +165,12 @@ export class PluginsPanel extends ScrollableListPanel<PluginStatus> {
138
165
  ]),
139
166
  buildPanelLine(width, [
140
167
  [' Description: ', C.label],
141
- [selected.description.slice(0, Math.max(0, width - 15)), C.dim],
168
+ [truncateDisplay(selected.description, Math.max(0, width - 15)), C.dim],
142
169
  ]),
143
170
  ];
144
171
 
145
172
  if (selectedCaps) {
146
- detailLines.push(buildPanelLine(width, [
173
+ detailRows.push(buildPanelLine(width, [
147
174
  [' Capabilities: ', C.label],
148
175
  [String(selectedCaps.requested.length), C.value],
149
176
  [' High-risk: ', C.label],
@@ -154,25 +181,36 @@ export class PluginsPanel extends ScrollableListPanel<PluginStatus> {
154
181
  }
155
182
 
156
183
  if (trustRecord?.signatureFingerprint) {
157
- detailLines.push(buildPanelLine(width, [
184
+ detailRows.push(buildPanelLine(width, [
158
185
  [' Signature: ', C.label],
159
- [trustRecord.signatureFingerprint, C.info],
186
+ [truncateDisplay(trustRecord.signatureFingerprint, Math.max(0, width - 14)), C.info],
160
187
  ]));
188
+ } else {
189
+ detailRows.push(buildPanelLine(width, [[' Signature: unsigned (no provenance fingerprint on record)', C.warn]]));
161
190
  }
162
191
 
163
192
  if (quarantineRecord) {
164
- detailLines.push(buildPanelLine(width, [
193
+ detailRows.push(buildPanelLine(width, [
165
194
  [' Quarantine: ', C.label],
166
- [quarantineRecord.reason.slice(0, Math.max(0, width - 14)), C.error],
195
+ [truncateDisplay(quarantineRecord.reason, Math.max(0, width - 14)), C.error],
167
196
  ]));
168
197
  }
169
198
 
170
- detailLines.push(buildPanelLine(width, [[' Inspect trust and capability state here, then use /plugin to take action.', C.dim]]));
171
- detailLines.push(buildPanelLine(width, [[' Up/Down move through discovered plugins', C.dim]]));
199
+ const hints = this.filterActive
200
+ ? [{ keys: 'type', label: 'filter' }, { keys: 'Enter', label: 'apply' }, { keys: 'Esc', label: 'clear' }]
201
+ : [
202
+ { keys: 'Up/Down', label: 'move' },
203
+ { keys: '/plugin', label: 'act' },
204
+ { keys: '/', label: 'filter' },
205
+ ];
172
206
 
173
207
  return this.renderList(width, height, {
174
208
  title: 'Plugin Control Room',
175
- footer: detailLines,
209
+ header: headerLines,
210
+ footer: [
211
+ ...buildDetailBlock(width, `Plugin · ${selected.name}`, detailRows, C),
212
+ buildKeyboardHints(width, hints, C),
213
+ ],
176
214
  });
177
215
  }
178
216
  }
@@ -1,9 +1,13 @@
1
1
  import type { Line } from '../types/grid.ts';
2
2
  import { createEmptyLine } from '../types/grid.ts';
3
+ import { truncateDisplay } from '../utils/terminal-width.ts';
3
4
  import { BasePanel } from './base-panel.ts';
4
5
  import type { PolicyRuntimeState } from '@/runtime/index.ts';
5
6
  import type { PolicyPanelSnapshot } from '../runtime/diagnostics/panels/policy.ts';
6
7
  import {
8
+ buildEmptyState,
9
+ buildKeyboardHints,
10
+ buildKeyValueLine,
7
11
  buildPanelLine,
8
12
  buildPanelWorkspace,
9
13
  resolveScrollablePanelSection,
@@ -89,22 +93,30 @@ export class PolicyPanel extends BasePanel {
89
93
  public render(width: number, height: number): Line[] {
90
94
  this.needsRender = false;
91
95
  const snapshot = this._state.getSnapshot();
96
+ const summaryLine = this._buildSummary(width, snapshot);
92
97
  const content = this._buildContent(width, snapshot);
98
+ const hintsLine = buildKeyboardHints(width, [
99
+ { keys: '↑/↓', label: 'scroll' },
100
+ { keys: 'r', label: 'record divergence snapshot' },
101
+ ], C);
102
+ const summarySection = { lines: [summaryLine] };
93
103
  const governanceSection = resolveScrollablePanelSection(width, height, {
94
- footerLines: [buildPanelLine(width, [[' Up/Down scroll r record divergence trend snapshot', C.dim]])],
104
+ footerLines: [hintsLine],
95
105
  palette: C,
106
+ beforeSections: [summarySection],
96
107
  section: {
97
108
  title: 'Governance',
98
109
  scrollableLines: content,
99
110
  scrollOffset: this._scrollOffset,
100
111
  minRows: 4,
112
+ appendWindowSummary: content.length > 0 ? { dimColor: C.dim } : undefined,
101
113
  },
102
114
  });
103
115
  this._scrollOffset = governanceSection.scrollOffset;
104
116
  const lines = buildPanelWorkspace(width, height, {
105
117
  title: 'Policy And Governance',
106
- sections: [governanceSection.section],
107
- footerLines: [buildPanelLine(width, [[' Up/Down scroll r record divergence trend snapshot', C.dim]])],
118
+ sections: [summarySection, governanceSection.section],
119
+ footerLines: [hintsLine],
108
120
  palette: C,
109
121
  });
110
122
  while (lines.length < height) {
@@ -113,6 +125,28 @@ export class PolicyPanel extends BasePanel {
113
125
  return lines.slice(0, height);
114
126
  }
115
127
 
128
+ /** Top-of-panel posture summary: the highest-signal governance state at a glance. */
129
+ private _buildSummary(width: number, snapshot: PolicyPanelSnapshot): Line {
130
+ const preflight = snapshot.lastPreflightReview;
131
+ const divergence = snapshot.divergence;
132
+ const lintCount = snapshot.lintFindings.length;
133
+ const preflightStatus = preflight ? preflight.status : 'none';
134
+ const preflightColor = !preflight
135
+ ? C.dim
136
+ : preflight.status === 'pass'
137
+ ? C.ok
138
+ : preflight.status === 'warn'
139
+ ? C.warn
140
+ : C.error;
141
+ const gateStatus = divergence?.gate.status ?? 'n/a';
142
+ return buildKeyValueLine(width, [
143
+ { label: 'bundles', value: `${snapshot.current ? 1 : 0}+${snapshot.candidate ? 1 : 0}c`, valueColor: snapshot.current || snapshot.candidate ? C.value : C.dim },
144
+ { label: 'preflight', value: preflightStatus.toUpperCase(), valueColor: preflightColor },
145
+ { label: 'gate', value: gateStatus, valueColor: gateColor(gateStatus) },
146
+ { label: 'lint', value: String(lintCount), valueColor: lintCount > 0 ? C.warn : C.dim },
147
+ ], C);
148
+ }
149
+
116
150
  private _buildContent(width: number, snapshot: PolicyPanelSnapshot): Line[] {
117
151
  const lines: Line[] = [];
118
152
  const current = snapshot.current;
@@ -123,8 +157,22 @@ export class PolicyPanel extends BasePanel {
123
157
  const simulationSummary = snapshot.lastSimulationSummary;
124
158
  const preflightReview = snapshot.lastPreflightReview;
125
159
 
126
- if (!current && !candidate) {
127
- lines.push(buildPanelLine(width, [[' No policy bundles loaded. Use /policy load to begin.', C.empty]]));
160
+ const nothingRecorded = !current && !candidate && !divergence
161
+ && snapshot.history.length === 0 && permissionAudit.length === 0
162
+ && lintFindings.length === 0 && !simulationSummary && !preflightReview;
163
+ if (nothingRecorded) {
164
+ lines.push(...buildEmptyState(
165
+ width,
166
+ ' No policy bundles loaded.',
167
+ 'Load a policy bundle to inspect governance gates, divergence trends, permission audit, lint findings, and preflight posture.',
168
+ [
169
+ { command: '/policy load', summary: 'load a policy bundle to begin governance review' },
170
+ { command: '/policy preflight', summary: 'run a proactive preflight posture review' },
171
+ { command: '/policy simulate', summary: 'compare authoritative vs candidate decisions' },
172
+ ],
173
+ C,
174
+ ));
175
+ return lines;
128
176
  }
129
177
 
130
178
  if (current) {
@@ -220,7 +268,7 @@ export class PolicyPanel extends BasePanel {
220
268
  ]));
221
269
  lines.push(buildPanelLine(width, [
222
270
  [' ', C.label],
223
- [entry.summary.slice(0, Math.max(0, width - 6)), C.dim],
271
+ [truncateDisplay(entry.summary, Math.max(0, width - 6)), C.dim],
224
272
  ]));
225
273
  }
226
274
  }
@@ -233,17 +281,12 @@ export class PolicyPanel extends BasePanel {
233
281
  [' ', C.label],
234
282
  [finding.severity.toUpperCase(), color],
235
283
  [' ', C.label],
236
- [finding.message.slice(0, Math.max(0, width - 14)), color],
284
+ [truncateDisplay(finding.message, Math.max(0, width - 14)), color],
237
285
  ]));
238
286
  }
239
287
  }
240
288
 
241
- if (!preflightReview) {
242
- if (!current && !candidate && !divergence && snapshot.history.length === 0 && permissionAudit.length === 0 && lintFindings.length === 0 && !simulationSummary) {
243
- lines.push(buildPanelLine(width, [[' Preflight Review', C.label]]));
244
- lines.push(buildPanelLine(width, [[' No proactive preflight review recorded yet.', C.empty]]));
245
- }
246
- } else {
289
+ if (preflightReview) {
247
290
  lines.push(buildPanelLine(width, [[' Preflight Review', C.label]]));
248
291
  const statusColor =
249
292
  preflightReview.status === 'pass'
@@ -261,7 +304,7 @@ export class PolicyPanel extends BasePanel {
261
304
  ]));
262
305
  lines.push(buildPanelLine(width, [
263
306
  [' ', C.label],
264
- [preflightReview.summary.slice(0, Math.max(0, width - 2)), C.dim],
307
+ [truncateDisplay(preflightReview.summary, Math.max(0, width - 2)), C.dim],
265
308
  ]));
266
309
  for (const issue of preflightReview.issues.slice(0, 4)) {
267
310
  const issueColor = issue.severity === 'error' ? C.error : issue.severity === 'warn' ? C.warn : C.info;
@@ -269,17 +312,12 @@ export class PolicyPanel extends BasePanel {
269
312
  [' ', C.label],
270
313
  [issue.severity.toUpperCase(), issueColor],
271
314
  [' ', C.label],
272
- [issue.message.slice(0, Math.max(0, width - 14)), issueColor],
315
+ [truncateDisplay(issue.message, Math.max(0, width - 14)), issueColor],
273
316
  ]));
274
317
  }
275
318
  }
276
319
 
277
- if (!simulationSummary) {
278
- if (!current && !candidate && !divergence && snapshot.history.length === 0 && permissionAudit.length === 0 && lintFindings.length === 0 && !preflightReview) {
279
- lines.push(buildPanelLine(width, [[' Simulation Samples', C.label]]));
280
- lines.push(buildPanelLine(width, [[' No concrete simulation samples recorded yet.', C.empty]]));
281
- }
282
- } else {
320
+ if (simulationSummary) {
283
321
  lines.push(buildPanelLine(width, [[' Simulation Samples', C.label]]));
284
322
  lines.push(buildPanelLine(width, [
285
323
  [' Mode: ', C.label],
@@ -293,7 +331,7 @@ export class PolicyPanel extends BasePanel {
293
331
  const color = result.diverged ? C.warn : (result.authoritativeDecision.allowed ? C.ok : C.error);
294
332
  lines.push(buildPanelLine(width, [
295
333
  [' ', C.label],
296
- [result.scenario.label.slice(0, Math.max(0, width - 40)), C.value],
334
+ [truncateDisplay(result.scenario.label, Math.max(0, width - 40)), C.value],
297
335
  [' ', C.label],
298
336
  [(result.authoritativeDecision.allowed ? 'allow' : 'deny').toUpperCase(), color],
299
337
  [' ', C.label],
@@ -302,7 +340,6 @@ export class PolicyPanel extends BasePanel {
302
340
  }
303
341
  }
304
342
 
305
- lines.push(buildPanelLine(width, [[' /policy opens this panel. Press r to record a divergence trend snapshot.', C.dim]]));
306
343
  return lines;
307
344
  }
308
345
  }
@@ -0,0 +1,157 @@
1
+ import type { Line } from '../types/grid.ts';
2
+ import { createEmptyLine, createStyledCell } from '../types/grid.ts';
3
+ import { getDisplayWidth } from '../utils/terminal-width.ts';
4
+ import { UI_TONES } from '../renderer/ui-primitives.ts';
5
+
6
+ // ---------------------------------------------------------------------------
7
+ // Panel palette + core line primitives.
8
+ //
9
+ // The leaf foundation of the panel formatting toolkit: both `polish.ts` and
10
+ // `polish-tables.ts` build on these. Kept dependency-free (only grid /
11
+ // terminal-width / ui-primitives) so it never participates in an import cycle.
12
+ // All symbols are re-exported from `polish.ts` — panels import from there.
13
+ // ---------------------------------------------------------------------------
14
+
15
+ export interface PanelPalette {
16
+ readonly label: string;
17
+ readonly value: string;
18
+ readonly dim: string;
19
+ readonly info: string;
20
+ readonly good?: string;
21
+ readonly warn?: string;
22
+ readonly bad?: string;
23
+ readonly empty: string;
24
+ readonly header?: string;
25
+ readonly headerBg?: string;
26
+ readonly surfaceBg?: string;
27
+ readonly sectionBg?: string;
28
+ readonly summaryBg?: string;
29
+ readonly inputBg?: string;
30
+ readonly accent?: string;
31
+ readonly selectBg?: string;
32
+ }
33
+
34
+ export const DEFAULT_PANEL_PALETTE: Readonly<Required<PanelPalette>> = {
35
+ header: UI_TONES.fg.primary,
36
+ headerBg: UI_TONES.bg.title,
37
+ label: UI_TONES.fg.muted,
38
+ value: UI_TONES.fg.primary,
39
+ dim: UI_TONES.fg.dim,
40
+ info: UI_TONES.state.info,
41
+ good: UI_TONES.state.good,
42
+ warn: UI_TONES.state.warn,
43
+ bad: UI_TONES.state.bad,
44
+ empty: '#334155',
45
+ surfaceBg: UI_TONES.bg.surface,
46
+ sectionBg: UI_TONES.bg.section,
47
+ summaryBg: UI_TONES.bg.summary,
48
+ inputBg: UI_TONES.bg.input,
49
+ accent: UI_TONES.fg.secondary,
50
+ selectBg: UI_TONES.bg.selected,
51
+ } as const;
52
+
53
+ /**
54
+ * Extend the base panel palette with domain-specific colors.
55
+ *
56
+ * Convention: raw hex colors may only live inside a palette constant declared
57
+ * at the top of a panel file, not inline in render calls.
58
+ *
59
+ * @example
60
+ * ```ts
61
+ * const C = extendPalette(DEFAULT_PANEL_PALETTE, {
62
+ * decision: '#38bdf8',
63
+ * incident: '#ef4444',
64
+ * });
65
+ * ```
66
+ */
67
+ export function extendPalette<T extends Record<string, string>>(
68
+ base: typeof DEFAULT_PANEL_PALETTE,
69
+ extras: T,
70
+ ): typeof DEFAULT_PANEL_PALETTE & T {
71
+ return { ...base, ...extras };
72
+ }
73
+
74
+ export function buildPanelLine(
75
+ width: number,
76
+ segments: Array<StyledPanelSegment | [string, string, string?]>,
77
+ ): Line {
78
+ return buildStyledPanelLine(
79
+ width,
80
+ segments.map((seg) =>
81
+ Array.isArray(seg) ? { text: seg[0], fg: seg[1], bg: seg[2] } : seg,
82
+ ),
83
+ );
84
+ }
85
+
86
+ export interface StyledPanelSegment {
87
+ readonly text: string;
88
+ readonly fg: string;
89
+ readonly bg?: string;
90
+ readonly bold?: boolean;
91
+ readonly dim?: boolean;
92
+ }
93
+
94
+ export function buildSelectablePanelLine(
95
+ width: number,
96
+ segments: ReadonlyArray<StyledPanelSegment>,
97
+ options: { selected?: boolean; selectedBg?: string; fillFg?: string; fillBg?: string; leadingMarker?: string } = {},
98
+ ): Line {
99
+ const selected = options.selected ?? false;
100
+ const selectedBg = selected ? (options.selectedBg ?? DEFAULT_PANEL_PALETTE.selectBg) : '';
101
+ const fillBg = selectedBg || options.fillBg || '';
102
+ const fillFg = options.fillFg ?? '';
103
+ const cells = createEmptyLine(width);
104
+ if (fillBg) {
105
+ for (let col = 0; col < width; col++) {
106
+ cells[col] = createStyledCell(' ', { bg: fillBg, fg: fillFg });
107
+ }
108
+ }
109
+
110
+ let col = 0;
111
+ if (selected && options.leadingMarker) {
112
+ for (const ch of options.leadingMarker) {
113
+ const charWidth = getDisplayWidth(ch);
114
+ if (charWidth <= 0 || col + charWidth > width) break;
115
+ cells[col] = createStyledCell(ch, { fg: DEFAULT_PANEL_PALETTE.info, bg: selectedBg, bold: true });
116
+ if (charWidth > 1 && col + 1 < width) cells[col + 1] = createStyledCell(' ', { fg: DEFAULT_PANEL_PALETTE.info, bg: selectedBg, bold: true });
117
+ col += charWidth;
118
+ }
119
+ }
120
+ for (const segment of segments) {
121
+ const fg = segment.fg;
122
+ const bg = segment.bg ?? fillBg;
123
+ for (const ch of segment.text) {
124
+ const charWidth = getDisplayWidth(ch);
125
+ if (charWidth <= 0) continue;
126
+ if (col + charWidth > width) return cells;
127
+ cells[col] = createStyledCell(ch, {
128
+ fg,
129
+ bg,
130
+ bold: segment.bold ?? false,
131
+ dim: segment.dim ?? false,
132
+ });
133
+ if (charWidth > 1 && col + 1 < width) {
134
+ cells[col + 1] = createStyledCell(' ', {
135
+ fg,
136
+ bg,
137
+ bold: segment.bold ?? false,
138
+ dim: segment.dim ?? false,
139
+ });
140
+ }
141
+ col += charWidth;
142
+ }
143
+ }
144
+
145
+ while (col < width) {
146
+ cells[col++] = createStyledCell(' ', { bg: fillBg, fg: fillFg });
147
+ }
148
+ return cells;
149
+ }
150
+
151
+ export function buildStyledPanelLine(
152
+ width: number,
153
+ segments: ReadonlyArray<StyledPanelSegment>,
154
+ options: { fillBg?: string; fillFg?: string } = {},
155
+ ): Line {
156
+ return buildSelectablePanelLine(width, segments, options);
157
+ }