@pellux/goodvibes-tui 0.28.0 → 1.0.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 +133 -0
- package/README.md +1 -1
- package/package.json +1 -1
- package/src/core/conversation-line-cache.ts +432 -0
- package/src/core/conversation-rendering.ts +11 -3
- package/src/core/conversation.ts +50 -4
- package/src/input/commands/cost-runtime.ts +49 -0
- package/src/input/commands/operator-runtime.ts +5 -1
- package/src/input/commands.ts +2 -0
- package/src/input/handler-content-actions.ts +8 -3
- package/src/input/handler-feed-routes.ts +12 -35
- package/src/input/handler-feed.ts +4 -8
- package/src/input/handler-shortcuts.ts +51 -0
- package/src/input/handler.ts +43 -6
- package/src/input/input-history.ts +17 -4
- package/src/input/keybindings.ts +48 -8
- package/src/input/panel-integration-actions.ts +107 -1
- package/src/main.ts +19 -11
- package/src/panels/agent-inspector-panel.ts +125 -36
- package/src/panels/agent-inspector-shared.ts +144 -0
- package/src/panels/approval-panel.ts +67 -16
- package/src/panels/automation-control-panel.ts +368 -124
- package/src/panels/base-panel.ts +1 -1
- 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 +232 -73
- package/src/panels/communication-panel.ts +58 -20
- package/src/panels/confirm-state.ts +8 -1
- package/src/panels/control-plane-panel.ts +351 -118
- package/src/panels/cost-tracker-panel.ts +165 -7
- package/src/panels/debug-panel.ts +312 -159
- package/src/panels/diff-panel.ts +209 -57
- package/src/panels/docs-panel.ts +152 -66
- package/src/panels/eval-panel.ts +220 -42
- package/src/panels/file-explorer-panel.ts +202 -122
- package/src/panels/file-preview-panel.ts +132 -57
- package/src/panels/git-panel.ts +310 -129
- package/src/panels/hooks-panel.ts +150 -22
- package/src/panels/incident-review-panel.ts +223 -35
- package/src/panels/index.ts +0 -4
- package/src/panels/intelligence-panel.ts +212 -86
- package/src/panels/knowledge-graph-panel.ts +461 -101
- package/src/panels/local-auth-panel.ts +240 -28
- package/src/panels/marketplace-panel.ts +193 -26
- package/src/panels/memory-panel.ts +78 -77
- package/src/panels/ops-control-panel.ts +146 -29
- package/src/panels/ops-strategy-panel.ts +72 -4
- package/src/panels/orchestration-panel.ts +231 -69
- package/src/panels/panel-list-panel.ts +154 -131
- package/src/panels/panel-manager.ts +97 -9
- package/src/panels/plan-dashboard-panel.ts +333 -17
- package/src/panels/plugins-panel.ts +184 -29
- package/src/panels/policy-panel.ts +210 -38
- package/src/panels/polish-core.ts +7 -2
- package/src/panels/polish.ts +23 -4
- package/src/panels/project-planning-answer-actions.ts +134 -0
- package/src/panels/project-planning-panel.ts +62 -113
- package/src/panels/provider-health-panel.ts +434 -518
- 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 +116 -30
- package/src/panels/remote-panel.ts +114 -36
- package/src/panels/routes-panel.ts +63 -22
- package/src/panels/sandbox-panel.ts +174 -42
- package/src/panels/scrollable-list-panel.ts +19 -135
- package/src/panels/security-panel.ts +133 -33
- package/src/panels/services-panel.ts +116 -64
- package/src/panels/session-browser-panel.ts +73 -2
- package/src/panels/session-maintenance.ts +4 -122
- package/src/panels/settings-sync-panel.ts +335 -72
- package/src/panels/skills-panel.ts +157 -89
- package/src/panels/subscription-panel.ts +86 -33
- package/src/panels/symbol-outline-panel.ts +248 -108
- package/src/panels/system-messages-panel.ts +114 -13
- package/src/panels/tasks-panel.ts +326 -139
- package/src/panels/thinking-panel.ts +43 -10
- package/src/panels/token-budget-panel.ts +194 -18
- package/src/panels/tool-inspector-panel.ts +144 -34
- package/src/panels/types.ts +40 -4
- package/src/panels/work-plan-panel.ts +280 -17
- package/src/panels/worktree-panel.ts +175 -42
- package/src/panels/wrfc-panel.ts +116 -24
- package/src/renderer/agent-detail-modal.ts +37 -8
- package/src/renderer/code-block.ts +58 -28
- package/src/renderer/context-inspector.ts +3 -6
- package/src/renderer/conversation-overlays.ts +25 -11
- package/src/renderer/conversation-surface.ts +1 -21
- package/src/renderer/diff-view.ts +6 -4
- package/src/renderer/footer-tips.ts +41 -0
- package/src/renderer/fullscreen-primitives.ts +23 -17
- package/src/renderer/fullscreen-workspace.ts +2 -7
- package/src/renderer/help-overlay.ts +91 -14
- package/src/renderer/hint-grammar.ts +52 -0
- package/src/renderer/history-search-overlay.ts +10 -16
- package/src/renderer/layout.ts +7 -7
- package/src/renderer/markdown.ts +9 -1
- package/src/renderer/modal-factory.ts +23 -15
- package/src/renderer/model-picker-overlay.ts +8 -485
- package/src/renderer/model-workspace.ts +9 -24
- package/src/renderer/overlay-box.ts +23 -19
- package/src/renderer/process-indicator.ts +15 -25
- package/src/renderer/profile-picker-modal.ts +13 -14
- package/src/renderer/prompt-content-width.ts +16 -0
- package/src/renderer/search-overlay.ts +27 -6
- package/src/renderer/selection-modal-overlay.ts +3 -1
- package/src/renderer/semantic-diff.ts +1 -1
- package/src/renderer/session-picker-modal.ts +6 -4
- package/src/renderer/settings-modal-helpers.ts +10 -34
- package/src/renderer/settings-modal.ts +70 -10
- package/src/renderer/shell-surface.ts +54 -15
- package/src/renderer/status-glyphs.ts +11 -9
- package/src/renderer/surface-layout.ts +0 -12
- package/src/renderer/term-caps.ts +1 -1
- package/src/renderer/theme.ts +60 -3
- package/src/renderer/tool-call.ts +6 -20
- package/src/renderer/ui-factory.ts +48 -37
- package/src/renderer/ui-primitives.ts +41 -3
- package/src/runtime/bootstrap-shell.ts +35 -18
- package/src/runtime/diagnostics/panels/index.ts +0 -3
- package/src/runtime/render-scheduler.ts +80 -0
- package/src/shell/ui-openers.ts +14 -22
- package/src/utils/format-duration.ts +2 -2
- package/src/utils/splash-lines.ts +54 -5
- package/src/version.ts +1 -1
- package/src/work-plans/work-plan-store.ts +13 -0
- package/src/panels/agent-logs-panel.ts +0 -635
- package/src/panels/agent-logs-shared.ts +0 -129
- package/src/panels/context-visualizer-panel.ts +0 -238
- package/src/panels/forensics-panel.ts +0 -378
- package/src/panels/provider-account-snapshot.ts +0 -259
- package/src/panels/provider-accounts-panel.ts +0 -255
- package/src/panels/provider-stats-panel.ts +0 -391
- package/src/panels/schedule-panel.ts +0 -365
- package/src/panels/watchers-panel.ts +0 -213
- package/src/renderer/file-tree.ts +0 -153
- package/src/renderer/progress.ts +0 -99
- package/src/renderer/status-token.ts +0 -67
- package/src/runtime/diagnostics/panels/panel-resources.ts +0 -118
- package/src/runtime/diagnostics/panels/policy.ts +0 -177
|
@@ -2,7 +2,13 @@ import type { Line } from '../types/grid.ts';
|
|
|
2
2
|
import { createEmptyLine } from '../types/grid.ts';
|
|
3
3
|
import { truncateDisplay } from '../utils/terminal-width.ts';
|
|
4
4
|
import { ScrollableListPanel } from './scrollable-list-panel.ts';
|
|
5
|
-
import type {
|
|
5
|
+
import type { PanelIntegrationContext } from './types.ts';
|
|
6
|
+
import type { PluginManager, PluginManagerObserver, PluginStatus } from '@pellux/goodvibes-sdk/platform/plugins';
|
|
7
|
+
import {
|
|
8
|
+
type ConfirmState,
|
|
9
|
+
handleConfirmInput,
|
|
10
|
+
renderConfirmLines,
|
|
11
|
+
} from './confirm-state.ts';
|
|
6
12
|
import {
|
|
7
13
|
buildDetailBlock,
|
|
8
14
|
buildEmptyState,
|
|
@@ -14,31 +20,39 @@ import {
|
|
|
14
20
|
type PanelPalette,
|
|
15
21
|
} from './polish.ts';
|
|
16
22
|
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
23
|
+
// Base chrome only — title band, state colors, and text tokens all come
|
|
24
|
+
// straight from DEFAULT_PANEL_PALETTE (WO-002).
|
|
25
|
+
const C = DEFAULT_PANEL_PALETTE;
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* WO-134: PluginsPanel drives real plugin-manager mutations (enable/disable/
|
|
29
|
+
* verify/lift-quarantine) from the keyboard, not just read-only inspection.
|
|
30
|
+
* `PluginManagerObserver` alone (the read-only surface most panels need) has
|
|
31
|
+
* no mutation methods, so the constructor param is widened to also require
|
|
32
|
+
* the handful of `PluginManager` methods this panel actually calls. The real
|
|
33
|
+
* runtime object passed at bootstrap (`services.pluginManager`, a full
|
|
34
|
+
* `PluginManager` instance — see src/runtime/services.ts) already satisfies
|
|
35
|
+
* this widened shape.
|
|
36
|
+
*/
|
|
37
|
+
export type PluginManagerControls = PluginManagerObserver
|
|
38
|
+
& Pick<PluginManager, 'enable' | 'disable' | 'verify' | 'liftQuarantine'>;
|
|
39
|
+
|
|
40
|
+
type PluginConfirmSubject = { readonly kind: 'disable' | 'lift-quarantine'; readonly name: string };
|
|
27
41
|
|
|
28
42
|
function trustColor(tier: PluginStatus['trustTier']): string {
|
|
29
43
|
switch (tier) {
|
|
30
44
|
case 'trusted':
|
|
31
|
-
return C.
|
|
45
|
+
return C.good;
|
|
32
46
|
case 'limited':
|
|
33
47
|
return C.warn;
|
|
34
48
|
case 'untrusted':
|
|
35
|
-
return C.
|
|
49
|
+
return C.bad;
|
|
36
50
|
}
|
|
37
51
|
}
|
|
38
52
|
|
|
39
53
|
function statusColor(status: PluginStatus): string {
|
|
40
|
-
if (status.quarantined) return C.
|
|
41
|
-
if (status.active) return C.
|
|
54
|
+
if (status.quarantined) return C.bad;
|
|
55
|
+
if (status.active) return C.good;
|
|
42
56
|
if (status.enabled) return C.warn;
|
|
43
57
|
return C.dim;
|
|
44
58
|
}
|
|
@@ -51,11 +65,15 @@ function statusLabel(status: PluginStatus): string {
|
|
|
51
65
|
}
|
|
52
66
|
|
|
53
67
|
export class PluginsPanel extends ScrollableListPanel<PluginStatus> {
|
|
54
|
-
private readonly manager:
|
|
68
|
+
private readonly manager: PluginManagerControls;
|
|
55
69
|
private readonly unsub: (() => void) | null;
|
|
70
|
+
/** Pending destructive/state-changing confirm for 'd' disable and 'q' lift-quarantine. */
|
|
71
|
+
private confirmAction: ConfirmState<PluginConfirmSubject> | null = null;
|
|
72
|
+
/** Result of the last 'v' verify press, rendered in the detail block until the selection or a new verify changes it. */
|
|
73
|
+
private _verifyResult: { name: string; valid: boolean; fingerprint?: string | undefined; reason?: string | undefined } | null = null;
|
|
56
74
|
|
|
57
|
-
public constructor(manager:
|
|
58
|
-
super('plugins', 'Plugins', '
|
|
75
|
+
public constructor(manager: PluginManagerControls) {
|
|
76
|
+
super('plugins', 'Plugins', '◐', 'automation-control');
|
|
59
77
|
this.showSelectionGutter = true; // I5: non-color selection affordance
|
|
60
78
|
this.filterEnabled = true;
|
|
61
79
|
this.filterLabel = 'Filter plugins';
|
|
@@ -69,15 +87,136 @@ export class PluginsPanel extends ScrollableListPanel<PluginStatus> {
|
|
|
69
87
|
|| plugin.version.toLowerCase().includes(q);
|
|
70
88
|
}
|
|
71
89
|
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
this.selectedIndex = 0;
|
|
75
|
-
}
|
|
90
|
+
// WO-134: selection is preserved across onActivate — BasePanel's default
|
|
91
|
+
// (mark dirty, no index reset) is exactly what we want, so no override.
|
|
76
92
|
|
|
77
93
|
public override onDestroy(): void {
|
|
78
94
|
this.unsub?.();
|
|
79
95
|
}
|
|
80
96
|
|
|
97
|
+
// -------------------------------------------------------------------------
|
|
98
|
+
// Input — e=enable, d=disable (confirm), v=verify, q=lift quarantine (confirm)
|
|
99
|
+
// -------------------------------------------------------------------------
|
|
100
|
+
|
|
101
|
+
public handleInput(key: string): boolean {
|
|
102
|
+
if (this.lastError !== null) this.clearError();
|
|
103
|
+
|
|
104
|
+
if (this.confirmAction) {
|
|
105
|
+
const result = handleConfirmInput(this.confirmAction, key);
|
|
106
|
+
if (result === 'confirmed') {
|
|
107
|
+
this._executeConfirmed(this.confirmAction.subject);
|
|
108
|
+
this.confirmAction = null;
|
|
109
|
+
this.markDirty();
|
|
110
|
+
return true;
|
|
111
|
+
}
|
|
112
|
+
if (result === 'cancelled') {
|
|
113
|
+
this.confirmAction = null;
|
|
114
|
+
this.markDirty();
|
|
115
|
+
}
|
|
116
|
+
return true;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
if (!this.filterActive) {
|
|
120
|
+
switch (key) {
|
|
121
|
+
case 'e':
|
|
122
|
+
this._enableSelected();
|
|
123
|
+
return true;
|
|
124
|
+
case 'd':
|
|
125
|
+
this._requestDisable();
|
|
126
|
+
return true;
|
|
127
|
+
case 'v':
|
|
128
|
+
this._verifySelected();
|
|
129
|
+
return true;
|
|
130
|
+
case 'q': {
|
|
131
|
+
const plugin = this.getSelectedItem();
|
|
132
|
+
if (!plugin?.quarantined) return false;
|
|
133
|
+
this._requestLiftQuarantine();
|
|
134
|
+
return true;
|
|
135
|
+
}
|
|
136
|
+
case 'm': {
|
|
137
|
+
// Consumed here only when there is a quarantined plugin selected;
|
|
138
|
+
// the actual /recall dispatch happens in handlePanelIntegrationAction
|
|
139
|
+
// below, which needs the executeCommand bridge from the router.
|
|
140
|
+
const plugin = this.getSelectedItem();
|
|
141
|
+
return Boolean(plugin?.quarantined);
|
|
142
|
+
}
|
|
143
|
+
default:
|
|
144
|
+
break;
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
return super.handleInput(key);
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
/**
|
|
152
|
+
* WO-134: optional one-key capture-to-memory for a quarantined plugin — 'm'
|
|
153
|
+
* dispatches `/recall capture plugin <name>` (recall-capture.ts:146 pattern)
|
|
154
|
+
* instead of requiring the operator to type the command by hand.
|
|
155
|
+
*/
|
|
156
|
+
public handlePanelIntegrationAction(key: string, ctx: PanelIntegrationContext): boolean {
|
|
157
|
+
if (key !== 'm' || !ctx.executeCommand) return false;
|
|
158
|
+
const plugin = this.getSelectedItem();
|
|
159
|
+
if (!plugin?.quarantined) return false;
|
|
160
|
+
void ctx.executeCommand('recall', ['capture', 'plugin', plugin.name]).catch(() => {});
|
|
161
|
+
return true;
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
private _enableSelected(): void {
|
|
165
|
+
const plugin = this.getSelectedItem();
|
|
166
|
+
if (!plugin) return;
|
|
167
|
+
void this.manager.enable(plugin.name).then((result) => {
|
|
168
|
+
if (!result.ok) this.setError(`Enable failed: ${result.error ?? 'unknown error'}`);
|
|
169
|
+
this.markDirty();
|
|
170
|
+
}).catch((err) => {
|
|
171
|
+
this.setError(`Enable failed: ${err instanceof Error ? err.message : String(err)}`);
|
|
172
|
+
this.markDirty();
|
|
173
|
+
});
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
private _requestDisable(): void {
|
|
177
|
+
const plugin = this.getSelectedItem();
|
|
178
|
+
if (!plugin) return;
|
|
179
|
+
this.confirmAction = { subject: { kind: 'disable', name: plugin.name }, label: plugin.name, verb: 'Disable' };
|
|
180
|
+
this.markDirty();
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
private _requestLiftQuarantine(): void {
|
|
184
|
+
const plugin = this.getSelectedItem();
|
|
185
|
+
if (!plugin?.quarantined) return;
|
|
186
|
+
this.confirmAction = { subject: { kind: 'lift-quarantine', name: plugin.name }, label: plugin.name, verb: 'Lift quarantine on' };
|
|
187
|
+
this.markDirty();
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
private _verifySelected(): void {
|
|
191
|
+
const plugin = this.getSelectedItem();
|
|
192
|
+
if (!plugin) return;
|
|
193
|
+
const result = this.manager.verify(plugin.name);
|
|
194
|
+
if (!result.ok && result.reason?.toLowerCase().includes('not found')) {
|
|
195
|
+
this._verifyResult = null;
|
|
196
|
+
this.setError(`Verify failed: ${result.reason}`);
|
|
197
|
+
this.markDirty();
|
|
198
|
+
return;
|
|
199
|
+
}
|
|
200
|
+
this._verifyResult = { name: plugin.name, valid: result.valid, fingerprint: result.fingerprint, reason: result.reason };
|
|
201
|
+
this.markDirty();
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
private _executeConfirmed(subject: PluginConfirmSubject): void {
|
|
205
|
+
if (subject.kind === 'disable') {
|
|
206
|
+
void this.manager.disable(subject.name).then((result) => {
|
|
207
|
+
if (!result.ok) this.setError(`Disable failed: ${result.error ?? 'unknown error'}`);
|
|
208
|
+
this.markDirty();
|
|
209
|
+
}).catch((err) => {
|
|
210
|
+
this.setError(`Disable failed: ${err instanceof Error ? err.message : String(err)}`);
|
|
211
|
+
this.markDirty();
|
|
212
|
+
});
|
|
213
|
+
return;
|
|
214
|
+
}
|
|
215
|
+
const result = this.manager.liftQuarantine(subject.name);
|
|
216
|
+
if (!result.ok) this.setError(`Lift quarantine failed: ${result.error ?? 'unknown error'}`);
|
|
217
|
+
this.markDirty();
|
|
218
|
+
}
|
|
219
|
+
|
|
81
220
|
protected override getPalette(): PanelPalette {
|
|
82
221
|
return C;
|
|
83
222
|
}
|
|
@@ -141,14 +280,14 @@ export class PluginsPanel extends ScrollableListPanel<PluginStatus> {
|
|
|
141
280
|
const headerLines: Line[] = [
|
|
142
281
|
buildKeyValueLine(width, [
|
|
143
282
|
{ label: 'plugins', value: String(plugins.length), valueColor: C.info },
|
|
144
|
-
{ label: 'active', value: String(active), valueColor: active > 0 ? C.
|
|
283
|
+
{ label: 'active', value: String(active), valueColor: active > 0 ? C.good : C.dim },
|
|
145
284
|
{ label: 'untrusted', value: String(untrusted), valueColor: untrusted > 0 ? C.warn : C.dim },
|
|
146
|
-
{ label: 'quarantined', value: String(quarantined), valueColor: quarantined > 0 ? C.
|
|
285
|
+
{ label: 'quarantined', value: String(quarantined), valueColor: quarantined > 0 ? C.bad : C.dim },
|
|
147
286
|
], C),
|
|
148
287
|
];
|
|
149
288
|
|
|
150
289
|
this.clampSelection();
|
|
151
|
-
const selected =
|
|
290
|
+
const selected = this.getSelectedItem()!;
|
|
152
291
|
const selectedCaps = this.manager.capabilities(selected.name);
|
|
153
292
|
const trustRecord = this.manager.getTrustRecord(selected.name);
|
|
154
293
|
const quarantineRecord = this.manager.getQuarantineRecord(selected.name);
|
|
@@ -174,9 +313,9 @@ export class PluginsPanel extends ScrollableListPanel<PluginStatus> {
|
|
|
174
313
|
[' Capabilities: ', C.label],
|
|
175
314
|
[String(selectedCaps.requested.length), C.value],
|
|
176
315
|
[' High-risk: ', C.label],
|
|
177
|
-
[String(selectedCaps.highRisk.length), selectedCaps.highRisk.length > 0 ? C.warn : C.
|
|
316
|
+
[String(selectedCaps.highRisk.length), selectedCaps.highRisk.length > 0 ? C.warn : C.good],
|
|
178
317
|
[' Blocked: ', C.label],
|
|
179
|
-
[String(selectedCaps.blocked.length), selectedCaps.blocked.length > 0 ? C.
|
|
318
|
+
[String(selectedCaps.blocked.length), selectedCaps.blocked.length > 0 ? C.bad : C.good],
|
|
180
319
|
]));
|
|
181
320
|
}
|
|
182
321
|
|
|
@@ -192,15 +331,31 @@ export class PluginsPanel extends ScrollableListPanel<PluginStatus> {
|
|
|
192
331
|
if (quarantineRecord) {
|
|
193
332
|
detailRows.push(buildPanelLine(width, [
|
|
194
333
|
[' Quarantine: ', C.label],
|
|
195
|
-
[truncateDisplay(quarantineRecord.reason, Math.max(0, width - 14)), C.
|
|
334
|
+
[truncateDisplay(quarantineRecord.reason, Math.max(0, width - 14)), C.bad],
|
|
196
335
|
]));
|
|
197
336
|
}
|
|
198
337
|
|
|
338
|
+
if (this._verifyResult && this._verifyResult.name === selected.name) {
|
|
339
|
+
const vr = this._verifyResult;
|
|
340
|
+
const verifySegments: Array<[string, string, string?]> = [
|
|
341
|
+
[' Verify: ', C.label],
|
|
342
|
+
[vr.valid ? 'VALID' : 'INVALID', vr.valid ? C.good : C.bad],
|
|
343
|
+
];
|
|
344
|
+
if (vr.fingerprint) verifySegments.push([` fp=${truncateDisplay(vr.fingerprint, 20)}`, C.info]);
|
|
345
|
+
if (!vr.valid && vr.reason) verifySegments.push([` ${truncateDisplay(vr.reason, Math.max(0, width - 40))}`, C.warn]);
|
|
346
|
+
detailRows.push(buildPanelLine(width, verifySegments));
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
if (this.confirmAction) detailRows.push(...renderConfirmLines(width, this.confirmAction));
|
|
350
|
+
|
|
199
351
|
const hints = this.filterActive
|
|
200
352
|
? [{ keys: 'type', label: 'filter' }, { keys: 'Enter', label: 'apply' }, { keys: 'Esc', label: 'clear' }]
|
|
201
353
|
: [
|
|
202
354
|
{ keys: 'Up/Down', label: 'move' },
|
|
203
|
-
{ keys: '
|
|
355
|
+
{ keys: 'e', label: 'enable' },
|
|
356
|
+
{ keys: 'd', label: 'disable' },
|
|
357
|
+
{ keys: 'v', label: 'verify' },
|
|
358
|
+
...(selected.quarantined ? [{ keys: 'q', label: 'lift quarantine' }, { keys: 'm', label: 'capture to memory' }] : []),
|
|
204
359
|
{ keys: '/', label: 'filter' },
|
|
205
360
|
];
|
|
206
361
|
|
|
@@ -1,9 +1,19 @@
|
|
|
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';
|
|
3
4
|
import { truncateDisplay } from '../utils/terminal-width.ts';
|
|
4
5
|
import { BasePanel } from './base-panel.ts';
|
|
5
|
-
import type {
|
|
6
|
-
|
|
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';
|
|
7
17
|
import {
|
|
8
18
|
buildEmptyState,
|
|
9
19
|
buildKeyboardHints,
|
|
@@ -13,20 +23,12 @@ import {
|
|
|
13
23
|
resolveScrollablePanelSection,
|
|
14
24
|
DEFAULT_PANEL_PALETTE,
|
|
15
25
|
} from './polish.ts';
|
|
26
|
+
import { type ConfirmState, handleConfirmInput, renderConfirmLines } from './confirm-state.ts';
|
|
27
|
+
import type { PanelIntegrationContext } from './types.ts';
|
|
16
28
|
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
headerBg: '#1e293b',
|
|
21
|
-
label: '#64748b',
|
|
22
|
-
value: '#e2e8f0',
|
|
23
|
-
dim: '#475569',
|
|
24
|
-
ok: '#22c55e',
|
|
25
|
-
warn: '#eab308',
|
|
26
|
-
error: '#ef4444',
|
|
27
|
-
info: '#38bdf8',
|
|
28
|
-
empty: '#334155',
|
|
29
|
-
} 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;
|
|
30
32
|
|
|
31
33
|
function fmtTime(value: string | undefined): string {
|
|
32
34
|
if (!value) return 'n/a';
|
|
@@ -41,9 +43,9 @@ function fmtRate(value: number | undefined): string {
|
|
|
41
43
|
function gateColor(status: string | undefined): string {
|
|
42
44
|
switch (status) {
|
|
43
45
|
case 'allowed':
|
|
44
|
-
return C.
|
|
46
|
+
return C.good;
|
|
45
47
|
case 'blocked':
|
|
46
|
-
return C.
|
|
48
|
+
return C.bad;
|
|
47
49
|
case 'no_data':
|
|
48
50
|
return C.warn;
|
|
49
51
|
default:
|
|
@@ -51,13 +53,49 @@ function gateColor(status: string | undefined): string {
|
|
|
51
53
|
}
|
|
52
54
|
}
|
|
53
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
|
+
|
|
54
85
|
export class PolicyPanel extends BasePanel {
|
|
55
86
|
private readonly _state: PolicyRuntimeState;
|
|
56
87
|
private readonly _unsub: (() => void) | null;
|
|
57
88
|
private _scrollOffset = 0;
|
|
58
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
|
+
|
|
59
97
|
public constructor(state: PolicyRuntimeState) {
|
|
60
|
-
super('policy', 'Policy', '
|
|
98
|
+
super('policy', 'Policy', '▭', 'security-policy');
|
|
61
99
|
this._state = state;
|
|
62
100
|
this._unsub = state.subscribe(() => this.markDirty());
|
|
63
101
|
}
|
|
@@ -72,6 +110,22 @@ export class PolicyPanel extends BasePanel {
|
|
|
72
110
|
}
|
|
73
111
|
|
|
74
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
|
+
|
|
75
129
|
if (key === 'up' || key === 'k') {
|
|
76
130
|
this._scrollOffset = Math.max(0, this._scrollOffset - 1);
|
|
77
131
|
this.markDirty();
|
|
@@ -82,7 +136,45 @@ export class PolicyPanel extends BasePanel {
|
|
|
82
136
|
this.markDirty();
|
|
83
137
|
return true;
|
|
84
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
|
+
}
|
|
85
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;
|
|
86
178
|
this._state.recordTrendEntry();
|
|
87
179
|
this.markDirty();
|
|
88
180
|
return true;
|
|
@@ -90,14 +182,41 @@ export class PolicyPanel extends BasePanel {
|
|
|
90
182
|
return false;
|
|
91
183
|
}
|
|
92
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
|
+
|
|
93
195
|
public render(width: number, height: number): Line[] {
|
|
94
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
|
+
}
|
|
95
208
|
const snapshot = this._state.getSnapshot();
|
|
96
209
|
const summaryLine = this._buildSummary(width, snapshot);
|
|
97
210
|
const content = this._buildContent(width, snapshot);
|
|
211
|
+
const dashboardActive = snapshot.divergence !== null;
|
|
98
212
|
const hintsLine = buildKeyboardHints(width, [
|
|
99
213
|
{ keys: '↑/↓', label: 'scroll' },
|
|
100
|
-
{ keys: '
|
|
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' }] : []),
|
|
101
220
|
], C);
|
|
102
221
|
const summarySection = { lines: [summaryLine] };
|
|
103
222
|
const governanceSection = resolveScrollablePanelSection(width, height, {
|
|
@@ -134,10 +253,10 @@ export class PolicyPanel extends BasePanel {
|
|
|
134
253
|
const preflightColor = !preflight
|
|
135
254
|
? C.dim
|
|
136
255
|
: preflight.status === 'pass'
|
|
137
|
-
? C.
|
|
256
|
+
? C.good
|
|
138
257
|
: preflight.status === 'warn'
|
|
139
258
|
? C.warn
|
|
140
|
-
: C.
|
|
259
|
+
: C.bad;
|
|
141
260
|
const gateStatus = divergence?.gate.status ?? 'n/a';
|
|
142
261
|
return buildKeyValueLine(width, [
|
|
143
262
|
{ label: 'bundles', value: `${snapshot.current ? 1 : 0}+${snapshot.candidate ? 1 : 0}c`, valueColor: snapshot.current || snapshot.candidate ? C.value : C.dim },
|
|
@@ -161,14 +280,14 @@ export class PolicyPanel extends BasePanel {
|
|
|
161
280
|
&& snapshot.history.length === 0 && permissionAudit.length === 0
|
|
162
281
|
&& lintFindings.length === 0 && !simulationSummary && !preflightReview;
|
|
163
282
|
if (nothingRecorded) {
|
|
283
|
+
// No divergence dashboard exists in this branch by construction
|
|
284
|
+
// (nothingRecorded requires !divergence), so the gate is honestly 'n/a'.
|
|
164
285
|
lines.push(...buildEmptyState(
|
|
165
286
|
width,
|
|
166
287
|
' No policy bundles loaded.',
|
|
167
|
-
'
|
|
288
|
+
'Bundle: none active, none candidate. Gate: n/a.',
|
|
168
289
|
[
|
|
169
290
|
{ 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
291
|
],
|
|
173
292
|
C,
|
|
174
293
|
));
|
|
@@ -208,17 +327,41 @@ export class PolicyPanel extends BasePanel {
|
|
|
208
327
|
}
|
|
209
328
|
|
|
210
329
|
if (snapshot.diff) {
|
|
330
|
+
const diff = snapshot.diff;
|
|
211
331
|
lines.push(buildPanelLine(width, [[' Diff', C.label]]));
|
|
212
332
|
lines.push(buildPanelLine(width, [
|
|
333
|
+
[' ', C.label],
|
|
334
|
+
[`${diff.fromBundleId} -> ${diff.toBundleId}`, C.value],
|
|
213
335
|
[' Changes: ', C.label],
|
|
214
|
-
[String(
|
|
215
|
-
[' Added: ', C.label],
|
|
216
|
-
[String(snapshot.diff.added.length), C.ok],
|
|
217
|
-
[' Removed: ', C.label],
|
|
218
|
-
[String(snapshot.diff.removed.length), C.error],
|
|
219
|
-
[' Changed: ', C.label],
|
|
220
|
-
[String(snapshot.diff.changed.length), C.warn],
|
|
336
|
+
[String(diff.totalChanges), C.value],
|
|
221
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
|
+
}
|
|
222
365
|
}
|
|
223
366
|
|
|
224
367
|
if (divergence) {
|
|
@@ -237,6 +380,35 @@ export class PolicyPanel extends BasePanel {
|
|
|
237
380
|
[' Trend points: ', C.label],
|
|
238
381
|
[String(divergence.trend.length), C.value],
|
|
239
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
|
+
}
|
|
240
412
|
}
|
|
241
413
|
|
|
242
414
|
if (snapshot.history.length > 0) {
|
|
@@ -257,7 +429,7 @@ export class PolicyPanel extends BasePanel {
|
|
|
257
429
|
lines.push(buildPanelLine(width, [[' Permission Audit', C.label]]));
|
|
258
430
|
for (const entry of permissionAudit.slice(0, 5)) {
|
|
259
431
|
const outcome = entry.approved === undefined ? 'pending' : entry.approved ? 'approved' : 'denied';
|
|
260
|
-
const outcomeColor = entry.approved === undefined ? C.warn : entry.approved ? C.
|
|
432
|
+
const outcomeColor = entry.approved === undefined ? C.warn : entry.approved ? C.good : C.bad;
|
|
261
433
|
lines.push(buildPanelLine(width, [
|
|
262
434
|
[' ', C.label],
|
|
263
435
|
[entry.tool, C.value],
|
|
@@ -276,7 +448,7 @@ export class PolicyPanel extends BasePanel {
|
|
|
276
448
|
if (lintFindings.length > 0) {
|
|
277
449
|
lines.push(buildPanelLine(width, [[' Policy Lint', C.label]]));
|
|
278
450
|
for (const finding of lintFindings.slice(0, 5)) {
|
|
279
|
-
const color = finding.severity === 'error' ? C.
|
|
451
|
+
const color = finding.severity === 'error' ? C.bad : finding.severity === 'warn' ? C.warn : C.info;
|
|
280
452
|
lines.push(buildPanelLine(width, [
|
|
281
453
|
[' ', C.label],
|
|
282
454
|
[finding.severity.toUpperCase(), color],
|
|
@@ -290,10 +462,10 @@ export class PolicyPanel extends BasePanel {
|
|
|
290
462
|
lines.push(buildPanelLine(width, [[' Preflight Review', C.label]]));
|
|
291
463
|
const statusColor =
|
|
292
464
|
preflightReview.status === 'pass'
|
|
293
|
-
? C.
|
|
465
|
+
? C.good
|
|
294
466
|
: preflightReview.status === 'warn'
|
|
295
467
|
? C.warn
|
|
296
|
-
: C.
|
|
468
|
+
: C.bad;
|
|
297
469
|
lines.push(buildPanelLine(width, [
|
|
298
470
|
[' Status: ', C.label],
|
|
299
471
|
[preflightReview.status.toUpperCase(), statusColor],
|
|
@@ -307,7 +479,7 @@ export class PolicyPanel extends BasePanel {
|
|
|
307
479
|
[truncateDisplay(preflightReview.summary, Math.max(0, width - 2)), C.dim],
|
|
308
480
|
]));
|
|
309
481
|
for (const issue of preflightReview.issues.slice(0, 4)) {
|
|
310
|
-
const issueColor = issue.severity === 'error' ? C.
|
|
482
|
+
const issueColor = issue.severity === 'error' ? C.bad : issue.severity === 'warn' ? C.warn : C.info;
|
|
311
483
|
lines.push(buildPanelLine(width, [
|
|
312
484
|
[' ', C.label],
|
|
313
485
|
[issue.severity.toUpperCase(), issueColor],
|
|
@@ -323,12 +495,12 @@ export class PolicyPanel extends BasePanel {
|
|
|
323
495
|
[' Mode: ', C.label],
|
|
324
496
|
[simulationSummary.mode, C.info],
|
|
325
497
|
[' Diverged: ', C.label],
|
|
326
|
-
[`${simulationSummary.divergentScenarios}/${simulationSummary.totalScenarios}`, simulationSummary.divergentScenarios > 0 ? C.warn : C.
|
|
498
|
+
[`${simulationSummary.divergentScenarios}/${simulationSummary.totalScenarios}`, simulationSummary.divergentScenarios > 0 ? C.warn : C.good],
|
|
327
499
|
[' Allowed(actual/sim): ', C.label],
|
|
328
500
|
[`${simulationSummary.allowedByActual}/${simulationSummary.allowedBySimulated}`, C.value],
|
|
329
501
|
]));
|
|
330
502
|
for (const result of simulationSummary.results.slice(0, 4)) {
|
|
331
|
-
const color = result.diverged ? C.warn : (result.authoritativeDecision.allowed ? C.
|
|
503
|
+
const color = result.diverged ? C.warn : (result.authoritativeDecision.allowed ? C.good : C.bad);
|
|
332
504
|
lines.push(buildPanelLine(width, [
|
|
333
505
|
[' ', C.label],
|
|
334
506
|
[truncateDisplay(result.scenario.label, Math.max(0, width - 40)), C.value],
|