@pellux/goodvibes-tui 0.27.0 → 0.29.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +124 -0
- package/README.md +1 -1
- package/package.json +1 -1
- package/src/core/context-usage.ts +53 -0
- package/src/core/conversation.ts +4 -6
- package/src/core/session-recovery.ts +2 -0
- package/src/core/turn-event-wiring.ts +1 -0
- package/src/input/commands/cost-runtime.ts +49 -0
- package/src/input/commands/eval.ts +4 -3
- package/src/input/commands/operator-runtime.ts +5 -1
- package/src/input/commands.ts +2 -0
- package/src/input/handler-feed-routes.ts +60 -26
- package/src/input/handler-feed.ts +17 -8
- package/src/input/handler-picker-routes.ts +18 -13
- package/src/input/handler-shortcuts.ts +51 -0
- package/src/input/handler-ui-state.ts +4 -0
- package/src/input/handler.ts +43 -6
- package/src/input/keybindings.ts +53 -8
- package/src/input/model-picker.ts +5 -0
- package/src/input/panel-integration-actions.ts +117 -1
- package/src/main.ts +5 -2
- package/src/panels/agent-inspector-panel.ts +139 -45
- package/src/panels/agent-inspector-shared.ts +147 -3
- package/src/panels/approval-panel.ts +204 -102
- package/src/panels/automation-control-panel.ts +370 -103
- package/src/panels/base-panel.ts +26 -2
- 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 +253 -75
- package/src/panels/cockpit-read-model.ts +2 -1
- package/src/panels/communication-panel.ts +159 -56
- package/src/panels/confirm-state.ts +8 -1
- package/src/panels/control-plane-panel.ts +354 -95
- package/src/panels/cost-tracker-panel.ts +243 -57
- package/src/panels/debug-panel.ts +347 -159
- package/src/panels/diff-panel.ts +257 -82
- package/src/panels/docs-panel.ts +166 -64
- package/src/panels/eval-panel.ts +336 -109
- package/src/panels/expandable-list-panel.ts +189 -0
- package/src/panels/file-explorer-panel.ts +238 -159
- package/src/panels/file-preview-panel.ts +141 -59
- package/src/panels/git-panel.ts +360 -191
- package/src/panels/hooks-panel.ts +181 -19
- package/src/panels/incident-review-panel.ts +252 -43
- package/src/panels/index.ts +0 -4
- package/src/panels/intelligence-panel.ts +310 -103
- package/src/panels/knowledge-graph-panel.ts +480 -58
- package/src/panels/local-auth-panel.ts +251 -33
- package/src/panels/marketplace-panel.ts +219 -33
- package/src/panels/memory-panel.ts +83 -81
- package/src/panels/ops-control-panel.ts +211 -32
- package/src/panels/ops-strategy-panel.ts +131 -45
- package/src/panels/orchestration-panel.ts +259 -67
- package/src/panels/panel-list-panel.ts +184 -136
- package/src/panels/panel-manager.ts +120 -13
- package/src/panels/plan-dashboard-panel.ts +507 -74
- package/src/panels/plugins-panel.ts +227 -34
- package/src/panels/policy-panel.ts +264 -55
- package/src/panels/polish-core.ts +162 -0
- package/src/panels/polish-tables.ts +258 -0
- package/src/panels/polish.ts +67 -149
- package/src/panels/project-planning-answer-actions.ts +134 -0
- package/src/panels/project-planning-panel.ts +84 -113
- package/src/panels/provider-health-panel.ts +438 -480
- 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 +136 -38
- package/src/panels/remote-panel.ts +123 -38
- package/src/panels/routes-panel.ts +87 -24
- package/src/panels/sandbox-panel.ts +232 -65
- package/src/panels/scrollable-list-panel.ts +143 -145
- package/src/panels/security-panel.ts +204 -52
- package/src/panels/services-panel.ts +174 -76
- package/src/panels/session-browser-panel.ts +101 -6
- package/src/panels/session-maintenance.ts +4 -122
- package/src/panels/settings-sync-panel.ts +346 -68
- package/src/panels/skills-panel.ts +157 -89
- package/src/panels/subscription-panel.ts +105 -34
- package/src/panels/symbol-outline-panel.ts +287 -154
- package/src/panels/system-messages-panel.ts +172 -38
- package/src/panels/tasks-panel.ts +348 -134
- package/src/panels/thinking-panel.ts +66 -32
- package/src/panels/token-budget-panel.ts +233 -46
- package/src/panels/tool-inspector-panel.ts +170 -54
- package/src/panels/types.ts +65 -4
- package/src/panels/work-plan-panel.ts +393 -39
- package/src/panels/worktree-panel.ts +239 -61
- package/src/panels/wrfc-panel-format.ts +133 -0
- package/src/panels/wrfc-panel.ts +146 -150
- package/src/renderer/compaction-preview.ts +2 -1
- package/src/renderer/compositor.ts +46 -43
- package/src/renderer/conversation-overlays.ts +25 -11
- package/src/renderer/footer-tips.ts +41 -0
- package/src/renderer/fullscreen-primitives.ts +22 -16
- package/src/renderer/help-overlay.ts +91 -14
- package/src/renderer/hint-grammar.ts +52 -0
- package/src/renderer/layout.ts +7 -7
- package/src/renderer/modal-factory.ts +23 -15
- package/src/renderer/model-picker-overlay.ts +30 -11
- package/src/renderer/model-workspace.ts +2 -3
- package/src/renderer/overlay-box.ts +16 -10
- package/src/renderer/panel-composite.ts +7 -20
- package/src/renderer/panel-workspace-bar.ts +14 -8
- package/src/renderer/process-indicator.ts +3 -1
- package/src/renderer/progress.ts +8 -6
- package/src/renderer/search-overlay.ts +27 -6
- package/src/renderer/session-picker-modal.ts +6 -4
- package/src/renderer/settings-modal.ts +70 -10
- package/src/renderer/shell-surface.ts +41 -15
- package/src/renderer/status-glyphs.ts +11 -9
- package/src/renderer/tab-strip.ts +148 -34
- package/src/renderer/theme.ts +60 -3
- package/src/renderer/ui-factory.ts +45 -36
- package/src/renderer/ui-primitives.ts +23 -2
- package/src/runtime/bootstrap-shell.ts +35 -18
- package/src/runtime/diagnostics/panels/index.ts +0 -3
- package/src/runtime/ui/model-picker/health-enrichment.ts +3 -0
- package/src/runtime/ui/model-picker/types.ts +4 -0
- package/src/shell/ui-openers.ts +14 -22
- package/src/utils/format-duration.ts +55 -0
- package/src/utils/format-number.ts +71 -0
- package/src/utils/splash-lines.ts +44 -3
- package/src/version.ts +1 -1
- package/src/work-plans/work-plan-store.ts +13 -0
- package/src/panels/agent-logs-panel.ts +0 -666
- package/src/panels/agent-logs-shared.ts +0 -129
- package/src/panels/context-visualizer-panel.ts +0 -214
- package/src/panels/forensics-panel.ts +0 -364
- package/src/panels/panel-picker.ts +0 -106
- package/src/panels/provider-account-snapshot.ts +0 -259
- package/src/panels/provider-accounts-panel.ts +0 -218
- package/src/panels/provider-stats-panel.ts +0 -366
- package/src/panels/schedule-panel.ts +0 -342
- package/src/panels/watchers-panel.ts +0 -193
- package/src/renderer/panel-picker-overlay.ts +0 -202
- package/src/renderer/panel-tab-bar.ts +0 -69
- package/src/runtime/diagnostics/panels/panel-resources.ts +0 -118
- package/src/runtime/diagnostics/panels/policy.ts +0 -177
|
@@ -1,17 +1,30 @@
|
|
|
1
1
|
import type { ConfigManager } from '@pellux/goodvibes-sdk/platform/config';
|
|
2
2
|
import { BasePanel } from './base-panel.ts';
|
|
3
|
-
import {
|
|
4
|
-
import type {
|
|
5
|
-
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';
|
|
6
6
|
import type { UiEventFeed } from '../runtime/ui-events.ts';
|
|
7
7
|
import {
|
|
8
8
|
type ProviderRuntimeInspectionQuery,
|
|
9
9
|
} from '../runtime/ui-service-queries.ts';
|
|
10
|
-
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';
|
|
11
18
|
import {
|
|
12
19
|
buildProviderHealthDomainSummaries,
|
|
13
20
|
type HealthDomainSummary,
|
|
14
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';
|
|
15
28
|
import type {
|
|
16
29
|
UiContinuitySnapshot,
|
|
17
30
|
UiIntelligenceSnapshot,
|
|
@@ -24,15 +37,9 @@ import type {
|
|
|
24
37
|
UiSettingsSnapshot,
|
|
25
38
|
UiWorktreeSnapshot,
|
|
26
39
|
} from '../runtime/ui-read-models.ts';
|
|
27
|
-
import { evaluateSessionMaintenance } from '@/runtime/index.ts';
|
|
28
40
|
import {
|
|
29
|
-
buildBodyText,
|
|
30
|
-
buildDetailBlock,
|
|
31
41
|
buildEmptyState,
|
|
32
|
-
|
|
33
|
-
buildKeyValueLine,
|
|
34
|
-
buildPanelListRow,
|
|
35
|
-
buildPanelLine,
|
|
42
|
+
buildKeyboardHints,
|
|
36
43
|
buildPanelWorkspace,
|
|
37
44
|
buildSummaryBlock,
|
|
38
45
|
DEFAULT_PANEL_PALETTE,
|
|
@@ -40,6 +47,17 @@ import {
|
|
|
40
47
|
resolvePrimaryScrollableSection,
|
|
41
48
|
type PanelWorkspaceSection,
|
|
42
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';
|
|
43
61
|
|
|
44
62
|
// ---------------------------------------------------------------------------
|
|
45
63
|
// Types
|
|
@@ -58,309 +76,60 @@ export interface ProviderHealthPanelDeps {
|
|
|
58
76
|
readonly intelligence: UiReadModel<UiIntelligenceSnapshot>;
|
|
59
77
|
readonly continuity: UiReadModel<UiContinuitySnapshot>;
|
|
60
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
|
+
};
|
|
61
88
|
}
|
|
62
89
|
|
|
63
|
-
|
|
64
|
-
|
|
90
|
+
type ConsoleView = 'providers' | 'routes' | 'domains';
|
|
91
|
+
|
|
92
|
+
const CONSOLE_VIEWS: readonly ConsoleView[] = ['providers', 'routes', 'domains'] as const;
|
|
65
93
|
|
|
94
|
+
// Base chrome plus console accents (domain accents only — hex ratchet).
|
|
66
95
|
const C = extendPalette(DEFAULT_PANEL_PALETTE, {
|
|
67
96
|
title: '#00ffff',
|
|
68
97
|
unknown: '244',
|
|
98
|
+
rowSelectBg: '#111827',
|
|
69
99
|
});
|
|
70
100
|
|
|
71
|
-
const
|
|
72
|
-
const LATENCY_BAD_MS = 5_000;
|
|
101
|
+
const INTRO = 'Provider console: status, latency timelines, error attribution, auth routes, and fallback posture.';
|
|
73
102
|
|
|
74
103
|
// ---------------------------------------------------------------------------
|
|
75
|
-
//
|
|
76
|
-
// ---------------------------------------------------------------------------
|
|
77
|
-
|
|
78
|
-
function statusDot(status: ProviderStatus): { char: string; color: string } {
|
|
79
|
-
switch (status) {
|
|
80
|
-
case 'healthy': return { char: '●', color: C.good };
|
|
81
|
-
case 'degraded': return { char: '◑', color: C.warn };
|
|
82
|
-
case 'rate_limited': return { char: '◐', color: C.warn };
|
|
83
|
-
case 'auth_error': return { char: '✕', color: C.bad };
|
|
84
|
-
case 'unavailable': return { char: '✕', color: C.bad };
|
|
85
|
-
default: return { char: '○', color: C.unknown };
|
|
86
|
-
}
|
|
87
|
-
}
|
|
88
|
-
|
|
89
|
-
function statusLabel(status: ProviderStatus): string {
|
|
90
|
-
switch (status) {
|
|
91
|
-
case 'healthy': return 'online';
|
|
92
|
-
case 'degraded': return 'degraded';
|
|
93
|
-
case 'rate_limited': return 'rate-limited';
|
|
94
|
-
case 'auth_error': return 'auth error';
|
|
95
|
-
case 'unavailable': return 'unavailable';
|
|
96
|
-
default: return 'unknown';
|
|
97
|
-
}
|
|
98
|
-
}
|
|
99
|
-
|
|
100
|
-
function latencyColor(ms: number): string {
|
|
101
|
-
if (ms >= LATENCY_BAD_MS) return C.bad;
|
|
102
|
-
if (ms >= LATENCY_WARN_MS) return C.warn;
|
|
103
|
-
return C.good;
|
|
104
|
-
}
|
|
105
|
-
|
|
106
|
-
function fmtMs(ms: number): string {
|
|
107
|
-
if (ms <= 0) return 'n/a';
|
|
108
|
-
if (ms >= 10_000) return `${(ms / 1000).toFixed(1)}s`;
|
|
109
|
-
if (ms >= 1_000) return `${(ms / 1000).toFixed(2)}s`;
|
|
110
|
-
return `${Math.round(ms)}ms`;
|
|
111
|
-
}
|
|
112
|
-
|
|
113
|
-
function fmtAgo(ts: number | undefined): string {
|
|
114
|
-
if (!ts) return 'n/a';
|
|
115
|
-
const sec = Math.floor((Date.now() - ts) / 1000);
|
|
116
|
-
if (sec < 60) return `${sec}s ago`;
|
|
117
|
-
if (sec < 3600) return `${Math.floor(sec / 60)}m ago`;
|
|
118
|
-
return `${Math.floor(sec / 3600)}h ago`;
|
|
119
|
-
}
|
|
120
|
-
|
|
121
|
-
function fmtCooldown(expiresAt: number): string {
|
|
122
|
-
const remaining = Math.ceil((expiresAt - Date.now()) / 1000);
|
|
123
|
-
if (remaining <= 0) return 'expiring';
|
|
124
|
-
return `${remaining}s cooldown`;
|
|
125
|
-
}
|
|
126
|
-
|
|
127
|
-
type ProviderPanelAuthRoute = ProviderAuthRouteDescriptor['route'] | 'unconfigured';
|
|
128
|
-
type ProviderPanelAuthFreshness = NonNullable<ProviderAuthRouteDescriptor['freshness']> | 'unconfigured';
|
|
129
|
-
|
|
130
|
-
interface ProviderRuntimeRecord {
|
|
131
|
-
readonly providerId: string;
|
|
132
|
-
readonly active: boolean;
|
|
133
|
-
readonly modelCount: number;
|
|
134
|
-
readonly activeRoute: ProviderPanelAuthRoute;
|
|
135
|
-
readonly preferredRoute: ProviderPanelAuthRoute;
|
|
136
|
-
readonly activeRouteReason: string;
|
|
137
|
-
readonly authFreshness: ProviderPanelAuthFreshness;
|
|
138
|
-
readonly fallbackRisk?: string;
|
|
139
|
-
readonly issues: readonly string[];
|
|
140
|
-
readonly recommendedActions: readonly string[];
|
|
141
|
-
}
|
|
142
|
-
|
|
143
|
-
function domainColor(level: HealthDomainSummary['level']): string {
|
|
144
|
-
switch (level) {
|
|
145
|
-
case 'good':
|
|
146
|
-
return C.good;
|
|
147
|
-
case 'warn':
|
|
148
|
-
return C.warn;
|
|
149
|
-
case 'bad':
|
|
150
|
-
return C.bad;
|
|
151
|
-
default:
|
|
152
|
-
return C.value;
|
|
153
|
-
}
|
|
154
|
-
}
|
|
155
|
-
|
|
156
|
-
const AUTH_ROUTE_PRIORITY: readonly ProviderPanelAuthRoute[] = [
|
|
157
|
-
'subscription-oauth',
|
|
158
|
-
'service-oauth',
|
|
159
|
-
'secret-ref',
|
|
160
|
-
'api-key',
|
|
161
|
-
'anonymous',
|
|
162
|
-
'none',
|
|
163
|
-
'unconfigured',
|
|
164
|
-
] as const;
|
|
165
|
-
|
|
166
|
-
function routePriority(route: ProviderPanelAuthRoute): number {
|
|
167
|
-
const priority = AUTH_ROUTE_PRIORITY.indexOf(route);
|
|
168
|
-
return priority >= 0 ? priority : AUTH_ROUTE_PRIORITY.length;
|
|
169
|
-
}
|
|
170
|
-
|
|
171
|
-
function routeColor(route: ProviderPanelAuthRoute): string {
|
|
172
|
-
switch (route) {
|
|
173
|
-
case 'subscription-oauth':
|
|
174
|
-
return C.title;
|
|
175
|
-
case 'service-oauth':
|
|
176
|
-
return C.good;
|
|
177
|
-
case 'api-key':
|
|
178
|
-
return C.warn;
|
|
179
|
-
case 'secret-ref':
|
|
180
|
-
return C.value;
|
|
181
|
-
case 'anonymous':
|
|
182
|
-
case 'none':
|
|
183
|
-
return C.dim;
|
|
184
|
-
default:
|
|
185
|
-
return C.value;
|
|
186
|
-
}
|
|
187
|
-
}
|
|
188
|
-
|
|
189
|
-
function freshnessColor(freshness: ProviderPanelAuthFreshness): string {
|
|
190
|
-
switch (freshness) {
|
|
191
|
-
case 'expired':
|
|
192
|
-
return C.bad;
|
|
193
|
-
case 'expiring':
|
|
194
|
-
case 'pending':
|
|
195
|
-
return C.warn;
|
|
196
|
-
case 'healthy':
|
|
197
|
-
return C.good;
|
|
198
|
-
default:
|
|
199
|
-
return C.dim;
|
|
200
|
-
}
|
|
201
|
-
}
|
|
202
|
-
|
|
203
|
-
function buildSyntheticAuthRoutes(
|
|
204
|
-
auth: {
|
|
205
|
-
readonly mode: 'api-key' | 'oauth' | 'anonymous' | 'none';
|
|
206
|
-
readonly configured: boolean;
|
|
207
|
-
readonly detail?: string;
|
|
208
|
-
readonly envVars?: readonly string[];
|
|
209
|
-
} | undefined,
|
|
210
|
-
): readonly ProviderAuthRouteDescriptor[] {
|
|
211
|
-
if (!auth) return [];
|
|
212
|
-
switch (auth.mode) {
|
|
213
|
-
case 'none':
|
|
214
|
-
return [{
|
|
215
|
-
route: 'none',
|
|
216
|
-
label: 'No auth required',
|
|
217
|
-
configured: true,
|
|
218
|
-
usable: true,
|
|
219
|
-
freshness: 'healthy',
|
|
220
|
-
detail: auth.detail ?? 'Provider does not require interactive credentials.',
|
|
221
|
-
}];
|
|
222
|
-
case 'anonymous':
|
|
223
|
-
return [{
|
|
224
|
-
route: 'anonymous',
|
|
225
|
-
label: 'Anonymous / local access',
|
|
226
|
-
configured: auth.configured,
|
|
227
|
-
usable: auth.configured,
|
|
228
|
-
freshness: auth.configured ? 'healthy' : 'unconfigured',
|
|
229
|
-
detail: auth.detail ?? 'Provider can be used without stored credentials.',
|
|
230
|
-
}];
|
|
231
|
-
case 'api-key':
|
|
232
|
-
return [{
|
|
233
|
-
route: 'api-key',
|
|
234
|
-
label: 'Ambient API key',
|
|
235
|
-
configured: auth.configured,
|
|
236
|
-
usable: auth.configured,
|
|
237
|
-
freshness: auth.configured ? 'healthy' : 'unconfigured',
|
|
238
|
-
detail: auth.detail ?? 'Provider expects a configured API key.',
|
|
239
|
-
...(auth.envVars?.length ? { envVars: auth.envVars } : {}),
|
|
240
|
-
...(auth.envVars?.length
|
|
241
|
-
? { repairHints: [`Set ${auth.envVars.join(' or ')} in the environment or secrets store.`] }
|
|
242
|
-
: {}),
|
|
243
|
-
}];
|
|
244
|
-
case 'oauth':
|
|
245
|
-
return [{
|
|
246
|
-
route: 'service-oauth',
|
|
247
|
-
label: 'OAuth session',
|
|
248
|
-
configured: auth.configured,
|
|
249
|
-
usable: auth.configured,
|
|
250
|
-
freshness: auth.configured ? 'healthy' : 'unconfigured',
|
|
251
|
-
detail: auth.detail ?? 'Provider expects an OAuth-backed credential.',
|
|
252
|
-
repairHints: ['Refresh or repair the provider OAuth session before relying on it.'],
|
|
253
|
-
}];
|
|
254
|
-
default:
|
|
255
|
-
return [];
|
|
256
|
-
}
|
|
257
|
-
}
|
|
258
|
-
|
|
259
|
-
function getUsableRoute(route: ProviderAuthRouteDescriptor): boolean {
|
|
260
|
-
return route.usable ?? route.configured;
|
|
261
|
-
}
|
|
262
|
-
|
|
263
|
-
function pickRoute(
|
|
264
|
-
routes: readonly ProviderAuthRouteDescriptor[],
|
|
265
|
-
): ProviderAuthRouteDescriptor | null {
|
|
266
|
-
if (routes.length === 0) return null;
|
|
267
|
-
return [...routes].sort((left, right) => routePriority(left.route) - routePriority(right.route))[0] ?? null;
|
|
268
|
-
}
|
|
269
|
-
|
|
270
|
-
function buildProviderRuntimeRecord(
|
|
271
|
-
snapshot: {
|
|
272
|
-
readonly providerId: string;
|
|
273
|
-
readonly active: boolean;
|
|
274
|
-
readonly modelCount: number;
|
|
275
|
-
readonly runtime: {
|
|
276
|
-
readonly auth?: {
|
|
277
|
-
readonly mode: 'api-key' | 'oauth' | 'anonymous' | 'none';
|
|
278
|
-
readonly configured: boolean;
|
|
279
|
-
readonly detail?: string;
|
|
280
|
-
readonly envVars?: readonly string[];
|
|
281
|
-
readonly routes?: readonly ProviderAuthRouteDescriptor[];
|
|
282
|
-
};
|
|
283
|
-
};
|
|
284
|
-
},
|
|
285
|
-
): ProviderRuntimeRecord {
|
|
286
|
-
const auth = snapshot.runtime.auth;
|
|
287
|
-
const routes = auth?.routes?.length ? auth.routes : buildSyntheticAuthRoutes(auth);
|
|
288
|
-
const configuredRoutes = routes.filter((route) => route.configured);
|
|
289
|
-
const usableRoutes = routes.filter(getUsableRoute);
|
|
290
|
-
const preferredRoute = pickRoute(configuredRoutes.length > 0 ? configuredRoutes : routes);
|
|
291
|
-
const activeRoute = pickRoute(usableRoutes.length > 0 ? usableRoutes : (preferredRoute ? [preferredRoute] : []));
|
|
292
|
-
const activeRouteId = activeRoute?.route ?? 'unconfigured';
|
|
293
|
-
const preferredRouteId = preferredRoute?.route ?? activeRouteId;
|
|
294
|
-
const authFreshness = activeRoute?.freshness ?? (activeRouteId === 'none' ? 'healthy' : 'unconfigured');
|
|
295
|
-
const fallbackRisk = usableRoutes.length > 1
|
|
296
|
-
? 'Multiple auth routes are simultaneously usable; verify route priority before switching providers.'
|
|
297
|
-
: undefined;
|
|
298
|
-
|
|
299
|
-
const issueSet = new Set<string>();
|
|
300
|
-
const actionSet = new Set<string>();
|
|
301
|
-
|
|
302
|
-
if (activeRouteId === 'unconfigured' && auth?.mode !== 'none') {
|
|
303
|
-
issueSet.add('Provider has no usable auth route configured.');
|
|
304
|
-
}
|
|
305
|
-
|
|
306
|
-
for (const route of routes) {
|
|
307
|
-
if (route.freshness === 'expired') {
|
|
308
|
-
issueSet.add(route.detail ?? `${route.label} is expired.`);
|
|
309
|
-
} else if (route.freshness === 'pending') {
|
|
310
|
-
issueSet.add(route.detail ?? `${route.label} is pending completion.`);
|
|
311
|
-
} else if (route.configured && !getUsableRoute(route)) {
|
|
312
|
-
issueSet.add(route.detail ?? `${route.label} is configured but not currently usable.`);
|
|
313
|
-
}
|
|
314
|
-
for (const hint of route.repairHints ?? []) {
|
|
315
|
-
if (hint.trim().length > 0) actionSet.add(hint);
|
|
316
|
-
}
|
|
317
|
-
}
|
|
318
|
-
|
|
319
|
-
if (fallbackRisk) issueSet.add(fallbackRisk);
|
|
320
|
-
if (issueSet.size > 0 && actionSet.size === 0 && activeRouteId !== 'none') {
|
|
321
|
-
actionSet.add(`Review ${snapshot.providerId} provider credentials and routing metadata.`);
|
|
322
|
-
}
|
|
323
|
-
|
|
324
|
-
return {
|
|
325
|
-
providerId: snapshot.providerId,
|
|
326
|
-
active: snapshot.active,
|
|
327
|
-
modelCount: snapshot.modelCount,
|
|
328
|
-
activeRoute: activeRouteId,
|
|
329
|
-
preferredRoute: preferredRouteId,
|
|
330
|
-
activeRouteReason: activeRoute?.detail
|
|
331
|
-
?? auth?.detail
|
|
332
|
-
?? (activeRouteId === 'none'
|
|
333
|
-
? 'Provider does not require interactive credentials.'
|
|
334
|
-
: 'No usable auth route is configured for this provider.'),
|
|
335
|
-
authFreshness,
|
|
336
|
-
...(fallbackRisk ? { fallbackRisk } : {}),
|
|
337
|
-
issues: [...issueSet],
|
|
338
|
-
recommendedActions: [...actionSet],
|
|
339
|
-
};
|
|
340
|
-
}
|
|
341
|
-
|
|
342
|
-
// ---------------------------------------------------------------------------
|
|
343
|
-
// ProviderHealthPanel
|
|
104
|
+
// ProviderHealthPanel — the merged provider console (WO-112)
|
|
344
105
|
// ---------------------------------------------------------------------------
|
|
345
106
|
|
|
346
107
|
/**
|
|
347
|
-
*
|
|
348
|
-
*
|
|
349
|
-
*
|
|
350
|
-
*
|
|
351
|
-
* -
|
|
352
|
-
* -
|
|
353
|
-
*
|
|
354
|
-
* -
|
|
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.
|
|
355
118
|
*/
|
|
356
119
|
export class ProviderHealthPanel extends BasePanel {
|
|
357
120
|
private _unsubs: Array<() => void> = [];
|
|
358
|
-
private
|
|
121
|
+
private _tickTimerId: ReturnType<typeof setInterval> | null = null;
|
|
359
122
|
private _selectedIndex = 0;
|
|
360
123
|
private _scrollOffset = 0;
|
|
361
|
-
private
|
|
124
|
+
private _view: ConsoleView = 'providers';
|
|
125
|
+
private _accountRecords = new Map<string, ProviderAccountPosture>();
|
|
362
126
|
private _accountRefreshAt = 0;
|
|
363
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;
|
|
364
133
|
private readonly providerHealthTracker = new ProviderHealthTracker();
|
|
365
134
|
|
|
366
135
|
constructor(
|
|
@@ -368,10 +137,13 @@ export class ProviderHealthPanel extends BasePanel {
|
|
|
368
137
|
private readonly deps: ProviderHealthPanelDeps,
|
|
369
138
|
private readonly requestRender: () => void = () => {},
|
|
370
139
|
) {
|
|
371
|
-
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);
|
|
372
143
|
this._subscribe();
|
|
373
144
|
void this._refreshAccountPosture(true);
|
|
374
|
-
|
|
145
|
+
// Background 30s posture refresh (auth routes go stale otherwise); cleared on destroy.
|
|
146
|
+
this.registerTimer(setInterval(() => { void this._refreshAccountPosture(); }, 30_000));
|
|
375
147
|
}
|
|
376
148
|
|
|
377
149
|
// -------------------------------------------------------------------------
|
|
@@ -391,33 +163,79 @@ export class ProviderHealthPanel extends BasePanel {
|
|
|
391
163
|
}),
|
|
392
164
|
);
|
|
393
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
|
+
|
|
394
174
|
this._unsubs.push(
|
|
395
175
|
this.deps.turnEvents.on('LLM_RESPONSE_RECEIVED', (payload) => {
|
|
396
|
-
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();
|
|
397
185
|
this._markDirtyAndRender();
|
|
398
186
|
}),
|
|
399
187
|
);
|
|
400
188
|
|
|
401
189
|
this._unsubs.push(
|
|
402
190
|
this.deps.turnEvents.on('TURN_ERROR', (payload) => {
|
|
403
|
-
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();
|
|
404
202
|
this._markDirtyAndRender();
|
|
405
203
|
}),
|
|
406
204
|
);
|
|
407
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
|
+
|
|
408
218
|
this._unsubs.push(
|
|
409
219
|
this.deps.providerEvents.on('PROVIDERS_CHANGED', () => {
|
|
410
|
-
this.providerHealthTracker.onProvidersChanged(
|
|
411
|
-
...new Set([
|
|
412
|
-
...this.deps.providers.getSnapshot().providerIds,
|
|
413
|
-
...this.providerRuntime.listProviderIds(),
|
|
414
|
-
]),
|
|
415
|
-
]);
|
|
220
|
+
this.providerHealthTracker.onProvidersChanged(this._knownProviders());
|
|
416
221
|
void this._refreshAccountPosture(true);
|
|
222
|
+
this._pushHealthState();
|
|
417
223
|
this._markDirtyAndRender();
|
|
418
224
|
}),
|
|
419
225
|
);
|
|
420
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
|
+
|
|
421
239
|
for (const readModel of [
|
|
422
240
|
this.deps.providers,
|
|
423
241
|
this.deps.session,
|
|
@@ -446,39 +264,57 @@ export class ProviderHealthPanel extends BasePanel {
|
|
|
446
264
|
super.onActivate();
|
|
447
265
|
this.markDirty();
|
|
448
266
|
void this._refreshAccountPosture(true);
|
|
449
|
-
this.
|
|
267
|
+
this._startTickTimer();
|
|
450
268
|
}
|
|
451
269
|
|
|
452
270
|
override onDeactivate(): void {
|
|
453
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();
|
|
454
275
|
}
|
|
455
276
|
|
|
456
277
|
override onDestroy(): void {
|
|
457
278
|
super.onDestroy();
|
|
458
|
-
this.
|
|
279
|
+
this._tickTimerId = null;
|
|
280
|
+
this._dataProvider.dispose();
|
|
459
281
|
for (const unsub of this._unsubs) unsub();
|
|
460
282
|
this._unsubs = [];
|
|
461
283
|
}
|
|
462
284
|
|
|
463
|
-
private
|
|
464
|
-
if (this.
|
|
465
|
-
this.
|
|
466
|
-
if (Date.now() - this._accountRefreshAt > 30_000) {
|
|
467
|
-
void this._refreshAccountPosture();
|
|
468
|
-
}
|
|
285
|
+
private _startTickTimer(): void {
|
|
286
|
+
if (this._tickTimerId !== null) return;
|
|
287
|
+
this._tickTimerId = this.registerTimer(setInterval(() => {
|
|
469
288
|
this.markDirty();
|
|
470
289
|
this.requestRender();
|
|
471
290
|
}, 1_000));
|
|
472
291
|
}
|
|
473
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
|
+
|
|
474
304
|
handleInput(key: string): boolean {
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
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();
|
|
482
318
|
if (providers.length === 0) return false;
|
|
483
319
|
if (key === 'j' || key === 'down' || key === '\x1b[B') {
|
|
484
320
|
this._selectedIndex = Math.min(providers.length - 1, this._selectedIndex + 1);
|
|
@@ -493,6 +329,95 @@ export class ProviderHealthPanel extends BasePanel {
|
|
|
493
329
|
return false;
|
|
494
330
|
}
|
|
495
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
|
+
|
|
496
421
|
private async _refreshAccountPosture(force = false): Promise<void> {
|
|
497
422
|
if (this._accountLoading) return;
|
|
498
423
|
if (!force && Date.now() - this._accountRefreshAt < 15_000) return;
|
|
@@ -501,10 +426,11 @@ export class ProviderHealthPanel extends BasePanel {
|
|
|
501
426
|
const snapshots = await this.providerRuntime.inspectAll();
|
|
502
427
|
this._accountRecords = new Map(
|
|
503
428
|
snapshots
|
|
504
|
-
.map((snapshot) =>
|
|
429
|
+
.map((snapshot) => buildAccountPosture(snapshot))
|
|
505
430
|
.map((record) => [record.providerId, record] as const),
|
|
506
431
|
);
|
|
507
432
|
this._accountRefreshAt = Date.now();
|
|
433
|
+
this._pushHealthState();
|
|
508
434
|
this.markDirty();
|
|
509
435
|
this.requestRender();
|
|
510
436
|
} finally {
|
|
@@ -512,26 +438,45 @@ export class ProviderHealthPanel extends BasePanel {
|
|
|
512
438
|
}
|
|
513
439
|
}
|
|
514
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
|
+
|
|
515
466
|
// -------------------------------------------------------------------------
|
|
516
467
|
// Rendering
|
|
517
468
|
// -------------------------------------------------------------------------
|
|
518
469
|
|
|
519
470
|
override render(width: number, height: number): Line[] {
|
|
520
|
-
const
|
|
471
|
+
const palette = { ...DEFAULT_PANEL_PALETTE, header: C.title };
|
|
521
472
|
|
|
522
|
-
const
|
|
523
|
-
...this.deps.providers.getSnapshot().providerIds,
|
|
524
|
-
...this.providerRuntime.listProviderIds(),
|
|
525
|
-
...this._accountRecords.keys(),
|
|
526
|
-
]);
|
|
527
|
-
for (const h of this.providerHealthTracker.getAll()) knownSet.add(h.name);
|
|
528
|
-
const providers = [...knownSet].sort();
|
|
473
|
+
const providers = this._knownProviders();
|
|
529
474
|
this._selectedIndex = Math.min(this._selectedIndex, Math.max(0, providers.length - 1));
|
|
530
475
|
|
|
531
476
|
if (providers.length === 0) {
|
|
532
477
|
return buildPanelWorkspace(width, height, {
|
|
533
478
|
title: 'Health',
|
|
534
|
-
intro,
|
|
479
|
+
intro: INTRO,
|
|
535
480
|
sections: [{
|
|
536
481
|
lines: buildEmptyState(
|
|
537
482
|
width,
|
|
@@ -541,158 +486,97 @@ export class ProviderHealthPanel extends BasePanel {
|
|
|
541
486
|
{ command: '/provider', summary: 'review current provider and model selection' },
|
|
542
487
|
{ command: '/subscription', summary: 'review provider login and subscription state' },
|
|
543
488
|
],
|
|
544
|
-
|
|
489
|
+
palette,
|
|
545
490
|
),
|
|
546
491
|
}],
|
|
547
|
-
palette
|
|
492
|
+
palette,
|
|
548
493
|
});
|
|
549
494
|
}
|
|
550
495
|
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
let accountIssues = 0;
|
|
555
|
-
let expiringAuth = 0;
|
|
556
|
-
for (const name of providers) {
|
|
557
|
-
const status = this.providerHealthTracker.get(name)?.status ?? 'unknown';
|
|
558
|
-
if (status === 'healthy') online++;
|
|
559
|
-
else if (status === 'rate_limited') rateLimited++;
|
|
560
|
-
else if (status === 'degraded' || status === 'auth_error' || status === 'unavailable') errored++;
|
|
561
|
-
const account = this._accountRecords.get(name);
|
|
562
|
-
if (account) {
|
|
563
|
-
accountIssues += account.issues.length;
|
|
564
|
-
if (account.authFreshness === 'expiring' || account.authFreshness === 'expired' || account.authFreshness === 'pending') {
|
|
565
|
-
expiringAuth++;
|
|
566
|
-
}
|
|
567
|
-
}
|
|
568
|
-
}
|
|
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];
|
|
569
499
|
|
|
570
|
-
const
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
const domainLines: Line[] = [];
|
|
584
|
-
for (const domain of buildProviderHealthDomainSummaries({
|
|
585
|
-
configManager: this.deps.configManager,
|
|
586
|
-
auth: this.deps.localAuth.getSnapshot(),
|
|
587
|
-
settings: this.deps.settings.getSnapshot(),
|
|
588
|
-
remote: this.deps.remote.getSnapshot(),
|
|
589
|
-
security: this.deps.security.getSnapshot(),
|
|
590
|
-
intelligence: this.deps.intelligence.getSnapshot(),
|
|
591
|
-
continuity: this.deps.continuity.getSnapshot(),
|
|
592
|
-
worktrees: this.deps.worktrees.getSnapshot(),
|
|
593
|
-
session: this.deps.session.getSnapshot(),
|
|
594
|
-
})) {
|
|
595
|
-
domainLines.push(buildPanelLine(width, [
|
|
596
|
-
[' ', C.label],
|
|
597
|
-
[domain.name.padEnd(14), C.value],
|
|
598
|
-
[domain.summary.slice(0, Math.max(0, width - 36)).padEnd(Math.max(0, width - 36)), domainColor(domain.level)],
|
|
599
|
-
[' ', C.label],
|
|
600
|
-
[domain.next.slice(0, 20), C.dim],
|
|
601
|
-
]));
|
|
602
|
-
for (const detail of domain.details.slice(0, 2)) {
|
|
603
|
-
domainLines.push(...buildBodyText(width, ` ${detail}`, { ...DEFAULT_PANEL_PALETTE, header: C.title }, C.dim));
|
|
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,
|
|
604
512
|
}
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
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
|
+
};
|
|
609
528
|
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
const selectedAccount = selectedName ? this._accountRecords.get(selectedName) : undefined;
|
|
613
|
-
const selectedLines: Line[] = [];
|
|
614
|
-
const maintenanceLines: Line[] = [];
|
|
615
|
-
const session = this.deps.session.getSnapshot();
|
|
616
|
-
const maintenance = evaluateSessionMaintenance({
|
|
617
|
-
configManager: this.deps.configManager,
|
|
618
|
-
currentTokens: session.estimatedContextTokens,
|
|
619
|
-
contextWindow: session.contextWindow,
|
|
620
|
-
messageCount: session.messageCount,
|
|
621
|
-
session: session.session,
|
|
622
|
-
});
|
|
623
|
-
maintenanceLines.push(buildKeyValueLine(width, [
|
|
624
|
-
{ label: 'level', value: maintenance.level, valueColor: maintenance.level === 'needs-repair' ? C.bad : maintenance.level === 'suggest-compact' || maintenance.level === 'watch' ? C.warn : C.good },
|
|
625
|
-
{ label: 'guidance', value: maintenance.guidanceMode, valueColor: C.value },
|
|
626
|
-
{ label: 'usage', value: `${maintenance.usagePct}%`, valueColor: maintenance.usagePct >= 80 ? C.bad : maintenance.usagePct >= 70 ? C.warn : C.value },
|
|
627
|
-
{ label: 'remaining', value: maintenance.remainingTokens.toLocaleString(), valueColor: C.value },
|
|
628
|
-
], { ...DEFAULT_PANEL_PALETTE, header: C.title }));
|
|
629
|
-
for (const reason of maintenance.reasons.slice(0, 3)) {
|
|
630
|
-
maintenanceLines.push(...buildBodyText(width, reason, { ...DEFAULT_PANEL_PALETTE, header: C.title }, C.dim));
|
|
529
|
+
if (this._view === 'domains') {
|
|
530
|
+
return this._renderDomainsView(width, height, postureSection, footerHint, palette);
|
|
631
531
|
}
|
|
632
|
-
if (
|
|
633
|
-
|
|
634
|
-
}
|
|
635
|
-
if (selectedName) {
|
|
636
|
-
const status = selectedHealth?.status ?? 'unknown';
|
|
637
|
-
selectedLines.push(buildKeyValueLine(width, [
|
|
638
|
-
{ label: 'provider', value: selectedName, valueColor: C.value },
|
|
639
|
-
{ label: 'status', value: statusLabel(status), valueColor: statusDot(status).color },
|
|
640
|
-
{ label: 'last ok', value: fmtAgo(selectedHealth?.lastSuccessAt), valueColor: C.value },
|
|
641
|
-
], { ...DEFAULT_PANEL_PALETTE, header: C.title }));
|
|
642
|
-
if (selectedHealth?.rateLimitExpiresAt && selectedHealth.rateLimitExpiresAt > Date.now()) {
|
|
643
|
-
selectedLines.push(...buildBodyText(width, `Cooldown: ${fmtCooldown(selectedHealth.rateLimitExpiresAt)}`, { ...DEFAULT_PANEL_PALETTE, header: C.title }, C.warn));
|
|
644
|
-
}
|
|
645
|
-
if (selectedHealth?.lastErrorMessage) {
|
|
646
|
-
selectedLines.push(...buildBodyText(width, `Last error: ${selectedHealth.lastErrorMessage}`, { ...DEFAULT_PANEL_PALETTE, header: C.title }, C.bad));
|
|
647
|
-
}
|
|
648
|
-
if (selectedAccount) {
|
|
649
|
-
selectedLines.push(buildKeyValueLine(width, [
|
|
650
|
-
{ label: 'route', value: selectedAccount.activeRoute, valueColor: routeColor(selectedAccount.activeRoute) },
|
|
651
|
-
{ label: 'preferred', value: selectedAccount.preferredRoute, valueColor: C.dim },
|
|
652
|
-
{ label: 'freshness', value: selectedAccount.authFreshness, valueColor: freshnessColor(selectedAccount.authFreshness) },
|
|
653
|
-
], { ...DEFAULT_PANEL_PALETTE, header: C.title }));
|
|
654
|
-
selectedLines.push(buildKeyValueLine(width, [
|
|
655
|
-
{ label: 'models', value: String(selectedAccount.modelCount), valueColor: C.value },
|
|
656
|
-
{ label: 'active', value: selectedAccount.active ? 'yes' : 'no', valueColor: selectedAccount.active ? C.good : C.dim },
|
|
657
|
-
], { ...DEFAULT_PANEL_PALETTE, header: C.title }));
|
|
658
|
-
selectedLines.push(...buildBodyText(width, `Auth route: ${selectedAccount.activeRouteReason}`, { ...DEFAULT_PANEL_PALETTE, header: C.title }, C.dim));
|
|
659
|
-
if (selectedAccount.fallbackRisk) {
|
|
660
|
-
selectedLines.push(...buildBodyText(width, `Fallback: ${selectedAccount.fallbackRisk}`, { ...DEFAULT_PANEL_PALETTE, header: C.title }, C.warn));
|
|
661
|
-
}
|
|
662
|
-
if (selectedAccount.issues.length > 0) {
|
|
663
|
-
selectedLines.push(...buildBodyText(width, `Issue: ${selectedAccount.issues[0]!}`, { ...DEFAULT_PANEL_PALETTE, header: C.title }, C.bad));
|
|
664
|
-
}
|
|
665
|
-
if (selectedAccount.recommendedActions.length > 0) {
|
|
666
|
-
selectedLines.push(...buildBodyText(width, `Next: ${selectedAccount.recommendedActions[0]!}`, { ...DEFAULT_PANEL_PALETTE, header: C.title }, C.title));
|
|
667
|
-
}
|
|
668
|
-
}
|
|
532
|
+
if (this._view === 'routes') {
|
|
533
|
+
return this._renderRoutesView(width, height, postureSection, footerHint, palette, selectedName);
|
|
669
534
|
}
|
|
535
|
+
return this._renderProvidersView(width, height, postureSection, footerHint, palette, providers, entriesById, snapshot.fallbackChain, selectedName);
|
|
536
|
+
}
|
|
670
537
|
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
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,
|
|
573
|
+
footerLines: [footerHint],
|
|
574
|
+
palette,
|
|
575
|
+
beforeSections: [postureSection, ...chainSections],
|
|
680
576
|
section: {
|
|
681
577
|
title: 'Providers',
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
const status = health?.status ?? 'unknown';
|
|
685
|
-
const latency = health?.lastLatencyMs !== undefined ? fmtMs(health.lastLatencyMs) : 'n/a';
|
|
686
|
-
const latencyFg = health?.lastLatencyMs !== undefined ? latencyColor(health.lastLatencyMs) : C.dim;
|
|
687
|
-
return buildPanelListRow(width, [
|
|
688
|
-
{ text: name.padEnd(16), fg: C.value },
|
|
689
|
-
{ text: statusLabel(status).padEnd(14), fg: statusDot(status).color },
|
|
690
|
-
{ text: ' lat ', fg: C.label },
|
|
691
|
-
{ text: latency.padEnd(8), fg: latencyFg },
|
|
692
|
-
{ text: ' ok ', fg: C.label },
|
|
693
|
-
{ text: fmtAgo(health?.lastSuccessAt).padEnd(10), fg: C.value },
|
|
694
|
-
], { ...DEFAULT_PANEL_PALETTE, header: C.title }, { selected: absolute === this._selectedIndex, selectedBg: '#111827' });
|
|
695
|
-
}),
|
|
578
|
+
fixedLines: [buildProviderColumnHeader(width, C)],
|
|
579
|
+
scrollableLines: rows,
|
|
696
580
|
selectedIndex: this._selectedIndex,
|
|
697
581
|
scrollOffset: this._scrollOffset,
|
|
698
582
|
guardRows: 1,
|
|
@@ -701,20 +585,94 @@ export class ProviderHealthPanel extends BasePanel {
|
|
|
701
585
|
},
|
|
702
586
|
afterSections: selectedSections,
|
|
703
587
|
});
|
|
704
|
-
this._scrollOffset =
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
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
|
+
|
|
712
670
|
return buildPanelWorkspace(width, height, {
|
|
713
671
|
title: 'Health',
|
|
714
|
-
intro,
|
|
715
|
-
sections,
|
|
716
|
-
footerLines: [
|
|
717
|
-
palette
|
|
672
|
+
intro: INTRO,
|
|
673
|
+
sections: [postureSection, resolved.section, maintenanceSection],
|
|
674
|
+
footerLines: [footerHint],
|
|
675
|
+
palette,
|
|
718
676
|
});
|
|
719
677
|
}
|
|
720
678
|
}
|