@pellux/goodvibes-tui 1.1.0 → 1.9.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 (267) hide show
  1. package/CHANGELOG.md +85 -21
  2. package/README.md +28 -18
  3. package/docs/foundation-artifacts/operator-contract.json +4045 -1763
  4. package/package.json +2 -2
  5. package/src/audio/spoken-turn-controller.ts +12 -2
  6. package/src/audio/spoken-turn-wiring.ts +2 -1
  7. package/src/cli/help.ts +8 -1
  8. package/src/cli/service-posture.ts +2 -1
  9. package/src/cli/status.ts +2 -1
  10. package/src/cli/surface-command.ts +2 -2
  11. package/src/core/alert-gating.ts +67 -0
  12. package/src/core/approval-alert.ts +72 -0
  13. package/src/core/budget-breach-notifier.ts +106 -0
  14. package/src/core/composer-state.ts +11 -3
  15. package/src/core/conversation-line-cache.ts +27 -3
  16. package/src/core/conversation-rendering.ts +90 -14
  17. package/src/core/conversation.ts +18 -0
  18. package/src/core/focus-tracker.ts +41 -0
  19. package/src/core/long-task-notifier.ts +33 -6
  20. package/src/core/stream-event-wiring.ts +20 -1
  21. package/src/core/system-message-noise.ts +87 -0
  22. package/src/core/system-message-router.ts +104 -51
  23. package/src/core/turn-cancellation.ts +25 -0
  24. package/src/core/turn-event-wiring.ts +72 -3
  25. package/src/daemon/cli.ts +29 -2
  26. package/src/daemon/handlers/register.ts +8 -1
  27. package/src/daemon/service-commands.ts +329 -0
  28. package/src/export/cost-utils.ts +36 -0
  29. package/src/input/autocomplete.ts +27 -1
  30. package/src/input/command-registry.ts +84 -5
  31. package/src/input/commands/checkpoint-runtime.ts +280 -0
  32. package/src/input/commands/codebase-runtime.ts +232 -0
  33. package/src/input/commands/config.ts +43 -3
  34. package/src/input/commands/eval.ts +1 -1
  35. package/src/input/commands/health-runtime.ts +10 -2
  36. package/src/input/commands/image-runtime.ts +112 -0
  37. package/src/input/commands/intelligence-runtime.ts +11 -1
  38. package/src/input/commands/local-auth-runtime.ts +4 -1
  39. package/src/input/commands/local-runtime.ts +9 -3
  40. package/src/input/commands/marketplace-runtime.ts +2 -2
  41. package/src/input/commands/memory.ts +73 -35
  42. package/src/input/commands/operator-panel-runtime.ts +71 -31
  43. package/src/input/commands/planning-runtime.ts +116 -4
  44. package/src/input/commands/platform-sandbox-runtime.ts +1 -6
  45. package/src/input/commands/plugin-runtime.ts +2 -2
  46. package/src/input/commands/policy-dispatch.ts +21 -0
  47. package/src/input/commands/provider-accounts-runtime.ts +1 -1
  48. package/src/input/commands/qrcode-runtime.ts +26 -6
  49. package/src/input/commands/recall-review.ts +35 -0
  50. package/src/input/commands/remote-runtime-setup.ts +5 -3
  51. package/src/input/commands/remote-runtime.ts +3 -3
  52. package/src/input/commands/runtime-services.ts +54 -13
  53. package/src/input/commands/services-runtime.ts +1 -1
  54. package/src/input/commands/session-content.ts +20 -9
  55. package/src/input/commands/session-workflow.ts +16 -1
  56. package/src/input/commands/settings-sync-runtime.ts +16 -4
  57. package/src/input/commands/shell-core.ts +25 -8
  58. package/src/input/commands/skills-runtime.ts +2 -8
  59. package/src/input/commands/subscription-runtime.ts +2 -2
  60. package/src/input/commands/test-runtime.ts +277 -0
  61. package/src/input/commands/websearch-runtime.ts +101 -0
  62. package/src/input/commands/work-plan-runtime.ts +36 -10
  63. package/src/input/commands/workstream-runtime.ts +488 -0
  64. package/src/input/commands.ts +12 -0
  65. package/src/input/config-modal-types.ts +175 -0
  66. package/src/input/config-modal.ts +592 -0
  67. package/src/input/feed-context-factory.ts +10 -8
  68. package/src/input/handler-command-route.ts +37 -20
  69. package/src/input/handler-content-actions.ts +17 -2
  70. package/src/input/handler-feed-routes.ts +74 -114
  71. package/src/input/handler-feed.ts +79 -27
  72. package/src/input/handler-interactions.ts +1 -3
  73. package/src/input/handler-modal-routes.ts +136 -6
  74. package/src/input/handler-modal-stack.ts +14 -7
  75. package/src/input/handler-modal-token-routes.ts +16 -49
  76. package/src/input/handler-onboarding-daemon-adopt.ts +149 -0
  77. package/src/input/handler-onboarding.ts +71 -59
  78. package/src/input/handler-picker-routes.ts +26 -102
  79. package/src/input/handler-shortcuts.ts +74 -14
  80. package/src/input/handler-types.ts +2 -6
  81. package/src/input/handler-ui-state.ts +10 -22
  82. package/src/input/handler.ts +12 -29
  83. package/src/input/keybindings.ts +23 -8
  84. package/src/input/model-picker-types.ts +24 -6
  85. package/src/input/model-picker.ts +77 -2
  86. package/src/input/onboarding/onboarding-runtime-status.ts +10 -1
  87. package/src/input/onboarding/onboarding-wizard-apply.ts +8 -1
  88. package/src/input/onboarding/onboarding-wizard-constants.ts +4 -1
  89. package/src/input/onboarding/onboarding-wizard-network-adopt.ts +136 -0
  90. package/src/input/onboarding/onboarding-wizard-steps.ts +9 -6
  91. package/src/input/onboarding/onboarding-wizard-types.ts +3 -1
  92. package/src/input/panel-integration-actions.ts +9 -170
  93. package/src/input/panel-mouse-geometry.ts +97 -0
  94. package/src/input/panel-paste-flood-guard.ts +86 -0
  95. package/src/input/selection-modal.ts +11 -0
  96. package/src/input/session-picker-modal.ts +44 -4
  97. package/src/input/settings-modal-data.ts +146 -2
  98. package/src/input/settings-modal-mutations.ts +6 -4
  99. package/src/input/settings-modal-types.ts +4 -2
  100. package/src/main.ts +49 -51
  101. package/src/panels/agent-inspector-shared.ts +2 -1
  102. package/src/panels/base-panel.ts +22 -1
  103. package/src/panels/builtin/agent.ts +21 -107
  104. package/src/panels/builtin/development.ts +15 -61
  105. package/src/panels/builtin/knowledge.ts +8 -32
  106. package/src/panels/builtin/operations.ts +84 -428
  107. package/src/panels/builtin/session.ts +21 -112
  108. package/src/panels/builtin/shared.ts +18 -46
  109. package/src/panels/builtin-modals.ts +218 -0
  110. package/src/panels/builtin-panels.ts +5 -0
  111. package/src/panels/cost-tracker-panel.ts +36 -10
  112. package/src/panels/diff-panel.ts +12 -43
  113. package/src/panels/eval-registry.ts +60 -0
  114. package/src/panels/fleet-deep-link.ts +31 -0
  115. package/src/panels/fleet-panel-format.ts +62 -0
  116. package/src/panels/fleet-panel-worktree-detail.ts +48 -0
  117. package/src/panels/fleet-panel.ts +758 -0
  118. package/src/panels/fleet-read-model.ts +520 -0
  119. package/src/panels/fleet-steer.ts +125 -0
  120. package/src/panels/fleet-stop.ts +153 -0
  121. package/src/panels/fleet-tabs.ts +230 -0
  122. package/src/panels/fleet-transcript.ts +356 -0
  123. package/src/panels/index.ts +7 -31
  124. package/src/panels/modals/hooks-modal.ts +187 -0
  125. package/src/panels/modals/keybindings-modal.ts +166 -0
  126. package/src/panels/modals/knowledge-modal.ts +158 -0
  127. package/src/panels/modals/local-auth-modal.ts +132 -0
  128. package/src/panels/modals/marketplace-modal.ts +218 -0
  129. package/src/panels/modals/memory-modal.ts +180 -0
  130. package/src/panels/modals/modal-surface-helpers.ts +54 -0
  131. package/src/panels/modals/modal-theme.ts +42 -0
  132. package/src/panels/modals/pairing-modal.ts +163 -0
  133. package/src/panels/modals/planning-modal.ts +304 -0
  134. package/src/panels/modals/plugins-modal.ts +174 -0
  135. package/src/panels/modals/policy-modal.ts +256 -0
  136. package/src/panels/modals/provider-health-modal.ts +136 -0
  137. package/src/panels/modals/remote-modal.ts +115 -0
  138. package/src/panels/modals/sandbox-modal.ts +150 -0
  139. package/src/panels/modals/security-modal.ts +217 -0
  140. package/src/panels/modals/services-modal.ts +179 -0
  141. package/src/panels/modals/settings-sync-modal.ts +142 -0
  142. package/src/panels/modals/skills-modal.ts +189 -0
  143. package/src/panels/modals/subscription-modal.ts +172 -0
  144. package/src/panels/modals/work-plan-modal.ts +177 -0
  145. package/src/panels/panel-confirm-overlay.ts +72 -0
  146. package/src/panels/panel-manager.ts +123 -9
  147. package/src/panels/polish-core.ts +38 -25
  148. package/src/panels/project-planning-answer-actions.ts +8 -11
  149. package/src/panels/skills-panel.ts +7 -51
  150. package/src/panels/types.ts +37 -0
  151. package/src/permissions/hunk-selection.ts +179 -0
  152. package/src/permissions/prompt.ts +216 -13
  153. package/src/renderer/autocomplete-overlay.ts +28 -2
  154. package/src/renderer/compaction-history-modal.ts +19 -3
  155. package/src/renderer/compaction-preview.ts +13 -2
  156. package/src/renderer/compaction-quality.ts +100 -0
  157. package/src/renderer/compositor.ts +2 -3
  158. package/src/renderer/config-modal.ts +95 -0
  159. package/src/renderer/conversation-overlays.ts +10 -17
  160. package/src/renderer/fleet-tab-strip.ts +56 -0
  161. package/src/renderer/footer-tips.ts +15 -3
  162. package/src/renderer/fullscreen-primitives.ts +32 -22
  163. package/src/renderer/git-status.ts +43 -1
  164. package/src/renderer/help-overlay.ts +2 -2
  165. package/src/renderer/layout.ts +0 -4
  166. package/src/renderer/markdown.ts +7 -3
  167. package/src/renderer/modal-factory.ts +25 -20
  168. package/src/renderer/model-workspace.ts +62 -4
  169. package/src/renderer/overlay-box.ts +21 -17
  170. package/src/renderer/panel-workspace-bar.ts +8 -2
  171. package/src/renderer/process-indicator.ts +14 -3
  172. package/src/renderer/selection-modal-overlay.ts +6 -1
  173. package/src/renderer/session-picker-modal.ts +196 -3
  174. package/src/renderer/settings-modal-helpers.ts +2 -0
  175. package/src/renderer/settings-modal.ts +7 -0
  176. package/src/renderer/shell-surface.ts +8 -1
  177. package/src/renderer/status-glyphs.ts +14 -15
  178. package/src/renderer/system-message.ts +15 -3
  179. package/src/renderer/terminal-bg-probe.ts +339 -0
  180. package/src/renderer/terminal-escapes.ts +20 -0
  181. package/src/renderer/theme-mode-config.ts +67 -0
  182. package/src/renderer/theme.ts +91 -1
  183. package/src/renderer/thinking.ts +11 -3
  184. package/src/renderer/tool-call.ts +15 -9
  185. package/src/renderer/tool-result-summary.ts +148 -0
  186. package/src/renderer/turn-injection.ts +133 -0
  187. package/src/renderer/ui-factory.ts +154 -85
  188. package/src/renderer/ui-primitives.ts +30 -129
  189. package/src/runtime/bootstrap-command-context.ts +27 -1
  190. package/src/runtime/bootstrap-command-parts.ts +41 -10
  191. package/src/runtime/bootstrap-core.ts +47 -15
  192. package/src/runtime/bootstrap-hook-bridge.ts +7 -0
  193. package/src/runtime/bootstrap-shell.ts +48 -17
  194. package/src/runtime/bootstrap.ts +129 -22
  195. package/src/runtime/code-index-services.ts +135 -0
  196. package/src/runtime/legacy-daemon-migration.ts +516 -0
  197. package/src/runtime/memory-fold.ts +26 -0
  198. package/src/runtime/onboarding/derivation.ts +7 -2
  199. package/src/runtime/onboarding/snapshot.ts +27 -1
  200. package/src/runtime/onboarding/types.ts +31 -1
  201. package/src/runtime/operator-token-cleanup.ts +82 -1
  202. package/src/runtime/orchestrator-core-services.ts +59 -0
  203. package/src/runtime/process-lifecycle.ts +3 -1
  204. package/src/runtime/resume-notice.ts +209 -0
  205. package/src/runtime/services.ts +98 -46
  206. package/src/runtime/session-inbound-inputs.ts +252 -0
  207. package/src/runtime/session-spine-transport.ts +64 -0
  208. package/src/runtime/terminal-output-guard.ts +15 -8
  209. package/src/runtime/ui-services.ts +27 -3
  210. package/src/runtime/workstream-services.ts +327 -0
  211. package/src/runtime/wrfc-persistence.ts +124 -17
  212. package/src/shell/blocking-input.ts +68 -4
  213. package/src/shell/recovery-input-helpers.ts +170 -1
  214. package/src/shell/ui-openers.ts +171 -26
  215. package/src/utils/format-duration.ts +8 -18
  216. package/src/utils/splash-lines.ts +1 -1
  217. package/src/utils/terminal-width.ts +52 -0
  218. package/src/version.ts +1 -1
  219. package/src/panels/agent-inspector-panel.ts +0 -786
  220. package/src/panels/approval-panel.ts +0 -252
  221. package/src/panels/automation-control-panel.ts +0 -479
  222. package/src/panels/cockpit-panel.ts +0 -481
  223. package/src/panels/cockpit-read-model.ts +0 -238
  224. package/src/panels/communication-panel.ts +0 -256
  225. package/src/panels/control-plane-panel.ts +0 -470
  226. package/src/panels/debug-panel.ts +0 -615
  227. package/src/panels/docs-panel.ts +0 -384
  228. package/src/panels/eval-panel.ts +0 -627
  229. package/src/panels/file-explorer-panel.ts +0 -673
  230. package/src/panels/file-preview-panel.ts +0 -517
  231. package/src/panels/hooks-panel.ts +0 -401
  232. package/src/panels/incident-review-panel.ts +0 -406
  233. package/src/panels/intelligence-panel.ts +0 -383
  234. package/src/panels/knowledge-graph-panel.ts +0 -515
  235. package/src/panels/marketplace-panel.ts +0 -399
  236. package/src/panels/memory-panel.ts +0 -558
  237. package/src/panels/ops-control-panel.ts +0 -329
  238. package/src/panels/ops-strategy-panel.ts +0 -321
  239. package/src/panels/orchestration-panel.ts +0 -465
  240. package/src/panels/panel-list-panel.ts +0 -566
  241. package/src/panels/plan-dashboard-panel.ts +0 -707
  242. package/src/panels/policy-panel.ts +0 -517
  243. package/src/panels/project-planning-panel.ts +0 -731
  244. package/src/panels/provider-health-panel.ts +0 -678
  245. package/src/panels/provider-health-tracker.ts +0 -310
  246. package/src/panels/provider-health-views.ts +0 -567
  247. package/src/panels/qr-panel.ts +0 -280
  248. package/src/panels/remote-panel.ts +0 -534
  249. package/src/panels/routes-panel.ts +0 -241
  250. package/src/panels/sandbox-panel.ts +0 -456
  251. package/src/panels/security-panel.ts +0 -447
  252. package/src/panels/services-panel.ts +0 -329
  253. package/src/panels/session-browser-panel.ts +0 -496
  254. package/src/panels/settings-sync-panel.ts +0 -398
  255. package/src/panels/subscription-panel.ts +0 -342
  256. package/src/panels/symbol-outline-panel.ts +0 -619
  257. package/src/panels/system-messages-panel.ts +0 -364
  258. package/src/panels/tasks-panel.ts +0 -608
  259. package/src/panels/thinking-panel.ts +0 -333
  260. package/src/panels/tool-inspector-panel.ts +0 -537
  261. package/src/panels/work-plan-panel.ts +0 -540
  262. package/src/panels/worktree-panel.ts +0 -360
  263. package/src/panels/wrfc-panel.ts +0 -790
  264. package/src/renderer/agent-detail-modal.ts +0 -466
  265. package/src/renderer/live-tail-modal.ts +0 -156
  266. package/src/renderer/process-modal.ts +0 -671
  267. 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
- }