@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,38 +1,34 @@
|
|
|
1
1
|
import type { Line } from '../types/grid.ts';
|
|
2
|
+
import { truncateDisplay } from '../utils/terminal-width.ts';
|
|
2
3
|
import { ScrollableListPanel } from './scrollable-list-panel.ts';
|
|
3
4
|
import type { TokenAuditResult } from '@pellux/goodvibes-sdk/platform/security';
|
|
4
5
|
import type { UiReadModel, UiSecuritySnapshot } from '../runtime/ui-read-models.ts';
|
|
5
6
|
import {
|
|
7
|
+
buildAlignedRow,
|
|
6
8
|
buildEmptyState,
|
|
7
|
-
|
|
9
|
+
buildKeyboardHints,
|
|
8
10
|
buildPanelLine,
|
|
9
11
|
buildPanelWorkspace,
|
|
10
12
|
buildStatusPill,
|
|
11
13
|
DEFAULT_PANEL_PALETTE,
|
|
12
14
|
} from './polish.ts';
|
|
13
15
|
import { createEmptyLine } from '../types/grid.ts';
|
|
16
|
+
import type { PanelIntegrationContext } from './types.ts';
|
|
14
17
|
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
headerBg: '#1e293b',
|
|
19
|
-
dim: '#475569',
|
|
20
|
-
ok: '#22c55e',
|
|
21
|
-
warn: '#eab308',
|
|
22
|
-
error: '#ef4444',
|
|
23
|
-
selectBg: '#0f172a',
|
|
24
|
-
} as const;
|
|
18
|
+
// Base chrome only — title band, state colors, and text tokens all come
|
|
19
|
+
// straight from DEFAULT_PANEL_PALETTE (WO-002).
|
|
20
|
+
const C = DEFAULT_PANEL_PALETTE;
|
|
25
21
|
|
|
26
22
|
function managedColor(managed: boolean): string {
|
|
27
23
|
return managed ? C.warn : C.info;
|
|
28
24
|
}
|
|
29
25
|
|
|
30
26
|
function resultColor(result: TokenAuditResult): string {
|
|
31
|
-
if (result.blocked) return C.
|
|
32
|
-
if (result.scope.outcome === 'violation') return C.
|
|
33
|
-
if (result.rotation.outcome === 'overdue') return C.
|
|
27
|
+
if (result.blocked) return C.bad;
|
|
28
|
+
if (result.scope.outcome === 'violation') return C.bad;
|
|
29
|
+
if (result.rotation.outcome === 'overdue') return C.bad;
|
|
34
30
|
if (result.rotation.outcome === 'warning') return C.warn;
|
|
35
|
-
return C.
|
|
31
|
+
return C.good;
|
|
36
32
|
}
|
|
37
33
|
|
|
38
34
|
function resultSummary(result: TokenAuditResult): string {
|
|
@@ -48,21 +44,58 @@ function severityColor(severity: 'low' | 'medium' | 'high' | 'critical'): string
|
|
|
48
44
|
switch (severity) {
|
|
49
45
|
case 'critical':
|
|
50
46
|
case 'high':
|
|
51
|
-
return C.
|
|
47
|
+
return C.bad;
|
|
52
48
|
case 'medium':
|
|
53
49
|
return C.warn;
|
|
54
50
|
case 'low':
|
|
55
51
|
default:
|
|
56
|
-
return C.
|
|
52
|
+
return C.good;
|
|
57
53
|
}
|
|
58
54
|
}
|
|
59
55
|
|
|
56
|
+
// Relative-time formatting for audit/rotation timestamps — humanized instead
|
|
57
|
+
// of raw ISO strings (WO-137). Mirrors the fmtAgo pattern already used by
|
|
58
|
+
// approval-panel.ts / debug-panel.ts / communication-panel.ts.
|
|
59
|
+
function fmtAgo(ts: number): string {
|
|
60
|
+
const sec = Math.max(0, Math.floor((Date.now() - ts) / 1000));
|
|
61
|
+
if (sec < 5) return 'just now';
|
|
62
|
+
if (sec < 60) return `${sec}s ago`;
|
|
63
|
+
if (sec < 3600) return `${Math.floor(sec / 60)}m ago`;
|
|
64
|
+
if (sec < 86400) return `${Math.floor(sec / 3600)}h ago`;
|
|
65
|
+
return `${Math.floor(sec / 86400)}d ago`;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
function fmtDue(msUntilDue: number): string {
|
|
69
|
+
const overdue = msUntilDue < 0;
|
|
70
|
+
const sec = Math.floor(Math.abs(msUntilDue) / 1000);
|
|
71
|
+
const unit = sec < 3600
|
|
72
|
+
? `${Math.max(1, Math.floor(sec / 60))}m`
|
|
73
|
+
: sec < 86400
|
|
74
|
+
? `${Math.floor(sec / 3600)}h`
|
|
75
|
+
: `${Math.floor(sec / 86400)}d`;
|
|
76
|
+
return overdue ? `overdue by ${unit}` : `in ${unit}`;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
// Attack-path finding rows: 3 rendered lines each (severity/route, reason,
|
|
80
|
+
// evidence). Bounds the per-render window to the panel's actual height
|
|
81
|
+
// instead of a fixed "3 findings" cap, and pages through the rest via
|
|
82
|
+
// attackPathScroll ('[' / ']').
|
|
83
|
+
const ATTACK_PATH_FINDING_LINES = 3;
|
|
84
|
+
|
|
60
85
|
export class SecurityPanel extends ScrollableListPanel<TokenAuditResult> {
|
|
61
86
|
private readonly unsub: (() => void) | null;
|
|
87
|
+
/** Scroll offset into attackPathReview.findings (in finding units, not lines). */
|
|
88
|
+
private attackPathScroll = 0;
|
|
89
|
+
/** Set by 'f'; consumed by handlePanelIntegrationAction to dispatch /policy preflight. */
|
|
90
|
+
private pendingPreflight = false;
|
|
91
|
+
/** Set by 'i'; consumed by handlePanelIntegrationAction to jump to the incident panel. */
|
|
92
|
+
private pendingIncidentJump = false;
|
|
62
93
|
|
|
63
94
|
public constructor(private readonly readModel: UiReadModel<UiSecuritySnapshot>) {
|
|
64
|
-
super('security', 'Security', '
|
|
95
|
+
super('security', 'Security', '▬', 'security-policy');
|
|
65
96
|
this.showSelectionGutter = true; // I5: non-color selection affordance
|
|
97
|
+
this.filterEnabled = true;
|
|
98
|
+
this.filterLabel = 'Filter tokens';
|
|
66
99
|
this.unsub = this.readModel.subscribe(() => this.markDirty());
|
|
67
100
|
}
|
|
68
101
|
|
|
@@ -73,9 +106,13 @@ export class SecurityPanel extends ScrollableListPanel<TokenAuditResult> {
|
|
|
73
106
|
protected override getPalette() { return C; }
|
|
74
107
|
protected override getEmptyStateMessage() { return ' No API tokens are registered with the security auditor yet.'; }
|
|
75
108
|
protected override getEmptyStateActions() {
|
|
109
|
+
// WO-160: '/policy preflight' dropped — 'f' already dispatches it for
|
|
110
|
+
// real (see handleInput/handlePanelIntegrationAction) and is advertised
|
|
111
|
+
// as a live key hint in the footer even in this empty state, so listing
|
|
112
|
+
// it here too was a redundant action substitute. '/storage review' and
|
|
113
|
+
// '/mcp trust' stay: neither has an in-panel key equivalent.
|
|
76
114
|
return [
|
|
77
115
|
{ command: '/storage review', summary: 'inspect secure secret storage and environment overrides' },
|
|
78
|
-
{ command: '/policy preflight', summary: 'run a live preflight posture review' },
|
|
79
116
|
{ command: '/mcp trust', summary: 'inspect active MCP trust and quarantine posture' },
|
|
80
117
|
];
|
|
81
118
|
}
|
|
@@ -85,24 +122,84 @@ export class SecurityPanel extends ScrollableListPanel<TokenAuditResult> {
|
|
|
85
122
|
}
|
|
86
123
|
|
|
87
124
|
protected renderItem(result: TokenAuditResult, index: number, selected: boolean, width: number): Line {
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
[
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
125
|
+
return buildAlignedRow(
|
|
126
|
+
width,
|
|
127
|
+
[
|
|
128
|
+
{ text: result.label, fg: C.value },
|
|
129
|
+
{ text: result.tokenId, fg: C.info },
|
|
130
|
+
{ text: result.scope.policyId, fg: C.label },
|
|
131
|
+
{ text: resultSummary(result), fg: resultColor(result) },
|
|
132
|
+
],
|
|
133
|
+
[
|
|
134
|
+
{ width: 22 },
|
|
135
|
+
{ width: 12 },
|
|
136
|
+
{ width: 10 },
|
|
137
|
+
{ width: Math.max(8, width - 50) },
|
|
138
|
+
],
|
|
139
|
+
{ selected, selectedBg: C.selectBg },
|
|
140
|
+
);
|
|
96
141
|
}
|
|
97
142
|
|
|
98
143
|
public handleInput(key: string): boolean {
|
|
99
|
-
if (key === 'r') {
|
|
144
|
+
if (!this.filterActive && key === 'r') {
|
|
145
|
+
// Real re-audit: force the read model to recompute (ApiTokenAuditor.auditAll
|
|
146
|
+
// under the hood) right now instead of waiting on the next incidental
|
|
147
|
+
// render, so lastAuditAt visibly advances the instant 'r' is pressed.
|
|
148
|
+
this.readModel.getSnapshot();
|
|
149
|
+
this.markDirty();
|
|
150
|
+
return true;
|
|
151
|
+
}
|
|
152
|
+
if (!this.filterActive && key === 'f') {
|
|
153
|
+
this.pendingPreflight = true;
|
|
154
|
+
this.markDirty();
|
|
155
|
+
return true;
|
|
156
|
+
}
|
|
157
|
+
if (!this.filterActive && key === 'i') {
|
|
158
|
+
if (!this.readModel.getSnapshot().latestIncident) return false;
|
|
159
|
+
this.pendingIncidentJump = true;
|
|
160
|
+
this.markDirty();
|
|
161
|
+
return true;
|
|
162
|
+
}
|
|
163
|
+
if (!this.filterActive && key === '[') {
|
|
164
|
+
this.attackPathScroll = Math.max(0, this.attackPathScroll - 1);
|
|
165
|
+
this.markDirty();
|
|
166
|
+
return true;
|
|
167
|
+
}
|
|
168
|
+
if (!this.filterActive && key === ']') {
|
|
169
|
+
this.attackPathScroll += 1;
|
|
100
170
|
this.markDirty();
|
|
101
171
|
return true;
|
|
102
172
|
}
|
|
103
173
|
return super.handleInput(key);
|
|
104
174
|
}
|
|
105
175
|
|
|
176
|
+
/**
|
|
177
|
+
* f (preflight) and i (jump to incident) both require the integration
|
|
178
|
+
* context (executeCommand / panelManager), which is only available here —
|
|
179
|
+
* same staged-pending-action pattern as worktree-panel.ts and
|
|
180
|
+
* incident-review-panel.ts.
|
|
181
|
+
*/
|
|
182
|
+
public handlePanelIntegrationAction(_key: string, ctx: PanelIntegrationContext): boolean {
|
|
183
|
+
if (this.pendingPreflight) {
|
|
184
|
+
this.pendingPreflight = false;
|
|
185
|
+
if (!ctx.executeCommand) return false;
|
|
186
|
+
void ctx.executeCommand('policy', ['preflight']).catch(() => { /* surfaced via /policy output */ });
|
|
187
|
+
return true;
|
|
188
|
+
}
|
|
189
|
+
if (this.pendingIncidentJump) {
|
|
190
|
+
this.pendingIncidentJump = false;
|
|
191
|
+
ctx.panelManager.open('incident');
|
|
192
|
+
return true;
|
|
193
|
+
}
|
|
194
|
+
return false;
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
protected override filterMatches(result: TokenAuditResult, q: string): boolean {
|
|
198
|
+
return result.label.toLowerCase().includes(q)
|
|
199
|
+
|| result.scope.policyId.toLowerCase().includes(q)
|
|
200
|
+
|| result.tokenId.toLowerCase().includes(q);
|
|
201
|
+
}
|
|
202
|
+
|
|
106
203
|
public render(width: number, height: number): Line[] {
|
|
107
204
|
this.clampSelection();
|
|
108
205
|
const snapshot = this.readModel.getSnapshot();
|
|
@@ -118,7 +215,10 @@ export class SecurityPanel extends ScrollableListPanel<TokenAuditResult> {
|
|
|
118
215
|
const untrustedPlugins = snapshot.untrustedPlugins;
|
|
119
216
|
const attackPathReview = snapshot.attackPathReview;
|
|
120
217
|
const intro = 'Token audit, policy posture, MCP attack-path review, plugin trust, and incident pressure.';
|
|
121
|
-
|
|
218
|
+
// WO-160: dropped the printed '/policy preflight' guidance line — 'f' is
|
|
219
|
+
// already bound to dispatch it for real (see handleInput/
|
|
220
|
+
// handlePanelIntegrationAction below) and is advertised in the keyboard
|
|
221
|
+
// hints footer, so the printed command was a pure action substitute.
|
|
122
222
|
|
|
123
223
|
const governanceLines: Line[] = [
|
|
124
224
|
buildPanelLine(width, [
|
|
@@ -168,7 +268,7 @@ export class SecurityPanel extends ScrollableListPanel<TokenAuditResult> {
|
|
|
168
268
|
[' critical ', C.label],
|
|
169
269
|
...buildStatusPill(attackPathReview.incoherentFindings > 0 ? 'warn' : 'good', String(attackPathReview.incoherentFindings)),
|
|
170
270
|
[' review ', C.label],
|
|
171
|
-
[attackPathReview.summary
|
|
271
|
+
[truncateDisplay(attackPathReview.summary, Math.max(0, width - 36)), C.dim],
|
|
172
272
|
]),
|
|
173
273
|
];
|
|
174
274
|
|
|
@@ -194,7 +294,7 @@ export class SecurityPanel extends ScrollableListPanel<TokenAuditResult> {
|
|
|
194
294
|
{ title: 'Governance', lines: emptyStateLines },
|
|
195
295
|
{ title: 'Attack Paths', lines: attackPathLines },
|
|
196
296
|
],
|
|
197
|
-
footerLines: [
|
|
297
|
+
footerLines: [buildKeyboardHints(width, [{ keys: 'f', label: 'preflight' }], C)],
|
|
198
298
|
palette: C,
|
|
199
299
|
});
|
|
200
300
|
while (workspace.length < height) workspace.push(createEmptyLine(width));
|
|
@@ -203,23 +303,60 @@ export class SecurityPanel extends ScrollableListPanel<TokenAuditResult> {
|
|
|
203
303
|
|
|
204
304
|
if (attackPathReview.findings.length > 0) {
|
|
205
305
|
attackPathLines.push(buildPanelLine(width, [[' MCP attack-path review', C.label]]));
|
|
206
|
-
|
|
306
|
+
// Fully scrollable — no fixed "3 findings" cap. The visible window
|
|
307
|
+
// scales with the panel's actual height; '[' / ']' page through the
|
|
308
|
+
// rest when there are more findings than currently fit.
|
|
309
|
+
const windowSize = Math.max(1, Math.floor((height - 16) / ATTACK_PATH_FINDING_LINES));
|
|
310
|
+
const maxScroll = Math.max(0, attackPathReview.findings.length - windowSize);
|
|
311
|
+
this.attackPathScroll = Math.min(this.attackPathScroll, maxScroll);
|
|
312
|
+
const start = this.attackPathScroll;
|
|
313
|
+
const end = Math.min(attackPathReview.findings.length, start + windowSize);
|
|
314
|
+
for (const finding of attackPathReview.findings.slice(start, end)) {
|
|
207
315
|
attackPathLines.push(buildPanelLine(width, [[
|
|
208
|
-
` ${finding.severity.toUpperCase()} ${finding.serverName}: ${finding.route}
|
|
316
|
+
truncateDisplay(` ${finding.severity.toUpperCase()} ${finding.serverName}: ${finding.route}`, width),
|
|
209
317
|
severityColor(finding.severity),
|
|
210
318
|
]]));
|
|
211
319
|
attackPathLines.push(buildPanelLine(width, [[
|
|
212
|
-
` ${finding.reason}
|
|
320
|
+
truncateDisplay(` ${finding.reason}`, width),
|
|
213
321
|
C.dim,
|
|
214
322
|
]]));
|
|
215
323
|
attackPathLines.push(buildPanelLine(width, [[
|
|
216
|
-
` evidence: ${finding.evidence.join(' | ')}
|
|
324
|
+
truncateDisplay(` evidence: ${finding.evidence.join(' | ')}`, width),
|
|
325
|
+
C.dim,
|
|
326
|
+
]]));
|
|
327
|
+
}
|
|
328
|
+
if (attackPathReview.findings.length > windowSize) {
|
|
329
|
+
attackPathLines.push(buildPanelLine(width, [[
|
|
330
|
+
` showing ${start + 1}-${end} of ${attackPathReview.findings.length} findings ([ / ] to scroll)`,
|
|
217
331
|
C.dim,
|
|
218
332
|
]]));
|
|
219
333
|
}
|
|
220
334
|
}
|
|
221
335
|
|
|
222
|
-
|
|
336
|
+
// Column header for the token audit list so the aligned columns are legible.
|
|
337
|
+
const listHeader: Line[] = [
|
|
338
|
+
...governanceLines,
|
|
339
|
+
buildAlignedRow(
|
|
340
|
+
width,
|
|
341
|
+
[
|
|
342
|
+
{ text: 'TOKEN LABEL', fg: C.label, bold: true },
|
|
343
|
+
{ text: 'TOKEN ID', fg: C.label, bold: true },
|
|
344
|
+
{ text: 'POLICY', fg: C.label, bold: true },
|
|
345
|
+
{ text: `STATUS (${view.results.length} audited)`, fg: C.label, bold: true },
|
|
346
|
+
],
|
|
347
|
+
[
|
|
348
|
+
{ width: 22 },
|
|
349
|
+
{ width: 12 },
|
|
350
|
+
{ width: 10 },
|
|
351
|
+
{ width: Math.max(8, width - 50) },
|
|
352
|
+
],
|
|
353
|
+
{},
|
|
354
|
+
),
|
|
355
|
+
];
|
|
356
|
+
|
|
357
|
+
// Detail must track the filtered view the list highlights, not the raw
|
|
358
|
+
// audit results — under an applied token filter they diverge.
|
|
359
|
+
const selected = this.getSelectedItem();
|
|
223
360
|
const detailLines: Line[] = [];
|
|
224
361
|
if (selected) {
|
|
225
362
|
detailLines.push(buildPanelLine(width, [
|
|
@@ -228,68 +365,83 @@ export class SecurityPanel extends ScrollableListPanel<TokenAuditResult> {
|
|
|
228
365
|
[' Policy: ', C.label],
|
|
229
366
|
[selected.scope.policyId, C.info],
|
|
230
367
|
[' Blocked: ', C.label],
|
|
231
|
-
[selected.blocked ? 'yes' : 'no', selected.blocked ? C.
|
|
368
|
+
[selected.blocked ? 'yes' : 'no', selected.blocked ? C.bad : C.good],
|
|
232
369
|
]));
|
|
233
370
|
detailLines.push(buildPanelLine(width, [
|
|
234
371
|
[' Scope: ', C.label],
|
|
235
|
-
[selected.scope.outcome, selected.scope.outcome === 'violation' ? C.
|
|
372
|
+
[selected.scope.outcome, selected.scope.outcome === 'violation' ? C.bad : C.good],
|
|
236
373
|
[' Excess: ', C.label],
|
|
237
|
-
[(selected.scope.excessScopes.length > 0 ? selected.scope.excessScopes.join(', ') : 'none'
|
|
374
|
+
[truncateDisplay(selected.scope.excessScopes.length > 0 ? selected.scope.excessScopes.join(', ') : 'none', Math.max(0, width - 27)), selected.scope.excessScopes.length > 0 ? C.bad : C.dim],
|
|
238
375
|
]));
|
|
239
376
|
detailLines.push(buildPanelLine(width, [
|
|
240
377
|
[' Rotation: ', C.label],
|
|
241
|
-
[selected.rotation.outcome, selected.rotation.outcome === 'ok' ? C.
|
|
378
|
+
[selected.rotation.outcome, selected.rotation.outcome === 'ok' ? C.good : selected.rotation.outcome === 'warning' ? C.warn : C.bad],
|
|
242
379
|
[' Due: ', C.label],
|
|
243
|
-
[
|
|
380
|
+
[fmtDue(selected.rotation.msUntilDue), selected.rotation.msUntilDue < 0 ? C.bad : C.value],
|
|
244
381
|
[' Age(d): ', C.label],
|
|
245
382
|
[String(Math.floor(selected.rotation.ageMs / (24 * 60 * 60 * 1000))), C.value],
|
|
246
383
|
]));
|
|
247
384
|
detailLines.push(buildPanelLine(width, [[
|
|
248
|
-
`Last audit: ${view.lastAuditAt ?
|
|
385
|
+
`Last audit: ${view.lastAuditAt ? fmtAgo(view.lastAuditAt) : 'never'} Press r to refresh.`,
|
|
249
386
|
C.dim,
|
|
250
387
|
]]));
|
|
251
388
|
if (preflightStatus !== 'n/a') {
|
|
252
389
|
detailLines.push(buildPanelLine(width, [[
|
|
253
|
-
`Policy preflight: ${preflightStatus} (${preflightIssueCount} issue${preflightIssueCount === 1 ? '' : 's'})
|
|
254
|
-
preflightStatus === 'block' ? C.
|
|
390
|
+
truncateDisplay(`Policy preflight: ${preflightStatus} (${preflightIssueCount} issue${preflightIssueCount === 1 ? '' : 's'})`, width),
|
|
391
|
+
preflightStatus === 'block' ? C.bad : preflightStatus === 'warn' ? C.warn : C.dim,
|
|
255
392
|
]]));
|
|
256
393
|
}
|
|
257
394
|
if (quarantinedMcp.length > 0) {
|
|
258
395
|
const server = quarantinedMcp[0]!;
|
|
259
396
|
detailLines.push(buildPanelLine(width, [[
|
|
260
|
-
`MCP quarantine: ${server.name} ${server.quarantineReason ?? 'unknown'}${server.quarantineDetail ? ` - ${server.quarantineDetail}` : ''}
|
|
261
|
-
C.
|
|
397
|
+
truncateDisplay(`MCP quarantine: ${server.name} ${server.quarantineReason ?? 'unknown'}${server.quarantineDetail ? ` - ${server.quarantineDetail}` : ''}`, width),
|
|
398
|
+
C.bad,
|
|
262
399
|
]]));
|
|
263
400
|
}
|
|
264
401
|
if (quarantinedPlugins.length > 0) {
|
|
265
402
|
const plugin = quarantinedPlugins[0]!;
|
|
266
403
|
detailLines.push(buildPanelLine(width, [[
|
|
267
|
-
`Plugin quarantine: ${plugin.name} (${plugin.trustTier})
|
|
268
|
-
C.
|
|
404
|
+
truncateDisplay(`Plugin quarantine: ${plugin.name} (${plugin.trustTier})`, width),
|
|
405
|
+
C.bad,
|
|
269
406
|
]]));
|
|
270
407
|
} else if (untrustedPlugins.length > 0) {
|
|
271
408
|
const plugin = untrustedPlugins[0]!;
|
|
272
409
|
detailLines.push(buildPanelLine(width, [[
|
|
273
|
-
`Plugin trust warning: ${plugin.name} remains untrusted
|
|
410
|
+
truncateDisplay(`Plugin trust warning: ${plugin.name} remains untrusted.`, width),
|
|
274
411
|
C.warn,
|
|
275
412
|
]]));
|
|
276
413
|
}
|
|
277
414
|
if (latestIncident) {
|
|
278
415
|
detailLines.push(buildPanelLine(width, [[
|
|
279
|
-
`Latest incident: ${latestIncident.classification} - ${latestIncident.summary}
|
|
416
|
+
truncateDisplay(`Latest incident: ${latestIncident.classification} - ${latestIncident.summary}`, width),
|
|
280
417
|
C.warn,
|
|
281
418
|
]]));
|
|
282
419
|
}
|
|
283
420
|
}
|
|
284
421
|
|
|
422
|
+
const hints = this.filterActive
|
|
423
|
+
? [
|
|
424
|
+
{ keys: 'type', label: 'filter tokens' },
|
|
425
|
+
{ keys: 'Enter', label: 'apply' },
|
|
426
|
+
{ keys: 'Esc', label: 'clear' },
|
|
427
|
+
]
|
|
428
|
+
: [
|
|
429
|
+
{ keys: '↑/↓', label: 'select token' },
|
|
430
|
+
{ keys: '/', label: 'filter' },
|
|
431
|
+
{ keys: 'r', label: 'refresh audit' },
|
|
432
|
+
{ keys: 'f', label: 'preflight' },
|
|
433
|
+
...(latestIncident ? [{ keys: 'i', label: 'jump to incident' }] : []),
|
|
434
|
+
...(attackPathReview.findings.length > 0 ? [{ keys: '[ / ]', label: 'scroll attack paths' }] : []),
|
|
435
|
+
];
|
|
436
|
+
|
|
285
437
|
return this.renderList(width, height, {
|
|
286
438
|
title: 'Security Control Room',
|
|
287
|
-
header:
|
|
439
|
+
header: listHeader,
|
|
288
440
|
footer: [
|
|
289
441
|
...detailLines,
|
|
290
442
|
...attackPathLines,
|
|
291
|
-
footerLine,
|
|
292
443
|
],
|
|
444
|
+
hints,
|
|
293
445
|
});
|
|
294
446
|
}
|
|
295
447
|
}
|