@pellux/goodvibes-tui 0.28.0 → 1.0.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 (144) hide show
  1. package/CHANGELOG.md +133 -0
  2. package/README.md +1 -1
  3. package/package.json +1 -1
  4. package/src/core/conversation-line-cache.ts +432 -0
  5. package/src/core/conversation-rendering.ts +11 -3
  6. package/src/core/conversation.ts +50 -4
  7. package/src/input/commands/cost-runtime.ts +49 -0
  8. package/src/input/commands/operator-runtime.ts +5 -1
  9. package/src/input/commands.ts +2 -0
  10. package/src/input/handler-content-actions.ts +8 -3
  11. package/src/input/handler-feed-routes.ts +12 -35
  12. package/src/input/handler-feed.ts +4 -8
  13. package/src/input/handler-shortcuts.ts +51 -0
  14. package/src/input/handler.ts +43 -6
  15. package/src/input/input-history.ts +17 -4
  16. package/src/input/keybindings.ts +48 -8
  17. package/src/input/panel-integration-actions.ts +107 -1
  18. package/src/main.ts +19 -11
  19. package/src/panels/agent-inspector-panel.ts +125 -36
  20. package/src/panels/agent-inspector-shared.ts +144 -0
  21. package/src/panels/approval-panel.ts +67 -16
  22. package/src/panels/automation-control-panel.ts +368 -124
  23. package/src/panels/base-panel.ts +1 -1
  24. package/src/panels/builtin/agent.ts +28 -37
  25. package/src/panels/builtin/development.ts +40 -32
  26. package/src/panels/builtin/knowledge.ts +15 -6
  27. package/src/panels/builtin/operations.ts +178 -117
  28. package/src/panels/builtin/session.ts +35 -9
  29. package/src/panels/builtin/shared.ts +98 -6
  30. package/src/panels/cockpit-panel.ts +232 -73
  31. package/src/panels/communication-panel.ts +58 -20
  32. package/src/panels/confirm-state.ts +8 -1
  33. package/src/panels/control-plane-panel.ts +351 -118
  34. package/src/panels/cost-tracker-panel.ts +165 -7
  35. package/src/panels/debug-panel.ts +312 -159
  36. package/src/panels/diff-panel.ts +209 -57
  37. package/src/panels/docs-panel.ts +152 -66
  38. package/src/panels/eval-panel.ts +220 -42
  39. package/src/panels/file-explorer-panel.ts +202 -122
  40. package/src/panels/file-preview-panel.ts +132 -57
  41. package/src/panels/git-panel.ts +310 -129
  42. package/src/panels/hooks-panel.ts +150 -22
  43. package/src/panels/incident-review-panel.ts +223 -35
  44. package/src/panels/index.ts +0 -4
  45. package/src/panels/intelligence-panel.ts +212 -86
  46. package/src/panels/knowledge-graph-panel.ts +461 -101
  47. package/src/panels/local-auth-panel.ts +240 -28
  48. package/src/panels/marketplace-panel.ts +193 -26
  49. package/src/panels/memory-panel.ts +78 -77
  50. package/src/panels/ops-control-panel.ts +146 -29
  51. package/src/panels/ops-strategy-panel.ts +72 -4
  52. package/src/panels/orchestration-panel.ts +231 -69
  53. package/src/panels/panel-list-panel.ts +154 -131
  54. package/src/panels/panel-manager.ts +97 -9
  55. package/src/panels/plan-dashboard-panel.ts +333 -17
  56. package/src/panels/plugins-panel.ts +184 -29
  57. package/src/panels/policy-panel.ts +210 -38
  58. package/src/panels/polish-core.ts +7 -2
  59. package/src/panels/polish.ts +23 -4
  60. package/src/panels/project-planning-answer-actions.ts +134 -0
  61. package/src/panels/project-planning-panel.ts +62 -113
  62. package/src/panels/provider-health-panel.ts +434 -518
  63. package/src/panels/provider-health-routes.ts +203 -0
  64. package/src/panels/provider-health-tracker.ts +194 -6
  65. package/src/panels/provider-health-views.ts +560 -0
  66. package/src/panels/qr-panel.ts +116 -30
  67. package/src/panels/remote-panel.ts +114 -36
  68. package/src/panels/routes-panel.ts +63 -22
  69. package/src/panels/sandbox-panel.ts +174 -42
  70. package/src/panels/scrollable-list-panel.ts +19 -135
  71. package/src/panels/security-panel.ts +133 -33
  72. package/src/panels/services-panel.ts +116 -64
  73. package/src/panels/session-browser-panel.ts +73 -2
  74. package/src/panels/session-maintenance.ts +4 -122
  75. package/src/panels/settings-sync-panel.ts +335 -72
  76. package/src/panels/skills-panel.ts +157 -89
  77. package/src/panels/subscription-panel.ts +86 -33
  78. package/src/panels/symbol-outline-panel.ts +248 -108
  79. package/src/panels/system-messages-panel.ts +114 -13
  80. package/src/panels/tasks-panel.ts +326 -139
  81. package/src/panels/thinking-panel.ts +43 -10
  82. package/src/panels/token-budget-panel.ts +194 -18
  83. package/src/panels/tool-inspector-panel.ts +144 -34
  84. package/src/panels/types.ts +40 -4
  85. package/src/panels/work-plan-panel.ts +280 -17
  86. package/src/panels/worktree-panel.ts +175 -42
  87. package/src/panels/wrfc-panel.ts +116 -24
  88. package/src/renderer/agent-detail-modal.ts +37 -8
  89. package/src/renderer/code-block.ts +58 -28
  90. package/src/renderer/context-inspector.ts +3 -6
  91. package/src/renderer/conversation-overlays.ts +25 -11
  92. package/src/renderer/conversation-surface.ts +1 -21
  93. package/src/renderer/diff-view.ts +6 -4
  94. package/src/renderer/footer-tips.ts +41 -0
  95. package/src/renderer/fullscreen-primitives.ts +23 -17
  96. package/src/renderer/fullscreen-workspace.ts +2 -7
  97. package/src/renderer/help-overlay.ts +91 -14
  98. package/src/renderer/hint-grammar.ts +52 -0
  99. package/src/renderer/history-search-overlay.ts +10 -16
  100. package/src/renderer/layout.ts +7 -7
  101. package/src/renderer/markdown.ts +9 -1
  102. package/src/renderer/modal-factory.ts +23 -15
  103. package/src/renderer/model-picker-overlay.ts +8 -485
  104. package/src/renderer/model-workspace.ts +9 -24
  105. package/src/renderer/overlay-box.ts +23 -19
  106. package/src/renderer/process-indicator.ts +15 -25
  107. package/src/renderer/profile-picker-modal.ts +13 -14
  108. package/src/renderer/prompt-content-width.ts +16 -0
  109. package/src/renderer/search-overlay.ts +27 -6
  110. package/src/renderer/selection-modal-overlay.ts +3 -1
  111. package/src/renderer/semantic-diff.ts +1 -1
  112. package/src/renderer/session-picker-modal.ts +6 -4
  113. package/src/renderer/settings-modal-helpers.ts +10 -34
  114. package/src/renderer/settings-modal.ts +70 -10
  115. package/src/renderer/shell-surface.ts +54 -15
  116. package/src/renderer/status-glyphs.ts +11 -9
  117. package/src/renderer/surface-layout.ts +0 -12
  118. package/src/renderer/term-caps.ts +1 -1
  119. package/src/renderer/theme.ts +60 -3
  120. package/src/renderer/tool-call.ts +6 -20
  121. package/src/renderer/ui-factory.ts +48 -37
  122. package/src/renderer/ui-primitives.ts +41 -3
  123. package/src/runtime/bootstrap-shell.ts +35 -18
  124. package/src/runtime/diagnostics/panels/index.ts +0 -3
  125. package/src/runtime/render-scheduler.ts +80 -0
  126. package/src/shell/ui-openers.ts +14 -22
  127. package/src/utils/format-duration.ts +2 -2
  128. package/src/utils/splash-lines.ts +54 -5
  129. package/src/version.ts +1 -1
  130. package/src/work-plans/work-plan-store.ts +13 -0
  131. package/src/panels/agent-logs-panel.ts +0 -635
  132. package/src/panels/agent-logs-shared.ts +0 -129
  133. package/src/panels/context-visualizer-panel.ts +0 -238
  134. package/src/panels/forensics-panel.ts +0 -378
  135. package/src/panels/provider-account-snapshot.ts +0 -259
  136. package/src/panels/provider-accounts-panel.ts +0 -255
  137. package/src/panels/provider-stats-panel.ts +0 -391
  138. package/src/panels/schedule-panel.ts +0 -365
  139. package/src/panels/watchers-panel.ts +0 -213
  140. package/src/renderer/file-tree.ts +0 -153
  141. package/src/renderer/progress.ts +0 -99
  142. package/src/renderer/status-token.ts +0 -67
  143. package/src/runtime/diagnostics/panels/panel-resources.ts +0 -118
  144. package/src/runtime/diagnostics/panels/policy.ts +0 -177
@@ -1,4 +1,7 @@
1
1
  import { formatDuration } from '../utils/format-duration.ts';
2
+ import { calcSessionCost } from '../export/cost-utils.ts';
3
+ import type { AgentEvent } from '@/runtime/index.ts';
4
+ import type { UiEventFeed } from '../runtime/ui-events.ts';
2
5
 
3
6
  export type AgentInspectorEntryKind = 'user' | 'assistant' | 'tool_call' | 'tool_result' | 'session' | 'error';
4
7
 
@@ -121,3 +124,144 @@ export function agentKindStyle(kind: AgentInspectorEntryKind, colors: Record<str
121
124
  default: return { fg: colors.dimmed ?? colors.system, prefix: '[?] ' };
122
125
  }
123
126
  }
127
+
128
+ // ---------------------------------------------------------------------------
129
+ // Inspector filter modes (ported from the merged agent-logs console — WO-110)
130
+ // ---------------------------------------------------------------------------
131
+
132
+ export type AgentInspectorFilterType = 'all' | 'assistant' | 'tool' | 'error';
133
+
134
+ export const AGENT_INSPECTOR_FILTER_CYCLE: readonly AgentInspectorFilterType[] = ['all', 'assistant', 'tool', 'error'];
135
+
136
+ export const AGENT_INSPECTOR_FILTER_LABELS: Record<AgentInspectorFilterType, string> = {
137
+ all: 'All',
138
+ assistant: 'Assistant',
139
+ tool: 'Tool',
140
+ error: 'Error',
141
+ };
142
+
143
+ /**
144
+ * True when a timeline entry's kind should be visible under the given filter.
145
+ * Mapped against the REAL row kinds jsonlToTimeline() produces (tool_call /
146
+ * tool_result), not the legacy agent-logs-shared schema (assistant/tool_call/
147
+ * tool_result vs. the writer-emitted tool_execution/llm_response) that never
148
+ * matched anything.
149
+ */
150
+ export function matchesInspectorFilter(kind: AgentInspectorEntryKind, filter: AgentInspectorFilterType): boolean {
151
+ switch (filter) {
152
+ case 'all': return true;
153
+ case 'assistant': return kind === 'assistant';
154
+ case 'tool': return kind === 'tool_call' || kind === 'tool_result';
155
+ case 'error': return kind === 'error';
156
+ default: return true;
157
+ }
158
+ }
159
+
160
+ // ---------------------------------------------------------------------------
161
+ // Per-agent cost/token summary (calcSessionCost, cost-utils.ts)
162
+ // ---------------------------------------------------------------------------
163
+
164
+ export function formatAgentCost(usd: number): string {
165
+ if (usd === 0) return '$0.00';
166
+ if (usd < 0.0001) return '<$0.0001';
167
+ if (usd < 0.01) return `$${usd.toFixed(4)}`;
168
+ return `$${usd.toFixed(3)}`;
169
+ }
170
+
171
+ export interface AgentUsageSummary {
172
+ readonly tokens: number;
173
+ readonly cost: number;
174
+ }
175
+
176
+ interface AgentUsageLike {
177
+ readonly usage?: {
178
+ readonly inputTokens: number;
179
+ readonly outputTokens: number;
180
+ readonly cacheReadTokens?: number;
181
+ readonly cacheWriteTokens?: number;
182
+ } | null;
183
+ readonly model?: string | null;
184
+ }
185
+
186
+ /**
187
+ * Total token count + cost for an agent record's usage, or null when no usage
188
+ * data has landed yet (honest-UX: never fabricate a $0.00/0-token reading for
189
+ * an agent that simply hasn't reported usage).
190
+ */
191
+ export function summarizeAgentUsage(rec: AgentUsageLike): AgentUsageSummary | null {
192
+ if (!rec.usage) return null;
193
+ const inputTokens = rec.usage.inputTokens + (rec.usage.cacheReadTokens ?? 0) + (rec.usage.cacheWriteTokens ?? 0);
194
+ const cost = calcSessionCost(
195
+ rec.usage.inputTokens,
196
+ rec.usage.outputTokens,
197
+ rec.usage.cacheReadTokens ?? 0,
198
+ rec.usage.cacheWriteTokens ?? 0,
199
+ rec.model ?? 'unknown',
200
+ );
201
+ return { tokens: inputTokens + rec.usage.outputTokens, cost };
202
+ }
203
+
204
+ // ---------------------------------------------------------------------------
205
+ // WRFC badge segments (AgentRecord.wrfcId/wrfcRole)
206
+ // ---------------------------------------------------------------------------
207
+
208
+ interface WrfcLike {
209
+ readonly wrfcId?: string | null;
210
+ readonly wrfcRole?: string | null;
211
+ }
212
+
213
+ /** Segments for the WRFC chain badge + token/cost line, or null when neither applies. */
214
+ export function buildWrfcCostSegments(
215
+ rec: WrfcLike & AgentUsageLike,
216
+ palette: { readonly label: string; readonly info: string },
217
+ formatTokens: (n: number) => string,
218
+ ): Array<[string, string]> | null {
219
+ const segments: Array<[string, string]> = [];
220
+ if (rec.wrfcId) {
221
+ segments.push([' WRFC ', palette.label]);
222
+ segments.push([`${rec.wrfcRole ?? 'agent'} · ${rec.wrfcId.slice(-8)}`, palette.info]);
223
+ }
224
+ const usage = summarizeAgentUsage(rec);
225
+ if (usage) {
226
+ segments.push([segments.length > 0 ? ' Tokens ' : ' Tokens ', palette.label]);
227
+ segments.push([formatTokens(usage.tokens), palette.info]);
228
+ segments.push([' Cost ', palette.label]);
229
+ segments.push([formatAgentCost(usage.cost), palette.info]);
230
+ }
231
+ return segments.length > 0 ? segments : null;
232
+ }
233
+
234
+ // ---------------------------------------------------------------------------
235
+ // Agent lifecycle event subscriptions (ported from agent-logs — WO-110)
236
+ // ---------------------------------------------------------------------------
237
+
238
+ export interface AgentActivityHooks {
239
+ /** Any AGENT_* activity event for this agent (running/progress/stream/awaiting/finalizing/spawning). */
240
+ readonly onActivity: (agentId: string) => void;
241
+ /** AGENT_SPAWNING only — used for auto-select-newest-agent. */
242
+ readonly onSpawn: (agentId: string) => void;
243
+ /** AGENT_COMPLETED/FAILED/CANCELLED. */
244
+ readonly onTerminal: (agentId: string) => void;
245
+ }
246
+
247
+ /** Wires the standard set of agent lifecycle listeners; returns their unsubscribe functions. */
248
+ export function subscribeAgentActivity(agentEvents: UiEventFeed<AgentEvent>, hooks: AgentActivityHooks): Array<() => void> {
249
+ const unsubs: Array<() => void> = [];
250
+ const activityTypes = [
251
+ 'AGENT_RUNNING', 'AGENT_PROGRESS', 'AGENT_STREAM_DELTA',
252
+ 'AGENT_AWAITING_MESSAGE', 'AGENT_AWAITING_TOOL', 'AGENT_FINALIZING',
253
+ ] as const;
254
+ for (const type of activityTypes) {
255
+ unsubs.push(agentEvents.on(type, (payload: { agentId: string }) => hooks.onActivity(payload.agentId)));
256
+ }
257
+ unsubs.push(agentEvents.on('AGENT_SPAWNING', (payload) => {
258
+ hooks.onActivity(payload.agentId);
259
+ hooks.onSpawn(payload.agentId);
260
+ }));
261
+ unsubs.push(
262
+ agentEvents.on('AGENT_COMPLETED', (payload) => hooks.onTerminal(payload.agentId)),
263
+ agentEvents.on('AGENT_FAILED', (payload) => hooks.onTerminal(payload.agentId)),
264
+ agentEvents.on('AGENT_CANCELLED', (payload) => hooks.onTerminal(payload.agentId)),
265
+ );
266
+ return unsubs;
267
+ }
@@ -1,5 +1,6 @@
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 {
4
5
  buildBodyText,
5
6
  buildDetailBlock,
@@ -15,10 +16,18 @@ import { truncateDisplay } from '../utils/terminal-width.ts';
15
16
  import type { PolicyRuntimeState, PermissionAuditEntry } from '@/runtime/index.ts';
16
17
  import { buildPermissionRuleSuggestions } from '@/runtime/index.ts';
17
18
 
18
- const C = {
19
- ...DEFAULT_PANEL_PALETTE,
20
- headerBg: '#111827',
21
- } as const;
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;
22
31
 
23
32
  // Reference catalog of approval lanes and where each one is reviewed. Used to
24
33
  // resolve the next-step command for a live request and as a fallback reference
@@ -71,13 +80,45 @@ function fmtAgo(ts: number): string {
71
80
 
72
81
  export class ApprovalPanel extends ScrollableListPanel<PermissionAuditEntry> {
73
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;
74
87
 
75
88
  public constructor(policyRuntimeState: Pick<PolicyRuntimeState, 'getSnapshot'>) {
76
- super('approval', 'Approval', 'A', 'monitoring');
89
+ super('approval', 'Approval', 'A', 'security-policy');
77
90
  this.showSelectionGutter = true; // I5: non-color selection affordance
78
91
  this.policyRuntimeState = policyRuntimeState;
79
92
  }
80
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
+
81
122
  protected override getPalette() { return C; }
82
123
  protected override getEmptyStateMessage() { return ' No approval pressure right now.'; }
83
124
  protected override getEmptyStateActions() {
@@ -112,8 +153,7 @@ export class ApprovalPanel extends ScrollableListPanel<PermissionAuditEntry> {
112
153
 
113
154
  /** Next-step review command for the currently selected request (if any). */
114
155
  public getSelectedCommand(): string | null {
115
- const items = this.getVisibleItems();
116
- const selected = items[this.selectedIndex];
156
+ const selected = this.getSelectedItem();
117
157
  return selected ? reviewFor(selected).command : null;
118
158
  }
119
159
 
@@ -124,8 +164,7 @@ export class ApprovalPanel extends ScrollableListPanel<PermissionAuditEntry> {
124
164
  const denialCount = audit.filter((e) => e.approved === false).length;
125
165
  const pendingCount = audit.filter((e) => e.approved === undefined).length;
126
166
 
127
- const items = this.getVisibleItems();
128
- const selected = items[this.selectedIndex] ?? null;
167
+ const selected = this.getSelectedItem() ?? null;
129
168
 
130
169
  // ---- Posture summary (severity + counts first) ----
131
170
  const headerLines: Line[] = [
@@ -139,7 +178,10 @@ export class ApprovalPanel extends ScrollableListPanel<PermissionAuditEntry> {
139
178
  ]),
140
179
  pendingCount > 0
141
180
  ? buildPanelLine(width, [[` ${pendingCount} request${pendingCount !== 1 ? 's' : ''} awaiting a decision — select one to see its review path.`, C.warn]])
142
- : buildGuidanceLine(width, '/policy simulate', 'preview which requests a scoped rule change would auto-approve', C),
181
+ : buildPanelLine(width, [
182
+ [' p ', C.info],
183
+ ['/policy simulate — preview which requests a scoped rule change would auto-approve', C.dim],
184
+ ]),
143
185
  ];
144
186
 
145
187
  // ---- Detail block for the selected request ----
@@ -162,18 +204,25 @@ export class ApprovalPanel extends ScrollableListPanel<PermissionAuditEntry> {
162
204
  ...(selected.reasons[0]
163
205
  ? buildBodyText(width, `why prompted: ${selected.reasons[0]}`, C, C.dim)
164
206
  : buildBodyText(width, `why prompted: ${review.why}`, C, C.dim)),
165
- buildGuidanceLine(width, review.command, `review and decide the ${laneOf(selected)} request`, C),
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.
166
211
  ], C));
167
212
  }
168
213
 
169
- // ---- Durable-rule suggestions from repeated denials ----
214
+ // ---- Durable-rule suggestions from repeated denials (dispatchable via 1/2/3) ----
215
+ const suggestions = this.getRuleSuggestions();
170
216
  const ruleSuggestionLines: Line[] = [];
171
- for (const suggestion of buildPermissionRuleSuggestions(audit).slice(0, 3)) {
172
- ruleSuggestionLines.push(buildPanelLine(width, [[` ${truncateDisplay(suggestion.summary, Math.max(0, width - 4))}`, 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
+ ]));
173
222
  ruleSuggestionLines.push(buildGuidanceLine(width, suggestion.command, suggestion.reason, C));
174
- }
223
+ });
175
224
  if (ruleSuggestionLines.length > 0) {
176
- ruleSuggestionLines.unshift(buildPanelLine(width, [[' Suggested durable rules', C.label]]));
225
+ ruleSuggestionLines.unshift(buildPanelLine(width, [[' Suggested durable rules (press 1/2/3 to apply)', C.label]]));
177
226
  }
178
227
 
179
228
  // ---- Context-aware footer: only show review key when a request is selected ----
@@ -187,6 +236,8 @@ export class ApprovalPanel extends ScrollableListPanel<PermissionAuditEntry> {
187
236
  { keys: '↑/↓', label: 'select' },
188
237
  { keys: 'g/G', label: 'top/bottom' },
189
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' });
190
241
 
191
242
  return this.renderList(width, height, {
192
243
  title: 'Approval Control Room',