@pellux/goodvibes-tui 0.27.0 → 0.29.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 (143) hide show
  1. package/CHANGELOG.md +124 -0
  2. package/README.md +1 -1
  3. package/package.json +1 -1
  4. package/src/core/context-usage.ts +53 -0
  5. package/src/core/conversation.ts +4 -6
  6. package/src/core/session-recovery.ts +2 -0
  7. package/src/core/turn-event-wiring.ts +1 -0
  8. package/src/input/commands/cost-runtime.ts +49 -0
  9. package/src/input/commands/eval.ts +4 -3
  10. package/src/input/commands/operator-runtime.ts +5 -1
  11. package/src/input/commands.ts +2 -0
  12. package/src/input/handler-feed-routes.ts +60 -26
  13. package/src/input/handler-feed.ts +17 -8
  14. package/src/input/handler-picker-routes.ts +18 -13
  15. package/src/input/handler-shortcuts.ts +51 -0
  16. package/src/input/handler-ui-state.ts +4 -0
  17. package/src/input/handler.ts +43 -6
  18. package/src/input/keybindings.ts +53 -8
  19. package/src/input/model-picker.ts +5 -0
  20. package/src/input/panel-integration-actions.ts +117 -1
  21. package/src/main.ts +5 -2
  22. package/src/panels/agent-inspector-panel.ts +139 -45
  23. package/src/panels/agent-inspector-shared.ts +147 -3
  24. package/src/panels/approval-panel.ts +204 -102
  25. package/src/panels/automation-control-panel.ts +370 -103
  26. package/src/panels/base-panel.ts +26 -2
  27. package/src/panels/builtin/agent.ts +28 -37
  28. package/src/panels/builtin/development.ts +40 -32
  29. package/src/panels/builtin/knowledge.ts +15 -6
  30. package/src/panels/builtin/operations.ts +178 -117
  31. package/src/panels/builtin/session.ts +35 -9
  32. package/src/panels/builtin/shared.ts +98 -6
  33. package/src/panels/cockpit-panel.ts +253 -75
  34. package/src/panels/cockpit-read-model.ts +2 -1
  35. package/src/panels/communication-panel.ts +159 -56
  36. package/src/panels/confirm-state.ts +8 -1
  37. package/src/panels/control-plane-panel.ts +354 -95
  38. package/src/panels/cost-tracker-panel.ts +243 -57
  39. package/src/panels/debug-panel.ts +347 -159
  40. package/src/panels/diff-panel.ts +257 -82
  41. package/src/panels/docs-panel.ts +166 -64
  42. package/src/panels/eval-panel.ts +336 -109
  43. package/src/panels/expandable-list-panel.ts +189 -0
  44. package/src/panels/file-explorer-panel.ts +238 -159
  45. package/src/panels/file-preview-panel.ts +141 -59
  46. package/src/panels/git-panel.ts +360 -191
  47. package/src/panels/hooks-panel.ts +181 -19
  48. package/src/panels/incident-review-panel.ts +252 -43
  49. package/src/panels/index.ts +0 -4
  50. package/src/panels/intelligence-panel.ts +310 -103
  51. package/src/panels/knowledge-graph-panel.ts +480 -58
  52. package/src/panels/local-auth-panel.ts +251 -33
  53. package/src/panels/marketplace-panel.ts +219 -33
  54. package/src/panels/memory-panel.ts +83 -81
  55. package/src/panels/ops-control-panel.ts +211 -32
  56. package/src/panels/ops-strategy-panel.ts +131 -45
  57. package/src/panels/orchestration-panel.ts +259 -67
  58. package/src/panels/panel-list-panel.ts +184 -136
  59. package/src/panels/panel-manager.ts +120 -13
  60. package/src/panels/plan-dashboard-panel.ts +507 -74
  61. package/src/panels/plugins-panel.ts +227 -34
  62. package/src/panels/policy-panel.ts +264 -55
  63. package/src/panels/polish-core.ts +162 -0
  64. package/src/panels/polish-tables.ts +258 -0
  65. package/src/panels/polish.ts +67 -149
  66. package/src/panels/project-planning-answer-actions.ts +134 -0
  67. package/src/panels/project-planning-panel.ts +84 -113
  68. package/src/panels/provider-health-panel.ts +438 -480
  69. package/src/panels/provider-health-routes.ts +203 -0
  70. package/src/panels/provider-health-tracker.ts +194 -6
  71. package/src/panels/provider-health-views.ts +560 -0
  72. package/src/panels/qr-panel.ts +136 -38
  73. package/src/panels/remote-panel.ts +123 -38
  74. package/src/panels/routes-panel.ts +87 -24
  75. package/src/panels/sandbox-panel.ts +232 -65
  76. package/src/panels/scrollable-list-panel.ts +143 -145
  77. package/src/panels/security-panel.ts +204 -52
  78. package/src/panels/services-panel.ts +174 -76
  79. package/src/panels/session-browser-panel.ts +101 -6
  80. package/src/panels/session-maintenance.ts +4 -122
  81. package/src/panels/settings-sync-panel.ts +346 -68
  82. package/src/panels/skills-panel.ts +157 -89
  83. package/src/panels/subscription-panel.ts +105 -34
  84. package/src/panels/symbol-outline-panel.ts +287 -154
  85. package/src/panels/system-messages-panel.ts +172 -38
  86. package/src/panels/tasks-panel.ts +348 -134
  87. package/src/panels/thinking-panel.ts +66 -32
  88. package/src/panels/token-budget-panel.ts +233 -46
  89. package/src/panels/tool-inspector-panel.ts +170 -54
  90. package/src/panels/types.ts +65 -4
  91. package/src/panels/work-plan-panel.ts +393 -39
  92. package/src/panels/worktree-panel.ts +239 -61
  93. package/src/panels/wrfc-panel-format.ts +133 -0
  94. package/src/panels/wrfc-panel.ts +146 -150
  95. package/src/renderer/compaction-preview.ts +2 -1
  96. package/src/renderer/compositor.ts +46 -43
  97. package/src/renderer/conversation-overlays.ts +25 -11
  98. package/src/renderer/footer-tips.ts +41 -0
  99. package/src/renderer/fullscreen-primitives.ts +22 -16
  100. package/src/renderer/help-overlay.ts +91 -14
  101. package/src/renderer/hint-grammar.ts +52 -0
  102. package/src/renderer/layout.ts +7 -7
  103. package/src/renderer/modal-factory.ts +23 -15
  104. package/src/renderer/model-picker-overlay.ts +30 -11
  105. package/src/renderer/model-workspace.ts +2 -3
  106. package/src/renderer/overlay-box.ts +16 -10
  107. package/src/renderer/panel-composite.ts +7 -20
  108. package/src/renderer/panel-workspace-bar.ts +14 -8
  109. package/src/renderer/process-indicator.ts +3 -1
  110. package/src/renderer/progress.ts +8 -6
  111. package/src/renderer/search-overlay.ts +27 -6
  112. package/src/renderer/session-picker-modal.ts +6 -4
  113. package/src/renderer/settings-modal.ts +70 -10
  114. package/src/renderer/shell-surface.ts +41 -15
  115. package/src/renderer/status-glyphs.ts +11 -9
  116. package/src/renderer/tab-strip.ts +148 -34
  117. package/src/renderer/theme.ts +60 -3
  118. package/src/renderer/ui-factory.ts +45 -36
  119. package/src/renderer/ui-primitives.ts +23 -2
  120. package/src/runtime/bootstrap-shell.ts +35 -18
  121. package/src/runtime/diagnostics/panels/index.ts +0 -3
  122. package/src/runtime/ui/model-picker/health-enrichment.ts +3 -0
  123. package/src/runtime/ui/model-picker/types.ts +4 -0
  124. package/src/shell/ui-openers.ts +14 -22
  125. package/src/utils/format-duration.ts +55 -0
  126. package/src/utils/format-number.ts +71 -0
  127. package/src/utils/splash-lines.ts +44 -3
  128. package/src/version.ts +1 -1
  129. package/src/work-plans/work-plan-store.ts +13 -0
  130. package/src/panels/agent-logs-panel.ts +0 -666
  131. package/src/panels/agent-logs-shared.ts +0 -129
  132. package/src/panels/context-visualizer-panel.ts +0 -214
  133. package/src/panels/forensics-panel.ts +0 -364
  134. package/src/panels/panel-picker.ts +0 -106
  135. package/src/panels/provider-account-snapshot.ts +0 -259
  136. package/src/panels/provider-accounts-panel.ts +0 -218
  137. package/src/panels/provider-stats-panel.ts +0 -366
  138. package/src/panels/schedule-panel.ts +0 -342
  139. package/src/panels/watchers-panel.ts +0 -193
  140. package/src/renderer/panel-picker-overlay.ts +0 -202
  141. package/src/renderer/panel-tab-bar.ts +0 -69
  142. package/src/runtime/diagnostics/panels/panel-resources.ts +0 -118
  143. package/src/runtime/diagnostics/panels/policy.ts +0 -177
@@ -1,150 +1,252 @@
1
1
  import type { Line } from '../types/grid.ts';
2
2
  import { ScrollableListPanel } from './scrollable-list-panel.ts';
3
+ import type { PanelIntegrationContext } from './types.ts';
3
4
  import {
5
+ buildBodyText,
6
+ buildDetailBlock,
4
7
  buildGuidanceLine,
5
8
  buildKeyValueLine,
9
+ buildKeyboardHints,
6
10
  buildPanelLine,
11
+ buildStatusBadge,
7
12
  DEFAULT_PANEL_PALETTE,
13
+ type StatusBadgeKind,
8
14
  } from './polish.ts';
9
- import type { PolicyRuntimeState } from '@/runtime/index.ts';
15
+ import { truncateDisplay } from '../utils/terminal-width.ts';
16
+ import type { PolicyRuntimeState, PermissionAuditEntry } from '@/runtime/index.ts';
10
17
  import { buildPermissionRuleSuggestions } from '@/runtime/index.ts';
11
18
 
12
- const C = {
13
- ...DEFAULT_PANEL_PALETTE,
14
- headerBg: '#111827',
15
- } as const;
16
-
17
- const APPROVAL_ROWS = [
18
- ['shell', 'why prompted: side effects, destructive ops, secret exposure, escalation', 'review via /security and /policy preflight'],
19
- ['file', 'why prompted: config mutation, notebook edits, secret-bearing paths', 'review via /approval review file'],
20
- ['network', 'why prompted: external hosts, fetch scope, egress policy', 'review via /approval review network'],
21
- ['delegate', 'why prompted: recursive agents, spawn ceilings, write-set inheritance', 'review via /approval review delegate'],
22
- ['mcp', 'why prompted: trust escalation, host scope, path scope, coherence mismatch', 'review via /mcp trust and /security'],
23
- ['remote', 'why prompted: runner trust, remote write scope, artifact requirements', 'review via /remote and /sandbox'],
24
- ['hook', 'why prompted: deny/mutate authority, blocking behavior, runner provenance', 'review via /hooks and /security'],
25
- ['plugin', 'why prompted: install/update lifecycle, provenance, capability grants', 'review via /marketplace and /security'],
26
- ['sandbox', 'why prompted: WSL/VM isolation changes alter host risk posture', 'review via /sandbox preset and /sandbox review'],
27
- ] as const;
28
-
29
- type ApprovalRow = (typeof APPROVAL_ROWS)[number];
30
-
31
- export class ApprovalPanel extends ScrollableListPanel<ApprovalRow> {
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> {
32
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;
33
87
 
34
88
  public constructor(policyRuntimeState: Pick<PolicyRuntimeState, 'getSnapshot'>) {
35
- super('approval', 'Approval', 'A', 'monitoring');
89
+ super('approval', 'Approval', 'A', 'security-policy');
36
90
  this.showSelectionGutter = true; // I5: non-color selection affordance
37
91
  this.policyRuntimeState = policyRuntimeState;
38
92
  }
39
93
 
40
- protected override getPalette() { return C; }
41
- protected override getEmptyStateMessage() { return ' No approval lanes defined.'; }
42
-
43
- protected getItems(): readonly ApprovalRow[] {
44
- return APPROVAL_ROWS;
45
- }
46
-
47
- protected renderItem(row: ApprovalRow, index: number, selected: boolean, width: number): Line {
48
- const bg = selected ? C.selectBg : undefined;
49
- return buildPanelLine(width, [
50
- [' ', C.label],
51
- [row[0].padEnd(10), C.info, bg],
52
- [row[1].slice(0, Math.max(0, width - 18)), C.value, bg],
53
- ]);
94
+ private getRuleSuggestions() {
95
+ const audit = this.policyRuntimeState.getSnapshot().recentPermissionAudit;
96
+ return buildPermissionRuleSuggestions(audit).slice(0, 3);
54
97
  }
55
98
 
56
- public handleInput(key: string): boolean {
57
- if (key === 'home') {
58
- this.selectedIndex = 0;
59
- this.markDirty();
60
- return true;
61
- }
62
- if (key === 'end') {
63
- this.selectedIndex = APPROVAL_ROWS.length - 1;
64
- this.markDirty();
99
+ override handleInput(key: string): boolean {
100
+ if (key === 'p') {
101
+ this.pendingCommand = parseCommand('/policy simulate');
65
102
  return true;
66
103
  }
67
- if (key === 'enter' || key === 'return') {
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);
68
108
  return true;
69
109
  }
70
110
  return super.handleInput(key);
71
111
  }
72
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). */
73
155
  public getSelectedCommand(): string | null {
74
- const selected = APPROVAL_ROWS[this.selectedIndex] ?? null;
75
- return selected ? selected[2].replace('review via ', '').trim() : null;
156
+ const selected = this.getSelectedItem();
157
+ return selected ? reviewFor(selected).command : null;
76
158
  }
77
159
 
78
160
  public render(width: number, height: number): Line[] {
79
161
  this.clampSelection();
80
- const policySnapshot = this.policyRuntimeState.getSnapshot();
81
- const approvalCount = policySnapshot.recentPermissionAudit.filter((e) => e.approved === true).length;
82
- const denialCount = policySnapshot.recentPermissionAudit.filter((e) => e.approved === false).length;
83
- const pendingCount = policySnapshot.recentPermissionAudit.filter((e) => e.approved === undefined).length;
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
+ ];
84
186
 
85
- const selected = APPROVAL_ROWS[this.selectedIndex] ?? null;
187
+ // ---- Detail block for the selected request ----
86
188
  const detailLines: Line[] = [];
87
189
  if (selected) {
88
- detailLines.push(buildPanelLine(width, [[' Selected Lane', C.label]]));
89
- detailLines.push(buildKeyValueLine(width, [
90
- { label: 'lane', value: selected[0], valueColor: C.info },
91
- { label: 'next review', value: selected[2], valueColor: C.dim },
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.
92
211
  ], C));
93
- detailLines.push(buildPanelLine(width, [[` ${selected[1]}`, C.value]]));
94
- detailLines.push(buildGuidanceLine(width, selected[2].replace('review via ', ''), `open the ${selected[0]} review path`, C));
95
- }
96
-
97
- const recentAuditLines: Line[] = [];
98
- for (const entry of policySnapshot.recentPermissionAudit.slice(0, 5)) {
99
- const decision = entry.approved === undefined ? 'pending' : entry.approved ? 'approved' : 'denied';
100
- const decisionColor = entry.approved === undefined ? C.info : entry.approved ? C.good : C.bad;
101
- recentAuditLines.push(buildPanelLine(width, [
102
- [` ${decision.padEnd(8)}`, decisionColor],
103
- [`${entry.tool}`.padEnd(14), C.label],
104
- [entry.summary.slice(0, Math.max(0, width - 28)), C.value],
105
- ]));
106
- if (entry.reasons[0]) {
107
- recentAuditLines.push(buildPanelLine(width, [[` ${entry.reasons[0]}`, C.dim]]));
108
- }
109
- }
110
- if (recentAuditLines.length === 0) {
111
- recentAuditLines.push(buildPanelLine(width, [[` No recent approval pressure. Live requests and decisions will appear here.`, C.dim]]));
112
212
  }
113
213
 
214
+ // ---- Durable-rule suggestions from repeated denials (dispatchable via 1/2/3) ----
215
+ const suggestions = this.getRuleSuggestions();
114
216
  const ruleSuggestionLines: Line[] = [];
115
- for (const suggestion of buildPermissionRuleSuggestions(policySnapshot.recentPermissionAudit).slice(0, 3)) {
116
- ruleSuggestionLines.push(buildPanelLine(width, [[` ${suggestion.summary}`, C.info]]));
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
+ ]));
117
222
  ruleSuggestionLines.push(buildGuidanceLine(width, suggestion.command, suggestion.reason, C));
118
- }
119
- if (ruleSuggestionLines.length === 0) {
120
- ruleSuggestionLines.push(buildPanelLine(width, [[` No repeated denials currently suggest a durable rule.`, C.dim]]));
223
+ });
224
+ if (ruleSuggestionLines.length > 0) {
225
+ ruleSuggestionLines.unshift(buildPanelLine(width, [[' Suggested durable rules (press 1/2/3 to apply)', C.label]]));
121
226
  }
122
227
 
123
- const headerLines: Line[] = [
124
- buildPanelLine(width, [[' Approval posture', C.label]]),
125
- buildKeyValueLine(width, [
126
- { label: 'why prompted', value: 'risk summary', valueColor: C.value },
127
- { label: 'what-if', value: '/policy simulate + preflight', valueColor: C.info },
128
- { label: 'operator', value: '/security + /cockpit', valueColor: C.good },
129
- ], C),
130
- buildPanelLine(width, [
131
- [' \u2713 ', C.good],
132
- [`approvals (${approvalCount}) `, C.good],
133
- ['\u2715 ', C.bad],
134
- [`denials (${denialCount}) `, C.bad],
135
- ['\u25cb ', C.info],
136
- [`pending (${pendingCount})`, C.info],
137
- ]),
138
- buildGuidanceLine(width, '/approval review shell', 'inspect the highest-risk approval lane and refine scoped review posture', C),
139
- ...detailLines,
140
- ...recentAuditLines,
141
- ...ruleSuggestionLines,
142
- ];
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' });
143
241
 
144
242
  return this.renderList(width, height, {
145
243
  title: 'Approval Control Room',
146
244
  header: headerLines,
147
- footer: [buildPanelLine(width, [[` Up/Down move Home/End jump selected lane opens the next command path`, C.dim]])],
245
+ footer: [...detailLines, ...ruleSuggestionLines, buildKeyboardHints(width, hints, C)],
148
246
  });
149
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
+ }
150
252
  }