@pellux/goodvibes-tui 1.1.0 → 1.7.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.
Files changed (194) hide show
  1. package/CHANGELOG.md +85 -21
  2. package/README.md +20 -11
  3. package/docs/foundation-artifacts/operator-contract.json +1 -1
  4. package/package.json +2 -2
  5. package/src/core/alert-gating.ts +67 -0
  6. package/src/core/approval-alert.ts +72 -0
  7. package/src/core/budget-breach-notifier.ts +106 -0
  8. package/src/core/conversation-rendering.ts +19 -0
  9. package/src/core/conversation.ts +18 -0
  10. package/src/core/focus-tracker.ts +41 -0
  11. package/src/core/long-task-notifier.ts +33 -6
  12. package/src/core/system-message-router.ts +37 -51
  13. package/src/core/turn-cancellation.ts +20 -0
  14. package/src/core/turn-event-wiring.ts +64 -3
  15. package/src/export/cost-utils.ts +36 -0
  16. package/src/input/command-registry.ts +38 -1
  17. package/src/input/commands/checkpoint-runtime.ts +280 -0
  18. package/src/input/commands/codebase-runtime.ts +192 -0
  19. package/src/input/commands/eval.ts +1 -1
  20. package/src/input/commands/health-runtime.ts +1 -1
  21. package/src/input/commands/image-runtime.ts +112 -0
  22. package/src/input/commands/intelligence-runtime.ts +11 -1
  23. package/src/input/commands/local-auth-runtime.ts +4 -1
  24. package/src/input/commands/local-runtime.ts +9 -3
  25. package/src/input/commands/marketplace-runtime.ts +2 -2
  26. package/src/input/commands/memory.ts +6 -1
  27. package/src/input/commands/operator-panel-runtime.ts +40 -24
  28. package/src/input/commands/planning-runtime.ts +26 -3
  29. package/src/input/commands/platform-sandbox-runtime.ts +1 -6
  30. package/src/input/commands/plugin-runtime.ts +2 -2
  31. package/src/input/commands/policy-dispatch.ts +21 -0
  32. package/src/input/commands/provider-accounts-runtime.ts +1 -1
  33. package/src/input/commands/qrcode-runtime.ts +3 -3
  34. package/src/input/commands/recall-review.ts +35 -0
  35. package/src/input/commands/remote-runtime.ts +3 -3
  36. package/src/input/commands/runtime-services.ts +54 -13
  37. package/src/input/commands/services-runtime.ts +1 -1
  38. package/src/input/commands/session-workflow.ts +16 -1
  39. package/src/input/commands/settings-sync-runtime.ts +1 -1
  40. package/src/input/commands/shell-core.ts +15 -7
  41. package/src/input/commands/skills-runtime.ts +2 -8
  42. package/src/input/commands/subscription-runtime.ts +2 -2
  43. package/src/input/commands/test-runtime.ts +277 -0
  44. package/src/input/commands/websearch-runtime.ts +101 -0
  45. package/src/input/commands/work-plan-runtime.ts +36 -10
  46. package/src/input/commands/workstream-runtime.ts +338 -0
  47. package/src/input/commands.ts +12 -0
  48. package/src/input/config-modal-types.ts +161 -0
  49. package/src/input/config-modal.ts +377 -0
  50. package/src/input/feed-context-factory.ts +7 -8
  51. package/src/input/handler-command-route.ts +27 -17
  52. package/src/input/handler-feed-routes.ts +61 -23
  53. package/src/input/handler-feed.ts +47 -23
  54. package/src/input/handler-interactions.ts +1 -3
  55. package/src/input/handler-modal-routes.ts +65 -0
  56. package/src/input/handler-modal-stack.ts +3 -5
  57. package/src/input/handler-modal-token-routes.ts +16 -49
  58. package/src/input/handler-picker-routes.ts +11 -94
  59. package/src/input/handler-shortcuts.ts +24 -14
  60. package/src/input/handler-types.ts +2 -6
  61. package/src/input/handler-ui-state.ts +10 -22
  62. package/src/input/handler.ts +6 -28
  63. package/src/input/keybindings.ts +22 -8
  64. package/src/input/model-picker-types.ts +24 -6
  65. package/src/input/model-picker.ts +58 -0
  66. package/src/input/panel-integration-actions.ts +9 -170
  67. package/src/input/settings-modal-data.ts +95 -0
  68. package/src/input/settings-modal-mutations.ts +6 -4
  69. package/src/main.ts +24 -27
  70. package/src/panels/agent-inspector-shared.ts +2 -1
  71. package/src/panels/base-panel.ts +22 -1
  72. package/src/panels/builtin/agent.ts +21 -107
  73. package/src/panels/builtin/development.ts +15 -61
  74. package/src/panels/builtin/knowledge.ts +8 -32
  75. package/src/panels/builtin/operations.ts +84 -428
  76. package/src/panels/builtin/session.ts +21 -112
  77. package/src/panels/builtin/shared.ts +9 -43
  78. package/src/panels/builtin-modals.ts +218 -0
  79. package/src/panels/builtin-panels.ts +5 -0
  80. package/src/panels/cost-tracker-panel.ts +36 -10
  81. package/src/panels/diff-panel.ts +12 -43
  82. package/src/panels/eval-registry.ts +60 -0
  83. package/src/panels/fleet-panel.ts +800 -0
  84. package/src/panels/fleet-read-model.ts +477 -0
  85. package/src/panels/fleet-steer.ts +92 -0
  86. package/src/panels/fleet-stop.ts +125 -0
  87. package/src/panels/fleet-tabs.ts +230 -0
  88. package/src/panels/fleet-transcript.ts +356 -0
  89. package/src/panels/index.ts +7 -31
  90. package/src/panels/modals/hooks-modal.ts +187 -0
  91. package/src/panels/modals/keybindings-modal.ts +151 -0
  92. package/src/panels/modals/knowledge-modal.ts +158 -0
  93. package/src/panels/modals/local-auth-modal.ts +132 -0
  94. package/src/panels/modals/marketplace-modal.ts +218 -0
  95. package/src/panels/modals/memory-modal.ts +180 -0
  96. package/src/panels/modals/modal-surface-helpers.ts +54 -0
  97. package/src/panels/modals/modal-theme.ts +36 -0
  98. package/src/panels/modals/pairing-modal.ts +144 -0
  99. package/src/panels/modals/planning-modal.ts +282 -0
  100. package/src/panels/modals/plugins-modal.ts +174 -0
  101. package/src/panels/modals/policy-modal.ts +256 -0
  102. package/src/panels/modals/provider-health-modal.ts +136 -0
  103. package/src/panels/modals/remote-modal.ts +115 -0
  104. package/src/panels/modals/sandbox-modal.ts +150 -0
  105. package/src/panels/modals/security-modal.ts +217 -0
  106. package/src/panels/modals/services-modal.ts +179 -0
  107. package/src/panels/modals/settings-sync-modal.ts +142 -0
  108. package/src/panels/modals/skills-modal.ts +189 -0
  109. package/src/panels/modals/subscription-modal.ts +172 -0
  110. package/src/panels/modals/work-plan-modal.ts +149 -0
  111. package/src/panels/panel-confirm-overlay.ts +72 -0
  112. package/src/panels/panel-manager.ts +108 -5
  113. package/src/panels/project-planning-answer-actions.ts +4 -2
  114. package/src/panels/skills-panel.ts +7 -51
  115. package/src/panels/types.ts +13 -0
  116. package/src/permissions/hunk-selection.ts +179 -0
  117. package/src/permissions/prompt.ts +60 -4
  118. package/src/renderer/compaction-history-modal.ts +19 -3
  119. package/src/renderer/compaction-preview.ts +13 -2
  120. package/src/renderer/compaction-quality.ts +100 -0
  121. package/src/renderer/config-modal.ts +81 -0
  122. package/src/renderer/conversation-overlays.ts +10 -17
  123. package/src/renderer/fleet-tab-strip.ts +56 -0
  124. package/src/renderer/footer-tips.ts +10 -2
  125. package/src/renderer/git-status.ts +40 -0
  126. package/src/renderer/help-overlay.ts +2 -2
  127. package/src/renderer/model-workspace.ts +44 -1
  128. package/src/renderer/panel-workspace-bar.ts +8 -2
  129. package/src/renderer/turn-injection.ts +114 -0
  130. package/src/runtime/bootstrap-command-context.ts +21 -1
  131. package/src/runtime/bootstrap-command-parts.ts +34 -7
  132. package/src/runtime/bootstrap-core.ts +14 -4
  133. package/src/runtime/bootstrap-shell.ts +29 -16
  134. package/src/runtime/bootstrap.ts +11 -17
  135. package/src/runtime/code-index-services.ts +112 -0
  136. package/src/runtime/orchestrator-core-services.ts +49 -0
  137. package/src/runtime/process-lifecycle.ts +3 -1
  138. package/src/runtime/services.ts +91 -43
  139. package/src/runtime/ui-services.ts +8 -0
  140. package/src/runtime/workstream-services.ts +195 -0
  141. package/src/shell/blocking-input.ts +22 -1
  142. package/src/shell/ui-openers.ts +129 -17
  143. package/src/utils/format-duration.ts +8 -18
  144. package/src/utils/splash-lines.ts +1 -1
  145. package/src/version.ts +1 -1
  146. package/src/panels/agent-inspector-panel.ts +0 -786
  147. package/src/panels/approval-panel.ts +0 -252
  148. package/src/panels/automation-control-panel.ts +0 -479
  149. package/src/panels/cockpit-panel.ts +0 -481
  150. package/src/panels/cockpit-read-model.ts +0 -238
  151. package/src/panels/communication-panel.ts +0 -256
  152. package/src/panels/control-plane-panel.ts +0 -470
  153. package/src/panels/debug-panel.ts +0 -615
  154. package/src/panels/docs-panel.ts +0 -384
  155. package/src/panels/eval-panel.ts +0 -627
  156. package/src/panels/file-explorer-panel.ts +0 -673
  157. package/src/panels/file-preview-panel.ts +0 -517
  158. package/src/panels/hooks-panel.ts +0 -401
  159. package/src/panels/incident-review-panel.ts +0 -406
  160. package/src/panels/intelligence-panel.ts +0 -383
  161. package/src/panels/knowledge-graph-panel.ts +0 -515
  162. package/src/panels/marketplace-panel.ts +0 -399
  163. package/src/panels/memory-panel.ts +0 -558
  164. package/src/panels/ops-control-panel.ts +0 -329
  165. package/src/panels/ops-strategy-panel.ts +0 -321
  166. package/src/panels/orchestration-panel.ts +0 -465
  167. package/src/panels/panel-list-panel.ts +0 -566
  168. package/src/panels/plan-dashboard-panel.ts +0 -707
  169. package/src/panels/policy-panel.ts +0 -517
  170. package/src/panels/project-planning-panel.ts +0 -731
  171. package/src/panels/provider-health-panel.ts +0 -678
  172. package/src/panels/provider-health-tracker.ts +0 -310
  173. package/src/panels/provider-health-views.ts +0 -567
  174. package/src/panels/qr-panel.ts +0 -280
  175. package/src/panels/remote-panel.ts +0 -534
  176. package/src/panels/routes-panel.ts +0 -241
  177. package/src/panels/sandbox-panel.ts +0 -456
  178. package/src/panels/security-panel.ts +0 -447
  179. package/src/panels/services-panel.ts +0 -329
  180. package/src/panels/session-browser-panel.ts +0 -496
  181. package/src/panels/settings-sync-panel.ts +0 -398
  182. package/src/panels/subscription-panel.ts +0 -342
  183. package/src/panels/symbol-outline-panel.ts +0 -619
  184. package/src/panels/system-messages-panel.ts +0 -364
  185. package/src/panels/tasks-panel.ts +0 -608
  186. package/src/panels/thinking-panel.ts +0 -333
  187. package/src/panels/tool-inspector-panel.ts +0 -537
  188. package/src/panels/work-plan-panel.ts +0 -540
  189. package/src/panels/worktree-panel.ts +0 -360
  190. package/src/panels/wrfc-panel.ts +0 -790
  191. package/src/renderer/agent-detail-modal.ts +0 -466
  192. package/src/renderer/live-tail-modal.ts +0 -156
  193. package/src/renderer/process-modal.ts +0 -671
  194. package/src/renderer/qr-renderer.ts +0 -120
@@ -1,678 +0,0 @@
1
- import type { ConfigManager } from '@pellux/goodvibes-sdk/platform/config';
2
- import { BasePanel } from './base-panel.ts';
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
- import type { UiEventFeed } from '../runtime/ui-events.ts';
7
- import {
8
- type ProviderRuntimeInspectionQuery,
9
- } from '../runtime/ui-service-queries.ts';
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';
18
- import {
19
- buildProviderHealthDomainSummaries,
20
- type HealthDomainSummary,
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';
28
- import type {
29
- UiContinuitySnapshot,
30
- UiIntelligenceSnapshot,
31
- UiLocalAuthSnapshot,
32
- UiProvidersSnapshot,
33
- UiReadModel,
34
- UiRemoteSnapshot,
35
- UiSecuritySnapshot,
36
- UiSessionSnapshot,
37
- UiSettingsSnapshot,
38
- UiWorktreeSnapshot,
39
- } from '../runtime/ui-read-models.ts';
40
- import {
41
- buildEmptyState,
42
- buildKeyboardHints,
43
- buildPanelWorkspace,
44
- buildSummaryBlock,
45
- DEFAULT_PANEL_PALETTE,
46
- extendPalette,
47
- resolvePrimaryScrollableSection,
48
- type PanelWorkspaceSection,
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';
61
-
62
- // ---------------------------------------------------------------------------
63
- // Types
64
- // ---------------------------------------------------------------------------
65
-
66
- export interface ProviderHealthPanelDeps {
67
- readonly configManager: Pick<ConfigManager, 'get'>;
68
- readonly turnEvents: UiEventFeed<TurnEvent>;
69
- readonly providerEvents: UiEventFeed<ProviderEvent>;
70
- readonly providers: UiReadModel<UiProvidersSnapshot>;
71
- readonly session: UiReadModel<UiSessionSnapshot>;
72
- readonly security: UiReadModel<UiSecuritySnapshot>;
73
- readonly localAuth: UiReadModel<UiLocalAuthSnapshot>;
74
- readonly settings: UiReadModel<UiSettingsSnapshot>;
75
- readonly remote: UiReadModel<UiRemoteSnapshot>;
76
- readonly intelligence: UiReadModel<UiIntelligenceSnapshot>;
77
- readonly continuity: UiReadModel<UiContinuitySnapshot>;
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
- };
88
- }
89
-
90
- type ConsoleView = 'providers' | 'routes' | 'domains';
91
-
92
- const CONSOLE_VIEWS: readonly ConsoleView[] = ['providers', 'routes', 'domains'] as const;
93
-
94
- // Base chrome plus console accents (domain accents only — hex ratchet).
95
- const C = extendPalette(DEFAULT_PANEL_PALETTE, {
96
- title: '#00ffff',
97
- unknown: '244',
98
- rowSelectBg: '#111827',
99
- });
100
-
101
- const INTRO = 'Provider console: status, latency timelines, error attribution, auth routes, and fallback posture.';
102
-
103
- // ---------------------------------------------------------------------------
104
- // ProviderHealthPanel — the merged provider console (WO-112)
105
- // ---------------------------------------------------------------------------
106
-
107
- /**
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.
118
- */
119
- export class ProviderHealthPanel extends BasePanel {
120
- private _unsubs: Array<() => void> = [];
121
- private _tickTimerId: ReturnType<typeof setInterval> | null = null;
122
- private _selectedIndex = 0;
123
- private _scrollOffset = 0;
124
- private _view: ConsoleView = 'providers';
125
- private _accountRecords = new Map<string, ProviderAccountPosture>();
126
- private _accountRefreshAt = 0;
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;
133
- private readonly providerHealthTracker = new ProviderHealthTracker();
134
-
135
- constructor(
136
- private readonly providerRuntime: ProviderRuntimeInspectionQuery,
137
- private readonly deps: ProviderHealthPanelDeps,
138
- private readonly requestRender: () => void = () => {},
139
- ) {
140
- super('provider-health', 'Health', 'N', 'providers');
141
- this._modelState = deps.modelState?.get() ?? this._syntheticModelState();
142
- this._dataProvider = new ProviderHealthDataProvider(this._buildHealthState(), this._modelState);
143
- this._subscribe();
144
- void this._refreshAccountPosture(true);
145
- // Background 30s posture refresh (auth routes go stale otherwise); cleared on destroy.
146
- this.registerTimer(setInterval(() => { void this._refreshAccountPosture(); }, 30_000));
147
- }
148
-
149
- // -------------------------------------------------------------------------
150
- // Event subscription
151
- // -------------------------------------------------------------------------
152
-
153
- private _subscribe(): void {
154
- this._unsubs.push(
155
- this.deps.turnEvents.on('TURN_SUBMITTED', () => {
156
- this.providerHealthTracker.onTurnStart();
157
- }),
158
- );
159
-
160
- this._unsubs.push(
161
- this.deps.turnEvents.on('STREAM_START', () => {
162
- this.providerHealthTracker.onStreamStart();
163
- }),
164
- );
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
-
174
- this._unsubs.push(
175
- this.deps.turnEvents.on('LLM_RESPONSE_RECEIVED', (payload) => {
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();
185
- this._markDirtyAndRender();
186
- }),
187
- );
188
-
189
- this._unsubs.push(
190
- this.deps.turnEvents.on('TURN_ERROR', (payload) => {
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();
202
- this._markDirtyAndRender();
203
- }),
204
- );
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
-
218
- this._unsubs.push(
219
- this.deps.providerEvents.on('PROVIDERS_CHANGED', () => {
220
- this.providerHealthTracker.onProvidersChanged(this._knownProviders());
221
- void this._refreshAccountPosture(true);
222
- this._pushHealthState();
223
- this._markDirtyAndRender();
224
- }),
225
- );
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
-
239
- for (const readModel of [
240
- this.deps.providers,
241
- this.deps.session,
242
- this.deps.security,
243
- this.deps.localAuth,
244
- this.deps.settings,
245
- this.deps.remote,
246
- this.deps.intelligence,
247
- this.deps.continuity,
248
- this.deps.worktrees,
249
- ] as const) {
250
- this._unsubs.push(readModel.subscribe(() => this._markDirtyAndRender()));
251
- }
252
- }
253
-
254
- private _markDirtyAndRender(): void {
255
- this.markDirty();
256
- this.requestRender();
257
- }
258
-
259
- // -------------------------------------------------------------------------
260
- // Lifecycle
261
- // -------------------------------------------------------------------------
262
-
263
- override onActivate(): void {
264
- super.onActivate();
265
- this.markDirty();
266
- void this._refreshAccountPosture(true);
267
- this._startTickTimer();
268
- }
269
-
270
- override onDeactivate(): void {
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();
275
- }
276
-
277
- override onDestroy(): void {
278
- super.onDestroy();
279
- this._tickTimerId = null;
280
- this._dataProvider.dispose();
281
- for (const unsub of this._unsubs) unsub();
282
- this._unsubs = [];
283
- }
284
-
285
- private _startTickTimer(): void {
286
- if (this._tickTimerId !== null) return;
287
- this._tickTimerId = this.registerTimer(setInterval(() => {
288
- this.markDirty();
289
- this.requestRender();
290
- }, 1_000));
291
- }
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
-
304
- handleInput(key: string): boolean {
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();
318
- if (providers.length === 0) return false;
319
- if (key === 'j' || key === 'down' || key === '\x1b[B') {
320
- this._selectedIndex = Math.min(providers.length - 1, this._selectedIndex + 1);
321
- this.markDirty();
322
- return true;
323
- }
324
- if (key === 'k' || key === 'up' || key === '\x1b[A') {
325
- this._selectedIndex = Math.max(0, this._selectedIndex - 1);
326
- this.markDirty();
327
- return true;
328
- }
329
- return false;
330
- }
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
-
421
- private async _refreshAccountPosture(force = false): Promise<void> {
422
- if (this._accountLoading) return;
423
- if (!force && Date.now() - this._accountRefreshAt < 15_000) return;
424
- this._accountLoading = true;
425
- try {
426
- const snapshots = await this.providerRuntime.inspectAll();
427
- this._accountRecords = new Map(
428
- snapshots
429
- .map((snapshot) => buildAccountPosture(snapshot))
430
- .map((record) => [record.providerId, record] as const),
431
- );
432
- this._accountRefreshAt = Date.now();
433
- this._pushHealthState();
434
- this.markDirty();
435
- this.requestRender();
436
- } finally {
437
- this._accountLoading = false;
438
- }
439
- }
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
-
466
- // -------------------------------------------------------------------------
467
- // Rendering
468
- // -------------------------------------------------------------------------
469
-
470
- override render(width: number, height: number): Line[] {
471
- const palette = { ...DEFAULT_PANEL_PALETTE, header: C.title };
472
-
473
- const providers = this._knownProviders();
474
- this._selectedIndex = Math.min(this._selectedIndex, Math.max(0, providers.length - 1));
475
-
476
- if (providers.length === 0) {
477
- return buildPanelWorkspace(width, height, {
478
- title: 'Health',
479
- intro: INTRO,
480
- sections: [{
481
- lines: buildEmptyState(
482
- width,
483
- ' No providers registered.',
484
- 'Provider health appears here once model providers are available and the runtime begins making requests.',
485
- [
486
- { command: '/provider', summary: 'review current provider and model selection' },
487
- { command: '/subscription', summary: 'review provider login and subscription state' },
488
- ],
489
- palette,
490
- ),
491
- }],
492
- palette,
493
- });
494
- }
495
-
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];
499
-
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,
512
- }
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
- };
528
-
529
- if (this._view === 'domains') {
530
- return this._renderDomainsView(width, height, postureSection, footerHint, palette);
531
- }
532
- if (this._view === 'routes') {
533
- return this._renderRoutesView(width, height, postureSection, footerHint, palette, selectedName);
534
- }
535
- return this._renderProvidersView(width, height, postureSection, footerHint, palette, providers, entriesById, snapshot.fallbackChain, selectedName);
536
- }
537
-
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],
576
- section: {
577
- title: 'Providers',
578
- fixedLines: [buildProviderColumnHeader(width, C)],
579
- scrollableLines: rows,
580
- selectedIndex: this._selectedIndex,
581
- scrollOffset: this._scrollOffset,
582
- guardRows: 1,
583
- minRows: 4,
584
- appendWindowSummary: { dimColor: C.dim },
585
- },
586
- afterSections: selectedSections,
587
- });
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
-
670
- return buildPanelWorkspace(width, height, {
671
- title: 'Health',
672
- intro: INTRO,
673
- sections: [postureSection, resolved.section, maintenanceSection],
674
- footerLines: [footerHint],
675
- palette,
676
- });
677
- }
678
- }