@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,793 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* SettingsModal — state management for the /settings and /config fullscreen workspace.
|
|
3
|
+
*
|
|
4
|
+
* Loads CONFIG_SCHEMA, groups settings by category, and tracks UI state:
|
|
5
|
+
* - Active category (Tab to cycle)
|
|
6
|
+
* - Selected setting index within category (↑↓)
|
|
7
|
+
* - Editing mode for inline string/number input
|
|
8
|
+
* - Feature flags tab with runtime toggle support
|
|
9
|
+
*
|
|
10
|
+
* Saves changes via configManager.set(key, value) or featureFlagManager methods.
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
import { CONFIG_SCHEMA, type ConfigKey, type PersistedFlagState } from '@pellux/goodvibes-sdk/platform/config';
|
|
14
|
+
import type { ModelPickerTarget } from './model-picker.ts';
|
|
15
|
+
import type { ConfigManager } from '@pellux/goodvibes-sdk/platform/config';
|
|
16
|
+
import type { SubscriptionManager } from '@pellux/goodvibes-sdk/platform/config';
|
|
17
|
+
import { getResolvedSettingLookup } from '@/runtime/index.ts';
|
|
18
|
+
import type { ServiceInspectionQuery } from '../runtime/ui-service-queries.ts';
|
|
19
|
+
import { buildGoodVibesSecretKey, isSecretConfigKey } from '../config/secret-config.ts';
|
|
20
|
+
import {
|
|
21
|
+
getNumericAdjustmentMeta,
|
|
22
|
+
modelPickerLaunchForKey,
|
|
23
|
+
roundToPrecision,
|
|
24
|
+
} from './settings-modal-behavior.ts';
|
|
25
|
+
import {
|
|
26
|
+
setSecretBackedSettingValue,
|
|
27
|
+
type SettingsSecretsManager,
|
|
28
|
+
} from './settings-modal-secrets.ts';
|
|
29
|
+
import { buildSubscriptionEntries } from './settings-modal-subscriptions.ts';
|
|
30
|
+
import type { FeatureFlagManager } from '@/runtime/index.ts';
|
|
31
|
+
import type { FeatureFlag, FlagState } from '@/runtime/index.ts';
|
|
32
|
+
import type { McpRegistry } from '@pellux/goodvibes-sdk/platform/mcp';
|
|
33
|
+
import { logger } from '@pellux/goodvibes-sdk/platform/utils';
|
|
34
|
+
import { summarizeError } from '@pellux/goodvibes-sdk/platform/utils';
|
|
35
|
+
import {
|
|
36
|
+
SETTINGS_CATEGORIES,
|
|
37
|
+
SETTINGS_CATEGORY_GROUPS,
|
|
38
|
+
type FlagEntry,
|
|
39
|
+
type McpEntry,
|
|
40
|
+
type SettingEntry,
|
|
41
|
+
type SettingsCategory,
|
|
42
|
+
type SettingsFocusPane,
|
|
43
|
+
type SubscriptionEntry,
|
|
44
|
+
} from './settings-modal-types.ts';
|
|
45
|
+
|
|
46
|
+
export interface SettingsModalChange {
|
|
47
|
+
readonly key: ConfigKey;
|
|
48
|
+
readonly previousValue: unknown;
|
|
49
|
+
readonly value: unknown;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export interface SettingsModalChangeResult {
|
|
53
|
+
readonly message?: string;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
export type SettingsModalChangeHandler = (change: SettingsModalChange) => SettingsModalChangeResult | void;
|
|
57
|
+
|
|
58
|
+
export interface SettingsModalOpenOptions {
|
|
59
|
+
readonly onSettingApplied?: SettingsModalChangeHandler;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
export {
|
|
63
|
+
SETTINGS_CATEGORIES,
|
|
64
|
+
SETTINGS_CATEGORY_GROUPS,
|
|
65
|
+
type FlagEntry,
|
|
66
|
+
type McpEntry,
|
|
67
|
+
type SettingEntry,
|
|
68
|
+
type SettingsCategory,
|
|
69
|
+
type SettingsFocusPane,
|
|
70
|
+
type SubscriptionEntry,
|
|
71
|
+
} from './settings-modal-types.ts';
|
|
72
|
+
|
|
73
|
+
// ---------------------------------------------------------------------------
|
|
74
|
+
// SettingsModal
|
|
75
|
+
// ---------------------------------------------------------------------------
|
|
76
|
+
|
|
77
|
+
export class SettingsModal {
|
|
78
|
+
public active = false;
|
|
79
|
+
|
|
80
|
+
/** Index into SETTINGS_CATEGORIES. */
|
|
81
|
+
public categoryIndex = 0;
|
|
82
|
+
|
|
83
|
+
/** Selected setting index within the current category. */
|
|
84
|
+
public selectedIndex = 0;
|
|
85
|
+
|
|
86
|
+
/** Which pane receives up/down navigation and Enter/Space actions. */
|
|
87
|
+
public focusPane: SettingsFocusPane = 'settings';
|
|
88
|
+
|
|
89
|
+
/** Whether we're in inline edit mode for the selected string/number setting. */
|
|
90
|
+
public editingMode = false;
|
|
91
|
+
|
|
92
|
+
/** Current value of the inline edit buffer. */
|
|
93
|
+
public editBuffer = '';
|
|
94
|
+
/** Server awaiting explicit allow-all confirmation, if any. */
|
|
95
|
+
public mcpAllowAllConfirmationTarget: string | null = null;
|
|
96
|
+
/**
|
|
97
|
+
* Set by activateSelected() when the highlighted setting should open the
|
|
98
|
+
* model picker rather than entering inline text edit mode.
|
|
99
|
+
* Consumed and cleared by the route handler after each Enter/Space action.
|
|
100
|
+
*/
|
|
101
|
+
public pendingModelPickerTarget: ModelPickerTarget | null = null;
|
|
102
|
+
/** Set when the highlighted setting should open provider selection before model selection. */
|
|
103
|
+
public pendingProviderModelPickerTarget: ModelPickerTarget | null = null;
|
|
104
|
+
/** Set when a highlighted setting needs an external picker owned by the shell route. */
|
|
105
|
+
public pendingSettingsPickerAction: 'tts-provider' | 'tts-voice' | null = null;
|
|
106
|
+
/** Provider awaiting explicit logout confirmation, if any. */
|
|
107
|
+
public subscriptionLogoutConfirmationTarget: string | null = null;
|
|
108
|
+
|
|
109
|
+
/** Settings grouped by category. */
|
|
110
|
+
public groups: Map<SettingsCategory, SettingEntry[]> = new Map();
|
|
111
|
+
|
|
112
|
+
/** Feature flag entries (populated when flags tab is active). */
|
|
113
|
+
public flagEntries: FlagEntry[] = [];
|
|
114
|
+
/** MCP server trust entries (populated when mcp tab is active). */
|
|
115
|
+
public mcpEntries: McpEntry[] = [];
|
|
116
|
+
/** Provider subscription entries (populated when subscriptions tab is active). */
|
|
117
|
+
public subscriptionEntries: SubscriptionEntry[] = [];
|
|
118
|
+
|
|
119
|
+
/**
|
|
120
|
+
* Set after a network-category save that touches controlPlane or httpListener
|
|
121
|
+
* config keys. Renderer reads this to display a transient restart notice.
|
|
122
|
+
* Cleared on next open() or close().
|
|
123
|
+
*/
|
|
124
|
+
public lastSaveTriggeredRestart: 'control-plane' | 'http-listener' | 'web' | null = null;
|
|
125
|
+
public lastSettingEffectMessage: string | null = null;
|
|
126
|
+
|
|
127
|
+
private configManager: ConfigManager | null = null;
|
|
128
|
+
private secretsManager: SettingsSecretsManager | null = null;
|
|
129
|
+
private featureFlagManager: FeatureFlagManager | null = null;
|
|
130
|
+
private mcpRegistry: McpRegistry | null = null;
|
|
131
|
+
private subscriptionManager: SubscriptionManager | null = null;
|
|
132
|
+
private serviceRegistry: Pick<ServiceInspectionQuery, 'getAll'> | null = null;
|
|
133
|
+
private onSettingApplied: SettingsModalChangeHandler | null = null;
|
|
134
|
+
|
|
135
|
+
/**
|
|
136
|
+
* Open the modal, loading current config values from configManager.
|
|
137
|
+
*
|
|
138
|
+
* @param configManager - Config manager instance for reading/writing settings.
|
|
139
|
+
* @param featureFlagManager - Feature flag manager for the flags tab.
|
|
140
|
+
*/
|
|
141
|
+
open(
|
|
142
|
+
configManager: ConfigManager,
|
|
143
|
+
featureFlagManager: FeatureFlagManager,
|
|
144
|
+
subscriptionManager: SubscriptionManager,
|
|
145
|
+
serviceRegistry: Pick<ServiceInspectionQuery, 'getAll'>,
|
|
146
|
+
mcpRegistry?: McpRegistry,
|
|
147
|
+
secretsManager?: SettingsSecretsManager,
|
|
148
|
+
options?: SettingsModalOpenOptions,
|
|
149
|
+
): void {
|
|
150
|
+
this.configManager = configManager;
|
|
151
|
+
this.secretsManager = secretsManager ?? null;
|
|
152
|
+
this.featureFlagManager = featureFlagManager;
|
|
153
|
+
this.subscriptionManager = subscriptionManager;
|
|
154
|
+
this.serviceRegistry = serviceRegistry;
|
|
155
|
+
this.mcpRegistry = mcpRegistry ?? null;
|
|
156
|
+
this.onSettingApplied = options?.onSettingApplied ?? null;
|
|
157
|
+
this._loadGroups(configManager);
|
|
158
|
+
this._loadFlagEntries();
|
|
159
|
+
this._loadMcpEntries();
|
|
160
|
+
this._loadSubscriptionEntries();
|
|
161
|
+
this.categoryIndex = 0;
|
|
162
|
+
this.selectedIndex = 0;
|
|
163
|
+
this.focusPane = 'categories';
|
|
164
|
+
this.editingMode = false;
|
|
165
|
+
this.editBuffer = '';
|
|
166
|
+
this.pendingModelPickerTarget = null;
|
|
167
|
+
this.pendingProviderModelPickerTarget = null;
|
|
168
|
+
this.pendingSettingsPickerAction = null;
|
|
169
|
+
this.mcpAllowAllConfirmationTarget = null;
|
|
170
|
+
this.subscriptionLogoutConfirmationTarget = null;
|
|
171
|
+
this.lastSaveTriggeredRestart = null;
|
|
172
|
+
this.lastSettingEffectMessage = null;
|
|
173
|
+
this.active = true;
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
close(): void {
|
|
177
|
+
this.active = false;
|
|
178
|
+
this.editingMode = false;
|
|
179
|
+
this.editBuffer = '';
|
|
180
|
+
this.pendingModelPickerTarget = null;
|
|
181
|
+
this.pendingProviderModelPickerTarget = null;
|
|
182
|
+
this.pendingSettingsPickerAction = null;
|
|
183
|
+
this.mcpAllowAllConfirmationTarget = null;
|
|
184
|
+
this.subscriptionLogoutConfirmationTarget = null;
|
|
185
|
+
this.lastSaveTriggeredRestart = null;
|
|
186
|
+
this.lastSettingEffectMessage = null;
|
|
187
|
+
this.serviceRegistry = null;
|
|
188
|
+
this.secretsManager = null;
|
|
189
|
+
this.onSettingApplied = null;
|
|
190
|
+
this.focusPane = 'settings';
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
/** Cycle to the next category (Tab). */
|
|
194
|
+
nextCategory(): void {
|
|
195
|
+
if (this.editingMode) return;
|
|
196
|
+
this.categoryIndex = (this.categoryIndex + 1) % SETTINGS_CATEGORIES.length;
|
|
197
|
+
this.selectedIndex = 0;
|
|
198
|
+
this.subscriptionLogoutConfirmationTarget = null;
|
|
199
|
+
if (this.currentCategory === 'flags') {
|
|
200
|
+
this._loadFlagEntries();
|
|
201
|
+
} else if (this.currentCategory === 'mcp') {
|
|
202
|
+
this._loadMcpEntries();
|
|
203
|
+
} else if (this.currentCategory === 'subscriptions') {
|
|
204
|
+
this._loadSubscriptionEntries();
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
/** Cycle to the previous category (Shift+Tab). */
|
|
209
|
+
prevCategory(): void {
|
|
210
|
+
if (this.editingMode) return;
|
|
211
|
+
this.categoryIndex = (this.categoryIndex - 1 + SETTINGS_CATEGORIES.length) % SETTINGS_CATEGORIES.length;
|
|
212
|
+
this.selectedIndex = 0;
|
|
213
|
+
this.subscriptionLogoutConfirmationTarget = null;
|
|
214
|
+
if (this.currentCategory === 'flags') {
|
|
215
|
+
this._loadFlagEntries();
|
|
216
|
+
} else if (this.currentCategory === 'mcp') {
|
|
217
|
+
this._loadMcpEntries();
|
|
218
|
+
} else if (this.currentCategory === 'subscriptions') {
|
|
219
|
+
this._loadSubscriptionEntries();
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
focusCategories(): void {
|
|
224
|
+
if (this.editingMode) return;
|
|
225
|
+
this.focusPane = 'categories';
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
focusSettings(): void {
|
|
229
|
+
if (this.editingMode) return;
|
|
230
|
+
this.focusPane = 'settings';
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
toggleFocusPane(): void {
|
|
234
|
+
if (this.editingMode) return;
|
|
235
|
+
this.focusPane = this.focusPane === 'settings' ? 'categories' : 'settings';
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
moveFocusedUp(): void {
|
|
239
|
+
if (this.focusPane === 'categories') this.prevCategory();
|
|
240
|
+
else this.moveUp();
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
moveFocusedDown(): void {
|
|
244
|
+
if (this.focusPane === 'categories') this.nextCategory();
|
|
245
|
+
else this.moveDown();
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
moveUp(): void {
|
|
249
|
+
if (this.editingMode) return;
|
|
250
|
+
const items = this._currentItems();
|
|
251
|
+
if (items.length === 0) {
|
|
252
|
+
if (this.currentCategory === 'flags' && this.flagEntries.length > 0) {
|
|
253
|
+
this.selectedIndex = (this.selectedIndex - 1 + this.flagEntries.length) % this.flagEntries.length;
|
|
254
|
+
} else if (this.currentCategory === 'mcp' && this.mcpEntries.length > 0) {
|
|
255
|
+
this.selectedIndex = (this.selectedIndex - 1 + this.mcpEntries.length) % this.mcpEntries.length;
|
|
256
|
+
} else if (this.currentCategory === 'subscriptions' && this.subscriptionEntries.length > 0) {
|
|
257
|
+
this.selectedIndex = (this.selectedIndex - 1 + this.subscriptionEntries.length) % this.subscriptionEntries.length;
|
|
258
|
+
this.subscriptionLogoutConfirmationTarget = null;
|
|
259
|
+
}
|
|
260
|
+
return;
|
|
261
|
+
}
|
|
262
|
+
this.selectedIndex = (this.selectedIndex - 1 + items.length) % items.length;
|
|
263
|
+
this.subscriptionLogoutConfirmationTarget = null;
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
moveDown(): void {
|
|
267
|
+
if (this.editingMode) return;
|
|
268
|
+
const items = this._currentItems();
|
|
269
|
+
if (items.length === 0) {
|
|
270
|
+
if (this.currentCategory === 'flags' && this.flagEntries.length > 0) {
|
|
271
|
+
this.selectedIndex = (this.selectedIndex + 1) % this.flagEntries.length;
|
|
272
|
+
} else if (this.currentCategory === 'mcp' && this.mcpEntries.length > 0) {
|
|
273
|
+
this.selectedIndex = (this.selectedIndex + 1) % this.mcpEntries.length;
|
|
274
|
+
} else if (this.currentCategory === 'subscriptions' && this.subscriptionEntries.length > 0) {
|
|
275
|
+
this.selectedIndex = (this.selectedIndex + 1) % this.subscriptionEntries.length;
|
|
276
|
+
this.subscriptionLogoutConfirmationTarget = null;
|
|
277
|
+
}
|
|
278
|
+
return;
|
|
279
|
+
}
|
|
280
|
+
this.selectedIndex = (this.selectedIndex + 1) % items.length;
|
|
281
|
+
this.subscriptionLogoutConfirmationTarget = null;
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
getSelected(): SettingEntry | null {
|
|
285
|
+
const items = this._currentItems();
|
|
286
|
+
if (items.length === 0) return null;
|
|
287
|
+
return items[Math.max(0, Math.min(items.length - 1, this.selectedIndex))] ?? null;
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
/** Get the currently selected flag entry (flags tab only). */
|
|
291
|
+
getSelectedFlag(): FlagEntry | null {
|
|
292
|
+
if (this.currentCategory !== 'flags') return null;
|
|
293
|
+
if (this.flagEntries.length === 0) return null;
|
|
294
|
+
return this.flagEntries[Math.max(0, Math.min(this.flagEntries.length - 1, this.selectedIndex))] ?? null;
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
getSelectedMcp(): McpEntry | null {
|
|
298
|
+
if (this.currentCategory !== 'mcp') return null;
|
|
299
|
+
if (this.mcpEntries.length === 0) return null;
|
|
300
|
+
return this.mcpEntries[Math.max(0, Math.min(this.mcpEntries.length - 1, this.selectedIndex))] ?? null;
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
getSelectedSubscription(): SubscriptionEntry | null {
|
|
304
|
+
if (this.currentCategory !== 'subscriptions') return null;
|
|
305
|
+
if (this.subscriptionEntries.length === 0) return null;
|
|
306
|
+
return this.subscriptionEntries[Math.max(0, Math.min(this.subscriptionEntries.length - 1, this.selectedIndex))] ?? null;
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
get currentCategory(): SettingsCategory {
|
|
310
|
+
return SETTINGS_CATEGORIES[this.categoryIndex];
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
get currentItems(): SettingEntry[] {
|
|
314
|
+
return this._currentItems();
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
selectTarget(target?: string): void {
|
|
318
|
+
const normalized = target?.trim();
|
|
319
|
+
if (!normalized) return;
|
|
320
|
+
|
|
321
|
+
const categoryIndex = SETTINGS_CATEGORIES.indexOf(normalized as SettingsCategory);
|
|
322
|
+
if (categoryIndex >= 0) {
|
|
323
|
+
this.categoryIndex = categoryIndex;
|
|
324
|
+
this.selectedIndex = 0;
|
|
325
|
+
this.focusPane = 'settings';
|
|
326
|
+
return;
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
for (let index = 0; index < SETTINGS_CATEGORIES.length; index += 1) {
|
|
330
|
+
const category = SETTINGS_CATEGORIES[index]!;
|
|
331
|
+
const entries = this.groups.get(category) ?? [];
|
|
332
|
+
const entryIndex = entries.findIndex((entry) => entry.setting.key === normalized);
|
|
333
|
+
if (entryIndex >= 0) {
|
|
334
|
+
this.categoryIndex = index;
|
|
335
|
+
this.selectedIndex = entryIndex;
|
|
336
|
+
this.focusPane = 'settings';
|
|
337
|
+
return;
|
|
338
|
+
}
|
|
339
|
+
}
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
/**
|
|
343
|
+
* Toggle boolean or begin cycling enum values, or enter edit mode for string/number.
|
|
344
|
+
*/
|
|
345
|
+
activateSelected(): void {
|
|
346
|
+
if (this.currentCategory === 'mcp') {
|
|
347
|
+
const entry = this.getSelectedMcp();
|
|
348
|
+
if (!entry) return;
|
|
349
|
+
this.editingMode = true;
|
|
350
|
+
this.editBuffer = entry.trustMode;
|
|
351
|
+
this.mcpAllowAllConfirmationTarget = null;
|
|
352
|
+
return;
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
if (this.currentCategory === 'subscriptions') {
|
|
356
|
+
const entry = this.getSelectedSubscription();
|
|
357
|
+
if (!entry) return;
|
|
358
|
+
if (entry.state === 'active' || entry.state === 'pending') {
|
|
359
|
+
if (this.subscriptionLogoutConfirmationTarget !== entry.provider) {
|
|
360
|
+
this.subscriptionLogoutConfirmationTarget = entry.provider;
|
|
361
|
+
return;
|
|
362
|
+
}
|
|
363
|
+
this.subscriptionManager?.logout(entry.provider);
|
|
364
|
+
this._loadSubscriptionEntries();
|
|
365
|
+
this.subscriptionLogoutConfirmationTarget = null;
|
|
366
|
+
}
|
|
367
|
+
return;
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
const entry = this.getSelected();
|
|
371
|
+
if (!entry || !this.configManager) return;
|
|
372
|
+
|
|
373
|
+
const { setting } = entry;
|
|
374
|
+
|
|
375
|
+
// Delegate provider/model picker settings to the model picker UI
|
|
376
|
+
if (setting.key === 'tts.provider') {
|
|
377
|
+
this.pendingSettingsPickerAction = 'tts-provider';
|
|
378
|
+
return;
|
|
379
|
+
}
|
|
380
|
+
if (setting.key === 'tts.voice') {
|
|
381
|
+
this.pendingSettingsPickerAction = 'tts-voice';
|
|
382
|
+
return;
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
const pickerLaunch = modelPickerLaunchForKey(setting.key);
|
|
386
|
+
if (pickerLaunch !== null) {
|
|
387
|
+
if (pickerLaunch.flow === 'providerModel') {
|
|
388
|
+
this.pendingProviderModelPickerTarget = pickerLaunch.target;
|
|
389
|
+
} else {
|
|
390
|
+
this.pendingModelPickerTarget = pickerLaunch.target;
|
|
391
|
+
}
|
|
392
|
+
return;
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
if (setting.type === 'boolean') {
|
|
396
|
+
const newVal = !entry.currentValue;
|
|
397
|
+
this._setValue(setting.key, newVal);
|
|
398
|
+
} else if (setting.type === 'enum' && setting.enumValues) {
|
|
399
|
+
const idx = setting.enumValues.indexOf(entry.currentValue as string);
|
|
400
|
+
const nextIdx = (idx + 1) % setting.enumValues.length;
|
|
401
|
+
this._setValue(setting.key, setting.enumValues[nextIdx]);
|
|
402
|
+
} else if (setting.type === 'string' || setting.type === 'number') {
|
|
403
|
+
// Enter inline edit mode
|
|
404
|
+
this.editingMode = true;
|
|
405
|
+
this.editBuffer = String(entry.currentValue ?? '');
|
|
406
|
+
}
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
adjustSelected(direction: 'left' | 'right', step = 1): void {
|
|
410
|
+
if (this.editingMode) return;
|
|
411
|
+
|
|
412
|
+
if (this.currentCategory === 'flags') {
|
|
413
|
+
const flagEntry = this.getSelectedFlag();
|
|
414
|
+
if (!flagEntry || flagEntry.state === 'killed' || !this.featureFlagManager || !this.configManager) return;
|
|
415
|
+
const targetState: FlagState = direction === 'right' ? 'enabled' : 'disabled';
|
|
416
|
+
if (flagEntry.state !== targetState) this._setSelectedFlagState(flagEntry, targetState);
|
|
417
|
+
return;
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
if (this.currentCategory === 'mcp') {
|
|
421
|
+
const entry = this.getSelectedMcp();
|
|
422
|
+
if (!entry || !this.mcpRegistry) return;
|
|
423
|
+
const modes: McpEntry['trustMode'][] = ['constrained', 'ask-on-risk', 'allow-all', 'blocked'];
|
|
424
|
+
const currentIndex = Math.max(0, modes.indexOf(entry.trustMode));
|
|
425
|
+
const nextIndex = direction === 'right'
|
|
426
|
+
? (currentIndex + 1) % modes.length
|
|
427
|
+
: (currentIndex - 1 + modes.length) % modes.length;
|
|
428
|
+
this.mcpRegistry.setServerTrustMode(entry.name, modes[nextIndex]!);
|
|
429
|
+
this._loadMcpEntries();
|
|
430
|
+
this.mcpAllowAllConfirmationTarget = null;
|
|
431
|
+
return;
|
|
432
|
+
}
|
|
433
|
+
|
|
434
|
+
const entry = this.getSelected();
|
|
435
|
+
if (!entry || !this.configManager) return;
|
|
436
|
+
const { setting } = entry;
|
|
437
|
+
|
|
438
|
+
if (setting.type === 'boolean') {
|
|
439
|
+
this._setValue(setting.key, direction === 'right');
|
|
440
|
+
return;
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
if (setting.type === 'enum' && setting.enumValues && setting.enumValues.length > 0) {
|
|
444
|
+
const currentIndex = Math.max(0, setting.enumValues.indexOf(String(entry.currentValue)));
|
|
445
|
+
const nextIndex = direction === 'right'
|
|
446
|
+
? (currentIndex + 1) % setting.enumValues.length
|
|
447
|
+
: (currentIndex - 1 + setting.enumValues.length) % setting.enumValues.length;
|
|
448
|
+
this._setValue(setting.key, setting.enumValues[nextIndex]!);
|
|
449
|
+
return;
|
|
450
|
+
}
|
|
451
|
+
|
|
452
|
+
if (setting.type === 'number') {
|
|
453
|
+
const currentNumber = Number(entry.currentValue ?? 0);
|
|
454
|
+
if (!Number.isFinite(currentNumber)) return;
|
|
455
|
+
const adjustment = getNumericAdjustmentMeta(setting);
|
|
456
|
+
const delta = adjustment.step * step;
|
|
457
|
+
const rounded = roundToPrecision(currentNumber + (direction === 'right' ? delta : -delta), adjustment.precision);
|
|
458
|
+
const nextValue = Math.min(
|
|
459
|
+
adjustment.max ?? rounded,
|
|
460
|
+
Math.max(adjustment.min ?? rounded, rounded),
|
|
461
|
+
);
|
|
462
|
+
if (setting.validate && !setting.validate(nextValue)) return;
|
|
463
|
+
this._setValue(setting.key, nextValue);
|
|
464
|
+
}
|
|
465
|
+
}
|
|
466
|
+
|
|
467
|
+
/**
|
|
468
|
+
* Toggle the currently selected feature flag.
|
|
469
|
+
*
|
|
470
|
+
* Killed flags cannot be toggled. Non-runtimeToggleable flags toggle in config
|
|
471
|
+
* only (require restart). runtimeToggleable flags toggle immediately.
|
|
472
|
+
*/
|
|
473
|
+
toggleSelectedFlag(): void {
|
|
474
|
+
const flagEntry = this.getSelectedFlag();
|
|
475
|
+
if (!flagEntry || !this.featureFlagManager || !this.configManager) return;
|
|
476
|
+
|
|
477
|
+
const { flag, state } = flagEntry;
|
|
478
|
+
|
|
479
|
+
// Killed flags are blocked
|
|
480
|
+
if (state === 'killed') return;
|
|
481
|
+
|
|
482
|
+
const newState: FlagState = state === 'enabled' ? 'disabled' : 'enabled';
|
|
483
|
+
|
|
484
|
+
this._setSelectedFlagState(flagEntry, newState);
|
|
485
|
+
}
|
|
486
|
+
|
|
487
|
+
private _setSelectedFlagState(flagEntry: FlagEntry, newState: FlagState): void {
|
|
488
|
+
if (!this.featureFlagManager || !this.configManager) return;
|
|
489
|
+
const { flag } = flagEntry;
|
|
490
|
+
|
|
491
|
+
if (!flag.runtimeToggleable) {
|
|
492
|
+
// Persist to config only — takes effect on restart
|
|
493
|
+
this._persistFlagState(flag.id, newState, flag.defaultState as FlagState);
|
|
494
|
+
flagEntry.state = newState;
|
|
495
|
+
} else {
|
|
496
|
+
// Toggle immediately in manager
|
|
497
|
+
try {
|
|
498
|
+
if (newState === 'enabled') {
|
|
499
|
+
this.featureFlagManager.enable(flag.id);
|
|
500
|
+
} else {
|
|
501
|
+
this.featureFlagManager.disable(flag.id);
|
|
502
|
+
}
|
|
503
|
+
this._persistFlagState(flag.id, newState, flag.defaultState as FlagState);
|
|
504
|
+
flagEntry.state = newState;
|
|
505
|
+
} catch (e) {
|
|
506
|
+
logger.error('SettingsModal: failed to toggle feature flag', { flag: flag.id, error: summarizeError(e) });
|
|
507
|
+
}
|
|
508
|
+
}
|
|
509
|
+
}
|
|
510
|
+
|
|
511
|
+
/**
|
|
512
|
+
* Commit the current editBuffer to the config.
|
|
513
|
+
* Returns true on success, false if validation failed.
|
|
514
|
+
*/
|
|
515
|
+
commitEdit(): boolean {
|
|
516
|
+
if (!this.editingMode) return false;
|
|
517
|
+
|
|
518
|
+
if (this.currentCategory === 'mcp') {
|
|
519
|
+
const entry = this.getSelectedMcp();
|
|
520
|
+
if (!entry || !this.mcpRegistry) return false;
|
|
521
|
+
if (this.mcpAllowAllConfirmationTarget) {
|
|
522
|
+
const expected = `ALLOW ALL ${this.mcpAllowAllConfirmationTarget}`;
|
|
523
|
+
if (this.editBuffer.trim() !== expected) {
|
|
524
|
+
return false;
|
|
525
|
+
}
|
|
526
|
+
this.mcpRegistry.setServerTrustMode(entry.name, 'allow-all');
|
|
527
|
+
this._loadMcpEntries();
|
|
528
|
+
this.editingMode = false;
|
|
529
|
+
this.editBuffer = '';
|
|
530
|
+
this.mcpAllowAllConfirmationTarget = null;
|
|
531
|
+
return true;
|
|
532
|
+
}
|
|
533
|
+
|
|
534
|
+
const nextMode = this.editBuffer.trim() as McpEntry['trustMode'];
|
|
535
|
+
const validModes: McpEntry['trustMode'][] = ['constrained', 'ask-on-risk', 'allow-all', 'blocked'];
|
|
536
|
+
if (!validModes.includes(nextMode)) {
|
|
537
|
+
this.editingMode = false;
|
|
538
|
+
this.editBuffer = '';
|
|
539
|
+
this.mcpAllowAllConfirmationTarget = null;
|
|
540
|
+
return false;
|
|
541
|
+
}
|
|
542
|
+
if (nextMode === 'allow-all' && entry.trustMode !== 'allow-all') {
|
|
543
|
+
this.mcpAllowAllConfirmationTarget = entry.name;
|
|
544
|
+
this.editBuffer = '';
|
|
545
|
+
return false;
|
|
546
|
+
}
|
|
547
|
+
this.mcpRegistry.setServerTrustMode(entry.name, nextMode);
|
|
548
|
+
this._loadMcpEntries();
|
|
549
|
+
this.editingMode = false;
|
|
550
|
+
this.editBuffer = '';
|
|
551
|
+
this.mcpAllowAllConfirmationTarget = null;
|
|
552
|
+
return true;
|
|
553
|
+
}
|
|
554
|
+
|
|
555
|
+
const entry = this.getSelected();
|
|
556
|
+
if (!entry || !this.configManager) return false;
|
|
557
|
+
|
|
558
|
+
const { setting } = entry;
|
|
559
|
+
let parsed: unknown = this.editBuffer;
|
|
560
|
+
|
|
561
|
+
if (setting.type === 'number') {
|
|
562
|
+
parsed = Number(this.editBuffer);
|
|
563
|
+
if (isNaN(parsed as number)) {
|
|
564
|
+
this.editingMode = false;
|
|
565
|
+
this.editBuffer = '';
|
|
566
|
+
return false;
|
|
567
|
+
}
|
|
568
|
+
}
|
|
569
|
+
|
|
570
|
+
if (setting.validate && !setting.validate(parsed)) {
|
|
571
|
+
this.editingMode = false;
|
|
572
|
+
this.editBuffer = '';
|
|
573
|
+
return false;
|
|
574
|
+
}
|
|
575
|
+
|
|
576
|
+
if (setting.type === 'string' && isSecretConfigKey(setting.key)) {
|
|
577
|
+
setSecretBackedSettingValue({
|
|
578
|
+
key: setting.key,
|
|
579
|
+
value: String(parsed ?? ''),
|
|
580
|
+
configManager: this.configManager,
|
|
581
|
+
secretsManager: this.secretsManager,
|
|
582
|
+
setConfigValue: (key, value) => this._setValue(key, value),
|
|
583
|
+
});
|
|
584
|
+
} else {
|
|
585
|
+
this._setValue(setting.key, parsed);
|
|
586
|
+
}
|
|
587
|
+
this.editingMode = false;
|
|
588
|
+
this.editBuffer = '';
|
|
589
|
+
return true;
|
|
590
|
+
}
|
|
591
|
+
|
|
592
|
+
/** Cancel inline edit without saving. */
|
|
593
|
+
cancelEdit(): void {
|
|
594
|
+
this.editingMode = false;
|
|
595
|
+
this.editBuffer = '';
|
|
596
|
+
this.mcpAllowAllConfirmationTarget = null;
|
|
597
|
+
}
|
|
598
|
+
|
|
599
|
+
resetSelected(): { key: ConfigKey; value: unknown } | null {
|
|
600
|
+
if (this.editingMode || !this.configManager) return null;
|
|
601
|
+
const entry = this.getSelected();
|
|
602
|
+
if (!entry) return null;
|
|
603
|
+
const key = entry.setting.key as ConfigKey;
|
|
604
|
+
this._setValue(key, entry.setting.default);
|
|
605
|
+
if (isSecretConfigKey(key) && this.secretsManager) {
|
|
606
|
+
void this.secretsManager.delete(buildGoodVibesSecretKey(key), { scope: 'user' }).catch((error) => {
|
|
607
|
+
logger.error('SettingsModal: failed to clear secret while resetting setting', { key, error: summarizeError(error) });
|
|
608
|
+
});
|
|
609
|
+
}
|
|
610
|
+
return { key, value: entry.setting.default };
|
|
611
|
+
}
|
|
612
|
+
|
|
613
|
+
/** Handle a keystroke in edit mode: regular chars appended, Backspace removes last char. */
|
|
614
|
+
editChar(char: string): void {
|
|
615
|
+
if (!this.editingMode) return;
|
|
616
|
+
this.editBuffer += char;
|
|
617
|
+
}
|
|
618
|
+
|
|
619
|
+
editBackspace(): void {
|
|
620
|
+
if (!this.editingMode) return;
|
|
621
|
+
this.editBuffer = this.editBuffer.slice(0, -1);
|
|
622
|
+
}
|
|
623
|
+
|
|
624
|
+
// ── Private helpers ────────────────────────────────────────────
|
|
625
|
+
|
|
626
|
+
private _loadGroups(configManager: ConfigManager): void {
|
|
627
|
+
this.groups.clear();
|
|
628
|
+
for (const cat of SETTINGS_CATEGORIES) {
|
|
629
|
+
if (cat === 'flags') continue; // flags tab handled separately
|
|
630
|
+
this.groups.set(cat, []);
|
|
631
|
+
}
|
|
632
|
+
|
|
633
|
+
for (const setting of CONFIG_SCHEMA) {
|
|
634
|
+
const rawCat = setting.key.split('.')[0] as string;
|
|
635
|
+
const cat = rawCat as SettingsCategory;
|
|
636
|
+
const currentValue = configManager.get(setting.key as ConfigKey);
|
|
637
|
+
const resolved = getResolvedSettingLookup(configManager, setting.key as ConfigKey)?.entry;
|
|
638
|
+
const entry: SettingEntry = {
|
|
639
|
+
setting,
|
|
640
|
+
currentValue,
|
|
641
|
+
isDefault: currentValue === setting.default,
|
|
642
|
+
effectiveSource: resolved?.effectiveSource,
|
|
643
|
+
locked: resolved?.locked,
|
|
644
|
+
conflict: resolved?.conflict,
|
|
645
|
+
sourceLabel: resolved?.sourceLabel,
|
|
646
|
+
lockReason: resolved?.lockReason,
|
|
647
|
+
};
|
|
648
|
+
if (this.groups.has(cat)) this.groups.get(cat)!.push(entry);
|
|
649
|
+
if ((rawCat === 'controlPlane' || rawCat === 'httpListener' || rawCat === 'web') && this.groups.has('network')) {
|
|
650
|
+
this.groups.get('network')!.push(entry);
|
|
651
|
+
}
|
|
652
|
+
}
|
|
653
|
+
|
|
654
|
+
const uiEntries = this.groups.get('ui');
|
|
655
|
+
if (uiEntries) {
|
|
656
|
+
const uiPriority: Record<string, number> = {
|
|
657
|
+
'ui.systemMessages': 0,
|
|
658
|
+
'ui.operationalMessages': 1,
|
|
659
|
+
'ui.wrfcMessages': 2,
|
|
660
|
+
'ui.voiceEnabled': 3,
|
|
661
|
+
};
|
|
662
|
+
uiEntries.sort((a, b) => (uiPriority[a.setting.key] ?? 99) - (uiPriority[b.setting.key] ?? 99));
|
|
663
|
+
}
|
|
664
|
+
}
|
|
665
|
+
|
|
666
|
+
/** Load or refresh the flags tab entries from the feature flag manager. */
|
|
667
|
+
private _loadFlagEntries(): void {
|
|
668
|
+
if (!this.featureFlagManager) {
|
|
669
|
+
this.flagEntries = [];
|
|
670
|
+
return;
|
|
671
|
+
}
|
|
672
|
+
this.flagEntries = Array.from(this.featureFlagManager.getAll().values()).map(({ flag, state }) => ({
|
|
673
|
+
flag,
|
|
674
|
+
state,
|
|
675
|
+
}));
|
|
676
|
+
}
|
|
677
|
+
|
|
678
|
+
private _loadMcpEntries(): void {
|
|
679
|
+
if (!this.mcpRegistry) {
|
|
680
|
+
this.mcpEntries = [];
|
|
681
|
+
return;
|
|
682
|
+
}
|
|
683
|
+
this.mcpEntries = this.mcpRegistry.listServerSecurity().map((entry) => ({
|
|
684
|
+
name: entry.name,
|
|
685
|
+
connected: entry.connected,
|
|
686
|
+
role: entry.role,
|
|
687
|
+
trustMode: entry.trustMode,
|
|
688
|
+
allowedPaths: [...entry.allowedPaths],
|
|
689
|
+
allowedHosts: [...entry.allowedHosts],
|
|
690
|
+
}));
|
|
691
|
+
}
|
|
692
|
+
|
|
693
|
+
private _loadSubscriptionEntries(): void {
|
|
694
|
+
this.subscriptionEntries = buildSubscriptionEntries(this.subscriptionManager, this.serviceRegistry);
|
|
695
|
+
}
|
|
696
|
+
|
|
697
|
+
/**
|
|
698
|
+
* Persist a flag state override to config.
|
|
699
|
+
* Deletes the entry when reverting to defaultState. Skips killed state.
|
|
700
|
+
*/
|
|
701
|
+
private _persistFlagState(flagId: string, newState: FlagState, defaultState: FlagState): void {
|
|
702
|
+
if (!this.configManager) return;
|
|
703
|
+
if (newState === 'killed') return; // never persist killed state
|
|
704
|
+
|
|
705
|
+
try {
|
|
706
|
+
const current = (this.configManager.getCategory('featureFlags') as Record<string, PersistedFlagState>) ?? {};
|
|
707
|
+
if (newState === defaultState) {
|
|
708
|
+
// Revert to default — remove override
|
|
709
|
+
delete current[flagId];
|
|
710
|
+
} else {
|
|
711
|
+
current[flagId] = newState;
|
|
712
|
+
}
|
|
713
|
+
this.configManager.mergeCategory('featureFlags', current);
|
|
714
|
+
} catch (e) {
|
|
715
|
+
logger.error('SettingsModal: failed to persist flag state', { flagId, error: summarizeError(e) });
|
|
716
|
+
}
|
|
717
|
+
}
|
|
718
|
+
|
|
719
|
+
/** Returns [] for the flags category (flags use flagEntries instead). */
|
|
720
|
+
private _currentItems(): SettingEntry[] {
|
|
721
|
+
if (this.currentCategory === 'flags' || this.currentCategory === 'mcp' || this.currentCategory === 'subscriptions') return [];
|
|
722
|
+
const items = this.groups.get(this.currentCategory) ?? [];
|
|
723
|
+
if (this.currentCategory === 'network') {
|
|
724
|
+
return items.filter(entry => {
|
|
725
|
+
if (entry.setting.key === 'controlPlane.host') {
|
|
726
|
+
const hostMode = this.configManager?.get('controlPlane.hostMode');
|
|
727
|
+
return hostMode === 'custom';
|
|
728
|
+
}
|
|
729
|
+
if (entry.setting.key === 'httpListener.host') {
|
|
730
|
+
const hostMode = this.configManager?.get('httpListener.hostMode');
|
|
731
|
+
return hostMode === 'custom';
|
|
732
|
+
}
|
|
733
|
+
if (entry.setting.key === 'web.host') {
|
|
734
|
+
const hostMode = this.configManager?.get('web.hostMode');
|
|
735
|
+
return hostMode === 'custom';
|
|
736
|
+
}
|
|
737
|
+
return true;
|
|
738
|
+
});
|
|
739
|
+
}
|
|
740
|
+
return items;
|
|
741
|
+
}
|
|
742
|
+
|
|
743
|
+
private _refreshAllEntries(): void {
|
|
744
|
+
if (!this.configManager) return;
|
|
745
|
+
for (const entries of this.groups.values()) {
|
|
746
|
+
for (const entry of entries) {
|
|
747
|
+
entry.currentValue = this.configManager.get(entry.setting.key as ConfigKey);
|
|
748
|
+
entry.isDefault = entry.currentValue === entry.setting.default;
|
|
749
|
+
}
|
|
750
|
+
}
|
|
751
|
+
}
|
|
752
|
+
|
|
753
|
+
private _setValue(key: ConfigKey, value: unknown): void {
|
|
754
|
+
if (!this.configManager) return;
|
|
755
|
+
// Diff previous value before writing — avoids false restart notices on no-op saves
|
|
756
|
+
const previousValue = this.configManager.get(key);
|
|
757
|
+
const isRestartKey = ['host', 'port', 'hostMode', 'enabled'].includes(key.split('.')[1] ?? '');
|
|
758
|
+
try {
|
|
759
|
+
this.configManager.setDynamic(key, value);
|
|
760
|
+
const rawCat = key.split('.')[0] as string;
|
|
761
|
+
if (rawCat === 'controlPlane') {
|
|
762
|
+
if (isRestartKey && previousValue !== value) {
|
|
763
|
+
this.lastSaveTriggeredRestart = 'control-plane';
|
|
764
|
+
}
|
|
765
|
+
} else if (rawCat === 'httpListener') {
|
|
766
|
+
if (isRestartKey && previousValue !== value) {
|
|
767
|
+
this.lastSaveTriggeredRestart = 'http-listener';
|
|
768
|
+
}
|
|
769
|
+
} else if (rawCat === 'web') {
|
|
770
|
+
if (isRestartKey && previousValue !== value) {
|
|
771
|
+
this.lastSaveTriggeredRestart = 'web';
|
|
772
|
+
}
|
|
773
|
+
}
|
|
774
|
+
|
|
775
|
+
for (const entries of this.groups.values()) {
|
|
776
|
+
const entry = entries.find((candidate) => candidate.setting.key === key);
|
|
777
|
+
if (entry) {
|
|
778
|
+
entry.currentValue = this.configManager!.get(key);
|
|
779
|
+
entry.isDefault = entry.currentValue === entry.setting.default;
|
|
780
|
+
}
|
|
781
|
+
}
|
|
782
|
+
if (previousValue !== value && this.onSettingApplied) {
|
|
783
|
+
const result = this.onSettingApplied({ key, previousValue, value });
|
|
784
|
+
this.lastSettingEffectMessage = result?.message ?? null;
|
|
785
|
+
this._refreshAllEntries();
|
|
786
|
+
}
|
|
787
|
+
} catch (e) {
|
|
788
|
+
logger.error('SettingsModal: failed to set config value', { key, error: summarizeError(e) });
|
|
789
|
+
this.lastSettingEffectMessage = `Save failed: ${summarizeError(e)}`;
|
|
790
|
+
}
|
|
791
|
+
}
|
|
792
|
+
|
|
793
|
+
}
|