@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,252 +0,0 @@
1
- import type { Line } from '../types/grid.ts';
2
- import { ScrollableListPanel } from './scrollable-list-panel.ts';
3
- import type { PanelIntegrationContext } from './types.ts';
4
- import {
5
- buildBodyText,
6
- buildDetailBlock,
7
- buildGuidanceLine,
8
- buildKeyValueLine,
9
- buildKeyboardHints,
10
- buildPanelLine,
11
- buildStatusBadge,
12
- DEFAULT_PANEL_PALETTE,
13
- type StatusBadgeKind,
14
- } from './polish.ts';
15
- import { truncateDisplay } from '../utils/terminal-width.ts';
16
- import type { PolicyRuntimeState, PermissionAuditEntry } from '@/runtime/index.ts';
17
- import { buildPermissionRuleSuggestions } from '@/runtime/index.ts';
18
-
19
- // Splits a computed '/command arg1 arg2' string into the { name, args } shape
20
- // ctx.executeCommand expects — same leading-slash-stripping approach used by
21
- // remote-panel.ts / session-browser-panel.ts for their own dispatched commands.
22
- function parseCommand(command: string): { name: string; args: string[] } | null {
23
- const parts = command.replace(/^\//, '').split(/\s+/).filter(Boolean);
24
- const [name, ...args] = parts;
25
- return name ? { name, args } : null;
26
- }
27
-
28
- // Base chrome only — title band comes straight from DEFAULT_PANEL_PALETTE
29
- // (WO-002).
30
- const C = DEFAULT_PANEL_PALETTE;
31
-
32
- // Reference catalog of approval lanes and where each one is reviewed. Used to
33
- // resolve the next-step command for a live request and as a fallback reference
34
- // when no live requests are present.
35
- const LANE_REVIEW: Record<string, { command: string; why: string }> = {
36
- shell: { command: '/security', why: 'side effects, destructive ops, secret exposure, escalation' },
37
- file: { command: '/approval review file', why: 'config mutation, notebook edits, secret-bearing paths' },
38
- network: { command: '/approval review network', why: 'external hosts, fetch scope, egress policy' },
39
- delegate: { command: '/approval review delegate', why: 'recursive agents, spawn ceilings, write-set inheritance' },
40
- mcp: { command: '/mcp trust', why: 'trust escalation, host scope, path scope, coherence mismatch' },
41
- remote: { command: '/remote', why: 'runner trust, remote write scope, artifact requirements' },
42
- hook: { command: '/hooks', why: 'deny/mutate authority, blocking behavior, runner provenance' },
43
- plugin: { command: '/marketplace', why: 'install/update lifecycle, provenance, capability grants' },
44
- sandbox: { command: '/sandbox review', why: 'WSL/VM isolation changes alter host risk posture' },
45
- };
46
-
47
- function laneOf(entry: PermissionAuditEntry): string {
48
- const key = (entry.category || entry.tool || '').toLowerCase();
49
- for (const lane of Object.keys(LANE_REVIEW)) {
50
- if (key.includes(lane)) return lane;
51
- }
52
- return key || 'shell';
53
- }
54
-
55
- function reviewFor(entry: PermissionAuditEntry): { command: string; why: string } {
56
- return LANE_REVIEW[laneOf(entry)] ?? { command: '/security', why: 'review approval posture' };
57
- }
58
-
59
- function decisionOf(entry: PermissionAuditEntry): 'pending' | 'approved' | 'denied' {
60
- return entry.approved === undefined ? 'pending' : entry.approved ? 'approved' : 'denied';
61
- }
62
-
63
- function badgeKind(decision: 'pending' | 'approved' | 'denied'): StatusBadgeKind {
64
- return decision === 'pending' ? 'pending' : decision === 'approved' ? 'completed' : 'failed';
65
- }
66
-
67
- function riskColor(risk: string): string {
68
- const r = risk.toLowerCase();
69
- if (r.includes('critical') || r.includes('high')) return C.bad;
70
- if (r.includes('medium') || r.includes('moderate')) return C.warn;
71
- return C.good;
72
- }
73
-
74
- function fmtAgo(ts: number): string {
75
- const sec = Math.max(0, Math.floor((Date.now() - ts) / 1000));
76
- if (sec < 60) return `${sec}s ago`;
77
- if (sec < 3600) return `${Math.floor(sec / 60)}m ago`;
78
- return `${Math.floor(sec / 3600)}h ago`;
79
- }
80
-
81
- export class ApprovalPanel extends ScrollableListPanel<PermissionAuditEntry> {
82
- private readonly policyRuntimeState: Pick<PolicyRuntimeState, 'getSnapshot'>;
83
- // p / 1-3 = dispatch '/policy simulate' or a rule-suggestion command via the
84
- // handleInput -> handlePanelIntegrationAction bridge (handleInput has no
85
- // ctx.executeCommand — same pattern as remote-panel.ts / session-browser-panel.ts).
86
- private pendingCommand: { name: string; args: string[] } | null = null;
87
-
88
- public constructor(policyRuntimeState: Pick<PolicyRuntimeState, 'getSnapshot'>) {
89
- super('approval', 'Approval', 'A', 'security-policy');
90
- this.showSelectionGutter = true; // I5: non-color selection affordance
91
- this.policyRuntimeState = policyRuntimeState;
92
- }
93
-
94
- private getRuleSuggestions() {
95
- const audit = this.policyRuntimeState.getSnapshot().recentPermissionAudit;
96
- return buildPermissionRuleSuggestions(audit).slice(0, 3);
97
- }
98
-
99
- override handleInput(key: string): boolean {
100
- if (key === 'p') {
101
- this.pendingCommand = parseCommand('/policy simulate');
102
- return true;
103
- }
104
- if (key === '1' || key === '2' || key === '3') {
105
- const suggestion = this.getRuleSuggestions()[Number(key) - 1];
106
- if (!suggestion) return false;
107
- this.pendingCommand = parseCommand(suggestion.command);
108
- return true;
109
- }
110
- return super.handleInput(key);
111
- }
112
-
113
- /** Drains `pendingCommand` (set by 'p' or '1'/'2'/'3') via the ctx.executeCommand bridge. */
114
- handlePanelIntegrationAction(_key: string, ctx: PanelIntegrationContext): boolean {
115
- if (!this.pendingCommand) return false;
116
- const { name, args } = this.pendingCommand;
117
- this.pendingCommand = null;
118
- void ctx.executeCommand?.(name, args);
119
- return true;
120
- }
121
-
122
- protected override getPalette() { return C; }
123
- protected override getEmptyStateMessage() { return ' No approval pressure right now.'; }
124
- protected override getEmptyStateActions() {
125
- return [
126
- { command: '/security', summary: 'inspect trust, tokens, quarantines, and incident pressure' },
127
- { command: '/policy simulate', summary: 'preview which requests a rule change would auto-approve' },
128
- ];
129
- }
130
-
131
- // Pending requests (those still needing a decision) are surfaced first so the
132
- // operator sees actionable items at the top, then most-recent decisions.
133
- protected getItems(): readonly PermissionAuditEntry[] {
134
- const audit = this.policyRuntimeState.getSnapshot().recentPermissionAudit;
135
- const pending = audit.filter((e) => e.approved === undefined);
136
- const decided = audit.filter((e) => e.approved !== undefined);
137
- return [...pending, ...decided];
138
- }
139
-
140
- protected renderItem(entry: PermissionAuditEntry, _index: number, selected: boolean, width: number): Line {
141
- const bg = selected ? C.selectBg : undefined;
142
- const decision = decisionOf(entry);
143
- const badge = buildStatusBadge(badgeKind(decision), decision)[0]!;
144
- const detailWidth = Math.max(0, width - 40);
145
- return buildPanelLine(width, [
146
- [' ', C.label, bg],
147
- [`${badge.text} `.padEnd(12), badge.fg, bg],
148
- [`${truncateDisplay(entry.tool, 12)} `.padEnd(13), C.value, bg],
149
- [`${truncateDisplay(entry.riskLevel, 6)} `.padEnd(7), riskColor(entry.riskLevel), bg],
150
- [truncateDisplay(entry.summary, detailWidth), C.dim, bg],
151
- ]);
152
- }
153
-
154
- /** Next-step review command for the currently selected request (if any). */
155
- public getSelectedCommand(): string | null {
156
- const selected = this.getSelectedItem();
157
- return selected ? reviewFor(selected).command : null;
158
- }
159
-
160
- public render(width: number, height: number): Line[] {
161
- this.clampSelection();
162
- const audit = this.policyRuntimeState.getSnapshot().recentPermissionAudit;
163
- const approvalCount = audit.filter((e) => e.approved === true).length;
164
- const denialCount = audit.filter((e) => e.approved === false).length;
165
- const pendingCount = audit.filter((e) => e.approved === undefined).length;
166
-
167
- const selected = this.getSelectedItem() ?? null;
168
-
169
- // ---- Posture summary (severity + counts first) ----
170
- const headerLines: Line[] = [
171
- buildPanelLine(width, [
172
- [' ', C.label],
173
- ...buildStatusBadge('pending', 'pending', { count: pendingCount }),
174
- [' ', C.dim],
175
- ...buildStatusBadge('completed', 'approved', { count: approvalCount }),
176
- [' ', C.dim],
177
- ...buildStatusBadge('failed', 'denied', { count: denialCount }),
178
- ]),
179
- pendingCount > 0
180
- ? buildPanelLine(width, [[` ${pendingCount} request${pendingCount !== 1 ? 's' : ''} awaiting a decision — select one to see its review path.`, C.warn]])
181
- : buildPanelLine(width, [
182
- [' p ', C.info],
183
- ['/policy simulate — preview which requests a scoped rule change would auto-approve', C.dim],
184
- ]),
185
- ];
186
-
187
- // ---- Detail block for the selected request ----
188
- const detailLines: Line[] = [];
189
- if (selected) {
190
- const review = reviewFor(selected);
191
- const decision = decisionOf(selected);
192
- detailLines.push(...buildDetailBlock(width, `Request · ${selected.tool}`, [
193
- buildKeyValueLine(width, [
194
- { label: 'decision', value: decision, valueColor: decision === 'pending' ? C.info : decision === 'approved' ? C.good : C.bad },
195
- { label: 'risk', value: selected.riskLevel, valueColor: riskColor(selected.riskLevel) },
196
- { label: 'lane', value: laneOf(selected), valueColor: C.info },
197
- ], C),
198
- buildKeyValueLine(width, [
199
- { label: 'requested', value: fmtAgo(selected.requestedAt), valueColor: C.dim },
200
- ...(selected.decidedAt ? [{ label: 'decided', value: fmtAgo(selected.decidedAt), valueColor: C.dim }] : []),
201
- ...(selected.target ? [{ label: 'target', value: truncateDisplay(selected.target, 24), valueColor: C.value }] : []),
202
- ], C),
203
- ...buildBodyText(width, selected.summary, C, C.value),
204
- ...(selected.reasons[0]
205
- ? buildBodyText(width, `why prompted: ${selected.reasons[0]}`, C, C.dim)
206
- : buildBodyText(width, `why prompted: ${review.why}`, C, C.dim)),
207
- // WO-160: dropped the printed `review.command` guidance line here —
208
- // Enter is already wired by the shell to dispatch that exact command
209
- // (see onSelect below and the "Enter: review (<command>)" footer
210
- // hint), so restating it as a signpost was a pure action substitute.
211
- ], C));
212
- }
213
-
214
- // ---- Durable-rule suggestions from repeated denials (dispatchable via 1/2/3) ----
215
- const suggestions = this.getRuleSuggestions();
216
- const ruleSuggestionLines: Line[] = [];
217
- suggestions.forEach((suggestion, index) => {
218
- ruleSuggestionLines.push(buildPanelLine(width, [
219
- [` ${index + 1}) `, C.info],
220
- [truncateDisplay(suggestion.summary, Math.max(0, width - 8)), C.info],
221
- ]));
222
- ruleSuggestionLines.push(buildGuidanceLine(width, suggestion.command, suggestion.reason, C));
223
- });
224
- if (ruleSuggestionLines.length > 0) {
225
- ruleSuggestionLines.unshift(buildPanelLine(width, [[' Suggested durable rules (press 1/2/3 to apply)', C.label]]));
226
- }
227
-
228
- // ---- Context-aware footer: only show review key when a request is selected ----
229
- const hints = selected
230
- ? [
231
- { keys: '↑/↓', label: 'select' },
232
- { keys: 'Enter', label: `review (${reviewFor(selected).command})` },
233
- { keys: 'g/G', label: 'top/bottom' },
234
- ]
235
- : [
236
- { keys: '↑/↓', label: 'select' },
237
- { keys: 'g/G', label: 'top/bottom' },
238
- ];
239
- if (suggestions.length > 0) hints.push({ keys: '1-3', label: 'apply rule suggestion' });
240
- if (pendingCount === 0) hints.push({ keys: 'p', label: 'policy simulate' });
241
-
242
- return this.renderList(width, height, {
243
- title: 'Approval Control Room',
244
- header: headerLines,
245
- footer: [...detailLines, ...ruleSuggestionLines, buildKeyboardHints(width, hints, C)],
246
- });
247
- }
248
-
249
- protected override onSelect(item: PermissionAuditEntry): void {
250
- void item; // Enter is wired by the shell to open getSelectedCommand(); selection model lives here.
251
- }
252
- }