@pellux/goodvibes-tui 0.28.0 → 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +133 -0
- package/README.md +1 -1
- package/package.json +1 -1
- package/src/core/conversation-line-cache.ts +432 -0
- package/src/core/conversation-rendering.ts +11 -3
- package/src/core/conversation.ts +50 -4
- package/src/input/commands/cost-runtime.ts +49 -0
- package/src/input/commands/operator-runtime.ts +5 -1
- package/src/input/commands.ts +2 -0
- package/src/input/handler-content-actions.ts +8 -3
- package/src/input/handler-feed-routes.ts +12 -35
- package/src/input/handler-feed.ts +4 -8
- package/src/input/handler-shortcuts.ts +51 -0
- package/src/input/handler.ts +43 -6
- package/src/input/input-history.ts +17 -4
- package/src/input/keybindings.ts +48 -8
- package/src/input/panel-integration-actions.ts +107 -1
- package/src/main.ts +19 -11
- package/src/panels/agent-inspector-panel.ts +125 -36
- package/src/panels/agent-inspector-shared.ts +144 -0
- package/src/panels/approval-panel.ts +67 -16
- package/src/panels/automation-control-panel.ts +368 -124
- package/src/panels/base-panel.ts +1 -1
- package/src/panels/builtin/agent.ts +28 -37
- package/src/panels/builtin/development.ts +40 -32
- package/src/panels/builtin/knowledge.ts +15 -6
- package/src/panels/builtin/operations.ts +178 -117
- package/src/panels/builtin/session.ts +35 -9
- package/src/panels/builtin/shared.ts +98 -6
- package/src/panels/cockpit-panel.ts +232 -73
- package/src/panels/communication-panel.ts +58 -20
- package/src/panels/confirm-state.ts +8 -1
- package/src/panels/control-plane-panel.ts +351 -118
- package/src/panels/cost-tracker-panel.ts +165 -7
- package/src/panels/debug-panel.ts +312 -159
- package/src/panels/diff-panel.ts +209 -57
- package/src/panels/docs-panel.ts +152 -66
- package/src/panels/eval-panel.ts +220 -42
- package/src/panels/file-explorer-panel.ts +202 -122
- package/src/panels/file-preview-panel.ts +132 -57
- package/src/panels/git-panel.ts +310 -129
- package/src/panels/hooks-panel.ts +150 -22
- package/src/panels/incident-review-panel.ts +223 -35
- package/src/panels/index.ts +0 -4
- package/src/panels/intelligence-panel.ts +212 -86
- package/src/panels/knowledge-graph-panel.ts +461 -101
- package/src/panels/local-auth-panel.ts +240 -28
- package/src/panels/marketplace-panel.ts +193 -26
- package/src/panels/memory-panel.ts +78 -77
- package/src/panels/ops-control-panel.ts +146 -29
- package/src/panels/ops-strategy-panel.ts +72 -4
- package/src/panels/orchestration-panel.ts +231 -69
- package/src/panels/panel-list-panel.ts +154 -131
- package/src/panels/panel-manager.ts +97 -9
- package/src/panels/plan-dashboard-panel.ts +333 -17
- package/src/panels/plugins-panel.ts +184 -29
- package/src/panels/policy-panel.ts +210 -38
- package/src/panels/polish-core.ts +7 -2
- package/src/panels/polish.ts +23 -4
- package/src/panels/project-planning-answer-actions.ts +134 -0
- package/src/panels/project-planning-panel.ts +62 -113
- package/src/panels/provider-health-panel.ts +434 -518
- package/src/panels/provider-health-routes.ts +203 -0
- package/src/panels/provider-health-tracker.ts +194 -6
- package/src/panels/provider-health-views.ts +560 -0
- package/src/panels/qr-panel.ts +116 -30
- package/src/panels/remote-panel.ts +114 -36
- package/src/panels/routes-panel.ts +63 -22
- package/src/panels/sandbox-panel.ts +174 -42
- package/src/panels/scrollable-list-panel.ts +19 -135
- package/src/panels/security-panel.ts +133 -33
- package/src/panels/services-panel.ts +116 -64
- package/src/panels/session-browser-panel.ts +73 -2
- package/src/panels/session-maintenance.ts +4 -122
- package/src/panels/settings-sync-panel.ts +335 -72
- package/src/panels/skills-panel.ts +157 -89
- package/src/panels/subscription-panel.ts +86 -33
- package/src/panels/symbol-outline-panel.ts +248 -108
- package/src/panels/system-messages-panel.ts +114 -13
- package/src/panels/tasks-panel.ts +326 -139
- package/src/panels/thinking-panel.ts +43 -10
- package/src/panels/token-budget-panel.ts +194 -18
- package/src/panels/tool-inspector-panel.ts +144 -34
- package/src/panels/types.ts +40 -4
- package/src/panels/work-plan-panel.ts +280 -17
- package/src/panels/worktree-panel.ts +175 -42
- package/src/panels/wrfc-panel.ts +116 -24
- package/src/renderer/agent-detail-modal.ts +37 -8
- package/src/renderer/code-block.ts +58 -28
- package/src/renderer/context-inspector.ts +3 -6
- package/src/renderer/conversation-overlays.ts +25 -11
- package/src/renderer/conversation-surface.ts +1 -21
- package/src/renderer/diff-view.ts +6 -4
- package/src/renderer/footer-tips.ts +41 -0
- package/src/renderer/fullscreen-primitives.ts +23 -17
- package/src/renderer/fullscreen-workspace.ts +2 -7
- package/src/renderer/help-overlay.ts +91 -14
- package/src/renderer/hint-grammar.ts +52 -0
- package/src/renderer/history-search-overlay.ts +10 -16
- package/src/renderer/layout.ts +7 -7
- package/src/renderer/markdown.ts +9 -1
- package/src/renderer/modal-factory.ts +23 -15
- package/src/renderer/model-picker-overlay.ts +8 -485
- package/src/renderer/model-workspace.ts +9 -24
- package/src/renderer/overlay-box.ts +23 -19
- package/src/renderer/process-indicator.ts +15 -25
- package/src/renderer/profile-picker-modal.ts +13 -14
- package/src/renderer/prompt-content-width.ts +16 -0
- package/src/renderer/search-overlay.ts +27 -6
- package/src/renderer/selection-modal-overlay.ts +3 -1
- package/src/renderer/semantic-diff.ts +1 -1
- package/src/renderer/session-picker-modal.ts +6 -4
- package/src/renderer/settings-modal-helpers.ts +10 -34
- package/src/renderer/settings-modal.ts +70 -10
- package/src/renderer/shell-surface.ts +54 -15
- package/src/renderer/status-glyphs.ts +11 -9
- package/src/renderer/surface-layout.ts +0 -12
- package/src/renderer/term-caps.ts +1 -1
- package/src/renderer/theme.ts +60 -3
- package/src/renderer/tool-call.ts +6 -20
- package/src/renderer/ui-factory.ts +48 -37
- package/src/renderer/ui-primitives.ts +41 -3
- package/src/runtime/bootstrap-shell.ts +35 -18
- package/src/runtime/diagnostics/panels/index.ts +0 -3
- package/src/runtime/render-scheduler.ts +80 -0
- package/src/shell/ui-openers.ts +14 -22
- package/src/utils/format-duration.ts +2 -2
- package/src/utils/splash-lines.ts +54 -5
- package/src/version.ts +1 -1
- package/src/work-plans/work-plan-store.ts +13 -0
- package/src/panels/agent-logs-panel.ts +0 -635
- package/src/panels/agent-logs-shared.ts +0 -129
- package/src/panels/context-visualizer-panel.ts +0 -238
- package/src/panels/forensics-panel.ts +0 -378
- package/src/panels/provider-account-snapshot.ts +0 -259
- package/src/panels/provider-accounts-panel.ts +0 -255
- package/src/panels/provider-stats-panel.ts +0 -391
- package/src/panels/schedule-panel.ts +0 -365
- package/src/panels/watchers-panel.ts +0 -213
- package/src/renderer/file-tree.ts +0 -153
- package/src/renderer/progress.ts +0 -99
- package/src/renderer/status-token.ts +0 -67
- package/src/runtime/diagnostics/panels/panel-resources.ts +0 -118
- package/src/runtime/diagnostics/panels/policy.ts +0 -177
|
@@ -8,31 +8,40 @@ import type { HookAuthoringAction, HookSimulationResult } from '@pellux/goodvibe
|
|
|
8
8
|
import type { HookChain, HookDefinition } from '@pellux/goodvibes-sdk/platform/hooks';
|
|
9
9
|
import type { HookWorkbench } from '@pellux/goodvibes-sdk/platform/hooks';
|
|
10
10
|
import { truncateDisplay } from '../utils/terminal-width.ts';
|
|
11
|
+
import {
|
|
12
|
+
type ConfirmState,
|
|
13
|
+
handleConfirmInput,
|
|
14
|
+
renderConfirmLines,
|
|
15
|
+
} from './confirm-state.ts';
|
|
11
16
|
import {
|
|
12
17
|
buildKeyValueLine,
|
|
13
18
|
buildKeyboardHints,
|
|
14
19
|
buildPanelLine,
|
|
15
20
|
buildStatusPill,
|
|
16
21
|
DEFAULT_PANEL_PALETTE,
|
|
22
|
+
resolveScrollablePanelSection,
|
|
17
23
|
} from './polish.ts';
|
|
18
24
|
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
headerBg: '#1e293b',
|
|
23
|
-
ok: '#22c55e',
|
|
24
|
-
warn: '#eab308',
|
|
25
|
-
error: '#ef4444',
|
|
26
|
-
info: '#38bdf8',
|
|
27
|
-
selectBg: '#0f172a',
|
|
28
|
-
} as const;
|
|
25
|
+
// Base chrome only — title band, state colors, and text tokens all come
|
|
26
|
+
// straight from DEFAULT_PANEL_PALETTE (WO-002).
|
|
27
|
+
const C = DEFAULT_PANEL_PALETTE;
|
|
29
28
|
|
|
29
|
+
/**
|
|
30
|
+
* WO-134: widened past read-only listing to also carry the three managed-hook
|
|
31
|
+
* mutation methods the panel drives directly (t=toggle, x=remove, s=simulate),
|
|
32
|
+
* mirroring the /hooks enable|disable|remove|simulate subcommands
|
|
33
|
+
* (hooks-runtime.ts). The raw `HookWorkbench` class instance passed at
|
|
34
|
+
* bootstrap already implements all three.
|
|
35
|
+
*/
|
|
30
36
|
export interface HooksPanelWorkbenchView {
|
|
31
37
|
listManagedHooks(): Array<{ pattern: string; hook: HookDefinition }>;
|
|
32
38
|
listManagedChains(): HookChain[];
|
|
33
39
|
listRecentActions(limit?: number): HookAuthoringAction[];
|
|
34
40
|
getLastSimulation(): HookSimulationResult | null;
|
|
35
41
|
getHooksFilePath(): string;
|
|
42
|
+
toggleManagedHook(name: string, enabled: boolean): boolean;
|
|
43
|
+
removeManagedEntry(name: string): boolean;
|
|
44
|
+
simulate(eventPath: string, payload?: Record<string, unknown>): HookSimulationResult;
|
|
36
45
|
}
|
|
37
46
|
|
|
38
47
|
export interface HooksPanelDataSource {
|
|
@@ -61,6 +70,10 @@ type HookEntry = { pattern: string; hook: HookDefinition };
|
|
|
61
70
|
|
|
62
71
|
export class HooksPanel extends ScrollableListPanel<HookEntry> {
|
|
63
72
|
private readonly dataSource: HooksPanelDataSource;
|
|
73
|
+
/** Pending confirm for 'x' remove — destructive, so it goes through the project-standard confirm contract. Subject is the managed hook name. */
|
|
74
|
+
private confirmAction: ConfirmState<string> | null = null;
|
|
75
|
+
/** Toggled by 'a' — widens the Recent Activity window from a short glance to a fuller scrollable review. */
|
|
76
|
+
private _activityExpanded = false;
|
|
64
77
|
|
|
65
78
|
public constructor(
|
|
66
79
|
hookDispatcher: Pick<HookDispatcher, 'listHooks' | 'getChains'>,
|
|
@@ -68,7 +81,7 @@ export class HooksPanel extends ScrollableListPanel<HookEntry> {
|
|
|
68
81
|
hookActivityTracker: Pick<HookActivityTracker, 'listRecent'>,
|
|
69
82
|
dataSource: HooksPanelDataSource = createDefaultDataSource(hookDispatcher, hookWorkbench, hookActivityTracker),
|
|
70
83
|
) {
|
|
71
|
-
super('hooks', 'Hooks', '
|
|
84
|
+
super('hooks', 'Hooks', '▨', 'automation-control');
|
|
72
85
|
this.showSelectionGutter = true; // I5: non-color selection affordance
|
|
73
86
|
this.filterEnabled = true;
|
|
74
87
|
this.filterLabel = 'Filter hooks';
|
|
@@ -106,19 +119,113 @@ export class HooksPanel extends ScrollableListPanel<HookEntry> {
|
|
|
106
119
|
}
|
|
107
120
|
|
|
108
121
|
public handleInput(key: string): boolean {
|
|
109
|
-
if (
|
|
110
|
-
|
|
122
|
+
if (this.lastError !== null) this.clearError();
|
|
123
|
+
|
|
124
|
+
if (this.confirmAction) {
|
|
125
|
+
const result = handleConfirmInput(this.confirmAction, key);
|
|
126
|
+
if (result === 'confirmed') {
|
|
127
|
+
this._removeManaged(this.confirmAction.subject);
|
|
128
|
+
this.confirmAction = null;
|
|
129
|
+
this.markDirty();
|
|
130
|
+
return true;
|
|
131
|
+
}
|
|
132
|
+
if (result === 'cancelled') {
|
|
133
|
+
this.confirmAction = null;
|
|
134
|
+
this.markDirty();
|
|
135
|
+
}
|
|
111
136
|
return true;
|
|
112
137
|
}
|
|
138
|
+
|
|
139
|
+
if (!this.filterActive) {
|
|
140
|
+
switch (key) {
|
|
141
|
+
case 'r':
|
|
142
|
+
this.markDirty();
|
|
143
|
+
return true;
|
|
144
|
+
case 't':
|
|
145
|
+
this._toggleSelected();
|
|
146
|
+
return true;
|
|
147
|
+
case 'x':
|
|
148
|
+
this._requestRemove();
|
|
149
|
+
return true;
|
|
150
|
+
case 's':
|
|
151
|
+
this._simulateSelected();
|
|
152
|
+
return true;
|
|
153
|
+
case 'a':
|
|
154
|
+
this._activityExpanded = !this._activityExpanded;
|
|
155
|
+
this.markDirty();
|
|
156
|
+
return true;
|
|
157
|
+
default:
|
|
158
|
+
break;
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
|
|
113
162
|
return super.handleInput(key);
|
|
114
163
|
}
|
|
115
164
|
|
|
165
|
+
private _selectedEntry(): HookEntry | undefined {
|
|
166
|
+
return this.getSelectedItem();
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
/**
|
|
170
|
+
* WO-134: t=toggle — mirrors `/hooks enable|disable <name>` (hooks-runtime.ts:64-77),
|
|
171
|
+
* flipping the selected hook's managed enabled state via workbench.toggleManagedHook.
|
|
172
|
+
*/
|
|
173
|
+
private _toggleSelected(): void {
|
|
174
|
+
const entry = this._selectedEntry();
|
|
175
|
+
if (!entry) return;
|
|
176
|
+
const name = entry.hook.name;
|
|
177
|
+
if (!name) {
|
|
178
|
+
this.setError('This hook has no managed name to toggle.');
|
|
179
|
+
this.markDirty();
|
|
180
|
+
return;
|
|
181
|
+
}
|
|
182
|
+
const nextEnabled = entry.hook.enabled === false;
|
|
183
|
+
const changed = this.dataSource.getWorkbench().toggleManagedHook(name, nextEnabled);
|
|
184
|
+
if (!changed) this.setError(`No managed hook named '${name}'.`);
|
|
185
|
+
this.markDirty();
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
private _requestRemove(): void {
|
|
189
|
+
const entry = this._selectedEntry();
|
|
190
|
+
if (!entry) return;
|
|
191
|
+
const name = entry.hook.name;
|
|
192
|
+
if (!name) {
|
|
193
|
+
this.setError('This hook has no managed name to remove.');
|
|
194
|
+
this.markDirty();
|
|
195
|
+
return;
|
|
196
|
+
}
|
|
197
|
+
this.confirmAction = { subject: name, label: name, verb: 'Remove' };
|
|
198
|
+
this.markDirty();
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
private _removeManaged(name: string): void {
|
|
202
|
+
const removed = this.dataSource.getWorkbench().removeManagedEntry(name);
|
|
203
|
+
if (!removed) this.setError(`No managed hook or chain named '${name}'.`);
|
|
204
|
+
this.markDirty();
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
/**
|
|
208
|
+
* WO-134: s=simulate — mirrors `/hooks simulate <eventPath>` (hooks-runtime.ts:78-93)
|
|
209
|
+
* using the selected hook's own pattern as the simulated event path. The
|
|
210
|
+
* result lands in workbench.getLastSimulation(), already rendered below.
|
|
211
|
+
*/
|
|
212
|
+
private _simulateSelected(): void {
|
|
213
|
+
const entry = this._selectedEntry();
|
|
214
|
+
if (!entry) return;
|
|
215
|
+
this.dataSource.getWorkbench().simulate(entry.pattern);
|
|
216
|
+
this.markDirty();
|
|
217
|
+
}
|
|
218
|
+
|
|
116
219
|
public render(width: number, height: number): Line[] {
|
|
117
220
|
this.clampSelection();
|
|
118
221
|
const hooks = this.dataSource.listHooks();
|
|
119
222
|
const contracts = this.dataSource.listContracts();
|
|
120
223
|
const chains = this.dataSource.listChains();
|
|
121
|
-
|
|
224
|
+
// Fixed-window stats stay stable regardless of the 'a' expand toggle below;
|
|
225
|
+
// only the displayed activity rows grow when expanded.
|
|
226
|
+
const recentActivityForStats = this.dataSource.listRecentActivity(3);
|
|
227
|
+
const activityLimit = this._activityExpanded ? 20 : 3;
|
|
228
|
+
const recentActivity = this.dataSource.listRecentActivity(activityLimit);
|
|
122
229
|
const workbench = this.dataSource.getWorkbench();
|
|
123
230
|
const managedHooks = workbench.listManagedHooks();
|
|
124
231
|
const managedChains = workbench.listManagedChains();
|
|
@@ -126,7 +233,7 @@ export class HooksPanel extends ScrollableListPanel<HookEntry> {
|
|
|
126
233
|
const lastSimulation = workbench.getLastSimulation();
|
|
127
234
|
const intro = 'Hook contracts, active registrations, managed authoring, recent runtime activity, and simulation matches.';
|
|
128
235
|
|
|
129
|
-
const selected =
|
|
236
|
+
const selected = this.getSelectedItem();
|
|
130
237
|
const contract = selected ? contracts.find((c) => c.pattern === selected.pattern) : undefined;
|
|
131
238
|
|
|
132
239
|
const detailLines: Line[] = [];
|
|
@@ -167,10 +274,10 @@ export class HooksPanel extends ScrollableListPanel<HookEntry> {
|
|
|
167
274
|
]));
|
|
168
275
|
}
|
|
169
276
|
|
|
170
|
-
const
|
|
277
|
+
const activityRows: Line[] = recentActivity.length === 0
|
|
171
278
|
? [buildPanelLine(width, [[' No hook activity recorded yet.', C.empty]])]
|
|
172
279
|
: recentActivity.map((record) => {
|
|
173
|
-
const color = !record.ok ? C.
|
|
280
|
+
const color = !record.ok ? C.bad : record.decision === 'deny' ? C.warn : C.good;
|
|
174
281
|
return buildPanelLine(width, [
|
|
175
282
|
[' ', C.label],
|
|
176
283
|
[truncateDisplay(record.hookName, 18).padEnd(18), C.value],
|
|
@@ -181,6 +288,20 @@ export class HooksPanel extends ScrollableListPanel<HookEntry> {
|
|
|
181
288
|
]);
|
|
182
289
|
});
|
|
183
290
|
|
|
291
|
+
// WO-134: 'a' expands the activity window from a short glance (3 rows) to
|
|
292
|
+
// a fuller scrollable review (up to 20); resolveScrollablePanelSection
|
|
293
|
+
// appends a "+N more" summary row when even the expanded fetch overflows
|
|
294
|
+
// the available space, so the list never silently truncates unlabeled.
|
|
295
|
+
const activityLines: readonly Line[] = resolveScrollablePanelSection(width, height, {
|
|
296
|
+
palette: C,
|
|
297
|
+
section: {
|
|
298
|
+
scrollableLines: activityRows,
|
|
299
|
+
scrollOffset: 0,
|
|
300
|
+
minRows: this._activityExpanded ? 8 : 3,
|
|
301
|
+
appendWindowSummary: recentActivity.length > 0 ? { dimColor: C.dim } : undefined,
|
|
302
|
+
},
|
|
303
|
+
}).section.lines;
|
|
304
|
+
|
|
184
305
|
const authoringLines: Line[] = recentAuthoring.length === 0
|
|
185
306
|
? [buildPanelLine(width, [[' No managed hook authoring actions recorded yet.', C.empty]])]
|
|
186
307
|
: recentAuthoring.map((action) => buildPanelLine(width, [
|
|
@@ -236,15 +357,15 @@ export class HooksPanel extends ScrollableListPanel<HookEntry> {
|
|
|
236
357
|
|
|
237
358
|
// Summary header — surface registration + activity counts first so the most
|
|
238
359
|
// important "what's firing" signal is visible without scrolling to the footer.
|
|
239
|
-
const denials =
|
|
240
|
-
const errors =
|
|
360
|
+
const denials = recentActivityForStats.filter((r) => r.ok && r.decision === 'deny').length;
|
|
361
|
+
const errors = recentActivityForStats.filter((r) => !r.ok).length;
|
|
241
362
|
const headerLines: Line[] = [
|
|
242
363
|
buildKeyValueLine(width, [
|
|
243
364
|
{ label: 'hooks', value: String(hooks.length), valueColor: C.info },
|
|
244
365
|
{ label: 'chains', value: String(chains.length), valueColor: C.value },
|
|
245
366
|
{ label: 'contracts', value: String(contracts.length), valueColor: C.value },
|
|
246
367
|
{ label: 'recent denials', value: String(denials), valueColor: denials > 0 ? C.warn : C.dim },
|
|
247
|
-
{ label: 'errors', value: String(errors), valueColor: errors > 0 ? C.
|
|
368
|
+
{ label: 'errors', value: String(errors), valueColor: errors > 0 ? C.bad : C.dim },
|
|
248
369
|
], C),
|
|
249
370
|
];
|
|
250
371
|
|
|
@@ -252,8 +373,11 @@ export class HooksPanel extends ScrollableListPanel<HookEntry> {
|
|
|
252
373
|
? [{ keys: 'type', label: 'filter' }, { keys: 'Enter', label: 'apply' }, { keys: 'Esc', label: 'clear' }]
|
|
253
374
|
: [
|
|
254
375
|
{ keys: 'Up/Down', label: 'move' },
|
|
376
|
+
{ keys: 't', label: 'toggle' },
|
|
377
|
+
{ keys: 'x', label: 'remove' },
|
|
378
|
+
{ keys: 's', label: 'simulate' },
|
|
379
|
+
{ keys: 'a', label: this._activityExpanded ? 'collapse activity' : 'expand activity' },
|
|
255
380
|
{ keys: 'r', label: 'refresh' },
|
|
256
|
-
{ keys: '/hooks', label: 'full listing' },
|
|
257
381
|
{ keys: '/', label: 'filter' },
|
|
258
382
|
];
|
|
259
383
|
|
|
@@ -262,10 +386,14 @@ export class HooksPanel extends ScrollableListPanel<HookEntry> {
|
|
|
262
386
|
header: headerLines,
|
|
263
387
|
footer: [
|
|
264
388
|
...detailLines,
|
|
265
|
-
buildPanelLine(width, [
|
|
389
|
+
buildPanelLine(width, [
|
|
390
|
+
[' Recent Activity', C.label],
|
|
391
|
+
[this._activityExpanded ? ' (a to collapse)' : ' (a to expand)', C.dim],
|
|
392
|
+
]),
|
|
266
393
|
...activityLines,
|
|
267
394
|
buildPanelLine(width, [[' Authoring', C.label]]),
|
|
268
395
|
...authoringLines,
|
|
396
|
+
...(this.confirmAction ? renderConfirmLines(width, this.confirmAction) : []),
|
|
269
397
|
buildKeyboardHints(width, hints, C),
|
|
270
398
|
],
|
|
271
399
|
});
|
|
@@ -1,6 +1,15 @@
|
|
|
1
|
+
import { logger } from '@pellux/goodvibes-sdk/platform/utils';
|
|
1
2
|
import type { Line } from '../types/grid.ts';
|
|
2
3
|
import { fitDisplay, truncateDisplay } from '../utils/terminal-width.ts';
|
|
3
|
-
import
|
|
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';
|
|
4
13
|
import { ScrollableListPanel } from './scrollable-list-panel.ts';
|
|
5
14
|
import {
|
|
6
15
|
buildBodyText,
|
|
@@ -13,16 +22,12 @@ import {
|
|
|
13
22
|
DEFAULT_PANEL_PALETTE,
|
|
14
23
|
type PanelPalette,
|
|
15
24
|
} from './polish.ts';
|
|
16
|
-
import type
|
|
25
|
+
import { type ConfirmState, handleConfirmInput, renderConfirmLines } from './confirm-state.ts';
|
|
26
|
+
import type { PanelIntegrationContext } from './types.ts';
|
|
17
27
|
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
headerBg: '#0f172a',
|
|
22
|
-
warn: '#f59e0b',
|
|
23
|
-
bad: '#ef4444',
|
|
24
|
-
selectBg: '#111827',
|
|
25
|
-
} as const;
|
|
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;
|
|
26
31
|
|
|
27
32
|
function classificationColor(value: string): string {
|
|
28
33
|
switch (value) {
|
|
@@ -36,17 +41,53 @@ function classificationColor(value: string): string {
|
|
|
36
41
|
}
|
|
37
42
|
}
|
|
38
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
|
+
|
|
39
62
|
export class IncidentReviewPanel extends ScrollableListPanel<FailureReport> {
|
|
40
63
|
private readonly registry?: ForensicsRegistry;
|
|
41
64
|
private readonly unsub: (() => void) | null;
|
|
42
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
|
+
|
|
43
79
|
public constructor(registry?: ForensicsRegistry) {
|
|
44
|
-
super('incident', 'Incident Review', '
|
|
80
|
+
super('incident', 'Incident Review', '◪', 'incidents-diagnostics');
|
|
45
81
|
this.showSelectionGutter = true; // I5: non-color selection affordance
|
|
46
82
|
this.filterEnabled = true;
|
|
47
83
|
this.filterLabel = 'Filter incidents';
|
|
48
84
|
this.registry = registry;
|
|
49
|
-
this.unsub = registry
|
|
85
|
+
this.unsub = registry
|
|
86
|
+
? registry.subscribe(() => {
|
|
87
|
+
this._bundleCache = null;
|
|
88
|
+
this.markDirty();
|
|
89
|
+
})
|
|
90
|
+
: null;
|
|
50
91
|
}
|
|
51
92
|
|
|
52
93
|
public override onDestroy(): void {
|
|
@@ -88,8 +129,152 @@ export class IncidentReviewPanel extends ScrollableListPanel<FailureReport> {
|
|
|
88
129
|
];
|
|
89
130
|
}
|
|
90
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
|
+
|
|
91
268
|
public render(width: number, height: number): Line[] {
|
|
92
|
-
|
|
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.';
|
|
93
278
|
|
|
94
279
|
if (!this.registry) {
|
|
95
280
|
return buildPanelWorkspace(width, height, {
|
|
@@ -98,12 +283,9 @@ export class IncidentReviewPanel extends ScrollableListPanel<FailureReport> {
|
|
|
98
283
|
sections: [{
|
|
99
284
|
lines: buildEmptyState(
|
|
100
285
|
width,
|
|
101
|
-
'
|
|
102
|
-
'
|
|
103
|
-
[
|
|
104
|
-
{ command: '/incident latest', summary: 'inspect the latest incident from the command surface' },
|
|
105
|
-
{ command: '/security', summary: 'open the broader trust and incident posture control room' },
|
|
106
|
-
],
|
|
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
|
+
[],
|
|
107
289
|
C,
|
|
108
290
|
),
|
|
109
291
|
}],
|
|
@@ -117,13 +299,19 @@ export class IncidentReviewPanel extends ScrollableListPanel<FailureReport> {
|
|
|
117
299
|
}
|
|
118
300
|
|
|
119
301
|
this.clampSelection();
|
|
120
|
-
|
|
121
|
-
|
|
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);
|
|
122
310
|
|
|
123
311
|
const headerLines: Line[] = [
|
|
124
312
|
buildKeyValueLine(width, [
|
|
125
313
|
{ label: 'incidents', value: String(reports.length), valueColor: C.value },
|
|
126
|
-
{ label: 'selected', value: `${this.selectedIndex + 1}/${
|
|
314
|
+
{ label: 'selected', value: `${this.selectedIndex + 1}/${visible.length}`, valueColor: C.info },
|
|
127
315
|
{ label: 'classification', value: selected.classification, valueColor: classificationColor(selected.classification) },
|
|
128
316
|
], C),
|
|
129
317
|
];
|
|
@@ -144,18 +332,13 @@ export class IncidentReviewPanel extends ScrollableListPanel<FailureReport> {
|
|
|
144
332
|
[' Related IDs: ', C.label],
|
|
145
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],
|
|
146
334
|
]));
|
|
147
|
-
if (
|
|
148
|
-
footerLines.push(buildPanelLine(width, [
|
|
149
|
-
|
|
150
|
-
...buildStatusPill('warn', truncateDisplay(bundle.evidence.slowPhases.join(', '), Math.max(0, width - 15))),
|
|
151
|
-
]));
|
|
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);
|
|
152
338
|
}
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
[' Root event: ', C.label],
|
|
157
|
-
[truncateDisplay(`${rootCause.sourceEventType} - ${rootCause.description}`, Math.max(0, width - 14)), C.dim],
|
|
158
|
-
]));
|
|
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);
|
|
159
342
|
}
|
|
160
343
|
const denied = selected.permissionEvidence.find((entry) => entry.approved === false);
|
|
161
344
|
if (denied) {
|
|
@@ -191,9 +374,11 @@ export class IncidentReviewPanel extends ScrollableListPanel<FailureReport> {
|
|
|
191
374
|
]));
|
|
192
375
|
}
|
|
193
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
|
+
}
|
|
194
381
|
}
|
|
195
|
-
footerLines.push(buildPanelLine(width, [[' Action Rail', C.label]]));
|
|
196
|
-
footerLines.push(buildPanelLine(width, [[` /incident latest /incident export ${selected.id} /recall capture incident ${selected.id}`, C.info]]));
|
|
197
382
|
footerLines.push(buildGuidanceLine(width, '/security', 'open the broader trust and incident posture control room', C));
|
|
198
383
|
|
|
199
384
|
const hints = this.filterActive
|
|
@@ -206,6 +391,9 @@ export class IncidentReviewPanel extends ScrollableListPanel<FailureReport> {
|
|
|
206
391
|
{ keys: '↑/↓', label: 'select incident' },
|
|
207
392
|
{ keys: 'Home/End', label: 'jump' },
|
|
208
393
|
{ keys: '/', label: 'filter' },
|
|
394
|
+
{ keys: 'x', label: 'export' },
|
|
395
|
+
{ keys: 'c', label: 'capture' },
|
|
396
|
+
...(selected.jumpLinks.length > 0 ? [{ keys: 'j', label: 'follow link' }] : []),
|
|
209
397
|
];
|
|
210
398
|
|
|
211
399
|
return this.renderList(width, height, {
|
package/src/panels/index.ts
CHANGED
|
@@ -5,7 +5,6 @@ export { PanelManager } from './panel-manager.ts';
|
|
|
5
5
|
export { TokenBudgetPanel } from './token-budget-panel.ts';
|
|
6
6
|
export { CostTrackerPanel } from './cost-tracker-panel.ts';
|
|
7
7
|
export { AgentInspectorPanel } from './agent-inspector-panel.ts';
|
|
8
|
-
export { AgentLogsPanel } from './agent-logs-panel.ts';
|
|
9
8
|
export { WrfcPanel } from './wrfc-panel.ts';
|
|
10
9
|
export { ProviderHealthPanel } from './provider-health-panel.ts';
|
|
11
10
|
export { ProviderHealthTracker } from './provider-health-tracker.ts';
|
|
@@ -13,17 +12,14 @@ export type { ProviderHealth, ProviderStatus } from './provider-health-tracker.t
|
|
|
13
12
|
export { GitPanel } from './git-panel.ts';
|
|
14
13
|
export { SymbolOutlinePanel } from './symbol-outline-panel.ts';
|
|
15
14
|
export type { SymbolEntry, SymbolKind } from './symbol-outline-panel.ts';
|
|
16
|
-
export { ProviderStatsPanel } from './provider-stats-panel.ts';
|
|
17
15
|
export { SessionBrowserPanel } from './session-browser-panel.ts';
|
|
18
16
|
export { DocsPanel } from './docs-panel.ts';
|
|
19
17
|
export { ThinkingPanel } from './thinking-panel.ts';
|
|
20
18
|
export { ToolInspectorPanel } from './tool-inspector-panel.ts';
|
|
21
|
-
export { ContextVisualizerPanel } from './context-visualizer-panel.ts';
|
|
22
19
|
export { DebugPanel } from './debug-panel.ts';
|
|
23
20
|
export type { ApiCallEntry, ApiCallStatus } from './debug-panel.ts';
|
|
24
21
|
export { registerBuiltinPanels } from './builtin-panels.ts';
|
|
25
22
|
export type { BuiltinPanelDeps } from './builtin-panels.ts';
|
|
26
|
-
export { ForensicsPanel } from './forensics-panel.ts';
|
|
27
23
|
export { IncidentReviewPanel } from './incident-review-panel.ts';
|
|
28
24
|
export { PolicyPanel } from './policy-panel.ts';
|
|
29
25
|
export { PluginsPanel } from './plugins-panel.ts';
|