@pellux/goodvibes-tui 1.1.0 → 1.7.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (194) hide show
  1. package/CHANGELOG.md +85 -21
  2. package/README.md +20 -11
  3. package/docs/foundation-artifacts/operator-contract.json +1 -1
  4. package/package.json +2 -2
  5. package/src/core/alert-gating.ts +67 -0
  6. package/src/core/approval-alert.ts +72 -0
  7. package/src/core/budget-breach-notifier.ts +106 -0
  8. package/src/core/conversation-rendering.ts +19 -0
  9. package/src/core/conversation.ts +18 -0
  10. package/src/core/focus-tracker.ts +41 -0
  11. package/src/core/long-task-notifier.ts +33 -6
  12. package/src/core/system-message-router.ts +37 -51
  13. package/src/core/turn-cancellation.ts +20 -0
  14. package/src/core/turn-event-wiring.ts +64 -3
  15. package/src/export/cost-utils.ts +36 -0
  16. package/src/input/command-registry.ts +38 -1
  17. package/src/input/commands/checkpoint-runtime.ts +280 -0
  18. package/src/input/commands/codebase-runtime.ts +192 -0
  19. package/src/input/commands/eval.ts +1 -1
  20. package/src/input/commands/health-runtime.ts +1 -1
  21. package/src/input/commands/image-runtime.ts +112 -0
  22. package/src/input/commands/intelligence-runtime.ts +11 -1
  23. package/src/input/commands/local-auth-runtime.ts +4 -1
  24. package/src/input/commands/local-runtime.ts +9 -3
  25. package/src/input/commands/marketplace-runtime.ts +2 -2
  26. package/src/input/commands/memory.ts +6 -1
  27. package/src/input/commands/operator-panel-runtime.ts +40 -24
  28. package/src/input/commands/planning-runtime.ts +26 -3
  29. package/src/input/commands/platform-sandbox-runtime.ts +1 -6
  30. package/src/input/commands/plugin-runtime.ts +2 -2
  31. package/src/input/commands/policy-dispatch.ts +21 -0
  32. package/src/input/commands/provider-accounts-runtime.ts +1 -1
  33. package/src/input/commands/qrcode-runtime.ts +3 -3
  34. package/src/input/commands/recall-review.ts +35 -0
  35. package/src/input/commands/remote-runtime.ts +3 -3
  36. package/src/input/commands/runtime-services.ts +54 -13
  37. package/src/input/commands/services-runtime.ts +1 -1
  38. package/src/input/commands/session-workflow.ts +16 -1
  39. package/src/input/commands/settings-sync-runtime.ts +1 -1
  40. package/src/input/commands/shell-core.ts +15 -7
  41. package/src/input/commands/skills-runtime.ts +2 -8
  42. package/src/input/commands/subscription-runtime.ts +2 -2
  43. package/src/input/commands/test-runtime.ts +277 -0
  44. package/src/input/commands/websearch-runtime.ts +101 -0
  45. package/src/input/commands/work-plan-runtime.ts +36 -10
  46. package/src/input/commands/workstream-runtime.ts +338 -0
  47. package/src/input/commands.ts +12 -0
  48. package/src/input/config-modal-types.ts +161 -0
  49. package/src/input/config-modal.ts +377 -0
  50. package/src/input/feed-context-factory.ts +7 -8
  51. package/src/input/handler-command-route.ts +27 -17
  52. package/src/input/handler-feed-routes.ts +61 -23
  53. package/src/input/handler-feed.ts +47 -23
  54. package/src/input/handler-interactions.ts +1 -3
  55. package/src/input/handler-modal-routes.ts +65 -0
  56. package/src/input/handler-modal-stack.ts +3 -5
  57. package/src/input/handler-modal-token-routes.ts +16 -49
  58. package/src/input/handler-picker-routes.ts +11 -94
  59. package/src/input/handler-shortcuts.ts +24 -14
  60. package/src/input/handler-types.ts +2 -6
  61. package/src/input/handler-ui-state.ts +10 -22
  62. package/src/input/handler.ts +6 -28
  63. package/src/input/keybindings.ts +22 -8
  64. package/src/input/model-picker-types.ts +24 -6
  65. package/src/input/model-picker.ts +58 -0
  66. package/src/input/panel-integration-actions.ts +9 -170
  67. package/src/input/settings-modal-data.ts +95 -0
  68. package/src/input/settings-modal-mutations.ts +6 -4
  69. package/src/main.ts +24 -27
  70. package/src/panels/agent-inspector-shared.ts +2 -1
  71. package/src/panels/base-panel.ts +22 -1
  72. package/src/panels/builtin/agent.ts +21 -107
  73. package/src/panels/builtin/development.ts +15 -61
  74. package/src/panels/builtin/knowledge.ts +8 -32
  75. package/src/panels/builtin/operations.ts +84 -428
  76. package/src/panels/builtin/session.ts +21 -112
  77. package/src/panels/builtin/shared.ts +9 -43
  78. package/src/panels/builtin-modals.ts +218 -0
  79. package/src/panels/builtin-panels.ts +5 -0
  80. package/src/panels/cost-tracker-panel.ts +36 -10
  81. package/src/panels/diff-panel.ts +12 -43
  82. package/src/panels/eval-registry.ts +60 -0
  83. package/src/panels/fleet-panel.ts +800 -0
  84. package/src/panels/fleet-read-model.ts +477 -0
  85. package/src/panels/fleet-steer.ts +92 -0
  86. package/src/panels/fleet-stop.ts +125 -0
  87. package/src/panels/fleet-tabs.ts +230 -0
  88. package/src/panels/fleet-transcript.ts +356 -0
  89. package/src/panels/index.ts +7 -31
  90. package/src/panels/modals/hooks-modal.ts +187 -0
  91. package/src/panels/modals/keybindings-modal.ts +151 -0
  92. package/src/panels/modals/knowledge-modal.ts +158 -0
  93. package/src/panels/modals/local-auth-modal.ts +132 -0
  94. package/src/panels/modals/marketplace-modal.ts +218 -0
  95. package/src/panels/modals/memory-modal.ts +180 -0
  96. package/src/panels/modals/modal-surface-helpers.ts +54 -0
  97. package/src/panels/modals/modal-theme.ts +36 -0
  98. package/src/panels/modals/pairing-modal.ts +144 -0
  99. package/src/panels/modals/planning-modal.ts +282 -0
  100. package/src/panels/modals/plugins-modal.ts +174 -0
  101. package/src/panels/modals/policy-modal.ts +256 -0
  102. package/src/panels/modals/provider-health-modal.ts +136 -0
  103. package/src/panels/modals/remote-modal.ts +115 -0
  104. package/src/panels/modals/sandbox-modal.ts +150 -0
  105. package/src/panels/modals/security-modal.ts +217 -0
  106. package/src/panels/modals/services-modal.ts +179 -0
  107. package/src/panels/modals/settings-sync-modal.ts +142 -0
  108. package/src/panels/modals/skills-modal.ts +189 -0
  109. package/src/panels/modals/subscription-modal.ts +172 -0
  110. package/src/panels/modals/work-plan-modal.ts +149 -0
  111. package/src/panels/panel-confirm-overlay.ts +72 -0
  112. package/src/panels/panel-manager.ts +108 -5
  113. package/src/panels/project-planning-answer-actions.ts +4 -2
  114. package/src/panels/skills-panel.ts +7 -51
  115. package/src/panels/types.ts +13 -0
  116. package/src/permissions/hunk-selection.ts +179 -0
  117. package/src/permissions/prompt.ts +60 -4
  118. package/src/renderer/compaction-history-modal.ts +19 -3
  119. package/src/renderer/compaction-preview.ts +13 -2
  120. package/src/renderer/compaction-quality.ts +100 -0
  121. package/src/renderer/config-modal.ts +81 -0
  122. package/src/renderer/conversation-overlays.ts +10 -17
  123. package/src/renderer/fleet-tab-strip.ts +56 -0
  124. package/src/renderer/footer-tips.ts +10 -2
  125. package/src/renderer/git-status.ts +40 -0
  126. package/src/renderer/help-overlay.ts +2 -2
  127. package/src/renderer/model-workspace.ts +44 -1
  128. package/src/renderer/panel-workspace-bar.ts +8 -2
  129. package/src/renderer/turn-injection.ts +114 -0
  130. package/src/runtime/bootstrap-command-context.ts +21 -1
  131. package/src/runtime/bootstrap-command-parts.ts +34 -7
  132. package/src/runtime/bootstrap-core.ts +14 -4
  133. package/src/runtime/bootstrap-shell.ts +29 -16
  134. package/src/runtime/bootstrap.ts +11 -17
  135. package/src/runtime/code-index-services.ts +112 -0
  136. package/src/runtime/orchestrator-core-services.ts +49 -0
  137. package/src/runtime/process-lifecycle.ts +3 -1
  138. package/src/runtime/services.ts +91 -43
  139. package/src/runtime/ui-services.ts +8 -0
  140. package/src/runtime/workstream-services.ts +195 -0
  141. package/src/shell/blocking-input.ts +22 -1
  142. package/src/shell/ui-openers.ts +129 -17
  143. package/src/utils/format-duration.ts +8 -18
  144. package/src/utils/splash-lines.ts +1 -1
  145. package/src/version.ts +1 -1
  146. package/src/panels/agent-inspector-panel.ts +0 -786
  147. package/src/panels/approval-panel.ts +0 -252
  148. package/src/panels/automation-control-panel.ts +0 -479
  149. package/src/panels/cockpit-panel.ts +0 -481
  150. package/src/panels/cockpit-read-model.ts +0 -238
  151. package/src/panels/communication-panel.ts +0 -256
  152. package/src/panels/control-plane-panel.ts +0 -470
  153. package/src/panels/debug-panel.ts +0 -615
  154. package/src/panels/docs-panel.ts +0 -384
  155. package/src/panels/eval-panel.ts +0 -627
  156. package/src/panels/file-explorer-panel.ts +0 -673
  157. package/src/panels/file-preview-panel.ts +0 -517
  158. package/src/panels/hooks-panel.ts +0 -401
  159. package/src/panels/incident-review-panel.ts +0 -406
  160. package/src/panels/intelligence-panel.ts +0 -383
  161. package/src/panels/knowledge-graph-panel.ts +0 -515
  162. package/src/panels/marketplace-panel.ts +0 -399
  163. package/src/panels/memory-panel.ts +0 -558
  164. package/src/panels/ops-control-panel.ts +0 -329
  165. package/src/panels/ops-strategy-panel.ts +0 -321
  166. package/src/panels/orchestration-panel.ts +0 -465
  167. package/src/panels/panel-list-panel.ts +0 -566
  168. package/src/panels/plan-dashboard-panel.ts +0 -707
  169. package/src/panels/policy-panel.ts +0 -517
  170. package/src/panels/project-planning-panel.ts +0 -731
  171. package/src/panels/provider-health-panel.ts +0 -678
  172. package/src/panels/provider-health-tracker.ts +0 -310
  173. package/src/panels/provider-health-views.ts +0 -567
  174. package/src/panels/qr-panel.ts +0 -280
  175. package/src/panels/remote-panel.ts +0 -534
  176. package/src/panels/routes-panel.ts +0 -241
  177. package/src/panels/sandbox-panel.ts +0 -456
  178. package/src/panels/security-panel.ts +0 -447
  179. package/src/panels/services-panel.ts +0 -329
  180. package/src/panels/session-browser-panel.ts +0 -496
  181. package/src/panels/settings-sync-panel.ts +0 -398
  182. package/src/panels/subscription-panel.ts +0 -342
  183. package/src/panels/symbol-outline-panel.ts +0 -619
  184. package/src/panels/system-messages-panel.ts +0 -364
  185. package/src/panels/tasks-panel.ts +0 -608
  186. package/src/panels/thinking-panel.ts +0 -333
  187. package/src/panels/tool-inspector-panel.ts +0 -537
  188. package/src/panels/work-plan-panel.ts +0 -540
  189. package/src/panels/worktree-panel.ts +0 -360
  190. package/src/panels/wrfc-panel.ts +0 -790
  191. package/src/renderer/agent-detail-modal.ts +0 -466
  192. package/src/renderer/live-tail-modal.ts +0 -156
  193. package/src/renderer/process-modal.ts +0 -671
  194. package/src/renderer/qr-renderer.ts +0 -120
@@ -1,406 +0,0 @@
1
- import { logger } from '@pellux/goodvibes-sdk/platform/utils';
2
- import type { Line } from '../types/grid.ts';
3
- import { fitDisplay, truncateDisplay } from '../utils/terminal-width.ts';
4
- import { formatShortDuration } from '../utils/format-duration.ts';
5
- import type {
6
- ForensicsRegistry,
7
- FailureReport,
8
- ForensicsBundle,
9
- CausalChainEntry,
10
- PhaseTimingEntry,
11
- ForensicsJumpLink,
12
- } from '@/runtime/index.ts';
13
- import { ScrollableListPanel } from './scrollable-list-panel.ts';
14
- import {
15
- buildBodyText,
16
- buildEmptyState,
17
- buildGuidanceLine,
18
- buildKeyValueLine,
19
- buildPanelLine,
20
- buildPanelWorkspace,
21
- buildStatusPill,
22
- DEFAULT_PANEL_PALETTE,
23
- type PanelPalette,
24
- } from './polish.ts';
25
- import { type ConfirmState, handleConfirmInput, renderConfirmLines } from './confirm-state.ts';
26
- import type { PanelIntegrationContext } from './types.ts';
27
-
28
- // Base chrome only — title band, state colors, and text tokens all come
29
- // straight from DEFAULT_PANEL_PALETTE (WO-002).
30
- const C = DEFAULT_PANEL_PALETTE;
31
-
32
- function classificationColor(value: string): string {
33
- switch (value) {
34
- case 'cancelled':
35
- return C.dim;
36
- case 'max_tokens':
37
- case 'unknown':
38
- return C.warn;
39
- default:
40
- return C.bad;
41
- }
42
- }
43
-
44
- function fmtTime(ts: number): string {
45
- const d = new Date(ts);
46
- const hh = String(d.getHours()).padStart(2, '0');
47
- const mm = String(d.getMinutes()).padStart(2, '0');
48
- const ss = String(d.getSeconds()).padStart(2, '0');
49
- return `${hh}:${mm}:${ss}`;
50
- }
51
-
52
- /**
53
- * Default workspace-relative destination for a panel-triggered `x` export.
54
- * Left relative (not resolved against home/cwd here) so the `/incident
55
- * export` command's own `shellPaths.resolveWorkspacePath` — the composition
56
- * root's owned path resolver — decides the final absolute location.
57
- */
58
- function buildIncidentExportPath(id: string): string {
59
- return `goodvibes-exports/incident-${id}-${Date.now()}.json`;
60
- }
61
-
62
- export class IncidentReviewPanel extends ScrollableListPanel<FailureReport> {
63
- private readonly registry?: ForensicsRegistry;
64
- private readonly unsub: (() => void) | null;
65
-
66
- // Inline confirm for the (non-destructive but deliberate) capture action.
67
- private _confirm: ConfirmState<string> | null = null;
68
-
69
- // Pending cross-panel actions resolved via handlePanelIntegrationAction,
70
- // which is the only place `executeCommand`/`panelManager` are available.
71
- private _pendingExport: string | null = null;
72
- private _pendingCapture: string | null = null;
73
- private _pendingJump: ForensicsJumpLink | null = null;
74
-
75
- // Cache buildBundle(id) per selected report id instead of recomputing it
76
- // on every render call; invalidated whenever the registry changes.
77
- private _bundleCache: { readonly id: string; readonly bundle: ForensicsBundle | undefined } | null = null;
78
-
79
- public constructor(registry?: ForensicsRegistry) {
80
- super('incident', 'Incident Review', '◪', 'incidents-diagnostics');
81
- this.showSelectionGutter = true; // I5: non-color selection affordance
82
- this.filterEnabled = true;
83
- this.filterLabel = 'Filter incidents';
84
- this.registry = registry;
85
- this.unsub = registry
86
- ? registry.subscribe(() => {
87
- this._bundleCache = null;
88
- this.markDirty();
89
- })
90
- : null;
91
- }
92
-
93
- public override onDestroy(): void {
94
- this.unsub?.();
95
- }
96
-
97
- protected override getPalette(): PanelPalette {
98
- return C;
99
- }
100
-
101
- protected getItems(): readonly FailureReport[] {
102
- return this.registry?.getAll() ?? [];
103
- }
104
-
105
- protected override filterMatches(report: FailureReport, q: string): boolean {
106
- return report.classification.toLowerCase().includes(q)
107
- || report.id.toLowerCase().includes(q)
108
- || (report.summary ?? '').toLowerCase().includes(q);
109
- }
110
-
111
- protected renderItem(report: FailureReport, index: number, selected: boolean, width: number): Line {
112
- const bg = selected ? C.selectBg : undefined;
113
- return buildPanelLine(width, [
114
- [' ', C.label, bg],
115
- [fitDisplay(report.id, 9), C.dim, bg],
116
- [fitDisplay(report.classification, 20), classificationColor(report.classification), bg],
117
- [truncateDisplay(report.summary, Math.max(0, width - 31)), C.value, bg],
118
- ]);
119
- }
120
-
121
- protected override getEmptyStateMessage(): string {
122
- return ' No incidents recorded yet.';
123
- }
124
-
125
- protected override getEmptyStateActions(): Array<{ command: string; summary: string }> {
126
- return [
127
- { command: '/incident latest', summary: 'inspect the latest report once one exists' },
128
- { command: '/recall capture incident latest', summary: 'promote incident evidence into project knowledge' },
129
- ];
130
- }
131
-
132
- private getBundle(id: string): ForensicsBundle | undefined {
133
- if (!this.registry) return undefined;
134
- if (!this._bundleCache || this._bundleCache.id !== id) {
135
- this._bundleCache = { id, bundle: this.registry.buildBundle(id) };
136
- }
137
- return this._bundleCache.bundle;
138
- }
139
-
140
- private _pushPhaseTimingLine(lines: Line[], pt: PhaseTimingEntry, width: number): void {
141
- const statusChar = pt.success ? '✓' : '✕';
142
- const statusColor = pt.success ? C.good : C.bad;
143
- const dur = formatShortDuration(pt.durationMs);
144
- const phaseLabel = fitDisplay(pt.phase, 14);
145
- const errPart = pt.error ? ` ${truncateDisplay(pt.error, Math.max(0, width - 34))}` : '';
146
- lines.push(buildPanelLine(width, [
147
- [' ', C.dim],
148
- [`${statusChar} `, statusColor],
149
- [phaseLabel, C.value],
150
- [dur.padStart(8, ' '), C.label],
151
- [errPart, C.bad],
152
- ]));
153
- }
154
-
155
- private _pushCausalLine(lines: Line[], entry: CausalChainEntry, width: number): void {
156
- const prefix = entry.isRootCause ? ' * ' : ' - ';
157
- const color = entry.isRootCause ? C.bad : C.dim;
158
- const timeStr = fmtTime(entry.ts);
159
- const descMax = Math.max(0, width - prefix.length - 9);
160
- lines.push(buildPanelLine(width, [
161
- [prefix, color],
162
- [`${timeStr} `, C.label],
163
- [truncateDisplay(entry.description, descMax), color],
164
- ]));
165
- }
166
-
167
- private _pushJumpLinkLine(lines: Line[], link: ForensicsJumpLink, width: number): void {
168
- const kindTag = link.kind === 'panel' ? '[panel]' : '[cmd] ';
169
- lines.push(buildPanelLine(width, [
170
- [' ', C.dim],
171
- [kindTag, C.label],
172
- [` ${link.label}`, C.info],
173
- [link.args ? ` (${link.args})` : '', C.dim],
174
- ]));
175
- }
176
-
177
- /**
178
- * Decision-making for every key: navigation is delegated to
179
- * ScrollableListPanel, while `x`/`c`/`j` and any in-progress confirm are
180
- * resolved here and staged as a pending action. The actual cross-panel
181
- * side effect (executeCommand / panelManager.open) requires the
182
- * PanelIntegrationContext, which is only available in
183
- * `handlePanelIntegrationAction` — invoked immediately after this method
184
- * returns `true` for the same key.
185
- */
186
- public override handleInput(key: string): boolean {
187
- if (this.lastError !== null) this.clearError();
188
-
189
- if (this._confirm) {
190
- const outcome = handleConfirmInput(this._confirm, key);
191
- if (outcome === 'confirmed') {
192
- this._pendingCapture = this._confirm.subject;
193
- this._confirm = null;
194
- this.markDirty();
195
- return true;
196
- }
197
- if (outcome === 'cancelled') {
198
- this._confirm = null;
199
- this.markDirty();
200
- return true;
201
- }
202
- return true; // absorbed — keep the confirm dialog pending
203
- }
204
-
205
- // Actions must target the filtered view the list highlights — raw
206
- // getItems() desyncs selectedIndex under an applied '/' filter.
207
- const reports = this.getVisibleItems();
208
- if (reports.length > 0 && (key === 'x' || key === 'c' || key === 'j')) {
209
- this.clampSelection();
210
- const selected = this.getSelectedItem();
211
- if (selected) {
212
- if (key === 'x') {
213
- this._pendingExport = selected.id;
214
- return true;
215
- }
216
- if (key === 'c') {
217
- this._confirm = { subject: selected.id, label: `incident ${selected.id}`, verb: 'Capture' };
218
- this.markDirty();
219
- return true;
220
- }
221
- if (key === 'j') {
222
- const link = selected.jumpLinks[0];
223
- if (link) {
224
- this._pendingJump = link;
225
- return true;
226
- }
227
- return false;
228
- }
229
- }
230
- }
231
-
232
- return super.handleInput(key);
233
- }
234
-
235
- public handlePanelIntegrationAction(_key: string, ctx: PanelIntegrationContext): boolean {
236
- if (this._pendingExport) {
237
- const id = this._pendingExport;
238
- this._pendingExport = null;
239
- const path = buildIncidentExportPath(id);
240
- void ctx.executeCommand?.('incident', ['export', id, path]).catch((err) => {
241
- logger.debug('incident export dispatch failed', { err });
242
- });
243
- return true;
244
- }
245
- if (this._pendingCapture) {
246
- const id = this._pendingCapture;
247
- this._pendingCapture = null;
248
- void ctx.executeCommand?.('incident', ['capture', id]).catch((err) => {
249
- logger.debug('incident capture dispatch failed', { err });
250
- });
251
- return true;
252
- }
253
- if (this._pendingJump) {
254
- const link = this._pendingJump;
255
- this._pendingJump = null;
256
- if (link.kind === 'panel') {
257
- ctx.panelManager.open(link.target);
258
- } else {
259
- void ctx.executeCommand?.(link.target, link.args ? link.args.split(/\s+/).filter(Boolean) : []).catch((err) => {
260
- logger.debug('jump link dispatch failed', { err });
261
- });
262
- }
263
- return true;
264
- }
265
- return false;
266
- }
267
-
268
- public render(width: number, height: number): Line[] {
269
- if (this._confirm) {
270
- return buildPanelWorkspace(width, height, {
271
- title: 'Incident Review Workspace',
272
- sections: [{ title: 'Confirmation', lines: renderConfirmLines(width, this._confirm) }],
273
- palette: C,
274
- });
275
- }
276
-
277
- const intro = 'Failure bundles with causal chains, phase timings, jump links, and exportable review evidence.';
278
-
279
- if (!this.registry) {
280
- return buildPanelWorkspace(width, height, {
281
- title: 'Incident Review Workspace',
282
- intro,
283
- sections: [{
284
- lines: buildEmptyState(
285
- width,
286
- ' Incident registry not configured for this session.',
287
- 'This runtime was not wired with a forensics registry at bootstrap, so no incident data is available.',
288
- [],
289
- C,
290
- ),
291
- }],
292
- palette: C,
293
- });
294
- }
295
-
296
- const reports = this.getItems();
297
- if (reports.length === 0) {
298
- return this.renderList(width, height, { title: 'Incident Review Workspace' });
299
- }
300
-
301
- this.clampSelection();
302
- // Header/detail must describe the row the (possibly filtered) list
303
- // highlights; a filter matching nothing leaves no selection at all.
304
- const visible = this.getVisibleItems();
305
- const selected = this.getSelectedItem();
306
- if (!selected) {
307
- return this.renderList(width, height, { title: 'Incident Review Workspace' });
308
- }
309
- const bundle = this.getBundle(selected.id);
310
-
311
- const headerLines: Line[] = [
312
- buildKeyValueLine(width, [
313
- { label: 'incidents', value: String(reports.length), valueColor: C.value },
314
- { label: 'selected', value: `${this.selectedIndex + 1}/${visible.length}`, valueColor: C.info },
315
- { label: 'classification', value: selected.classification, valueColor: classificationColor(selected.classification) },
316
- ], C),
317
- ];
318
-
319
- const footerLines: Line[] = [];
320
- if (bundle) {
321
- footerLines.push(buildKeyValueLine(width, [
322
- { label: 'id', value: selected.id, valueColor: C.dim },
323
- { label: 'trace', value: selected.traceId, valueColor: C.dim },
324
- ], C));
325
- footerLines.push(...buildBodyText(width, `Root cause: ${bundle.evidence.rootCause ?? 'n/a'}`, C, C.value));
326
- footerLines.push(buildKeyValueLine(width, [
327
- { label: 'Permissions denied', value: String(bundle.evidence.deniedPermissionCount), valueColor: bundle.evidence.deniedPermissionCount > 0 ? C.warn : C.dim },
328
- { label: 'Budget breaches', value: String(bundle.evidence.budgetBreachCount), valueColor: bundle.evidence.budgetBreachCount > 0 ? C.warn : C.dim },
329
- { label: 'Replay mismatches', value: String(bundle.replay.mismatchCount), valueColor: bundle.replay.mismatchCount > 0 ? C.bad : C.dim },
330
- ], C));
331
- footerLines.push(buildPanelLine(width, [
332
- [' Related IDs: ', C.label],
333
- [truncateDisplay(`turn=${bundle.evidence.relatedIds.turnId ?? 'n/a'} task=${bundle.evidence.relatedIds.taskId ?? 'n/a'} agent=${bundle.evidence.relatedIds.agentId ?? 'n/a'}`, Math.max(0, width - 14)), C.info],
334
- ]));
335
- if (selected.phaseTimings.length > 0) {
336
- footerLines.push(buildPanelLine(width, [[' Phase Timings:', C.label]]));
337
- for (const pt of selected.phaseTimings) this._pushPhaseTimingLine(footerLines, pt, width);
338
- }
339
- if (selected.causalChain.length > 0) {
340
- footerLines.push(buildPanelLine(width, [[' Causal Chain:', C.label]]));
341
- for (const entry of selected.causalChain) this._pushCausalLine(footerLines, entry, width);
342
- }
343
- const denied = selected.permissionEvidence.find((entry) => entry.approved === false);
344
- if (denied) {
345
- footerLines.push(buildPanelLine(width, [
346
- [' Permission: ', C.label],
347
- [truncateDisplay(`${denied.tool} denied${denied.riskLevel ? ` (${denied.riskLevel})` : ''}${denied.summary ? ` - ${denied.summary}` : ''}`, Math.max(0, width - 14)), C.warn],
348
- ]));
349
- }
350
- if (bundle.replay.relatedMismatches.length > 0) {
351
- const mismatch = bundle.replay.relatedMismatches[0]!;
352
- const ownerBreakdown = Object.entries(bundle.replay.mismatchBreakdown.byOwnerDomain)
353
- .filter(([, count]) => count > 0)
354
- .slice(0, 3)
355
- .map(([domain, count]) => `${domain}:${count}`)
356
- .join(', ');
357
- const replayDetail = ownerBreakdown.length > 0
358
- ? `Replay link: ${mismatch.kind}${mismatch.ownerDomain ? `/${mismatch.ownerDomain}` : ''} - ${mismatch.description} Replay owners: ${ownerBreakdown}`
359
- : `Replay link: ${mismatch.kind}${mismatch.ownerDomain ? `/${mismatch.ownerDomain}` : ''} - ${mismatch.description}`;
360
- footerLines.push(buildPanelLine(width, [
361
- [' ', C.label],
362
- ...buildStatusPill('bad', truncateDisplay(replayDetail, Math.max(0, width - 2))),
363
- ]));
364
- } else {
365
- const ownerBreakdown = Object.entries(bundle.replay.mismatchBreakdown.byOwnerDomain)
366
- .filter(([, count]) => count > 0)
367
- .slice(0, 3)
368
- .map(([domain, count]) => `${domain}:${count}`)
369
- .join(', ');
370
- if (ownerBreakdown.length > 0) {
371
- footerLines.push(buildPanelLine(width, [
372
- [' Replay owners: ', C.label],
373
- [truncateDisplay(ownerBreakdown, Math.max(0, width - 17)), C.info],
374
- ]));
375
- }
376
- }
377
- if (selected.jumpLinks.length > 0) {
378
- footerLines.push(buildPanelLine(width, [[' Jump Links:', C.label]]));
379
- for (const link of selected.jumpLinks) this._pushJumpLinkLine(footerLines, link, width);
380
- }
381
- }
382
- footerLines.push(buildGuidanceLine(width, '/security', 'open the broader trust and incident posture control room', C));
383
-
384
- const hints = this.filterActive
385
- ? [
386
- { keys: 'type', label: 'filter incidents' },
387
- { keys: 'Enter', label: 'apply' },
388
- { keys: 'Esc', label: 'clear' },
389
- ]
390
- : [
391
- { keys: '↑/↓', label: 'select incident' },
392
- { keys: 'Home/End', label: 'jump' },
393
- { keys: '/', label: 'filter' },
394
- { keys: 'x', label: 'export' },
395
- { keys: 'c', label: 'capture' },
396
- ...(selected.jumpLinks.length > 0 ? [{ keys: 'j', label: 'follow link' }] : []),
397
- ];
398
-
399
- return this.renderList(width, height, {
400
- title: 'Incident Review Workspace',
401
- header: headerLines,
402
- footer: footerLines,
403
- hints,
404
- });
405
- }
406
- }