@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,18 +1,30 @@
|
|
|
1
1
|
import type { ConfigManager } from '@pellux/goodvibes-sdk/platform/config';
|
|
2
|
-
import { formatLatencyMs } from '../utils/format-duration.ts';
|
|
3
2
|
import { BasePanel } from './base-panel.ts';
|
|
4
|
-
import {
|
|
5
|
-
import type {
|
|
6
|
-
import
|
|
3
|
+
import type { Line } from '../types/grid.ts';
|
|
4
|
+
import type { ModelDomainState, ProviderEvent, TurnEvent } from '@/runtime/index.ts';
|
|
5
|
+
import { createInitialModelState, evaluateSessionMaintenance } from '@/runtime/index.ts';
|
|
7
6
|
import type { UiEventFeed } from '../runtime/ui-events.ts';
|
|
8
7
|
import {
|
|
9
8
|
type ProviderRuntimeInspectionQuery,
|
|
10
9
|
} from '../runtime/ui-service-queries.ts';
|
|
11
|
-
import {
|
|
10
|
+
import {
|
|
11
|
+
ProviderHealthTracker,
|
|
12
|
+
type ProviderHealthMeta,
|
|
13
|
+
} from './provider-health-tracker.ts';
|
|
14
|
+
import {
|
|
15
|
+
buildAccountPosture,
|
|
16
|
+
type ProviderAccountPosture,
|
|
17
|
+
} from './provider-health-routes.ts';
|
|
12
18
|
import {
|
|
13
19
|
buildProviderHealthDomainSummaries,
|
|
14
20
|
type HealthDomainSummary,
|
|
15
21
|
} from './provider-health-domains.ts';
|
|
22
|
+
import {
|
|
23
|
+
ProviderHealthDataProvider,
|
|
24
|
+
type FallbackChainData,
|
|
25
|
+
type ProviderHealthEntry,
|
|
26
|
+
} from '../runtime/ui/provider-health/index.ts';
|
|
27
|
+
import type { PanelIntegrationContext } from './types.ts';
|
|
16
28
|
import type {
|
|
17
29
|
UiContinuitySnapshot,
|
|
18
30
|
UiIntelligenceSnapshot,
|
|
@@ -25,15 +37,9 @@ import type {
|
|
|
25
37
|
UiSettingsSnapshot,
|
|
26
38
|
UiWorktreeSnapshot,
|
|
27
39
|
} from '../runtime/ui-read-models.ts';
|
|
28
|
-
import { evaluateSessionMaintenance } from '@/runtime/index.ts';
|
|
29
40
|
import {
|
|
30
|
-
buildAlignedRow,
|
|
31
|
-
buildBodyText,
|
|
32
|
-
buildDetailBlock,
|
|
33
41
|
buildEmptyState,
|
|
34
|
-
buildGuidanceLine,
|
|
35
42
|
buildKeyboardHints,
|
|
36
|
-
buildKeyValueLine,
|
|
37
43
|
buildPanelWorkspace,
|
|
38
44
|
buildSummaryBlock,
|
|
39
45
|
DEFAULT_PANEL_PALETTE,
|
|
@@ -41,6 +47,17 @@ import {
|
|
|
41
47
|
resolvePrimaryScrollableSection,
|
|
42
48
|
type PanelWorkspaceSection,
|
|
43
49
|
} from './polish.ts';
|
|
50
|
+
import {
|
|
51
|
+
buildChainLines,
|
|
52
|
+
buildDomainLines,
|
|
53
|
+
buildMaintenanceLines,
|
|
54
|
+
buildPostureLines,
|
|
55
|
+
buildProviderColumnHeader,
|
|
56
|
+
buildProviderRow,
|
|
57
|
+
buildRouteColumnHeader,
|
|
58
|
+
buildRouteViewLines,
|
|
59
|
+
buildSelectedDetailSection,
|
|
60
|
+
} from './provider-health-views.ts';
|
|
44
61
|
|
|
45
62
|
// ---------------------------------------------------------------------------
|
|
46
63
|
// Types
|
|
@@ -59,307 +76,60 @@ export interface ProviderHealthPanelDeps {
|
|
|
59
76
|
readonly intelligence: UiReadModel<UiIntelligenceSnapshot>;
|
|
60
77
|
readonly continuity: UiReadModel<UiContinuitySnapshot>;
|
|
61
78
|
readonly worktrees: UiReadModel<UiWorktreeSnapshot>;
|
|
79
|
+
/**
|
|
80
|
+
* Model-domain access wired from the runtime store at the composition root
|
|
81
|
+
* (selectModel over RuntimeStore). Drives fallback-chain rendering and
|
|
82
|
+
* TURN_ERROR attribution to the active provider.
|
|
83
|
+
*/
|
|
84
|
+
readonly modelState?: {
|
|
85
|
+
readonly get: () => ModelDomainState;
|
|
86
|
+
readonly subscribe: (listener: () => void) => () => void;
|
|
87
|
+
};
|
|
62
88
|
}
|
|
63
89
|
|
|
64
|
-
|
|
65
|
-
|
|
90
|
+
type ConsoleView = 'providers' | 'routes' | 'domains';
|
|
91
|
+
|
|
92
|
+
const CONSOLE_VIEWS: readonly ConsoleView[] = ['providers', 'routes', 'domains'] as const;
|
|
66
93
|
|
|
94
|
+
// Base chrome plus console accents (domain accents only — hex ratchet).
|
|
67
95
|
const C = extendPalette(DEFAULT_PANEL_PALETTE, {
|
|
68
96
|
title: '#00ffff',
|
|
69
97
|
unknown: '244',
|
|
70
98
|
rowSelectBg: '#111827',
|
|
71
99
|
});
|
|
72
100
|
|
|
73
|
-
const
|
|
74
|
-
const LATENCY_BAD_MS = 5_000;
|
|
101
|
+
const INTRO = 'Provider console: status, latency timelines, error attribution, auth routes, and fallback posture.';
|
|
75
102
|
|
|
76
103
|
// ---------------------------------------------------------------------------
|
|
77
|
-
//
|
|
78
|
-
// ---------------------------------------------------------------------------
|
|
79
|
-
|
|
80
|
-
function statusDot(status: ProviderStatus): { char: string; color: string } {
|
|
81
|
-
switch (status) {
|
|
82
|
-
case 'healthy': return { char: '●', color: C.good };
|
|
83
|
-
case 'degraded': return { char: '◑', color: C.warn };
|
|
84
|
-
case 'rate_limited': return { char: '◐', color: C.warn };
|
|
85
|
-
case 'auth_error': return { char: '✕', color: C.bad };
|
|
86
|
-
case 'unavailable': return { char: '✕', color: C.bad };
|
|
87
|
-
default: return { char: '○', color: C.unknown };
|
|
88
|
-
}
|
|
89
|
-
}
|
|
90
|
-
|
|
91
|
-
function statusLabel(status: ProviderStatus): string {
|
|
92
|
-
switch (status) {
|
|
93
|
-
case 'healthy': return 'online';
|
|
94
|
-
case 'degraded': return 'degraded';
|
|
95
|
-
case 'rate_limited': return 'rate-limited';
|
|
96
|
-
case 'auth_error': return 'auth error';
|
|
97
|
-
case 'unavailable': return 'unavailable';
|
|
98
|
-
default: return 'unknown';
|
|
99
|
-
}
|
|
100
|
-
}
|
|
101
|
-
|
|
102
|
-
function latencyColor(ms: number): string {
|
|
103
|
-
if (ms >= LATENCY_BAD_MS) return C.bad;
|
|
104
|
-
if (ms >= LATENCY_WARN_MS) return C.warn;
|
|
105
|
-
return C.good;
|
|
106
|
-
}
|
|
107
|
-
|
|
108
|
-
function fmtMs(ms: number): string {
|
|
109
|
-
return formatLatencyMs(ms);
|
|
110
|
-
}
|
|
111
|
-
|
|
112
|
-
function fmtAgo(ts: number | undefined): string {
|
|
113
|
-
if (!ts) return 'n/a';
|
|
114
|
-
const sec = Math.floor((Date.now() - ts) / 1000);
|
|
115
|
-
if (sec < 60) return `${sec}s ago`;
|
|
116
|
-
if (sec < 3600) return `${Math.floor(sec / 60)}m ago`;
|
|
117
|
-
return `${Math.floor(sec / 3600)}h ago`;
|
|
118
|
-
}
|
|
119
|
-
|
|
120
|
-
function fmtCooldown(expiresAt: number): string {
|
|
121
|
-
const remaining = Math.ceil((expiresAt - Date.now()) / 1000);
|
|
122
|
-
if (remaining <= 0) return 'expiring';
|
|
123
|
-
return `${remaining}s cooldown`;
|
|
124
|
-
}
|
|
125
|
-
|
|
126
|
-
type ProviderPanelAuthRoute = ProviderAuthRouteDescriptor['route'] | 'unconfigured';
|
|
127
|
-
type ProviderPanelAuthFreshness = NonNullable<ProviderAuthRouteDescriptor['freshness']> | 'unconfigured';
|
|
128
|
-
|
|
129
|
-
interface ProviderRuntimeRecord {
|
|
130
|
-
readonly providerId: string;
|
|
131
|
-
readonly active: boolean;
|
|
132
|
-
readonly modelCount: number;
|
|
133
|
-
readonly activeRoute: ProviderPanelAuthRoute;
|
|
134
|
-
readonly preferredRoute: ProviderPanelAuthRoute;
|
|
135
|
-
readonly activeRouteReason: string;
|
|
136
|
-
readonly authFreshness: ProviderPanelAuthFreshness;
|
|
137
|
-
readonly fallbackRisk?: string;
|
|
138
|
-
readonly issues: readonly string[];
|
|
139
|
-
readonly recommendedActions: readonly string[];
|
|
140
|
-
}
|
|
141
|
-
|
|
142
|
-
function domainColor(level: HealthDomainSummary['level']): string {
|
|
143
|
-
switch (level) {
|
|
144
|
-
case 'good':
|
|
145
|
-
return C.good;
|
|
146
|
-
case 'warn':
|
|
147
|
-
return C.warn;
|
|
148
|
-
case 'bad':
|
|
149
|
-
return C.bad;
|
|
150
|
-
default:
|
|
151
|
-
return C.value;
|
|
152
|
-
}
|
|
153
|
-
}
|
|
154
|
-
|
|
155
|
-
const AUTH_ROUTE_PRIORITY: readonly ProviderPanelAuthRoute[] = [
|
|
156
|
-
'subscription-oauth',
|
|
157
|
-
'service-oauth',
|
|
158
|
-
'secret-ref',
|
|
159
|
-
'api-key',
|
|
160
|
-
'anonymous',
|
|
161
|
-
'none',
|
|
162
|
-
'unconfigured',
|
|
163
|
-
] as const;
|
|
164
|
-
|
|
165
|
-
function routePriority(route: ProviderPanelAuthRoute): number {
|
|
166
|
-
const priority = AUTH_ROUTE_PRIORITY.indexOf(route);
|
|
167
|
-
return priority >= 0 ? priority : AUTH_ROUTE_PRIORITY.length;
|
|
168
|
-
}
|
|
169
|
-
|
|
170
|
-
function routeColor(route: ProviderPanelAuthRoute): string {
|
|
171
|
-
switch (route) {
|
|
172
|
-
case 'subscription-oauth':
|
|
173
|
-
return C.title;
|
|
174
|
-
case 'service-oauth':
|
|
175
|
-
return C.good;
|
|
176
|
-
case 'api-key':
|
|
177
|
-
return C.warn;
|
|
178
|
-
case 'secret-ref':
|
|
179
|
-
return C.value;
|
|
180
|
-
case 'anonymous':
|
|
181
|
-
case 'none':
|
|
182
|
-
return C.dim;
|
|
183
|
-
default:
|
|
184
|
-
return C.value;
|
|
185
|
-
}
|
|
186
|
-
}
|
|
187
|
-
|
|
188
|
-
function freshnessColor(freshness: ProviderPanelAuthFreshness): string {
|
|
189
|
-
switch (freshness) {
|
|
190
|
-
case 'expired':
|
|
191
|
-
return C.bad;
|
|
192
|
-
case 'expiring':
|
|
193
|
-
case 'pending':
|
|
194
|
-
return C.warn;
|
|
195
|
-
case 'healthy':
|
|
196
|
-
return C.good;
|
|
197
|
-
default:
|
|
198
|
-
return C.dim;
|
|
199
|
-
}
|
|
200
|
-
}
|
|
201
|
-
|
|
202
|
-
function buildSyntheticAuthRoutes(
|
|
203
|
-
auth: {
|
|
204
|
-
readonly mode: 'api-key' | 'oauth' | 'anonymous' | 'none';
|
|
205
|
-
readonly configured: boolean;
|
|
206
|
-
readonly detail?: string;
|
|
207
|
-
readonly envVars?: readonly string[];
|
|
208
|
-
} | undefined,
|
|
209
|
-
): readonly ProviderAuthRouteDescriptor[] {
|
|
210
|
-
if (!auth) return [];
|
|
211
|
-
switch (auth.mode) {
|
|
212
|
-
case 'none':
|
|
213
|
-
return [{
|
|
214
|
-
route: 'none',
|
|
215
|
-
label: 'No auth required',
|
|
216
|
-
configured: true,
|
|
217
|
-
usable: true,
|
|
218
|
-
freshness: 'healthy',
|
|
219
|
-
detail: auth.detail ?? 'Provider does not require interactive credentials.',
|
|
220
|
-
}];
|
|
221
|
-
case 'anonymous':
|
|
222
|
-
return [{
|
|
223
|
-
route: 'anonymous',
|
|
224
|
-
label: 'Anonymous / local access',
|
|
225
|
-
configured: auth.configured,
|
|
226
|
-
usable: auth.configured,
|
|
227
|
-
freshness: auth.configured ? 'healthy' : 'unconfigured',
|
|
228
|
-
detail: auth.detail ?? 'Provider can be used without stored credentials.',
|
|
229
|
-
}];
|
|
230
|
-
case 'api-key':
|
|
231
|
-
return [{
|
|
232
|
-
route: 'api-key',
|
|
233
|
-
label: 'Ambient API key',
|
|
234
|
-
configured: auth.configured,
|
|
235
|
-
usable: auth.configured,
|
|
236
|
-
freshness: auth.configured ? 'healthy' : 'unconfigured',
|
|
237
|
-
detail: auth.detail ?? 'Provider expects a configured API key.',
|
|
238
|
-
...(auth.envVars?.length ? { envVars: auth.envVars } : {}),
|
|
239
|
-
...(auth.envVars?.length
|
|
240
|
-
? { repairHints: [`Set ${auth.envVars.join(' or ')} in the environment or secrets store.`] }
|
|
241
|
-
: {}),
|
|
242
|
-
}];
|
|
243
|
-
case 'oauth':
|
|
244
|
-
return [{
|
|
245
|
-
route: 'service-oauth',
|
|
246
|
-
label: 'OAuth session',
|
|
247
|
-
configured: auth.configured,
|
|
248
|
-
usable: auth.configured,
|
|
249
|
-
freshness: auth.configured ? 'healthy' : 'unconfigured',
|
|
250
|
-
detail: auth.detail ?? 'Provider expects an OAuth-backed credential.',
|
|
251
|
-
repairHints: ['Refresh or repair the provider OAuth session before relying on it.'],
|
|
252
|
-
}];
|
|
253
|
-
default:
|
|
254
|
-
return [];
|
|
255
|
-
}
|
|
256
|
-
}
|
|
257
|
-
|
|
258
|
-
function getUsableRoute(route: ProviderAuthRouteDescriptor): boolean {
|
|
259
|
-
return route.usable ?? route.configured;
|
|
260
|
-
}
|
|
261
|
-
|
|
262
|
-
function pickRoute(
|
|
263
|
-
routes: readonly ProviderAuthRouteDescriptor[],
|
|
264
|
-
): ProviderAuthRouteDescriptor | null {
|
|
265
|
-
if (routes.length === 0) return null;
|
|
266
|
-
return [...routes].sort((left, right) => routePriority(left.route) - routePriority(right.route))[0] ?? null;
|
|
267
|
-
}
|
|
268
|
-
|
|
269
|
-
function buildProviderRuntimeRecord(
|
|
270
|
-
snapshot: {
|
|
271
|
-
readonly providerId: string;
|
|
272
|
-
readonly active: boolean;
|
|
273
|
-
readonly modelCount: number;
|
|
274
|
-
readonly runtime: {
|
|
275
|
-
readonly auth?: {
|
|
276
|
-
readonly mode: 'api-key' | 'oauth' | 'anonymous' | 'none';
|
|
277
|
-
readonly configured: boolean;
|
|
278
|
-
readonly detail?: string;
|
|
279
|
-
readonly envVars?: readonly string[];
|
|
280
|
-
readonly routes?: readonly ProviderAuthRouteDescriptor[];
|
|
281
|
-
};
|
|
282
|
-
};
|
|
283
|
-
},
|
|
284
|
-
): ProviderRuntimeRecord {
|
|
285
|
-
const auth = snapshot.runtime.auth;
|
|
286
|
-
const routes = auth?.routes?.length ? auth.routes : buildSyntheticAuthRoutes(auth);
|
|
287
|
-
const configuredRoutes = routes.filter((route) => route.configured);
|
|
288
|
-
const usableRoutes = routes.filter(getUsableRoute);
|
|
289
|
-
const preferredRoute = pickRoute(configuredRoutes.length > 0 ? configuredRoutes : routes);
|
|
290
|
-
const activeRoute = pickRoute(usableRoutes.length > 0 ? usableRoutes : (preferredRoute ? [preferredRoute] : []));
|
|
291
|
-
const activeRouteId = activeRoute?.route ?? 'unconfigured';
|
|
292
|
-
const preferredRouteId = preferredRoute?.route ?? activeRouteId;
|
|
293
|
-
const authFreshness = activeRoute?.freshness ?? (activeRouteId === 'none' ? 'healthy' : 'unconfigured');
|
|
294
|
-
const fallbackRisk = usableRoutes.length > 1
|
|
295
|
-
? 'Multiple auth routes are simultaneously usable; verify route priority before switching providers.'
|
|
296
|
-
: undefined;
|
|
297
|
-
|
|
298
|
-
const issueSet = new Set<string>();
|
|
299
|
-
const actionSet = new Set<string>();
|
|
300
|
-
|
|
301
|
-
if (activeRouteId === 'unconfigured' && auth?.mode !== 'none') {
|
|
302
|
-
issueSet.add('Provider has no usable auth route configured.');
|
|
303
|
-
}
|
|
304
|
-
|
|
305
|
-
for (const route of routes) {
|
|
306
|
-
if (route.freshness === 'expired') {
|
|
307
|
-
issueSet.add(route.detail ?? `${route.label} is expired.`);
|
|
308
|
-
} else if (route.freshness === 'pending') {
|
|
309
|
-
issueSet.add(route.detail ?? `${route.label} is pending completion.`);
|
|
310
|
-
} else if (route.configured && !getUsableRoute(route)) {
|
|
311
|
-
issueSet.add(route.detail ?? `${route.label} is configured but not currently usable.`);
|
|
312
|
-
}
|
|
313
|
-
for (const hint of route.repairHints ?? []) {
|
|
314
|
-
if (hint.trim().length > 0) actionSet.add(hint);
|
|
315
|
-
}
|
|
316
|
-
}
|
|
317
|
-
|
|
318
|
-
if (fallbackRisk) issueSet.add(fallbackRisk);
|
|
319
|
-
if (issueSet.size > 0 && actionSet.size === 0 && activeRouteId !== 'none') {
|
|
320
|
-
actionSet.add(`Review ${snapshot.providerId} provider credentials and routing metadata.`);
|
|
321
|
-
}
|
|
322
|
-
|
|
323
|
-
return {
|
|
324
|
-
providerId: snapshot.providerId,
|
|
325
|
-
active: snapshot.active,
|
|
326
|
-
modelCount: snapshot.modelCount,
|
|
327
|
-
activeRoute: activeRouteId,
|
|
328
|
-
preferredRoute: preferredRouteId,
|
|
329
|
-
activeRouteReason: activeRoute?.detail
|
|
330
|
-
?? auth?.detail
|
|
331
|
-
?? (activeRouteId === 'none'
|
|
332
|
-
? 'Provider does not require interactive credentials.'
|
|
333
|
-
: 'No usable auth route is configured for this provider.'),
|
|
334
|
-
authFreshness,
|
|
335
|
-
...(fallbackRisk ? { fallbackRisk } : {}),
|
|
336
|
-
issues: [...issueSet],
|
|
337
|
-
recommendedActions: [...actionSet],
|
|
338
|
-
};
|
|
339
|
-
}
|
|
340
|
-
|
|
341
|
-
// ---------------------------------------------------------------------------
|
|
342
|
-
// ProviderHealthPanel
|
|
104
|
+
// ProviderHealthPanel — the merged provider console (WO-112)
|
|
343
105
|
// ---------------------------------------------------------------------------
|
|
344
106
|
|
|
345
107
|
/**
|
|
346
|
-
*
|
|
347
|
-
*
|
|
348
|
-
*
|
|
349
|
-
*
|
|
350
|
-
* -
|
|
351
|
-
* -
|
|
352
|
-
*
|
|
353
|
-
* -
|
|
108
|
+
* Single provider console. Absorbs the retired providers (stats) and accounts
|
|
109
|
+
* panels:
|
|
110
|
+
* - Status, latency avg/p95, error attribution, sparkline timelines, and
|
|
111
|
+
* per-provider token/cost totals in one table (ProviderHealthDataProvider).
|
|
112
|
+
* - Fallback chain with current node and fallover count.
|
|
113
|
+
* - Auth routes with per-route freshness, issues, and repair hints
|
|
114
|
+
* (ProviderRuntimeInspectionQuery.inspectAll() is the single route source).
|
|
115
|
+
* - Repair Domains + Session Maintenance as a secondary 't' view.
|
|
116
|
+
* - Enter dispatches '/accounts repair <provider>' through the panel
|
|
117
|
+
* integration context; r forces a posture refresh.
|
|
354
118
|
*/
|
|
355
119
|
export class ProviderHealthPanel extends BasePanel {
|
|
356
120
|
private _unsubs: Array<() => void> = [];
|
|
357
|
-
private
|
|
121
|
+
private _tickTimerId: ReturnType<typeof setInterval> | null = null;
|
|
358
122
|
private _selectedIndex = 0;
|
|
359
123
|
private _scrollOffset = 0;
|
|
360
|
-
private
|
|
124
|
+
private _view: ConsoleView = 'providers';
|
|
125
|
+
private _accountRecords = new Map<string, ProviderAccountPosture>();
|
|
361
126
|
private _accountRefreshAt = 0;
|
|
362
127
|
private _accountLoading = false;
|
|
128
|
+
private _inflightProvider: string | null = null;
|
|
129
|
+
private _lastResponseProvider: string | null = null;
|
|
130
|
+
private _unattributedError: string | null = null;
|
|
131
|
+
private _modelState: ModelDomainState;
|
|
132
|
+
private readonly _dataProvider: ProviderHealthDataProvider;
|
|
363
133
|
private readonly providerHealthTracker = new ProviderHealthTracker();
|
|
364
134
|
|
|
365
135
|
constructor(
|
|
@@ -367,10 +137,13 @@ export class ProviderHealthPanel extends BasePanel {
|
|
|
367
137
|
private readonly deps: ProviderHealthPanelDeps,
|
|
368
138
|
private readonly requestRender: () => void = () => {},
|
|
369
139
|
) {
|
|
370
|
-
super('provider-health', 'Health', 'N', '
|
|
140
|
+
super('provider-health', 'Health', 'N', 'providers');
|
|
141
|
+
this._modelState = deps.modelState?.get() ?? this._syntheticModelState();
|
|
142
|
+
this._dataProvider = new ProviderHealthDataProvider(this._buildHealthState(), this._modelState);
|
|
371
143
|
this._subscribe();
|
|
372
144
|
void this._refreshAccountPosture(true);
|
|
373
|
-
|
|
145
|
+
// Background 30s posture refresh (auth routes go stale otherwise); cleared on destroy.
|
|
146
|
+
this.registerTimer(setInterval(() => { void this._refreshAccountPosture(); }, 30_000));
|
|
374
147
|
}
|
|
375
148
|
|
|
376
149
|
// -------------------------------------------------------------------------
|
|
@@ -390,33 +163,79 @@ export class ProviderHealthPanel extends BasePanel {
|
|
|
390
163
|
}),
|
|
391
164
|
);
|
|
392
165
|
|
|
166
|
+
// The provider named on the in-flight LLM request is the authoritative
|
|
167
|
+
// attribution target when the turn subsequently errors.
|
|
168
|
+
this._unsubs.push(
|
|
169
|
+
this.deps.turnEvents.on('LLM_REQUEST_STARTED', (payload) => {
|
|
170
|
+
this._inflightProvider = payload.provider;
|
|
171
|
+
}),
|
|
172
|
+
);
|
|
173
|
+
|
|
393
174
|
this._unsubs.push(
|
|
394
175
|
this.deps.turnEvents.on('LLM_RESPONSE_RECEIVED', (payload) => {
|
|
395
|
-
this.providerHealthTracker.onLlmResponse(payload.provider
|
|
176
|
+
this.providerHealthTracker.onLlmResponse(payload.provider, {
|
|
177
|
+
model: payload.model,
|
|
178
|
+
inputTokens: payload.inputTokens,
|
|
179
|
+
outputTokens: payload.outputTokens,
|
|
180
|
+
...(payload.cacheReadTokens !== undefined ? { cacheReadTokens: payload.cacheReadTokens } : {}),
|
|
181
|
+
...(payload.cacheWriteTokens !== undefined ? { cacheWriteTokens: payload.cacheWriteTokens } : {}),
|
|
182
|
+
});
|
|
183
|
+
this._lastResponseProvider = payload.provider;
|
|
184
|
+
this._pushHealthState();
|
|
396
185
|
this._markDirtyAndRender();
|
|
397
186
|
}),
|
|
398
187
|
);
|
|
399
188
|
|
|
400
189
|
this._unsubs.push(
|
|
401
190
|
this.deps.turnEvents.on('TURN_ERROR', (payload) => {
|
|
402
|
-
this.
|
|
191
|
+
const providerId = this._resolveActiveProviderId();
|
|
192
|
+
if (providerId) {
|
|
193
|
+
this.providerHealthTracker.onTurnError(payload.error, providerId);
|
|
194
|
+
this._unattributedError = null;
|
|
195
|
+
} else {
|
|
196
|
+
// No provider is resolvable (nothing registered yet) — surface the
|
|
197
|
+
// error in the posture block instead of inventing a phantom row.
|
|
198
|
+
this._unattributedError = payload.error.slice(0, 120);
|
|
199
|
+
}
|
|
200
|
+
this._inflightProvider = null;
|
|
201
|
+
this._pushHealthState();
|
|
403
202
|
this._markDirtyAndRender();
|
|
404
203
|
}),
|
|
405
204
|
);
|
|
406
205
|
|
|
206
|
+
this._unsubs.push(
|
|
207
|
+
this.deps.turnEvents.on('TURN_COMPLETED', () => {
|
|
208
|
+
this._inflightProvider = null;
|
|
209
|
+
}),
|
|
210
|
+
);
|
|
211
|
+
|
|
212
|
+
this._unsubs.push(
|
|
213
|
+
this.deps.turnEvents.on('TURN_CANCEL', () => {
|
|
214
|
+
this._inflightProvider = null;
|
|
215
|
+
}),
|
|
216
|
+
);
|
|
217
|
+
|
|
407
218
|
this._unsubs.push(
|
|
408
219
|
this.deps.providerEvents.on('PROVIDERS_CHANGED', () => {
|
|
409
|
-
this.providerHealthTracker.onProvidersChanged(
|
|
410
|
-
...new Set([
|
|
411
|
-
...this.deps.providers.getSnapshot().providerIds,
|
|
412
|
-
...this.providerRuntime.listProviderIds(),
|
|
413
|
-
]),
|
|
414
|
-
]);
|
|
220
|
+
this.providerHealthTracker.onProvidersChanged(this._knownProviders());
|
|
415
221
|
void this._refreshAccountPosture(true);
|
|
222
|
+
this._pushHealthState();
|
|
416
223
|
this._markDirtyAndRender();
|
|
417
224
|
}),
|
|
418
225
|
);
|
|
419
226
|
|
|
227
|
+
if (this.deps.modelState) {
|
|
228
|
+
const modelState = this.deps.modelState;
|
|
229
|
+
this._unsubs.push(modelState.subscribe(() => {
|
|
230
|
+
const next = modelState.get();
|
|
231
|
+
if (next !== this._modelState) {
|
|
232
|
+
this._modelState = next;
|
|
233
|
+
this._dataProvider.updateModelState(next);
|
|
234
|
+
this._markDirtyAndRender();
|
|
235
|
+
}
|
|
236
|
+
}));
|
|
237
|
+
}
|
|
238
|
+
|
|
420
239
|
for (const readModel of [
|
|
421
240
|
this.deps.providers,
|
|
422
241
|
this.deps.session,
|
|
@@ -445,39 +264,57 @@ export class ProviderHealthPanel extends BasePanel {
|
|
|
445
264
|
super.onActivate();
|
|
446
265
|
this.markDirty();
|
|
447
266
|
void this._refreshAccountPosture(true);
|
|
448
|
-
this.
|
|
267
|
+
this._startTickTimer();
|
|
449
268
|
}
|
|
450
269
|
|
|
451
270
|
override onDeactivate(): void {
|
|
452
271
|
super.onDeactivate();
|
|
272
|
+
// Stop the per-second display tick while hidden (TokenBudgetPanel pattern);
|
|
273
|
+
// the 30s posture refresh keeps running so data stays warm for preload.
|
|
274
|
+
this._stopTickTimer();
|
|
453
275
|
}
|
|
454
276
|
|
|
455
277
|
override onDestroy(): void {
|
|
456
278
|
super.onDestroy();
|
|
457
|
-
this.
|
|
279
|
+
this._tickTimerId = null;
|
|
280
|
+
this._dataProvider.dispose();
|
|
458
281
|
for (const unsub of this._unsubs) unsub();
|
|
459
282
|
this._unsubs = [];
|
|
460
283
|
}
|
|
461
284
|
|
|
462
|
-
private
|
|
463
|
-
if (this.
|
|
464
|
-
this.
|
|
465
|
-
if (Date.now() - this._accountRefreshAt > 30_000) {
|
|
466
|
-
void this._refreshAccountPosture();
|
|
467
|
-
}
|
|
285
|
+
private _startTickTimer(): void {
|
|
286
|
+
if (this._tickTimerId !== null) return;
|
|
287
|
+
this._tickTimerId = this.registerTimer(setInterval(() => {
|
|
468
288
|
this.markDirty();
|
|
469
289
|
this.requestRender();
|
|
470
290
|
}, 1_000));
|
|
471
291
|
}
|
|
472
292
|
|
|
293
|
+
private _stopTickTimer(): void {
|
|
294
|
+
if (this._tickTimerId !== null) {
|
|
295
|
+
this.clearTimer(this._tickTimerId);
|
|
296
|
+
this._tickTimerId = null;
|
|
297
|
+
}
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
// -------------------------------------------------------------------------
|
|
301
|
+
// Input
|
|
302
|
+
// -------------------------------------------------------------------------
|
|
303
|
+
|
|
473
304
|
handleInput(key: string): boolean {
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
305
|
+
if (key === 't' || key === 'tab') {
|
|
306
|
+
const next = CONSOLE_VIEWS[(CONSOLE_VIEWS.indexOf(this._view) + 1) % CONSOLE_VIEWS.length];
|
|
307
|
+
this._view = next ?? 'providers';
|
|
308
|
+
this._scrollOffset = 0;
|
|
309
|
+
this.markDirty();
|
|
310
|
+
return true;
|
|
311
|
+
}
|
|
312
|
+
if (key === 'r') {
|
|
313
|
+
void this._refreshAccountPosture(true);
|
|
314
|
+
this.markDirty();
|
|
315
|
+
return true;
|
|
316
|
+
}
|
|
317
|
+
const providers = this._knownProviders();
|
|
481
318
|
if (providers.length === 0) return false;
|
|
482
319
|
if (key === 'j' || key === 'down' || key === '\x1b[B') {
|
|
483
320
|
this._selectedIndex = Math.min(providers.length - 1, this._selectedIndex + 1);
|
|
@@ -492,6 +329,95 @@ export class ProviderHealthPanel extends BasePanel {
|
|
|
492
329
|
return false;
|
|
493
330
|
}
|
|
494
331
|
|
|
332
|
+
/** Enter dispatches the real repair command for the selected provider. */
|
|
333
|
+
handlePanelIntegrationAction(key: string, ctx: PanelIntegrationContext): boolean {
|
|
334
|
+
if (key !== 'enter' && key !== 'return') return false;
|
|
335
|
+
if (!ctx.executeCommand) return false;
|
|
336
|
+
const provider = this._selectedProvider();
|
|
337
|
+
if (!provider) return false;
|
|
338
|
+
void ctx.executeCommand('accounts', ['repair', provider]).catch(() => {
|
|
339
|
+
// Command output surfaces in conversation; dispatch failures are non-fatal here.
|
|
340
|
+
});
|
|
341
|
+
return true;
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
// -------------------------------------------------------------------------
|
|
345
|
+
// Data plumbing
|
|
346
|
+
// -------------------------------------------------------------------------
|
|
347
|
+
|
|
348
|
+
private _knownProviders(): string[] {
|
|
349
|
+
const known = new Set<string>([
|
|
350
|
+
...this.deps.providers.getSnapshot().providerIds,
|
|
351
|
+
...this.providerRuntime.listProviderIds(),
|
|
352
|
+
...this._accountRecords.keys(),
|
|
353
|
+
]);
|
|
354
|
+
for (const health of this.providerHealthTracker.getAll()) known.add(health.name);
|
|
355
|
+
return [...known].sort();
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
private _selectedProvider(): string | undefined {
|
|
359
|
+
const providers = this._knownProviders();
|
|
360
|
+
return providers[Math.min(this._selectedIndex, Math.max(0, providers.length - 1))];
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
/**
|
|
364
|
+
* Resolve the provider that should own a TURN_ERROR: the in-flight LLM
|
|
365
|
+
* request's provider, then the store model domain, then the configured
|
|
366
|
+
* provider.model, then the last responding provider, then the sole known
|
|
367
|
+
* provider. Never returns 'unknown'.
|
|
368
|
+
*/
|
|
369
|
+
private _resolveActiveProviderId(): string | undefined {
|
|
370
|
+
if (this._inflightProvider) return this._inflightProvider;
|
|
371
|
+
if (this.deps.modelState) {
|
|
372
|
+
const storeProvider = this.deps.modelState.get().activeProviderId;
|
|
373
|
+
if (storeProvider && storeProvider !== 'unknown') return storeProvider;
|
|
374
|
+
}
|
|
375
|
+
const raw = this.deps.configManager.get('provider.model');
|
|
376
|
+
if (typeof raw === 'string' && raw.includes(':')) {
|
|
377
|
+
const providerId = raw.split(':')[0];
|
|
378
|
+
if (providerId) return providerId;
|
|
379
|
+
}
|
|
380
|
+
if (this._lastResponseProvider) return this._lastResponseProvider;
|
|
381
|
+
const known = this._knownProviders();
|
|
382
|
+
if (known.length === 1) return known[0];
|
|
383
|
+
return undefined;
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
private _syntheticModelState(): ModelDomainState {
|
|
387
|
+
const base = createInitialModelState();
|
|
388
|
+
const raw = this.deps.configManager.get('provider.model');
|
|
389
|
+
if (typeof raw !== 'string' || !raw.includes(':')) return base;
|
|
390
|
+
const [providerId = '', ...rest] = raw.split(':');
|
|
391
|
+
const modelId = rest.join(':');
|
|
392
|
+
if (!providerId || !modelId) return base;
|
|
393
|
+
return {
|
|
394
|
+
...base,
|
|
395
|
+
activeProviderId: providerId,
|
|
396
|
+
activeModelId: modelId,
|
|
397
|
+
displayName: raw,
|
|
398
|
+
registryKey: raw,
|
|
399
|
+
source: 'provider-health-panel',
|
|
400
|
+
lastUpdatedAt: Date.now(),
|
|
401
|
+
};
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
private _buildHealthState() {
|
|
405
|
+
const activeId = this._resolveActiveProviderId();
|
|
406
|
+
const meta: ProviderHealthMeta[] = this._knownProviders().map((providerId) => {
|
|
407
|
+
const account = this._accountRecords.get(providerId);
|
|
408
|
+
return {
|
|
409
|
+
providerId,
|
|
410
|
+
isActive: providerId === activeId,
|
|
411
|
+
isConfigured: account ? account.activeRoute !== 'unconfigured' : true,
|
|
412
|
+
};
|
|
413
|
+
});
|
|
414
|
+
return this.providerHealthTracker.buildHealthDomainState(meta);
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
private _pushHealthState(): void {
|
|
418
|
+
this._dataProvider.updateHealthState(this._buildHealthState());
|
|
419
|
+
}
|
|
420
|
+
|
|
495
421
|
private async _refreshAccountPosture(force = false): Promise<void> {
|
|
496
422
|
if (this._accountLoading) return;
|
|
497
423
|
if (!force && Date.now() - this._accountRefreshAt < 15_000) return;
|
|
@@ -500,10 +426,11 @@ export class ProviderHealthPanel extends BasePanel {
|
|
|
500
426
|
const snapshots = await this.providerRuntime.inspectAll();
|
|
501
427
|
this._accountRecords = new Map(
|
|
502
428
|
snapshots
|
|
503
|
-
.map((snapshot) =>
|
|
429
|
+
.map((snapshot) => buildAccountPosture(snapshot))
|
|
504
430
|
.map((record) => [record.providerId, record] as const),
|
|
505
431
|
);
|
|
506
432
|
this._accountRefreshAt = Date.now();
|
|
433
|
+
this._pushHealthState();
|
|
507
434
|
this.markDirty();
|
|
508
435
|
this.requestRender();
|
|
509
436
|
} finally {
|
|
@@ -511,26 +438,45 @@ export class ProviderHealthPanel extends BasePanel {
|
|
|
511
438
|
}
|
|
512
439
|
}
|
|
513
440
|
|
|
441
|
+
private _collectDomainSummaries(): HealthDomainSummary[] {
|
|
442
|
+
return [...buildProviderHealthDomainSummaries({
|
|
443
|
+
configManager: this.deps.configManager,
|
|
444
|
+
auth: this.deps.localAuth.getSnapshot(),
|
|
445
|
+
settings: this.deps.settings.getSnapshot(),
|
|
446
|
+
remote: this.deps.remote.getSnapshot(),
|
|
447
|
+
security: this.deps.security.getSnapshot(),
|
|
448
|
+
intelligence: this.deps.intelligence.getSnapshot(),
|
|
449
|
+
continuity: this.deps.continuity.getSnapshot(),
|
|
450
|
+
worktrees: this.deps.worktrees.getSnapshot(),
|
|
451
|
+
session: this.deps.session.getSnapshot(),
|
|
452
|
+
})];
|
|
453
|
+
}
|
|
454
|
+
|
|
455
|
+
private _evaluateMaintenance() {
|
|
456
|
+
const session = this.deps.session.getSnapshot();
|
|
457
|
+
return evaluateSessionMaintenance({
|
|
458
|
+
configManager: this.deps.configManager,
|
|
459
|
+
currentTokens: session.estimatedContextTokens,
|
|
460
|
+
contextWindow: session.contextWindow,
|
|
461
|
+
messageCount: session.messageCount,
|
|
462
|
+
session: session.session,
|
|
463
|
+
});
|
|
464
|
+
}
|
|
465
|
+
|
|
514
466
|
// -------------------------------------------------------------------------
|
|
515
467
|
// Rendering
|
|
516
468
|
// -------------------------------------------------------------------------
|
|
517
469
|
|
|
518
470
|
override render(width: number, height: number): Line[] {
|
|
519
|
-
const
|
|
471
|
+
const palette = { ...DEFAULT_PANEL_PALETTE, header: C.title };
|
|
520
472
|
|
|
521
|
-
const
|
|
522
|
-
...this.deps.providers.getSnapshot().providerIds,
|
|
523
|
-
...this.providerRuntime.listProviderIds(),
|
|
524
|
-
...this._accountRecords.keys(),
|
|
525
|
-
]);
|
|
526
|
-
for (const h of this.providerHealthTracker.getAll()) knownSet.add(h.name);
|
|
527
|
-
const providers = [...knownSet].sort();
|
|
473
|
+
const providers = this._knownProviders();
|
|
528
474
|
this._selectedIndex = Math.min(this._selectedIndex, Math.max(0, providers.length - 1));
|
|
529
475
|
|
|
530
476
|
if (providers.length === 0) {
|
|
531
477
|
return buildPanelWorkspace(width, height, {
|
|
532
478
|
title: 'Health',
|
|
533
|
-
intro,
|
|
479
|
+
intro: INTRO,
|
|
534
480
|
sections: [{
|
|
535
481
|
lines: buildEmptyState(
|
|
536
482
|
width,
|
|
@@ -540,201 +486,97 @@ export class ProviderHealthPanel extends BasePanel {
|
|
|
540
486
|
{ command: '/provider', summary: 'review current provider and model selection' },
|
|
541
487
|
{ command: '/subscription', summary: 'review provider login and subscription state' },
|
|
542
488
|
],
|
|
543
|
-
|
|
489
|
+
palette,
|
|
544
490
|
),
|
|
545
491
|
}],
|
|
546
|
-
palette
|
|
492
|
+
palette,
|
|
547
493
|
});
|
|
548
494
|
}
|
|
549
495
|
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
let accountIssues = 0;
|
|
554
|
-
let expiringAuth = 0;
|
|
555
|
-
for (const name of providers) {
|
|
556
|
-
const status = this.providerHealthTracker.get(name)?.status ?? 'unknown';
|
|
557
|
-
if (status === 'healthy') online++;
|
|
558
|
-
else if (status === 'rate_limited') rateLimited++;
|
|
559
|
-
else if (status === 'degraded' || status === 'auth_error' || status === 'unavailable') errored++;
|
|
560
|
-
const account = this._accountRecords.get(name);
|
|
561
|
-
if (account) {
|
|
562
|
-
accountIssues += account.issues.length;
|
|
563
|
-
if (account.authFreshness === 'expiring' || account.authFreshness === 'expired' || account.authFreshness === 'pending') {
|
|
564
|
-
expiringAuth++;
|
|
565
|
-
}
|
|
566
|
-
}
|
|
567
|
-
}
|
|
496
|
+
const snapshot = this._dataProvider.getSnapshot();
|
|
497
|
+
const entriesById = new Map(snapshot.entries.map((entry) => [entry.providerId, entry] as const));
|
|
498
|
+
const selectedName = providers[this._selectedIndex];
|
|
568
499
|
|
|
569
|
-
const
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
const domainLines: Line[] = [];
|
|
583
|
-
for (const domain of buildProviderHealthDomainSummaries({
|
|
584
|
-
configManager: this.deps.configManager,
|
|
585
|
-
auth: this.deps.localAuth.getSnapshot(),
|
|
586
|
-
settings: this.deps.settings.getSnapshot(),
|
|
587
|
-
remote: this.deps.remote.getSnapshot(),
|
|
588
|
-
security: this.deps.security.getSnapshot(),
|
|
589
|
-
intelligence: this.deps.intelligence.getSnapshot(),
|
|
590
|
-
continuity: this.deps.continuity.getSnapshot(),
|
|
591
|
-
worktrees: this.deps.worktrees.getSnapshot(),
|
|
592
|
-
session: this.deps.session.getSnapshot(),
|
|
593
|
-
})) {
|
|
594
|
-
domainLines.push(buildAlignedRow(
|
|
595
|
-
width,
|
|
596
|
-
[
|
|
597
|
-
{ text: domain.name, fg: C.value, bold: true },
|
|
598
|
-
{ text: domain.summary, fg: domainColor(domain.level) },
|
|
599
|
-
{ text: domain.next, fg: C.dim },
|
|
600
|
-
],
|
|
601
|
-
[
|
|
602
|
-
{ width: 14 },
|
|
603
|
-
{ width: Math.max(10, width - 38) },
|
|
604
|
-
{ width: 20 },
|
|
605
|
-
],
|
|
606
|
-
));
|
|
607
|
-
for (const detail of domain.details.slice(0, 2)) {
|
|
608
|
-
domainLines.push(...buildBodyText(width, ` ${detail}`, { ...DEFAULT_PANEL_PALETTE, header: C.title }, C.dim));
|
|
609
|
-
}
|
|
610
|
-
if (domain.nextSteps.length > 1) {
|
|
611
|
-
domainLines.push(...buildBodyText(width, ` next: ${domain.nextSteps.join(' | ')}`, { ...DEFAULT_PANEL_PALETTE, header: C.title }, C.title));
|
|
500
|
+
const footerHint = buildKeyboardHints(width, [
|
|
501
|
+
{ keys: 'j/k', label: 'select provider' },
|
|
502
|
+
{ keys: 'enter', label: 'repair auth routes' },
|
|
503
|
+
{ keys: 'r', label: 'refresh posture' },
|
|
504
|
+
{ keys: 't', label: `view: ${this._view}` },
|
|
505
|
+
{ keys: '/provider', label: 'switch model' },
|
|
506
|
+
], palette);
|
|
507
|
+
|
|
508
|
+
const collapsedDomains = this._view !== 'domains'
|
|
509
|
+
? {
|
|
510
|
+
attention: this._collectDomainSummaries().filter((summary) => summary.level === 'warn' || summary.level === 'bad').length,
|
|
511
|
+
maintenanceLevel: this._evaluateMaintenance().level,
|
|
612
512
|
}
|
|
613
|
-
|
|
513
|
+
: undefined;
|
|
514
|
+
|
|
515
|
+
const postureSection: PanelWorkspaceSection = {
|
|
516
|
+
lines: buildSummaryBlock(width, 'Provider console posture', buildPostureLines(width, C, palette, {
|
|
517
|
+
providers,
|
|
518
|
+
entriesById,
|
|
519
|
+
accounts: this._accountRecords,
|
|
520
|
+
trackerRecords: this.providerHealthTracker.getAll(),
|
|
521
|
+
compositeStatus: snapshot.compositeStatus,
|
|
522
|
+
falloverCount: snapshot.fallbackChain.falloverCount,
|
|
523
|
+
activeProvider: this._resolveActiveProviderId(),
|
|
524
|
+
...(collapsedDomains ? { collapsedDomains } : {}),
|
|
525
|
+
unattributedError: this._unattributedError,
|
|
526
|
+
}), palette),
|
|
527
|
+
};
|
|
614
528
|
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
const selectedAccount = selectedName ? this._accountRecords.get(selectedName) : undefined;
|
|
618
|
-
const selectedLines: Line[] = [];
|
|
619
|
-
const maintenanceLines: Line[] = [];
|
|
620
|
-
const session = this.deps.session.getSnapshot();
|
|
621
|
-
const maintenance = evaluateSessionMaintenance({
|
|
622
|
-
configManager: this.deps.configManager,
|
|
623
|
-
currentTokens: session.estimatedContextTokens,
|
|
624
|
-
contextWindow: session.contextWindow,
|
|
625
|
-
messageCount: session.messageCount,
|
|
626
|
-
session: session.session,
|
|
627
|
-
});
|
|
628
|
-
maintenanceLines.push(buildKeyValueLine(width, [
|
|
629
|
-
{ label: 'level', value: maintenance.level, valueColor: maintenance.level === 'needs-repair' ? C.bad : maintenance.level === 'suggest-compact' || maintenance.level === 'watch' ? C.warn : C.good },
|
|
630
|
-
{ label: 'guidance', value: maintenance.guidanceMode, valueColor: C.value },
|
|
631
|
-
{ label: 'usage', value: `${maintenance.usagePct}%`, valueColor: maintenance.usagePct >= 80 ? C.bad : maintenance.usagePct >= 70 ? C.warn : C.value },
|
|
632
|
-
{ label: 'remaining', value: maintenance.remainingTokens.toLocaleString(), valueColor: C.value },
|
|
633
|
-
], { ...DEFAULT_PANEL_PALETTE, header: C.title }));
|
|
634
|
-
for (const reason of maintenance.reasons.slice(0, 3)) {
|
|
635
|
-
maintenanceLines.push(...buildBodyText(width, reason, { ...DEFAULT_PANEL_PALETTE, header: C.title }, C.dim));
|
|
636
|
-
}
|
|
637
|
-
if (maintenance.nextSteps.length > 0) {
|
|
638
|
-
maintenanceLines.push(...buildBodyText(width, `Next: ${maintenance.nextSteps.join(' | ')}`, { ...DEFAULT_PANEL_PALETTE, header: C.title }, C.title));
|
|
529
|
+
if (this._view === 'domains') {
|
|
530
|
+
return this._renderDomainsView(width, height, postureSection, footerHint, palette);
|
|
639
531
|
}
|
|
640
|
-
if (
|
|
641
|
-
|
|
642
|
-
selectedLines.push(buildKeyValueLine(width, [
|
|
643
|
-
{ label: 'provider', value: selectedName, valueColor: C.value },
|
|
644
|
-
{ label: 'status', value: statusLabel(status), valueColor: statusDot(status).color },
|
|
645
|
-
{ label: 'last ok', value: fmtAgo(selectedHealth?.lastSuccessAt), valueColor: C.value },
|
|
646
|
-
], { ...DEFAULT_PANEL_PALETTE, header: C.title }));
|
|
647
|
-
if (selectedHealth?.rateLimitExpiresAt && selectedHealth.rateLimitExpiresAt > Date.now()) {
|
|
648
|
-
selectedLines.push(...buildBodyText(width, `Cooldown: ${fmtCooldown(selectedHealth.rateLimitExpiresAt)}`, { ...DEFAULT_PANEL_PALETTE, header: C.title }, C.warn));
|
|
649
|
-
}
|
|
650
|
-
if (selectedHealth?.lastErrorMessage) {
|
|
651
|
-
selectedLines.push(...buildBodyText(width, `Last error: ${selectedHealth.lastErrorMessage}`, { ...DEFAULT_PANEL_PALETTE, header: C.title }, C.bad));
|
|
652
|
-
}
|
|
653
|
-
if (selectedAccount) {
|
|
654
|
-
selectedLines.push(buildKeyValueLine(width, [
|
|
655
|
-
{ label: 'route', value: selectedAccount.activeRoute, valueColor: routeColor(selectedAccount.activeRoute) },
|
|
656
|
-
{ label: 'preferred', value: selectedAccount.preferredRoute, valueColor: C.dim },
|
|
657
|
-
{ label: 'freshness', value: selectedAccount.authFreshness, valueColor: freshnessColor(selectedAccount.authFreshness) },
|
|
658
|
-
], { ...DEFAULT_PANEL_PALETTE, header: C.title }));
|
|
659
|
-
selectedLines.push(buildKeyValueLine(width, [
|
|
660
|
-
{ label: 'models', value: String(selectedAccount.modelCount), valueColor: C.value },
|
|
661
|
-
{ label: 'active', value: selectedAccount.active ? 'yes' : 'no', valueColor: selectedAccount.active ? C.good : C.dim },
|
|
662
|
-
], { ...DEFAULT_PANEL_PALETTE, header: C.title }));
|
|
663
|
-
selectedLines.push(...buildBodyText(width, `Auth route: ${selectedAccount.activeRouteReason}`, { ...DEFAULT_PANEL_PALETTE, header: C.title }, C.dim));
|
|
664
|
-
if (selectedAccount.fallbackRisk) {
|
|
665
|
-
selectedLines.push(...buildBodyText(width, `Fallback: ${selectedAccount.fallbackRisk}`, { ...DEFAULT_PANEL_PALETTE, header: C.title }, C.warn));
|
|
666
|
-
}
|
|
667
|
-
if (selectedAccount.issues.length > 0) {
|
|
668
|
-
selectedLines.push(...buildBodyText(width, `Issue: ${selectedAccount.issues[0]!}`, { ...DEFAULT_PANEL_PALETTE, header: C.title }, C.bad));
|
|
669
|
-
}
|
|
670
|
-
if (selectedAccount.recommendedActions.length > 0) {
|
|
671
|
-
selectedLines.push(...buildBodyText(width, `Next: ${selectedAccount.recommendedActions[0]!}`, { ...DEFAULT_PANEL_PALETTE, header: C.title }, C.title));
|
|
672
|
-
}
|
|
673
|
-
}
|
|
532
|
+
if (this._view === 'routes') {
|
|
533
|
+
return this._renderRoutesView(width, height, postureSection, footerHint, palette, selectedName);
|
|
674
534
|
}
|
|
535
|
+
return this._renderProvidersView(width, height, postureSection, footerHint, palette, providers, entriesById, snapshot.fallbackChain, selectedName);
|
|
536
|
+
}
|
|
675
537
|
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
538
|
+
private _renderProvidersView(
|
|
539
|
+
width: number,
|
|
540
|
+
height: number,
|
|
541
|
+
postureSection: PanelWorkspaceSection,
|
|
542
|
+
footerHint: Line,
|
|
543
|
+
palette: typeof DEFAULT_PANEL_PALETTE,
|
|
544
|
+
providers: readonly string[],
|
|
545
|
+
entriesById: ReadonlyMap<string, ProviderHealthEntry>,
|
|
546
|
+
chain: FallbackChainData,
|
|
547
|
+
selectedName: string | undefined,
|
|
548
|
+
): Line[] {
|
|
549
|
+
const chainLines = buildChainLines(width, C, palette, chain);
|
|
550
|
+
const chainSections: PanelWorkspaceSection[] = chainLines.length > 0
|
|
551
|
+
? [{ title: 'Fallback Chain', lines: chainLines }]
|
|
552
|
+
: [];
|
|
553
|
+
|
|
554
|
+
const rows = providers.map((name, absolute) => buildProviderRow(width, C, {
|
|
555
|
+
name,
|
|
556
|
+
entry: entriesById.get(name),
|
|
557
|
+
health: this.providerHealthTracker.get(name),
|
|
558
|
+
account: this._accountRecords.get(name),
|
|
559
|
+
selected: absolute === this._selectedIndex,
|
|
560
|
+
}));
|
|
561
|
+
|
|
562
|
+
const selectedSections: PanelWorkspaceSection[] = selectedName
|
|
563
|
+
? [buildSelectedDetailSection(width, C, palette, {
|
|
564
|
+
selectedName,
|
|
565
|
+
entry: entriesById.get(selectedName),
|
|
566
|
+
health: this.providerHealthTracker.get(selectedName),
|
|
567
|
+
account: this._accountRecords.get(selectedName),
|
|
568
|
+
})]
|
|
569
|
+
: [];
|
|
570
|
+
|
|
571
|
+
const resolved = resolvePrimaryScrollableSection(width, height, {
|
|
572
|
+
intro: INTRO,
|
|
705
573
|
footerLines: [footerHint],
|
|
706
|
-
palette
|
|
707
|
-
beforeSections: [postureSection,
|
|
574
|
+
palette,
|
|
575
|
+
beforeSections: [postureSection, ...chainSections],
|
|
708
576
|
section: {
|
|
709
577
|
title: 'Providers',
|
|
710
|
-
fixedLines: [
|
|
711
|
-
scrollableLines:
|
|
712
|
-
const health = this.providerHealthTracker.get(name);
|
|
713
|
-
const status = health?.status ?? 'unknown';
|
|
714
|
-
const dot = statusDot(status);
|
|
715
|
-
const latency = health?.lastLatencyMs !== undefined ? fmtMs(health.lastLatencyMs) : 'n/a';
|
|
716
|
-
const latencyFg = health?.lastLatencyMs !== undefined ? latencyColor(health.lastLatencyMs) : C.dim;
|
|
717
|
-
const account = this._accountRecords.get(name);
|
|
718
|
-
const authFg = account ? freshnessColor(account.authFreshness) : C.dim;
|
|
719
|
-
return buildAlignedRow(
|
|
720
|
-
width,
|
|
721
|
-
[
|
|
722
|
-
{ text: `${dot.char} ${name}`, fg: C.value, bold: absolute === this._selectedIndex },
|
|
723
|
-
{ text: statusLabel(status), fg: dot.color },
|
|
724
|
-
{ text: latency, fg: latencyFg },
|
|
725
|
-
{ text: fmtAgo(health?.lastSuccessAt), fg: C.value },
|
|
726
|
-
{ text: account ? account.authFreshness : '-', fg: authFg },
|
|
727
|
-
],
|
|
728
|
-
[
|
|
729
|
-
{ width: 18 },
|
|
730
|
-
{ width: 13 },
|
|
731
|
-
{ width: 8, align: 'right' },
|
|
732
|
-
{ width: 10, align: 'right' },
|
|
733
|
-
{ width: 12 },
|
|
734
|
-
],
|
|
735
|
-
{ selected: absolute === this._selectedIndex, selectedBg: C.rowSelectBg, marker: '▸' },
|
|
736
|
-
);
|
|
737
|
-
}),
|
|
578
|
+
fixedLines: [buildProviderColumnHeader(width, C)],
|
|
579
|
+
scrollableLines: rows,
|
|
738
580
|
selectedIndex: this._selectedIndex,
|
|
739
581
|
scrollOffset: this._scrollOffset,
|
|
740
582
|
guardRows: 1,
|
|
@@ -743,20 +585,94 @@ export class ProviderHealthPanel extends BasePanel {
|
|
|
743
585
|
},
|
|
744
586
|
afterSections: selectedSections,
|
|
745
587
|
});
|
|
746
|
-
this._scrollOffset =
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
588
|
+
this._scrollOffset = resolved.scrollOffset;
|
|
589
|
+
|
|
590
|
+
return buildPanelWorkspace(width, height, {
|
|
591
|
+
title: 'Health',
|
|
592
|
+
intro: INTRO,
|
|
593
|
+
sections: [postureSection, ...chainSections, resolved.section, ...selectedSections],
|
|
594
|
+
footerLines: [footerHint],
|
|
595
|
+
palette,
|
|
596
|
+
});
|
|
597
|
+
}
|
|
598
|
+
|
|
599
|
+
private _renderRoutesView(
|
|
600
|
+
width: number,
|
|
601
|
+
height: number,
|
|
602
|
+
postureSection: PanelWorkspaceSection,
|
|
603
|
+
footerHint: Line,
|
|
604
|
+
palette: typeof DEFAULT_PANEL_PALETTE,
|
|
605
|
+
selectedName: string | undefined,
|
|
606
|
+
): Line[] {
|
|
607
|
+
const account = selectedName ? this._accountRecords.get(selectedName) : undefined;
|
|
608
|
+
const routeLines = buildRouteViewLines(width, C, palette, account);
|
|
609
|
+
|
|
610
|
+
const resolved = resolvePrimaryScrollableSection(width, height, {
|
|
611
|
+
intro: INTRO,
|
|
612
|
+
footerLines: [footerHint],
|
|
613
|
+
palette,
|
|
614
|
+
beforeSections: [postureSection],
|
|
615
|
+
section: {
|
|
616
|
+
title: `Auth Routes — ${selectedName ?? 'n/a'}`,
|
|
617
|
+
fixedLines: [buildRouteColumnHeader(width, C)],
|
|
618
|
+
scrollableLines: routeLines,
|
|
619
|
+
selectedIndex: 0,
|
|
620
|
+
scrollOffset: this._scrollOffset,
|
|
621
|
+
guardRows: 1,
|
|
622
|
+
minRows: 4,
|
|
623
|
+
appendWindowSummary: { dimColor: C.dim },
|
|
624
|
+
},
|
|
625
|
+
afterSections: [],
|
|
626
|
+
});
|
|
627
|
+
this._scrollOffset = resolved.scrollOffset;
|
|
628
|
+
|
|
629
|
+
return buildPanelWorkspace(width, height, {
|
|
630
|
+
title: 'Health',
|
|
631
|
+
intro: INTRO,
|
|
632
|
+
sections: [postureSection, resolved.section],
|
|
633
|
+
footerLines: [footerHint],
|
|
634
|
+
palette,
|
|
635
|
+
});
|
|
636
|
+
}
|
|
637
|
+
|
|
638
|
+
private _renderDomainsView(
|
|
639
|
+
width: number,
|
|
640
|
+
height: number,
|
|
641
|
+
postureSection: PanelWorkspaceSection,
|
|
642
|
+
footerHint: Line,
|
|
643
|
+
palette: typeof DEFAULT_PANEL_PALETTE,
|
|
644
|
+
): Line[] {
|
|
645
|
+
const domainLines = buildDomainLines(width, C, palette, this._collectDomainSummaries());
|
|
646
|
+
const maintenanceSection: PanelWorkspaceSection = {
|
|
647
|
+
title: 'Session Maintenance',
|
|
648
|
+
lines: buildMaintenanceLines(width, C, palette, this._evaluateMaintenance()),
|
|
649
|
+
};
|
|
650
|
+
|
|
651
|
+
const resolved = resolvePrimaryScrollableSection(width, height, {
|
|
652
|
+
intro: INTRO,
|
|
653
|
+
footerLines: [footerHint],
|
|
654
|
+
palette,
|
|
655
|
+
beforeSections: [postureSection],
|
|
656
|
+
section: {
|
|
657
|
+
title: 'Repair Domains',
|
|
658
|
+
fixedLines: [],
|
|
659
|
+
scrollableLines: domainLines,
|
|
660
|
+
selectedIndex: 0,
|
|
661
|
+
scrollOffset: this._scrollOffset,
|
|
662
|
+
guardRows: 1,
|
|
663
|
+
minRows: 4,
|
|
664
|
+
appendWindowSummary: { dimColor: C.dim },
|
|
665
|
+
},
|
|
666
|
+
afterSections: [maintenanceSection],
|
|
667
|
+
});
|
|
668
|
+
this._scrollOffset = resolved.scrollOffset;
|
|
669
|
+
|
|
754
670
|
return buildPanelWorkspace(width, height, {
|
|
755
671
|
title: 'Health',
|
|
756
|
-
intro,
|
|
757
|
-
sections,
|
|
672
|
+
intro: INTRO,
|
|
673
|
+
sections: [postureSection, resolved.section, maintenanceSection],
|
|
758
674
|
footerLines: [footerHint],
|
|
759
|
-
palette
|
|
675
|
+
palette,
|
|
760
676
|
});
|
|
761
677
|
}
|
|
762
678
|
}
|