@pellux/goodvibes-tui 0.18.20 → 0.19.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 +154 -0
- package/README.md +1 -1
- package/docs/foundation-artifacts/operator-contract.json +1 -1
- package/package.json +7 -3
- package/src/core/conversation-rendering.ts +22 -6
- package/src/core/orchestrator.ts +1 -1
- package/src/input/commands/diff-runtime.ts +6 -5
- package/src/input/commands/guidance-runtime.ts +1 -1
- package/src/input/commands/health-runtime.ts +2 -2
- package/src/input/commands/local-setup-review.ts +1 -1
- package/src/input/commands/session-content.ts +1 -1
- package/src/input/commands/session.ts +0 -1
- package/src/input/commands/shell-core.ts +3 -2
- package/src/input/commands/skills-runtime.ts +2 -2
- package/src/input/commands/subscription-runtime.ts +4 -4
- package/src/input/feed-context-factory.ts +236 -0
- package/src/input/handler-feed.ts +44 -6
- package/src/input/handler-shortcuts.ts +138 -125
- package/src/input/handler.ts +119 -119
- package/src/input/keybindings.ts +30 -0
- package/src/input/panel-integration-actions.ts +2 -1
- package/src/input/settings-modal-types.ts +60 -0
- package/src/input/settings-modal.ts +83 -65
- package/src/panels/agent-inspector-panel.ts +10 -9
- package/src/panels/agent-logs-panel.ts +26 -6
- package/src/panels/approval-panel.ts +55 -82
- package/src/panels/automation-control-panel.ts +120 -161
- package/src/panels/base-panel.ts +108 -3
- package/src/panels/communication-panel.ts +69 -107
- package/src/panels/context-visualizer-panel.ts +2 -0
- package/src/panels/control-plane-panel.ts +117 -172
- package/src/panels/diff-panel.ts +2 -0
- package/src/panels/file-explorer-panel.ts +51 -31
- package/src/panels/file-preview-panel.ts +57 -35
- package/src/panels/git-panel.ts +12 -13
- package/src/panels/hooks-panel.ts +103 -138
- package/src/panels/incident-review-panel.ts +59 -109
- package/src/panels/knowledge-panel.ts +75 -107
- package/src/panels/local-auth-panel.ts +77 -93
- package/src/panels/marketplace-panel.ts +51 -69
- package/src/panels/mcp-panel.ts +110 -155
- package/src/panels/memory-panel.ts +90 -158
- package/src/panels/ops-control-panel.ts +51 -85
- package/src/panels/orchestration-panel.ts +70 -51
- package/src/panels/panel-list-panel.ts +5 -4
- package/src/panels/panel-manager.ts +25 -2
- package/src/panels/plan-dashboard-panel.ts +2 -0
- package/src/panels/plugins-panel.ts +37 -60
- package/src/panels/polish.ts +51 -2
- package/src/panels/provider-accounts-panel.ts +1 -0
- package/src/panels/provider-health-panel.ts +6 -8
- package/src/panels/routes-panel.ts +91 -141
- package/src/panels/schedule-panel.ts +7 -6
- package/src/panels/scrollable-list-panel.ts +64 -16
- package/src/panels/security-panel.ts +118 -152
- package/src/panels/services-panel.ts +63 -105
- package/src/panels/session-browser-panel.ts +19 -18
- package/src/panels/settings-sync-panel.ts +79 -123
- package/src/panels/skills-panel.ts +114 -230
- package/src/panels/subscription-panel.ts +64 -86
- package/src/panels/system-messages-panel.ts +147 -141
- package/src/panels/tasks-panel.ts +130 -179
- package/src/panels/token-budget-panel.ts +2 -0
- package/src/panels/watchers-panel.ts +89 -137
- package/src/panels/worktree-panel.ts +1 -0
- package/src/panels/wrfc-panel.ts +2 -0
- package/src/renderer/agent-detail-modal.ts +2 -2
- package/src/renderer/ansi-sanitize.ts +76 -0
- package/src/renderer/buffer.ts +23 -1
- package/src/renderer/diff.ts +8 -0
- package/src/renderer/help-overlay.ts +48 -28
- package/src/renderer/markdown.ts +3 -145
- package/src/renderer/settings-modal-helpers.ts +27 -0
- package/src/renderer/settings-modal.ts +18 -1
- package/src/renderer/status-glyphs.ts +21 -0
- package/src/renderer/status-token.ts +4 -8
- package/src/renderer/tool-call.ts +4 -3
- package/src/runtime/bootstrap-core.ts +1 -1
- package/src/runtime/bootstrap-hook-bridge.ts +1 -1
- package/src/runtime/bootstrap.ts +7 -8
- package/src/runtime/diagnostics/panels/policy.ts +2 -1
- package/src/shell/ui-openers.ts +1 -1
- package/src/version.ts +1 -1
package/src/panels/git-panel.ts
CHANGED
|
@@ -103,7 +103,7 @@ export class GitPanel extends BasePanel {
|
|
|
103
103
|
/** Scroll offset for both main view and diff view. */
|
|
104
104
|
private scrollOffset = 0;
|
|
105
105
|
|
|
106
|
-
private
|
|
106
|
+
private refreshTimerId: ReturnType<typeof setInterval> | null = null;
|
|
107
107
|
private loading = true;
|
|
108
108
|
private error: string | null = null;
|
|
109
109
|
|
|
@@ -119,20 +119,21 @@ export class GitPanel extends BasePanel {
|
|
|
119
119
|
override onActivate(): void {
|
|
120
120
|
super.onActivate();
|
|
121
121
|
void this.refresh();
|
|
122
|
-
this.
|
|
122
|
+
this.refreshTimerId = this.registerTimer(setInterval(() => {
|
|
123
123
|
void this.refresh();
|
|
124
|
-
}, 5_000);
|
|
124
|
+
}, 5_000));
|
|
125
125
|
}
|
|
126
126
|
|
|
127
127
|
override onDeactivate(): void {
|
|
128
|
-
if (this.
|
|
129
|
-
|
|
130
|
-
this.
|
|
128
|
+
if (this.refreshTimerId !== null) {
|
|
129
|
+
this.clearTimer(this.refreshTimerId);
|
|
130
|
+
this.refreshTimerId = null;
|
|
131
131
|
}
|
|
132
132
|
}
|
|
133
133
|
|
|
134
134
|
override onDestroy(): void {
|
|
135
135
|
this.onDeactivate();
|
|
136
|
+
super.onDestroy();
|
|
136
137
|
}
|
|
137
138
|
|
|
138
139
|
// ---------------------------------------------------------------------------
|
|
@@ -326,18 +327,16 @@ export class GitPanel extends BasePanel {
|
|
|
326
327
|
const item = this.items[this.selectedIndex];
|
|
327
328
|
if (!item || item.kind !== 'file') return;
|
|
328
329
|
|
|
329
|
-
// I3:
|
|
330
|
-
this.startLoading('Loading diff...');
|
|
331
|
-
this.markDirty();
|
|
330
|
+
// I3: withLoading guarantees spinner is cleared even if diffFile throws
|
|
332
331
|
try {
|
|
333
|
-
const
|
|
334
|
-
|
|
335
|
-
|
|
332
|
+
const raw = await this.withLoading('Loading diff…', async () => {
|
|
333
|
+
const git = new GitService(this.workingDirectory);
|
|
334
|
+
return git.diffFile(item.entry.path, item.entry.staged);
|
|
335
|
+
});
|
|
336
336
|
this.expandedDiff = raw ? raw.split('\n') : ['(no diff available)'];
|
|
337
337
|
this.scrollOffset = 0;
|
|
338
338
|
this.markDirty();
|
|
339
339
|
} catch (err) {
|
|
340
|
-
this.stopLoading();
|
|
341
340
|
this.expandedDiff = [`Error: ${summarizeError(err)}`];
|
|
342
341
|
this.scrollOffset = 0;
|
|
343
342
|
this.markDirty();
|
|
@@ -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,9 @@ 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
|
-
|
|
13
|
+
buildStatusPill,
|
|
16
14
|
DEFAULT_PANEL_PALETTE,
|
|
17
|
-
resolvePrimaryScrollableSection,
|
|
18
|
-
type PanelWorkspaceSection,
|
|
19
15
|
} from './polish.ts';
|
|
20
16
|
|
|
21
17
|
const C = {
|
|
@@ -59,9 +55,9 @@ function createDefaultDataSource(
|
|
|
59
55
|
};
|
|
60
56
|
}
|
|
61
57
|
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
58
|
+
type HookEntry = { pattern: string; hook: HookDefinition };
|
|
59
|
+
|
|
60
|
+
export class HooksPanel extends ScrollableListPanel<HookEntry> {
|
|
65
61
|
private readonly dataSource: HooksPanelDataSource;
|
|
66
62
|
|
|
67
63
|
public constructor(
|
|
@@ -71,34 +67,46 @@ export class HooksPanel extends BasePanel {
|
|
|
71
67
|
dataSource: HooksPanelDataSource = createDefaultDataSource(hookDispatcher, hookWorkbench, hookActivityTracker),
|
|
72
68
|
) {
|
|
73
69
|
super('hooks', 'Hooks', 'H', 'monitoring');
|
|
70
|
+
this.showSelectionGutter = true; // I5: non-color selection affordance
|
|
74
71
|
this.dataSource = dataSource;
|
|
75
72
|
}
|
|
76
73
|
|
|
74
|
+
protected override getPalette() { return C; }
|
|
75
|
+
protected override getEmptyStateMessage() { return ' No hooks are currently registered.'; }
|
|
76
|
+
protected override getEmptyStateActions() {
|
|
77
|
+
return [
|
|
78
|
+
{ command: '/hooks', summary: 'review hook contracts and managed authoring actions' },
|
|
79
|
+
{ command: '/settings', summary: 'review hook/runtime behavior in the settings surface' },
|
|
80
|
+
];
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
protected getItems(): readonly HookEntry[] {
|
|
84
|
+
return this.dataSource.listHooks();
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
protected renderItem(entry: HookEntry, index: number, selected: boolean, width: number): Line {
|
|
88
|
+
const bg = selected ? C.selectBg : undefined;
|
|
89
|
+
return buildPanelLine(width, [
|
|
90
|
+
[' ', C.label, bg],
|
|
91
|
+
[truncateDisplay(entry.hook.name ?? '(unnamed)', 20).padEnd(20), C.value, bg],
|
|
92
|
+
[` ${truncateDisplay(entry.pattern, 28).padEnd(28)}`, C.info, bg],
|
|
93
|
+
...buildStatusPill(entry.hook.enabled === false ? 'warn' : 'good', ` ${(entry.hook.enabled === false ? 'DISABLED' : 'ENABLED').padEnd(8)}`, { bg }),
|
|
94
|
+
[` ${entry.hook.type}`, C.dim, bg],
|
|
95
|
+
]);
|
|
96
|
+
}
|
|
97
|
+
|
|
77
98
|
public handleInput(key: string): boolean {
|
|
78
|
-
const entries = this.dataSource.listHooks();
|
|
79
99
|
if (key === 'r') {
|
|
80
100
|
this.markDirty();
|
|
81
101
|
return true;
|
|
82
102
|
}
|
|
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;
|
|
103
|
+
return super.handleInput(key);
|
|
95
104
|
}
|
|
96
105
|
|
|
97
106
|
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();
|
|
107
|
+
this.clampSelection();
|
|
101
108
|
const hooks = this.dataSource.listHooks();
|
|
109
|
+
const contracts = this.dataSource.listContracts();
|
|
102
110
|
const chains = this.dataSource.listChains();
|
|
103
111
|
const recentActivity = this.dataSource.listRecentActivity(3);
|
|
104
112
|
const workbench = this.dataSource.getWorkbench();
|
|
@@ -106,93 +114,48 @@ export class HooksPanel extends BasePanel {
|
|
|
106
114
|
const managedChains = workbench.listManagedChains();
|
|
107
115
|
const recentAuthoring = workbench.listRecentActions(3);
|
|
108
116
|
const lastSimulation = workbench.getLastSimulation();
|
|
117
|
+
const intro = 'Hook contracts, active registrations, managed authoring, recent runtime activity, and simulation matches.';
|
|
109
118
|
|
|
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
|
-
}
|
|
119
|
+
const selected = hooks[this.selectedIndex];
|
|
120
|
+
const contract = selected ? contracts.find((c) => c.pattern === selected.pattern) : undefined;
|
|
156
121
|
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
const detailLines: Line[] = [
|
|
161
|
-
buildPanelLine(width, [
|
|
122
|
+
const detailLines: Line[] = [];
|
|
123
|
+
if (selected) {
|
|
124
|
+
detailLines.push(buildPanelLine(width, [
|
|
162
125
|
[' Hook: ', C.label],
|
|
163
126
|
[selected.hook.name ?? '(unnamed)', C.value],
|
|
164
127
|
[' Type: ', C.label],
|
|
165
128
|
[selected.hook.type, C.info],
|
|
166
129
|
[' Match: ', C.label],
|
|
167
130
|
[selected.hook.matcher ?? selected.hook.match, C.value],
|
|
168
|
-
])
|
|
169
|
-
buildPanelLine(width, [
|
|
131
|
+
]));
|
|
132
|
+
detailLines.push(buildPanelLine(width, [
|
|
170
133
|
[' Pattern: ', C.label],
|
|
171
134
|
[truncateDisplay(selected.pattern, Math.max(0, width - 12)), C.value],
|
|
172
|
-
])
|
|
173
|
-
|
|
174
|
-
|
|
135
|
+
]));
|
|
136
|
+
if (contract) {
|
|
137
|
+
detailLines.push(buildPanelLine(width, [
|
|
138
|
+
[' Contract: ', C.label],
|
|
139
|
+
[`${contract.authority} / ${contract.executionMode}`, C.info],
|
|
140
|
+
[' Policy: ', C.label],
|
|
141
|
+
[contract.failurePolicy, C.value],
|
|
142
|
+
]));
|
|
143
|
+
detailLines.push(buildPanelLine(width, [
|
|
144
|
+
[' Capabilities: ', C.label],
|
|
145
|
+
[`deny=${contract.canDeny ? 'yes' : 'no'} mutate=${contract.canMutateInput ? 'yes' : 'no'} inject=${contract.canInjectContext ? 'yes' : 'no'}`, C.dim],
|
|
146
|
+
]));
|
|
147
|
+
} else {
|
|
148
|
+
detailLines.push(buildPanelLine(width, [[' Contract: No exact contract registered for this pattern.', C.warn]]));
|
|
149
|
+
}
|
|
175
150
|
detailLines.push(buildPanelLine(width, [
|
|
176
|
-
['
|
|
177
|
-
[
|
|
178
|
-
[' Policy: ', C.label],
|
|
179
|
-
[contract.failurePolicy, C.value],
|
|
151
|
+
[' Summary: ', C.label],
|
|
152
|
+
[`hooks=${hooks.length} chains=${chains.length} contracts=${contracts.length} managed=${managedHooks.length}/${managedChains.length}`, C.dim],
|
|
180
153
|
]));
|
|
181
154
|
detailLines.push(buildPanelLine(width, [
|
|
182
|
-
['
|
|
183
|
-
[
|
|
155
|
+
[' Hooks file: ', C.label],
|
|
156
|
+
[truncateDisplay(workbench.getHooksFilePath(), Math.max(0, width - 15)), C.dim],
|
|
184
157
|
]));
|
|
185
|
-
} else {
|
|
186
|
-
detailLines.push(buildPanelLine(width, [[' Contract: No exact contract registered for this pattern.', C.warn]]));
|
|
187
158
|
}
|
|
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
159
|
|
|
197
160
|
const activityLines: Line[] = recentActivity.length === 0
|
|
198
161
|
? [buildPanelLine(width, [[' No hook activity recorded yet.', C.empty]])]
|
|
@@ -226,49 +189,51 @@ export class HooksPanel extends BasePanel {
|
|
|
226
189
|
[`hooks=${lastSimulation.matchedHooks.length} chains=${lastSimulation.matchedChains.length}`, C.dim],
|
|
227
190
|
]));
|
|
228
191
|
}
|
|
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
192
|
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
193
|
+
// Empty state: show extra context lines (hooks file, contracts, authoring) before base empty state
|
|
194
|
+
if (hooks.length === 0) {
|
|
195
|
+
const extraHeader: Line[] = [
|
|
196
|
+
buildPanelLine(width, [
|
|
197
|
+
[' Contracts: ', C.label],
|
|
198
|
+
[String(contracts.length), C.value],
|
|
199
|
+
[' Chains: ', C.label],
|
|
200
|
+
[String(chains.length), C.value],
|
|
201
|
+
[' Managed: ', C.label],
|
|
202
|
+
[String(managedHooks.length), C.info],
|
|
203
|
+
]),
|
|
204
|
+
buildPanelLine(width, [
|
|
205
|
+
[' Hooks file: ', C.label],
|
|
206
|
+
[truncateDisplay(workbench.getHooksFilePath(), Math.max(0, width - 15)), C.dim],
|
|
207
|
+
]),
|
|
208
|
+
];
|
|
209
|
+
if (recentAuthoring.length > 0) {
|
|
210
|
+
extraHeader.push(buildPanelLine(width, [
|
|
211
|
+
[' Authoring: ', C.label],
|
|
212
|
+
[truncateDisplay(`${recentAuthoring[0]!.kind} ${recentAuthoring[0]!.target}`, Math.max(0, width - 14)), C.info],
|
|
213
|
+
]));
|
|
214
|
+
}
|
|
215
|
+
if (lastSimulation) {
|
|
216
|
+
extraHeader.push(buildPanelLine(width, [
|
|
217
|
+
[' Last Simulation: ', C.label],
|
|
218
|
+
[truncateDisplay(lastSimulation.eventPath, Math.max(0, width - 20)), C.value],
|
|
219
|
+
]));
|
|
220
|
+
}
|
|
221
|
+
return this.renderList(width, height, {
|
|
222
|
+
title: 'Hooks Control Room',
|
|
223
|
+
header: extraHeader,
|
|
224
|
+
});
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
return this.renderList(width, height, {
|
|
265
228
|
title: 'Hooks Control Room',
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
229
|
+
footer: [
|
|
230
|
+
...detailLines,
|
|
231
|
+
buildPanelLine(width, [[' Recent Activity', C.label]]),
|
|
232
|
+
...activityLines,
|
|
233
|
+
buildPanelLine(width, [[' Authoring', C.label]]),
|
|
234
|
+
...authoringLines,
|
|
235
|
+
buildPanelLine(width, [[' Up/Down move r refresh /hooks for full contract listing', C.dim]]),
|
|
236
|
+
],
|
|
270
237
|
});
|
|
271
|
-
while (lines.length < height) lines.push(createEmptyLine(width));
|
|
272
|
-
return lines.slice(0, height);
|
|
273
238
|
}
|
|
274
239
|
}
|
|
@@ -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,11 @@ import {
|
|
|
8
8
|
buildKeyValueLine,
|
|
9
9
|
buildPanelLine,
|
|
10
10
|
buildPanelWorkspace,
|
|
11
|
-
|
|
11
|
+
buildStatusPill,
|
|
12
12
|
DEFAULT_PANEL_PALETTE,
|
|
13
|
-
type
|
|
13
|
+
type PanelPalette,
|
|
14
14
|
} from './polish.ts';
|
|
15
|
+
import type { FailureReport } from '@pellux/goodvibes-sdk/platform/runtime/forensics/types';
|
|
15
16
|
|
|
16
17
|
const C = {
|
|
17
18
|
...DEFAULT_PANEL_PALETTE,
|
|
@@ -34,14 +35,13 @@ function classificationColor(value: string): string {
|
|
|
34
35
|
}
|
|
35
36
|
}
|
|
36
37
|
|
|
37
|
-
export class IncidentReviewPanel extends
|
|
38
|
+
export class IncidentReviewPanel extends ScrollableListPanel<FailureReport> {
|
|
38
39
|
private readonly registry?: ForensicsRegistry;
|
|
39
40
|
private readonly unsub: (() => void) | null;
|
|
40
|
-
private selectedIndex = 0;
|
|
41
|
-
private scrollOffset = 0;
|
|
42
41
|
|
|
43
42
|
public constructor(registry?: ForensicsRegistry) {
|
|
44
43
|
super('incident', 'Incident Review', 'N', 'monitoring');
|
|
44
|
+
this.showSelectionGutter = true; // I5: non-color selection affordance
|
|
45
45
|
this.registry = registry;
|
|
46
46
|
this.unsub = registry ? registry.subscribe(() => this.markDirty()) : null;
|
|
47
47
|
}
|
|
@@ -50,34 +50,36 @@ export class IncidentReviewPanel extends BasePanel {
|
|
|
50
50
|
this.unsub?.();
|
|
51
51
|
}
|
|
52
52
|
|
|
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
|
|
53
|
+
protected override getPalette(): PanelPalette {
|
|
54
|
+
return C;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
protected getItems(): readonly FailureReport[] {
|
|
58
|
+
return this.registry?.getAll() ?? [];
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
protected renderItem(report: FailureReport, index: number, selected: boolean, width: number): Line {
|
|
62
|
+
const bg = selected ? C.selectBg : undefined;
|
|
63
|
+
return buildPanelLine(width, [
|
|
64
|
+
[' ', C.label, bg],
|
|
65
|
+
[report.id.slice(0, 8).padEnd(9), C.dim, bg],
|
|
66
|
+
[report.classification.padEnd(20), classificationColor(report.classification), bg],
|
|
67
|
+
[report.summary.slice(0, Math.max(0, width - 31)), C.value, bg],
|
|
68
|
+
]);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
protected override getEmptyStateMessage(): string {
|
|
72
|
+
return ' No incidents recorded yet.';
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
protected override getEmptyStateActions(): Array<{ command: string; summary: string }> {
|
|
76
|
+
return [
|
|
77
|
+
{ command: '/incident latest', summary: 'inspect the latest report once one exists' },
|
|
78
|
+
{ command: '/recall capture incident latest', summary: 'promote incident evidence into project knowledge' },
|
|
79
|
+
];
|
|
77
80
|
}
|
|
78
81
|
|
|
79
82
|
public render(width: number, height: number): Line[] {
|
|
80
|
-
this.needsRender = false;
|
|
81
83
|
const intro = 'Failure bundles, replay mismatches, permission fallout, and exportable review evidence.';
|
|
82
84
|
|
|
83
85
|
if (!this.registry) {
|
|
@@ -100,32 +102,16 @@ export class IncidentReviewPanel extends BasePanel {
|
|
|
100
102
|
});
|
|
101
103
|
}
|
|
102
104
|
|
|
103
|
-
const reports = this.
|
|
105
|
+
const reports = this.getItems();
|
|
104
106
|
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
|
-
});
|
|
107
|
+
return this.renderList(width, height, { title: 'Incident Review Workspace' });
|
|
122
108
|
}
|
|
123
109
|
|
|
124
|
-
this.
|
|
110
|
+
this.clampSelection();
|
|
125
111
|
const selected = reports[this.selectedIndex]!;
|
|
126
112
|
const bundle = this.registry.buildBundle(selected.id);
|
|
127
113
|
|
|
128
|
-
const
|
|
114
|
+
const headerLines: Line[] = [
|
|
129
115
|
buildKeyValueLine(width, [
|
|
130
116
|
{ label: 'incidents', value: String(reports.length), valueColor: C.value },
|
|
131
117
|
{ label: 'selected', value: `${this.selectedIndex + 1}/${reports.length}`, valueColor: C.info },
|
|
@@ -134,38 +120,38 @@ export class IncidentReviewPanel extends BasePanel {
|
|
|
134
120
|
buildPanelLine(width, [[' Up/Down move Home/End jump selected incident drives the action rail below', C.dim]]),
|
|
135
121
|
];
|
|
136
122
|
|
|
137
|
-
const
|
|
123
|
+
const footerLines: Line[] = [];
|
|
138
124
|
if (bundle) {
|
|
139
|
-
|
|
125
|
+
footerLines.push(buildKeyValueLine(width, [
|
|
140
126
|
{ label: 'id', value: selected.id, valueColor: C.dim },
|
|
141
127
|
{ label: 'trace', value: selected.traceId, valueColor: C.dim },
|
|
142
128
|
], C));
|
|
143
|
-
|
|
144
|
-
|
|
129
|
+
footerLines.push(...buildBodyText(width, `Root cause: ${bundle.evidence.rootCause ?? 'n/a'}`, C, C.value));
|
|
130
|
+
footerLines.push(buildKeyValueLine(width, [
|
|
145
131
|
{ label: 'Permissions denied', value: String(bundle.evidence.deniedPermissionCount), valueColor: bundle.evidence.deniedPermissionCount > 0 ? C.warn : C.dim },
|
|
146
132
|
{ label: 'Budget breaches', value: String(bundle.evidence.budgetBreachCount), valueColor: bundle.evidence.budgetBreachCount > 0 ? C.warn : C.dim },
|
|
147
133
|
{ label: 'Replay mismatches', value: String(bundle.replay.mismatchCount), valueColor: bundle.replay.mismatchCount > 0 ? C.bad : C.dim },
|
|
148
134
|
], C));
|
|
149
|
-
|
|
135
|
+
footerLines.push(buildPanelLine(width, [
|
|
150
136
|
[' Related IDs: ', C.label],
|
|
151
137
|
[`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
138
|
]));
|
|
153
139
|
if (bundle.evidence.slowPhases.length > 0) {
|
|
154
|
-
|
|
140
|
+
footerLines.push(buildPanelLine(width, [
|
|
155
141
|
[' Slow phases: ', C.label],
|
|
156
|
-
|
|
142
|
+
...buildStatusPill('warn', bundle.evidence.slowPhases.join(', ').slice(0, Math.max(0, width - 15))),
|
|
157
143
|
]));
|
|
158
144
|
}
|
|
159
145
|
const rootCause = selected.causalChain.find((entry) => entry.isRootCause);
|
|
160
146
|
if (rootCause) {
|
|
161
|
-
|
|
147
|
+
footerLines.push(buildPanelLine(width, [
|
|
162
148
|
[' Root event: ', C.label],
|
|
163
149
|
[`${rootCause.sourceEventType} - ${rootCause.description}`.slice(0, Math.max(0, width - 14)), C.dim],
|
|
164
150
|
]));
|
|
165
151
|
}
|
|
166
152
|
const denied = selected.permissionEvidence.find((entry) => entry.approved === false);
|
|
167
153
|
if (denied) {
|
|
168
|
-
|
|
154
|
+
footerLines.push(buildPanelLine(width, [
|
|
169
155
|
[' Permission: ', C.label],
|
|
170
156
|
[`${denied.tool} denied${denied.riskLevel ? ` (${denied.riskLevel})` : ''}${denied.summary ? ` - ${denied.summary}` : ''}`.slice(0, Math.max(0, width - 14)), C.warn],
|
|
171
157
|
]));
|
|
@@ -180,68 +166,32 @@ export class IncidentReviewPanel extends BasePanel {
|
|
|
180
166
|
const replayDetail = ownerBreakdown.length > 0
|
|
181
167
|
? `Replay link: ${mismatch.kind}${mismatch.ownerDomain ? `/${mismatch.ownerDomain}` : ''} - ${mismatch.description} Replay owners: ${ownerBreakdown}`
|
|
182
168
|
: `Replay link: ${mismatch.kind}${mismatch.ownerDomain ? `/${mismatch.ownerDomain}` : ''} - ${mismatch.description}`;
|
|
183
|
-
|
|
169
|
+
footerLines.push(buildPanelLine(width, [
|
|
184
170
|
[' ', C.label],
|
|
185
|
-
|
|
171
|
+
...buildStatusPill('bad', replayDetail.slice(0, Math.max(0, width - 2))),
|
|
186
172
|
]));
|
|
187
173
|
} else {
|
|
188
174
|
const ownerBreakdown = Object.entries(bundle.replay.mismatchBreakdown.byOwnerDomain)
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
175
|
+
.filter(([, count]) => count > 0)
|
|
176
|
+
.slice(0, 3)
|
|
177
|
+
.map(([domain, count]) => `${domain}:${count}`)
|
|
178
|
+
.join(', ');
|
|
193
179
|
if (ownerBreakdown.length > 0) {
|
|
194
|
-
|
|
180
|
+
footerLines.push(buildPanelLine(width, [
|
|
195
181
|
[' Replay owners: ', C.label],
|
|
196
182
|
[ownerBreakdown.slice(0, Math.max(0, width - 17)), C.info],
|
|
197
183
|
]));
|
|
198
184
|
}
|
|
199
185
|
}
|
|
200
186
|
}
|
|
187
|
+
footerLines.push(buildPanelLine(width, [[' Action Rail', C.label]]));
|
|
188
|
+
footerLines.push(buildPanelLine(width, [[` /incident latest /incident export ${selected.id} /recall capture incident ${selected.id}`, C.info]]));
|
|
189
|
+
footerLines.push(buildGuidanceLine(width, '/security', 'open the broader trust and incident posture control room', C));
|
|
201
190
|
|
|
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, {
|
|
191
|
+
return this.renderList(width, height, {
|
|
241
192
|
title: 'Incident Review Workspace',
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
palette: C,
|
|
193
|
+
header: headerLines,
|
|
194
|
+
footer: footerLines,
|
|
245
195
|
});
|
|
246
196
|
}
|
|
247
197
|
}
|