@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,39 +1,66 @@
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 type { UiCommunicationSnapshot, UiReadModel } from '../runtime/ui-read-models.ts';
4
5
  import { truncateDisplay } from '../utils/terminal-width.ts';
5
6
  import {
6
7
  buildBodyText,
8
+ buildDetailBlock,
7
9
  buildEmptyState,
8
10
  buildGuidanceLine,
9
11
  buildKeyValueLine,
12
+ buildKeyboardHints,
10
13
  buildPanelLine,
11
14
  buildPanelWorkspace,
15
+ buildStatusBadge,
12
16
  DEFAULT_PANEL_PALETTE,
13
17
  type PanelPalette,
18
+ type StatusBadgeKind,
14
19
  } from './polish.ts';
15
20
  import { createEmptyLine } from '../types/grid.ts';
16
21
 
17
- const C = {
18
- ...DEFAULT_PANEL_PALETTE,
19
- header: '#94a3b8',
20
- headerBg: '#1e293b',
21
- ok: '#22c55e',
22
- warn: '#eab308',
23
- error: '#ef4444',
24
- selectBg: '#0f172a',
25
- } as const;
22
+ // Base chrome only — title band, state colors, and text tokens all come
23
+ // straight from DEFAULT_PANEL_PALETTE (WO-002).
24
+ const C = DEFAULT_PANEL_PALETTE;
26
25
 
27
26
  type CommunicationRecord = UiCommunicationSnapshot['records'][number];
28
27
 
28
+ function statusBadgeKind(status: CommunicationRecord['status']): StatusBadgeKind {
29
+ return status === 'blocked' ? 'blocked' : status === 'delivered' ? 'completed' : 'running';
30
+ }
31
+
32
+ function fmtAgo(ts: number): string {
33
+ const sec = Math.max(0, Math.floor((Date.now() - ts) / 1000));
34
+ if (sec < 60) return `${sec}s`;
35
+ if (sec < 3600) return `${Math.floor(sec / 60)}m`;
36
+ return `${Math.floor(sec / 3600)}h`;
37
+ }
38
+
39
+ // Set by handleInput (enter/o) and consumed on the very next
40
+ // handlePanelIntegrationAction dispatch of that same key — handleInput has
41
+ // no access to the panelManager/openAgentDetail callback.
42
+ type PendingCommunicationAction =
43
+ | { readonly kind: 'open-orchestration' }
44
+ | { readonly kind: 'open-agent-detail'; readonly agentId: string };
45
+
29
46
  export class CommunicationPanel extends ScrollableListPanel<CommunicationRecord> {
30
47
  private readonly readModel?: UiReadModel<UiCommunicationSnapshot>;
31
48
  private readonly unsub: (() => void) | null;
49
+ private readonly openAgentDetail?: (agentId: string) => void;
50
+ /** When true, the list is narrowed to blocked messages only (b toggles). */
51
+ private blockedOnly = false;
52
+ private pendingAction: PendingCommunicationAction | null = null;
32
53
 
33
- public constructor(readModel?: UiReadModel<UiCommunicationSnapshot>) {
34
- super('communication', 'Communication', 'Y', 'monitoring');
54
+ public constructor(
55
+ readModel?: UiReadModel<UiCommunicationSnapshot>,
56
+ openAgentDetail?: (agentId: string) => void,
57
+ ) {
58
+ super('communication', 'Communication', 'Y', 'runtime-ops');
35
59
  this.showSelectionGutter = true; // I5: non-color selection affordance
60
+ this.filterEnabled = true;
61
+ this.filterLabel = 'Filter messages';
36
62
  this.readModel = readModel;
63
+ this.openAgentDetail = openAgentDetail;
37
64
  this.unsub = readModel ? readModel.subscribe(() => this.markDirty()) : null;
38
65
  }
39
66
 
@@ -47,18 +74,76 @@ export class CommunicationPanel extends ScrollableListPanel<CommunicationRecord>
47
74
 
48
75
  protected getItems(): readonly CommunicationRecord[] {
49
76
  if (!this.readModel) return [];
50
- return this.readModel.getSnapshot().records;
77
+ const records = this.readModel.getSnapshot().records;
78
+ return this.blockedOnly ? records.filter((r) => r.status === 'blocked') : records;
51
79
  }
52
80
 
53
- protected renderItem(record: CommunicationRecord, index: number, selected: boolean, width: number): Line {
81
+ protected override filterMatches(record: CommunicationRecord, q: string): boolean {
82
+ return (record.content ?? '').toLowerCase().includes(q)
83
+ || record.kind.toLowerCase().includes(q)
84
+ || String(record.fromId ?? '').toLowerCase().includes(q)
85
+ || String(record.toId ?? '').toLowerCase().includes(q);
86
+ }
87
+
88
+ public override handleInput(key: string): boolean {
89
+ // `b` toggles the blocked-lanes-only view — guarded so it only acts when not
90
+ // typing into the filter input.
91
+ if (key === 'b' && !this.filterActive) {
92
+ this.blockedOnly = !this.blockedOnly;
93
+ this.selectedIndex = 0;
94
+ this.markDirty();
95
+ return true;
96
+ }
97
+ // Enter jumps to the orchestration workspace that actually produces this
98
+ // traffic (a direct panel jump, not a printed "/orchestration" signpost).
99
+ // Guarded so it doesn't hijack the filter input's commit-on-enter.
100
+ if ((key === 'enter' || key === 'return') && !this.filterActive) {
101
+ this.pendingAction = { kind: 'open-orchestration' };
102
+ return true;
103
+ }
104
+ // `o` on a blocked record opens the sender's agent detail (falling back
105
+ // to the receiver when the sender id is missing) via deps.openAgentDetail.
106
+ if (key === 'o' && !this.filterActive) {
107
+ const record = this.getSelectedItem();
108
+ if (!record || record.status !== 'blocked') return false;
109
+ const agentId = record.fromId || record.toId;
110
+ if (!agentId) return false;
111
+ this.pendingAction = { kind: 'open-agent-detail', agentId };
112
+ return true;
113
+ }
114
+ return super.handleInput(key);
115
+ }
116
+
117
+ public handlePanelIntegrationAction(_key: string, ctx: PanelIntegrationContext): boolean {
118
+ if (!this.pendingAction) return false;
119
+ const action = this.pendingAction;
120
+ this.pendingAction = null;
121
+ if (action.kind === 'open-orchestration') {
122
+ ctx.panelManager.open('orchestration');
123
+ return true;
124
+ }
125
+ this.openAgentDetail?.(action.agentId);
126
+ return true;
127
+ }
128
+
129
+ protected renderItem(record: CommunicationRecord, _index: number, selected: boolean, width: number): Line {
54
130
  const bg = selected ? C.selectBg : undefined;
55
- const color = record.status === 'blocked' ? C.error : record.status === 'delivered' ? C.ok : C.info;
131
+ const badge = buildStatusBadge(statusBadgeKind(record.status), record.status)[0]!;
132
+ const route = truncateDisplay(`${record.fromId} → ${record.toId}`, 26);
133
+ const contentWidth = Math.max(0, width - 50);
134
+ // Blocked messages have no delivered content; surface the policy reason
135
+ // inline so the most important detail (why a lane is blocked) is visible
136
+ // without selecting the row.
137
+ const tail = record.status === 'blocked' && record.reason
138
+ ? { text: record.reason, fg: C.warn }
139
+ : { text: record.content, fg: C.dim };
56
140
  return buildPanelLine(width, [
57
141
  [' ', C.label, bg],
58
- [record.status.padEnd(10), color, bg],
59
- [` ${record.kind.padEnd(10)}`, C.info, bg],
60
- [` ${truncateDisplay(`${record.fromId} -> ${record.toId}`, 28).padEnd(28)}`, C.value, bg],
61
- [` ${truncateDisplay(record.content, Math.max(0, width - 53))}`, C.dim, bg],
142
+ [`${fmtAgo(record.timestamp)} `.padStart(5), C.dim, bg],
143
+ [` ${badge.text} `.padEnd(12), badge.fg, bg],
144
+ [`${record.kind} `.padEnd(9), C.info, bg],
145
+ [`${route} `.padEnd(27), C.value, bg],
146
+ [truncateDisplay(tail.text, contentWidth), tail.fg, bg],
62
147
  ]);
63
148
  }
64
149
 
@@ -68,8 +153,7 @@ export class CommunicationPanel extends ScrollableListPanel<CommunicationRecord>
68
153
 
69
154
  protected override getEmptyStateActions(): Array<{ command: string; summary: string }> {
70
155
  return [
71
- { command: '/orchestration', summary: 'review graphs and recursive agent activity' },
72
- { command: '/communication', summary: 'reopen this workspace once the runtime emits message traffic' },
156
+ { command: '/orchestration', summary: 'review graphs and recursive agent activity that emit messages' },
73
157
  ];
74
158
  }
75
159
 
@@ -85,7 +169,7 @@ export class CommunicationPanel extends ScrollableListPanel<CommunicationRecord>
85
169
  width,
86
170
  ' Runtime store not wired into this panel yet.',
87
171
  'This workspace needs the live runtime store before it can show communication history and policy outcomes.',
88
- [{ command: '/communication', summary: 'reopen the workspace from the shell-owned runtime' }],
172
+ [{ command: '/orchestration', summary: 'inspect recursive routing and agent activity while this workspace is unwired' }],
89
173
  C,
90
174
  ),
91
175
  }],
@@ -98,56 +182,75 @@ export class CommunicationPanel extends ScrollableListPanel<CommunicationRecord>
98
182
  const snapshot = this.readModel.getSnapshot();
99
183
  const records = this.getItems();
100
184
 
101
- const postureLines: Line[] = [
102
- buildPanelLine(width, [[' Communication posture', C.label]]),
103
- buildKeyValueLine(width, [
104
- { label: 'sent', value: String(snapshot.totalSent), valueColor: snapshot.totalSent > 0 ? C.info : C.dim },
105
- { label: 'delivered', value: String(snapshot.totalDelivered), valueColor: snapshot.totalDelivered > 0 ? C.ok : C.dim },
106
- { label: 'blocked', value: String(snapshot.totalBlocked), valueColor: snapshot.totalBlocked > 0 ? C.error : C.dim },
107
- ], C),
108
- buildGuidanceLine(width, '/orchestration', 'inspect recursive routing, message handoff, and blocked broadcast posture', C),
185
+ // Posture: blocked lanes / message flow surfaced first per the panel's purpose.
186
+ const postureLines = (): Line[] => [
187
+ buildPanelLine(width, [
188
+ [' ', C.label],
189
+ ...buildStatusBadge('blocked', 'blocked', { count: snapshot.totalBlocked }),
190
+ [' ', C.dim],
191
+ ...buildStatusBadge('completed', 'delivered', { count: snapshot.totalDelivered }),
192
+ [' ', C.dim],
193
+ ...buildStatusBadge('running', 'sent', { count: snapshot.totalSent }),
194
+ ...(this.blockedOnly ? ([[' (blocked-only view)', C.warn]] as Array<[string, string]>) : []),
195
+ ]),
196
+ snapshot.totalBlocked > 0
197
+ ? buildPanelLine(width, [[` ${snapshot.totalBlocked} message${snapshot.totalBlocked !== 1 ? 's' : ''} blocked by routing policy — press b to isolate blocked lanes.`, C.warn]])
198
+ : buildPanelLine(width, [[' No blocked lanes — press Enter on a record to inspect routing in the orchestration console.', C.dim]]),
109
199
  ];
110
200
 
111
201
  if (records.length === 0) {
112
202
  return this.renderList(width, height, {
113
203
  title: 'Communication Control Room',
114
- header: postureLines,
204
+ header: postureLines(),
205
+ footer: [buildKeyboardHints(width, this.footerHints(false), C)],
115
206
  });
116
207
  }
117
208
 
118
209
  this.clampSelection();
119
- const selected = records[this.selectedIndex];
120
-
121
- // Update posture with selected info
122
- const postureWithSelected: Line[] = [
123
- buildPanelLine(width, [[' Communication posture', C.label]]),
124
- buildKeyValueLine(width, [
125
- { label: 'sent', value: String(snapshot.totalSent), valueColor: snapshot.totalSent > 0 ? C.info : C.dim },
126
- { label: 'delivered', value: String(snapshot.totalDelivered), valueColor: snapshot.totalDelivered > 0 ? C.ok : C.dim },
127
- { label: 'blocked', value: String(snapshot.totalBlocked), valueColor: snapshot.totalBlocked > 0 ? C.error : C.dim },
128
- { label: 'selected', value: `${selected?.fromId ?? 'n/a'} -> ${selected?.toId ?? 'n/a'}`, valueColor: C.value },
129
- ], C),
130
- buildGuidanceLine(width, '/orchestration', 'inspect recursive routing, message handoff, and blocked broadcast posture', C),
131
- ];
210
+ const selected = this.getSelectedItem();
132
211
 
133
- const footerLines: Line[] = [];
212
+ const detailLines: Line[] = [];
134
213
  if (selected) {
135
- footerLines.push(
136
- buildPanelLine(width, [[' Route: ', C.label], [`${selected.scope} / ${selected.kind}`, C.value], [' Status: ', C.label], [selected.status, selected.status === 'blocked' ? C.error : C.ok]]),
137
- buildPanelLine(width, [[' From: ', C.label], [selected.fromId, C.value], [' To: ', C.label], [selected.toId, C.value]]),
138
- buildPanelLine(width, [[' Roles: ', C.label], [`${selected.fromRole ?? 'unknown'} -> ${selected.toRole ?? 'unknown'}`, C.dim]]),
139
- );
140
- if (selected.reason) {
141
- footerLines.push(buildPanelLine(width, [[' Reason: ', C.label], [truncateDisplay(selected.reason, Math.max(0, width - 11)), C.warn]]));
142
- }
143
- footerLines.push(...buildBodyText(width, ` Content: ${selected.content}`, C));
214
+ detailLines.push(...buildDetailBlock(width, `Message · ${selected.kind}`, [
215
+ buildKeyValueLine(width, [
216
+ { label: 'status', value: selected.status, valueColor: selected.status === 'blocked' ? C.bad : C.good },
217
+ { label: 'scope', value: selected.scope, valueColor: C.value },
218
+ { label: 'when', value: `${fmtAgo(selected.timestamp)} ago`, valueColor: C.dim },
219
+ ], C),
220
+ buildKeyValueLine(width, [
221
+ { label: 'from', value: `${selected.fromId} (${selected.fromRole ?? 'unknown'})`, valueColor: C.value },
222
+ { label: 'to', value: `${selected.toId} (${selected.toRole ?? 'unknown'})`, valueColor: C.value },
223
+ ], C),
224
+ ...(selected.reason
225
+ ? buildBodyText(width, `blocked: ${selected.reason}`, C, C.warn)
226
+ : []),
227
+ ...buildBodyText(width, selected.content, C, C.value),
228
+ ], C));
144
229
  }
145
- footerLines.push(buildPanelLine(width, [[' Up/Down move through messages', C.dim]]));
146
230
 
147
231
  return this.renderList(width, height, {
148
232
  title: 'Communication Control Room',
149
- header: postureWithSelected,
150
- footer: footerLines,
233
+ header: postureLines(),
234
+ footer: [...detailLines, buildKeyboardHints(width, this.footerHints(true, selected?.status === 'blocked'), C)],
151
235
  });
152
236
  }
237
+
238
+ // Context-aware hints: filter keys reflect filter state, `b` reflects the
239
+ // blocked-only toggle, `o` only appears on a blocked selection, and the
240
+ // inspect hint only appears when rows exist.
241
+ private footerHints(hasRows: boolean, onBlockedSelection = false): Array<{ keys: string; label: string }> {
242
+ const hints: Array<{ keys: string; label: string }> = [];
243
+ if (hasRows) hints.push({ keys: '↑/↓', label: 'select' });
244
+ hints.push({ keys: 'Enter', label: 'orchestration' });
245
+ if (onBlockedSelection) hints.push({ keys: 'o', label: 'sender detail' });
246
+ hints.push({ keys: 'b', label: this.blockedOnly ? 'show all lanes' : 'blocked only' });
247
+ if (this.filterActive) {
248
+ hints.push({ keys: 'Esc', label: 'clear filter' });
249
+ } else if (this.filterQuery) {
250
+ hints.push({ keys: '/', label: 'edit filter' }, { keys: 'Esc', label: 'clear filter' });
251
+ } else {
252
+ hints.push({ keys: '/', label: 'filter' });
253
+ }
254
+ return hints;
255
+ }
153
256
  }
@@ -28,6 +28,12 @@ export interface ConfirmState<T = string> {
28
28
  readonly subject: T;
29
29
  /** Human-readable label for the item being destroyed. */
30
30
  readonly label: string;
31
+ /**
32
+ * Action verb shown in the confirmation prompt (e.g. 'Cancel', 'Regenerate',
33
+ * 'Promote'). Defaults to 'Delete' so existing destructive confirms are
34
+ * unaffected; non-destructive confirms should set this for honest copy.
35
+ */
36
+ readonly verb?: string;
31
37
  }
32
38
 
33
39
  /**
@@ -61,9 +67,10 @@ export function handleConfirmInput<T = string>(
61
67
  */
62
68
  export function renderConfirmLines<T = string>(width: number, state: ConfirmState<T>): Line[] {
63
69
  const palette = DEFAULT_PANEL_PALETTE;
70
+ const verb = state.verb ?? 'Delete';
64
71
  return [
65
72
  buildPanelLine(width, [[
66
- ` Delete "${state.label}"?`,
73
+ ` ${verb} "${state.label}"?`,
67
74
  palette.warn,
68
75
  ]]),
69
76
  buildPanelLine(width, [