@pellux/goodvibes-tui 1.0.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 (209) hide show
  1. package/CHANGELOG.md +130 -127
  2. package/README.md +32 -14
  3. package/docs/foundation-artifacts/operator-contract.json +1 -1
  4. package/package.json +2 -2
  5. package/src/cli/ensure-goodvibes-gitignore.ts +32 -0
  6. package/src/cli/entrypoint.ts +2 -0
  7. package/src/core/alert-gating.ts +67 -0
  8. package/src/core/approval-alert.ts +72 -0
  9. package/src/core/budget-breach-notifier.ts +106 -0
  10. package/src/core/conversation-rendering.ts +19 -0
  11. package/src/core/conversation.ts +58 -0
  12. package/src/core/focus-tracker.ts +41 -0
  13. package/src/core/long-task-notifier.ts +33 -6
  14. package/src/core/stream-event-wiring.ts +104 -2
  15. package/src/core/stream-stall-watchdog.ts +41 -17
  16. package/src/core/system-message-router.ts +37 -51
  17. package/src/core/turn-cancellation.ts +20 -0
  18. package/src/core/turn-event-wiring.ts +64 -3
  19. package/src/export/cost-utils.ts +126 -8
  20. package/src/input/command-registry.ts +54 -1
  21. package/src/input/commands/checkpoint-runtime.ts +280 -0
  22. package/src/input/commands/codebase-runtime.ts +192 -0
  23. package/src/input/commands/diff-runtime.ts +61 -30
  24. package/src/input/commands/eval.ts +1 -1
  25. package/src/input/commands/health-runtime.ts +1 -1
  26. package/src/input/commands/image-runtime.ts +112 -0
  27. package/src/input/commands/intelligence-runtime.ts +11 -1
  28. package/src/input/commands/local-auth-runtime.ts +4 -1
  29. package/src/input/commands/local-runtime.ts +9 -3
  30. package/src/input/commands/marketplace-runtime.ts +2 -2
  31. package/src/input/commands/memory.ts +6 -1
  32. package/src/input/commands/operator-panel-runtime.ts +40 -24
  33. package/src/input/commands/planning-runtime.ts +26 -3
  34. package/src/input/commands/platform-sandbox-runtime.ts +1 -6
  35. package/src/input/commands/plugin-runtime.ts +2 -2
  36. package/src/input/commands/policy-dispatch.ts +21 -0
  37. package/src/input/commands/provider-accounts-runtime.ts +1 -1
  38. package/src/input/commands/qrcode-runtime.ts +3 -3
  39. package/src/input/commands/recall-review.ts +35 -0
  40. package/src/input/commands/remote-runtime.ts +3 -3
  41. package/src/input/commands/runtime-services.ts +54 -13
  42. package/src/input/commands/services-runtime.ts +1 -1
  43. package/src/input/commands/session-content.ts +12 -0
  44. package/src/input/commands/session-workflow.ts +19 -1
  45. package/src/input/commands/settings-sync-runtime.ts +1 -1
  46. package/src/input/commands/share-runtime.ts +9 -2
  47. package/src/input/commands/shell-core.ts +15 -7
  48. package/src/input/commands/skills-runtime.ts +2 -8
  49. package/src/input/commands/subscription-runtime.ts +2 -2
  50. package/src/input/commands/test-runtime.ts +277 -0
  51. package/src/input/commands/websearch-runtime.ts +101 -0
  52. package/src/input/commands/work-plan-runtime.ts +36 -10
  53. package/src/input/commands/workstream-runtime.ts +338 -0
  54. package/src/input/commands.ts +12 -0
  55. package/src/input/config-modal-types.ts +161 -0
  56. package/src/input/config-modal.ts +377 -0
  57. package/src/input/feed-context-factory.ts +7 -8
  58. package/src/input/handler-command-route.ts +27 -17
  59. package/src/input/handler-content-actions.ts +22 -8
  60. package/src/input/handler-feed-routes.ts +107 -7
  61. package/src/input/handler-feed.ts +49 -13
  62. package/src/input/handler-interactions.ts +3 -3
  63. package/src/input/handler-modal-routes.ts +65 -0
  64. package/src/input/handler-modal-stack.ts +3 -5
  65. package/src/input/handler-modal-token-routes.ts +16 -49
  66. package/src/input/handler-picker-routes.ts +11 -94
  67. package/src/input/handler-shortcuts.ts +24 -14
  68. package/src/input/handler-types.ts +3 -6
  69. package/src/input/handler-ui-state.ts +10 -22
  70. package/src/input/handler.ts +10 -28
  71. package/src/input/keybindings.ts +22 -8
  72. package/src/input/model-picker-types.ts +24 -6
  73. package/src/input/model-picker.ts +58 -0
  74. package/src/input/panel-integration-actions.ts +9 -170
  75. package/src/input/settings-modal-data.ts +95 -0
  76. package/src/input/settings-modal-mutations.ts +6 -4
  77. package/src/main.ts +41 -44
  78. package/src/panels/agent-inspector-shared.ts +35 -11
  79. package/src/panels/base-panel.ts +22 -1
  80. package/src/panels/builtin/agent.ts +21 -107
  81. package/src/panels/builtin/development.ts +16 -62
  82. package/src/panels/builtin/knowledge.ts +8 -32
  83. package/src/panels/builtin/operations.ts +84 -428
  84. package/src/panels/builtin/session.ts +21 -112
  85. package/src/panels/builtin/shared.ts +9 -43
  86. package/src/panels/builtin-modals.ts +218 -0
  87. package/src/panels/builtin-panels.ts +5 -0
  88. package/src/panels/cost-tracker-panel.ts +63 -14
  89. package/src/panels/diff-panel.ts +123 -60
  90. package/src/panels/eval-registry.ts +60 -0
  91. package/src/panels/fleet-panel.ts +800 -0
  92. package/src/panels/fleet-read-model.ts +477 -0
  93. package/src/panels/fleet-steer.ts +92 -0
  94. package/src/panels/fleet-stop.ts +125 -0
  95. package/src/panels/fleet-tabs.ts +230 -0
  96. package/src/panels/fleet-transcript.ts +356 -0
  97. package/src/panels/git-panel.ts +9 -9
  98. package/src/panels/index.ts +7 -31
  99. package/src/panels/local-auth-panel.ts +10 -0
  100. package/src/panels/modals/hooks-modal.ts +187 -0
  101. package/src/panels/modals/keybindings-modal.ts +151 -0
  102. package/src/panels/modals/knowledge-modal.ts +158 -0
  103. package/src/panels/modals/local-auth-modal.ts +132 -0
  104. package/src/panels/modals/marketplace-modal.ts +218 -0
  105. package/src/panels/modals/memory-modal.ts +180 -0
  106. package/src/panels/modals/modal-surface-helpers.ts +54 -0
  107. package/src/panels/modals/modal-theme.ts +36 -0
  108. package/src/panels/modals/pairing-modal.ts +144 -0
  109. package/src/panels/modals/planning-modal.ts +282 -0
  110. package/src/panels/modals/plugins-modal.ts +174 -0
  111. package/src/panels/modals/policy-modal.ts +256 -0
  112. package/src/panels/modals/provider-health-modal.ts +136 -0
  113. package/src/panels/modals/remote-modal.ts +115 -0
  114. package/src/panels/modals/sandbox-modal.ts +150 -0
  115. package/src/panels/modals/security-modal.ts +217 -0
  116. package/src/panels/modals/services-modal.ts +179 -0
  117. package/src/panels/modals/settings-sync-modal.ts +142 -0
  118. package/src/panels/modals/skills-modal.ts +189 -0
  119. package/src/panels/modals/subscription-modal.ts +172 -0
  120. package/src/panels/modals/work-plan-modal.ts +149 -0
  121. package/src/panels/panel-confirm-overlay.ts +72 -0
  122. package/src/panels/panel-manager.ts +108 -5
  123. package/src/panels/project-planning-answer-actions.ts +4 -2
  124. package/src/panels/scrollable-list-panel.ts +9 -0
  125. package/src/panels/skills-panel.ts +7 -51
  126. package/src/panels/token-budget-panel.ts +5 -3
  127. package/src/panels/types.ts +26 -0
  128. package/src/permissions/hunk-selection.ts +179 -0
  129. package/src/permissions/prompt.ts +60 -4
  130. package/src/renderer/compaction-history-modal.ts +19 -3
  131. package/src/renderer/compaction-preview.ts +13 -2
  132. package/src/renderer/compaction-quality.ts +100 -0
  133. package/src/renderer/config-modal.ts +81 -0
  134. package/src/renderer/conversation-overlays.ts +10 -17
  135. package/src/renderer/fleet-tab-strip.ts +56 -0
  136. package/src/renderer/footer-tips.ts +10 -2
  137. package/src/renderer/git-status.ts +40 -0
  138. package/src/renderer/help-overlay.ts +2 -2
  139. package/src/renderer/model-workspace.ts +44 -1
  140. package/src/renderer/panel-workspace-bar.ts +8 -2
  141. package/src/renderer/shell-surface.ts +8 -1
  142. package/src/renderer/turn-injection.ts +114 -0
  143. package/src/renderer/ui-factory.ts +91 -7
  144. package/src/runtime/bootstrap-command-context.ts +24 -1
  145. package/src/runtime/bootstrap-command-parts.ts +36 -7
  146. package/src/runtime/bootstrap-core.ts +19 -4
  147. package/src/runtime/bootstrap-hook-bridge.ts +5 -0
  148. package/src/runtime/bootstrap-shell.ts +41 -17
  149. package/src/runtime/bootstrap.ts +11 -17
  150. package/src/runtime/code-index-services.ts +112 -0
  151. package/src/runtime/orchestrator-core-services.ts +49 -0
  152. package/src/runtime/process-lifecycle.ts +3 -1
  153. package/src/runtime/services.ts +91 -43
  154. package/src/runtime/ui-services.ts +8 -0
  155. package/src/runtime/workstream-services.ts +195 -0
  156. package/src/shell/blocking-input.ts +22 -1
  157. package/src/shell/ui-openers.ts +129 -17
  158. package/src/utils/format-duration.ts +8 -18
  159. package/src/utils/splash-lines.ts +1 -1
  160. package/src/version.ts +1 -1
  161. package/src/panels/agent-inspector-panel.ts +0 -786
  162. package/src/panels/approval-panel.ts +0 -252
  163. package/src/panels/automation-control-panel.ts +0 -479
  164. package/src/panels/cockpit-panel.ts +0 -481
  165. package/src/panels/cockpit-read-model.ts +0 -233
  166. package/src/panels/communication-panel.ts +0 -256
  167. package/src/panels/control-plane-panel.ts +0 -470
  168. package/src/panels/debug-panel.ts +0 -609
  169. package/src/panels/docs-panel.ts +0 -375
  170. package/src/panels/eval-panel.ts +0 -627
  171. package/src/panels/file-explorer-panel.ts +0 -664
  172. package/src/panels/file-preview-panel.ts +0 -517
  173. package/src/panels/hooks-panel.ts +0 -401
  174. package/src/panels/incident-review-panel.ts +0 -406
  175. package/src/panels/intelligence-panel.ts +0 -383
  176. package/src/panels/knowledge-graph-panel.ts +0 -506
  177. package/src/panels/marketplace-panel.ts +0 -399
  178. package/src/panels/memory-panel.ts +0 -558
  179. package/src/panels/ops-control-panel.ts +0 -329
  180. package/src/panels/ops-strategy-panel.ts +0 -321
  181. package/src/panels/orchestration-panel.ts +0 -465
  182. package/src/panels/panel-list-panel.ts +0 -557
  183. package/src/panels/plan-dashboard-panel.ts +0 -707
  184. package/src/panels/policy-panel.ts +0 -517
  185. package/src/panels/project-planning-panel.ts +0 -721
  186. package/src/panels/provider-health-panel.ts +0 -678
  187. package/src/panels/provider-health-tracker.ts +0 -306
  188. package/src/panels/provider-health-views.ts +0 -560
  189. package/src/panels/qr-panel.ts +0 -280
  190. package/src/panels/remote-panel.ts +0 -534
  191. package/src/panels/routes-panel.ts +0 -241
  192. package/src/panels/sandbox-panel.ts +0 -456
  193. package/src/panels/security-panel.ts +0 -447
  194. package/src/panels/services-panel.ts +0 -329
  195. package/src/panels/session-browser-panel.ts +0 -487
  196. package/src/panels/settings-sync-panel.ts +0 -398
  197. package/src/panels/subscription-panel.ts +0 -342
  198. package/src/panels/symbol-outline-panel.ts +0 -619
  199. package/src/panels/system-messages-panel.ts +0 -364
  200. package/src/panels/tasks-panel.ts +0 -608
  201. package/src/panels/thinking-panel.ts +0 -333
  202. package/src/panels/tool-inspector-panel.ts +0 -537
  203. package/src/panels/work-plan-panel.ts +0 -530
  204. package/src/panels/worktree-panel.ts +0 -360
  205. package/src/panels/wrfc-panel.ts +0 -790
  206. package/src/renderer/agent-detail-modal.ts +0 -465
  207. package/src/renderer/live-tail-modal.ts +0 -156
  208. package/src/renderer/process-modal.ts +0 -656
  209. 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
- }