@pellux/goodvibes-tui 1.1.0 → 1.7.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (194) hide show
  1. package/CHANGELOG.md +85 -21
  2. package/README.md +20 -11
  3. package/docs/foundation-artifacts/operator-contract.json +1 -1
  4. package/package.json +2 -2
  5. package/src/core/alert-gating.ts +67 -0
  6. package/src/core/approval-alert.ts +72 -0
  7. package/src/core/budget-breach-notifier.ts +106 -0
  8. package/src/core/conversation-rendering.ts +19 -0
  9. package/src/core/conversation.ts +18 -0
  10. package/src/core/focus-tracker.ts +41 -0
  11. package/src/core/long-task-notifier.ts +33 -6
  12. package/src/core/system-message-router.ts +37 -51
  13. package/src/core/turn-cancellation.ts +20 -0
  14. package/src/core/turn-event-wiring.ts +64 -3
  15. package/src/export/cost-utils.ts +36 -0
  16. package/src/input/command-registry.ts +38 -1
  17. package/src/input/commands/checkpoint-runtime.ts +280 -0
  18. package/src/input/commands/codebase-runtime.ts +192 -0
  19. package/src/input/commands/eval.ts +1 -1
  20. package/src/input/commands/health-runtime.ts +1 -1
  21. package/src/input/commands/image-runtime.ts +112 -0
  22. package/src/input/commands/intelligence-runtime.ts +11 -1
  23. package/src/input/commands/local-auth-runtime.ts +4 -1
  24. package/src/input/commands/local-runtime.ts +9 -3
  25. package/src/input/commands/marketplace-runtime.ts +2 -2
  26. package/src/input/commands/memory.ts +6 -1
  27. package/src/input/commands/operator-panel-runtime.ts +40 -24
  28. package/src/input/commands/planning-runtime.ts +26 -3
  29. package/src/input/commands/platform-sandbox-runtime.ts +1 -6
  30. package/src/input/commands/plugin-runtime.ts +2 -2
  31. package/src/input/commands/policy-dispatch.ts +21 -0
  32. package/src/input/commands/provider-accounts-runtime.ts +1 -1
  33. package/src/input/commands/qrcode-runtime.ts +3 -3
  34. package/src/input/commands/recall-review.ts +35 -0
  35. package/src/input/commands/remote-runtime.ts +3 -3
  36. package/src/input/commands/runtime-services.ts +54 -13
  37. package/src/input/commands/services-runtime.ts +1 -1
  38. package/src/input/commands/session-workflow.ts +16 -1
  39. package/src/input/commands/settings-sync-runtime.ts +1 -1
  40. package/src/input/commands/shell-core.ts +15 -7
  41. package/src/input/commands/skills-runtime.ts +2 -8
  42. package/src/input/commands/subscription-runtime.ts +2 -2
  43. package/src/input/commands/test-runtime.ts +277 -0
  44. package/src/input/commands/websearch-runtime.ts +101 -0
  45. package/src/input/commands/work-plan-runtime.ts +36 -10
  46. package/src/input/commands/workstream-runtime.ts +338 -0
  47. package/src/input/commands.ts +12 -0
  48. package/src/input/config-modal-types.ts +161 -0
  49. package/src/input/config-modal.ts +377 -0
  50. package/src/input/feed-context-factory.ts +7 -8
  51. package/src/input/handler-command-route.ts +27 -17
  52. package/src/input/handler-feed-routes.ts +61 -23
  53. package/src/input/handler-feed.ts +47 -23
  54. package/src/input/handler-interactions.ts +1 -3
  55. package/src/input/handler-modal-routes.ts +65 -0
  56. package/src/input/handler-modal-stack.ts +3 -5
  57. package/src/input/handler-modal-token-routes.ts +16 -49
  58. package/src/input/handler-picker-routes.ts +11 -94
  59. package/src/input/handler-shortcuts.ts +24 -14
  60. package/src/input/handler-types.ts +2 -6
  61. package/src/input/handler-ui-state.ts +10 -22
  62. package/src/input/handler.ts +6 -28
  63. package/src/input/keybindings.ts +22 -8
  64. package/src/input/model-picker-types.ts +24 -6
  65. package/src/input/model-picker.ts +58 -0
  66. package/src/input/panel-integration-actions.ts +9 -170
  67. package/src/input/settings-modal-data.ts +95 -0
  68. package/src/input/settings-modal-mutations.ts +6 -4
  69. package/src/main.ts +24 -27
  70. package/src/panels/agent-inspector-shared.ts +2 -1
  71. package/src/panels/base-panel.ts +22 -1
  72. package/src/panels/builtin/agent.ts +21 -107
  73. package/src/panels/builtin/development.ts +15 -61
  74. package/src/panels/builtin/knowledge.ts +8 -32
  75. package/src/panels/builtin/operations.ts +84 -428
  76. package/src/panels/builtin/session.ts +21 -112
  77. package/src/panels/builtin/shared.ts +9 -43
  78. package/src/panels/builtin-modals.ts +218 -0
  79. package/src/panels/builtin-panels.ts +5 -0
  80. package/src/panels/cost-tracker-panel.ts +36 -10
  81. package/src/panels/diff-panel.ts +12 -43
  82. package/src/panels/eval-registry.ts +60 -0
  83. package/src/panels/fleet-panel.ts +800 -0
  84. package/src/panels/fleet-read-model.ts +477 -0
  85. package/src/panels/fleet-steer.ts +92 -0
  86. package/src/panels/fleet-stop.ts +125 -0
  87. package/src/panels/fleet-tabs.ts +230 -0
  88. package/src/panels/fleet-transcript.ts +356 -0
  89. package/src/panels/index.ts +7 -31
  90. package/src/panels/modals/hooks-modal.ts +187 -0
  91. package/src/panels/modals/keybindings-modal.ts +151 -0
  92. package/src/panels/modals/knowledge-modal.ts +158 -0
  93. package/src/panels/modals/local-auth-modal.ts +132 -0
  94. package/src/panels/modals/marketplace-modal.ts +218 -0
  95. package/src/panels/modals/memory-modal.ts +180 -0
  96. package/src/panels/modals/modal-surface-helpers.ts +54 -0
  97. package/src/panels/modals/modal-theme.ts +36 -0
  98. package/src/panels/modals/pairing-modal.ts +144 -0
  99. package/src/panels/modals/planning-modal.ts +282 -0
  100. package/src/panels/modals/plugins-modal.ts +174 -0
  101. package/src/panels/modals/policy-modal.ts +256 -0
  102. package/src/panels/modals/provider-health-modal.ts +136 -0
  103. package/src/panels/modals/remote-modal.ts +115 -0
  104. package/src/panels/modals/sandbox-modal.ts +150 -0
  105. package/src/panels/modals/security-modal.ts +217 -0
  106. package/src/panels/modals/services-modal.ts +179 -0
  107. package/src/panels/modals/settings-sync-modal.ts +142 -0
  108. package/src/panels/modals/skills-modal.ts +189 -0
  109. package/src/panels/modals/subscription-modal.ts +172 -0
  110. package/src/panels/modals/work-plan-modal.ts +149 -0
  111. package/src/panels/panel-confirm-overlay.ts +72 -0
  112. package/src/panels/panel-manager.ts +108 -5
  113. package/src/panels/project-planning-answer-actions.ts +4 -2
  114. package/src/panels/skills-panel.ts +7 -51
  115. package/src/panels/types.ts +13 -0
  116. package/src/permissions/hunk-selection.ts +179 -0
  117. package/src/permissions/prompt.ts +60 -4
  118. package/src/renderer/compaction-history-modal.ts +19 -3
  119. package/src/renderer/compaction-preview.ts +13 -2
  120. package/src/renderer/compaction-quality.ts +100 -0
  121. package/src/renderer/config-modal.ts +81 -0
  122. package/src/renderer/conversation-overlays.ts +10 -17
  123. package/src/renderer/fleet-tab-strip.ts +56 -0
  124. package/src/renderer/footer-tips.ts +10 -2
  125. package/src/renderer/git-status.ts +40 -0
  126. package/src/renderer/help-overlay.ts +2 -2
  127. package/src/renderer/model-workspace.ts +44 -1
  128. package/src/renderer/panel-workspace-bar.ts +8 -2
  129. package/src/renderer/turn-injection.ts +114 -0
  130. package/src/runtime/bootstrap-command-context.ts +21 -1
  131. package/src/runtime/bootstrap-command-parts.ts +34 -7
  132. package/src/runtime/bootstrap-core.ts +14 -4
  133. package/src/runtime/bootstrap-shell.ts +29 -16
  134. package/src/runtime/bootstrap.ts +11 -17
  135. package/src/runtime/code-index-services.ts +112 -0
  136. package/src/runtime/orchestrator-core-services.ts +49 -0
  137. package/src/runtime/process-lifecycle.ts +3 -1
  138. package/src/runtime/services.ts +91 -43
  139. package/src/runtime/ui-services.ts +8 -0
  140. package/src/runtime/workstream-services.ts +195 -0
  141. package/src/shell/blocking-input.ts +22 -1
  142. package/src/shell/ui-openers.ts +129 -17
  143. package/src/utils/format-duration.ts +8 -18
  144. package/src/utils/splash-lines.ts +1 -1
  145. package/src/version.ts +1 -1
  146. package/src/panels/agent-inspector-panel.ts +0 -786
  147. package/src/panels/approval-panel.ts +0 -252
  148. package/src/panels/automation-control-panel.ts +0 -479
  149. package/src/panels/cockpit-panel.ts +0 -481
  150. package/src/panels/cockpit-read-model.ts +0 -238
  151. package/src/panels/communication-panel.ts +0 -256
  152. package/src/panels/control-plane-panel.ts +0 -470
  153. package/src/panels/debug-panel.ts +0 -615
  154. package/src/panels/docs-panel.ts +0 -384
  155. package/src/panels/eval-panel.ts +0 -627
  156. package/src/panels/file-explorer-panel.ts +0 -673
  157. package/src/panels/file-preview-panel.ts +0 -517
  158. package/src/panels/hooks-panel.ts +0 -401
  159. package/src/panels/incident-review-panel.ts +0 -406
  160. package/src/panels/intelligence-panel.ts +0 -383
  161. package/src/panels/knowledge-graph-panel.ts +0 -515
  162. package/src/panels/marketplace-panel.ts +0 -399
  163. package/src/panels/memory-panel.ts +0 -558
  164. package/src/panels/ops-control-panel.ts +0 -329
  165. package/src/panels/ops-strategy-panel.ts +0 -321
  166. package/src/panels/orchestration-panel.ts +0 -465
  167. package/src/panels/panel-list-panel.ts +0 -566
  168. package/src/panels/plan-dashboard-panel.ts +0 -707
  169. package/src/panels/policy-panel.ts +0 -517
  170. package/src/panels/project-planning-panel.ts +0 -731
  171. package/src/panels/provider-health-panel.ts +0 -678
  172. package/src/panels/provider-health-tracker.ts +0 -310
  173. package/src/panels/provider-health-views.ts +0 -567
  174. package/src/panels/qr-panel.ts +0 -280
  175. package/src/panels/remote-panel.ts +0 -534
  176. package/src/panels/routes-panel.ts +0 -241
  177. package/src/panels/sandbox-panel.ts +0 -456
  178. package/src/panels/security-panel.ts +0 -447
  179. package/src/panels/services-panel.ts +0 -329
  180. package/src/panels/session-browser-panel.ts +0 -496
  181. package/src/panels/settings-sync-panel.ts +0 -398
  182. package/src/panels/subscription-panel.ts +0 -342
  183. package/src/panels/symbol-outline-panel.ts +0 -619
  184. package/src/panels/system-messages-panel.ts +0 -364
  185. package/src/panels/tasks-panel.ts +0 -608
  186. package/src/panels/thinking-panel.ts +0 -333
  187. package/src/panels/tool-inspector-panel.ts +0 -537
  188. package/src/panels/work-plan-panel.ts +0 -540
  189. package/src/panels/worktree-panel.ts +0 -360
  190. package/src/panels/wrfc-panel.ts +0 -790
  191. package/src/renderer/agent-detail-modal.ts +0 -466
  192. package/src/renderer/live-tail-modal.ts +0 -156
  193. package/src/renderer/process-modal.ts +0 -671
  194. package/src/renderer/qr-renderer.ts +0 -120
@@ -1,447 +0,0 @@
1
- import type { Line } from '../types/grid.ts';
2
- import { truncateDisplay } from '../utils/terminal-width.ts';
3
- import { ScrollableListPanel } from './scrollable-list-panel.ts';
4
- import type { TokenAuditResult } from '@pellux/goodvibes-sdk/platform/security';
5
- import type { UiReadModel, UiSecuritySnapshot } from '../runtime/ui-read-models.ts';
6
- import {
7
- buildAlignedRow,
8
- buildEmptyState,
9
- buildKeyboardHints,
10
- buildPanelLine,
11
- buildPanelWorkspace,
12
- buildStatusPill,
13
- DEFAULT_PANEL_PALETTE,
14
- } from './polish.ts';
15
- import { createEmptyLine } from '../types/grid.ts';
16
- import type { PanelIntegrationContext } from './types.ts';
17
-
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;
21
-
22
- function managedColor(managed: boolean): string {
23
- return managed ? C.warn : C.info;
24
- }
25
-
26
- function resultColor(result: TokenAuditResult): string {
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;
30
- if (result.rotation.outcome === 'warning') return C.warn;
31
- return C.good;
32
- }
33
-
34
- function resultSummary(result: TokenAuditResult): string {
35
- const parts: string[] = [];
36
- if (result.scope.outcome === 'violation') parts.push(`scope:${result.scope.excessScopes.join(',')}`);
37
- if (result.rotation.outcome === 'warning') parts.push('rotation warning');
38
- if (result.rotation.outcome === 'overdue') parts.push('rotation overdue');
39
- if (result.blocked) parts.push('blocked');
40
- return parts.length > 0 ? parts.join(' | ') : 'in policy';
41
- }
42
-
43
- function severityColor(severity: 'low' | 'medium' | 'high' | 'critical'): string {
44
- switch (severity) {
45
- case 'critical':
46
- case 'high':
47
- return C.bad;
48
- case 'medium':
49
- return C.warn;
50
- case 'low':
51
- default:
52
- return C.good;
53
- }
54
- }
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
-
85
- export class SecurityPanel extends ScrollableListPanel<TokenAuditResult> {
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;
93
-
94
- public constructor(private readonly readModel: UiReadModel<UiSecuritySnapshot>) {
95
- super('security', 'Security', '▬', 'security-policy');
96
- this.showSelectionGutter = true; // I5: non-color selection affordance
97
- this.filterEnabled = true;
98
- this.filterLabel = 'Filter tokens';
99
- this.unsub = this.readModel.subscribe(() => this.markDirty());
100
- }
101
-
102
- public override onDestroy(): void {
103
- this.unsub?.();
104
- }
105
-
106
- protected override getPalette() { return C; }
107
- protected override getEmptyStateMessage() { return ' No API tokens are registered with the security auditor yet.'; }
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.
114
- return [
115
- { command: '/storage review', summary: 'inspect secure secret storage and environment overrides' },
116
- { command: '/mcp trust', summary: 'inspect active MCP trust and quarantine posture' },
117
- ];
118
- }
119
-
120
- protected getItems(): readonly TokenAuditResult[] {
121
- return this.readModel.getSnapshot().audit.results;
122
- }
123
-
124
- protected renderItem(result: TokenAuditResult, index: number, selected: boolean, width: number): Line {
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
- );
141
- }
142
-
143
- public handleInput(key: string): boolean {
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;
170
- this.markDirty();
171
- return true;
172
- }
173
- return super.handleInput(key);
174
- }
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
-
203
- public render(width: number, height: number): Line[] {
204
- this.clampSelection();
205
- const snapshot = this.readModel.getSnapshot();
206
- const view = snapshot.audit;
207
- const preflightStatus = snapshot.policy.preflightStatus;
208
- const preflightIssueCount = snapshot.policy.preflightIssueCount;
209
- const lintFindingCount = snapshot.policy.lintFindingCount;
210
- const quarantinedMcp = snapshot.mcpServers.filter((server) => server.schemaFreshness === 'quarantined');
211
- const elevatedMcp = snapshot.mcpServers.filter((server) => server.trustMode === 'allow-all');
212
- const incidents = snapshot.incidents;
213
- const latestIncident = snapshot.latestIncident;
214
- const quarantinedPlugins = snapshot.quarantinedPlugins;
215
- const untrustedPlugins = snapshot.untrustedPlugins;
216
- const attackPathReview = snapshot.attackPathReview;
217
- const intro = 'Token audit, policy posture, MCP attack-path review, plugin trust, and incident pressure.';
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.
222
-
223
- const governanceLines: Line[] = [
224
- buildPanelLine(width, [
225
- [' mode ', C.label],
226
- [view.managed ? 'MANAGED' : 'ADVISORY', managedColor(view.managed)],
227
- [' tokens ', C.label],
228
- [String(view.totalTokens), C.value],
229
- [' blocked ', C.label],
230
- ...buildStatusPill(view.blocked.length > 0 ? 'bad' : 'good', String(view.blocked.length)),
231
- [' scope violations ', C.label],
232
- ...buildStatusPill(view.scopeViolations.length > 0 ? 'bad' : 'good', String(view.scopeViolations.length)),
233
- [' overdue ', C.label],
234
- ...buildStatusPill(view.rotationOverdue.length > 0 ? 'bad' : 'good', String(view.rotationOverdue.length)),
235
- [' warnings ', C.label],
236
- ...buildStatusPill(view.rotationWarnings.length > 0 ? 'warn' : 'good', String(view.rotationWarnings.length)),
237
- ]),
238
- buildPanelLine(width, [
239
- [' preflight ', C.label],
240
- ...buildStatusPill(preflightStatus === 'block' ? 'bad' : preflightStatus === 'warn' ? 'warn' : preflightStatus === 'pass' ? 'good' : 'info', preflightStatus.toUpperCase()),
241
- [' issues ', C.label],
242
- ...buildStatusPill(preflightIssueCount > 0 ? 'warn' : 'good', String(preflightIssueCount)),
243
- [' lint ', C.label],
244
- ...buildStatusPill(lintFindingCount > 0 ? 'warn' : 'good', String(lintFindingCount)),
245
- [' denied permissions ', C.label],
246
- ...buildStatusPill(snapshot.deniedPermissions > 0 ? 'warn' : 'good', String(snapshot.deniedPermissions)),
247
- ]),
248
- buildPanelLine(width, [
249
- [' quarantined MCP ', C.label],
250
- ...buildStatusPill(quarantinedMcp.length > 0 ? 'bad' : 'good', String(quarantinedMcp.length)),
251
- [' elevated MCP ', C.label],
252
- ...buildStatusPill(elevatedMcp.length > 0 ? 'warn' : 'good', String(elevatedMcp.length)),
253
- [' quarantined plugins ', C.label],
254
- ...buildStatusPill(quarantinedPlugins.length > 0 ? 'bad' : 'good', String(quarantinedPlugins.length)),
255
- [' untrusted plugins ', C.label],
256
- ...buildStatusPill(untrustedPlugins.length > 0 ? 'warn' : 'good', String(untrustedPlugins.length)),
257
- ]),
258
- buildPanelLine(width, [
259
- [' incidents ', C.label],
260
- ...buildStatusPill(incidents.length > 0 ? 'warn' : 'good', String(incidents.length)),
261
- ]),
262
- ];
263
-
264
- const attackPathLines: Line[] = [
265
- buildPanelLine(width, [
266
- [' attack paths ', C.label],
267
- ...buildStatusPill(attackPathReview.criticalFindings > 0 ? 'bad' : 'good', String(attackPathReview.criticalFindings)),
268
- [' critical ', C.label],
269
- ...buildStatusPill(attackPathReview.incoherentFindings > 0 ? 'warn' : 'good', String(attackPathReview.incoherentFindings)),
270
- [' review ', C.label],
271
- [truncateDisplay(attackPathReview.summary, Math.max(0, width - 36)), C.dim],
272
- ]),
273
- ];
274
-
275
- // Empty state: no token results yet — show governance + threat posture before base empty state
276
- if (view.results.length === 0) {
277
- const emptyStateLines = [
278
- ...governanceLines,
279
- ...buildEmptyState(
280
- width,
281
- this.getEmptyStateMessage(),
282
- 'The security control room can already review policy, MCP, plugin, and incident posture, but token-specific scope and rotation audit data has not been registered.',
283
- this.getEmptyStateActions(),
284
- C,
285
- ),
286
- ];
287
- if (quarantinedMcp.length > 0) {
288
- emptyStateLines.push(buildPanelLine(width, [[' MCP quarantine still active despite no registered tokens.', C.warn]]));
289
- }
290
- const workspace = buildPanelWorkspace(width, height, {
291
- title: 'Security Control Room',
292
- intro,
293
- sections: [
294
- { title: 'Governance', lines: emptyStateLines },
295
- { title: 'Attack Paths', lines: attackPathLines },
296
- ],
297
- footerLines: [buildKeyboardHints(width, [{ keys: 'f', label: 'preflight' }], C)],
298
- palette: C,
299
- });
300
- while (workspace.length < height) workspace.push(createEmptyLine(width));
301
- return workspace.slice(0, height);
302
- }
303
-
304
- if (attackPathReview.findings.length > 0) {
305
- attackPathLines.push(buildPanelLine(width, [[' MCP attack-path review', C.label]]));
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)) {
315
- attackPathLines.push(buildPanelLine(width, [[
316
- truncateDisplay(` ${finding.severity.toUpperCase()} ${finding.serverName}: ${finding.route}`, width),
317
- severityColor(finding.severity),
318
- ]]));
319
- attackPathLines.push(buildPanelLine(width, [[
320
- truncateDisplay(` ${finding.reason}`, width),
321
- C.dim,
322
- ]]));
323
- attackPathLines.push(buildPanelLine(width, [[
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)`,
331
- C.dim,
332
- ]]));
333
- }
334
- }
335
-
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();
360
- const detailLines: Line[] = [];
361
- if (selected) {
362
- detailLines.push(buildPanelLine(width, [
363
- [' Token: ', C.label],
364
- [selected.label, C.value],
365
- [' Policy: ', C.label],
366
- [selected.scope.policyId, C.info],
367
- [' Blocked: ', C.label],
368
- [selected.blocked ? 'yes' : 'no', selected.blocked ? C.bad : C.good],
369
- ]));
370
- detailLines.push(buildPanelLine(width, [
371
- [' Scope: ', C.label],
372
- [selected.scope.outcome, selected.scope.outcome === 'violation' ? C.bad : C.good],
373
- [' Excess: ', C.label],
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],
375
- ]));
376
- detailLines.push(buildPanelLine(width, [
377
- [' Rotation: ', C.label],
378
- [selected.rotation.outcome, selected.rotation.outcome === 'ok' ? C.good : selected.rotation.outcome === 'warning' ? C.warn : C.bad],
379
- [' Due: ', C.label],
380
- [fmtDue(selected.rotation.msUntilDue), selected.rotation.msUntilDue < 0 ? C.bad : C.value],
381
- [' Age(d): ', C.label],
382
- [String(Math.floor(selected.rotation.ageMs / (24 * 60 * 60 * 1000))), C.value],
383
- ]));
384
- detailLines.push(buildPanelLine(width, [[
385
- `Last audit: ${view.lastAuditAt ? fmtAgo(view.lastAuditAt) : 'never'} Press r to refresh.`,
386
- C.dim,
387
- ]]));
388
- if (preflightStatus !== 'n/a') {
389
- detailLines.push(buildPanelLine(width, [[
390
- truncateDisplay(`Policy preflight: ${preflightStatus} (${preflightIssueCount} issue${preflightIssueCount === 1 ? '' : 's'})`, width),
391
- preflightStatus === 'block' ? C.bad : preflightStatus === 'warn' ? C.warn : C.dim,
392
- ]]));
393
- }
394
- if (quarantinedMcp.length > 0) {
395
- const server = quarantinedMcp[0]!;
396
- detailLines.push(buildPanelLine(width, [[
397
- truncateDisplay(`MCP quarantine: ${server.name} ${server.quarantineReason ?? 'unknown'}${server.quarantineDetail ? ` - ${server.quarantineDetail}` : ''}`, width),
398
- C.bad,
399
- ]]));
400
- }
401
- if (quarantinedPlugins.length > 0) {
402
- const plugin = quarantinedPlugins[0]!;
403
- detailLines.push(buildPanelLine(width, [[
404
- truncateDisplay(`Plugin quarantine: ${plugin.name} (${plugin.trustTier})`, width),
405
- C.bad,
406
- ]]));
407
- } else if (untrustedPlugins.length > 0) {
408
- const plugin = untrustedPlugins[0]!;
409
- detailLines.push(buildPanelLine(width, [[
410
- truncateDisplay(`Plugin trust warning: ${plugin.name} remains untrusted.`, width),
411
- C.warn,
412
- ]]));
413
- }
414
- if (latestIncident) {
415
- detailLines.push(buildPanelLine(width, [[
416
- truncateDisplay(`Latest incident: ${latestIncident.classification} - ${latestIncident.summary}`, width),
417
- C.warn,
418
- ]]));
419
- }
420
- }
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
-
437
- return this.renderList(width, height, {
438
- title: 'Security Control Room',
439
- header: listHeader,
440
- footer: [
441
- ...detailLines,
442
- ...attackPathLines,
443
- ],
444
- hints,
445
- });
446
- }
447
- }