@pellux/goodvibes-tui 0.28.0 → 0.29.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +101 -0
- package/README.md +1 -1
- package/package.json +1 -1
- 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-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/keybindings.ts +48 -8
- package/src/input/panel-integration-actions.ts +107 -1
- package/src/main.ts +5 -1
- 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/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 +203 -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 +309 -128
- 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/conversation-overlays.ts +25 -11
- package/src/renderer/footer-tips.ts +41 -0
- package/src/renderer/fullscreen-primitives.ts +22 -16
- package/src/renderer/help-overlay.ts +91 -14
- package/src/renderer/hint-grammar.ts +52 -0
- package/src/renderer/layout.ts +7 -7
- package/src/renderer/modal-factory.ts +23 -15
- package/src/renderer/model-picker-overlay.ts +21 -7
- package/src/renderer/overlay-box.ts +16 -10
- package/src/renderer/process-indicator.ts +3 -1
- package/src/renderer/progress.ts +5 -4
- package/src/renderer/search-overlay.ts +27 -6
- package/src/renderer/session-picker-modal.ts +6 -4
- package/src/renderer/settings-modal.ts +70 -10
- package/src/renderer/shell-surface.ts +41 -15
- package/src/renderer/status-glyphs.ts +11 -9
- package/src/renderer/theme.ts +60 -3
- package/src/renderer/ui-factory.ts +41 -30
- package/src/renderer/ui-primitives.ts +23 -2
- package/src/runtime/bootstrap-shell.ts +35 -18
- package/src/runtime/diagnostics/panels/index.ts +0 -3
- package/src/shell/ui-openers.ts +14 -22
- package/src/utils/format-duration.ts +2 -2
- package/src/utils/splash-lines.ts +44 -3
- 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/runtime/diagnostics/panels/panel-resources.ts +0 -118
- package/src/runtime/diagnostics/panels/policy.ts +0 -177
|
@@ -1,378 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* ForensicsPanel — failure forensics TUI panel.
|
|
3
|
-
*
|
|
4
|
-
* Displays the most recent failure reports with auto-classified causes,
|
|
5
|
-
* causal chains, phase timings, and jump links to related panels.
|
|
6
|
-
*
|
|
7
|
-
* Open via /forensics or the panel picker.
|
|
8
|
-
*/
|
|
9
|
-
import type { Line } from '../types/grid.ts';
|
|
10
|
-
import { fitDisplay, truncateDisplay } from '../utils/terminal-width.ts';
|
|
11
|
-
import type { ForensicsRegistry } from '@/runtime/index.ts';
|
|
12
|
-
import type { FailureReport, CausalChainEntry, PhaseTimingEntry } from '@/runtime/index.ts';
|
|
13
|
-
import { ForensicsDataPanel } from '@/runtime/index.ts';
|
|
14
|
-
import { BasePanel } from './base-panel.ts';
|
|
15
|
-
import { createEmptyLine } from '../types/grid.ts';
|
|
16
|
-
import {
|
|
17
|
-
buildEmptyState,
|
|
18
|
-
buildPanelLine,
|
|
19
|
-
buildPanelWorkspace,
|
|
20
|
-
resolveScrollablePanelSection,
|
|
21
|
-
DEFAULT_PANEL_PALETTE,
|
|
22
|
-
type PanelWorkspaceSection,
|
|
23
|
-
} from './polish.ts';
|
|
24
|
-
import { formatShortDuration } from '../utils/format-duration.ts';
|
|
25
|
-
|
|
26
|
-
// ── Colour palette ────────────────────────────────────────────────────────────
|
|
27
|
-
const C = {
|
|
28
|
-
...DEFAULT_PANEL_PALETTE,
|
|
29
|
-
header: '#94a3b8',
|
|
30
|
-
headerBg: '#1e293b',
|
|
31
|
-
reportId: '#475569',
|
|
32
|
-
timestamp: '#64748b',
|
|
33
|
-
classification: '#f97316',
|
|
34
|
-
classOk: '#22c55e',
|
|
35
|
-
classCancelled: '#94a3b8',
|
|
36
|
-
classError: '#ef4444',
|
|
37
|
-
classWarn: '#eab308',
|
|
38
|
-
summaryText: '#cbd5e1',
|
|
39
|
-
label: '#64748b',
|
|
40
|
-
value: '#e2e8f0',
|
|
41
|
-
chainRoot: '#f97316',
|
|
42
|
-
chainEntry: '#94a3b8',
|
|
43
|
-
phaseOk: '#22c55e',
|
|
44
|
-
phaseFail: '#ef4444',
|
|
45
|
-
phasePending: '#64748b',
|
|
46
|
-
jumpLink: '#38bdf8',
|
|
47
|
-
separator: '#1e293b',
|
|
48
|
-
dim: '#334155',
|
|
49
|
-
empty: '#4b5563',
|
|
50
|
-
selectBg: '#1e3a5f',
|
|
51
|
-
} as const;
|
|
52
|
-
|
|
53
|
-
// ── Helpers ────────────────────────────────────────────────────────────────────
|
|
54
|
-
|
|
55
|
-
function fmtTime(ts: number): string {
|
|
56
|
-
const d = new Date(ts);
|
|
57
|
-
const hh = String(d.getHours()).padStart(2, '0');
|
|
58
|
-
const mm = String(d.getMinutes()).padStart(2, '0');
|
|
59
|
-
const ss = String(d.getSeconds()).padStart(2, '0');
|
|
60
|
-
return `${hh}:${mm}:${ss}`;
|
|
61
|
-
}
|
|
62
|
-
|
|
63
|
-
function fmtDuration(ms: number | undefined): string {
|
|
64
|
-
return formatShortDuration(ms);
|
|
65
|
-
}
|
|
66
|
-
|
|
67
|
-
function classificationColor(cls: FailureReport['classification']): string {
|
|
68
|
-
switch (cls) {
|
|
69
|
-
case 'cancelled': return C.classCancelled;
|
|
70
|
-
case 'max_tokens': return C.classWarn;
|
|
71
|
-
case 'unknown': return C.classWarn;
|
|
72
|
-
case 'llm_error': return C.classError;
|
|
73
|
-
case 'tool_failure': return C.classError;
|
|
74
|
-
case 'permission_denied':return C.classError;
|
|
75
|
-
case 'cascade_failure': return C.classError;
|
|
76
|
-
case 'turn_timeout': return C.classError;
|
|
77
|
-
case 'compaction_error': return C.classError;
|
|
78
|
-
default: return C.classification;
|
|
79
|
-
}
|
|
80
|
-
}
|
|
81
|
-
|
|
82
|
-
// ── ForensicsPanel ────────────────────────────────────────────────────────────
|
|
83
|
-
|
|
84
|
-
export class ForensicsPanel extends BasePanel {
|
|
85
|
-
private readonly _data: ForensicsDataPanel;
|
|
86
|
-
private _unsub: (() => void) | null = null;
|
|
87
|
-
// eslint-disable-next-line @typescript-eslint/prefer-readonly -- mutated in _renderDetail via direct assignment
|
|
88
|
-
private _scrollOffset = 0;
|
|
89
|
-
/** Index of the selected report in the all-reports list (newest-first). */
|
|
90
|
-
private _selectedIndex = 0;
|
|
91
|
-
/** View mode: 'list' shows report list; 'detail' shows a single report expanded. */
|
|
92
|
-
private _mode: 'list' | 'detail' = 'list';
|
|
93
|
-
|
|
94
|
-
public constructor(registry: ForensicsRegistry) {
|
|
95
|
-
super('forensics', 'Forensics', 'F', 'monitoring');
|
|
96
|
-
this._data = new ForensicsDataPanel(registry);
|
|
97
|
-
this._unsub = this._data.subscribe(() => this.markDirty());
|
|
98
|
-
}
|
|
99
|
-
|
|
100
|
-
public override onActivate(): void {
|
|
101
|
-
super.onActivate();
|
|
102
|
-
this._scrollOffset = 0;
|
|
103
|
-
this._selectedIndex = 0;
|
|
104
|
-
this._mode = 'list';
|
|
105
|
-
}
|
|
106
|
-
|
|
107
|
-
public handleInput(key: string): boolean {
|
|
108
|
-
if (this._mode === 'list') {
|
|
109
|
-
if (key === 'up' || key === 'k') {
|
|
110
|
-
this._selectedIndex = Math.max(0, this._selectedIndex - 1);
|
|
111
|
-
this.markDirty();
|
|
112
|
-
return true;
|
|
113
|
-
}
|
|
114
|
-
if (key === 'down' || key === 'j') {
|
|
115
|
-
const count = this._data.getAll().length;
|
|
116
|
-
this._selectedIndex = Math.min(count - 1, this._selectedIndex + 1);
|
|
117
|
-
this.markDirty();
|
|
118
|
-
return true;
|
|
119
|
-
}
|
|
120
|
-
if (key === 'return' || key === 'enter') {
|
|
121
|
-
this._mode = 'detail';
|
|
122
|
-
this._scrollOffset = 0;
|
|
123
|
-
this.markDirty();
|
|
124
|
-
return true;
|
|
125
|
-
}
|
|
126
|
-
} else {
|
|
127
|
-
if (key === 'escape' || key === 'q') {
|
|
128
|
-
this._mode = 'list';
|
|
129
|
-
this.markDirty();
|
|
130
|
-
return true;
|
|
131
|
-
}
|
|
132
|
-
if (key === 'up' || key === 'k') {
|
|
133
|
-
this._scrollOffset = Math.max(0, this._scrollOffset - 1);
|
|
134
|
-
this.markDirty();
|
|
135
|
-
return true;
|
|
136
|
-
}
|
|
137
|
-
if (key === 'down' || key === 'j') {
|
|
138
|
-
this._scrollOffset++;
|
|
139
|
-
this.markDirty();
|
|
140
|
-
return true;
|
|
141
|
-
}
|
|
142
|
-
}
|
|
143
|
-
return false;
|
|
144
|
-
}
|
|
145
|
-
|
|
146
|
-
public override onDestroy(): void {
|
|
147
|
-
if (this._unsub) {
|
|
148
|
-
this._unsub();
|
|
149
|
-
this._unsub = null;
|
|
150
|
-
}
|
|
151
|
-
this._data.dispose();
|
|
152
|
-
}
|
|
153
|
-
|
|
154
|
-
public render(width: number, height: number): Line[] {
|
|
155
|
-
this.needsRender = false;
|
|
156
|
-
const intro = 'Recent failure reports, causal chains, phase timings, and cross-panel jump targets for incident investigation.';
|
|
157
|
-
const reports = this._data.getAll();
|
|
158
|
-
|
|
159
|
-
if (reports.length === 0) {
|
|
160
|
-
const workspace = buildPanelWorkspace(width, height, {
|
|
161
|
-
title: 'Failure Forensics',
|
|
162
|
-
intro,
|
|
163
|
-
sections: [{
|
|
164
|
-
lines: buildEmptyState(
|
|
165
|
-
width,
|
|
166
|
-
' No failure reports. All systems nominal.',
|
|
167
|
-
'Failures will appear here with classification, causal chains, phase timings, and jump links as runtime incidents are captured.',
|
|
168
|
-
[{ command: '/incident', summary: 'open the incident review workspace and forensics surfaces' }],
|
|
169
|
-
C,
|
|
170
|
-
),
|
|
171
|
-
}],
|
|
172
|
-
palette: C,
|
|
173
|
-
});
|
|
174
|
-
while (workspace.length < height) workspace.push(createEmptyLine(width));
|
|
175
|
-
return workspace;
|
|
176
|
-
}
|
|
177
|
-
|
|
178
|
-
const lines: Line[] = [];
|
|
179
|
-
if (this._mode === 'list') {
|
|
180
|
-
this._renderList(lines, reports, width, height, intro);
|
|
181
|
-
} else {
|
|
182
|
-
const report = reports[this._selectedIndex];
|
|
183
|
-
if (report) {
|
|
184
|
-
this._renderDetail(lines, report, width, height, intro);
|
|
185
|
-
} else {
|
|
186
|
-
this._mode = 'list';
|
|
187
|
-
this._renderList(lines, reports, width, height, intro);
|
|
188
|
-
}
|
|
189
|
-
}
|
|
190
|
-
|
|
191
|
-
while (lines.length < height) lines.push(createEmptyLine(width));
|
|
192
|
-
return lines;
|
|
193
|
-
}
|
|
194
|
-
|
|
195
|
-
// ── List view ──────────────────────────────────────────────────────────────
|
|
196
|
-
|
|
197
|
-
private _renderList(lines: Line[], reports: FailureReport[], width: number, height: number, intro: string): void {
|
|
198
|
-
// Posture summary: how many reports and how many are hard errors, so the
|
|
199
|
-
// most important signal (recent failures) is visible before scrolling.
|
|
200
|
-
const errorCount = reports.filter((r) => classificationColor(r.classification) === C.classError).length;
|
|
201
|
-
const newest = reports[0];
|
|
202
|
-
const summaryLine = buildPanelLine(width, [
|
|
203
|
-
[' reports ', C.label],
|
|
204
|
-
[String(reports.length), C.value],
|
|
205
|
-
[' errors ', C.label],
|
|
206
|
-
[String(errorCount), errorCount > 0 ? C.classError : C.classOk],
|
|
207
|
-
[' newest ', C.label],
|
|
208
|
-
[newest ? fmtTime(newest.generatedAt) : 'n/a', C.timestamp],
|
|
209
|
-
]);
|
|
210
|
-
|
|
211
|
-
const reportRows: Line[] = [
|
|
212
|
-
summaryLine,
|
|
213
|
-
buildPanelLine(width, [[' ID TIME CLASS SUMMARY', C.label]]),
|
|
214
|
-
];
|
|
215
|
-
|
|
216
|
-
for (let i = 0; i < reports.length; i++) {
|
|
217
|
-
const report = reports[i]!;
|
|
218
|
-
const isSelected = i === this._selectedIndex;
|
|
219
|
-
const bg = isSelected ? C.selectBg : undefined;
|
|
220
|
-
|
|
221
|
-
const idStr = fitDisplay(report.id, 8);
|
|
222
|
-
const timeStr = fmtTime(report.generatedAt);
|
|
223
|
-
const cls = fitDisplay(report.classification, 20);
|
|
224
|
-
const clsColor = classificationColor(report.classification);
|
|
225
|
-
const summaryMax = Math.max(0, width - 42);
|
|
226
|
-
const summaryStr = truncateDisplay(report.summary, summaryMax);
|
|
227
|
-
|
|
228
|
-
const segs: Array<[string, string, string?]> = [
|
|
229
|
-
[isSelected ? '▸' : ' ', C.jumpLink, bg],
|
|
230
|
-
[`${idStr} `, C.reportId, bg],
|
|
231
|
-
[`${timeStr} `, C.timestamp, bg],
|
|
232
|
-
[`${cls} `, clsColor, bg],
|
|
233
|
-
[summaryStr, C.summaryText, bg],
|
|
234
|
-
];
|
|
235
|
-
reportRows.push(buildPanelLine(width, segs));
|
|
236
|
-
}
|
|
237
|
-
const reportsSection = resolveScrollablePanelSection(width, height, {
|
|
238
|
-
intro,
|
|
239
|
-
palette: C,
|
|
240
|
-
section: {
|
|
241
|
-
title: 'Reports',
|
|
242
|
-
scrollableLines: reportRows,
|
|
243
|
-
selectedIndex: this._selectedIndex + 2,
|
|
244
|
-
scrollOffset: this._scrollOffset,
|
|
245
|
-
minRows: 4,
|
|
246
|
-
appendWindowSummary: {
|
|
247
|
-
dimColor: C.label,
|
|
248
|
-
formatter: () => buildPanelLine(width, [[` [${this._selectedIndex + 1}/${reports.length}] ↑/↓ navigate Enter expand report`, C.label]]),
|
|
249
|
-
},
|
|
250
|
-
},
|
|
251
|
-
});
|
|
252
|
-
this._scrollOffset = reportsSection.scrollOffset;
|
|
253
|
-
|
|
254
|
-
lines.push(...buildPanelWorkspace(width, height, {
|
|
255
|
-
title: 'Failure Forensics',
|
|
256
|
-
intro,
|
|
257
|
-
sections: [reportsSection.section],
|
|
258
|
-
palette: C,
|
|
259
|
-
}));
|
|
260
|
-
}
|
|
261
|
-
|
|
262
|
-
// ── Detail view ────────────────────────────────────────────────────────────
|
|
263
|
-
|
|
264
|
-
private _renderDetail(lines: Line[], report: FailureReport, width: number, height: number, intro: string): void {
|
|
265
|
-
const detailLines: Line[] = [];
|
|
266
|
-
|
|
267
|
-
detailLines.push(buildPanelLine(width, [
|
|
268
|
-
[' Report: ', C.label],
|
|
269
|
-
[report.id, C.value],
|
|
270
|
-
[' Generated: ', C.label],
|
|
271
|
-
[fmtTime(report.generatedAt), C.timestamp],
|
|
272
|
-
]));
|
|
273
|
-
detailLines.push(buildPanelLine(width, [
|
|
274
|
-
[' Class: ', C.label],
|
|
275
|
-
[report.classification, classificationColor(report.classification)],
|
|
276
|
-
]));
|
|
277
|
-
detailLines.push(buildPanelLine(width, [
|
|
278
|
-
[' Summary: ', C.label],
|
|
279
|
-
[truncateDisplay(report.summary, Math.max(0, width - 11)), C.summaryText],
|
|
280
|
-
]));
|
|
281
|
-
|
|
282
|
-
if (report.errorMessage) {
|
|
283
|
-
detailLines.push(buildPanelLine(width, [
|
|
284
|
-
[' Error: ', C.label],
|
|
285
|
-
[truncateDisplay(report.errorMessage, Math.max(0, width - 11)), C.classError],
|
|
286
|
-
]));
|
|
287
|
-
}
|
|
288
|
-
if (report.stopReason) {
|
|
289
|
-
detailLines.push(buildPanelLine(width, [
|
|
290
|
-
[' Stop: ', C.label],
|
|
291
|
-
[report.stopReason, C.classWarn],
|
|
292
|
-
]));
|
|
293
|
-
}
|
|
294
|
-
if (report.taskId) {
|
|
295
|
-
detailLines.push(buildPanelLine(width, [[` Task: ${report.taskId}`, C.value]]));
|
|
296
|
-
}
|
|
297
|
-
if (report.turnId) {
|
|
298
|
-
detailLines.push(buildPanelLine(width, [[` Turn: ${report.turnId}`, C.value]]));
|
|
299
|
-
}
|
|
300
|
-
|
|
301
|
-
// Phase timings
|
|
302
|
-
if (report.phaseTimings.length > 0) {
|
|
303
|
-
detailLines.push(buildPanelLine(width, [[' Phase Timings:', C.label]]));
|
|
304
|
-
for (const pt of report.phaseTimings) {
|
|
305
|
-
this._renderPhase(detailLines, pt, width);
|
|
306
|
-
}
|
|
307
|
-
}
|
|
308
|
-
|
|
309
|
-
// Causal chain
|
|
310
|
-
if (report.causalChain.length > 0) {
|
|
311
|
-
detailLines.push(buildPanelLine(width, [[' Causal Chain:', C.label]]));
|
|
312
|
-
for (const entry of report.causalChain) {
|
|
313
|
-
this._renderCausal(detailLines, entry, width);
|
|
314
|
-
}
|
|
315
|
-
}
|
|
316
|
-
|
|
317
|
-
// Jump links
|
|
318
|
-
if (report.jumpLinks.length > 0) {
|
|
319
|
-
detailLines.push(buildPanelLine(width, [[' Jump Links:', C.label]]));
|
|
320
|
-
for (const link of report.jumpLinks) {
|
|
321
|
-
const kindTag = link.kind === 'panel' ? '[panel]' : '[cmd] ';
|
|
322
|
-
detailLines.push(buildPanelLine(width, [
|
|
323
|
-
[' ', C.dim],
|
|
324
|
-
[kindTag, C.timestamp],
|
|
325
|
-
[` ${link.label}`, C.jumpLink],
|
|
326
|
-
[link.args ? ` (${link.args})` : '', C.dim],
|
|
327
|
-
]));
|
|
328
|
-
}
|
|
329
|
-
}
|
|
330
|
-
|
|
331
|
-
detailLines.push(buildPanelLine(width, [[' Esc/q: back to list Up/Down: scroll', C.dim]]));
|
|
332
|
-
|
|
333
|
-
const detailSection = resolveScrollablePanelSection(width, height, {
|
|
334
|
-
intro,
|
|
335
|
-
palette: C,
|
|
336
|
-
section: {
|
|
337
|
-
title: 'Report Detail',
|
|
338
|
-
scrollableLines: detailLines,
|
|
339
|
-
scrollOffset: this._scrollOffset,
|
|
340
|
-
minRows: 1,
|
|
341
|
-
},
|
|
342
|
-
});
|
|
343
|
-
this._scrollOffset = detailSection.scrollOffset;
|
|
344
|
-
lines.push(...buildPanelWorkspace(width, height, {
|
|
345
|
-
title: 'Failure Forensics',
|
|
346
|
-
intro,
|
|
347
|
-
sections: [detailSection.section],
|
|
348
|
-
palette: C,
|
|
349
|
-
}));
|
|
350
|
-
}
|
|
351
|
-
|
|
352
|
-
private _renderPhase(lines: Line[], pt: PhaseTimingEntry, width: number): void {
|
|
353
|
-
const statusChar = pt.success ? '✓' : '✕';
|
|
354
|
-
const statusColor = pt.success ? C.phaseOk : C.phaseFail;
|
|
355
|
-
const dur = fmtDuration(pt.durationMs);
|
|
356
|
-
const phaseLabel = fitDisplay(pt.phase, 14);
|
|
357
|
-
const errPart = pt.error ? ` ${truncateDisplay(pt.error, Math.max(0, width - 32))}` : '';
|
|
358
|
-
lines.push(buildPanelLine(width, [
|
|
359
|
-
[' ', C.dim],
|
|
360
|
-
[statusChar + ' ', statusColor],
|
|
361
|
-
[phaseLabel, C.value],
|
|
362
|
-
[dur.padStart(6, ' '), C.timestamp],
|
|
363
|
-
[errPart, C.classError],
|
|
364
|
-
]));
|
|
365
|
-
}
|
|
366
|
-
|
|
367
|
-
private _renderCausal(lines: Line[], entry: CausalChainEntry, width: number): void {
|
|
368
|
-
const prefix = entry.isRootCause ? ' * ' : ' - ';
|
|
369
|
-
const color = entry.isRootCause ? C.chainRoot : C.chainEntry;
|
|
370
|
-
const timeStr = fmtTime(entry.ts);
|
|
371
|
-
const descMax = Math.max(0, width - prefix.length - 9);
|
|
372
|
-
lines.push(buildPanelLine(width, [
|
|
373
|
-
[prefix, color],
|
|
374
|
-
[`${timeStr} `, C.timestamp],
|
|
375
|
-
[truncateDisplay(entry.description, descMax), color],
|
|
376
|
-
]));
|
|
377
|
-
}
|
|
378
|
-
}
|
|
@@ -1,259 +0,0 @@
|
|
|
1
|
-
import { listBuiltinSubscriptionProviders } from '@pellux/goodvibes-sdk/platform/config';
|
|
2
|
-
import type {
|
|
3
|
-
ProviderAccountInspectionQuery,
|
|
4
|
-
} from '../runtime/ui-service-queries.ts';
|
|
5
|
-
|
|
6
|
-
export type ProviderAuthRoute = 'api-key' | 'subscription' | 'service-oauth' | 'unconfigured';
|
|
7
|
-
export type ProviderAuthFreshness = 'healthy' | 'expiring' | 'expired' | 'pending' | 'unconfigured';
|
|
8
|
-
|
|
9
|
-
export interface ProviderUsageWindow {
|
|
10
|
-
readonly label: string;
|
|
11
|
-
readonly detail: string;
|
|
12
|
-
}
|
|
13
|
-
|
|
14
|
-
export interface ProviderRouteRecord {
|
|
15
|
-
readonly route: Exclude<ProviderAuthRoute, 'unconfigured'>;
|
|
16
|
-
readonly usable: boolean;
|
|
17
|
-
readonly freshness: ProviderAuthFreshness;
|
|
18
|
-
readonly detail: string;
|
|
19
|
-
readonly issues: readonly string[];
|
|
20
|
-
}
|
|
21
|
-
|
|
22
|
-
export interface ProviderAccountRecord {
|
|
23
|
-
readonly providerId: string;
|
|
24
|
-
readonly active: boolean;
|
|
25
|
-
readonly modelCount: number;
|
|
26
|
-
readonly configured: boolean;
|
|
27
|
-
readonly oauthReady: boolean;
|
|
28
|
-
readonly pendingLogin: boolean;
|
|
29
|
-
readonly availableRoutes: readonly ProviderAuthRoute[];
|
|
30
|
-
readonly preferredRoute: ProviderAuthRoute;
|
|
31
|
-
readonly activeRoute: ProviderAuthRoute;
|
|
32
|
-
readonly activeRouteReason: string;
|
|
33
|
-
readonly authFreshness: ProviderAuthFreshness;
|
|
34
|
-
readonly fallbackRoute?: ProviderAuthRoute;
|
|
35
|
-
readonly fallbackRisk?: string;
|
|
36
|
-
readonly expiresAt?: number;
|
|
37
|
-
readonly tokenType?: string;
|
|
38
|
-
readonly notes: readonly string[];
|
|
39
|
-
readonly usageWindows: readonly ProviderUsageWindow[];
|
|
40
|
-
readonly issues: readonly string[];
|
|
41
|
-
readonly recommendedActions: readonly string[];
|
|
42
|
-
readonly routeRecords: readonly ProviderRouteRecord[];
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
export interface ProviderAccountSnapshot {
|
|
46
|
-
readonly capturedAt: number;
|
|
47
|
-
readonly providers: readonly ProviderAccountRecord[];
|
|
48
|
-
readonly configuredCount: number;
|
|
49
|
-
readonly issueCount: number;
|
|
50
|
-
}
|
|
51
|
-
|
|
52
|
-
export interface ProviderAccountSnapshotDeps extends ProviderAccountInspectionQuery {}
|
|
53
|
-
|
|
54
|
-
export interface ProviderAccountSnapshotQuery {
|
|
55
|
-
readonly loadSnapshot: () => Promise<ProviderAccountSnapshot>;
|
|
56
|
-
}
|
|
57
|
-
|
|
58
|
-
export function createProviderAccountSnapshotQuery(
|
|
59
|
-
deps: ProviderAccountSnapshotDeps,
|
|
60
|
-
): ProviderAccountSnapshotQuery {
|
|
61
|
-
return {
|
|
62
|
-
loadSnapshot: () => buildProviderAccountSnapshot(deps),
|
|
63
|
-
};
|
|
64
|
-
}
|
|
65
|
-
|
|
66
|
-
function determineActiveRoute(routes: readonly ProviderAuthRoute[]): ProviderAuthRoute {
|
|
67
|
-
if (routes.includes('subscription')) return 'subscription';
|
|
68
|
-
if (routes.includes('service-oauth')) return 'service-oauth';
|
|
69
|
-
if (routes.includes('api-key')) return 'api-key';
|
|
70
|
-
return 'unconfigured';
|
|
71
|
-
}
|
|
72
|
-
|
|
73
|
-
function isExpired(expiresAt?: number): boolean {
|
|
74
|
-
return typeof expiresAt === 'number' && Number.isFinite(expiresAt) && expiresAt <= Date.now();
|
|
75
|
-
}
|
|
76
|
-
|
|
77
|
-
function determineFreshness(input: {
|
|
78
|
-
readonly hasSubscription: boolean;
|
|
79
|
-
readonly expiresAt?: number;
|
|
80
|
-
readonly pending: boolean;
|
|
81
|
-
readonly hasServiceOAuth: boolean;
|
|
82
|
-
readonly hasApiKey: boolean;
|
|
83
|
-
}): ProviderAuthFreshness {
|
|
84
|
-
if (input.hasSubscription) {
|
|
85
|
-
if (isExpired(input.expiresAt)) return 'expired';
|
|
86
|
-
if (input.expiresAt && input.expiresAt <= Date.now() + 24 * 60 * 60 * 1000) return 'expiring';
|
|
87
|
-
if (input.pending) return 'pending';
|
|
88
|
-
return 'healthy';
|
|
89
|
-
}
|
|
90
|
-
if (input.hasServiceOAuth || input.hasApiKey) return 'healthy';
|
|
91
|
-
return 'unconfigured';
|
|
92
|
-
}
|
|
93
|
-
|
|
94
|
-
function builtinWindowsForProvider(providerId: string): readonly ProviderUsageWindow[] {
|
|
95
|
-
if (providerId === 'openai') {
|
|
96
|
-
return [
|
|
97
|
-
{ label: '5-hour window', detail: 'Subscription-backed Codex access may be constrained by rolling 5-hour usage limits.' },
|
|
98
|
-
{ label: '1-week window', detail: 'Subscription-backed Codex access may also be constrained by a rolling weekly limit.' },
|
|
99
|
-
];
|
|
100
|
-
}
|
|
101
|
-
return [];
|
|
102
|
-
}
|
|
103
|
-
|
|
104
|
-
export async function buildProviderAccountSnapshot(
|
|
105
|
-
deps: ProviderAccountSnapshotDeps,
|
|
106
|
-
): Promise<ProviderAccountSnapshot> {
|
|
107
|
-
const models = deps.providerModels.listModels();
|
|
108
|
-
const services = deps.services.getAll();
|
|
109
|
-
const subscriptions = deps.subscriptions;
|
|
110
|
-
const builtinSubscriptionProviders = new Set(listBuiltinSubscriptionProviders().map((entry) => entry.provider));
|
|
111
|
-
const serviceInspections = await Promise.all(Object.keys(services).map(async (name) => ({
|
|
112
|
-
name,
|
|
113
|
-
inspection: await deps.services.inspect(name),
|
|
114
|
-
})));
|
|
115
|
-
const serviceOauthByProvider = new Map<string, { configured: boolean; usable: boolean }>();
|
|
116
|
-
for (const { inspection } of serviceInspections) {
|
|
117
|
-
if (!inspection || inspection.config.authType !== 'oauth') continue;
|
|
118
|
-
const providerId = inspection.config.providerId ?? inspection.config.name;
|
|
119
|
-
const existing = serviceOauthByProvider.get(providerId);
|
|
120
|
-
serviceOauthByProvider.set(providerId, {
|
|
121
|
-
configured: true,
|
|
122
|
-
usable: Boolean(existing?.usable || inspection.hasPrimaryCredential),
|
|
123
|
-
});
|
|
124
|
-
}
|
|
125
|
-
|
|
126
|
-
const providerIds = new Set<string>([
|
|
127
|
-
...models.map((model) => model.provider),
|
|
128
|
-
...Object.keys(services),
|
|
129
|
-
...subscriptions.list().map((entry) => entry.provider),
|
|
130
|
-
...subscriptions.listPending().map((entry) => entry.provider),
|
|
131
|
-
...builtinSubscriptionProviders,
|
|
132
|
-
]);
|
|
133
|
-
|
|
134
|
-
const providers = await Promise.all([...providerIds].sort((a, b) => a.localeCompare(b)).map(async (providerId) => {
|
|
135
|
-
const subscription = subscriptions.get(providerId);
|
|
136
|
-
const pending = subscriptions.getPending(providerId);
|
|
137
|
-
const serviceConfig = Object.values(services).find((entry) => (entry.providerId ?? entry.name) === providerId) ?? null;
|
|
138
|
-
const serviceOauth = serviceOauthByProvider.get(providerId);
|
|
139
|
-
const hasApiKey = Boolean(serviceConfig?.tokenKey && deps.environment.hasEnvironmentVariable(serviceConfig.tokenKey));
|
|
140
|
-
const hasSubscription = subscription != null;
|
|
141
|
-
const hasServiceOAuth = Boolean(serviceOauth?.configured);
|
|
142
|
-
const routes: ProviderAuthRoute[] = [];
|
|
143
|
-
if (hasApiKey) routes.push('api-key');
|
|
144
|
-
if (hasSubscription) routes.push('subscription');
|
|
145
|
-
if (hasServiceOAuth) routes.push('service-oauth');
|
|
146
|
-
if (routes.length === 0) routes.push('unconfigured');
|
|
147
|
-
|
|
148
|
-
const usableRoutes: Exclude<ProviderAuthRoute, 'unconfigured'>[] = [];
|
|
149
|
-
if (hasApiKey) usableRoutes.push('api-key');
|
|
150
|
-
if (hasSubscription && !isExpired(subscription.expiresAt)) usableRoutes.push('subscription');
|
|
151
|
-
if (hasServiceOAuth) usableRoutes.push('service-oauth');
|
|
152
|
-
|
|
153
|
-
const activeRoute = determineActiveRoute(usableRoutes.length > 0 ? usableRoutes : routes);
|
|
154
|
-
const preferredRoute = determineActiveRoute(routes);
|
|
155
|
-
const freshness = determineFreshness({
|
|
156
|
-
hasSubscription,
|
|
157
|
-
expiresAt: subscription?.expiresAt,
|
|
158
|
-
pending: pending != null,
|
|
159
|
-
hasServiceOAuth,
|
|
160
|
-
hasApiKey,
|
|
161
|
-
});
|
|
162
|
-
const usageWindows = builtinWindowsForProvider(providerId);
|
|
163
|
-
const routeRecords: ProviderRouteRecord[] = [];
|
|
164
|
-
|
|
165
|
-
if (hasApiKey) {
|
|
166
|
-
routeRecords.push({
|
|
167
|
-
route: 'api-key',
|
|
168
|
-
usable: true,
|
|
169
|
-
freshness: 'healthy',
|
|
170
|
-
detail: 'Ambient API key is available for direct provider access.',
|
|
171
|
-
issues: [],
|
|
172
|
-
});
|
|
173
|
-
}
|
|
174
|
-
if (hasSubscription) {
|
|
175
|
-
routeRecords.push({
|
|
176
|
-
route: 'subscription',
|
|
177
|
-
usable: !isExpired(subscription?.expiresAt),
|
|
178
|
-
freshness: pending ? 'pending' : isExpired(subscription?.expiresAt) ? 'expired' : 'healthy',
|
|
179
|
-
detail: subscription?.overrideAmbientApiKeys
|
|
180
|
-
? 'Subscription route is configured to override ambient API-key resolution.'
|
|
181
|
-
: 'Subscription route is configured, but ambient API keys remain active unless selected explicitly.',
|
|
182
|
-
issues: isExpired(subscription?.expiresAt) ? ['Stored subscription session is expired.'] : [],
|
|
183
|
-
});
|
|
184
|
-
}
|
|
185
|
-
if (hasServiceOAuth) {
|
|
186
|
-
routeRecords.push({
|
|
187
|
-
route: 'service-oauth',
|
|
188
|
-
usable: true,
|
|
189
|
-
freshness: 'healthy',
|
|
190
|
-
detail: 'Service OAuth credential is available for this provider.',
|
|
191
|
-
issues: [],
|
|
192
|
-
});
|
|
193
|
-
}
|
|
194
|
-
|
|
195
|
-
const issues: string[] = [];
|
|
196
|
-
const notes: string[] = [`${models.filter((model) => model.provider === providerId).length} model${models.filter((model) => model.provider === providerId).length === 1 ? '' : 's'} registered`];
|
|
197
|
-
if (serviceConfig) notes.push(`service config: ${serviceConfig.authType}`);
|
|
198
|
-
const recommendedActions: string[] = [];
|
|
199
|
-
if (routes.length === 1 && routes[0] === 'unconfigured') {
|
|
200
|
-
issues.push('Provider has no configured auth route.');
|
|
201
|
-
recommendedActions.push(`Configure API keys, subscriptions, or service OAuth for ${providerId}.`);
|
|
202
|
-
}
|
|
203
|
-
if (hasSubscription && isExpired(subscription?.expiresAt)) {
|
|
204
|
-
issues.push('Stored subscription session is expired and needs refresh.');
|
|
205
|
-
recommendedActions.push(`Refresh or replace the ${providerId} subscription session before relying on it.`);
|
|
206
|
-
} else if (hasSubscription && subscription?.expiresAt && subscription.expiresAt <= Date.now() + 24 * 60 * 60 * 1000) {
|
|
207
|
-
issues.push('Stored subscription session is nearing expiry.');
|
|
208
|
-
recommendedActions.push(`Renew or verify the ${providerId} subscription session soon to avoid route drift.`);
|
|
209
|
-
}
|
|
210
|
-
if (pending) {
|
|
211
|
-
issues.push('Provider has a pending OAuth login that has not been completed yet.');
|
|
212
|
-
recommendedActions.push(`Finish /subscription login ${providerId} finish <code> or clear the pending login.`);
|
|
213
|
-
}
|
|
214
|
-
if (hasSubscription && hasApiKey) {
|
|
215
|
-
issues.push('Provider has both subscription and API-key auth paths; routing must remain explicit.');
|
|
216
|
-
recommendedActions.push('Review provider routing before switching models or auth paths.');
|
|
217
|
-
}
|
|
218
|
-
if (hasServiceOAuth && !serviceOauth?.usable) {
|
|
219
|
-
issues.push('Service OAuth is configured but missing a usable credential.');
|
|
220
|
-
recommendedActions.push(`Repair service OAuth credentials for ${providerId} in /services or /settings.`);
|
|
221
|
-
}
|
|
222
|
-
|
|
223
|
-
return {
|
|
224
|
-
providerId,
|
|
225
|
-
active: activeRoute !== 'unconfigured',
|
|
226
|
-
modelCount: models.filter((model) => model.provider === providerId).length,
|
|
227
|
-
configured: hasApiKey || hasSubscription || hasServiceOAuth || models.some((model) => model.provider === providerId),
|
|
228
|
-
oauthReady: Boolean(serviceConfig?.oauth),
|
|
229
|
-
pendingLogin: Boolean(pending),
|
|
230
|
-
availableRoutes: routes,
|
|
231
|
-
preferredRoute,
|
|
232
|
-
activeRoute,
|
|
233
|
-
activeRouteReason: activeRoute === 'subscription'
|
|
234
|
-
? 'Subscription route is currently preferred.'
|
|
235
|
-
: activeRoute === 'service-oauth'
|
|
236
|
-
? 'Service OAuth route is currently preferred.'
|
|
237
|
-
: activeRoute === 'api-key'
|
|
238
|
-
? 'Ambient API-key route is currently preferred.'
|
|
239
|
-
: 'No usable auth route is configured for this provider.',
|
|
240
|
-
authFreshness: freshness,
|
|
241
|
-
fallbackRoute: activeRoute !== preferredRoute ? preferredRoute : undefined,
|
|
242
|
-
fallbackRisk: hasSubscription && hasApiKey ? 'Both subscription and API key are present; check route priority.' : undefined,
|
|
243
|
-
expiresAt: subscription?.expiresAt,
|
|
244
|
-
tokenType: subscription?.tokenType,
|
|
245
|
-
notes,
|
|
246
|
-
usageWindows,
|
|
247
|
-
issues,
|
|
248
|
-
recommendedActions,
|
|
249
|
-
routeRecords,
|
|
250
|
-
};
|
|
251
|
-
}));
|
|
252
|
-
|
|
253
|
-
return {
|
|
254
|
-
capturedAt: Date.now(),
|
|
255
|
-
providers,
|
|
256
|
-
configuredCount: providers.filter((provider) => provider.configured).length,
|
|
257
|
-
issueCount: providers.reduce((sum, provider) => sum + provider.issues.length, 0),
|
|
258
|
-
};
|
|
259
|
-
}
|