@pellux/goodvibes-tui 1.1.0 → 1.7.0

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