@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
|
@@ -1,255 +0,0 @@
|
|
|
1
|
-
import type { Line } from '../types/grid.ts';
|
|
2
|
-
import { createEmptyLine } from '../types/grid.ts';
|
|
3
|
-
import { truncateDisplay } from '../utils/terminal-width.ts';
|
|
4
|
-
import { ScrollableListPanel } from './scrollable-list-panel.ts';
|
|
5
|
-
import {
|
|
6
|
-
buildAlignedRow,
|
|
7
|
-
buildDetailBlock,
|
|
8
|
-
buildEmptyState,
|
|
9
|
-
buildGuidanceLine,
|
|
10
|
-
buildKeyboardHints,
|
|
11
|
-
buildKeyValueLine,
|
|
12
|
-
buildPanelLine,
|
|
13
|
-
buildSummaryBlock,
|
|
14
|
-
buildPanelWorkspace,
|
|
15
|
-
DEFAULT_PANEL_PALETTE,
|
|
16
|
-
resolvePrimaryScrollableSection,
|
|
17
|
-
type PanelWorkspaceSection,
|
|
18
|
-
} from './polish.ts';
|
|
19
|
-
import {
|
|
20
|
-
type ProviderAccountRecord,
|
|
21
|
-
type ProviderAccountSnapshot,
|
|
22
|
-
type ProviderAccountSnapshotQuery,
|
|
23
|
-
} from './provider-account-snapshot.ts';
|
|
24
|
-
|
|
25
|
-
export interface ProviderAccountsPanelDeps {
|
|
26
|
-
readonly providerAccounts: ProviderAccountSnapshotQuery;
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
const C = {
|
|
30
|
-
...DEFAULT_PANEL_PALETTE,
|
|
31
|
-
selectBg: '#1e293b',
|
|
32
|
-
} as const;
|
|
33
|
-
|
|
34
|
-
export class ProviderAccountsPanel extends ScrollableListPanel<ProviderAccountRecord> {
|
|
35
|
-
private records: ProviderAccountRecord[] = [];
|
|
36
|
-
private loading = false;
|
|
37
|
-
private readonly providerAccounts: ProviderAccountSnapshotQuery;
|
|
38
|
-
|
|
39
|
-
public constructor(deps: ProviderAccountsPanelDeps) {
|
|
40
|
-
super('accounts', 'Accounts', 'Q', 'monitoring');
|
|
41
|
-
this.showSelectionGutter = true; // I5: non-color selection affordance
|
|
42
|
-
this.providerAccounts = deps.providerAccounts;
|
|
43
|
-
void this.refresh();
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
public override onActivate(): void {
|
|
47
|
-
super.onActivate();
|
|
48
|
-
if (!this.loading) void this.refresh();
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
protected getItems(): readonly ProviderAccountRecord[] {
|
|
52
|
-
return this.records;
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
private readonly listColumns = [
|
|
56
|
-
{ width: 16 },
|
|
57
|
-
{ width: 14 },
|
|
58
|
-
{ width: 8, align: 'right' as const },
|
|
59
|
-
{ width: 11 },
|
|
60
|
-
{ width: 8, align: 'right' as const },
|
|
61
|
-
];
|
|
62
|
-
|
|
63
|
-
protected renderItem(item: ProviderAccountRecord, _index: number, selected: boolean, width: number): Line {
|
|
64
|
-
const routeFg = item.activeRoute === 'subscription' ? C.info
|
|
65
|
-
: item.activeRoute === 'api-key' ? C.warn
|
|
66
|
-
: item.activeRoute === 'service-oauth' ? C.value : C.dim;
|
|
67
|
-
const freshFg = item.authFreshness === 'expired' ? C.bad
|
|
68
|
-
: item.authFreshness === 'expiring' || item.authFreshness === 'pending' ? C.warn : C.dim;
|
|
69
|
-
return buildAlignedRow(
|
|
70
|
-
width,
|
|
71
|
-
[
|
|
72
|
-
{ text: item.providerId, fg: item.active ? C.good : C.value, bold: selected },
|
|
73
|
-
{ text: item.activeRoute, fg: routeFg },
|
|
74
|
-
{ text: `${item.modelCount} mdl`, fg: C.dim },
|
|
75
|
-
{ text: item.authFreshness, fg: freshFg },
|
|
76
|
-
{ text: item.issues.length > 0 ? `${item.issues.length} !` : 'ok', fg: item.issues.length > 0 ? C.bad : C.good },
|
|
77
|
-
],
|
|
78
|
-
this.listColumns,
|
|
79
|
-
{ selected, selectedBg: C.selectBg, marker: '▸' },
|
|
80
|
-
);
|
|
81
|
-
}
|
|
82
|
-
|
|
83
|
-
public handleInput(key: string): boolean {
|
|
84
|
-
if (key === 'r') {
|
|
85
|
-
void this.refresh();
|
|
86
|
-
return true;
|
|
87
|
-
}
|
|
88
|
-
return super.handleInput(key);
|
|
89
|
-
}
|
|
90
|
-
|
|
91
|
-
private async refresh(): Promise<void> {
|
|
92
|
-
this.loading = true;
|
|
93
|
-
this.markDirty();
|
|
94
|
-
const snapshot = await this.buildSnapshot();
|
|
95
|
-
this.records = [...snapshot.providers];
|
|
96
|
-
this.clampSelection();
|
|
97
|
-
this.loading = false;
|
|
98
|
-
this.markDirty();
|
|
99
|
-
}
|
|
100
|
-
|
|
101
|
-
private async buildSnapshot(): Promise<ProviderAccountSnapshot> {
|
|
102
|
-
return this.providerAccounts.loadSnapshot();
|
|
103
|
-
}
|
|
104
|
-
|
|
105
|
-
public render(width: number, height: number): Line[] {
|
|
106
|
-
this.needsRender = false;
|
|
107
|
-
const intro = 'Provider auth routes, subscription posture, quota-window hints, and routing-safety notes.';
|
|
108
|
-
const footerLines = [buildKeyboardHints(width, [
|
|
109
|
-
{ keys: 'Up/Down', label: 'select' },
|
|
110
|
-
{ keys: 'r', label: 'refresh' },
|
|
111
|
-
{ keys: '/accounts repair <provider>', label: 'recover routing' },
|
|
112
|
-
], C)];
|
|
113
|
-
if (this.loading && this.records.length === 0) {
|
|
114
|
-
const lines = buildPanelWorkspace(width, height, {
|
|
115
|
-
title: 'Provider Account Control Room',
|
|
116
|
-
intro,
|
|
117
|
-
sections: [{ lines: [buildPanelLine(width, [[' Loading provider account posture...', C.info]])] }],
|
|
118
|
-
palette: C,
|
|
119
|
-
});
|
|
120
|
-
while (lines.length < height) lines.push(createEmptyLine(width));
|
|
121
|
-
return lines;
|
|
122
|
-
}
|
|
123
|
-
if (this.records.length === 0) {
|
|
124
|
-
const lines = buildPanelWorkspace(width, height, {
|
|
125
|
-
title: 'Provider Account Control Room',
|
|
126
|
-
intro,
|
|
127
|
-
sections: [{
|
|
128
|
-
lines: buildEmptyState(width, ' No provider accounts discovered.', 'Configure API keys or subscriptions to populate account routing posture.', [{ command: '/provider', summary: 'review current provider and model posture' }], C),
|
|
129
|
-
}],
|
|
130
|
-
palette: C,
|
|
131
|
-
});
|
|
132
|
-
while (lines.length < height) lines.push(createEmptyLine(width));
|
|
133
|
-
return lines;
|
|
134
|
-
}
|
|
135
|
-
const issueCount = this.records.reduce((sum, record) => sum + record.issues.length + (record.fallbackRisk ? 1 : 0), 0);
|
|
136
|
-
const expiredCount = this.records.filter((record) => record.authFreshness === 'expired').length;
|
|
137
|
-
const pendingCount = this.records.filter((record) => record.pendingLogin).length;
|
|
138
|
-
const fallbackCount = this.records.filter((record) => Boolean(record.fallbackRisk)).length;
|
|
139
|
-
const selected = this.records[this.selectedIndex]!;
|
|
140
|
-
const postureLines: Line[] = [
|
|
141
|
-
buildKeyValueLine(width, [
|
|
142
|
-
{ label: 'providers', value: String(this.records.length), valueColor: C.value },
|
|
143
|
-
{ label: 'expired auth', value: String(expiredCount), valueColor: expiredCount > 0 ? C.bad : C.good },
|
|
144
|
-
{ label: 'pending login', value: String(pendingCount), valueColor: pendingCount > 0 ? C.warn : C.dim },
|
|
145
|
-
{ label: 'fallback risk', value: String(fallbackCount), valueColor: fallbackCount > 0 ? C.warn : C.good },
|
|
146
|
-
], C),
|
|
147
|
-
buildKeyValueLine(width, [
|
|
148
|
-
{ label: 'total issues', value: String(issueCount), valueColor: issueCount > 0 ? C.bad : C.good },
|
|
149
|
-
{ label: 'selected', value: selected.providerId, valueColor: C.info },
|
|
150
|
-
{ label: 'route', value: selected.activeRoute, valueColor: selected.activeRoute === 'subscription' ? C.info : selected.activeRoute === 'api-key' ? C.warn : C.value },
|
|
151
|
-
{ label: 'freshness', value: selected.authFreshness, valueColor: selected.authFreshness === 'expired' ? C.bad : selected.authFreshness === 'expiring' || selected.authFreshness === 'pending' ? C.warn : C.good },
|
|
152
|
-
], C),
|
|
153
|
-
buildGuidanceLine(width, '/accounts repair <provider>', 'review routing safety, fallback cost, and provider-specific recovery steps', C),
|
|
154
|
-
];
|
|
155
|
-
const detailRows: Line[] = [
|
|
156
|
-
buildKeyValueLine(width, [
|
|
157
|
-
{ label: 'provider', value: selected.providerId, valueColor: C.value },
|
|
158
|
-
{ label: 'active route', value: selected.activeRoute, valueColor: selected.activeRoute === 'subscription' ? C.info : selected.activeRoute === 'api-key' ? C.warn : selected.activeRoute === 'service-oauth' ? C.value : C.bad },
|
|
159
|
-
{ label: 'preferred route', value: selected.preferredRoute, valueColor: C.dim },
|
|
160
|
-
{ label: 'freshness', value: selected.authFreshness, valueColor: selected.authFreshness === 'expired' ? C.bad : selected.authFreshness === 'expiring' || selected.authFreshness === 'pending' ? C.warn : C.good },
|
|
161
|
-
], C),
|
|
162
|
-
buildKeyValueLine(width, [
|
|
163
|
-
{ label: 'configured', value: selected.configured ? 'yes' : 'no', valueColor: selected.configured ? C.good : C.bad },
|
|
164
|
-
{ label: 'oauth ready', value: selected.oauthReady ? 'yes' : 'no', valueColor: selected.oauthReady ? C.info : C.dim },
|
|
165
|
-
{ label: 'pending login', value: selected.pendingLogin ? 'yes' : 'no', valueColor: selected.pendingLogin ? C.warn : C.dim },
|
|
166
|
-
], C),
|
|
167
|
-
buildPanelLine(width, [[truncateDisplay(` Active route reason: ${selected.activeRouteReason}`, width), C.dim]]),
|
|
168
|
-
buildPanelLine(width, [[truncateDisplay(` Available routes: ${selected.availableRoutes.join(', ') || 'unconfigured'}`, width), C.dim]]),
|
|
169
|
-
];
|
|
170
|
-
if (selected.expiresAt) {
|
|
171
|
-
detailRows.push(buildPanelLine(width, [
|
|
172
|
-
[' Expires: ', C.label],
|
|
173
|
-
[new Date(selected.expiresAt).toISOString(), C.dim],
|
|
174
|
-
[' Token: ', C.label],
|
|
175
|
-
[selected.tokenType ?? 'n/a', C.value],
|
|
176
|
-
]));
|
|
177
|
-
}
|
|
178
|
-
if (selected.fallbackRisk) {
|
|
179
|
-
detailRows.push(buildPanelLine(width, [[truncateDisplay(` fallback: ${selected.fallbackRisk}`, width), C.warn]]));
|
|
180
|
-
}
|
|
181
|
-
for (const route of selected.routeRecords) {
|
|
182
|
-
detailRows.push(buildPanelLine(width, [[
|
|
183
|
-
truncateDisplay(` route ${route.route}: ${route.usable ? 'usable' : 'blocked'} • ${route.freshness} • ${route.detail}`, width),
|
|
184
|
-
route.usable ? C.dim : C.bad,
|
|
185
|
-
]]));
|
|
186
|
-
for (const issue of route.issues) {
|
|
187
|
-
detailRows.push(buildPanelLine(width, [[truncateDisplay(` issue: ${issue}`, width), C.bad]]));
|
|
188
|
-
}
|
|
189
|
-
}
|
|
190
|
-
for (const windowHint of selected.usageWindows) {
|
|
191
|
-
detailRows.push(buildPanelLine(width, [[truncateDisplay(` ${windowHint.label}: ${windowHint.detail}`, width), C.dim]]));
|
|
192
|
-
}
|
|
193
|
-
for (const issue of selected.issues) {
|
|
194
|
-
detailRows.push(buildPanelLine(width, [[truncateDisplay(` issue: ${issue}`, width), C.bad]]));
|
|
195
|
-
}
|
|
196
|
-
for (const note of selected.notes) {
|
|
197
|
-
detailRows.push(buildPanelLine(width, [[truncateDisplay(` note: ${note}`, width), C.info]]));
|
|
198
|
-
}
|
|
199
|
-
for (const action of selected.recommendedActions) {
|
|
200
|
-
detailRows.push(buildPanelLine(width, [[truncateDisplay(` next: ${action}`, width), C.value]]));
|
|
201
|
-
}
|
|
202
|
-
if (selected.issues.length === 0 && selected.notes.length === 0 && selected.usageWindows.length === 0 && selected.recommendedActions.length === 0) {
|
|
203
|
-
detailRows.push(buildPanelLine(width, [[' No active account warnings for this provider.', C.dim]]));
|
|
204
|
-
}
|
|
205
|
-
const postureSection: PanelWorkspaceSection = { lines: buildSummaryBlock(width, 'Provider posture', postureLines, C) };
|
|
206
|
-
const detailsSection: PanelWorkspaceSection = { lines: buildDetailBlock(width, 'Selected provider', detailRows, C) };
|
|
207
|
-
const rawProviderLines: Line[] = this.records.map((record, absolute) =>
|
|
208
|
-
this.renderItem(record, absolute, absolute === this.selectedIndex, width),
|
|
209
|
-
);
|
|
210
|
-
const columnHeader = buildAlignedRow(
|
|
211
|
-
width,
|
|
212
|
-
[
|
|
213
|
-
{ text: 'provider', fg: C.label, bold: true },
|
|
214
|
-
{ text: 'route', fg: C.label, bold: true },
|
|
215
|
-
{ text: 'models', fg: C.label, bold: true },
|
|
216
|
-
{ text: 'auth', fg: C.label, bold: true },
|
|
217
|
-
{ text: 'issues', fg: C.label, bold: true },
|
|
218
|
-
],
|
|
219
|
-
this.listColumns,
|
|
220
|
-
{ marker: '▸' },
|
|
221
|
-
);
|
|
222
|
-
const resolvedProvidersSection = resolvePrimaryScrollableSection(width, height, {
|
|
223
|
-
intro,
|
|
224
|
-
footerLines,
|
|
225
|
-
palette: C,
|
|
226
|
-
beforeSections: [postureSection],
|
|
227
|
-
section: {
|
|
228
|
-
title: 'Providers',
|
|
229
|
-
fixedLines: [columnHeader],
|
|
230
|
-
scrollableLines: rawProviderLines,
|
|
231
|
-
selectedIndex: this.selectedIndex,
|
|
232
|
-
scrollOffset: this.scrollStart,
|
|
233
|
-
guardRows: 1,
|
|
234
|
-
minRows: 4,
|
|
235
|
-
appendWindowSummary: { dimColor: C.dim },
|
|
236
|
-
},
|
|
237
|
-
afterSections: [detailsSection],
|
|
238
|
-
});
|
|
239
|
-
this.scrollStart = resolvedProvidersSection.scrollOffset;
|
|
240
|
-
const sections: PanelWorkspaceSection[] = [
|
|
241
|
-
postureSection,
|
|
242
|
-
resolvedProvidersSection.section,
|
|
243
|
-
detailsSection,
|
|
244
|
-
];
|
|
245
|
-
const lines = buildPanelWorkspace(width, height, {
|
|
246
|
-
title: 'Provider Account Control Room',
|
|
247
|
-
intro,
|
|
248
|
-
sections,
|
|
249
|
-
footerLines,
|
|
250
|
-
palette: C,
|
|
251
|
-
});
|
|
252
|
-
while (lines.length < height) lines.push(createEmptyLine(width));
|
|
253
|
-
return lines.slice(0, height);
|
|
254
|
-
}
|
|
255
|
-
}
|
|
@@ -1,391 +0,0 @@
|
|
|
1
|
-
import { BasePanel } from './base-panel.ts';
|
|
2
|
-
import type { Line } from '../types/grid.ts';
|
|
3
|
-
import type { ProviderEvent, TurnEvent } from '@/runtime/index.ts';
|
|
4
|
-
import type { UiEventFeed } from '../runtime/ui-events.ts';
|
|
5
|
-
import type { UiProvidersSnapshot, UiReadModel } from '../runtime/ui-read-models.ts';
|
|
6
|
-
import {
|
|
7
|
-
buildEmptyState,
|
|
8
|
-
buildKeyboardHints,
|
|
9
|
-
buildKeyValueLine,
|
|
10
|
-
buildPanelLine,
|
|
11
|
-
buildStyledPanelLine,
|
|
12
|
-
buildPanelWorkspace,
|
|
13
|
-
DEFAULT_PANEL_PALETTE,
|
|
14
|
-
extendPalette,
|
|
15
|
-
type PanelWorkspaceSection,
|
|
16
|
-
} from './polish.ts';
|
|
17
|
-
import { truncateDisplay } from '../utils/terminal-width.ts';
|
|
18
|
-
import { formatLatencyMs } from '../utils/format-duration.ts';
|
|
19
|
-
|
|
20
|
-
// ---------------------------------------------------------------------------
|
|
21
|
-
// Constants
|
|
22
|
-
// ---------------------------------------------------------------------------
|
|
23
|
-
|
|
24
|
-
// Domain-specific tones not covered by the base palette (latency/token greys).
|
|
25
|
-
const C = extendPalette(DEFAULT_PANEL_PALETTE, {
|
|
26
|
-
muted2: '#6b7280', // secondary dim grey for unit labels
|
|
27
|
-
sep: '#374151', // inline separator grey
|
|
28
|
-
tokenDim: '#64748b', // token-count grey
|
|
29
|
-
p95: '#a78bfa', // p95 latency accent
|
|
30
|
-
latYellow: '#eab308', // mid-latency warning
|
|
31
|
-
});
|
|
32
|
-
|
|
33
|
-
const SPARKLINE_CHARS = '._-:=+*#';
|
|
34
|
-
const LATENCY_RING_SIZE = 20;
|
|
35
|
-
|
|
36
|
-
/** Latency thresholds in ms for color-coding. */
|
|
37
|
-
const LATENCY_GREEN = 500;
|
|
38
|
-
const LATENCY_YELLOW = 2000;
|
|
39
|
-
|
|
40
|
-
// ---------------------------------------------------------------------------
|
|
41
|
-
// Types
|
|
42
|
-
// ---------------------------------------------------------------------------
|
|
43
|
-
|
|
44
|
-
interface ProviderRecord {
|
|
45
|
-
/** Provider name (e.g. 'anthropic', 'openai'). */
|
|
46
|
-
name: string;
|
|
47
|
-
/** Currently active model ID for this provider (last seen). */
|
|
48
|
-
lastModelId: string;
|
|
49
|
-
/** Ring buffer of per-request latencies in ms (most-recent last). */
|
|
50
|
-
latencies: number[];
|
|
51
|
-
/** Total request count. */
|
|
52
|
-
requests: number;
|
|
53
|
-
/** Error count. */
|
|
54
|
-
errors: number;
|
|
55
|
-
/** Total input + output tokens summed across all requests. */
|
|
56
|
-
totalTokens: number;
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
// ---------------------------------------------------------------------------
|
|
60
|
-
// ProviderStatsPanel
|
|
61
|
-
// ---------------------------------------------------------------------------
|
|
62
|
-
|
|
63
|
-
export class ProviderStatsPanel extends BasePanel {
|
|
64
|
-
/** Per-provider metrics keyed by provider name. */
|
|
65
|
-
private records: Map<string, ProviderRecord> = new Map();
|
|
66
|
-
|
|
67
|
-
/** Timestamp (ms) recorded at turn:start — used to compute turn latency. */
|
|
68
|
-
private _turnStartMs: number | null = null;
|
|
69
|
-
|
|
70
|
-
/** Timestamp for the current streaming LLM call start. */
|
|
71
|
-
private _streamStartMs: number | null = null;
|
|
72
|
-
|
|
73
|
-
/** Unsubscribe functions for event listeners. */
|
|
74
|
-
private _unsubs: Array<() => void> = [];
|
|
75
|
-
|
|
76
|
-
constructor(
|
|
77
|
-
private readonly turnEvents: UiEventFeed<TurnEvent>,
|
|
78
|
-
private readonly providerEvents: UiEventFeed<ProviderEvent>,
|
|
79
|
-
private readonly requestRender: () => void = () => {},
|
|
80
|
-
private readonly providers: UiReadModel<UiProvidersSnapshot>,
|
|
81
|
-
) {
|
|
82
|
-
super('providers', 'Providers', 'R', 'monitoring');
|
|
83
|
-
this._subscribe();
|
|
84
|
-
}
|
|
85
|
-
|
|
86
|
-
// -------------------------------------------------------------------------
|
|
87
|
-
// Event subscription
|
|
88
|
-
// -------------------------------------------------------------------------
|
|
89
|
-
|
|
90
|
-
private _subscribe(): void {
|
|
91
|
-
// Record when a turn starts so we can compute latency later
|
|
92
|
-
this._unsubs.push(
|
|
93
|
-
this.turnEvents.on('TURN_SUBMITTED', () => {
|
|
94
|
-
this._turnStartMs = Date.now();
|
|
95
|
-
}),
|
|
96
|
-
);
|
|
97
|
-
|
|
98
|
-
// Per-streaming-call timing (each iteration of the agentic loop)
|
|
99
|
-
this._unsubs.push(
|
|
100
|
-
this.turnEvents.on('STREAM_START', () => {
|
|
101
|
-
this._streamStartMs = Date.now();
|
|
102
|
-
}),
|
|
103
|
-
);
|
|
104
|
-
|
|
105
|
-
// After each LLM response (streamed or not), record metrics for the
|
|
106
|
-
// current provider call inside the turn loop.
|
|
107
|
-
this._unsubs.push(
|
|
108
|
-
this.turnEvents.on('LLM_RESPONSE_RECEIVED', (env) => {
|
|
109
|
-
const now = Date.now();
|
|
110
|
-
const latencyMs = this._streamStartMs !== null
|
|
111
|
-
? now - this._streamStartMs
|
|
112
|
-
: this._turnStartMs !== null
|
|
113
|
-
? now - this._turnStartMs
|
|
114
|
-
: 0;
|
|
115
|
-
// Reset stream start — ready for next iteration in the agentic loop
|
|
116
|
-
this._streamStartMs = null;
|
|
117
|
-
this._recordRequest(
|
|
118
|
-
env.provider,
|
|
119
|
-
env.model,
|
|
120
|
-
latencyMs,
|
|
121
|
-
false,
|
|
122
|
-
env.inputTokens
|
|
123
|
-
+ env.outputTokens
|
|
124
|
-
+ (env.cacheReadTokens ?? 0)
|
|
125
|
-
+ (env.cacheWriteTokens ?? 0),
|
|
126
|
-
);
|
|
127
|
-
|
|
128
|
-
this.markDirty();
|
|
129
|
-
this.requestRender();
|
|
130
|
-
}),
|
|
131
|
-
);
|
|
132
|
-
|
|
133
|
-
// On error, record a failed request
|
|
134
|
-
this._unsubs.push(
|
|
135
|
-
this.turnEvents.on('TURN_ERROR', () => {
|
|
136
|
-
this._turnStartMs = null;
|
|
137
|
-
this._streamStartMs = null;
|
|
138
|
-
this._recordRequest('unknown', 'unknown', 0, true, 0);
|
|
139
|
-
|
|
140
|
-
this.markDirty();
|
|
141
|
-
this.requestRender();
|
|
142
|
-
}),
|
|
143
|
-
);
|
|
144
|
-
|
|
145
|
-
// Re-render when providers change (new custom providers loaded)
|
|
146
|
-
this._unsubs.push(
|
|
147
|
-
this.providerEvents.on('PROVIDERS_CHANGED', () => {
|
|
148
|
-
this.markDirty();
|
|
149
|
-
this.requestRender();
|
|
150
|
-
}),
|
|
151
|
-
);
|
|
152
|
-
}
|
|
153
|
-
|
|
154
|
-
// -------------------------------------------------------------------------
|
|
155
|
-
// Metric recording
|
|
156
|
-
// -------------------------------------------------------------------------
|
|
157
|
-
|
|
158
|
-
private _recordRequest(
|
|
159
|
-
providerName: string,
|
|
160
|
-
modelId: string,
|
|
161
|
-
latencyMs: number,
|
|
162
|
-
isError: boolean,
|
|
163
|
-
tokens: number,
|
|
164
|
-
): void {
|
|
165
|
-
let rec = this.records.get(providerName);
|
|
166
|
-
if (!rec) {
|
|
167
|
-
rec = {
|
|
168
|
-
name: providerName,
|
|
169
|
-
lastModelId: modelId,
|
|
170
|
-
latencies: [],
|
|
171
|
-
requests: 0,
|
|
172
|
-
errors: 0,
|
|
173
|
-
totalTokens: 0,
|
|
174
|
-
};
|
|
175
|
-
this.records.set(providerName, rec);
|
|
176
|
-
}
|
|
177
|
-
|
|
178
|
-
rec.lastModelId = modelId;
|
|
179
|
-
rec.requests++;
|
|
180
|
-
if (isError) rec.errors++;
|
|
181
|
-
rec.totalTokens += tokens;
|
|
182
|
-
|
|
183
|
-
if (latencyMs > 0) {
|
|
184
|
-
rec.latencies.push(latencyMs);
|
|
185
|
-
// Keep only the most recent LATENCY_RING_SIZE samples
|
|
186
|
-
if (rec.latencies.length > LATENCY_RING_SIZE) {
|
|
187
|
-
rec.latencies.shift();
|
|
188
|
-
}
|
|
189
|
-
}
|
|
190
|
-
}
|
|
191
|
-
|
|
192
|
-
// -------------------------------------------------------------------------
|
|
193
|
-
// Lifecycle
|
|
194
|
-
// -------------------------------------------------------------------------
|
|
195
|
-
|
|
196
|
-
override onDestroy(): void {
|
|
197
|
-
for (const unsub of this._unsubs) unsub();
|
|
198
|
-
this._unsubs = [];
|
|
199
|
-
}
|
|
200
|
-
|
|
201
|
-
// -------------------------------------------------------------------------
|
|
202
|
-
// Rendering
|
|
203
|
-
// -------------------------------------------------------------------------
|
|
204
|
-
|
|
205
|
-
override render(width: number, height: number): Line[] {
|
|
206
|
-
const knownProviders = [...this.providers.getSnapshot().providerIds];
|
|
207
|
-
|
|
208
|
-
if (knownProviders.length === 0) {
|
|
209
|
-
return buildPanelWorkspace(width, height, {
|
|
210
|
-
title: ' Provider Stats',
|
|
211
|
-
intro: 'Per-provider request performance, latency distribution, error pressure, and session totals.',
|
|
212
|
-
sections: [
|
|
213
|
-
{
|
|
214
|
-
lines: buildEmptyState(
|
|
215
|
-
width,
|
|
216
|
-
' No providers registered',
|
|
217
|
-
'Load or configure a provider to begin collecting per-provider latency and error metrics.',
|
|
218
|
-
[
|
|
219
|
-
{ command: '/provider', summary: 'review current provider and model selection' },
|
|
220
|
-
{ command: '/subscription', summary: 'sign in to a subscription-backed provider' },
|
|
221
|
-
],
|
|
222
|
-
DEFAULT_PANEL_PALETTE,
|
|
223
|
-
),
|
|
224
|
-
},
|
|
225
|
-
],
|
|
226
|
-
palette: DEFAULT_PANEL_PALETTE,
|
|
227
|
-
});
|
|
228
|
-
}
|
|
229
|
-
|
|
230
|
-
const totalReq = [...this.records.values()].reduce((sum, rec) => sum + rec.requests, 0);
|
|
231
|
-
const totalErr = [...this.records.values()].reduce((sum, rec) => sum + rec.errors, 0);
|
|
232
|
-
const totalTok = [...this.records.values()].reduce((sum, rec) => sum + rec.totalTokens, 0);
|
|
233
|
-
const allLatencies = [...this.records.values()].flatMap((rec) => rec.latencies);
|
|
234
|
-
const providerSections: PanelWorkspaceSection[] = [
|
|
235
|
-
{
|
|
236
|
-
title: 'Session',
|
|
237
|
-
lines: [
|
|
238
|
-
buildKeyValueLine(width, [
|
|
239
|
-
{ label: 'Providers', value: String(knownProviders.length) },
|
|
240
|
-
{ label: 'Requests', value: String(totalReq), valueColor: DEFAULT_PANEL_PALETTE.info },
|
|
241
|
-
{ label: 'Errors', value: String(totalErr), valueColor: totalErr > 0 ? DEFAULT_PANEL_PALETTE.bad : DEFAULT_PANEL_PALETTE.good },
|
|
242
|
-
{ label: 'Tokens', value: String(totalTok), valueColor: DEFAULT_PANEL_PALETTE.value },
|
|
243
|
-
], DEFAULT_PANEL_PALETTE),
|
|
244
|
-
buildKeyValueLine(width, [
|
|
245
|
-
{ label: 'Avg Latency', value: this._fmtMs(this._avg(allLatencies)), valueColor: this._latencyColor(this._avg(allLatencies)) },
|
|
246
|
-
{ label: 'P95', value: this._fmtMs(this._p95(allLatencies)), valueColor: DEFAULT_PANEL_PALETTE.warn },
|
|
247
|
-
], DEFAULT_PANEL_PALETTE),
|
|
248
|
-
],
|
|
249
|
-
},
|
|
250
|
-
];
|
|
251
|
-
|
|
252
|
-
for (const provName of knownProviders) {
|
|
253
|
-
const rec = this.records.get(provName);
|
|
254
|
-
providerSections.push({
|
|
255
|
-
title: provName,
|
|
256
|
-
lines: this._buildProviderRows(provName, rec, width),
|
|
257
|
-
});
|
|
258
|
-
}
|
|
259
|
-
|
|
260
|
-
return buildPanelWorkspace(width, height, {
|
|
261
|
-
title: ' Provider Stats',
|
|
262
|
-
intro: 'Per-provider request performance, latency distribution, error pressure, and session totals.',
|
|
263
|
-
sections: providerSections,
|
|
264
|
-
footerLines: [
|
|
265
|
-
buildPanelLine(width, [
|
|
266
|
-
[' sparkline ', C.muted2],
|
|
267
|
-
['low', C.good],
|
|
268
|
-
[' → ', C.muted2],
|
|
269
|
-
['high latency', C.bad],
|
|
270
|
-
]),
|
|
271
|
-
buildKeyboardHints(width, [
|
|
272
|
-
{ keys: '/provider', label: 'switch provider/model' },
|
|
273
|
-
{ keys: '/health', label: 'live provider health' },
|
|
274
|
-
], DEFAULT_PANEL_PALETTE),
|
|
275
|
-
],
|
|
276
|
-
palette: DEFAULT_PANEL_PALETTE,
|
|
277
|
-
});
|
|
278
|
-
}
|
|
279
|
-
|
|
280
|
-
// -------------------------------------------------------------------------
|
|
281
|
-
// Line builders
|
|
282
|
-
// -------------------------------------------------------------------------
|
|
283
|
-
|
|
284
|
-
private _buildProviderRows(
|
|
285
|
-
provName: string,
|
|
286
|
-
rec: ProviderRecord | undefined,
|
|
287
|
-
width: number,
|
|
288
|
-
): Line[] {
|
|
289
|
-
const rows: Line[] = [];
|
|
290
|
-
|
|
291
|
-
// Determine health
|
|
292
|
-
const hasErrors = rec !== undefined && rec.errors > 0;
|
|
293
|
-
const dotColor = hasErrors ? C.bad : C.good;
|
|
294
|
-
|
|
295
|
-
// Model ID (truncated)
|
|
296
|
-
const modelId = rec?.lastModelId ?? 'n/a';
|
|
297
|
-
const modelDisplay = truncateDisplay(modelId, 30);
|
|
298
|
-
|
|
299
|
-
// Header row: * provider model
|
|
300
|
-
// Build as segments to avoid multi-byte char indexing issues
|
|
301
|
-
const headerLine = buildStyledPanelLine(width, [
|
|
302
|
-
{ text: ' ', fg: C.label },
|
|
303
|
-
{ text: '●', fg: dotColor },
|
|
304
|
-
{ text: ' ', fg: C.label },
|
|
305
|
-
{ text: `${truncateDisplay(provName, 14).padEnd(14)} `, fg: C.value, bold: true },
|
|
306
|
-
{ text: modelDisplay, fg: C.accent },
|
|
307
|
-
]);
|
|
308
|
-
|
|
309
|
-
rows.push(headerLine);
|
|
310
|
-
|
|
311
|
-
if (rec === undefined || rec.requests === 0) {
|
|
312
|
-
rows.push(buildStyledPanelLine(width, [
|
|
313
|
-
{ text: ' No requests yet.', fg: C.muted2 },
|
|
314
|
-
]));
|
|
315
|
-
} else {
|
|
316
|
-
const avgLatency = this._avg(rec.latencies);
|
|
317
|
-
const p95Latency = this._p95(rec.latencies);
|
|
318
|
-
const errRate = rec.requests > 0 ? (rec.errors / rec.requests) * 100 : 0;
|
|
319
|
-
const sparkline = this._sparkline(rec.latencies);
|
|
320
|
-
|
|
321
|
-
const latFg = avgLatency < LATENCY_GREEN
|
|
322
|
-
? C.good
|
|
323
|
-
: avgLatency < LATENCY_YELLOW
|
|
324
|
-
? C.latYellow
|
|
325
|
-
: C.bad;
|
|
326
|
-
|
|
327
|
-
const segments = [
|
|
328
|
-
{ text: ' avg ', fg: C.muted2 },
|
|
329
|
-
{ text: this._fmtMs(avgLatency).padStart(6), fg: latFg, bold: true },
|
|
330
|
-
{ text: ' p95 ', fg: C.muted2 },
|
|
331
|
-
{ text: this._fmtMs(p95Latency).padStart(6), fg: C.p95 },
|
|
332
|
-
{ text: ' ', fg: C.sep },
|
|
333
|
-
{ text: sparkline, fg: latFg },
|
|
334
|
-
{ text: ' err ', fg: C.muted2 },
|
|
335
|
-
{ text: `${errRate.toFixed(0).padStart(3)}%`, fg: errRate > 0 ? C.bad : C.good },
|
|
336
|
-
{ text: ` ${rec.requests.toString().padStart(4)}r`, fg: C.label },
|
|
337
|
-
] as const;
|
|
338
|
-
const tokenSegment = rec.totalTokens > 0
|
|
339
|
-
? [{ text: ` ${rec.totalTokens.toString().padStart(6)}tok`, fg: C.tokenDim }]
|
|
340
|
-
: [];
|
|
341
|
-
rows.push(buildStyledPanelLine(width, [...segments, ...tokenSegment]));
|
|
342
|
-
}
|
|
343
|
-
|
|
344
|
-
return rows;
|
|
345
|
-
}
|
|
346
|
-
|
|
347
|
-
// -------------------------------------------------------------------------
|
|
348
|
-
// Utilities
|
|
349
|
-
// -------------------------------------------------------------------------
|
|
350
|
-
|
|
351
|
-
private _avg(arr: number[]): number {
|
|
352
|
-
if (arr.length === 0) return 0;
|
|
353
|
-
return arr.reduce((s, v) => s + v, 0) / arr.length;
|
|
354
|
-
}
|
|
355
|
-
|
|
356
|
-
private _p95(arr: number[]): number {
|
|
357
|
-
if (arr.length === 0) return 0;
|
|
358
|
-
const sorted = [...arr].sort((a, b) => a - b);
|
|
359
|
-
const idx = Math.floor(sorted.length * 0.95);
|
|
360
|
-
return sorted[Math.min(idx, sorted.length - 1)] ?? 0;
|
|
361
|
-
}
|
|
362
|
-
|
|
363
|
-
private _sparkline(latencies: number[]): string {
|
|
364
|
-
if (latencies.length === 0) return ' '.repeat(LATENCY_RING_SIZE);
|
|
365
|
-
const vals = latencies.slice(-LATENCY_RING_SIZE);
|
|
366
|
-
const minV = Math.min(...vals);
|
|
367
|
-
const maxV = Math.max(...vals);
|
|
368
|
-
const range = maxV - minV || 1;
|
|
369
|
-
const spark: string[] = vals.map((v) => {
|
|
370
|
-
const idx = Math.min(
|
|
371
|
-
SPARKLINE_CHARS.length - 1,
|
|
372
|
-
Math.floor(((v - minV) / range) * (SPARKLINE_CHARS.length - 1)),
|
|
373
|
-
);
|
|
374
|
-
return SPARKLINE_CHARS[idx] ?? '.';
|
|
375
|
-
});
|
|
376
|
-
// Pad left to always be LATENCY_RING_SIZE wide
|
|
377
|
-
while (spark.length < LATENCY_RING_SIZE) spark.unshift(' ');
|
|
378
|
-
return spark.join('');
|
|
379
|
-
}
|
|
380
|
-
|
|
381
|
-
private _fmtMs(ms: number): string {
|
|
382
|
-
return formatLatencyMs(ms);
|
|
383
|
-
}
|
|
384
|
-
|
|
385
|
-
private _latencyColor(ms: number): string {
|
|
386
|
-
if (ms <= 0) return DEFAULT_PANEL_PALETTE.dim;
|
|
387
|
-
if (ms < LATENCY_GREEN) return DEFAULT_PANEL_PALETTE.good;
|
|
388
|
-
if (ms < LATENCY_YELLOW) return DEFAULT_PANEL_PALETTE.warn;
|
|
389
|
-
return DEFAULT_PANEL_PALETTE.bad;
|
|
390
|
-
}
|
|
391
|
-
}
|