@pellux/goodvibes-tui 0.18.19 → 0.18.23
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 +170 -0
- package/README.md +1 -1
- package/package.json +1 -1
- package/src/core/conversation-rendering.ts +20 -6
- package/src/input/commands/session.ts +0 -1
- package/src/input/feed-context-factory.ts +236 -0
- package/src/input/handler-feed-routes.ts +10 -0
- package/src/input/handler-feed.ts +44 -6
- package/src/input/handler-shortcuts.ts +138 -125
- package/src/input/handler.ts +121 -119
- package/src/input/keybindings.ts +30 -0
- package/src/panels/approval-panel.ts +54 -74
- package/src/panels/automation-control-panel.ts +119 -161
- package/src/panels/base-panel.ts +71 -0
- package/src/panels/communication-panel.ts +68 -107
- package/src/panels/confirm-state.ts +61 -0
- package/src/panels/control-plane-panel.ts +116 -172
- package/src/panels/git-panel.ts +9 -0
- package/src/panels/hooks-panel.ts +101 -138
- package/src/panels/incident-review-panel.ts +55 -107
- package/src/panels/knowledge-panel.ts +63 -14
- package/src/panels/local-auth-panel.ts +76 -93
- package/src/panels/marketplace-panel.ts +19 -12
- package/src/panels/mcp-panel.ts +108 -155
- package/src/panels/ops-control-panel.ts +50 -85
- package/src/panels/panel-manager.ts +22 -2
- package/src/panels/plugins-panel.ts +36 -60
- package/src/panels/routes-panel.ts +89 -141
- package/src/panels/scrollable-list-panel.ts +71 -16
- package/src/panels/security-panel.ts +101 -137
- package/src/panels/services-panel.ts +58 -102
- package/src/panels/settings-sync-panel.ts +76 -122
- package/src/panels/skills-panel.ts +44 -0
- package/src/panels/subscription-panel.ts +69 -80
- package/src/panels/tasks-panel.ts +129 -179
- package/src/panels/watchers-panel.ts +88 -137
- package/src/renderer/buffer.ts +11 -0
- package/src/renderer/diff.ts +8 -0
- package/src/renderer/help-overlay.ts +37 -28
- package/src/renderer/markdown.ts +3 -145
- package/src/renderer/status-token.ts +71 -0
- package/src/version.ts +1 -1
package/src/panels/git-panel.ts
CHANGED
|
@@ -326,13 +326,18 @@ export class GitPanel extends BasePanel {
|
|
|
326
326
|
const item = this.items[this.selectedIndex];
|
|
327
327
|
if (!item || item.kind !== 'file') return;
|
|
328
328
|
|
|
329
|
+
// I3: show base-class spinner while awaiting diff
|
|
330
|
+
this.startLoading('Loading diff...');
|
|
331
|
+
this.markDirty();
|
|
329
332
|
try {
|
|
330
333
|
const git = new GitService(this.workingDirectory);
|
|
331
334
|
const raw = await git.diffFile(item.entry.path, item.entry.staged);
|
|
335
|
+
this.stopLoading();
|
|
332
336
|
this.expandedDiff = raw ? raw.split('\n') : ['(no diff available)'];
|
|
333
337
|
this.scrollOffset = 0;
|
|
334
338
|
this.markDirty();
|
|
335
339
|
} catch (err) {
|
|
340
|
+
this.stopLoading();
|
|
336
341
|
this.expandedDiff = [`Error: ${summarizeError(err)}`];
|
|
337
342
|
this.scrollOffset = 0;
|
|
338
343
|
this.markDirty();
|
|
@@ -350,6 +355,10 @@ export class GitPanel extends BasePanel {
|
|
|
350
355
|
if (this.error) {
|
|
351
356
|
return this.renderMessage(width, height, `Git error: ${this.error}`, C.unstaged);
|
|
352
357
|
}
|
|
358
|
+
// I3: spinner during openDiff() async fetch
|
|
359
|
+
if (this.loadingState === 'loading') {
|
|
360
|
+
return this.renderMessage(width, height, 'Loading diff...', C.branch);
|
|
361
|
+
}
|
|
353
362
|
if (this.expandedDiff !== null) {
|
|
354
363
|
return this.renderDiff(width, height);
|
|
355
364
|
}
|
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
import type { Line } from '../types/grid.ts';
|
|
2
|
-
import {
|
|
3
|
-
import { BasePanel } from './base-panel.ts';
|
|
2
|
+
import { ScrollableListPanel } from './scrollable-list-panel.ts';
|
|
4
3
|
import { listHookPointContracts } from '@pellux/goodvibes-sdk/platform/hooks/index';
|
|
5
4
|
import type { HookDispatcher } from '@pellux/goodvibes-sdk/platform/hooks/dispatcher';
|
|
6
5
|
import type { HookPointContract } from '@pellux/goodvibes-sdk/platform/hooks/contracts';
|
|
@@ -10,12 +9,8 @@ import type { HookChain, HookDefinition } from '@pellux/goodvibes-sdk/platform/h
|
|
|
10
9
|
import type { HookWorkbench } from '@pellux/goodvibes-sdk/platform/hooks/workbench';
|
|
11
10
|
import { truncateDisplay } from '../utils/terminal-width.ts';
|
|
12
11
|
import {
|
|
13
|
-
buildEmptyState,
|
|
14
12
|
buildPanelLine,
|
|
15
|
-
buildPanelWorkspace,
|
|
16
13
|
DEFAULT_PANEL_PALETTE,
|
|
17
|
-
resolvePrimaryScrollableSection,
|
|
18
|
-
type PanelWorkspaceSection,
|
|
19
14
|
} from './polish.ts';
|
|
20
15
|
|
|
21
16
|
const C = {
|
|
@@ -59,9 +54,9 @@ function createDefaultDataSource(
|
|
|
59
54
|
};
|
|
60
55
|
}
|
|
61
56
|
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
57
|
+
type HookEntry = { pattern: string; hook: HookDefinition };
|
|
58
|
+
|
|
59
|
+
export class HooksPanel extends ScrollableListPanel<HookEntry> {
|
|
65
60
|
private readonly dataSource: HooksPanelDataSource;
|
|
66
61
|
|
|
67
62
|
public constructor(
|
|
@@ -74,31 +69,42 @@ export class HooksPanel extends BasePanel {
|
|
|
74
69
|
this.dataSource = dataSource;
|
|
75
70
|
}
|
|
76
71
|
|
|
72
|
+
protected override getPalette() { return C; }
|
|
73
|
+
protected override getEmptyStateMessage() { return ' No hooks are currently registered.'; }
|
|
74
|
+
protected override getEmptyStateActions() {
|
|
75
|
+
return [
|
|
76
|
+
{ command: '/hooks', summary: 'review hook contracts and managed authoring actions' },
|
|
77
|
+
{ command: '/settings', summary: 'review hook/runtime behavior in the settings surface' },
|
|
78
|
+
];
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
protected getItems(): readonly HookEntry[] {
|
|
82
|
+
return this.dataSource.listHooks();
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
protected renderItem(entry: HookEntry, index: number, selected: boolean, width: number): Line {
|
|
86
|
+
const bg = selected ? C.selectBg : undefined;
|
|
87
|
+
return buildPanelLine(width, [
|
|
88
|
+
[' ', C.label, bg],
|
|
89
|
+
[truncateDisplay(entry.hook.name ?? '(unnamed)', 20).padEnd(20), C.value, bg],
|
|
90
|
+
[` ${truncateDisplay(entry.pattern, 28).padEnd(28)}`, C.info, bg],
|
|
91
|
+
[` ${(entry.hook.enabled === false ? 'DISABLED' : 'ENABLED').padEnd(8)}`, entry.hook.enabled === false ? C.warn : C.ok, bg],
|
|
92
|
+
[` ${entry.hook.type}`, C.dim, bg],
|
|
93
|
+
]);
|
|
94
|
+
}
|
|
95
|
+
|
|
77
96
|
public handleInput(key: string): boolean {
|
|
78
|
-
const entries = this.dataSource.listHooks();
|
|
79
97
|
if (key === 'r') {
|
|
80
98
|
this.markDirty();
|
|
81
99
|
return true;
|
|
82
100
|
}
|
|
83
|
-
|
|
84
|
-
if (key === 'up' || key === 'k') {
|
|
85
|
-
this.selectedIndex = Math.max(0, this.selectedIndex - 1);
|
|
86
|
-
this.markDirty();
|
|
87
|
-
return true;
|
|
88
|
-
}
|
|
89
|
-
if (key === 'down' || key === 'j') {
|
|
90
|
-
this.selectedIndex = Math.min(entries.length - 1, this.selectedIndex + 1);
|
|
91
|
-
this.markDirty();
|
|
92
|
-
return true;
|
|
93
|
-
}
|
|
94
|
-
return false;
|
|
101
|
+
return super.handleInput(key);
|
|
95
102
|
}
|
|
96
103
|
|
|
97
104
|
public render(width: number, height: number): Line[] {
|
|
98
|
-
this.
|
|
99
|
-
const intro = 'Hook contracts, active registrations, managed authoring, recent runtime activity, and simulation matches.';
|
|
100
|
-
const contracts = this.dataSource.listContracts();
|
|
105
|
+
this.clampSelection();
|
|
101
106
|
const hooks = this.dataSource.listHooks();
|
|
107
|
+
const contracts = this.dataSource.listContracts();
|
|
102
108
|
const chains = this.dataSource.listChains();
|
|
103
109
|
const recentActivity = this.dataSource.listRecentActivity(3);
|
|
104
110
|
const workbench = this.dataSource.getWorkbench();
|
|
@@ -106,93 +112,48 @@ export class HooksPanel extends BasePanel {
|
|
|
106
112
|
const managedChains = workbench.listManagedChains();
|
|
107
113
|
const recentAuthoring = workbench.listRecentActions(3);
|
|
108
114
|
const lastSimulation = workbench.getLastSimulation();
|
|
115
|
+
const intro = 'Hook contracts, active registrations, managed authoring, recent runtime activity, and simulation matches.';
|
|
109
116
|
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
...buildEmptyState(
|
|
113
|
-
width,
|
|
114
|
-
' No hooks are currently registered.',
|
|
115
|
-
'Configure hooks.json or register hooks programmatically, then use this workspace to review contracts, activity, and managed authoring state.',
|
|
116
|
-
[
|
|
117
|
-
{ command: '/hooks', summary: 'review hook contracts and managed authoring actions' },
|
|
118
|
-
{ command: '/settings', summary: 'review hook/runtime behavior in the settings surface' },
|
|
119
|
-
],
|
|
120
|
-
C,
|
|
121
|
-
),
|
|
122
|
-
buildPanelLine(width, [
|
|
123
|
-
[' Contracts: ', C.label],
|
|
124
|
-
[String(contracts.length), C.value],
|
|
125
|
-
[' Chains: ', C.label],
|
|
126
|
-
[String(chains.length), C.value],
|
|
127
|
-
[' Managed: ', C.label],
|
|
128
|
-
[String(managedHooks.length), C.info],
|
|
129
|
-
]),
|
|
130
|
-
buildPanelLine(width, [
|
|
131
|
-
[' Hooks file: ', C.label],
|
|
132
|
-
[truncateDisplay(workbench.getHooksFilePath(), Math.max(0, width - 15)), C.dim],
|
|
133
|
-
]),
|
|
134
|
-
];
|
|
135
|
-
if (recentAuthoring.length > 0) {
|
|
136
|
-
emptyLines.push(buildPanelLine(width, [
|
|
137
|
-
[' Authoring: ', C.label],
|
|
138
|
-
[truncateDisplay(`${recentAuthoring[0]!.kind} ${recentAuthoring[0]!.target}`, Math.max(0, width - 14)), C.info],
|
|
139
|
-
]));
|
|
140
|
-
}
|
|
141
|
-
if (lastSimulation) {
|
|
142
|
-
emptyLines.push(buildPanelLine(width, [
|
|
143
|
-
[' Last Simulation: ', C.label],
|
|
144
|
-
[truncateDisplay(lastSimulation.eventPath, Math.max(0, width - 20)), C.value],
|
|
145
|
-
]));
|
|
146
|
-
}
|
|
147
|
-
const workspace = buildPanelWorkspace(width, height, {
|
|
148
|
-
title: 'Hooks Control Room',
|
|
149
|
-
intro,
|
|
150
|
-
sections: [{ lines: emptyLines }],
|
|
151
|
-
palette: C,
|
|
152
|
-
});
|
|
153
|
-
while (workspace.length < height) workspace.push(createEmptyLine(width));
|
|
154
|
-
return workspace;
|
|
155
|
-
}
|
|
117
|
+
const selected = hooks[this.selectedIndex];
|
|
118
|
+
const contract = selected ? contracts.find((c) => c.pattern === selected.pattern) : undefined;
|
|
156
119
|
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
const detailLines: Line[] = [
|
|
161
|
-
buildPanelLine(width, [
|
|
120
|
+
const detailLines: Line[] = [];
|
|
121
|
+
if (selected) {
|
|
122
|
+
detailLines.push(buildPanelLine(width, [
|
|
162
123
|
[' Hook: ', C.label],
|
|
163
124
|
[selected.hook.name ?? '(unnamed)', C.value],
|
|
164
125
|
[' Type: ', C.label],
|
|
165
126
|
[selected.hook.type, C.info],
|
|
166
127
|
[' Match: ', C.label],
|
|
167
128
|
[selected.hook.matcher ?? selected.hook.match, C.value],
|
|
168
|
-
])
|
|
169
|
-
buildPanelLine(width, [
|
|
129
|
+
]));
|
|
130
|
+
detailLines.push(buildPanelLine(width, [
|
|
170
131
|
[' Pattern: ', C.label],
|
|
171
132
|
[truncateDisplay(selected.pattern, Math.max(0, width - 12)), C.value],
|
|
172
|
-
])
|
|
173
|
-
|
|
174
|
-
|
|
133
|
+
]));
|
|
134
|
+
if (contract) {
|
|
135
|
+
detailLines.push(buildPanelLine(width, [
|
|
136
|
+
[' Contract: ', C.label],
|
|
137
|
+
[`${contract.authority} / ${contract.executionMode}`, C.info],
|
|
138
|
+
[' Policy: ', C.label],
|
|
139
|
+
[contract.failurePolicy, C.value],
|
|
140
|
+
]));
|
|
141
|
+
detailLines.push(buildPanelLine(width, [
|
|
142
|
+
[' Capabilities: ', C.label],
|
|
143
|
+
[`deny=${contract.canDeny ? 'yes' : 'no'} mutate=${contract.canMutateInput ? 'yes' : 'no'} inject=${contract.canInjectContext ? 'yes' : 'no'}`, C.dim],
|
|
144
|
+
]));
|
|
145
|
+
} else {
|
|
146
|
+
detailLines.push(buildPanelLine(width, [[' Contract: No exact contract registered for this pattern.', C.warn]]));
|
|
147
|
+
}
|
|
175
148
|
detailLines.push(buildPanelLine(width, [
|
|
176
|
-
['
|
|
177
|
-
[
|
|
178
|
-
[' Policy: ', C.label],
|
|
179
|
-
[contract.failurePolicy, C.value],
|
|
149
|
+
[' Summary: ', C.label],
|
|
150
|
+
[`hooks=${hooks.length} chains=${chains.length} contracts=${contracts.length} managed=${managedHooks.length}/${managedChains.length}`, C.dim],
|
|
180
151
|
]));
|
|
181
152
|
detailLines.push(buildPanelLine(width, [
|
|
182
|
-
['
|
|
183
|
-
[
|
|
153
|
+
[' Hooks file: ', C.label],
|
|
154
|
+
[truncateDisplay(workbench.getHooksFilePath(), Math.max(0, width - 15)), C.dim],
|
|
184
155
|
]));
|
|
185
|
-
} else {
|
|
186
|
-
detailLines.push(buildPanelLine(width, [[' Contract: No exact contract registered for this pattern.', C.warn]]));
|
|
187
156
|
}
|
|
188
|
-
detailLines.push(buildPanelLine(width, [
|
|
189
|
-
[' Summary: ', C.label],
|
|
190
|
-
[`hooks=${hooks.length} chains=${chains.length} contracts=${contracts.length} managed=${managedHooks.length}/${managedChains.length}`, C.dim],
|
|
191
|
-
]));
|
|
192
|
-
detailLines.push(buildPanelLine(width, [
|
|
193
|
-
[' Hooks file: ', C.label],
|
|
194
|
-
[truncateDisplay(workbench.getHooksFilePath(), Math.max(0, width - 15)), C.dim],
|
|
195
|
-
]));
|
|
196
157
|
|
|
197
158
|
const activityLines: Line[] = recentActivity.length === 0
|
|
198
159
|
? [buildPanelLine(width, [[' No hook activity recorded yet.', C.empty]])]
|
|
@@ -226,49 +187,51 @@ export class HooksPanel extends BasePanel {
|
|
|
226
187
|
[`hooks=${lastSimulation.matchedHooks.length} chains=${lastSimulation.matchedChains.length}`, C.dim],
|
|
227
188
|
]));
|
|
228
189
|
}
|
|
229
|
-
const selectedSection: PanelWorkspaceSection = { title: 'Selected Hook', lines: detailLines };
|
|
230
|
-
const activitySection: PanelWorkspaceSection = { title: 'Recent Activity', lines: activityLines };
|
|
231
|
-
const authoringSection: PanelWorkspaceSection = { title: 'Authoring', lines: authoringLines };
|
|
232
|
-
const resolvedHooksSection = resolvePrimaryScrollableSection(width, height, {
|
|
233
|
-
intro,
|
|
234
|
-
footerLines: [buildPanelLine(width, [[' Up/Down move r refresh /hooks for full contract listing', C.dim]])],
|
|
235
|
-
palette: C,
|
|
236
|
-
section: {
|
|
237
|
-
title: 'Hooks',
|
|
238
|
-
scrollableLines: hooks.map((entry, absolute) => {
|
|
239
|
-
const bg = absolute === this.selectedIndex ? C.selectBg : undefined;
|
|
240
|
-
return buildPanelLine(width, [
|
|
241
|
-
[' ', C.label, bg],
|
|
242
|
-
[truncateDisplay(entry.hook.name ?? '(unnamed)', 20).padEnd(20), C.value, bg],
|
|
243
|
-
[` ${truncateDisplay(entry.pattern, 28).padEnd(28)}`, C.info, bg],
|
|
244
|
-
[` ${(entry.hook.enabled === false ? 'DISABLED' : 'ENABLED').padEnd(8)}`, entry.hook.enabled === false ? C.warn : C.ok, bg],
|
|
245
|
-
[` ${entry.hook.type}`, C.dim, bg],
|
|
246
|
-
]);
|
|
247
|
-
}),
|
|
248
|
-
selectedIndex: this.selectedIndex,
|
|
249
|
-
scrollOffset: this.scrollOffset,
|
|
250
|
-
guardRows: 1,
|
|
251
|
-
minRows: 4,
|
|
252
|
-
appendWindowSummary: { dimColor: C.dim },
|
|
253
|
-
},
|
|
254
|
-
afterSections: [selectedSection, activitySection, authoringSection],
|
|
255
|
-
});
|
|
256
|
-
this.scrollOffset = resolvedHooksSection.scrollOffset;
|
|
257
190
|
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
191
|
+
// Empty state: show extra context lines (hooks file, contracts, authoring) before base empty state
|
|
192
|
+
if (hooks.length === 0) {
|
|
193
|
+
const extraHeader: Line[] = [
|
|
194
|
+
buildPanelLine(width, [
|
|
195
|
+
[' Contracts: ', C.label],
|
|
196
|
+
[String(contracts.length), C.value],
|
|
197
|
+
[' Chains: ', C.label],
|
|
198
|
+
[String(chains.length), C.value],
|
|
199
|
+
[' Managed: ', C.label],
|
|
200
|
+
[String(managedHooks.length), C.info],
|
|
201
|
+
]),
|
|
202
|
+
buildPanelLine(width, [
|
|
203
|
+
[' Hooks file: ', C.label],
|
|
204
|
+
[truncateDisplay(workbench.getHooksFilePath(), Math.max(0, width - 15)), C.dim],
|
|
205
|
+
]),
|
|
206
|
+
];
|
|
207
|
+
if (recentAuthoring.length > 0) {
|
|
208
|
+
extraHeader.push(buildPanelLine(width, [
|
|
209
|
+
[' Authoring: ', C.label],
|
|
210
|
+
[truncateDisplay(`${recentAuthoring[0]!.kind} ${recentAuthoring[0]!.target}`, Math.max(0, width - 14)), C.info],
|
|
211
|
+
]));
|
|
212
|
+
}
|
|
213
|
+
if (lastSimulation) {
|
|
214
|
+
extraHeader.push(buildPanelLine(width, [
|
|
215
|
+
[' Last Simulation: ', C.label],
|
|
216
|
+
[truncateDisplay(lastSimulation.eventPath, Math.max(0, width - 20)), C.value],
|
|
217
|
+
]));
|
|
218
|
+
}
|
|
219
|
+
return this.renderList(width, height, {
|
|
220
|
+
title: 'Hooks Control Room',
|
|
221
|
+
header: extraHeader,
|
|
222
|
+
});
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
return this.renderList(width, height, {
|
|
265
226
|
title: 'Hooks Control Room',
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
227
|
+
footer: [
|
|
228
|
+
...detailLines,
|
|
229
|
+
buildPanelLine(width, [[' Recent Activity', C.label]]),
|
|
230
|
+
...activityLines,
|
|
231
|
+
buildPanelLine(width, [[' Authoring', C.label]]),
|
|
232
|
+
...authoringLines,
|
|
233
|
+
buildPanelLine(width, [[' Up/Down move r refresh /hooks for full contract listing', C.dim]]),
|
|
234
|
+
],
|
|
270
235
|
});
|
|
271
|
-
while (lines.length < height) lines.push(createEmptyLine(width));
|
|
272
|
-
return lines.slice(0, height);
|
|
273
236
|
}
|
|
274
237
|
}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import type { Line } from '../types/grid.ts';
|
|
2
2
|
import type { ForensicsRegistry } from '@pellux/goodvibes-sdk/platform/runtime/forensics/registry';
|
|
3
|
-
import {
|
|
3
|
+
import { ScrollableListPanel } from './scrollable-list-panel.ts';
|
|
4
4
|
import {
|
|
5
5
|
buildBodyText,
|
|
6
6
|
buildEmptyState,
|
|
@@ -8,10 +8,10 @@ import {
|
|
|
8
8
|
buildKeyValueLine,
|
|
9
9
|
buildPanelLine,
|
|
10
10
|
buildPanelWorkspace,
|
|
11
|
-
resolveScrollablePanelSection,
|
|
12
11
|
DEFAULT_PANEL_PALETTE,
|
|
13
|
-
type
|
|
12
|
+
type PanelPalette,
|
|
14
13
|
} from './polish.ts';
|
|
14
|
+
import type { FailureReport } from '@pellux/goodvibes-sdk/platform/runtime/forensics/types';
|
|
15
15
|
|
|
16
16
|
const C = {
|
|
17
17
|
...DEFAULT_PANEL_PALETTE,
|
|
@@ -34,11 +34,9 @@ function classificationColor(value: string): string {
|
|
|
34
34
|
}
|
|
35
35
|
}
|
|
36
36
|
|
|
37
|
-
export class IncidentReviewPanel extends
|
|
37
|
+
export class IncidentReviewPanel extends ScrollableListPanel<FailureReport> {
|
|
38
38
|
private readonly registry?: ForensicsRegistry;
|
|
39
39
|
private readonly unsub: (() => void) | null;
|
|
40
|
-
private selectedIndex = 0;
|
|
41
|
-
private scrollOffset = 0;
|
|
42
40
|
|
|
43
41
|
public constructor(registry?: ForensicsRegistry) {
|
|
44
42
|
super('incident', 'Incident Review', 'N', 'monitoring');
|
|
@@ -50,34 +48,36 @@ export class IncidentReviewPanel extends BasePanel {
|
|
|
50
48
|
this.unsub?.();
|
|
51
49
|
}
|
|
52
50
|
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
return
|
|
51
|
+
protected override getPalette(): PanelPalette {
|
|
52
|
+
return C;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
protected getItems(): readonly FailureReport[] {
|
|
56
|
+
return this.registry?.getAll() ?? [];
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
protected renderItem(report: FailureReport, index: number, selected: boolean, width: number): Line {
|
|
60
|
+
const bg = selected ? C.selectBg : undefined;
|
|
61
|
+
return buildPanelLine(width, [
|
|
62
|
+
[' ', C.label, bg],
|
|
63
|
+
[report.id.slice(0, 8).padEnd(9), C.dim, bg],
|
|
64
|
+
[report.classification.padEnd(20), classificationColor(report.classification), bg],
|
|
65
|
+
[report.summary.slice(0, Math.max(0, width - 31)), C.value, bg],
|
|
66
|
+
]);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
protected override getEmptyStateMessage(): string {
|
|
70
|
+
return ' No incidents recorded yet.';
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
protected override getEmptyStateActions(): Array<{ command: string; summary: string }> {
|
|
74
|
+
return [
|
|
75
|
+
{ command: '/incident latest', summary: 'inspect the latest report once one exists' },
|
|
76
|
+
{ command: '/recall capture incident latest', summary: 'promote incident evidence into project knowledge' },
|
|
77
|
+
];
|
|
77
78
|
}
|
|
78
79
|
|
|
79
80
|
public render(width: number, height: number): Line[] {
|
|
80
|
-
this.needsRender = false;
|
|
81
81
|
const intro = 'Failure bundles, replay mismatches, permission fallout, and exportable review evidence.';
|
|
82
82
|
|
|
83
83
|
if (!this.registry) {
|
|
@@ -100,32 +100,16 @@ export class IncidentReviewPanel extends BasePanel {
|
|
|
100
100
|
});
|
|
101
101
|
}
|
|
102
102
|
|
|
103
|
-
const reports = this.
|
|
103
|
+
const reports = this.getItems();
|
|
104
104
|
if (reports.length === 0) {
|
|
105
|
-
return
|
|
106
|
-
title: 'Incident Review Workspace',
|
|
107
|
-
intro,
|
|
108
|
-
sections: [{
|
|
109
|
-
lines: buildEmptyState(
|
|
110
|
-
width,
|
|
111
|
-
' No incidents recorded yet.',
|
|
112
|
-
'The incident workspace fills automatically when failures produce forensics reports, replay mismatches, or policy-linked fallout.',
|
|
113
|
-
[
|
|
114
|
-
{ command: '/incident latest', summary: 'inspect the latest report once one exists' },
|
|
115
|
-
{ command: '/recall capture incident latest', summary: 'promote incident evidence into project knowledge' },
|
|
116
|
-
],
|
|
117
|
-
C,
|
|
118
|
-
),
|
|
119
|
-
}],
|
|
120
|
-
palette: C,
|
|
121
|
-
});
|
|
105
|
+
return this.renderList(width, height, { title: 'Incident Review Workspace' });
|
|
122
106
|
}
|
|
123
107
|
|
|
124
|
-
this.
|
|
108
|
+
this.clampSelection();
|
|
125
109
|
const selected = reports[this.selectedIndex]!;
|
|
126
110
|
const bundle = this.registry.buildBundle(selected.id);
|
|
127
111
|
|
|
128
|
-
const
|
|
112
|
+
const headerLines: Line[] = [
|
|
129
113
|
buildKeyValueLine(width, [
|
|
130
114
|
{ label: 'incidents', value: String(reports.length), valueColor: C.value },
|
|
131
115
|
{ label: 'selected', value: `${this.selectedIndex + 1}/${reports.length}`, valueColor: C.info },
|
|
@@ -134,38 +118,38 @@ export class IncidentReviewPanel extends BasePanel {
|
|
|
134
118
|
buildPanelLine(width, [[' Up/Down move Home/End jump selected incident drives the action rail below', C.dim]]),
|
|
135
119
|
];
|
|
136
120
|
|
|
137
|
-
const
|
|
121
|
+
const footerLines: Line[] = [];
|
|
138
122
|
if (bundle) {
|
|
139
|
-
|
|
123
|
+
footerLines.push(buildKeyValueLine(width, [
|
|
140
124
|
{ label: 'id', value: selected.id, valueColor: C.dim },
|
|
141
125
|
{ label: 'trace', value: selected.traceId, valueColor: C.dim },
|
|
142
126
|
], C));
|
|
143
|
-
|
|
144
|
-
|
|
127
|
+
footerLines.push(...buildBodyText(width, `Root cause: ${bundle.evidence.rootCause ?? 'n/a'}`, C, C.value));
|
|
128
|
+
footerLines.push(buildKeyValueLine(width, [
|
|
145
129
|
{ label: 'Permissions denied', value: String(bundle.evidence.deniedPermissionCount), valueColor: bundle.evidence.deniedPermissionCount > 0 ? C.warn : C.dim },
|
|
146
130
|
{ label: 'Budget breaches', value: String(bundle.evidence.budgetBreachCount), valueColor: bundle.evidence.budgetBreachCount > 0 ? C.warn : C.dim },
|
|
147
131
|
{ label: 'Replay mismatches', value: String(bundle.replay.mismatchCount), valueColor: bundle.replay.mismatchCount > 0 ? C.bad : C.dim },
|
|
148
132
|
], C));
|
|
149
|
-
|
|
133
|
+
footerLines.push(buildPanelLine(width, [
|
|
150
134
|
[' Related IDs: ', C.label],
|
|
151
135
|
[`turn=${bundle.evidence.relatedIds.turnId ?? 'n/a'} task=${bundle.evidence.relatedIds.taskId ?? 'n/a'} agent=${bundle.evidence.relatedIds.agentId ?? 'n/a'}`.slice(0, Math.max(0, width - 14)), C.info],
|
|
152
136
|
]));
|
|
153
137
|
if (bundle.evidence.slowPhases.length > 0) {
|
|
154
|
-
|
|
138
|
+
footerLines.push(buildPanelLine(width, [
|
|
155
139
|
[' Slow phases: ', C.label],
|
|
156
140
|
[bundle.evidence.slowPhases.join(', ').slice(0, Math.max(0, width - 15)), C.warn],
|
|
157
141
|
]));
|
|
158
142
|
}
|
|
159
143
|
const rootCause = selected.causalChain.find((entry) => entry.isRootCause);
|
|
160
144
|
if (rootCause) {
|
|
161
|
-
|
|
145
|
+
footerLines.push(buildPanelLine(width, [
|
|
162
146
|
[' Root event: ', C.label],
|
|
163
147
|
[`${rootCause.sourceEventType} - ${rootCause.description}`.slice(0, Math.max(0, width - 14)), C.dim],
|
|
164
148
|
]));
|
|
165
149
|
}
|
|
166
150
|
const denied = selected.permissionEvidence.find((entry) => entry.approved === false);
|
|
167
151
|
if (denied) {
|
|
168
|
-
|
|
152
|
+
footerLines.push(buildPanelLine(width, [
|
|
169
153
|
[' Permission: ', C.label],
|
|
170
154
|
[`${denied.tool} denied${denied.riskLevel ? ` (${denied.riskLevel})` : ''}${denied.summary ? ` - ${denied.summary}` : ''}`.slice(0, Math.max(0, width - 14)), C.warn],
|
|
171
155
|
]));
|
|
@@ -180,68 +164,32 @@ export class IncidentReviewPanel extends BasePanel {
|
|
|
180
164
|
const replayDetail = ownerBreakdown.length > 0
|
|
181
165
|
? `Replay link: ${mismatch.kind}${mismatch.ownerDomain ? `/${mismatch.ownerDomain}` : ''} - ${mismatch.description} Replay owners: ${ownerBreakdown}`
|
|
182
166
|
: `Replay link: ${mismatch.kind}${mismatch.ownerDomain ? `/${mismatch.ownerDomain}` : ''} - ${mismatch.description}`;
|
|
183
|
-
|
|
167
|
+
footerLines.push(buildPanelLine(width, [
|
|
184
168
|
[' ', C.label],
|
|
185
169
|
[replayDetail.slice(0, Math.max(0, width - 2)), C.bad],
|
|
186
170
|
]));
|
|
187
171
|
} else {
|
|
188
172
|
const ownerBreakdown = Object.entries(bundle.replay.mismatchBreakdown.byOwnerDomain)
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
173
|
+
.filter(([, count]) => count > 0)
|
|
174
|
+
.slice(0, 3)
|
|
175
|
+
.map(([domain, count]) => `${domain}:${count}`)
|
|
176
|
+
.join(', ');
|
|
193
177
|
if (ownerBreakdown.length > 0) {
|
|
194
|
-
|
|
178
|
+
footerLines.push(buildPanelLine(width, [
|
|
195
179
|
[' Replay owners: ', C.label],
|
|
196
180
|
[ownerBreakdown.slice(0, Math.max(0, width - 17)), C.info],
|
|
197
181
|
]));
|
|
198
182
|
}
|
|
199
183
|
}
|
|
200
184
|
}
|
|
185
|
+
footerLines.push(buildPanelLine(width, [[' Action Rail', C.label]]));
|
|
186
|
+
footerLines.push(buildPanelLine(width, [[` /incident latest /incident export ${selected.id} /recall capture incident ${selected.id}`, C.info]]));
|
|
187
|
+
footerLines.push(buildGuidanceLine(width, '/security', 'open the broader trust and incident posture control room', C));
|
|
201
188
|
|
|
202
|
-
|
|
203
|
-
buildPanelLine(width, [[` /incident latest /incident export ${selected.id} /recall capture incident ${selected.id}`, C.info]]),
|
|
204
|
-
];
|
|
205
|
-
|
|
206
|
-
const summarySection: PanelWorkspaceSection = { title: 'Summary', lines: summaryLines };
|
|
207
|
-
const actionSection: PanelWorkspaceSection = { title: 'Action Rail', lines: actionLines };
|
|
208
|
-
const selectedIncidentSection: PanelWorkspaceSection = { title: 'Selected Incident', lines: selectedLines };
|
|
209
|
-
const incidentsSection = resolveScrollablePanelSection(width, height, {
|
|
210
|
-
intro,
|
|
211
|
-
palette: C,
|
|
212
|
-
beforeSections: [summarySection],
|
|
213
|
-
section: {
|
|
214
|
-
title: 'Incidents',
|
|
215
|
-
scrollableLines: reports.map((report, globalIndex) => {
|
|
216
|
-
const bg = globalIndex === this.selectedIndex ? C.selectBg : undefined;
|
|
217
|
-
return buildPanelLine(width, [
|
|
218
|
-
[' ', C.label, bg],
|
|
219
|
-
[report.id.slice(0, 8).padEnd(9), C.dim, bg],
|
|
220
|
-
[report.classification.padEnd(20), classificationColor(report.classification), bg],
|
|
221
|
-
[report.summary.slice(0, Math.max(0, width - 31)), C.value, bg],
|
|
222
|
-
]);
|
|
223
|
-
}),
|
|
224
|
-
selectedIndex: this.selectedIndex,
|
|
225
|
-
scrollOffset: this.scrollOffset,
|
|
226
|
-
minRows: 4,
|
|
227
|
-
appendWindowSummary: { dimColor: C.dim },
|
|
228
|
-
},
|
|
229
|
-
afterSections: [actionSection, selectedIncidentSection],
|
|
230
|
-
});
|
|
231
|
-
this.scrollOffset = incidentsSection.scrollOffset;
|
|
232
|
-
|
|
233
|
-
const sections: PanelWorkspaceSection[] = [
|
|
234
|
-
summarySection,
|
|
235
|
-
incidentsSection.section,
|
|
236
|
-
actionSection,
|
|
237
|
-
selectedIncidentSection,
|
|
238
|
-
];
|
|
239
|
-
|
|
240
|
-
return buildPanelWorkspace(width, height, {
|
|
189
|
+
return this.renderList(width, height, {
|
|
241
190
|
title: 'Incident Review Workspace',
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
palette: C,
|
|
191
|
+
header: headerLines,
|
|
192
|
+
footer: footerLines,
|
|
245
193
|
});
|
|
246
194
|
}
|
|
247
195
|
}
|