@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.
- package/CHANGELOG.md +40 -0
- package/README.md +12 -7
- package/docs/foundation-artifacts/operator-contract.json +4 -1
- package/package.json +2 -2
- package/src/cli/bundle-command.ts +2 -1
- package/src/cli/service-posture.ts +2 -1
- package/src/core/context-usage.ts +53 -0
- package/src/core/conversation-rendering.ts +2 -2
- package/src/core/conversation.ts +53 -8
- package/src/core/session-recovery.ts +26 -18
- package/src/core/system-message-router.ts +42 -26
- package/src/core/turn-event-wiring.ts +13 -16
- package/src/daemon/handlers/calendar/caldav-client.ts +2 -1
- package/src/daemon/handlers/inbox/index.ts +2 -1
- package/src/daemon/handlers/inbox/poller.ts +2 -1
- package/src/daemon/handlers/inbox/providers/discord.ts +2 -1
- package/src/daemon/handlers/inbox/providers/email.ts +2 -1
- package/src/daemon/handlers/inbox/providers/route-util.ts +2 -1
- package/src/daemon/handlers/inbox/providers/slack.ts +2 -1
- package/src/daemon/handlers/triage/integration.ts +2 -1
- package/src/daemon/handlers/triage/pipeline.ts +2 -1
- package/src/daemon/handlers/triage/tagger/discord.ts +2 -1
- package/src/daemon/handlers/triage/tagger/imap.ts +4 -3
- package/src/export/gist-uploader.ts +3 -1
- package/src/input/command-registry.ts +1 -0
- package/src/input/commands/cloudflare-runtime.ts +2 -1
- package/src/input/commands/eval.ts +4 -3
- package/src/input/commands/guidance-runtime.ts +2 -4
- package/src/input/commands/health-runtime.ts +13 -7
- package/src/input/commands/knowledge.ts +2 -1
- package/src/input/commands/runtime-services.ts +40 -10
- package/src/input/handler-command-route.ts +1 -1
- package/src/input/handler-feed-routes.ts +61 -4
- package/src/input/handler-feed.ts +13 -0
- package/src/input/handler-interactions.ts +2 -1
- package/src/input/handler-modal-routes.ts +2 -1
- package/src/input/handler-onboarding-cloudflare.ts +2 -1
- package/src/input/handler-onboarding.ts +8 -8
- package/src/input/handler-picker-routes.ts +18 -13
- package/src/input/handler-ui-state.ts +5 -1
- package/src/input/keybindings.ts +5 -0
- package/src/input/model-picker.ts +5 -0
- package/src/input/panel-integration-actions.ts +10 -0
- package/src/input/tts-settings-actions.ts +2 -1
- package/src/main.ts +52 -60
- package/src/panels/agent-inspector-panel.ts +90 -15
- package/src/panels/agent-inspector-shared.ts +3 -3
- package/src/panels/agent-logs-panel.ts +149 -72
- package/src/panels/approval-panel.ts +146 -95
- package/src/panels/automation-control-panel.ts +24 -1
- package/src/panels/base-panel.ts +28 -3
- package/src/panels/builtin/agent.ts +3 -1
- package/src/panels/builtin/development.ts +2 -1
- package/src/panels/builtin/session.ts +1 -1
- package/src/panels/cockpit-panel.ts +24 -5
- package/src/panels/cockpit-read-model.ts +2 -1
- package/src/panels/communication-panel.ts +108 -43
- package/src/panels/context-visualizer-panel.ts +49 -15
- package/src/panels/control-plane-panel.ts +27 -1
- package/src/panels/cost-tracker-panel.ts +87 -65
- package/src/panels/debug-panel.ts +61 -37
- package/src/panels/diff-panel.ts +59 -30
- package/src/panels/docs-panel.ts +30 -24
- package/src/panels/eval-panel.ts +130 -81
- package/src/panels/expandable-list-panel.ts +189 -0
- package/src/panels/file-explorer-panel.ts +42 -42
- package/src/panels/file-preview-panel.ts +11 -3
- package/src/panels/forensics-panel.ts +27 -13
- package/src/panels/git-panel.ts +65 -84
- package/src/panels/hooks-panel.ts +36 -2
- package/src/panels/incident-review-panel.ts +31 -10
- package/src/panels/intelligence-panel.ts +152 -71
- package/src/panels/knowledge-graph-panel.ts +89 -27
- package/src/panels/local-auth-panel.ts +17 -11
- package/src/panels/marketplace-panel.ts +33 -13
- package/src/panels/memory-panel.ts +7 -5
- package/src/panels/ops-control-panel.ts +69 -7
- package/src/panels/ops-strategy-panel.ts +61 -43
- package/src/panels/orchestration-panel.ts +34 -4
- package/src/panels/panel-list-panel.ts +33 -8
- package/src/panels/panel-manager.ts +23 -4
- package/src/panels/plan-dashboard-panel.ts +183 -66
- package/src/panels/plugins-panel.ts +48 -10
- package/src/panels/policy-panel.ts +60 -23
- package/src/panels/polish-core.ts +157 -0
- package/src/panels/polish-tables.ts +258 -0
- package/src/panels/polish.ts +44 -145
- package/src/panels/project-planning-panel.ts +27 -4
- package/src/panels/provider-accounts-panel.ts +55 -18
- package/src/panels/provider-health-panel.ts +118 -87
- package/src/panels/provider-stats-panel.ts +47 -22
- package/src/panels/qr-panel.ts +23 -11
- package/src/panels/remote-panel.ts +12 -5
- package/src/panels/routes-panel.ts +27 -5
- package/src/panels/sandbox-panel.ts +62 -27
- package/src/panels/schedule-panel.ts +44 -24
- package/src/panels/scrollable-list-panel.ts +124 -10
- package/src/panels/security-panel.ts +72 -20
- package/src/panels/services-panel.ts +68 -22
- package/src/panels/session-browser-panel.ts +42 -26
- package/src/panels/session-maintenance.ts +2 -2
- package/src/panels/settings-sync-panel.ts +30 -15
- package/src/panels/skills-panel.ts +2 -1
- package/src/panels/subscription-panel.ts +21 -3
- package/src/panels/symbol-outline-panel.ts +40 -47
- package/src/panels/system-messages-panel.ts +60 -27
- package/src/panels/tasks-panel.ts +36 -14
- package/src/panels/thinking-panel.ts +32 -36
- package/src/panels/token-budget-panel.ts +80 -53
- package/src/panels/tool-inspector-panel.ts +37 -39
- package/src/panels/types.ts +25 -0
- package/src/panels/watchers-panel.ts +25 -5
- package/src/panels/work-plan-panel.ts +127 -35
- package/src/panels/worktree-panel.ts +65 -20
- package/src/panels/wrfc-panel-format.ts +133 -0
- package/src/panels/wrfc-panel.ts +53 -147
- package/src/renderer/agent-detail-modal.ts +2 -2
- package/src/renderer/compaction-preview.ts +2 -1
- package/src/renderer/compositor.ts +46 -43
- package/src/renderer/modal-utils.ts +0 -10
- package/src/renderer/model-picker-overlay.ts +9 -4
- package/src/renderer/model-workspace.ts +2 -3
- package/src/renderer/panel-composite.ts +7 -20
- package/src/renderer/panel-workspace-bar.ts +14 -8
- package/src/renderer/process-modal.ts +2 -2
- package/src/renderer/progress.ts +3 -2
- package/src/renderer/tab-strip.ts +148 -34
- package/src/renderer/ui-factory.ts +4 -6
- package/src/runtime/bootstrap-command-context.ts +3 -0
- package/src/runtime/bootstrap-command-parts.ts +3 -1
- package/src/runtime/bootstrap-core.ts +31 -31
- package/src/runtime/bootstrap-shell.ts +1 -0
- package/src/runtime/onboarding/apply.ts +4 -3
- package/src/runtime/onboarding/snapshot.ts +4 -3
- package/src/runtime/process-lifecycle.ts +195 -0
- package/src/runtime/services.ts +4 -1
- package/src/runtime/ui/model-picker/health-enrichment.ts +3 -0
- package/src/runtime/ui/model-picker/types.ts +4 -0
- package/src/runtime/wrfc-persistence.ts +20 -5
- package/src/shell/ui-openers.ts +3 -2
- package/src/utils/format-duration.ts +55 -0
- package/src/utils/format-number.ts +71 -0
- package/src/verification/live-verifier.ts +4 -3
- package/src/version.ts +1 -1
- package/src/core/context-auto-compact.ts +0 -110
- package/src/panels/panel-picker.ts +0 -106
- package/src/renderer/panel-picker-overlay.ts +0 -202
- 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
|
|
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
|
|
168
|
+
[truncateDisplay(selected.description, Math.max(0, width - 15)), C.dim],
|
|
142
169
|
]),
|
|
143
170
|
];
|
|
144
171
|
|
|
145
172
|
if (selectedCaps) {
|
|
146
|
-
|
|
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
|
-
|
|
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
|
-
|
|
193
|
+
detailRows.push(buildPanelLine(width, [
|
|
165
194
|
[' Quarantine: ', C.label],
|
|
166
|
-
[quarantineRecord.reason
|
|
195
|
+
[truncateDisplay(quarantineRecord.reason, Math.max(0, width - 14)), C.error],
|
|
167
196
|
]));
|
|
168
197
|
}
|
|
169
198
|
|
|
170
|
-
|
|
171
|
-
|
|
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
|
-
|
|
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: [
|
|
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: [
|
|
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
|
-
|
|
127
|
-
|
|
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
|
|
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
|
|
284
|
+
[truncateDisplay(finding.message, Math.max(0, width - 14)), color],
|
|
237
285
|
]));
|
|
238
286
|
}
|
|
239
287
|
}
|
|
240
288
|
|
|
241
|
-
if (
|
|
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
|
|
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
|
|
315
|
+
[truncateDisplay(issue.message, Math.max(0, width - 14)), issueColor],
|
|
273
316
|
]));
|
|
274
317
|
}
|
|
275
318
|
}
|
|
276
319
|
|
|
277
|
-
if (
|
|
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
|
|
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
|
+
}
|