@pellux/goodvibes-tui 0.27.0 → 0.29.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 +124 -0
- package/README.md +1 -1
- package/package.json +1 -1
- package/src/core/context-usage.ts +53 -0
- package/src/core/conversation.ts +4 -6
- package/src/core/session-recovery.ts +2 -0
- package/src/core/turn-event-wiring.ts +1 -0
- package/src/input/commands/cost-runtime.ts +49 -0
- package/src/input/commands/eval.ts +4 -3
- package/src/input/commands/operator-runtime.ts +5 -1
- package/src/input/commands.ts +2 -0
- package/src/input/handler-feed-routes.ts +60 -26
- package/src/input/handler-feed.ts +17 -8
- package/src/input/handler-picker-routes.ts +18 -13
- package/src/input/handler-shortcuts.ts +51 -0
- package/src/input/handler-ui-state.ts +4 -0
- package/src/input/handler.ts +43 -6
- package/src/input/keybindings.ts +53 -8
- package/src/input/model-picker.ts +5 -0
- package/src/input/panel-integration-actions.ts +117 -1
- package/src/main.ts +5 -2
- package/src/panels/agent-inspector-panel.ts +139 -45
- package/src/panels/agent-inspector-shared.ts +147 -3
- package/src/panels/approval-panel.ts +204 -102
- package/src/panels/automation-control-panel.ts +370 -103
- package/src/panels/base-panel.ts +26 -2
- package/src/panels/builtin/agent.ts +28 -37
- package/src/panels/builtin/development.ts +40 -32
- package/src/panels/builtin/knowledge.ts +15 -6
- package/src/panels/builtin/operations.ts +178 -117
- package/src/panels/builtin/session.ts +35 -9
- package/src/panels/builtin/shared.ts +98 -6
- package/src/panels/cockpit-panel.ts +253 -75
- package/src/panels/cockpit-read-model.ts +2 -1
- package/src/panels/communication-panel.ts +159 -56
- package/src/panels/confirm-state.ts +8 -1
- package/src/panels/control-plane-panel.ts +354 -95
- package/src/panels/cost-tracker-panel.ts +243 -57
- package/src/panels/debug-panel.ts +347 -159
- package/src/panels/diff-panel.ts +257 -82
- package/src/panels/docs-panel.ts +166 -64
- package/src/panels/eval-panel.ts +336 -109
- package/src/panels/expandable-list-panel.ts +189 -0
- package/src/panels/file-explorer-panel.ts +238 -159
- package/src/panels/file-preview-panel.ts +141 -59
- package/src/panels/git-panel.ts +360 -191
- package/src/panels/hooks-panel.ts +181 -19
- package/src/panels/incident-review-panel.ts +252 -43
- package/src/panels/index.ts +0 -4
- package/src/panels/intelligence-panel.ts +310 -103
- package/src/panels/knowledge-graph-panel.ts +480 -58
- package/src/panels/local-auth-panel.ts +251 -33
- package/src/panels/marketplace-panel.ts +219 -33
- package/src/panels/memory-panel.ts +83 -81
- package/src/panels/ops-control-panel.ts +211 -32
- package/src/panels/ops-strategy-panel.ts +131 -45
- package/src/panels/orchestration-panel.ts +259 -67
- package/src/panels/panel-list-panel.ts +184 -136
- package/src/panels/panel-manager.ts +120 -13
- package/src/panels/plan-dashboard-panel.ts +507 -74
- package/src/panels/plugins-panel.ts +227 -34
- package/src/panels/policy-panel.ts +264 -55
- package/src/panels/polish-core.ts +162 -0
- package/src/panels/polish-tables.ts +258 -0
- package/src/panels/polish.ts +67 -149
- package/src/panels/project-planning-answer-actions.ts +134 -0
- package/src/panels/project-planning-panel.ts +84 -113
- package/src/panels/provider-health-panel.ts +438 -480
- package/src/panels/provider-health-routes.ts +203 -0
- package/src/panels/provider-health-tracker.ts +194 -6
- package/src/panels/provider-health-views.ts +560 -0
- package/src/panels/qr-panel.ts +136 -38
- package/src/panels/remote-panel.ts +123 -38
- package/src/panels/routes-panel.ts +87 -24
- package/src/panels/sandbox-panel.ts +232 -65
- package/src/panels/scrollable-list-panel.ts +143 -145
- package/src/panels/security-panel.ts +204 -52
- package/src/panels/services-panel.ts +174 -76
- package/src/panels/session-browser-panel.ts +101 -6
- package/src/panels/session-maintenance.ts +4 -122
- package/src/panels/settings-sync-panel.ts +346 -68
- package/src/panels/skills-panel.ts +157 -89
- package/src/panels/subscription-panel.ts +105 -34
- package/src/panels/symbol-outline-panel.ts +287 -154
- package/src/panels/system-messages-panel.ts +172 -38
- package/src/panels/tasks-panel.ts +348 -134
- package/src/panels/thinking-panel.ts +66 -32
- package/src/panels/token-budget-panel.ts +233 -46
- package/src/panels/tool-inspector-panel.ts +170 -54
- package/src/panels/types.ts +65 -4
- package/src/panels/work-plan-panel.ts +393 -39
- package/src/panels/worktree-panel.ts +239 -61
- package/src/panels/wrfc-panel-format.ts +133 -0
- package/src/panels/wrfc-panel.ts +146 -150
- package/src/renderer/compaction-preview.ts +2 -1
- package/src/renderer/compositor.ts +46 -43
- package/src/renderer/conversation-overlays.ts +25 -11
- package/src/renderer/footer-tips.ts +41 -0
- package/src/renderer/fullscreen-primitives.ts +22 -16
- package/src/renderer/help-overlay.ts +91 -14
- package/src/renderer/hint-grammar.ts +52 -0
- package/src/renderer/layout.ts +7 -7
- package/src/renderer/modal-factory.ts +23 -15
- package/src/renderer/model-picker-overlay.ts +30 -11
- package/src/renderer/model-workspace.ts +2 -3
- package/src/renderer/overlay-box.ts +16 -10
- package/src/renderer/panel-composite.ts +7 -20
- package/src/renderer/panel-workspace-bar.ts +14 -8
- package/src/renderer/process-indicator.ts +3 -1
- package/src/renderer/progress.ts +8 -6
- package/src/renderer/search-overlay.ts +27 -6
- package/src/renderer/session-picker-modal.ts +6 -4
- package/src/renderer/settings-modal.ts +70 -10
- package/src/renderer/shell-surface.ts +41 -15
- package/src/renderer/status-glyphs.ts +11 -9
- package/src/renderer/tab-strip.ts +148 -34
- package/src/renderer/theme.ts +60 -3
- package/src/renderer/ui-factory.ts +45 -36
- package/src/renderer/ui-primitives.ts +23 -2
- package/src/runtime/bootstrap-shell.ts +35 -18
- package/src/runtime/diagnostics/panels/index.ts +0 -3
- package/src/runtime/ui/model-picker/health-enrichment.ts +3 -0
- package/src/runtime/ui/model-picker/types.ts +4 -0
- package/src/shell/ui-openers.ts +14 -22
- package/src/utils/format-duration.ts +55 -0
- package/src/utils/format-number.ts +71 -0
- package/src/utils/splash-lines.ts +44 -3
- package/src/version.ts +1 -1
- package/src/work-plans/work-plan-store.ts +13 -0
- package/src/panels/agent-logs-panel.ts +0 -666
- package/src/panels/agent-logs-shared.ts +0 -129
- package/src/panels/context-visualizer-panel.ts +0 -214
- package/src/panels/forensics-panel.ts +0 -364
- package/src/panels/panel-picker.ts +0 -106
- package/src/panels/provider-account-snapshot.ts +0 -259
- package/src/panels/provider-accounts-panel.ts +0 -218
- package/src/panels/provider-stats-panel.ts +0 -366
- package/src/panels/schedule-panel.ts +0 -342
- package/src/panels/watchers-panel.ts +0 -193
- package/src/renderer/panel-picker-overlay.ts +0 -202
- package/src/renderer/panel-tab-bar.ts +0 -69
- package/src/runtime/diagnostics/panels/panel-resources.ts +0 -118
- package/src/runtime/diagnostics/panels/policy.ts +0 -177
|
@@ -1,28 +1,34 @@
|
|
|
1
|
+
import { logger } from '@pellux/goodvibes-sdk/platform/utils';
|
|
1
2
|
import type { Line } from '../types/grid.ts';
|
|
2
3
|
import { createEmptyLine } from '../types/grid.ts';
|
|
4
|
+
import { truncateDisplay } from '../utils/terminal-width.ts';
|
|
3
5
|
import { BasePanel } from './base-panel.ts';
|
|
4
|
-
import type {
|
|
5
|
-
|
|
6
|
+
import type {
|
|
7
|
+
PolicyRuntimeState,
|
|
8
|
+
PolicyBundleVersion,
|
|
9
|
+
PolicyDiffResult,
|
|
10
|
+
DivergenceDashboardSnapshot,
|
|
11
|
+
DivergenceStats,
|
|
12
|
+
PermissionAuditEntry,
|
|
13
|
+
PolicyLintFinding,
|
|
14
|
+
PolicySimulationSummary,
|
|
15
|
+
PolicyPreflightReview,
|
|
16
|
+
} from '@/runtime/index.ts';
|
|
6
17
|
import {
|
|
18
|
+
buildEmptyState,
|
|
19
|
+
buildKeyboardHints,
|
|
20
|
+
buildKeyValueLine,
|
|
7
21
|
buildPanelLine,
|
|
8
22
|
buildPanelWorkspace,
|
|
9
23
|
resolveScrollablePanelSection,
|
|
10
24
|
DEFAULT_PANEL_PALETTE,
|
|
11
25
|
} from './polish.ts';
|
|
26
|
+
import { type ConfirmState, handleConfirmInput, renderConfirmLines } from './confirm-state.ts';
|
|
27
|
+
import type { PanelIntegrationContext } from './types.ts';
|
|
12
28
|
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
headerBg: '#1e293b',
|
|
17
|
-
label: '#64748b',
|
|
18
|
-
value: '#e2e8f0',
|
|
19
|
-
dim: '#475569',
|
|
20
|
-
ok: '#22c55e',
|
|
21
|
-
warn: '#eab308',
|
|
22
|
-
error: '#ef4444',
|
|
23
|
-
info: '#38bdf8',
|
|
24
|
-
empty: '#334155',
|
|
25
|
-
} as const;
|
|
29
|
+
// Base chrome only — no domain accents needed; the title band, state colors,
|
|
30
|
+
// and text tokens all come straight from DEFAULT_PANEL_PALETTE (WO-002).
|
|
31
|
+
const C = DEFAULT_PANEL_PALETTE;
|
|
26
32
|
|
|
27
33
|
function fmtTime(value: string | undefined): string {
|
|
28
34
|
if (!value) return 'n/a';
|
|
@@ -37,9 +43,9 @@ function fmtRate(value: number | undefined): string {
|
|
|
37
43
|
function gateColor(status: string | undefined): string {
|
|
38
44
|
switch (status) {
|
|
39
45
|
case 'allowed':
|
|
40
|
-
return C.
|
|
46
|
+
return C.good;
|
|
41
47
|
case 'blocked':
|
|
42
|
-
return C.
|
|
48
|
+
return C.bad;
|
|
43
49
|
case 'no_data':
|
|
44
50
|
return C.warn;
|
|
45
51
|
default:
|
|
@@ -47,13 +53,49 @@ function gateColor(status: string | undefined): string {
|
|
|
47
53
|
}
|
|
48
54
|
}
|
|
49
55
|
|
|
56
|
+
/**
|
|
57
|
+
* A point-in-time snapshot of policy state for diagnostics rendering.
|
|
58
|
+
*/
|
|
59
|
+
interface PolicyPanelSnapshot {
|
|
60
|
+
/** The currently enforced bundle, or null if no policy is active. */
|
|
61
|
+
current: PolicyBundleVersion | null;
|
|
62
|
+
/** The pending candidate bundle, or null if none loaded. */
|
|
63
|
+
candidate: PolicyBundleVersion | null;
|
|
64
|
+
/** History of previous active bundles (most recent first). */
|
|
65
|
+
history: PolicyBundleVersion[];
|
|
66
|
+
/** Diff between current and candidate, or null if unavailable. */
|
|
67
|
+
diff: PolicyDiffResult | null;
|
|
68
|
+
/** Divergence dashboard snapshot, or null if no panel attached. */
|
|
69
|
+
divergence: DivergenceDashboardSnapshot | null;
|
|
70
|
+
/** Recent permission requests and decisions for operator audit review. */
|
|
71
|
+
recentPermissionAudit: readonly PermissionAuditEntry[];
|
|
72
|
+
/** Policy lint findings for current and candidate bundles. */
|
|
73
|
+
lintFindings: readonly PolicyLintFinding[];
|
|
74
|
+
/** Concrete scenario results from the most recent policy simulation run. */
|
|
75
|
+
lastSimulationSummary: PolicySimulationSummary | null;
|
|
76
|
+
/** Most recent proactive policy preflight review. */
|
|
77
|
+
lastPreflightReview: PolicyPreflightReview | null;
|
|
78
|
+
/** ISO 8601 timestamp of when this snapshot was captured. */
|
|
79
|
+
capturedAt: string;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/** Subactions dispatched through `/policy <name>` via `dispatchPolicyCommand`. */
|
|
83
|
+
type PolicyDispatchAction = 'simulate' | 'preflight' | 'lint' | 'promote' | 'rollback';
|
|
84
|
+
|
|
50
85
|
export class PolicyPanel extends BasePanel {
|
|
51
86
|
private readonly _state: PolicyRuntimeState;
|
|
52
87
|
private readonly _unsub: (() => void) | null;
|
|
53
88
|
private _scrollOffset = 0;
|
|
54
89
|
|
|
90
|
+
// Inline confirm for the gate-aware promote/rollback actions.
|
|
91
|
+
private _confirm: ConfirmState<PolicyDispatchAction> | null = null;
|
|
92
|
+
|
|
93
|
+
// Pending dispatch resolved via handlePanelIntegrationAction, which is the
|
|
94
|
+
// only place `executeCommand` is available (mirrors IncidentReviewPanel).
|
|
95
|
+
private _pendingAction: PolicyDispatchAction | null = null;
|
|
96
|
+
|
|
55
97
|
public constructor(state: PolicyRuntimeState) {
|
|
56
|
-
super('policy', 'Policy', '
|
|
98
|
+
super('policy', 'Policy', '▭', 'security-policy');
|
|
57
99
|
this._state = state;
|
|
58
100
|
this._unsub = state.subscribe(() => this.markDirty());
|
|
59
101
|
}
|
|
@@ -68,6 +110,22 @@ export class PolicyPanel extends BasePanel {
|
|
|
68
110
|
}
|
|
69
111
|
|
|
70
112
|
public handleInput(key: string): boolean {
|
|
113
|
+
if (this._confirm) {
|
|
114
|
+
const outcome = handleConfirmInput(this._confirm, key);
|
|
115
|
+
if (outcome === 'confirmed') {
|
|
116
|
+
this._pendingAction = this._confirm.subject;
|
|
117
|
+
this._confirm = null;
|
|
118
|
+
this.markDirty();
|
|
119
|
+
return true;
|
|
120
|
+
}
|
|
121
|
+
if (outcome === 'cancelled') {
|
|
122
|
+
this._confirm = null;
|
|
123
|
+
this.markDirty();
|
|
124
|
+
return true;
|
|
125
|
+
}
|
|
126
|
+
return true; // absorbed — keep the confirm dialog pending
|
|
127
|
+
}
|
|
128
|
+
|
|
71
129
|
if (key === 'up' || key === 'k') {
|
|
72
130
|
this._scrollOffset = Math.max(0, this._scrollOffset - 1);
|
|
73
131
|
this.markDirty();
|
|
@@ -78,7 +136,45 @@ export class PolicyPanel extends BasePanel {
|
|
|
78
136
|
this.markDirty();
|
|
79
137
|
return true;
|
|
80
138
|
}
|
|
139
|
+
if (key === 's') {
|
|
140
|
+
this._pendingAction = 'simulate';
|
|
141
|
+
return true;
|
|
142
|
+
}
|
|
143
|
+
if (key === 'f') {
|
|
144
|
+
this._pendingAction = 'preflight';
|
|
145
|
+
return true;
|
|
146
|
+
}
|
|
147
|
+
if (key === 'l') {
|
|
148
|
+
this._pendingAction = 'lint';
|
|
149
|
+
return true;
|
|
150
|
+
}
|
|
151
|
+
if (key === 'p') {
|
|
152
|
+
const snapshot = this._state.getSnapshot();
|
|
153
|
+
if (!snapshot.candidate) return false;
|
|
154
|
+
const gateStatus = snapshot.divergence?.gate.status ?? 'no_data';
|
|
155
|
+
this._confirm = {
|
|
156
|
+
subject: 'promote',
|
|
157
|
+
label: `promote ${snapshot.candidate.bundle.bundleId} (gate: ${gateStatus})`,
|
|
158
|
+
verb: 'Promote',
|
|
159
|
+
};
|
|
160
|
+
this.markDirty();
|
|
161
|
+
return true;
|
|
162
|
+
}
|
|
163
|
+
if (key === 'b') {
|
|
164
|
+
const snapshot = this._state.getSnapshot();
|
|
165
|
+
if (!snapshot.current) return false;
|
|
166
|
+
this._confirm = {
|
|
167
|
+
subject: 'rollback',
|
|
168
|
+
label: `rollback active bundle ${snapshot.current.bundle.bundleId}`,
|
|
169
|
+
verb: 'Rollback',
|
|
170
|
+
};
|
|
171
|
+
this.markDirty();
|
|
172
|
+
return true;
|
|
173
|
+
}
|
|
81
174
|
if (key === 'r') {
|
|
175
|
+
// Trend-record is only meaningful once a simulation dashboard exists;
|
|
176
|
+
// otherwise there is nothing to sample and the key is a no-op.
|
|
177
|
+
if (!this._state.getDashboard()) return false;
|
|
82
178
|
this._state.recordTrendEntry();
|
|
83
179
|
this.markDirty();
|
|
84
180
|
return true;
|
|
@@ -86,25 +182,60 @@ export class PolicyPanel extends BasePanel {
|
|
|
86
182
|
return false;
|
|
87
183
|
}
|
|
88
184
|
|
|
185
|
+
public handlePanelIntegrationAction(_key: string, ctx: PanelIntegrationContext): boolean {
|
|
186
|
+
if (!this._pendingAction) return false;
|
|
187
|
+
const action = this._pendingAction;
|
|
188
|
+
this._pendingAction = null;
|
|
189
|
+
void ctx.executeCommand?.('policy', [action]).catch((err) => {
|
|
190
|
+
logger.debug('policy panel command dispatch failed', { err, action });
|
|
191
|
+
});
|
|
192
|
+
return true;
|
|
193
|
+
}
|
|
194
|
+
|
|
89
195
|
public render(width: number, height: number): Line[] {
|
|
90
196
|
this.needsRender = false;
|
|
197
|
+
if (this._confirm) {
|
|
198
|
+
const lines = buildPanelWorkspace(width, height, {
|
|
199
|
+
title: 'Policy And Governance',
|
|
200
|
+
sections: [{ title: 'Confirmation', lines: renderConfirmLines(width, this._confirm) }],
|
|
201
|
+
palette: C,
|
|
202
|
+
});
|
|
203
|
+
while (lines.length < height) {
|
|
204
|
+
lines.push(createEmptyLine(width));
|
|
205
|
+
}
|
|
206
|
+
return lines.slice(0, height);
|
|
207
|
+
}
|
|
91
208
|
const snapshot = this._state.getSnapshot();
|
|
209
|
+
const summaryLine = this._buildSummary(width, snapshot);
|
|
92
210
|
const content = this._buildContent(width, snapshot);
|
|
211
|
+
const dashboardActive = snapshot.divergence !== null;
|
|
212
|
+
const hintsLine = buildKeyboardHints(width, [
|
|
213
|
+
{ keys: '↑/↓', label: 'scroll' },
|
|
214
|
+
{ keys: 's', label: 'simulate' },
|
|
215
|
+
{ keys: 'f', label: 'preflight' },
|
|
216
|
+
{ keys: 'l', label: 'lint' },
|
|
217
|
+
{ keys: 'p', label: 'promote' },
|
|
218
|
+
{ keys: 'b', label: 'rollback' },
|
|
219
|
+
...(dashboardActive ? [{ keys: 'r', label: 'record divergence snapshot' }] : []),
|
|
220
|
+
], C);
|
|
221
|
+
const summarySection = { lines: [summaryLine] };
|
|
93
222
|
const governanceSection = resolveScrollablePanelSection(width, height, {
|
|
94
|
-
footerLines: [
|
|
223
|
+
footerLines: [hintsLine],
|
|
95
224
|
palette: C,
|
|
225
|
+
beforeSections: [summarySection],
|
|
96
226
|
section: {
|
|
97
227
|
title: 'Governance',
|
|
98
228
|
scrollableLines: content,
|
|
99
229
|
scrollOffset: this._scrollOffset,
|
|
100
230
|
minRows: 4,
|
|
231
|
+
appendWindowSummary: content.length > 0 ? { dimColor: C.dim } : undefined,
|
|
101
232
|
},
|
|
102
233
|
});
|
|
103
234
|
this._scrollOffset = governanceSection.scrollOffset;
|
|
104
235
|
const lines = buildPanelWorkspace(width, height, {
|
|
105
236
|
title: 'Policy And Governance',
|
|
106
|
-
sections: [governanceSection.section],
|
|
107
|
-
footerLines: [
|
|
237
|
+
sections: [summarySection, governanceSection.section],
|
|
238
|
+
footerLines: [hintsLine],
|
|
108
239
|
palette: C,
|
|
109
240
|
});
|
|
110
241
|
while (lines.length < height) {
|
|
@@ -113,6 +244,28 @@ export class PolicyPanel extends BasePanel {
|
|
|
113
244
|
return lines.slice(0, height);
|
|
114
245
|
}
|
|
115
246
|
|
|
247
|
+
/** Top-of-panel posture summary: the highest-signal governance state at a glance. */
|
|
248
|
+
private _buildSummary(width: number, snapshot: PolicyPanelSnapshot): Line {
|
|
249
|
+
const preflight = snapshot.lastPreflightReview;
|
|
250
|
+
const divergence = snapshot.divergence;
|
|
251
|
+
const lintCount = snapshot.lintFindings.length;
|
|
252
|
+
const preflightStatus = preflight ? preflight.status : 'none';
|
|
253
|
+
const preflightColor = !preflight
|
|
254
|
+
? C.dim
|
|
255
|
+
: preflight.status === 'pass'
|
|
256
|
+
? C.good
|
|
257
|
+
: preflight.status === 'warn'
|
|
258
|
+
? C.warn
|
|
259
|
+
: C.bad;
|
|
260
|
+
const gateStatus = divergence?.gate.status ?? 'n/a';
|
|
261
|
+
return buildKeyValueLine(width, [
|
|
262
|
+
{ label: 'bundles', value: `${snapshot.current ? 1 : 0}+${snapshot.candidate ? 1 : 0}c`, valueColor: snapshot.current || snapshot.candidate ? C.value : C.dim },
|
|
263
|
+
{ label: 'preflight', value: preflightStatus.toUpperCase(), valueColor: preflightColor },
|
|
264
|
+
{ label: 'gate', value: gateStatus, valueColor: gateColor(gateStatus) },
|
|
265
|
+
{ label: 'lint', value: String(lintCount), valueColor: lintCount > 0 ? C.warn : C.dim },
|
|
266
|
+
], C);
|
|
267
|
+
}
|
|
268
|
+
|
|
116
269
|
private _buildContent(width: number, snapshot: PolicyPanelSnapshot): Line[] {
|
|
117
270
|
const lines: Line[] = [];
|
|
118
271
|
const current = snapshot.current;
|
|
@@ -123,8 +276,22 @@ export class PolicyPanel extends BasePanel {
|
|
|
123
276
|
const simulationSummary = snapshot.lastSimulationSummary;
|
|
124
277
|
const preflightReview = snapshot.lastPreflightReview;
|
|
125
278
|
|
|
126
|
-
|
|
127
|
-
|
|
279
|
+
const nothingRecorded = !current && !candidate && !divergence
|
|
280
|
+
&& snapshot.history.length === 0 && permissionAudit.length === 0
|
|
281
|
+
&& lintFindings.length === 0 && !simulationSummary && !preflightReview;
|
|
282
|
+
if (nothingRecorded) {
|
|
283
|
+
// No divergence dashboard exists in this branch by construction
|
|
284
|
+
// (nothingRecorded requires !divergence), so the gate is honestly 'n/a'.
|
|
285
|
+
lines.push(...buildEmptyState(
|
|
286
|
+
width,
|
|
287
|
+
' No policy bundles loaded.',
|
|
288
|
+
'Bundle: none active, none candidate. Gate: n/a.',
|
|
289
|
+
[
|
|
290
|
+
{ command: '/policy load', summary: 'load a policy bundle to begin governance review' },
|
|
291
|
+
],
|
|
292
|
+
C,
|
|
293
|
+
));
|
|
294
|
+
return lines;
|
|
128
295
|
}
|
|
129
296
|
|
|
130
297
|
if (current) {
|
|
@@ -160,17 +327,41 @@ export class PolicyPanel extends BasePanel {
|
|
|
160
327
|
}
|
|
161
328
|
|
|
162
329
|
if (snapshot.diff) {
|
|
330
|
+
const diff = snapshot.diff;
|
|
163
331
|
lines.push(buildPanelLine(width, [[' Diff', C.label]]));
|
|
164
332
|
lines.push(buildPanelLine(width, [
|
|
333
|
+
[' ', C.label],
|
|
334
|
+
[`${diff.fromBundleId} -> ${diff.toBundleId}`, C.value],
|
|
165
335
|
[' Changes: ', C.label],
|
|
166
|
-
[String(
|
|
167
|
-
[' Added: ', C.label],
|
|
168
|
-
[String(snapshot.diff.added.length), C.ok],
|
|
169
|
-
[' Removed: ', C.label],
|
|
170
|
-
[String(snapshot.diff.removed.length), C.error],
|
|
171
|
-
[' Changed: ', C.label],
|
|
172
|
-
[String(snapshot.diff.changed.length), C.warn],
|
|
336
|
+
[String(diff.totalChanges), C.value],
|
|
173
337
|
]));
|
|
338
|
+
if (diff.added.length > 0) {
|
|
339
|
+
lines.push(buildPanelLine(width, [[' Added', C.good]]));
|
|
340
|
+
for (const rule of diff.added.slice(0, 5)) {
|
|
341
|
+
lines.push(buildPanelLine(width, [
|
|
342
|
+
[' + ', C.good],
|
|
343
|
+
[truncateDisplay(`${rule.id} (${rule.type}, effect=${rule.effect})`, Math.max(0, width - 6)), C.value],
|
|
344
|
+
]));
|
|
345
|
+
}
|
|
346
|
+
}
|
|
347
|
+
if (diff.removed.length > 0) {
|
|
348
|
+
lines.push(buildPanelLine(width, [[' Removed', C.bad]]));
|
|
349
|
+
for (const rule of diff.removed.slice(0, 5)) {
|
|
350
|
+
lines.push(buildPanelLine(width, [
|
|
351
|
+
[' - ', C.bad],
|
|
352
|
+
[truncateDisplay(`${rule.id} (${rule.type}, effect=${rule.effect})`, Math.max(0, width - 6)), C.value],
|
|
353
|
+
]));
|
|
354
|
+
}
|
|
355
|
+
}
|
|
356
|
+
if (diff.changed.length > 0) {
|
|
357
|
+
lines.push(buildPanelLine(width, [[' Changed', C.warn]]));
|
|
358
|
+
for (const change of diff.changed.slice(0, 5)) {
|
|
359
|
+
lines.push(buildPanelLine(width, [
|
|
360
|
+
[' ~ ', C.warn],
|
|
361
|
+
[truncateDisplay(change.ruleId, Math.max(0, width - 6)), C.value],
|
|
362
|
+
]));
|
|
363
|
+
}
|
|
364
|
+
}
|
|
174
365
|
}
|
|
175
366
|
|
|
176
367
|
if (divergence) {
|
|
@@ -189,6 +380,35 @@ export class PolicyPanel extends BasePanel {
|
|
|
189
380
|
[' Trend points: ', C.label],
|
|
190
381
|
[String(divergence.trend.length), C.value],
|
|
191
382
|
]));
|
|
383
|
+
const prefixEntries = Object.entries(divergence.report.byCommandPrefix);
|
|
384
|
+
if (prefixEntries.length > 0) {
|
|
385
|
+
lines.push(buildPanelLine(width, [[' Divergence by command prefix', C.label]]));
|
|
386
|
+
for (const [prefix, stats] of prefixEntries.slice(0, 5)) {
|
|
387
|
+
lines.push(buildPanelLine(width, [
|
|
388
|
+
[' ', C.label],
|
|
389
|
+
[truncateDisplay(prefix, Math.max(0, Math.floor(width * 0.4))), C.value],
|
|
390
|
+
[' ', C.label],
|
|
391
|
+
[fmtRate(stats.divergenceRate), stats.divergenceRate > 0 ? C.warn : C.good],
|
|
392
|
+
[' ', C.label],
|
|
393
|
+
[`${stats.total}/${stats.totalEvaluations}`, C.dim],
|
|
394
|
+
]));
|
|
395
|
+
}
|
|
396
|
+
}
|
|
397
|
+
const classEntries = Object.entries(divergence.report.byToolClass) as Array<[string, DivergenceStats | undefined]>;
|
|
398
|
+
const populatedClassEntries = classEntries.filter((entry): entry is [string, DivergenceStats] => entry[1] !== undefined);
|
|
399
|
+
if (populatedClassEntries.length > 0) {
|
|
400
|
+
lines.push(buildPanelLine(width, [[' Divergence by tool class', C.label]]));
|
|
401
|
+
for (const [cls, stats] of populatedClassEntries.slice(0, 5)) {
|
|
402
|
+
lines.push(buildPanelLine(width, [
|
|
403
|
+
[' ', C.label],
|
|
404
|
+
[truncateDisplay(cls, Math.max(0, Math.floor(width * 0.4))), C.value],
|
|
405
|
+
[' ', C.label],
|
|
406
|
+
[fmtRate(stats.divergenceRate), stats.divergenceRate > 0 ? C.warn : C.good],
|
|
407
|
+
[' ', C.label],
|
|
408
|
+
[`${stats.total}/${stats.totalEvaluations}`, C.dim],
|
|
409
|
+
]));
|
|
410
|
+
}
|
|
411
|
+
}
|
|
192
412
|
}
|
|
193
413
|
|
|
194
414
|
if (snapshot.history.length > 0) {
|
|
@@ -209,7 +429,7 @@ export class PolicyPanel extends BasePanel {
|
|
|
209
429
|
lines.push(buildPanelLine(width, [[' Permission Audit', C.label]]));
|
|
210
430
|
for (const entry of permissionAudit.slice(0, 5)) {
|
|
211
431
|
const outcome = entry.approved === undefined ? 'pending' : entry.approved ? 'approved' : 'denied';
|
|
212
|
-
const outcomeColor = entry.approved === undefined ? C.warn : entry.approved ? C.
|
|
432
|
+
const outcomeColor = entry.approved === undefined ? C.warn : entry.approved ? C.good : C.bad;
|
|
213
433
|
lines.push(buildPanelLine(width, [
|
|
214
434
|
[' ', C.label],
|
|
215
435
|
[entry.tool, C.value],
|
|
@@ -220,7 +440,7 @@ export class PolicyPanel extends BasePanel {
|
|
|
220
440
|
]));
|
|
221
441
|
lines.push(buildPanelLine(width, [
|
|
222
442
|
[' ', C.label],
|
|
223
|
-
[entry.summary
|
|
443
|
+
[truncateDisplay(entry.summary, Math.max(0, width - 6)), C.dim],
|
|
224
444
|
]));
|
|
225
445
|
}
|
|
226
446
|
}
|
|
@@ -228,29 +448,24 @@ export class PolicyPanel extends BasePanel {
|
|
|
228
448
|
if (lintFindings.length > 0) {
|
|
229
449
|
lines.push(buildPanelLine(width, [[' Policy Lint', C.label]]));
|
|
230
450
|
for (const finding of lintFindings.slice(0, 5)) {
|
|
231
|
-
const color = finding.severity === 'error' ? C.
|
|
451
|
+
const color = finding.severity === 'error' ? C.bad : finding.severity === 'warn' ? C.warn : C.info;
|
|
232
452
|
lines.push(buildPanelLine(width, [
|
|
233
453
|
[' ', C.label],
|
|
234
454
|
[finding.severity.toUpperCase(), color],
|
|
235
455
|
[' ', C.label],
|
|
236
|
-
[finding.message
|
|
456
|
+
[truncateDisplay(finding.message, Math.max(0, width - 14)), color],
|
|
237
457
|
]));
|
|
238
458
|
}
|
|
239
459
|
}
|
|
240
460
|
|
|
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 {
|
|
461
|
+
if (preflightReview) {
|
|
247
462
|
lines.push(buildPanelLine(width, [[' Preflight Review', C.label]]));
|
|
248
463
|
const statusColor =
|
|
249
464
|
preflightReview.status === 'pass'
|
|
250
|
-
? C.
|
|
465
|
+
? C.good
|
|
251
466
|
: preflightReview.status === 'warn'
|
|
252
467
|
? C.warn
|
|
253
|
-
: C.
|
|
468
|
+
: C.bad;
|
|
254
469
|
lines.push(buildPanelLine(width, [
|
|
255
470
|
[' Status: ', C.label],
|
|
256
471
|
[preflightReview.status.toUpperCase(), statusColor],
|
|
@@ -261,39 +476,34 @@ export class PolicyPanel extends BasePanel {
|
|
|
261
476
|
]));
|
|
262
477
|
lines.push(buildPanelLine(width, [
|
|
263
478
|
[' ', C.label],
|
|
264
|
-
[preflightReview.summary
|
|
479
|
+
[truncateDisplay(preflightReview.summary, Math.max(0, width - 2)), C.dim],
|
|
265
480
|
]));
|
|
266
481
|
for (const issue of preflightReview.issues.slice(0, 4)) {
|
|
267
|
-
const issueColor = issue.severity === 'error' ? C.
|
|
482
|
+
const issueColor = issue.severity === 'error' ? C.bad : issue.severity === 'warn' ? C.warn : C.info;
|
|
268
483
|
lines.push(buildPanelLine(width, [
|
|
269
484
|
[' ', C.label],
|
|
270
485
|
[issue.severity.toUpperCase(), issueColor],
|
|
271
486
|
[' ', C.label],
|
|
272
|
-
[issue.message
|
|
487
|
+
[truncateDisplay(issue.message, Math.max(0, width - 14)), issueColor],
|
|
273
488
|
]));
|
|
274
489
|
}
|
|
275
490
|
}
|
|
276
491
|
|
|
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 {
|
|
492
|
+
if (simulationSummary) {
|
|
283
493
|
lines.push(buildPanelLine(width, [[' Simulation Samples', C.label]]));
|
|
284
494
|
lines.push(buildPanelLine(width, [
|
|
285
495
|
[' Mode: ', C.label],
|
|
286
496
|
[simulationSummary.mode, C.info],
|
|
287
497
|
[' Diverged: ', C.label],
|
|
288
|
-
[`${simulationSummary.divergentScenarios}/${simulationSummary.totalScenarios}`, simulationSummary.divergentScenarios > 0 ? C.warn : C.
|
|
498
|
+
[`${simulationSummary.divergentScenarios}/${simulationSummary.totalScenarios}`, simulationSummary.divergentScenarios > 0 ? C.warn : C.good],
|
|
289
499
|
[' Allowed(actual/sim): ', C.label],
|
|
290
500
|
[`${simulationSummary.allowedByActual}/${simulationSummary.allowedBySimulated}`, C.value],
|
|
291
501
|
]));
|
|
292
502
|
for (const result of simulationSummary.results.slice(0, 4)) {
|
|
293
|
-
const color = result.diverged ? C.warn : (result.authoritativeDecision.allowed ? C.
|
|
503
|
+
const color = result.diverged ? C.warn : (result.authoritativeDecision.allowed ? C.good : C.bad);
|
|
294
504
|
lines.push(buildPanelLine(width, [
|
|
295
505
|
[' ', C.label],
|
|
296
|
-
[result.scenario.label
|
|
506
|
+
[truncateDisplay(result.scenario.label, Math.max(0, width - 40)), C.value],
|
|
297
507
|
[' ', C.label],
|
|
298
508
|
[(result.authoritativeDecision.allowed ? 'allow' : 'deny').toUpperCase(), color],
|
|
299
509
|
[' ', C.label],
|
|
@@ -302,7 +512,6 @@ export class PolicyPanel extends BasePanel {
|
|
|
302
512
|
}
|
|
303
513
|
}
|
|
304
514
|
|
|
305
|
-
lines.push(buildPanelLine(width, [[' /policy opens this panel. Press r to record a divergence trend snapshot.', C.dim]]));
|
|
306
515
|
return lines;
|
|
307
516
|
}
|
|
308
517
|
}
|
|
@@ -0,0 +1,162 @@
|
|
|
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 { resolveUiTones } from '../renderer/theme.ts';
|
|
5
|
+
|
|
6
|
+
// DEFAULT_PANEL_PALETTE is built from the mode-resolved chrome tones
|
|
7
|
+
// (resolveUiTones) rather than the static UI_TONES constant — WO-001 single
|
|
8
|
+
// read path. Mode is fixed to 'dark' until the terminal-bg-probe lands.
|
|
9
|
+
const UI_TONES = resolveUiTones('dark');
|
|
10
|
+
|
|
11
|
+
// ---------------------------------------------------------------------------
|
|
12
|
+
// Panel palette + core line primitives.
|
|
13
|
+
//
|
|
14
|
+
// The leaf foundation of the panel formatting toolkit: both `polish.ts` and
|
|
15
|
+
// `polish-tables.ts` build on these. Kept dependency-free (only grid /
|
|
16
|
+
// terminal-width / ui-primitives) so it never participates in an import cycle.
|
|
17
|
+
// All symbols are re-exported from `polish.ts` — panels import from there.
|
|
18
|
+
// ---------------------------------------------------------------------------
|
|
19
|
+
|
|
20
|
+
export interface PanelPalette {
|
|
21
|
+
readonly label: string;
|
|
22
|
+
readonly value: string;
|
|
23
|
+
readonly dim: string;
|
|
24
|
+
readonly info: string;
|
|
25
|
+
readonly good?: string;
|
|
26
|
+
readonly warn?: string;
|
|
27
|
+
readonly bad?: string;
|
|
28
|
+
readonly empty: string;
|
|
29
|
+
readonly header?: string;
|
|
30
|
+
readonly headerBg?: string;
|
|
31
|
+
readonly surfaceBg?: string;
|
|
32
|
+
readonly sectionBg?: string;
|
|
33
|
+
readonly summaryBg?: string;
|
|
34
|
+
readonly inputBg?: string;
|
|
35
|
+
readonly accent?: string;
|
|
36
|
+
readonly selectBg?: string;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export const DEFAULT_PANEL_PALETTE: Readonly<Required<PanelPalette>> = {
|
|
40
|
+
header: UI_TONES.fg.primary,
|
|
41
|
+
headerBg: UI_TONES.bg.title,
|
|
42
|
+
label: UI_TONES.fg.muted,
|
|
43
|
+
value: UI_TONES.fg.primary,
|
|
44
|
+
dim: UI_TONES.fg.dim,
|
|
45
|
+
info: UI_TONES.state.info,
|
|
46
|
+
good: UI_TONES.state.good,
|
|
47
|
+
warn: UI_TONES.state.warn,
|
|
48
|
+
bad: UI_TONES.state.bad,
|
|
49
|
+
empty: UI_TONES.fg.empty,
|
|
50
|
+
surfaceBg: UI_TONES.bg.surface,
|
|
51
|
+
sectionBg: UI_TONES.bg.section,
|
|
52
|
+
summaryBg: UI_TONES.bg.summary,
|
|
53
|
+
inputBg: UI_TONES.bg.input,
|
|
54
|
+
accent: UI_TONES.fg.secondary,
|
|
55
|
+
selectBg: UI_TONES.bg.selected,
|
|
56
|
+
} as const;
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Extend the base panel palette with domain-specific colors.
|
|
60
|
+
*
|
|
61
|
+
* Convention: raw hex colors may only live inside a palette constant declared
|
|
62
|
+
* at the top of a panel file, not inline in render calls.
|
|
63
|
+
*
|
|
64
|
+
* @example
|
|
65
|
+
* ```ts
|
|
66
|
+
* const C = extendPalette(DEFAULT_PANEL_PALETTE, {
|
|
67
|
+
* decision: '#38bdf8',
|
|
68
|
+
* incident: '#ef4444',
|
|
69
|
+
* });
|
|
70
|
+
* ```
|
|
71
|
+
*/
|
|
72
|
+
export function extendPalette<T extends Record<string, string>>(
|
|
73
|
+
base: typeof DEFAULT_PANEL_PALETTE,
|
|
74
|
+
extras: T,
|
|
75
|
+
): typeof DEFAULT_PANEL_PALETTE & T {
|
|
76
|
+
return { ...base, ...extras };
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
export function buildPanelLine(
|
|
80
|
+
width: number,
|
|
81
|
+
segments: Array<StyledPanelSegment | [string, string, string?]>,
|
|
82
|
+
): Line {
|
|
83
|
+
return buildStyledPanelLine(
|
|
84
|
+
width,
|
|
85
|
+
segments.map((seg) =>
|
|
86
|
+
Array.isArray(seg) ? { text: seg[0], fg: seg[1], bg: seg[2] } : seg,
|
|
87
|
+
),
|
|
88
|
+
);
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
export interface StyledPanelSegment {
|
|
92
|
+
readonly text: string;
|
|
93
|
+
readonly fg: string;
|
|
94
|
+
readonly bg?: string;
|
|
95
|
+
readonly bold?: boolean;
|
|
96
|
+
readonly dim?: boolean;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
export function buildSelectablePanelLine(
|
|
100
|
+
width: number,
|
|
101
|
+
segments: ReadonlyArray<StyledPanelSegment>,
|
|
102
|
+
options: { selected?: boolean; selectedBg?: string; fillFg?: string; fillBg?: string; leadingMarker?: string } = {},
|
|
103
|
+
): Line {
|
|
104
|
+
const selected = options.selected ?? false;
|
|
105
|
+
const selectedBg = selected ? (options.selectedBg ?? DEFAULT_PANEL_PALETTE.selectBg) : '';
|
|
106
|
+
const fillBg = selectedBg || options.fillBg || '';
|
|
107
|
+
const fillFg = options.fillFg ?? '';
|
|
108
|
+
const cells = createEmptyLine(width);
|
|
109
|
+
if (fillBg) {
|
|
110
|
+
for (let col = 0; col < width; col++) {
|
|
111
|
+
cells[col] = createStyledCell(' ', { bg: fillBg, fg: fillFg });
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
let col = 0;
|
|
116
|
+
if (selected && options.leadingMarker) {
|
|
117
|
+
for (const ch of options.leadingMarker) {
|
|
118
|
+
const charWidth = getDisplayWidth(ch);
|
|
119
|
+
if (charWidth <= 0 || col + charWidth > width) break;
|
|
120
|
+
cells[col] = createStyledCell(ch, { fg: DEFAULT_PANEL_PALETTE.info, bg: selectedBg, bold: true });
|
|
121
|
+
if (charWidth > 1 && col + 1 < width) cells[col + 1] = createStyledCell(' ', { fg: DEFAULT_PANEL_PALETTE.info, bg: selectedBg, bold: true });
|
|
122
|
+
col += charWidth;
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
for (const segment of segments) {
|
|
126
|
+
const fg = segment.fg;
|
|
127
|
+
const bg = segment.bg ?? fillBg;
|
|
128
|
+
for (const ch of segment.text) {
|
|
129
|
+
const charWidth = getDisplayWidth(ch);
|
|
130
|
+
if (charWidth <= 0) continue;
|
|
131
|
+
if (col + charWidth > width) return cells;
|
|
132
|
+
cells[col] = createStyledCell(ch, {
|
|
133
|
+
fg,
|
|
134
|
+
bg,
|
|
135
|
+
bold: segment.bold ?? false,
|
|
136
|
+
dim: segment.dim ?? false,
|
|
137
|
+
});
|
|
138
|
+
if (charWidth > 1 && col + 1 < width) {
|
|
139
|
+
cells[col + 1] = createStyledCell(' ', {
|
|
140
|
+
fg,
|
|
141
|
+
bg,
|
|
142
|
+
bold: segment.bold ?? false,
|
|
143
|
+
dim: segment.dim ?? false,
|
|
144
|
+
});
|
|
145
|
+
}
|
|
146
|
+
col += charWidth;
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
while (col < width) {
|
|
151
|
+
cells[col++] = createStyledCell(' ', { bg: fillBg, fg: fillFg });
|
|
152
|
+
}
|
|
153
|
+
return cells;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
export function buildStyledPanelLine(
|
|
157
|
+
width: number,
|
|
158
|
+
segments: ReadonlyArray<StyledPanelSegment>,
|
|
159
|
+
options: { fillBg?: string; fillFg?: string } = {},
|
|
160
|
+
): Line {
|
|
161
|
+
return buildSelectablePanelLine(width, segments, options);
|
|
162
|
+
}
|