@pellux/goodvibes-agent 1.4.4 → 1.5.1

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 (230) hide show
  1. package/CHANGELOG.md +22 -0
  2. package/README.md +52 -7
  3. package/dist/package/main.js +7491 -12253
  4. package/docs/README.md +2 -2
  5. package/docs/channels-remote-and-api.md +1 -1
  6. package/docs/getting-started.md +2 -2
  7. package/docs/release-and-publishing.md +1 -1
  8. package/docs/tools-and-commands.md +5 -5
  9. package/package.json +4 -2
  10. package/release/live-verification/live-verification.json +13 -13
  11. package/release/live-verification/live-verification.md +15 -15
  12. package/release/performance-snapshot.json +2 -2
  13. package/release/release-notes.md +5 -7
  14. package/release/release-readiness.json +6 -6
  15. package/src/agent/behavior-discovery-summary.ts +4 -18
  16. package/src/agent/calendar-registry.ts +322 -0
  17. package/src/agent/competitive-feature-inventory.ts +268 -130
  18. package/src/agent/document-registry.ts +5 -1
  19. package/src/agent/email/email-service.ts +350 -0
  20. package/src/agent/email/imap-client.ts +596 -0
  21. package/src/agent/email/smtp-client.ts +453 -0
  22. package/src/agent/ics-calendar.ts +662 -0
  23. package/src/agent/ics-timezone.ts +172 -0
  24. package/src/agent/markdown-frontmatter.ts +31 -0
  25. package/src/agent/memory-safety.ts +1 -1
  26. package/src/agent/note-registry.ts +3 -9
  27. package/src/agent/persona-discovery.ts +3 -15
  28. package/src/agent/persona-registry.ts +1 -10
  29. package/src/agent/research-source-registry.ts +5 -1
  30. package/src/agent/routine-discovery.ts +3 -15
  31. package/src/agent/runtime-profile.ts +3 -0
  32. package/src/agent/skill-discovery.ts +3 -15
  33. package/src/agent/skill-draft-proposer.ts +203 -0
  34. package/src/agent/skill-draft-runner.ts +201 -0
  35. package/src/agent/skill-registry.ts +36 -1
  36. package/src/agent/skill-standard.ts +99 -0
  37. package/src/agent/vibe-file.ts +7 -22
  38. package/src/cli/completion.ts +1 -1
  39. package/src/cli/config-overrides.ts +10 -1
  40. package/src/cli/redaction.ts +17 -5
  41. package/src/config/provider-model.ts +2 -1
  42. package/src/config/secret-config.ts +13 -6
  43. package/src/core/activity-feed.ts +97 -0
  44. package/src/core/away-digest.ts +161 -0
  45. package/src/core/conversation-rendering.ts +7 -3
  46. package/src/core/conversation.ts +22 -15
  47. package/src/core/hardware-profile.ts +362 -0
  48. package/src/core/last-seen-store.ts +78 -0
  49. package/src/core/plain-language.ts +52 -0
  50. package/src/core/setup-incomplete-hint.ts +90 -0
  51. package/src/core/system-message-router.ts +38 -87
  52. package/src/input/agent-workspace-basic-command-editor-submission.ts +60 -220
  53. package/src/input/agent-workspace-basic-command-editors.ts +39 -141
  54. package/src/input/agent-workspace-categories.ts +40 -125
  55. package/src/input/agent-workspace-command-editor.ts +4 -0
  56. package/src/input/agent-workspace-host-category.ts +0 -5
  57. package/src/input/agent-workspace-local-editor-submission.ts +3 -1
  58. package/src/input/agent-workspace-navigation.ts +10 -3
  59. package/src/input/agent-workspace-onboarding-actions.ts +132 -0
  60. package/src/input/agent-workspace-onboarding-categories.ts +29 -26
  61. package/src/input/agent-workspace-onboarding-finish.ts +11 -4
  62. package/src/input/agent-workspace-onboarding-state.ts +111 -0
  63. package/src/input/agent-workspace-profile-editor-submission.ts +260 -0
  64. package/src/input/agent-workspace-profile-editors.ts +155 -0
  65. package/src/input/agent-workspace-subscription-editor.ts +7 -0
  66. package/src/input/agent-workspace-types.ts +5 -1
  67. package/src/input/agent-workspace.ts +65 -39
  68. package/src/input/command-registry.ts +18 -5
  69. package/src/input/commands/agent-runtime-profile-runtime.ts +2 -1
  70. package/src/input/commands/agent-skills-runtime.ts +60 -3
  71. package/src/input/commands/calendar-runtime.ts +209 -0
  72. package/src/input/commands/channels-runtime.ts +1 -0
  73. package/src/input/commands/command-error.ts +9 -0
  74. package/src/input/commands/compat-runtime.ts +1 -0
  75. package/src/input/commands/config.ts +1 -0
  76. package/src/input/commands/conversation-runtime.ts +1 -1
  77. package/src/input/commands/delegation-runtime.ts +5 -6
  78. package/src/input/commands/email-runtime.ts +387 -0
  79. package/src/input/commands/experience-runtime.ts +17 -4
  80. package/src/input/commands/guidance-runtime.ts +1 -1
  81. package/src/input/commands/health-runtime.ts +6 -1
  82. package/src/input/commands/knowledge-format.ts +73 -0
  83. package/src/input/commands/knowledge.ts +9 -74
  84. package/src/input/commands/local-provider-runtime.ts +2 -1
  85. package/src/input/commands/local-runtime.ts +10 -4
  86. package/src/input/commands/mcp-runtime.ts +7 -0
  87. package/src/input/commands/notify-runtime.ts +17 -1
  88. package/src/input/commands/onboarding-runtime.ts +1 -0
  89. package/src/input/commands/operator-actions-runtime.ts +1 -0
  90. package/src/input/commands/operator-runtime.ts +3 -0
  91. package/src/input/commands/personas-runtime.ts +1 -0
  92. package/src/input/commands/platform-access-runtime.ts +40 -17
  93. package/src/input/commands/product-runtime.ts +6 -1
  94. package/src/input/commands/provider-accounts-runtime.ts +3 -2
  95. package/src/input/commands/qrcode-runtime.ts +1 -0
  96. package/src/input/commands/routines-runtime.ts +1 -0
  97. package/src/input/commands/runtime-services.ts +0 -13
  98. package/src/input/commands/security-runtime.ts +1 -0
  99. package/src/input/commands/session-content.ts +1 -0
  100. package/src/input/commands/shell-core.ts +5 -2
  101. package/src/input/commands/subscription-runtime.ts +33 -9
  102. package/src/input/commands/support-bundle-runtime.ts +8 -1
  103. package/src/input/commands/tasks-runtime.ts +1 -0
  104. package/src/input/commands/tts-runtime.ts +1 -0
  105. package/src/input/commands/vibe-runtime.ts +1 -0
  106. package/src/input/commands.ts +4 -0
  107. package/src/input/feed-context-factory.ts +3 -12
  108. package/src/input/handler-command-route.ts +7 -60
  109. package/src/input/handler-feed-routes.ts +0 -194
  110. package/src/input/handler-feed.ts +2 -54
  111. package/src/input/handler-interactions.ts +0 -2
  112. package/src/input/handler-modal-stack.ts +0 -9
  113. package/src/input/handler-picker-routes.ts +24 -1
  114. package/src/input/handler-shortcuts.ts +11 -40
  115. package/src/input/handler-ui-state.ts +13 -0
  116. package/src/input/handler.ts +8 -35
  117. package/src/input/keybindings.ts +40 -17
  118. package/src/input/model-picker-local-fit.ts +130 -0
  119. package/src/input/shell-passthrough.ts +58 -0
  120. package/src/input/submission-router.ts +0 -5
  121. package/src/main.ts +129 -90
  122. package/src/renderer/activity-sidebar.ts +186 -0
  123. package/src/renderer/agent-workspace-context-lines.ts +5 -48
  124. package/src/renderer/agent-workspace-style.ts +1 -1
  125. package/src/renderer/agent-workspace.ts +14 -2
  126. package/src/renderer/compositor.ts +37 -125
  127. package/src/renderer/conversation-overlays.ts +3 -3
  128. package/src/renderer/help-overlay.ts +7 -5
  129. package/src/renderer/model-workspace.ts +101 -86
  130. package/src/{panels → renderer}/polish.ts +3 -3
  131. package/src/renderer/shell-surface.ts +4 -5
  132. package/src/renderer/status-token.ts +31 -11
  133. package/src/renderer/tab-strip.ts +1 -1
  134. package/src/renderer/tool-call.ts +7 -8
  135. package/src/renderer/tool-labels.ts +92 -0
  136. package/src/renderer/ui-factory.ts +48 -96
  137. package/src/runtime/bootstrap-command-context.ts +0 -5
  138. package/src/runtime/bootstrap-command-parts.ts +6 -15
  139. package/src/runtime/bootstrap-core.ts +34 -13
  140. package/src/runtime/bootstrap-hook-bridge.ts +3 -1
  141. package/src/runtime/bootstrap-shell.ts +3 -50
  142. package/src/runtime/bootstrap.ts +3 -4
  143. package/src/runtime/context.ts +1 -1
  144. package/src/runtime/diagnostics/panels/index.ts +0 -1
  145. package/src/runtime/diagnostics/panels/policy.ts +1 -1
  146. package/src/runtime/execution-ledger.ts +16 -1
  147. package/src/runtime/index.ts +6 -1
  148. package/src/runtime/onboarding/index.ts +1 -0
  149. package/src/runtime/onboarding/onboarding-state.ts +200 -0
  150. package/src/{panels → runtime}/provider-account-snapshot.ts +5 -2
  151. package/src/runtime/services.ts +16 -4
  152. package/src/runtime/terminal-output-guard.ts +7 -0
  153. package/src/runtime/tool-permission-safety.ts +1 -1
  154. package/src/runtime/ui/model-picker/data-provider.ts +1 -1
  155. package/src/runtime/ui/model-picker/health-enrichment.ts +2 -2
  156. package/src/runtime/ui/model-picker/types.ts +2 -2
  157. package/src/runtime/ui/provider-health/data-provider.ts +3 -3
  158. package/src/runtime/ui/provider-health/types.ts +2 -2
  159. package/src/runtime/ui-services.ts +0 -2
  160. package/src/shell/agent-workspace-fullscreen.ts +0 -1
  161. package/src/shell/autonomy-surfacing.ts +272 -0
  162. package/src/shell/session-continuity-hints.ts +0 -6
  163. package/src/shell/startup-wiring.ts +221 -0
  164. package/src/shell/ui-openers.ts +18 -29
  165. package/src/tools/agent-context-policy.ts +1 -1
  166. package/src/tools/agent-harness-background-processes.ts +4 -4
  167. package/src/tools/agent-harness-browser-cockpit-route.ts +3 -3
  168. package/src/tools/agent-harness-command-runner.ts +6 -1
  169. package/src/tools/agent-harness-document-ops.ts +26 -53
  170. package/src/tools/agent-harness-execution-history.ts +1 -13
  171. package/src/tools/agent-harness-execution-posture.ts +0 -15
  172. package/src/tools/agent-harness-keybinding-metadata.ts +28 -29
  173. package/src/tools/agent-harness-local-model-cookbook.ts +24 -1
  174. package/src/tools/agent-harness-metadata.ts +16 -0
  175. package/src/tools/agent-harness-mode-catalog.ts +2 -9
  176. package/src/tools/agent-harness-provider-account-metadata.ts +2 -2
  177. package/src/tools/agent-harness-setup-posture-utils.ts +1 -1
  178. package/src/tools/agent-harness-tool-schema.ts +13 -14
  179. package/src/tools/agent-harness-tool.ts +27 -23
  180. package/src/tools/agent-harness-ui-surface-metadata.ts +10 -20
  181. package/src/tools/agent-harness-workspace-action-runner.ts +3 -4
  182. package/src/tools/agent-workspace-tool.ts +2 -33
  183. package/src/utils/terminal-width.ts +9 -3
  184. package/src/version.ts +1 -1
  185. package/src/input/agent-workspace-panel-route.ts +0 -44
  186. package/src/input/panel-integration-actions.ts +0 -26
  187. package/src/panels/approval-panel.ts +0 -149
  188. package/src/panels/automation-control-panel.ts +0 -212
  189. package/src/panels/base-panel.ts +0 -254
  190. package/src/panels/builtin/agent.ts +0 -58
  191. package/src/panels/builtin/knowledge.ts +0 -26
  192. package/src/panels/builtin/operations.ts +0 -121
  193. package/src/panels/builtin/session.ts +0 -138
  194. package/src/panels/builtin/shared.ts +0 -275
  195. package/src/panels/builtin/usage.ts +0 -21
  196. package/src/panels/builtin-panels.ts +0 -23
  197. package/src/panels/confirm-state.ts +0 -61
  198. package/src/panels/context-visualizer-panel.ts +0 -204
  199. package/src/panels/cost-tracker-panel.ts +0 -444
  200. package/src/panels/docs-panel.ts +0 -285
  201. package/src/panels/index.ts +0 -25
  202. package/src/panels/knowledge-panel.ts +0 -417
  203. package/src/panels/memory-panel.ts +0 -226
  204. package/src/panels/panel-list-panel.ts +0 -464
  205. package/src/panels/panel-manager.ts +0 -570
  206. package/src/panels/provider-accounts-panel.ts +0 -233
  207. package/src/panels/provider-health-domains.ts +0 -208
  208. package/src/panels/provider-health-panel.ts +0 -720
  209. package/src/panels/provider-health-tracker.ts +0 -115
  210. package/src/panels/provider-stats-panel.ts +0 -366
  211. package/src/panels/qr-panel.ts +0 -207
  212. package/src/panels/schedule-panel.ts +0 -321
  213. package/src/panels/scrollable-list-panel.ts +0 -491
  214. package/src/panels/search-focus.ts +0 -32
  215. package/src/panels/security-panel.ts +0 -295
  216. package/src/panels/session-browser-panel.ts +0 -395
  217. package/src/panels/session-maintenance.ts +0 -125
  218. package/src/panels/subscription-panel.ts +0 -263
  219. package/src/panels/system-messages-panel.ts +0 -230
  220. package/src/panels/tasks-panel.ts +0 -344
  221. package/src/panels/thinking-panel.ts +0 -304
  222. package/src/panels/token-budget-panel.ts +0 -475
  223. package/src/panels/tool-inspector-panel.ts +0 -436
  224. package/src/panels/types.ts +0 -54
  225. package/src/renderer/panel-composite.ts +0 -158
  226. package/src/renderer/panel-tab-bar.ts +0 -69
  227. package/src/renderer/panel-workspace-bar.ts +0 -42
  228. package/src/runtime/diagnostics/panels/panel-resources.ts +0 -118
  229. package/src/tools/agent-harness-panel-metadata.ts +0 -211
  230. /package/src/runtime/perf/{panel-health-monitor.ts → component-health.ts} +0 -0
@@ -1,720 +0,0 @@
1
- import type { ConfigManager } from '@pellux/goodvibes-sdk/platform/config';
2
- import { BasePanel } from './base-panel.ts';
3
- import { createEmptyLine, createStyledCell, type Line } from '../types/grid.ts';
4
- import type { ProviderAuthRouteDescriptor } from '@pellux/goodvibes-sdk/platform/providers';
5
- import type { ProviderEvent, TurnEvent } 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 { ProviderHealthTracker, type ProviderStatus } from './provider-health-tracker.ts';
11
- import {
12
- buildProviderHealthDomainSummaries,
13
- type HealthDomainSummary,
14
- } from './provider-health-domains.ts';
15
- import type {
16
- UiContinuitySnapshot,
17
- UiLocalAuthSnapshot,
18
- UiProvidersSnapshot,
19
- UiReadModel,
20
- UiRemoteSnapshot,
21
- UiSecuritySnapshot,
22
- UiSessionSnapshot,
23
- UiSettingsSnapshot,
24
- } from '../runtime/ui-read-models.ts';
25
- import { evaluateSessionMaintenance } from '@/runtime/index.ts';
26
- import {
27
- buildBodyText,
28
- buildDetailBlock,
29
- buildEmptyState,
30
- buildGuidanceLine,
31
- buildKeyValueLine,
32
- buildPanelListRow,
33
- buildPanelLine,
34
- buildPanelWorkspace,
35
- buildSummaryBlock,
36
- DEFAULT_PANEL_PALETTE,
37
- resolvePrimaryScrollableSection,
38
- type PanelWorkspaceSection,
39
- } from './polish.ts';
40
- import { formatProviderAuthRouteId } from '../provider-auth-route-display.ts';
41
-
42
- // ---------------------------------------------------------------------------
43
- // Types
44
- // ---------------------------------------------------------------------------
45
-
46
- export interface ProviderHealthPanelDeps {
47
- readonly configManager: Pick<ConfigManager, 'get'>;
48
- readonly turnEvents: UiEventFeed<TurnEvent>;
49
- readonly providerEvents: UiEventFeed<ProviderEvent>;
50
- readonly providers: UiReadModel<UiProvidersSnapshot>;
51
- readonly session: UiReadModel<UiSessionSnapshot>;
52
- readonly security: UiReadModel<UiSecuritySnapshot>;
53
- readonly localAuth: UiReadModel<UiLocalAuthSnapshot>;
54
- readonly settings: UiReadModel<UiSettingsSnapshot>;
55
- readonly remote: UiReadModel<UiRemoteSnapshot>;
56
- readonly continuity: UiReadModel<UiContinuitySnapshot>;
57
- }
58
-
59
- // Colors
60
- // ---------------------------------------------------------------------------
61
-
62
- const C = {
63
- title: '#00ffff',
64
- online: '#5fd700',
65
- rateLimit: '#ffaf00',
66
- error: '#ff5f5f',
67
- unknown: '244',
68
- label: '244',
69
- value: '252',
70
- dim: '240',
71
- provName: '#e2e8f0',
72
- errMsg: '#ff5f5f',
73
- latGood: '#5fd700',
74
- latWarn: '#ffaf00',
75
- latBad: '#ff5f5f',
76
- separator: '#374151',
77
- } as const;
78
-
79
- const LATENCY_WARN_MS = 2_000;
80
- const LATENCY_BAD_MS = 5_000;
81
-
82
- // ---------------------------------------------------------------------------
83
- // Helpers
84
- // ---------------------------------------------------------------------------
85
-
86
- function statusDot(status: ProviderStatus): { char: string; color: string } {
87
- switch (status) {
88
- case 'online': return { char: '●', color: C.online };
89
- case 'rate-limited': return { char: '◐', color: C.rateLimit };
90
- case 'error': return { char: '✕', color: C.error };
91
- default: return { char: '○', color: C.unknown };
92
- }
93
- }
94
-
95
- function statusLabel(status: ProviderStatus): string {
96
- switch (status) {
97
- case 'online': return 'online';
98
- case 'rate-limited': return 'rate-limited';
99
- case 'error': return 'error';
100
- default: return 'unknown';
101
- }
102
- }
103
-
104
- function latencyColor(ms: number): string {
105
- if (ms >= LATENCY_BAD_MS) return C.latBad;
106
- if (ms >= LATENCY_WARN_MS) return C.latWarn;
107
- return C.latGood;
108
- }
109
-
110
- function fmtMs(ms: number): string {
111
- if (ms <= 0) return 'n/a';
112
- if (ms >= 10_000) return `${(ms / 1000).toFixed(1)}s`;
113
- if (ms >= 1_000) return `${(ms / 1000).toFixed(2)}s`;
114
- return `${Math.round(ms)}ms`;
115
- }
116
-
117
- function fmtAgo(ts: number | undefined): string {
118
- if (!ts) return 'n/a';
119
- const sec = Math.floor((Date.now() - ts) / 1000);
120
- if (sec < 60) return `${sec}s ago`;
121
- if (sec < 3600) return `${Math.floor(sec / 60)}m ago`;
122
- return `${Math.floor(sec / 3600)}h ago`;
123
- }
124
-
125
- function fmtCooldown(expiresAt: number): string {
126
- const remaining = Math.ceil((expiresAt - Date.now()) / 1000);
127
- if (remaining <= 0) return 'expiring';
128
- return `${remaining}s cooldown`;
129
- }
130
-
131
- type ProviderPanelAuthRoute = ProviderAuthRouteDescriptor['route'] | 'unconfigured';
132
- type ProviderPanelAuthFreshness = NonNullable<ProviderAuthRouteDescriptor['freshness']> | 'unconfigured';
133
-
134
- interface ProviderRuntimeRecord {
135
- readonly providerId: string;
136
- readonly active: boolean;
137
- readonly modelCount: number;
138
- readonly activeRoute: ProviderPanelAuthRoute;
139
- readonly preferredRoute: ProviderPanelAuthRoute;
140
- readonly activeRouteReason: string;
141
- readonly authFreshness: ProviderPanelAuthFreshness;
142
- readonly fallbackRisk?: string;
143
- readonly issues: readonly string[];
144
- readonly recommendedActions: readonly string[];
145
- }
146
-
147
- function domainColor(level: HealthDomainSummary['level']): string {
148
- switch (level) {
149
- case 'good':
150
- return C.online;
151
- case 'warn':
152
- return C.rateLimit;
153
- case 'bad':
154
- return C.error;
155
- default:
156
- return C.value;
157
- }
158
- }
159
-
160
- const AUTH_ROUTE_PRIORITY: readonly ProviderPanelAuthRoute[] = [
161
- 'subscription-oauth',
162
- 'service-oauth',
163
- 'secret-ref',
164
- 'api-key',
165
- 'anonymous',
166
- 'none',
167
- 'unconfigured',
168
- ] as const;
169
-
170
- function routePriority(route: ProviderPanelAuthRoute): number {
171
- const priority = AUTH_ROUTE_PRIORITY.indexOf(route);
172
- return priority >= 0 ? priority : AUTH_ROUTE_PRIORITY.length;
173
- }
174
-
175
- function routeColor(route: ProviderPanelAuthRoute): string {
176
- switch (route) {
177
- case 'subscription-oauth':
178
- return C.title;
179
- case 'service-oauth':
180
- return C.online;
181
- case 'api-key':
182
- return C.rateLimit;
183
- case 'secret-ref':
184
- return C.value;
185
- case 'anonymous':
186
- case 'none':
187
- return C.dim;
188
- default:
189
- return C.value;
190
- }
191
- }
192
-
193
- function freshnessColor(freshness: ProviderPanelAuthFreshness): string {
194
- switch (freshness) {
195
- case 'expired':
196
- return C.error;
197
- case 'expiring':
198
- case 'pending':
199
- return C.rateLimit;
200
- case 'healthy':
201
- return C.online;
202
- default:
203
- return C.dim;
204
- }
205
- }
206
-
207
- function buildSyntheticAuthRoutes(
208
- auth: {
209
- readonly mode: 'api-key' | 'oauth' | 'anonymous' | 'none';
210
- readonly configured: boolean;
211
- readonly detail?: string;
212
- readonly envVars?: readonly string[];
213
- } | undefined,
214
- ): readonly ProviderAuthRouteDescriptor[] {
215
- if (!auth) return [];
216
- switch (auth.mode) {
217
- case 'none':
218
- return [{
219
- route: 'none',
220
- label: 'No auth required',
221
- configured: true,
222
- usable: true,
223
- freshness: 'healthy',
224
- detail: auth.detail ?? 'Provider does not require interactive credentials.',
225
- }];
226
- case 'anonymous':
227
- return [{
228
- route: 'anonymous',
229
- label: 'Anonymous / local access',
230
- configured: auth.configured,
231
- usable: auth.configured,
232
- freshness: auth.configured ? 'healthy' : 'unconfigured',
233
- detail: auth.detail ?? 'Provider can be used without stored credentials.',
234
- }];
235
- case 'api-key':
236
- return [{
237
- route: 'api-key',
238
- label: 'Ambient API key',
239
- configured: auth.configured,
240
- usable: auth.configured,
241
- freshness: auth.configured ? 'healthy' : 'unconfigured',
242
- detail: auth.detail ?? 'Provider expects a configured API key.',
243
- ...(auth.envVars?.length ? { envVars: auth.envVars } : {}),
244
- ...(auth.envVars?.length
245
- ? { repairHints: [`Set ${auth.envVars.join(' or ')} in the environment or secrets store.`] }
246
- : {}),
247
- }];
248
- case 'oauth':
249
- return [{
250
- route: 'service-oauth',
251
- label: 'OAuth session',
252
- configured: auth.configured,
253
- usable: auth.configured,
254
- freshness: auth.configured ? 'healthy' : 'unconfigured',
255
- detail: auth.detail ?? 'Provider expects an OAuth-backed credential.',
256
- repairHints: ['Refresh or repair the provider OAuth session before relying on it.'],
257
- }];
258
- default:
259
- return [];
260
- }
261
- }
262
-
263
- function getUsableRoute(route: ProviderAuthRouteDescriptor): boolean {
264
- return route.usable ?? route.configured;
265
- }
266
-
267
- function pickRoute(
268
- routes: readonly ProviderAuthRouteDescriptor[],
269
- ): ProviderAuthRouteDescriptor | null {
270
- if (routes.length === 0) return null;
271
- return [...routes].sort((left, right) => routePriority(left.route) - routePriority(right.route))[0] ?? null;
272
- }
273
-
274
- function buildProviderRuntimeRecord(
275
- snapshot: {
276
- readonly providerId: string;
277
- readonly active: boolean;
278
- readonly modelCount: number;
279
- readonly runtime: {
280
- readonly auth?: {
281
- readonly mode: 'api-key' | 'oauth' | 'anonymous' | 'none';
282
- readonly configured: boolean;
283
- readonly detail?: string;
284
- readonly envVars?: readonly string[];
285
- readonly routes?: readonly ProviderAuthRouteDescriptor[];
286
- };
287
- };
288
- },
289
- ): ProviderRuntimeRecord {
290
- const auth = snapshot.runtime.auth;
291
- const routes = auth?.routes?.length ? auth.routes : buildSyntheticAuthRoutes(auth);
292
- const configuredRoutes = routes.filter((route) => route.configured);
293
- const usableRoutes = routes.filter(getUsableRoute);
294
- const preferredRoute = pickRoute(configuredRoutes.length > 0 ? configuredRoutes : routes);
295
- const activeRoute = pickRoute(usableRoutes.length > 0 ? usableRoutes : (preferredRoute ? [preferredRoute] : []));
296
- const activeRouteId = activeRoute?.route ?? 'unconfigured';
297
- const preferredRouteId = preferredRoute?.route ?? activeRouteId;
298
- const authFreshness = activeRoute?.freshness ?? (activeRouteId === 'none' ? 'healthy' : 'unconfigured');
299
- const fallbackRisk = usableRoutes.length > 1
300
- ? 'Multiple auth routes are simultaneously usable; verify route priority before switching providers.'
301
- : undefined;
302
-
303
- const issueSet = new Set<string>();
304
- const actionSet = new Set<string>();
305
-
306
- if (activeRouteId === 'unconfigured' && auth?.mode !== 'none') {
307
- issueSet.add('Provider has no usable auth route configured.');
308
- }
309
-
310
- for (const route of routes) {
311
- if (route.freshness === 'expired') {
312
- issueSet.add(route.detail ?? `${route.label} is expired.`);
313
- } else if (route.freshness === 'pending') {
314
- issueSet.add(route.detail ?? `${route.label} is pending completion.`);
315
- } else if (route.configured && !getUsableRoute(route)) {
316
- issueSet.add(route.detail ?? `${route.label} is configured but not currently usable.`);
317
- }
318
- for (const hint of route.repairHints ?? []) {
319
- if (hint.trim().length > 0) actionSet.add(hint);
320
- }
321
- }
322
-
323
- if (fallbackRisk) issueSet.add(fallbackRisk);
324
- if (issueSet.size > 0 && actionSet.size === 0 && activeRouteId !== 'none') {
325
- actionSet.add(`Review ${snapshot.providerId} provider credentials and routing metadata.`);
326
- }
327
-
328
- return {
329
- providerId: snapshot.providerId,
330
- active: snapshot.active,
331
- modelCount: snapshot.modelCount,
332
- activeRoute: activeRouteId,
333
- preferredRoute: preferredRouteId,
334
- activeRouteReason: activeRoute?.detail
335
- ?? auth?.detail
336
- ?? (activeRouteId === 'none'
337
- ? 'Provider does not require interactive credentials.'
338
- : 'No usable auth route is configured for this provider.'),
339
- authFreshness,
340
- ...(fallbackRisk ? { fallbackRisk } : {}),
341
- issues: [...issueSet],
342
- recommendedActions: [...actionSet],
343
- };
344
- }
345
-
346
- // ---------------------------------------------------------------------------
347
- // ProviderHealthPanel
348
- // ---------------------------------------------------------------------------
349
-
350
- /**
351
- * Real-time provider health / status dashboard.
352
- *
353
- * Displays for each known provider:
354
- * - Status indicator (online / rate-limited / error / unknown)
355
- * - Last response latency
356
- * - Last seen timestamp
357
- * - Last error message (if any)
358
- * - Active cooldown timer for rate-limited providers
359
- */
360
- export class ProviderHealthPanel extends BasePanel {
361
- private _unsubs: Array<() => void> = [];
362
- private _refreshTimerId: ReturnType<typeof setInterval> | null = null;
363
- private _selectedIndex = 0;
364
- private _scrollOffset = 0;
365
- private _accountRecords = new Map<string, ProviderRuntimeRecord>();
366
- private _accountRefreshAt = 0;
367
- private _accountLoading = false;
368
- private readonly providerHealthTracker = new ProviderHealthTracker();
369
-
370
- constructor(
371
- private readonly providerRuntime: ProviderRuntimeInspectionQuery,
372
- private readonly deps: ProviderHealthPanelDeps,
373
- private readonly requestRender: () => void = () => {},
374
- ) {
375
- super('provider-health', 'Health', 'N', 'monitoring');
376
- this._subscribe();
377
- void this._refreshAccountPosture(true);
378
- this._ensureRefreshTimer();
379
- }
380
-
381
- // -------------------------------------------------------------------------
382
- // Event subscription
383
- // -------------------------------------------------------------------------
384
-
385
- private _subscribe(): void {
386
- this._unsubs.push(
387
- this.deps.turnEvents.on('TURN_SUBMITTED', () => {
388
- this.providerHealthTracker.onTurnStart();
389
- }),
390
- );
391
-
392
- this._unsubs.push(
393
- this.deps.turnEvents.on('STREAM_START', () => {
394
- this.providerHealthTracker.onStreamStart();
395
- }),
396
- );
397
-
398
- this._unsubs.push(
399
- this.deps.turnEvents.on('LLM_RESPONSE_RECEIVED', (payload) => {
400
- this.providerHealthTracker.onLlmResponse(payload.provider);
401
- this._markDirtyAndRender();
402
- }),
403
- );
404
-
405
- this._unsubs.push(
406
- this.deps.turnEvents.on('TURN_ERROR', (payload) => {
407
- this.providerHealthTracker.onTurnError(payload.error);
408
- this._markDirtyAndRender();
409
- }),
410
- );
411
-
412
- this._unsubs.push(
413
- this.deps.providerEvents.on('PROVIDERS_CHANGED', () => {
414
- this.providerHealthTracker.onProvidersChanged([
415
- ...new Set([
416
- ...this.deps.providers.getSnapshot().providerIds,
417
- ...this.providerRuntime.listProviderIds(),
418
- ]),
419
- ]);
420
- void this._refreshAccountPosture(true);
421
- this._markDirtyAndRender();
422
- }),
423
- );
424
-
425
- for (const readModel of [
426
- this.deps.providers,
427
- this.deps.session,
428
- this.deps.security,
429
- this.deps.localAuth,
430
- this.deps.settings,
431
- this.deps.remote,
432
- this.deps.continuity,
433
- ] as const) {
434
- this._unsubs.push(readModel.subscribe(() => this._markDirtyAndRender()));
435
- }
436
- }
437
-
438
- private _markDirtyAndRender(): void {
439
- this.markDirty();
440
- this.requestRender();
441
- }
442
-
443
- // -------------------------------------------------------------------------
444
- // Lifecycle
445
- // -------------------------------------------------------------------------
446
-
447
- override onActivate(): void {
448
- super.onActivate();
449
- this.markDirty();
450
- void this._refreshAccountPosture(true);
451
- this._ensureRefreshTimer();
452
- }
453
-
454
- override onDeactivate(): void {
455
- super.onDeactivate();
456
- }
457
-
458
- override onDestroy(): void {
459
- super.onDestroy();
460
- this._refreshTimerId = null;
461
- for (const unsub of this._unsubs) unsub();
462
- this._unsubs = [];
463
- }
464
-
465
- private _ensureRefreshTimer(): void {
466
- if (this._refreshTimerId !== null) return;
467
- this._refreshTimerId = this.registerTimer(setInterval(() => {
468
- if (Date.now() - this._accountRefreshAt > 30_000) {
469
- void this._refreshAccountPosture();
470
- }
471
- this.markDirty();
472
- this.requestRender();
473
- }, 1_000));
474
- }
475
-
476
- handleInput(key: string): boolean {
477
- const knownSet = new Set([
478
- ...this.deps.providers.getSnapshot().providerIds,
479
- ...this.providerRuntime.listProviderIds(),
480
- ...this._accountRecords.keys(),
481
- ]);
482
- for (const h of this.providerHealthTracker.getAll()) knownSet.add(h.name);
483
- const providers = [...knownSet].sort();
484
- if (providers.length === 0) return false;
485
- if (key === 'j' || key === 'down' || key === '\x1b[B') {
486
- this._selectedIndex = Math.min(providers.length - 1, this._selectedIndex + 1);
487
- this.markDirty();
488
- return true;
489
- }
490
- if (key === 'k' || key === 'up' || key === '\x1b[A') {
491
- this._selectedIndex = Math.max(0, this._selectedIndex - 1);
492
- this.markDirty();
493
- return true;
494
- }
495
- return false;
496
- }
497
-
498
- private async _refreshAccountPosture(force = false): Promise<void> {
499
- if (this._accountLoading) return;
500
- if (!force && Date.now() - this._accountRefreshAt < 15_000) return;
501
- this._accountLoading = true;
502
- try {
503
- const snapshots = await this.providerRuntime.inspectAll();
504
- this._accountRecords = new Map(
505
- snapshots
506
- .map((snapshot) => buildProviderRuntimeRecord(snapshot))
507
- .map((record) => [record.providerId, record] as const),
508
- );
509
- this._accountRefreshAt = Date.now();
510
- this.markDirty();
511
- this.requestRender();
512
- } finally {
513
- this._accountLoading = false;
514
- }
515
- }
516
-
517
- // -------------------------------------------------------------------------
518
- // Rendering
519
- // -------------------------------------------------------------------------
520
-
521
- override render(width: number, height: number): Line[] {
522
- const intro = 'Cross-domain health workspace for providers, auth, settings, remote, MCP, continuity, and maintenance posture.';
523
-
524
- const knownSet = new Set([
525
- ...this.deps.providers.getSnapshot().providerIds,
526
- ...this.providerRuntime.listProviderIds(),
527
- ...this._accountRecords.keys(),
528
- ]);
529
- for (const h of this.providerHealthTracker.getAll()) knownSet.add(h.name);
530
- const providers = [...knownSet].sort();
531
- this._selectedIndex = Math.min(this._selectedIndex, Math.max(0, providers.length - 1));
532
-
533
- if (providers.length === 0) {
534
- return buildPanelWorkspace(width, height, {
535
- title: 'Health',
536
- intro,
537
- sections: [{
538
- lines: buildEmptyState(
539
- width,
540
- ' No providers registered.',
541
- 'Provider health appears here once model providers are available and the runtime begins making requests.',
542
- [
543
- { command: '/provider', summary: 'review current provider and model selection' },
544
- { command: '/subscription', summary: 'review provider login and subscription state' },
545
- ],
546
- { ...DEFAULT_PANEL_PALETTE, header: C.title, headerBg: '#0f172a' },
547
- ),
548
- }],
549
- palette: { ...DEFAULT_PANEL_PALETTE, header: C.title, headerBg: '#0f172a' },
550
- });
551
- }
552
-
553
- let online = 0;
554
- let rateLimited = 0;
555
- let errored = 0;
556
- let accountIssues = 0;
557
- let expiringAuth = 0;
558
- for (const name of providers) {
559
- const status = this.providerHealthTracker.get(name)?.status ?? 'unknown';
560
- if (status === 'online') online++;
561
- else if (status === 'rate-limited') rateLimited++;
562
- else if (status === 'error') errored++;
563
- const account = this._accountRecords.get(name);
564
- if (account) {
565
- accountIssues += account.issues.length;
566
- if (account.authFreshness === 'expiring' || account.authFreshness === 'expired' || account.authFreshness === 'pending') {
567
- expiringAuth++;
568
- }
569
- }
570
- }
571
-
572
- const postureLines = [
573
- buildKeyValueLine(width, [
574
- { label: 'providers', value: String(providers.length), valueColor: C.value },
575
- { label: 'online', value: String(online), valueColor: C.online },
576
- { label: 'rate-limited', value: String(rateLimited), valueColor: C.rateLimit },
577
- { label: 'error', value: String(errored), valueColor: C.error },
578
- { label: 'auth alerts', value: String(expiringAuth), valueColor: expiringAuth > 0 ? C.rateLimit : C.dim },
579
- { label: 'account issues', value: String(accountIssues), valueColor: accountIssues > 0 ? C.error : C.dim },
580
- ], { ...DEFAULT_PANEL_PALETTE, header: C.title, headerBg: '#0f172a' }),
581
- buildGuidanceLine(width, '/provider', 'review provider selection and routing if health posture degrades', { ...DEFAULT_PANEL_PALETTE, header: C.title, headerBg: '#0f172a' }),
582
- buildGuidanceLine(width, '/accounts', 'inspect auth routes, fallback posture, and billing-path safety', { ...DEFAULT_PANEL_PALETTE, header: C.title, headerBg: '#0f172a' }),
583
- ];
584
-
585
- const domainLines: Line[] = [];
586
- for (const domain of buildProviderHealthDomainSummaries({
587
- configManager: this.deps.configManager,
588
- auth: this.deps.localAuth.getSnapshot(),
589
- settings: this.deps.settings.getSnapshot(),
590
- remote: this.deps.remote.getSnapshot(),
591
- security: this.deps.security.getSnapshot(),
592
- continuity: this.deps.continuity.getSnapshot(),
593
- session: this.deps.session.getSnapshot(),
594
- })) {
595
- domainLines.push(buildPanelLine(width, [
596
- [' ', C.label],
597
- [domain.name.padEnd(14), C.provName],
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, headerBg: '#0f172a' }, C.dim));
604
- }
605
- if (domain.nextSteps.length > 1) {
606
- domainLines.push(...buildBodyText(width, ` next: ${domain.nextSteps.join(' | ')}`, { ...DEFAULT_PANEL_PALETTE, header: C.title, headerBg: '#0f172a' }, C.title));
607
- }
608
- }
609
-
610
- const selectedName = providers[this._selectedIndex];
611
- const selectedHealth = selectedName ? this.providerHealthTracker.get(selectedName) : undefined;
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.error : maintenance.level === 'suggest-compact' || maintenance.level === 'watch' ? C.rateLimit : C.online },
625
- { label: 'guidance', value: maintenance.guidanceMode, valueColor: C.value },
626
- { label: 'usage', value: `${maintenance.usagePct}%`, valueColor: maintenance.usagePct >= 80 ? C.error : maintenance.usagePct >= 70 ? C.rateLimit : C.value },
627
- { label: 'remaining', value: maintenance.remainingTokens.toLocaleString(), valueColor: C.value },
628
- ], { ...DEFAULT_PANEL_PALETTE, header: C.title, headerBg: '#0f172a' }));
629
- for (const reason of maintenance.reasons.slice(0, 3)) {
630
- maintenanceLines.push(...buildBodyText(width, reason, { ...DEFAULT_PANEL_PALETTE, header: C.title, headerBg: '#0f172a' }, C.dim));
631
- }
632
- if (maintenance.nextSteps.length > 0) {
633
- maintenanceLines.push(...buildBodyText(width, `Next: ${maintenance.nextSteps.join(' | ')}`, { ...DEFAULT_PANEL_PALETTE, header: C.title, headerBg: '#0f172a' }, C.title));
634
- }
635
- if (selectedName) {
636
- const status = selectedHealth?.status ?? 'unknown';
637
- selectedLines.push(buildKeyValueLine(width, [
638
- { label: 'provider', value: selectedName, valueColor: C.provName },
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, headerBg: '#0f172a' }));
642
- if (selectedHealth?.rateLimitExpiresAt && selectedHealth.rateLimitExpiresAt > Date.now()) {
643
- selectedLines.push(...buildBodyText(width, `Cooldown: ${fmtCooldown(selectedHealth.rateLimitExpiresAt)}`, { ...DEFAULT_PANEL_PALETTE, header: C.title, headerBg: '#0f172a' }, C.rateLimit));
644
- }
645
- if (selectedHealth?.lastErrorMessage) {
646
- selectedLines.push(...buildBodyText(width, `Last error: ${selectedHealth.lastErrorMessage}`, { ...DEFAULT_PANEL_PALETTE, header: C.title, headerBg: '#0f172a' }, C.errMsg));
647
- }
648
- if (selectedAccount) {
649
- selectedLines.push(buildKeyValueLine(width, [
650
- { label: 'route', value: formatProviderAuthRouteId(selectedAccount.activeRoute), valueColor: routeColor(selectedAccount.activeRoute) },
651
- { label: 'preferred', value: formatProviderAuthRouteId(selectedAccount.preferredRoute), valueColor: C.dim },
652
- { label: 'freshness', value: selectedAccount.authFreshness, valueColor: freshnessColor(selectedAccount.authFreshness) },
653
- ], { ...DEFAULT_PANEL_PALETTE, header: C.title, headerBg: '#0f172a' }));
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.online : C.dim },
657
- ], { ...DEFAULT_PANEL_PALETTE, header: C.title, headerBg: '#0f172a' }));
658
- selectedLines.push(...buildBodyText(width, `Route reason: ${selectedAccount.activeRouteReason}`, { ...DEFAULT_PANEL_PALETTE, header: C.title, headerBg: '#0f172a' }, C.dim));
659
- if (selectedAccount.fallbackRisk) {
660
- selectedLines.push(...buildBodyText(width, `Routing risk: ${selectedAccount.fallbackRisk}`, { ...DEFAULT_PANEL_PALETTE, header: C.title, headerBg: '#0f172a' }, C.rateLimit));
661
- }
662
- if (selectedAccount.issues.length > 0) {
663
- selectedLines.push(...buildBodyText(width, `Issue: ${selectedAccount.issues[0]!}`, { ...DEFAULT_PANEL_PALETTE, header: C.title, headerBg: '#0f172a' }, C.errMsg));
664
- }
665
- if (selectedAccount.recommendedActions.length > 0) {
666
- selectedLines.push(...buildBodyText(width, `Next: ${selectedAccount.recommendedActions[0]!}`, { ...DEFAULT_PANEL_PALETTE, header: C.title, headerBg: '#0f172a' }, C.title));
667
- }
668
- }
669
- }
670
-
671
- const postureSection: PanelWorkspaceSection = { lines: buildSummaryBlock(width, 'Health posture', postureLines, { ...DEFAULT_PANEL_PALETTE, header: C.title, headerBg: '#0f172a' }) };
672
- const domainsSection: PanelWorkspaceSection = { title: 'Repair Domains', lines: domainLines };
673
- const maintenanceSections = maintenanceLines.length > 0 ? [{ title: 'Session Maintenance', lines: maintenanceLines } satisfies PanelWorkspaceSection] : [];
674
- const selectedSections = selectedLines.length > 0 ? [{ lines: buildDetailBlock(width, 'Selected provider', selectedLines, { ...DEFAULT_PANEL_PALETTE, header: C.title, headerBg: '#0f172a' }) } satisfies PanelWorkspaceSection] : [];
675
- const resolvedProvidersSection = resolvePrimaryScrollableSection(width, height, {
676
- intro,
677
- footerLines: [buildPanelLine(width, [[' j/k or Up/Down move live cooldowns refresh while active', C.dim]])],
678
- palette: { ...DEFAULT_PANEL_PALETTE, header: C.title, headerBg: '#0f172a' },
679
- beforeSections: [postureSection, domainsSection, ...maintenanceSections],
680
- section: {
681
- title: 'Providers',
682
- scrollableLines: providers.map((name, absolute) => {
683
- const health = this.providerHealthTracker.get(name);
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.provName },
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, headerBg: '#0f172a' }, { selected: absolute === this._selectedIndex, selectedBg: '#111827' });
695
- }),
696
- selectedIndex: this._selectedIndex,
697
- scrollOffset: this._scrollOffset,
698
- guardRows: 1,
699
- minRows: 4,
700
- appendWindowSummary: { dimColor: C.dim },
701
- },
702
- afterSections: selectedSections,
703
- });
704
- this._scrollOffset = resolvedProvidersSection.scrollOffset;
705
- const sections: PanelWorkspaceSection[] = [
706
- postureSection,
707
- domainsSection,
708
- ...maintenanceSections,
709
- resolvedProvidersSection.section,
710
- ...selectedSections,
711
- ];
712
- return buildPanelWorkspace(width, height, {
713
- title: 'Health',
714
- intro,
715
- sections,
716
- footerLines: [buildPanelLine(width, [[' j/k or Up/Down move live cooldowns refresh while active', C.dim]])],
717
- palette: { ...DEFAULT_PANEL_PALETTE, header: C.title, headerBg: '#0f172a' },
718
- });
719
- }
720
- }