@pellux/goodvibes-agent 0.1.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/.goodvibes/GOODVIBES.md +35 -0
- package/.goodvibes/agents/reviewer.md +48 -0
- package/.goodvibes/skills/add-provider/SKILL.md +199 -0
- package/CHANGELOG.md +25 -0
- package/README.md +74 -0
- package/bin/goodvibes-agent.ts +2 -0
- package/docs/README.md +23 -0
- package/docs/deployment-and-services.md +57 -0
- package/docs/getting-started.md +53 -0
- package/docs/release-and-publishing.md +46 -0
- package/package.json +134 -0
- package/scripts/check-bun.sh +20 -0
- package/src/audio/player.ts +156 -0
- package/src/audio/spoken-turn-controller.ts +203 -0
- package/src/audio/spoken-turn-model-routing.ts +117 -0
- package/src/audio/spoken-turn-wiring.ts +44 -0
- package/src/audio/text-chunker.ts +110 -0
- package/src/cli/bundle-command.ts +227 -0
- package/src/cli/completion.ts +90 -0
- package/src/cli/config-overrides.ts +159 -0
- package/src/cli/endpoints.ts +63 -0
- package/src/cli/entrypoint.ts +172 -0
- package/src/cli/help.ts +299 -0
- package/src/cli/index.ts +11 -0
- package/src/cli/management-commands.ts +426 -0
- package/src/cli/management.ts +744 -0
- package/src/cli/network-posture.ts +46 -0
- package/src/cli/package-verification.ts +123 -0
- package/src/cli/parser.ts +369 -0
- package/src/cli/provider-auth-routes.ts +22 -0
- package/src/cli/provider-classification.ts +107 -0
- package/src/cli/redaction.ts +105 -0
- package/src/cli/service-command.ts +26 -0
- package/src/cli/service-posture.ts +482 -0
- package/src/cli/status.ts +383 -0
- package/src/cli/surface-command.ts +247 -0
- package/src/cli/tui-startup.ts +32 -0
- package/src/cli/types.ts +69 -0
- package/src/cli-flags.ts +21 -0
- package/src/config/goodvibes-home-audit.ts +465 -0
- package/src/config/index.ts +57 -0
- package/src/config/provider-model.ts +23 -0
- package/src/config/secret-config.ts +119 -0
- package/src/config/secrets.ts +71 -0
- package/src/config/surface.ts +1 -0
- package/src/core/composer-state.ts +61 -0
- package/src/core/conversation-rendering.ts +359 -0
- package/src/core/conversation.ts +551 -0
- package/src/core/history.ts +45 -0
- package/src/core/orchestrator.ts +7 -0
- package/src/core/system-message-router.ts +171 -0
- package/src/daemon/cli.ts +55 -0
- package/src/daemon/safe-serve.ts +61 -0
- package/src/input/agent-workspace.ts +428 -0
- package/src/input/autocomplete.ts +96 -0
- package/src/input/bookmark-modal.ts +115 -0
- package/src/input/command-args-hint.ts +36 -0
- package/src/input/command-registry.ts +329 -0
- package/src/input/commands/agent-externalized-tui.ts +73 -0
- package/src/input/commands/agent-workspace-runtime.ts +17 -0
- package/src/input/commands/branch-runtime.ts +72 -0
- package/src/input/commands/cloudflare-runtime.ts +370 -0
- package/src/input/commands/config.ts +18 -0
- package/src/input/commands/control-room-runtime.ts +255 -0
- package/src/input/commands/conversation-runtime.ts +207 -0
- package/src/input/commands/discovery-runtime.ts +52 -0
- package/src/input/commands/eval.ts +204 -0
- package/src/input/commands/experience-runtime.ts +278 -0
- package/src/input/commands/guidance-runtime.ts +106 -0
- package/src/input/commands/health-runtime.ts +434 -0
- package/src/input/commands/hooks-runtime.ts +148 -0
- package/src/input/commands/incident-runtime.ts +95 -0
- package/src/input/commands/integration-runtime.ts +394 -0
- package/src/input/commands/intelligence-runtime.ts +223 -0
- package/src/input/commands/knowledge.ts +531 -0
- package/src/input/commands/local-auth-runtime.ts +105 -0
- package/src/input/commands/local-provider-runtime.ts +170 -0
- package/src/input/commands/local-runtime.ts +392 -0
- package/src/input/commands/local-setup-review.ts +199 -0
- package/src/input/commands/local-setup-transfer.ts +135 -0
- package/src/input/commands/local-setup.ts +282 -0
- package/src/input/commands/managed-runtime.ts +209 -0
- package/src/input/commands/marketplace-runtime.ts +290 -0
- package/src/input/commands/mcp-runtime.ts +432 -0
- package/src/input/commands/memory-product-runtime.ts +111 -0
- package/src/input/commands/memory.ts +151 -0
- package/src/input/commands/notify-runtime.ts +83 -0
- package/src/input/commands/onboarding-runtime.ts +14 -0
- package/src/input/commands/operator-panel-runtime.ts +146 -0
- package/src/input/commands/operator-runtime.ts +392 -0
- package/src/input/commands/planning-runtime.ts +205 -0
- package/src/input/commands/platform-access-runtime.ts +422 -0
- package/src/input/commands/platform-services-runtime.ts +246 -0
- package/src/input/commands/policy-dispatch.ts +339 -0
- package/src/input/commands/policy.ts +17 -0
- package/src/input/commands/product-runtime.ts +351 -0
- package/src/input/commands/profile-sync-runtime.ts +99 -0
- package/src/input/commands/provider-accounts-runtime.ts +113 -0
- package/src/input/commands/provider.ts +363 -0
- package/src/input/commands/qrcode-runtime.ts +20 -0
- package/src/input/commands/quit-shared.ts +162 -0
- package/src/input/commands/recall-bundle.ts +132 -0
- package/src/input/commands/recall-capture.ts +152 -0
- package/src/input/commands/recall-query.ts +229 -0
- package/src/input/commands/recall-review.ts +98 -0
- package/src/input/commands/recall-shared.ts +22 -0
- package/src/input/commands/remote-runtime-pool.ts +106 -0
- package/src/input/commands/remote-runtime-setup.ts +199 -0
- package/src/input/commands/remote-runtime.ts +431 -0
- package/src/input/commands/replay-runtime.ts +18 -0
- package/src/input/commands/runtime-services.ts +291 -0
- package/src/input/commands/schedule-runtime.ts +91 -0
- package/src/input/commands/services-runtime.ts +209 -0
- package/src/input/commands/session-content.ts +408 -0
- package/src/input/commands/session-workflow.ts +464 -0
- package/src/input/commands/session.ts +375 -0
- package/src/input/commands/settings-sync-runtime.ts +174 -0
- package/src/input/commands/share-runtime.ts +119 -0
- package/src/input/commands/shell-core.ts +307 -0
- package/src/input/commands/skills-runtime.ts +221 -0
- package/src/input/commands/subscription-runtime.ts +434 -0
- package/src/input/commands/tasks-runtime.ts +230 -0
- package/src/input/commands/teamwork-runtime.ts +339 -0
- package/src/input/commands/teleport-runtime.ts +57 -0
- package/src/input/commands/tts-runtime.ts +29 -0
- package/src/input/commands/work-plan-runtime.ts +169 -0
- package/src/input/commands.ts +131 -0
- package/src/input/feed-context-factory.ts +254 -0
- package/src/input/file-picker.ts +192 -0
- package/src/input/handler-command-route.ts +180 -0
- package/src/input/handler-content-actions.ts +497 -0
- package/src/input/handler-feed-routes.ts +648 -0
- package/src/input/handler-feed.ts +452 -0
- package/src/input/handler-interactions.ts +281 -0
- package/src/input/handler-modal-routes.ts +418 -0
- package/src/input/handler-modal-stack.ts +263 -0
- package/src/input/handler-modal-token-routes.ts +329 -0
- package/src/input/handler-onboarding-cloudflare.ts +391 -0
- package/src/input/handler-onboarding.ts +620 -0
- package/src/input/handler-picker-routes.ts +472 -0
- package/src/input/handler-prompt-buffer.ts +320 -0
- package/src/input/handler-shortcuts.ts +213 -0
- package/src/input/handler-ui-state.ts +372 -0
- package/src/input/handler.ts +729 -0
- package/src/input/input-history.ts +297 -0
- package/src/input/keybindings.ts +292 -0
- package/src/input/mcp-workspace.ts +554 -0
- package/src/input/model-picker-provider-filter.ts +28 -0
- package/src/input/model-picker-types.ts +137 -0
- package/src/input/model-picker.ts +797 -0
- package/src/input/onboarding/handler-onboarding-routes.ts +125 -0
- package/src/input/onboarding/onboarding-runtime-status.ts +87 -0
- package/src/input/onboarding/onboarding-wizard-apply.ts +277 -0
- package/src/input/onboarding/onboarding-wizard-cloudflare-step.ts +494 -0
- package/src/input/onboarding/onboarding-wizard-cloudflare.ts +204 -0
- package/src/input/onboarding/onboarding-wizard-constants.ts +158 -0
- package/src/input/onboarding/onboarding-wizard-external-surface-extra-specs.ts +130 -0
- package/src/input/onboarding/onboarding-wizard-external-surfaces.ts +762 -0
- package/src/input/onboarding/onboarding-wizard-helpers.ts +167 -0
- package/src/input/onboarding/onboarding-wizard-rules.ts +256 -0
- package/src/input/onboarding/onboarding-wizard-state.ts +365 -0
- package/src/input/onboarding/onboarding-wizard-steps.ts +798 -0
- package/src/input/onboarding/onboarding-wizard-types.ts +195 -0
- package/src/input/onboarding/onboarding-wizard.ts +711 -0
- package/src/input/panel-integration-actions.ts +78 -0
- package/src/input/profile-picker-modal.ts +222 -0
- package/src/input/search.ts +100 -0
- package/src/input/selection-modal.ts +163 -0
- package/src/input/selection.ts +135 -0
- package/src/input/session-picker-modal.ts +136 -0
- package/src/input/settings-modal-behavior.ts +37 -0
- package/src/input/settings-modal-secrets.ts +41 -0
- package/src/input/settings-modal-subscriptions.ts +95 -0
- package/src/input/settings-modal-types.ts +91 -0
- package/src/input/settings-modal.ts +793 -0
- package/src/input/submission-intent.ts +17 -0
- package/src/input/submission-router.ts +59 -0
- package/src/input/tts-settings-actions.ts +100 -0
- package/src/main.ts +792 -0
- package/src/mcp/runtime-reload.ts +81 -0
- package/src/panels/agent-inspector-panel.ts +521 -0
- package/src/panels/agent-inspector-shared.ts +94 -0
- package/src/panels/agent-logs-panel.ts +559 -0
- package/src/panels/agent-logs-shared.ts +129 -0
- package/src/panels/approval-panel.ts +150 -0
- package/src/panels/automation-control-panel.ts +212 -0
- package/src/panels/base-panel.ts +254 -0
- package/src/panels/builtin/agent.ts +117 -0
- package/src/panels/builtin/development.ts +31 -0
- package/src/panels/builtin/knowledge.ts +26 -0
- package/src/panels/builtin/operations.ts +349 -0
- package/src/panels/builtin/session.ts +129 -0
- package/src/panels/builtin/shared.ts +274 -0
- package/src/panels/builtin-panels.ts +23 -0
- package/src/panels/cockpit-panel.ts +183 -0
- package/src/panels/communication-panel.ts +153 -0
- package/src/panels/confirm-state.ts +61 -0
- package/src/panels/context-visualizer-panel.ts +204 -0
- package/src/panels/control-plane-panel.ts +211 -0
- package/src/panels/cost-tracker-panel.ts +444 -0
- package/src/panels/debug-panel.ts +432 -0
- package/src/panels/diff-panel.ts +520 -0
- package/src/panels/docs-panel.ts +283 -0
- package/src/panels/eval-panel.ts +399 -0
- package/src/panels/file-explorer-panel.ts +584 -0
- package/src/panels/file-preview-panel.ts +434 -0
- package/src/panels/forensics-panel.ts +364 -0
- package/src/panels/git-panel.ts +638 -0
- package/src/panels/hooks-panel.ts +239 -0
- package/src/panels/incident-review-panel.ts +197 -0
- package/src/panels/index.ts +46 -0
- package/src/panels/intelligence-panel.ts +176 -0
- package/src/panels/knowledge-panel.ts +345 -0
- package/src/panels/local-auth-panel.ts +130 -0
- package/src/panels/marketplace-panel.ts +212 -0
- package/src/panels/memory-panel.ts +225 -0
- package/src/panels/ops-control-panel.ts +150 -0
- package/src/panels/ops-strategy-panel.ts +235 -0
- package/src/panels/orchestration-panel.ts +273 -0
- package/src/panels/panel-list-panel.ts +509 -0
- package/src/panels/panel-manager.ts +570 -0
- package/src/panels/panel-picker.ts +106 -0
- package/src/panels/plan-dashboard-panel.ts +274 -0
- package/src/panels/plugins-panel.ts +178 -0
- package/src/panels/policy-panel.ts +308 -0
- package/src/panels/polish.ts +717 -0
- package/src/panels/project-planning-panel.ts +711 -0
- package/src/panels/provider-account-snapshot.ts +259 -0
- package/src/panels/provider-accounts-panel.ts +218 -0
- package/src/panels/provider-health-domains.ts +215 -0
- package/src/panels/provider-health-panel.ts +727 -0
- package/src/panels/provider-health-tracker.ts +115 -0
- package/src/panels/provider-stats-panel.ts +366 -0
- package/src/panels/qr-panel.ts +182 -0
- package/src/panels/remote-panel.ts +449 -0
- package/src/panels/routes-panel.ts +178 -0
- package/src/panels/sandbox-panel.ts +283 -0
- package/src/panels/schedule-panel.ts +329 -0
- package/src/panels/scrollable-list-panel.ts +491 -0
- package/src/panels/search-focus.ts +32 -0
- package/src/panels/security-panel.ts +295 -0
- package/src/panels/services-panel.ts +231 -0
- package/src/panels/session-browser-panel.ts +400 -0
- package/src/panels/session-maintenance.ts +125 -0
- package/src/panels/settings-sync-panel.ts +120 -0
- package/src/panels/skills-panel.ts +431 -0
- package/src/panels/subscription-panel.ts +263 -0
- package/src/panels/symbol-outline-panel.ts +486 -0
- package/src/panels/system-messages-panel.ts +230 -0
- package/src/panels/tasks-panel.ts +399 -0
- package/src/panels/thinking-panel.ts +304 -0
- package/src/panels/token-budget-panel.ts +475 -0
- package/src/panels/tool-inspector-panel.ts +429 -0
- package/src/panels/types.ts +54 -0
- package/src/panels/watchers-panel.ts +193 -0
- package/src/panels/work-plan-panel.ts +175 -0
- package/src/panels/worktree-panel.ts +182 -0
- package/src/panels/wrfc-panel.ts +609 -0
- package/src/permissions/prompt.ts +165 -0
- package/src/planning/project-planning-coordinator.ts +543 -0
- package/src/plugins/loader.ts +15 -0
- package/src/renderer/agent-detail-modal.ts +331 -0
- package/src/renderer/agent-workspace.ts +238 -0
- package/src/renderer/ansi-sanitize.ts +76 -0
- package/src/renderer/autocomplete-overlay.ts +154 -0
- package/src/renderer/block-actions.ts +76 -0
- package/src/renderer/bookmark-modal.ts +101 -0
- package/src/renderer/bottom-bar.ts +58 -0
- package/src/renderer/buffer.ts +113 -0
- package/src/renderer/code-block.ts +373 -0
- package/src/renderer/compositor.ts +283 -0
- package/src/renderer/context-inspector.ts +219 -0
- package/src/renderer/conversation-layout.ts +67 -0
- package/src/renderer/conversation-overlays.ts +140 -0
- package/src/renderer/conversation-surface.ts +260 -0
- package/src/renderer/diff-view.ts +132 -0
- package/src/renderer/diff.ts +130 -0
- package/src/renderer/file-picker-overlay.ts +101 -0
- package/src/renderer/file-tree.ts +153 -0
- package/src/renderer/fullscreen-primitives.ts +130 -0
- package/src/renderer/fullscreen-workspace.ts +199 -0
- package/src/renderer/git-status.ts +89 -0
- package/src/renderer/help-overlay.ts +267 -0
- package/src/renderer/history-search-overlay.ts +73 -0
- package/src/renderer/layout-engine.ts +97 -0
- package/src/renderer/layout.ts +32 -0
- package/src/renderer/live-tail-modal.ts +156 -0
- package/src/renderer/markdown.ts +635 -0
- package/src/renderer/mcp-workspace.ts +237 -0
- package/src/renderer/modal-factory.ts +467 -0
- package/src/renderer/modal-utils.ts +24 -0
- package/src/renderer/model-picker-overlay.ts +473 -0
- package/src/renderer/model-workspace.ts +488 -0
- package/src/renderer/onboarding/onboarding-wizard.ts +615 -0
- package/src/renderer/overlay-box.ts +146 -0
- package/src/renderer/overlay-viewport.ts +104 -0
- package/src/renderer/panel-composite.ts +158 -0
- package/src/renderer/panel-picker-overlay.ts +202 -0
- package/src/renderer/panel-tab-bar.ts +69 -0
- package/src/renderer/panel-workspace-bar.ts +42 -0
- package/src/renderer/process-indicator.ts +96 -0
- package/src/renderer/process-modal.ts +656 -0
- package/src/renderer/process-summary.ts +67 -0
- package/src/renderer/profile-picker-modal.ts +129 -0
- package/src/renderer/progress.ts +98 -0
- package/src/renderer/qr-renderer.ts +120 -0
- package/src/renderer/search-overlay.ts +54 -0
- package/src/renderer/selection-modal-overlay.ts +214 -0
- package/src/renderer/semantic-diff.ts +369 -0
- package/src/renderer/session-picker-modal.ts +127 -0
- package/src/renderer/settings-modal-helpers.ts +193 -0
- package/src/renderer/settings-modal.ts +537 -0
- package/src/renderer/shell-surface.ts +88 -0
- package/src/renderer/status-glyphs.ts +21 -0
- package/src/renderer/status-token.ts +67 -0
- package/src/renderer/surface-layout.ts +101 -0
- package/src/renderer/syntax-highlighter.ts +542 -0
- package/src/renderer/system-message.ts +83 -0
- package/src/renderer/tab-strip.ts +108 -0
- package/src/renderer/text-layout.ts +31 -0
- package/src/renderer/thinking.ts +17 -0
- package/src/renderer/tool-call.ts +234 -0
- package/src/renderer/ui-factory.ts +524 -0
- package/src/renderer/ui-primitives.ts +96 -0
- package/src/runtime/bootstrap-command-context.ts +278 -0
- package/src/runtime/bootstrap-command-parts.ts +386 -0
- package/src/runtime/bootstrap-core.ts +540 -0
- package/src/runtime/bootstrap-hook-bridge.ts +112 -0
- package/src/runtime/bootstrap-shell.ts +283 -0
- package/src/runtime/bootstrap.ts +575 -0
- package/src/runtime/cloudflare-control-plane.ts +349 -0
- package/src/runtime/context.ts +142 -0
- package/src/runtime/diagnostics/panels/index.ts +24 -0
- package/src/runtime/diagnostics/panels/ops.ts +156 -0
- package/src/runtime/diagnostics/panels/panel-resources.ts +118 -0
- package/src/runtime/diagnostics/panels/policy.ts +177 -0
- package/src/runtime/index.ts +662 -0
- package/src/runtime/onboarding/apply.ts +642 -0
- package/src/runtime/onboarding/derivation.ts +534 -0
- package/src/runtime/onboarding/index.ts +7 -0
- package/src/runtime/onboarding/markers.ts +148 -0
- package/src/runtime/onboarding/snapshot.ts +406 -0
- package/src/runtime/onboarding/state.ts +141 -0
- package/src/runtime/onboarding/types.ts +404 -0
- package/src/runtime/onboarding/verify.ts +171 -0
- package/src/runtime/operator-token-cleanup.ts +27 -0
- package/src/runtime/perf/panel-contracts.ts +32 -0
- package/src/runtime/perf/panel-health-monitor.ts +18 -0
- package/src/runtime/sandbox-public-gaps.ts +358 -0
- package/src/runtime/services.ts +670 -0
- package/src/runtime/store/domains/domain-read-matrix.ts +15 -0
- package/src/runtime/store/domains/index.ts +222 -0
- package/src/runtime/store/domains/panels.ts +117 -0
- package/src/runtime/store/domains/ui-perf.ts +103 -0
- package/src/runtime/store/index.ts +305 -0
- package/src/runtime/store/selectors/index.ts +359 -0
- package/src/runtime/store/state.ts +145 -0
- package/src/runtime/surface-feature-flags.ts +65 -0
- package/src/runtime/terminal-output-guard.ts +228 -0
- package/src/runtime/ui/index.ts +39 -0
- package/src/runtime/ui/model-picker/data-provider.ts +182 -0
- package/src/runtime/ui/model-picker/health-enrichment.ts +228 -0
- package/src/runtime/ui/model-picker/index.ts +59 -0
- package/src/runtime/ui/model-picker/types.ts +149 -0
- package/src/runtime/ui/provider-health/data-provider.ts +244 -0
- package/src/runtime/ui/provider-health/fallback-visualizer.ts +71 -0
- package/src/runtime/ui/provider-health/index.ts +46 -0
- package/src/runtime/ui/provider-health/types.ts +146 -0
- package/src/runtime/ui-events.ts +1 -0
- package/src/runtime/ui-read-model-helpers.ts +1 -0
- package/src/runtime/ui-read-models-observability-maintenance.ts +1 -0
- package/src/runtime/ui-read-models-observability-options.ts +1 -0
- package/src/runtime/ui-read-models-observability-remote.ts +1 -0
- package/src/runtime/ui-read-models-observability-security.ts +1 -0
- package/src/runtime/ui-read-models-observability-system.ts +1 -0
- package/src/runtime/ui-read-models-observability.ts +1 -0
- package/src/runtime/ui-read-models.ts +61 -0
- package/src/runtime/ui-service-queries.ts +1 -0
- package/src/runtime/ui-services.ts +190 -0
- package/src/scripts/process-messages.ts +42 -0
- package/src/shell/blocking-input.ts +98 -0
- package/src/shell/service-settings-sync.ts +273 -0
- package/src/shell/ui-openers.ts +352 -0
- package/src/tools/index.ts +1 -0
- package/src/tools/wrfc-agent-guard.ts +49 -0
- package/src/types/grid.ts +48 -0
- package/src/types/sql-js.d.ts +15 -0
- package/src/utils/clipboard.ts +22 -0
- package/src/utils/splash-lines.ts +46 -0
- package/src/utils/terminal-width.ts +185 -0
- package/src/verification/live-verifier.ts +430 -0
- package/src/verification/verification-ledger.ts +242 -0
- package/src/version.ts +17 -0
- package/src/widget/index.ts +2 -0
- package/src/widget/types.ts +9 -0
- package/src/widget/widget.ts +8 -0
- package/src/work-plans/work-plan-store.ts +374 -0
- package/tsconfig.json +18 -0
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
export type ProviderStatus = 'online' | 'rate-limited' | 'error' | 'unknown';
|
|
2
|
+
|
|
3
|
+
export interface ProviderHealth {
|
|
4
|
+
name: string;
|
|
5
|
+
status: ProviderStatus;
|
|
6
|
+
lastLatencyMs?: number;
|
|
7
|
+
lastErrorMessage?: string;
|
|
8
|
+
lastSuccessAt?: number;
|
|
9
|
+
lastErrorAt?: number;
|
|
10
|
+
rateLimitExpiresAt: number;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Tracks provider request posture from shell-facing turn and provider events.
|
|
15
|
+
* The panel owns event subscriptions and feeds those events into this tracker.
|
|
16
|
+
*/
|
|
17
|
+
export class ProviderHealthTracker {
|
|
18
|
+
private records = new Map<string, ProviderHealth>();
|
|
19
|
+
private streamStartMs: number | null = null;
|
|
20
|
+
private turnStartMs: number | null = null;
|
|
21
|
+
|
|
22
|
+
private static readonly DEFAULT_COOLDOWN_MS = 60_000;
|
|
23
|
+
|
|
24
|
+
onTurnStart(): void {
|
|
25
|
+
this.turnStartMs = Date.now();
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
onStreamStart(): void {
|
|
29
|
+
this.streamStartMs = Date.now();
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
onLlmResponse(providerName: string): void {
|
|
33
|
+
const now = Date.now();
|
|
34
|
+
const latencyMs =
|
|
35
|
+
this.streamStartMs !== null
|
|
36
|
+
? now - this.streamStartMs
|
|
37
|
+
: this.turnStartMs !== null
|
|
38
|
+
? now - this.turnStartMs
|
|
39
|
+
: undefined;
|
|
40
|
+
this.streamStartMs = null;
|
|
41
|
+
|
|
42
|
+
this.recordSuccess(providerName, latencyMs);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
onTurnError(error: string, providerName = 'unknown'): void {
|
|
46
|
+
this.streamStartMs = null;
|
|
47
|
+
this.turnStartMs = null;
|
|
48
|
+
const isRateLimit = this.isRateLimitMessage(error);
|
|
49
|
+
|
|
50
|
+
this.recordError(providerName, error, isRateLimit);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
onProvidersChanged(providerIds: readonly string[]): void {
|
|
54
|
+
try {
|
|
55
|
+
for (const providerId of providerIds) {
|
|
56
|
+
if (!this.records.has(providerId)) {
|
|
57
|
+
this.ensureRecord(providerId);
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
} catch {
|
|
61
|
+
// Ignore provider catalog churn while the shell is refreshing.
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
getAll(): ProviderHealth[] {
|
|
66
|
+
return [...this.records.values()];
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
get(name: string): ProviderHealth | undefined {
|
|
70
|
+
return this.records.get(name);
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
private ensureRecord(name: string): ProviderHealth {
|
|
74
|
+
let record = this.records.get(name);
|
|
75
|
+
if (!record) {
|
|
76
|
+
record = { name, status: 'unknown', rateLimitExpiresAt: 0 };
|
|
77
|
+
this.records.set(name, record);
|
|
78
|
+
}
|
|
79
|
+
return record;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
private recordSuccess(name: string, latencyMs?: number): void {
|
|
83
|
+
const record = this.ensureRecord(name);
|
|
84
|
+
record.status = 'online';
|
|
85
|
+
record.lastSuccessAt = Date.now();
|
|
86
|
+
record.lastErrorMessage = undefined;
|
|
87
|
+
if (latencyMs !== undefined) {
|
|
88
|
+
record.lastLatencyMs = latencyMs;
|
|
89
|
+
}
|
|
90
|
+
if (record.rateLimitExpiresAt > 0 && record.rateLimitExpiresAt <= Date.now()) {
|
|
91
|
+
record.rateLimitExpiresAt = 0;
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
private recordError(name: string, message: string, isRateLimit: boolean): void {
|
|
96
|
+
const record = this.ensureRecord(name);
|
|
97
|
+
record.lastErrorAt = Date.now();
|
|
98
|
+
record.lastErrorMessage = message.slice(0, 120);
|
|
99
|
+
if (isRateLimit) {
|
|
100
|
+
record.status = 'rate-limited';
|
|
101
|
+
record.rateLimitExpiresAt = Date.now() + ProviderHealthTracker.DEFAULT_COOLDOWN_MS;
|
|
102
|
+
return;
|
|
103
|
+
}
|
|
104
|
+
record.status = 'error';
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
private isRateLimitMessage(message: string): boolean {
|
|
108
|
+
const lower = message.toLowerCase();
|
|
109
|
+
return (
|
|
110
|
+
lower.includes('429')
|
|
111
|
+
|| lower.includes('402')
|
|
112
|
+
|| /rate.limit|too many requests|quota exceeded|throttl|depleted|credits/.test(lower)
|
|
113
|
+
);
|
|
114
|
+
}
|
|
115
|
+
}
|
|
@@ -0,0 +1,366 @@
|
|
|
1
|
+
import { BasePanel } from './base-panel.ts';
|
|
2
|
+
import type { Line } from '../types/grid.ts';
|
|
3
|
+
import type { ProviderEvent, TurnEvent } from '@/runtime/index.ts';
|
|
4
|
+
import type { UiEventFeed } from '../runtime/ui-events.ts';
|
|
5
|
+
import type { UiProvidersSnapshot, UiReadModel } from '../runtime/ui-read-models.ts';
|
|
6
|
+
import {
|
|
7
|
+
buildEmptyState,
|
|
8
|
+
buildKeyValueLine,
|
|
9
|
+
buildStyledPanelLine,
|
|
10
|
+
buildPanelWorkspace,
|
|
11
|
+
DEFAULT_PANEL_PALETTE,
|
|
12
|
+
type PanelWorkspaceSection,
|
|
13
|
+
} from './polish.ts';
|
|
14
|
+
import { truncateDisplay } from '../utils/terminal-width.ts';
|
|
15
|
+
|
|
16
|
+
// ---------------------------------------------------------------------------
|
|
17
|
+
// Constants
|
|
18
|
+
// ---------------------------------------------------------------------------
|
|
19
|
+
|
|
20
|
+
const SPARKLINE_CHARS = '._-:=+*#';
|
|
21
|
+
const LATENCY_RING_SIZE = 20;
|
|
22
|
+
|
|
23
|
+
/** Latency thresholds in ms for color-coding. */
|
|
24
|
+
const LATENCY_GREEN = 500;
|
|
25
|
+
const LATENCY_YELLOW = 2000;
|
|
26
|
+
|
|
27
|
+
// ---------------------------------------------------------------------------
|
|
28
|
+
// Types
|
|
29
|
+
// ---------------------------------------------------------------------------
|
|
30
|
+
|
|
31
|
+
interface ProviderRecord {
|
|
32
|
+
/** Provider name (e.g. 'anthropic', 'openai'). */
|
|
33
|
+
name: string;
|
|
34
|
+
/** Currently active model ID for this provider (last seen). */
|
|
35
|
+
lastModelId: string;
|
|
36
|
+
/** Ring buffer of per-request latencies in ms (most-recent last). */
|
|
37
|
+
latencies: number[];
|
|
38
|
+
/** Total request count. */
|
|
39
|
+
requests: number;
|
|
40
|
+
/** Error count. */
|
|
41
|
+
errors: number;
|
|
42
|
+
/** Total input + output tokens summed across all requests. */
|
|
43
|
+
totalTokens: number;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
// ---------------------------------------------------------------------------
|
|
47
|
+
// ProviderStatsPanel
|
|
48
|
+
// ---------------------------------------------------------------------------
|
|
49
|
+
|
|
50
|
+
export class ProviderStatsPanel extends BasePanel {
|
|
51
|
+
/** Per-provider metrics keyed by provider name. */
|
|
52
|
+
private records: Map<string, ProviderRecord> = new Map();
|
|
53
|
+
|
|
54
|
+
/** Timestamp (ms) recorded at turn:start — used to compute turn latency. */
|
|
55
|
+
private _turnStartMs: number | null = null;
|
|
56
|
+
|
|
57
|
+
/** Timestamp for the current streaming LLM call start. */
|
|
58
|
+
private _streamStartMs: number | null = null;
|
|
59
|
+
|
|
60
|
+
/** Unsubscribe functions for event listeners. */
|
|
61
|
+
private _unsubs: Array<() => void> = [];
|
|
62
|
+
|
|
63
|
+
constructor(
|
|
64
|
+
private readonly turnEvents: UiEventFeed<TurnEvent>,
|
|
65
|
+
private readonly providerEvents: UiEventFeed<ProviderEvent>,
|
|
66
|
+
private readonly requestRender: () => void = () => {},
|
|
67
|
+
private readonly providers: UiReadModel<UiProvidersSnapshot>,
|
|
68
|
+
) {
|
|
69
|
+
super('providers', 'Providers', 'R', 'monitoring');
|
|
70
|
+
this._subscribe();
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
// -------------------------------------------------------------------------
|
|
74
|
+
// Event subscription
|
|
75
|
+
// -------------------------------------------------------------------------
|
|
76
|
+
|
|
77
|
+
private _subscribe(): void {
|
|
78
|
+
// Record when a turn starts so we can compute latency later
|
|
79
|
+
this._unsubs.push(
|
|
80
|
+
this.turnEvents.on('TURN_SUBMITTED', () => {
|
|
81
|
+
this._turnStartMs = Date.now();
|
|
82
|
+
}),
|
|
83
|
+
);
|
|
84
|
+
|
|
85
|
+
// Per-streaming-call timing (each iteration of the agentic loop)
|
|
86
|
+
this._unsubs.push(
|
|
87
|
+
this.turnEvents.on('STREAM_START', () => {
|
|
88
|
+
this._streamStartMs = Date.now();
|
|
89
|
+
}),
|
|
90
|
+
);
|
|
91
|
+
|
|
92
|
+
// After each LLM response (streamed or not), record metrics for the
|
|
93
|
+
// current provider call inside the turn loop.
|
|
94
|
+
this._unsubs.push(
|
|
95
|
+
this.turnEvents.on('LLM_RESPONSE_RECEIVED', (env) => {
|
|
96
|
+
const now = Date.now();
|
|
97
|
+
const latencyMs = this._streamStartMs !== null
|
|
98
|
+
? now - this._streamStartMs
|
|
99
|
+
: this._turnStartMs !== null
|
|
100
|
+
? now - this._turnStartMs
|
|
101
|
+
: 0;
|
|
102
|
+
// Reset stream start — ready for next iteration in the agentic loop
|
|
103
|
+
this._streamStartMs = null;
|
|
104
|
+
this._recordRequest(
|
|
105
|
+
env.provider,
|
|
106
|
+
env.model,
|
|
107
|
+
latencyMs,
|
|
108
|
+
false,
|
|
109
|
+
env.inputTokens
|
|
110
|
+
+ env.outputTokens
|
|
111
|
+
+ (env.cacheReadTokens ?? 0)
|
|
112
|
+
+ (env.cacheWriteTokens ?? 0),
|
|
113
|
+
);
|
|
114
|
+
|
|
115
|
+
this.markDirty();
|
|
116
|
+
this.requestRender();
|
|
117
|
+
}),
|
|
118
|
+
);
|
|
119
|
+
|
|
120
|
+
// On error, record a failed request
|
|
121
|
+
this._unsubs.push(
|
|
122
|
+
this.turnEvents.on('TURN_ERROR', () => {
|
|
123
|
+
this._turnStartMs = null;
|
|
124
|
+
this._streamStartMs = null;
|
|
125
|
+
this._recordRequest('unknown', 'unknown', 0, true, 0);
|
|
126
|
+
|
|
127
|
+
this.markDirty();
|
|
128
|
+
this.requestRender();
|
|
129
|
+
}),
|
|
130
|
+
);
|
|
131
|
+
|
|
132
|
+
// Re-render when providers change (new custom providers loaded)
|
|
133
|
+
this._unsubs.push(
|
|
134
|
+
this.providerEvents.on('PROVIDERS_CHANGED', () => {
|
|
135
|
+
this.markDirty();
|
|
136
|
+
this.requestRender();
|
|
137
|
+
}),
|
|
138
|
+
);
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
// -------------------------------------------------------------------------
|
|
142
|
+
// Metric recording
|
|
143
|
+
// -------------------------------------------------------------------------
|
|
144
|
+
|
|
145
|
+
private _recordRequest(
|
|
146
|
+
providerName: string,
|
|
147
|
+
modelId: string,
|
|
148
|
+
latencyMs: number,
|
|
149
|
+
isError: boolean,
|
|
150
|
+
tokens: number,
|
|
151
|
+
): void {
|
|
152
|
+
let rec = this.records.get(providerName);
|
|
153
|
+
if (!rec) {
|
|
154
|
+
rec = {
|
|
155
|
+
name: providerName,
|
|
156
|
+
lastModelId: modelId,
|
|
157
|
+
latencies: [],
|
|
158
|
+
requests: 0,
|
|
159
|
+
errors: 0,
|
|
160
|
+
totalTokens: 0,
|
|
161
|
+
};
|
|
162
|
+
this.records.set(providerName, rec);
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
rec.lastModelId = modelId;
|
|
166
|
+
rec.requests++;
|
|
167
|
+
if (isError) rec.errors++;
|
|
168
|
+
rec.totalTokens += tokens;
|
|
169
|
+
|
|
170
|
+
if (latencyMs > 0) {
|
|
171
|
+
rec.latencies.push(latencyMs);
|
|
172
|
+
// Keep only the most recent LATENCY_RING_SIZE samples
|
|
173
|
+
if (rec.latencies.length > LATENCY_RING_SIZE) {
|
|
174
|
+
rec.latencies.shift();
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
// -------------------------------------------------------------------------
|
|
180
|
+
// Lifecycle
|
|
181
|
+
// -------------------------------------------------------------------------
|
|
182
|
+
|
|
183
|
+
override onDestroy(): void {
|
|
184
|
+
for (const unsub of this._unsubs) unsub();
|
|
185
|
+
this._unsubs = [];
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
// -------------------------------------------------------------------------
|
|
189
|
+
// Rendering
|
|
190
|
+
// -------------------------------------------------------------------------
|
|
191
|
+
|
|
192
|
+
override render(width: number, height: number): Line[] {
|
|
193
|
+
const knownProviders = [...this.providers.getSnapshot().providerIds];
|
|
194
|
+
|
|
195
|
+
if (knownProviders.length === 0) {
|
|
196
|
+
return buildPanelWorkspace(width, height, {
|
|
197
|
+
title: ' Provider Stats',
|
|
198
|
+
intro: 'Per-provider request performance, latency distribution, error pressure, and session totals.',
|
|
199
|
+
sections: [
|
|
200
|
+
{
|
|
201
|
+
lines: buildEmptyState(
|
|
202
|
+
width,
|
|
203
|
+
' No providers registered',
|
|
204
|
+
'Load or configure a provider to begin collecting per-provider latency and error metrics.',
|
|
205
|
+
[],
|
|
206
|
+
DEFAULT_PANEL_PALETTE,
|
|
207
|
+
),
|
|
208
|
+
},
|
|
209
|
+
],
|
|
210
|
+
palette: DEFAULT_PANEL_PALETTE,
|
|
211
|
+
});
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
const totalReq = [...this.records.values()].reduce((sum, rec) => sum + rec.requests, 0);
|
|
215
|
+
const totalErr = [...this.records.values()].reduce((sum, rec) => sum + rec.errors, 0);
|
|
216
|
+
const totalTok = [...this.records.values()].reduce((sum, rec) => sum + rec.totalTokens, 0);
|
|
217
|
+
const allLatencies = [...this.records.values()].flatMap((rec) => rec.latencies);
|
|
218
|
+
const providerSections: PanelWorkspaceSection[] = [
|
|
219
|
+
{
|
|
220
|
+
title: 'Session',
|
|
221
|
+
lines: [
|
|
222
|
+
buildKeyValueLine(width, [
|
|
223
|
+
{ label: 'Providers', value: String(knownProviders.length) },
|
|
224
|
+
{ label: 'Requests', value: String(totalReq), valueColor: DEFAULT_PANEL_PALETTE.info },
|
|
225
|
+
{ label: 'Errors', value: String(totalErr), valueColor: totalErr > 0 ? DEFAULT_PANEL_PALETTE.bad : DEFAULT_PANEL_PALETTE.good },
|
|
226
|
+
{ label: 'Tokens', value: String(totalTok), valueColor: DEFAULT_PANEL_PALETTE.value },
|
|
227
|
+
], DEFAULT_PANEL_PALETTE),
|
|
228
|
+
buildKeyValueLine(width, [
|
|
229
|
+
{ label: 'Avg Latency', value: this._fmtMs(this._avg(allLatencies)), valueColor: this._latencyColor(this._avg(allLatencies)) },
|
|
230
|
+
{ label: 'P95', value: this._fmtMs(this._p95(allLatencies)), valueColor: DEFAULT_PANEL_PALETTE.warn },
|
|
231
|
+
], DEFAULT_PANEL_PALETTE),
|
|
232
|
+
],
|
|
233
|
+
},
|
|
234
|
+
];
|
|
235
|
+
|
|
236
|
+
for (const provName of knownProviders) {
|
|
237
|
+
const rec = this.records.get(provName);
|
|
238
|
+
providerSections.push({
|
|
239
|
+
title: provName,
|
|
240
|
+
lines: this._buildProviderRows(provName, rec, width),
|
|
241
|
+
});
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
return buildPanelWorkspace(width, height, {
|
|
245
|
+
title: ' Provider Stats',
|
|
246
|
+
intro: 'Per-provider request performance, latency distribution, error pressure, and session totals.',
|
|
247
|
+
sections: providerSections,
|
|
248
|
+
palette: DEFAULT_PANEL_PALETTE,
|
|
249
|
+
});
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
// -------------------------------------------------------------------------
|
|
253
|
+
// Line builders
|
|
254
|
+
// -------------------------------------------------------------------------
|
|
255
|
+
|
|
256
|
+
private _buildProviderRows(
|
|
257
|
+
provName: string,
|
|
258
|
+
rec: ProviderRecord | undefined,
|
|
259
|
+
width: number,
|
|
260
|
+
): Line[] {
|
|
261
|
+
const rows: Line[] = [];
|
|
262
|
+
|
|
263
|
+
// Determine health
|
|
264
|
+
const hasErrors = rec !== undefined && rec.errors > 0;
|
|
265
|
+
const dotColor = hasErrors ? '#ef4444' : '#22c55e';
|
|
266
|
+
|
|
267
|
+
// Model ID (truncated)
|
|
268
|
+
const modelId = rec?.lastModelId ?? 'n/a';
|
|
269
|
+
const modelDisplay = truncateDisplay(modelId, 30);
|
|
270
|
+
|
|
271
|
+
// Header row: * provider model
|
|
272
|
+
// Build as segments to avoid multi-byte char indexing issues
|
|
273
|
+
const headerLine = buildStyledPanelLine(width, [
|
|
274
|
+
{ text: ' ', fg: '#94a3b8' },
|
|
275
|
+
{ text: '●', fg: dotColor },
|
|
276
|
+
{ text: ' ', fg: '#94a3b8' },
|
|
277
|
+
{ text: `${truncateDisplay(provName, 14).padEnd(14)} `, fg: '#e2e8f0', bold: true },
|
|
278
|
+
{ text: modelDisplay, fg: '#cbd5e1' },
|
|
279
|
+
]);
|
|
280
|
+
|
|
281
|
+
rows.push(headerLine);
|
|
282
|
+
|
|
283
|
+
if (rec === undefined || rec.requests === 0) {
|
|
284
|
+
rows.push(buildStyledPanelLine(width, [
|
|
285
|
+
{ text: ' No requests yet.', fg: '#6b7280' },
|
|
286
|
+
]));
|
|
287
|
+
} else {
|
|
288
|
+
const avgLatency = this._avg(rec.latencies);
|
|
289
|
+
const p95Latency = this._p95(rec.latencies);
|
|
290
|
+
const errRate = rec.requests > 0 ? (rec.errors / rec.requests) * 100 : 0;
|
|
291
|
+
const sparkline = this._sparkline(rec.latencies);
|
|
292
|
+
|
|
293
|
+
const latFg = avgLatency < LATENCY_GREEN
|
|
294
|
+
? '#22c55e'
|
|
295
|
+
: avgLatency < LATENCY_YELLOW
|
|
296
|
+
? '#eab308'
|
|
297
|
+
: '#ef4444';
|
|
298
|
+
|
|
299
|
+
const segments = [
|
|
300
|
+
{ text: ' avg ', fg: '#6b7280' },
|
|
301
|
+
{ text: this._fmtMs(avgLatency).padStart(6), fg: latFg, bold: true },
|
|
302
|
+
{ text: ' p95 ', fg: '#6b7280' },
|
|
303
|
+
{ text: this._fmtMs(p95Latency).padStart(6), fg: '#a78bfa' },
|
|
304
|
+
{ text: ' ', fg: '#374151' },
|
|
305
|
+
{ text: sparkline, fg: latFg },
|
|
306
|
+
{ text: ' err ', fg: '#6b7280' },
|
|
307
|
+
{ text: `${errRate.toFixed(0).padStart(3)}%`, fg: errRate > 0 ? '#ef4444' : '#22c55e' },
|
|
308
|
+
{ text: ` ${rec.requests.toString().padStart(4)}r`, fg: '#94a3b8' },
|
|
309
|
+
] as const;
|
|
310
|
+
const tokenSegment = rec.totalTokens > 0
|
|
311
|
+
? [{ text: ` ${rec.totalTokens.toString().padStart(6)}tok`, fg: '#64748b' }]
|
|
312
|
+
: [];
|
|
313
|
+
rows.push(buildStyledPanelLine(width, [...segments, ...tokenSegment]));
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
return rows;
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
// -------------------------------------------------------------------------
|
|
320
|
+
// Utilities
|
|
321
|
+
// -------------------------------------------------------------------------
|
|
322
|
+
|
|
323
|
+
private _avg(arr: number[]): number {
|
|
324
|
+
if (arr.length === 0) return 0;
|
|
325
|
+
return arr.reduce((s, v) => s + v, 0) / arr.length;
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
private _p95(arr: number[]): number {
|
|
329
|
+
if (arr.length === 0) return 0;
|
|
330
|
+
const sorted = [...arr].sort((a, b) => a - b);
|
|
331
|
+
const idx = Math.floor(sorted.length * 0.95);
|
|
332
|
+
return sorted[Math.min(idx, sorted.length - 1)] ?? 0;
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
private _sparkline(latencies: number[]): string {
|
|
336
|
+
if (latencies.length === 0) return ' '.repeat(LATENCY_RING_SIZE);
|
|
337
|
+
const vals = latencies.slice(-LATENCY_RING_SIZE);
|
|
338
|
+
const minV = Math.min(...vals);
|
|
339
|
+
const maxV = Math.max(...vals);
|
|
340
|
+
const range = maxV - minV || 1;
|
|
341
|
+
const spark: string[] = vals.map((v) => {
|
|
342
|
+
const idx = Math.min(
|
|
343
|
+
SPARKLINE_CHARS.length - 1,
|
|
344
|
+
Math.floor(((v - minV) / range) * (SPARKLINE_CHARS.length - 1)),
|
|
345
|
+
);
|
|
346
|
+
return SPARKLINE_CHARS[idx] ?? '.';
|
|
347
|
+
});
|
|
348
|
+
// Pad left to always be LATENCY_RING_SIZE wide
|
|
349
|
+
while (spark.length < LATENCY_RING_SIZE) spark.unshift(' ');
|
|
350
|
+
return spark.join('');
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
private _fmtMs(ms: number): string {
|
|
354
|
+
if (ms <= 0) return 'n/a';
|
|
355
|
+
if (ms >= 10000) return `${(ms / 1000).toFixed(1)}s`;
|
|
356
|
+
if (ms >= 1000) return `${(ms / 1000).toFixed(2)}s`;
|
|
357
|
+
return `${Math.round(ms)}ms`;
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
private _latencyColor(ms: number): string {
|
|
361
|
+
if (ms <= 0) return DEFAULT_PANEL_PALETTE.dim;
|
|
362
|
+
if (ms < LATENCY_GREEN) return DEFAULT_PANEL_PALETTE.good;
|
|
363
|
+
if (ms < LATENCY_YELLOW) return DEFAULT_PANEL_PALETTE.warn;
|
|
364
|
+
return DEFAULT_PANEL_PALETTE.bad;
|
|
365
|
+
}
|
|
366
|
+
}
|
|
@@ -0,0 +1,182 @@
|
|
|
1
|
+
import type { Line } from '../types/grid.ts';
|
|
2
|
+
import { createEmptyLine } from '../types/grid.ts';
|
|
3
|
+
import { BasePanel } from './base-panel.ts';
|
|
4
|
+
import {
|
|
5
|
+
buildPanelLine,
|
|
6
|
+
DEFAULT_PANEL_PALETTE,
|
|
7
|
+
} from './polish.ts';
|
|
8
|
+
import { renderQrMatrix, generateQrMatrix } from '../renderer/qr-renderer.ts';
|
|
9
|
+
import { encodeConnectionPayload } from '@pellux/goodvibes-sdk/platform/pairing';
|
|
10
|
+
|
|
11
|
+
const C = {
|
|
12
|
+
...DEFAULT_PANEL_PALETTE,
|
|
13
|
+
url: '#38bdf8',
|
|
14
|
+
token: '#a78bfa',
|
|
15
|
+
hint: '#64748b',
|
|
16
|
+
qrFg: '#000000',
|
|
17
|
+
qrBg: '#ffffff',
|
|
18
|
+
} as const;
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Connection info passed to the QR panel.
|
|
22
|
+
* Populated at construction; updated when the token is regenerated.
|
|
23
|
+
*/
|
|
24
|
+
export interface QrPanelConnectionInfo {
|
|
25
|
+
/** Full connection URL (e.g. http://192.168.1.x:3141) */
|
|
26
|
+
readonly url: string;
|
|
27
|
+
/** Auth token */
|
|
28
|
+
readonly token: string;
|
|
29
|
+
/** Username associated with the companion session */
|
|
30
|
+
readonly username: string;
|
|
31
|
+
/** Bootstrap password for companion authentication */
|
|
32
|
+
readonly password?: string;
|
|
33
|
+
/** SDK/surface version (defaults to '0.0.0' if omitted) */
|
|
34
|
+
readonly version?: string;
|
|
35
|
+
/** Surface identifier (defaults to 'tui' if omitted) */
|
|
36
|
+
readonly surface?: string;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Callback used by the panel to regenerate the companion token.
|
|
41
|
+
* Returns updated connection info.
|
|
42
|
+
*/
|
|
43
|
+
export type RegenerateTokenFn = () => QrPanelConnectionInfo;
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Callback used by the panel to copy text to the clipboard.
|
|
47
|
+
*/
|
|
48
|
+
export type CopyToClipboardFn = (text: string) => void;
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* QrPanel - displays a QR code for companion app pairing.
|
|
52
|
+
*
|
|
53
|
+
* Shows connection URL, truncated token, and username above the QR code.
|
|
54
|
+
* Supports `r` to regenerate the token and `c` to copy the token.
|
|
55
|
+
*
|
|
56
|
+
* QR matrix generation uses the SDK's `generateQrMatrix` via `encodeConnectionPayload`.
|
|
57
|
+
*/
|
|
58
|
+
export class QrPanel extends BasePanel {
|
|
59
|
+
private connectionInfo: QrPanelConnectionInfo;
|
|
60
|
+
private readonly regenerateToken: RegenerateTokenFn | undefined;
|
|
61
|
+
private readonly copyToClipboard: CopyToClipboardFn | undefined;
|
|
62
|
+
private lastStatus = '';
|
|
63
|
+
|
|
64
|
+
public constructor(
|
|
65
|
+
connectionInfo: QrPanelConnectionInfo,
|
|
66
|
+
regenerateToken?: RegenerateTokenFn,
|
|
67
|
+
copyToClipboard?: CopyToClipboardFn,
|
|
68
|
+
) {
|
|
69
|
+
super('qr-code', 'QR Code', 'Q', 'session');
|
|
70
|
+
this.connectionInfo = connectionInfo;
|
|
71
|
+
this.regenerateToken = regenerateToken;
|
|
72
|
+
this.copyToClipboard = copyToClipboard;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
public handleInput(key: string): boolean {
|
|
76
|
+
if (key === 'r') {
|
|
77
|
+
if (this.regenerateToken) {
|
|
78
|
+
this.connectionInfo = this.regenerateToken();
|
|
79
|
+
this.lastStatus = 'Token regenerated.';
|
|
80
|
+
} else {
|
|
81
|
+
this.lastStatus = 'Regeneration not available.';
|
|
82
|
+
}
|
|
83
|
+
this.markDirty();
|
|
84
|
+
return true;
|
|
85
|
+
}
|
|
86
|
+
if (key === 'c') {
|
|
87
|
+
if (this.copyToClipboard) {
|
|
88
|
+
this.copyToClipboard(this.connectionInfo.token);
|
|
89
|
+
this.lastStatus = 'Token copied to clipboard.';
|
|
90
|
+
} else {
|
|
91
|
+
this.lastStatus = 'Clipboard not available.';
|
|
92
|
+
}
|
|
93
|
+
this.markDirty();
|
|
94
|
+
return true;
|
|
95
|
+
}
|
|
96
|
+
return false;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
public render(width: number, height: number): Line[] {
|
|
100
|
+
this.needsRender = false;
|
|
101
|
+
const lines: Line[] = [];
|
|
102
|
+
|
|
103
|
+
const { url, token, username, password } = this.connectionInfo;
|
|
104
|
+
|
|
105
|
+
// ── Connection info header ─────────────────────────────────────────────
|
|
106
|
+
lines.push(createEmptyLine(width));
|
|
107
|
+
lines.push(
|
|
108
|
+
buildPanelLine(width, [
|
|
109
|
+
[' URL ', C.label],
|
|
110
|
+
[url.slice(0, Math.max(0, width - 12)), C.url],
|
|
111
|
+
]),
|
|
112
|
+
);
|
|
113
|
+
lines.push(
|
|
114
|
+
buildPanelLine(width, [
|
|
115
|
+
[' Token ', C.label],
|
|
116
|
+
[token, C.token],
|
|
117
|
+
]),
|
|
118
|
+
);
|
|
119
|
+
lines.push(
|
|
120
|
+
buildPanelLine(width, [
|
|
121
|
+
[' Username ', C.label],
|
|
122
|
+
[username.slice(0, Math.max(0, width - 12)), C.value],
|
|
123
|
+
]),
|
|
124
|
+
);
|
|
125
|
+
if (password !== undefined) {
|
|
126
|
+
lines.push(
|
|
127
|
+
buildPanelLine(width, [
|
|
128
|
+
[' Password ', C.label],
|
|
129
|
+
[password, C.value],
|
|
130
|
+
]),
|
|
131
|
+
);
|
|
132
|
+
}
|
|
133
|
+
lines.push(createEmptyLine(width));
|
|
134
|
+
|
|
135
|
+
// ── QR code ────────────────────────────────────────────────────────────
|
|
136
|
+
const payload = encodeConnectionPayload({
|
|
137
|
+
url: this.connectionInfo.url,
|
|
138
|
+
token: this.connectionInfo.token,
|
|
139
|
+
username: this.connectionInfo.username,
|
|
140
|
+
...(this.connectionInfo.password !== undefined ? { password: this.connectionInfo.password } : {}),
|
|
141
|
+
version: this.connectionInfo.version ?? '0.0.0',
|
|
142
|
+
surface: this.connectionInfo.surface ?? 'tui',
|
|
143
|
+
});
|
|
144
|
+
const matrix = generateQrMatrix(payload);
|
|
145
|
+
const qrLines = renderQrMatrix(matrix.modules, width, { fg: C.qrFg, bg: C.qrBg });
|
|
146
|
+
for (const qrLine of qrLines) {
|
|
147
|
+
lines.push(qrLine);
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
lines.push(createEmptyLine(width));
|
|
151
|
+
|
|
152
|
+
// ── Status message (ephemeral) ─────────────────────────────────────────
|
|
153
|
+
if (this.lastStatus) {
|
|
154
|
+
lines.push(
|
|
155
|
+
buildPanelLine(width, [
|
|
156
|
+
[` ${this.lastStatus} `, C.hint],
|
|
157
|
+
]),
|
|
158
|
+
);
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
// ── Hints ──────────────────────────────────────────────────────────────
|
|
162
|
+
const hintsLine = buildPanelLine(width, [
|
|
163
|
+
[' r ', C.hint],
|
|
164
|
+
['regenerate ', C.dim],
|
|
165
|
+
[' c ', C.hint],
|
|
166
|
+
['copy token', C.dim],
|
|
167
|
+
]);
|
|
168
|
+
|
|
169
|
+
// Push hints at the bottom if we have room, otherwise append after QR
|
|
170
|
+
const remaining = height - lines.length;
|
|
171
|
+
if (remaining > 2) {
|
|
172
|
+
// Fill with empty lines to push hints toward bottom
|
|
173
|
+
const fillCount = Math.max(0, remaining - 2);
|
|
174
|
+
for (let i = 0; i < fillCount; i++) {
|
|
175
|
+
lines.push(createEmptyLine(width));
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
lines.push(hintsLine);
|
|
179
|
+
|
|
180
|
+
return lines;
|
|
181
|
+
}
|
|
182
|
+
}
|