@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.
- package/CHANGELOG.md +133 -0
- package/README.md +1 -1
- package/package.json +1 -1
- package/src/core/conversation-line-cache.ts +432 -0
- package/src/core/conversation-rendering.ts +11 -3
- package/src/core/conversation.ts +50 -4
- package/src/input/commands/cost-runtime.ts +49 -0
- package/src/input/commands/operator-runtime.ts +5 -1
- package/src/input/commands.ts +2 -0
- package/src/input/handler-content-actions.ts +8 -3
- package/src/input/handler-feed-routes.ts +12 -35
- package/src/input/handler-feed.ts +4 -8
- package/src/input/handler-shortcuts.ts +51 -0
- package/src/input/handler.ts +43 -6
- package/src/input/input-history.ts +17 -4
- package/src/input/keybindings.ts +48 -8
- package/src/input/panel-integration-actions.ts +107 -1
- package/src/main.ts +19 -11
- package/src/panels/agent-inspector-panel.ts +125 -36
- package/src/panels/agent-inspector-shared.ts +144 -0
- package/src/panels/approval-panel.ts +67 -16
- package/src/panels/automation-control-panel.ts +368 -124
- package/src/panels/base-panel.ts +1 -1
- package/src/panels/builtin/agent.ts +28 -37
- package/src/panels/builtin/development.ts +40 -32
- package/src/panels/builtin/knowledge.ts +15 -6
- package/src/panels/builtin/operations.ts +178 -117
- package/src/panels/builtin/session.ts +35 -9
- package/src/panels/builtin/shared.ts +98 -6
- package/src/panels/cockpit-panel.ts +232 -73
- package/src/panels/communication-panel.ts +58 -20
- package/src/panels/confirm-state.ts +8 -1
- package/src/panels/control-plane-panel.ts +351 -118
- package/src/panels/cost-tracker-panel.ts +165 -7
- package/src/panels/debug-panel.ts +312 -159
- package/src/panels/diff-panel.ts +209 -57
- package/src/panels/docs-panel.ts +152 -66
- package/src/panels/eval-panel.ts +220 -42
- package/src/panels/file-explorer-panel.ts +202 -122
- package/src/panels/file-preview-panel.ts +132 -57
- package/src/panels/git-panel.ts +310 -129
- package/src/panels/hooks-panel.ts +150 -22
- package/src/panels/incident-review-panel.ts +223 -35
- package/src/panels/index.ts +0 -4
- package/src/panels/intelligence-panel.ts +212 -86
- package/src/panels/knowledge-graph-panel.ts +461 -101
- package/src/panels/local-auth-panel.ts +240 -28
- package/src/panels/marketplace-panel.ts +193 -26
- package/src/panels/memory-panel.ts +78 -77
- package/src/panels/ops-control-panel.ts +146 -29
- package/src/panels/ops-strategy-panel.ts +72 -4
- package/src/panels/orchestration-panel.ts +231 -69
- package/src/panels/panel-list-panel.ts +154 -131
- package/src/panels/panel-manager.ts +97 -9
- package/src/panels/plan-dashboard-panel.ts +333 -17
- package/src/panels/plugins-panel.ts +184 -29
- package/src/panels/policy-panel.ts +210 -38
- package/src/panels/polish-core.ts +7 -2
- package/src/panels/polish.ts +23 -4
- package/src/panels/project-planning-answer-actions.ts +134 -0
- package/src/panels/project-planning-panel.ts +62 -113
- package/src/panels/provider-health-panel.ts +434 -518
- package/src/panels/provider-health-routes.ts +203 -0
- package/src/panels/provider-health-tracker.ts +194 -6
- package/src/panels/provider-health-views.ts +560 -0
- package/src/panels/qr-panel.ts +116 -30
- package/src/panels/remote-panel.ts +114 -36
- package/src/panels/routes-panel.ts +63 -22
- package/src/panels/sandbox-panel.ts +174 -42
- package/src/panels/scrollable-list-panel.ts +19 -135
- package/src/panels/security-panel.ts +133 -33
- package/src/panels/services-panel.ts +116 -64
- package/src/panels/session-browser-panel.ts +73 -2
- package/src/panels/session-maintenance.ts +4 -122
- package/src/panels/settings-sync-panel.ts +335 -72
- package/src/panels/skills-panel.ts +157 -89
- package/src/panels/subscription-panel.ts +86 -33
- package/src/panels/symbol-outline-panel.ts +248 -108
- package/src/panels/system-messages-panel.ts +114 -13
- package/src/panels/tasks-panel.ts +326 -139
- package/src/panels/thinking-panel.ts +43 -10
- package/src/panels/token-budget-panel.ts +194 -18
- package/src/panels/tool-inspector-panel.ts +144 -34
- package/src/panels/types.ts +40 -4
- package/src/panels/work-plan-panel.ts +280 -17
- package/src/panels/worktree-panel.ts +175 -42
- package/src/panels/wrfc-panel.ts +116 -24
- package/src/renderer/agent-detail-modal.ts +37 -8
- package/src/renderer/code-block.ts +58 -28
- package/src/renderer/context-inspector.ts +3 -6
- package/src/renderer/conversation-overlays.ts +25 -11
- package/src/renderer/conversation-surface.ts +1 -21
- package/src/renderer/diff-view.ts +6 -4
- package/src/renderer/footer-tips.ts +41 -0
- package/src/renderer/fullscreen-primitives.ts +23 -17
- package/src/renderer/fullscreen-workspace.ts +2 -7
- package/src/renderer/help-overlay.ts +91 -14
- package/src/renderer/hint-grammar.ts +52 -0
- package/src/renderer/history-search-overlay.ts +10 -16
- package/src/renderer/layout.ts +7 -7
- package/src/renderer/markdown.ts +9 -1
- package/src/renderer/modal-factory.ts +23 -15
- package/src/renderer/model-picker-overlay.ts +8 -485
- package/src/renderer/model-workspace.ts +9 -24
- package/src/renderer/overlay-box.ts +23 -19
- package/src/renderer/process-indicator.ts +15 -25
- package/src/renderer/profile-picker-modal.ts +13 -14
- package/src/renderer/prompt-content-width.ts +16 -0
- package/src/renderer/search-overlay.ts +27 -6
- package/src/renderer/selection-modal-overlay.ts +3 -1
- package/src/renderer/semantic-diff.ts +1 -1
- package/src/renderer/session-picker-modal.ts +6 -4
- package/src/renderer/settings-modal-helpers.ts +10 -34
- package/src/renderer/settings-modal.ts +70 -10
- package/src/renderer/shell-surface.ts +54 -15
- package/src/renderer/status-glyphs.ts +11 -9
- package/src/renderer/surface-layout.ts +0 -12
- package/src/renderer/term-caps.ts +1 -1
- package/src/renderer/theme.ts +60 -3
- package/src/renderer/tool-call.ts +6 -20
- package/src/renderer/ui-factory.ts +48 -37
- package/src/renderer/ui-primitives.ts +41 -3
- package/src/runtime/bootstrap-shell.ts +35 -18
- package/src/runtime/diagnostics/panels/index.ts +0 -3
- package/src/runtime/render-scheduler.ts +80 -0
- package/src/shell/ui-openers.ts +14 -22
- package/src/utils/format-duration.ts +2 -2
- package/src/utils/splash-lines.ts +54 -5
- package/src/version.ts +1 -1
- package/src/work-plans/work-plan-store.ts +13 -0
- package/src/panels/agent-logs-panel.ts +0 -635
- package/src/panels/agent-logs-shared.ts +0 -129
- package/src/panels/context-visualizer-panel.ts +0 -238
- package/src/panels/forensics-panel.ts +0 -378
- package/src/panels/provider-account-snapshot.ts +0 -259
- package/src/panels/provider-accounts-panel.ts +0 -255
- package/src/panels/provider-stats-panel.ts +0 -391
- package/src/panels/schedule-panel.ts +0 -365
- package/src/panels/watchers-panel.ts +0 -213
- package/src/renderer/file-tree.ts +0 -153
- package/src/renderer/progress.ts +0 -99
- package/src/renderer/status-token.ts +0 -67
- package/src/runtime/diagnostics/panels/panel-resources.ts +0 -118
- 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
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
}
|
|
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', '
|
|
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
|
|
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
|
|
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
|
-
:
|
|
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
|
-
|
|
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
|
-
|
|
172
|
-
ruleSuggestionLines.push(buildPanelLine(width, [
|
|
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',
|