@pellux/goodvibes-tui 1.16.1 → 1.17.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +89 -9
- package/README.md +1 -1
- package/docs/foundation-artifacts/operator-contract.json +1091 -271
- package/package.json +7 -7
- package/src/cli/completions/generate.ts +5 -4
- package/src/cli/config-overrides.ts +51 -11
- package/src/cli/doctor.ts +4 -5
- package/src/cli/entrypoint.ts +13 -8
- package/src/cli/help.ts +7 -6
- package/src/cli/launch-auto-update.ts +214 -0
- package/src/cli/parser.ts +5 -0
- package/src/cli/status.ts +13 -0
- package/src/cli/surface-command.ts +5 -5
- package/src/cli/tui-startup.ts +60 -97
- package/src/cli/types.ts +2 -0
- package/src/config/index.ts +1 -1
- package/src/config/tui-extension-settings.ts +33 -0
- package/src/core/composer-state.ts +1 -1
- package/src/core/conversation-line-cache.ts +3 -3
- package/src/core/conversation-rendering.ts +4 -4
- package/src/core/conversation.ts +2 -2
- package/src/core/long-task-notifier.ts +7 -7
- package/src/core/stream-event-wiring.ts +8 -6
- package/src/core/system-message-noise.ts +1 -1
- package/src/core/system-message-router.ts +6 -6
- package/src/core/turn-cancellation.ts +1 -1
- package/src/core/turn-event-wiring.ts +8 -8
- package/src/daemon/cli.ts +20 -5
- package/src/daemon/lifecycle.ts +41 -0
- package/src/daemon/service-commands.ts +5 -5
- package/src/export/cost-utils.ts +128 -6
- package/src/input/autocomplete.ts +2 -2
- package/src/input/command-registry.ts +34 -9
- package/src/input/commands/ci-runtime.ts +5 -1
- package/src/input/commands/codebase-runtime.ts +6 -6
- package/src/input/commands/cost-runtime.ts +1 -1
- package/src/input/commands/health-runtime.ts +2 -2
- package/src/input/commands/intelligence-runtime.ts +1 -1
- package/src/input/commands/local-auth-runtime.ts +1 -1
- package/src/input/commands/local-provider-runtime.ts +49 -10
- package/src/input/commands/local-runtime.ts +53 -5
- package/src/input/commands/memory.ts +2 -2
- package/src/input/commands/operator-panel-runtime.ts +5 -5
- package/src/input/commands/permissions-provenance.ts +1 -1
- package/src/input/commands/permissions-runtime.ts +99 -8
- package/src/input/commands/planning-runtime.ts +3 -3
- package/src/input/commands/platform-sandbox-runtime.ts +1 -1
- package/src/input/commands/provider-accounts-runtime.ts +71 -6
- package/src/input/commands/provider.ts +7 -5
- package/src/input/commands/recall-files-git.ts +13 -51
- package/src/input/commands/recall-files.ts +8 -11
- package/src/input/commands/recall-review.ts +2 -2
- package/src/input/commands/relay-runtime.ts +1 -1
- package/src/input/commands/remote-runtime.ts +1 -1
- package/src/input/commands/runtime-services.ts +4 -4
- package/src/input/commands/schedule-runtime.ts +14 -7
- package/src/input/commands/services-runtime.ts +1 -1
- package/src/input/commands/session-workflow.ts +2 -2
- package/src/input/commands/settings-sync-runtime.ts +2 -2
- package/src/input/commands/share-runtime.ts +1 -1
- package/src/input/commands/shell-core.ts +46 -68
- package/src/input/commands/subscription-runtime.ts +1 -1
- package/src/input/commands/update-runtime.ts +153 -96
- package/src/input/commands/work-plan-runtime.ts +1 -1
- package/src/input/commands/workstream-runtime.ts +2 -2
- package/src/input/commands.ts +0 -2
- package/src/input/config-modal-types.ts +3 -3
- package/src/input/config-modal.ts +10 -11
- package/src/input/feature-unit-layout.ts +93 -82
- package/src/input/feed-context-factory.ts +2 -2
- package/src/input/handler-command-route.ts +2 -2
- package/src/input/handler-content-actions.ts +1 -1
- package/src/input/handler-feed-routes.ts +7 -7
- package/src/input/handler-feed.ts +4 -4
- package/src/input/handler-interactions.ts +3 -2
- package/src/input/handler-modal-routes.ts +12 -4
- package/src/input/handler-modal-token-routes.ts +1 -1
- package/src/input/handler-onboarding-daemon-adopt.ts +2 -2
- package/src/input/handler-onboarding.ts +3 -2
- package/src/input/handler-picker-routes.ts +26 -13
- package/src/input/handler-shortcuts.ts +6 -6
- package/src/input/handler.ts +2 -2
- package/src/input/keybindings.ts +2 -2
- package/src/input/onboarding/onboarding-feature-units.ts +34 -27
- package/src/input/onboarding/onboarding-provider-key-fields.ts +112 -0
- package/src/input/onboarding/onboarding-wizard-apply.ts +22 -21
- package/src/input/onboarding/onboarding-wizard-external-surfaces.ts +17 -0
- package/src/input/onboarding/onboarding-wizard-network-adopt.ts +2 -2
- package/src/input/onboarding/onboarding-wizard-steps.ts +30 -42
- package/src/input/panel-integration-actions.ts +2 -2
- package/src/input/panel-mouse-geometry.ts +1 -1
- package/src/input/panel-paste-flood-guard.ts +1 -1
- package/src/input/provider-key-intake.ts +167 -0
- package/src/input/sandbox-exec-config.ts +2 -2
- package/src/input/selection-modal.ts +1 -1
- package/src/input/settings-modal-activation.ts +5 -0
- package/src/input/settings-modal-adjustment.ts +4 -31
- package/src/input/settings-modal-data.ts +22 -21
- package/src/input/settings-modal-mutations.ts +12 -68
- package/src/input/settings-modal-types.ts +23 -16
- package/src/input/settings-modal.ts +74 -55
- package/src/main.ts +48 -48
- package/src/panels/agent-inspector-shared.ts +4 -4
- package/src/panels/base-panel.ts +2 -2
- package/src/panels/builtin/agent.ts +4 -4
- package/src/panels/builtin/development.ts +4 -4
- package/src/panels/builtin/knowledge.ts +1 -1
- package/src/panels/builtin/operations.ts +29 -10
- package/src/panels/builtin/session.ts +5 -5
- package/src/panels/builtin/shared.ts +4 -4
- package/src/panels/builtin-modals.ts +10 -10
- package/src/panels/builtin-panels.ts +1 -1
- package/src/panels/cost-tracker-panel.ts +124 -7
- package/src/panels/diff-panel.ts +5 -5
- package/src/panels/eval-registry.ts +2 -2
- package/src/panels/fleet-deep-link.ts +1 -1
- package/src/panels/fleet-panel-format.ts +44 -8
- package/src/panels/fleet-panel.ts +16 -16
- package/src/panels/fleet-read-model.ts +52 -13
- package/src/panels/fleet-steer.ts +2 -2
- package/src/panels/fleet-stop.ts +3 -3
- package/src/panels/fleet-tabs.ts +13 -14
- package/src/panels/fleet-transcript.ts +5 -5
- package/src/panels/git-panel.ts +2 -2
- package/src/panels/index.ts +4 -1
- package/src/panels/local-auth-panel.ts +1 -1
- package/src/panels/modals/hooks-modal.ts +1 -1
- package/src/panels/modals/keybindings-modal.ts +2 -2
- package/src/panels/modals/knowledge-modal.ts +1 -1
- package/src/panels/modals/marketplace-modal.ts +4 -4
- package/src/panels/modals/memory-modal.ts +4 -4
- package/src/panels/modals/modal-surface-helpers.ts +1 -1
- package/src/panels/modals/modal-theme.ts +1 -1
- package/src/panels/modals/pairing-modal.ts +2 -2
- package/src/panels/modals/planning-modal.ts +2 -2
- package/src/panels/modals/plugins-modal.ts +1 -1
- package/src/panels/modals/policy-modal.ts +1 -1
- package/src/panels/modals/security-modal.ts +1 -1
- package/src/panels/modals/skills-modal.ts +1 -1
- package/src/panels/modals/work-plan-modal.ts +3 -3
- package/src/panels/notifications-feed.ts +127 -0
- package/src/panels/notifications-panel.ts +160 -0
- package/src/panels/panel-manager.ts +9 -9
- package/src/panels/plugins-panel.ts +4 -4
- package/src/panels/project-planning-answer-actions.ts +1 -1
- package/src/panels/provider-health-routes.ts +2 -2
- package/src/panels/skills-panel.ts +4 -5
- package/src/panels/token-budget-panel.ts +11 -3
- package/src/panels/types.ts +5 -5
- package/src/permissions/broker-approval-card.ts +201 -0
- package/src/permissions/hunk-selection.ts +28 -4
- package/src/permissions/prompt.ts +244 -29
- package/src/renderer/autocomplete-overlay.ts +107 -54
- package/src/renderer/code-block.ts +2 -2
- package/src/renderer/compaction-history-modal.ts +1 -1
- package/src/renderer/compaction-preview.ts +1 -1
- package/src/renderer/compaction-quality.ts +1 -1
- package/src/renderer/compositor.ts +2 -2
- package/src/renderer/config-modal.ts +1 -1
- package/src/renderer/conversation-overlays.ts +1 -1
- package/src/renderer/footer-tips.ts +3 -3
- package/src/renderer/hint-grammar.ts +1 -1
- package/src/renderer/modal-factory.ts +1 -1
- package/src/renderer/model-picker-overlay.ts +1 -1
- package/src/renderer/model-workspace.ts +7 -2
- package/src/renderer/panel-composite.ts +2 -2
- package/src/renderer/panel-workspace-bar.ts +1 -1
- package/src/renderer/process-indicator.ts +1 -1
- package/src/renderer/selection-modal-overlay.ts +1 -1
- package/src/renderer/session-picker-modal.ts +2 -2
- package/src/renderer/settings-modal-helpers.ts +14 -5
- package/src/renderer/settings-modal.ts +127 -39
- package/src/renderer/shell-surface.ts +3 -0
- package/src/renderer/terminal-bg-probe.ts +1 -1
- package/src/renderer/theme.ts +3 -3
- package/src/renderer/tool-call.ts +1 -1
- package/src/renderer/tool-result-summary.ts +1 -1
- package/src/renderer/turn-injection.ts +7 -6
- package/src/renderer/ui-factory.ts +6 -1
- package/src/runtime/bootstrap-command-context.ts +4 -1
- package/src/runtime/bootstrap-command-parts.ts +6 -4
- package/src/runtime/bootstrap-core.ts +35 -35
- package/src/runtime/bootstrap-hook-bridge.ts +1 -1
- package/src/runtime/bootstrap-shell.ts +6 -5
- package/src/runtime/bootstrap.ts +34 -32
- package/src/runtime/channel-composition.ts +46 -0
- package/src/runtime/code-index-services.ts +3 -4
- package/src/runtime/daemon-attach-notices.ts +120 -0
- package/src/runtime/durability-services.ts +55 -0
- package/src/runtime/embedded-service-factories.ts +40 -0
- package/src/runtime/feature-settings.ts +147 -0
- package/src/runtime/index.ts +4 -21
- package/src/runtime/legacy-daemon-migration.ts +29 -19
- package/src/runtime/memory-fold.ts +1 -1
- package/src/runtime/notification-dispatch.ts +110 -0
- package/src/runtime/onboarding/apply.ts +3 -29
- package/src/runtime/onboarding/fast-path.ts +39 -6
- package/src/runtime/onboarding/provider-key-capture.ts +126 -0
- package/src/runtime/onboarding/snapshot.ts +0 -1
- package/src/runtime/onboarding/types.ts +9 -3
- package/src/runtime/provider-fallback.ts +55 -0
- package/src/runtime/provider-optimizer-wiring.ts +55 -0
- package/src/runtime/release-artifacts.ts +19 -103
- package/src/runtime/resume-notice.ts +1 -1
- package/src/runtime/services.ts +81 -82
- package/src/runtime/session-continuity-hints.ts +38 -0
- package/src/runtime/surface-feature-flags.ts +40 -9
- package/src/runtime/terminal-output-guard.ts +2 -2
- package/src/runtime/trust/workspace-registration.ts +10 -2
- package/src/runtime/trust/workspace-trust.ts +58 -31
- package/src/runtime/ui-services.ts +3 -1
- package/src/runtime/update-check.ts +15 -69
- package/src/runtime/workstream-services.ts +2 -2
- package/src/shell/blocking-input.ts +102 -6
- package/src/shell/ui-openers.ts +38 -6
- package/src/utils/format-duration.ts +1 -1
- package/src/verification/verification-ledger.ts +21 -4
- package/src/version.ts +1 -1
- package/src/input/commands/flags-runtime.ts +0 -328
|
@@ -10,6 +10,8 @@ import {
|
|
|
10
10
|
} from './onboarding-wizard-external-surfaces.ts';
|
|
11
11
|
import { buildFeatureUnitSteps } from './onboarding-feature-units.ts';
|
|
12
12
|
import { countSelected, modelSelectionLabel, normalizeText } from './onboarding-wizard-helpers.ts';
|
|
13
|
+
import { deriveProviderKeyCaptureTargets } from '../../runtime/onboarding/provider-key-capture.ts';
|
|
14
|
+
import { buildProviderKeyInventoryField, buildProviderKeyMaskedFields, buildSubscriptionPosture } from './onboarding-provider-key-fields.ts';
|
|
13
15
|
import { getDaemonSource, pushDaemonAdoptionFields, pushLegacyDaemonMigrationFields } from './onboarding-wizard-network-adopt.ts';
|
|
14
16
|
import type { OnboardingWizardControllerLike } from './onboarding-wizard-types.ts';
|
|
15
17
|
import type { OnboardingWizardAcknowledgementFieldDefinition, OnboardingWizardActionFieldDefinition, OnboardingWizardChecklistFieldDefinition, OnboardingWizardExternalSurfaceStepId, OnboardingWizardFieldDefinition, OnboardingWizardModelPickerFieldDefinition, OnboardingWizardRadioFieldDefinition, OnboardingWizardRadioOption, OnboardingWizardStepDefinition } from './onboarding-wizard-types.ts';
|
|
@@ -150,10 +152,20 @@ export function buildProvidersStep(controller: OnboardingWizardControllerLike):
|
|
|
150
152
|
const providerAck = controller.runtimeDerived.reopenEditAcknowledgements.providers;
|
|
151
153
|
const activeSubscriptions = controller.runtimeSnapshot?.subscriptions.active ?? [];
|
|
152
154
|
const pendingSubscriptions = controller.runtimeSnapshot?.subscriptions.pending ?? [];
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
const
|
|
155
|
+
// Provider-agnostic subscription posture: one status row per
|
|
156
|
+
// subscription-capable provider (registration truth), plus any provider with
|
|
157
|
+
// a live/pending session, and which provider's in-wizard sign-in applies now.
|
|
158
|
+
const subscription = buildSubscriptionPosture(
|
|
159
|
+
activeSubscriptions.map((entry) => entry.provider),
|
|
160
|
+
pendingSubscriptions.map((entry) => entry.provider),
|
|
161
|
+
controller.runtimeSnapshot?.providerAccounts?.providers,
|
|
162
|
+
);
|
|
163
|
+
const signInPending = subscription.signInPending;
|
|
164
|
+
// Provider-agnostic key capture: one field per provider the registry knows
|
|
165
|
+
// accepts an API key (derived from registration truth — never a hardcoded
|
|
166
|
+
// vendor list). No provider is named as the only option.
|
|
167
|
+
const keyTargets = deriveProviderKeyCaptureTargets(controller.runtimeSnapshot?.providerAccounts?.providers);
|
|
168
|
+
const configuredKeyCount = keyTargets.filter((target) => target.configured).length;
|
|
157
169
|
const providerReviewField: OnboardingWizardAcknowledgementFieldDefinition = {
|
|
158
170
|
kind: 'acknowledgement',
|
|
159
171
|
id: 'providers.reviewed',
|
|
@@ -166,46 +178,21 @@ export function buildProvidersStep(controller: OnboardingWizardControllerLike):
|
|
|
166
178
|
};
|
|
167
179
|
|
|
168
180
|
const fields: OnboardingWizardFieldDefinition[] = [
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
: openAiPending
|
|
176
|
-
? 'An OpenAI subscription login is pending.'
|
|
177
|
-
: 'No OpenAI subscription session was found in the current runtime state.',
|
|
178
|
-
defaultValue: openAiActive ? 'Active' : openAiPending ? 'Pending' : 'Not detected',
|
|
179
|
-
},
|
|
180
|
-
{
|
|
181
|
-
kind: 'status',
|
|
182
|
-
id: 'providers.api-key-inventory',
|
|
183
|
-
label: 'Provider API key inventory',
|
|
184
|
-
hint: providerSecretCount > 0
|
|
185
|
-
? `${providerSecretCount} provider credential reference(s) were found. Values stay masked.`
|
|
186
|
-
: 'No provider API key references were detected in the current runtime state.',
|
|
187
|
-
defaultValue: providerSecretCount > 0 ? `${providerSecretCount} configured` : 'None detected',
|
|
188
|
-
},
|
|
189
|
-
{
|
|
190
|
-
kind: 'masked',
|
|
191
|
-
id: 'providers.openai-api-key',
|
|
192
|
-
label: 'OpenAI API key',
|
|
193
|
-
hint: openAiApiKeyConfigured
|
|
194
|
-
? 'An OpenAI API key is already stored. Leave blank to keep it; enter a new key to replace it through the secret manager.'
|
|
195
|
-
: 'Optional: enter an OpenAI API key now. The value is stored through the secret manager, not in config.',
|
|
196
|
-
placeholder: openAiApiKeyConfigured ? 'already configured' : 'sk-...',
|
|
197
|
-
defaultValue: '',
|
|
198
|
-
},
|
|
199
|
-
...(openAiActive ? [] : [
|
|
181
|
+
...subscription.statusFields,
|
|
182
|
+
buildProviderKeyInventoryField(keyTargets),
|
|
183
|
+
// One masked field per key-accepting provider. Entered values are stored
|
|
184
|
+
// through the secret manager at apply and the provider re-registers live.
|
|
185
|
+
...buildProviderKeyMaskedFields(keyTargets),
|
|
186
|
+
...(!subscription.signInProviderId ? [] : [
|
|
200
187
|
{
|
|
201
188
|
kind: 'action' as const,
|
|
202
189
|
id: 'providers.openai-subscription-start',
|
|
203
190
|
action: 'start-openai-subscription' as const,
|
|
204
|
-
label:
|
|
191
|
+
label: signInPending ? 'Restart OpenAI subscription sign-in' : 'Start OpenAI subscription sign-in',
|
|
205
192
|
hint: 'Opens the OpenAI sign-in flow from the wizard and records pending login state here.',
|
|
206
|
-
defaultValue:
|
|
193
|
+
defaultValue: signInPending ? 'Restart' : 'Start',
|
|
207
194
|
},
|
|
208
|
-
...(
|
|
195
|
+
...(signInPending ? [
|
|
209
196
|
{
|
|
210
197
|
kind: 'text' as const,
|
|
211
198
|
id: 'providers.openai-authorization-url',
|
|
@@ -239,12 +226,13 @@ export function buildProvidersStep(controller: OnboardingWizardControllerLike):
|
|
|
239
226
|
id: 'provider-access',
|
|
240
227
|
title: 'AI provider access',
|
|
241
228
|
shortLabel: 'Providers',
|
|
242
|
-
description: 'Review subscription posture and optionally add an
|
|
229
|
+
description: 'Review subscription posture and optionally add an API key for any provider that accepts one, directly through the wizard.',
|
|
243
230
|
summaryTitle: 'Provider access summary',
|
|
244
231
|
summaryLines: [
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
232
|
+
subscription.summaryLine,
|
|
233
|
+
keyTargets.length === 0
|
|
234
|
+
? 'Provider API keys: no key-accepting providers registered'
|
|
235
|
+
: `Provider API keys: ${configuredKeyCount}/${keyTargets.length} configured`,
|
|
248
236
|
`Review: ${controller.getFieldValueLabel(providerReviewField)}`,
|
|
249
237
|
],
|
|
250
238
|
fields,
|
|
@@ -2,14 +2,14 @@ import type { CommandContext } from './command-registry.ts';
|
|
|
2
2
|
import type { Panel } from '../panels/types.ts';
|
|
3
3
|
import type { PanelManager } from '../panels/panel-manager.ts';
|
|
4
4
|
|
|
5
|
-
//
|
|
5
|
+
// (the purge): every instanceof-routed branch this file used to carry
|
|
6
6
|
// (FileExplorerPanel/FilePreviewPanel/SymbolOutlinePanel — DELETE;
|
|
7
7
|
// ApprovalPanel/TasksPanel/OrchestrationPanel/AgentInspectorPanel —
|
|
8
8
|
// RETIRE-INTO-FLEET) targeted a panel class that no longer exists. Panels
|
|
9
9
|
// migrated onto the formal `Panel.handlePanelIntegrationAction` hook (the
|
|
10
10
|
// preferred seam — see the comment below) are unaffected; this function now
|
|
11
11
|
// only provides that passthrough. See
|
|
12
|
-
// .goodvibes/audit/2026-07-04-wave6-briefs.json
|
|
12
|
+
// .goodvibes/audit/2026-07-04-wave6-briefs.json for the disposition
|
|
13
13
|
// map.
|
|
14
14
|
export function handlePanelIntegrationAction(
|
|
15
15
|
panelManager: PanelManager,
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
//
|
|
4
4
|
// Pure mouse-geometry helpers for the panel workspace, extracted from
|
|
5
5
|
// handler-feed-routes.ts so that file stays under the 800-line architecture
|
|
6
|
-
// cap once the
|
|
6
|
+
// cap once the focus verbs and the paste-flood guard both live in
|
|
7
7
|
// it. These map a terminal (row, col) onto the panel/pane/workspace-tab under
|
|
8
8
|
// the cursor. No dependency on the feed-route state types, so no import cycle.
|
|
9
9
|
// ---------------------------------------------------------------------------
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
// ---------------------------------------------------------------------------
|
|
2
|
-
// panel-paste-flood-guard.ts —
|
|
2
|
+
// panel-paste-flood-guard.ts — item 5.
|
|
3
3
|
//
|
|
4
4
|
// A terminal WITHOUT bracketed paste delivers a pasted block as a burst of
|
|
5
5
|
// discrete 1-char 'text' tokens (isPasteToken stays false for every one of
|
|
@@ -0,0 +1,167 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Provider key intake — capture a provider's API key at the moment the user
|
|
3
|
+
* selects an unconfigured provider or model, store it through the secrets
|
|
4
|
+
* manager (never env-only, never cleartext into config or provider JSON), let
|
|
5
|
+
* the provider re-register live, then complete the user's original selection.
|
|
6
|
+
*
|
|
7
|
+
* The decision of whether a key is needed is read from the provider's own
|
|
8
|
+
* registration-time auth state (describeAuthState / isConfigured) — never a
|
|
9
|
+
* hardcoded vendor list — so this works for any provider the registry knows
|
|
10
|
+
* accepts a key. The secrets-store key name is the provider's declared auth env
|
|
11
|
+
* var; it is used only as the storage key and is never shown to the user.
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
import type { LLMProvider } from '@pellux/goodvibes-sdk/platform/providers';
|
|
15
|
+
import type { ConcealedInputRequest } from './concealed-input.ts';
|
|
16
|
+
import type { CommandContext } from './command-registry.ts';
|
|
17
|
+
import { summarizeError } from '@pellux/goodvibes-sdk/platform/utils';
|
|
18
|
+
|
|
19
|
+
/** The intake decision derived purely from a provider's registration truth. */
|
|
20
|
+
export interface ProviderKeyIntakeDecision {
|
|
21
|
+
/** True when the provider accepts a key and does not yet have one. */
|
|
22
|
+
readonly needsKey: boolean;
|
|
23
|
+
/** Secrets-store key to write the value under (the provider's auth env var). */
|
|
24
|
+
readonly secretKey?: string;
|
|
25
|
+
/** True when the provider already has a credential (no prompt needed). */
|
|
26
|
+
readonly alreadyConfigured: boolean;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Decide whether to prompt for a key, reading only the provider's own auth
|
|
31
|
+
* state. A provider that is already configured, works anonymously, or declares
|
|
32
|
+
* no accepted env var is left alone (proceed straight through). A provider that
|
|
33
|
+
* does not declare an auth state at all is treated per isConfigured(), and an
|
|
34
|
+
* unknown state is assumed configured so a working selection is never nagged.
|
|
35
|
+
*/
|
|
36
|
+
export function decideProviderKeyIntake(provider: LLMProvider | undefined): ProviderKeyIntakeDecision {
|
|
37
|
+
if (!provider) return { needsKey: false, alreadyConfigured: false };
|
|
38
|
+
const state = provider.describeAuthState?.();
|
|
39
|
+
if (state) {
|
|
40
|
+
if (state.configured) return { needsKey: false, alreadyConfigured: true };
|
|
41
|
+
if (state.anonymousReady) return { needsKey: false, alreadyConfigured: false };
|
|
42
|
+
const secretKey = state.authEnvVars[0];
|
|
43
|
+
if (secretKey) return { needsKey: true, secretKey, alreadyConfigured: false };
|
|
44
|
+
return { needsKey: false, alreadyConfigured: false };
|
|
45
|
+
}
|
|
46
|
+
const configured = provider.isConfigured?.() ?? true;
|
|
47
|
+
return { needsKey: false, alreadyConfigured: configured };
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/** Human-facing name for a provider — plain, vendor-neutral, never an env var. */
|
|
51
|
+
export function providerIntakeLabel(providerId: string): string {
|
|
52
|
+
return `the ${providerId} provider`;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/** Dependencies the intake orchestrator needs, decoupled from the command context for testing. */
|
|
56
|
+
export interface ProviderKeyIntakeDeps {
|
|
57
|
+
/** The provider instance whose registration-time auth state drives the decision. */
|
|
58
|
+
readonly provider: LLMProvider | undefined;
|
|
59
|
+
/** Canonical secret store — the same path /secrets set writes through. */
|
|
60
|
+
readonly secretsManager?: {
|
|
61
|
+
set(key: string, value: string, options?: { scope?: 'project' | 'user'; medium?: 'secure' | 'plaintext' }): Promise<void>;
|
|
62
|
+
} | undefined;
|
|
63
|
+
/** Re-resolves credentials and re-registers providers live (no restart). */
|
|
64
|
+
readonly refreshProviderCredentials?: (() => Promise<void>) | undefined;
|
|
65
|
+
/** Begins one line of concealed composer input for the key. */
|
|
66
|
+
readonly beginConcealedInput?: ((request: ConcealedInputRequest) => void) | undefined;
|
|
67
|
+
/** Surfaces short status lines to the transcript. */
|
|
68
|
+
readonly print: (text: string) => void;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* If the selected provider needs a key, open the concealed prompt, store the
|
|
73
|
+
* pasted value through the secrets manager, re-register the provider live, then
|
|
74
|
+
* run `proceed` (the user's original selection). If no key is needed — or the
|
|
75
|
+
* concealed surface is unavailable — `proceed` runs immediately. `proceed`
|
|
76
|
+
* always runs exactly once, including when the user skips (empty/Escape), so a
|
|
77
|
+
* selection is never stranded.
|
|
78
|
+
*/
|
|
79
|
+
export function runProviderKeyIntake(
|
|
80
|
+
providerId: string,
|
|
81
|
+
deps: ProviderKeyIntakeDeps,
|
|
82
|
+
proceed: () => void,
|
|
83
|
+
): void {
|
|
84
|
+
const decision = decideProviderKeyIntake(deps.provider);
|
|
85
|
+
if (!decision.needsKey || !decision.secretKey) {
|
|
86
|
+
proceed();
|
|
87
|
+
return;
|
|
88
|
+
}
|
|
89
|
+
const { secretsManager, beginConcealedInput } = deps;
|
|
90
|
+
if (!secretsManager || !beginConcealedInput) {
|
|
91
|
+
// No canonical store or no concealed surface here — keep the selection
|
|
92
|
+
// rather than blocking it; the user can add the key via /secrets set later.
|
|
93
|
+
proceed();
|
|
94
|
+
return;
|
|
95
|
+
}
|
|
96
|
+
const secretKey = decision.secretKey;
|
|
97
|
+
const label = providerIntakeLabel(providerId);
|
|
98
|
+
let completed = false;
|
|
99
|
+
const finishOnce = (): void => {
|
|
100
|
+
if (completed) return;
|
|
101
|
+
completed = true;
|
|
102
|
+
proceed();
|
|
103
|
+
};
|
|
104
|
+
deps.print(`Enter the API key for ${label} (input is masked; press Enter to store, Esc to skip).`);
|
|
105
|
+
beginConcealedInput({
|
|
106
|
+
label: providerId,
|
|
107
|
+
onSubmit: (value) => {
|
|
108
|
+
if (value.length === 0) {
|
|
109
|
+
deps.print(`No key entered for ${label}; selection kept, no key stored.`);
|
|
110
|
+
finishOnce();
|
|
111
|
+
return;
|
|
112
|
+
}
|
|
113
|
+
void (async () => {
|
|
114
|
+
try {
|
|
115
|
+
await secretsManager.set(secretKey, value, { scope: 'user', medium: 'secure' });
|
|
116
|
+
await deps.refreshProviderCredentials?.();
|
|
117
|
+
deps.print(`Stored the API key for ${label} and registered it live (value hidden). No restart needed.`);
|
|
118
|
+
} catch (error) {
|
|
119
|
+
deps.print(`Could not store the key for ${label}: ${summarizeError(error)}`);
|
|
120
|
+
}
|
|
121
|
+
finishOnce();
|
|
122
|
+
})();
|
|
123
|
+
},
|
|
124
|
+
onCancel: () => {
|
|
125
|
+
deps.print(`Key entry skipped for ${label}; selection kept, no key stored.`);
|
|
126
|
+
finishOnce();
|
|
127
|
+
},
|
|
128
|
+
});
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
/**
|
|
132
|
+
* Command-context adapter for {@link runProviderKeyIntake}: resolves the live
|
|
133
|
+
* provider, secrets manager, live re-registration hook, and concealed-input
|
|
134
|
+
* surface from the shell command context, then runs the intake before
|
|
135
|
+
* completing the user's original selection (`proceed`).
|
|
136
|
+
*/
|
|
137
|
+
export function ensureProviderKeyThenSelect(
|
|
138
|
+
ctx: CommandContext,
|
|
139
|
+
providerId: string,
|
|
140
|
+
proceed: () => void,
|
|
141
|
+
): void {
|
|
142
|
+
// Resolve the live provider defensively: a command context that routes only
|
|
143
|
+
// through the provider API (no raw registry access) leaves the intake a no-op
|
|
144
|
+
// and the selection completes unchanged.
|
|
145
|
+
let registry: CommandContext['provider']['providerRegistry'] | undefined;
|
|
146
|
+
let provider: ReturnType<CommandContext['provider']['providerRegistry']['tryGet']> | undefined;
|
|
147
|
+
try {
|
|
148
|
+
registry = ctx.provider?.providerRegistry;
|
|
149
|
+
provider = registry?.tryGet?.(providerId);
|
|
150
|
+
} catch {
|
|
151
|
+
registry = undefined;
|
|
152
|
+
provider = undefined;
|
|
153
|
+
}
|
|
154
|
+
runProviderKeyIntake(
|
|
155
|
+
providerId,
|
|
156
|
+
{
|
|
157
|
+
provider,
|
|
158
|
+
secretsManager: ctx.platform?.secretsManager,
|
|
159
|
+
refreshProviderCredentials: registry
|
|
160
|
+
? () => registry!.refreshProviderCredentials()
|
|
161
|
+
: undefined,
|
|
162
|
+
beginConcealedInput: ctx.beginConcealedInput,
|
|
163
|
+
print: (text) => ctx.print(text),
|
|
164
|
+
},
|
|
165
|
+
proceed,
|
|
166
|
+
);
|
|
167
|
+
}
|
|
@@ -59,14 +59,14 @@ export const SANDBOX_EGRESS_ALLOWLIST_SYNTHETIC_SETTING: ConfigSetting = {
|
|
|
59
59
|
key: SANDBOX_EGRESS_ALLOWLIST_CONFIG_KEY,
|
|
60
60
|
type: 'string',
|
|
61
61
|
default: [],
|
|
62
|
-
description: 'Comma-separated command base names (or "*" for all) whose network access is re-enabled inside the per-command exec sandbox boundary, as a named escalation the approval flow surfaces ("wants network"). Empty = the sandbox disables network for every command. Only takes effect when sandbox.enabled is on, the
|
|
62
|
+
description: 'Comma-separated command base names (or "*" for all) whose network access is re-enabled inside the per-command exec sandbox boundary, as a named escalation the approval flow surfaces ("wants network"). Empty = the sandbox disables network for every command. Only takes effect when sandbox.enabled is on, the exec-sandbox setting is on, and bubblewrap is available (Linux only — reports unavailable elsewhere).',
|
|
63
63
|
};
|
|
64
64
|
|
|
65
65
|
export const SANDBOX_WORKSPACE_WRITABLE_SYNTHETIC_SETTING: ConfigSetting = {
|
|
66
66
|
key: SANDBOX_WORKSPACE_WRITABLE_CONFIG_KEY,
|
|
67
67
|
type: 'string',
|
|
68
68
|
default: [],
|
|
69
|
-
description: 'Comma-separated absolute paths outside the workspace bound writable into the per-command exec sandbox boundary, as a named escalation the approval flow surfaces ("wants path outside workspace"). Empty = only the workspace (and an isolated /tmp) is writable. Only takes effect when sandbox.enabled is on, the
|
|
69
|
+
description: 'Comma-separated absolute paths outside the workspace bound writable into the per-command exec sandbox boundary, as a named escalation the approval flow surfaces ("wants path outside workspace"). Empty = only the workspace (and an isolated /tmp) is writable. Only takes effect when sandbox.enabled is on, the exec-sandbox setting is on, and bubblewrap is available (Linux only — reports unavailable elsewhere).',
|
|
70
70
|
};
|
|
71
71
|
|
|
72
72
|
function buildSandboxExecListEntry(configManager: Pick<ConfigManager, 'get'>, setting: ConfigSetting): SettingEntry {
|
|
@@ -37,7 +37,7 @@ export class SelectionModal {
|
|
|
37
37
|
public allowSearch = true;
|
|
38
38
|
public customActions: Map<string, SelectionAction> = new Map();
|
|
39
39
|
/**
|
|
40
|
-
*
|
|
40
|
+
* vocab unification: overrides the default per-primaryAction Enter
|
|
41
41
|
* verb ("Select"/"Toggle"/"Edit"/"Delete") shown in the footer hint. Used by
|
|
42
42
|
* callers whose items are really commands (e.g. /help) so the hint reads
|
|
43
43
|
* "[Enter] Run", matching the slash-command palette's own verb, instead of
|
|
@@ -92,6 +92,11 @@ export function activateSelected(ctx: ActivateSelectedContext): void {
|
|
|
92
92
|
const idx = setting.enumValues.indexOf(entry.currentValue as string);
|
|
93
93
|
const nextIdx = (idx + 1) % setting.enumValues.length;
|
|
94
94
|
ctx.setValue(setting.key as ConfigKey, setting.enumValues[nextIdx]);
|
|
95
|
+
} else if (setting.type === 'object') {
|
|
96
|
+
// Object-typed keys (e.g. pricing.modelPrices) edit as JSON: seed the
|
|
97
|
+
// buffer with the current value's JSON so the edit starts from truth.
|
|
98
|
+
ctx.setEditingMode(true);
|
|
99
|
+
ctx.setEditBuffer(JSON.stringify(entry.currentValue ?? {}));
|
|
95
100
|
} else if (setting.type === 'string' || setting.type === 'number') {
|
|
96
101
|
// Enter inline edit mode
|
|
97
102
|
ctx.setEditingMode(true);
|
|
@@ -1,9 +1,8 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* settings-modal-adjustment — pure adjustment helpers for SettingsModal.
|
|
3
3
|
*
|
|
4
|
-
* These functions encapsulate the
|
|
4
|
+
* These functions encapsulate the directional-adjustment operation:
|
|
5
5
|
* - adjustSelected: cycle enum/boolean/number values via left/right arrow keys
|
|
6
|
-
* - toggleSelectedFlag: toggle a feature flag between enabled and disabled
|
|
7
6
|
*
|
|
8
7
|
* Each function takes its dependencies as explicit arguments rather than
|
|
9
8
|
* accessing class-level state directly, following the same pattern as
|
|
@@ -13,11 +12,9 @@
|
|
|
13
12
|
import type { ConfigKey } from '@pellux/goodvibes-sdk/platform/config';
|
|
14
13
|
import type { ConfigManager } from '@pellux/goodvibes-sdk/platform/config';
|
|
15
14
|
import type { FeatureFlagManager } from '@/runtime/index.ts';
|
|
16
|
-
import type { FlagState } from '@/runtime/index.ts';
|
|
17
15
|
import type { FlagEntry, McpEntry, SettingEntry } from './settings-modal-types.ts';
|
|
18
16
|
import { buildMcpEntries } from './settings-modal-data.ts';
|
|
19
17
|
import { getNumericAdjustmentMeta, roundToPrecision } from './settings-modal-behavior.ts';
|
|
20
|
-
import { applyFlagState } from './settings-modal-mutations.ts';
|
|
21
18
|
import type { McpRegistry } from '@pellux/goodvibes-sdk/platform/mcp';
|
|
22
19
|
|
|
23
20
|
// ---------------------------------------------------------------------------
|
|
@@ -45,10 +42,9 @@ export function adjustSelected(
|
|
|
45
42
|
): void {
|
|
46
43
|
if (ctx.editingMode) return;
|
|
47
44
|
|
|
48
|
-
// Feature-unit
|
|
49
|
-
//
|
|
50
|
-
//
|
|
51
|
-
// flag-toggle chokepoint. No category-special-case is needed here anymore.
|
|
45
|
+
// Feature-unit headers are the real config rows for their enablement keys
|
|
46
|
+
// (boolean or enum), so they fall through to the ordinary branches below —
|
|
47
|
+
// a plain config write the settings bridge forwards to the gate manager.
|
|
52
48
|
|
|
53
49
|
if (ctx.currentCategory === 'mcp') {
|
|
54
50
|
const entry = ctx.getSelectedMcp();
|
|
@@ -96,26 +92,3 @@ export function adjustSelected(
|
|
|
96
92
|
ctx.setValue(setting.key as ConfigKey, nextValue);
|
|
97
93
|
}
|
|
98
94
|
}
|
|
99
|
-
|
|
100
|
-
// ---------------------------------------------------------------------------
|
|
101
|
-
// toggleSelectedFlag
|
|
102
|
-
// ---------------------------------------------------------------------------
|
|
103
|
-
|
|
104
|
-
export interface ToggleSelectedFlagContext {
|
|
105
|
-
readonly featureFlagManager: FeatureFlagManager | null;
|
|
106
|
-
readonly configManager: ConfigManager | null;
|
|
107
|
-
getSelectedFlag(): FlagEntry | null;
|
|
108
|
-
}
|
|
109
|
-
|
|
110
|
-
export function toggleSelectedFlag(ctx: ToggleSelectedFlagContext): void {
|
|
111
|
-
const flagEntry = ctx.getSelectedFlag();
|
|
112
|
-
if (!flagEntry || !ctx.featureFlagManager || !ctx.configManager) return;
|
|
113
|
-
|
|
114
|
-
const { state } = flagEntry;
|
|
115
|
-
|
|
116
|
-
// Killed flags are blocked
|
|
117
|
-
if (state === 'killed') return;
|
|
118
|
-
|
|
119
|
-
const newState: FlagState = state === 'enabled' ? 'disabled' : 'enabled';
|
|
120
|
-
applyFlagState(flagEntry, newState, ctx.featureFlagManager, ctx.configManager);
|
|
121
|
-
}
|
|
@@ -52,16 +52,15 @@ import {
|
|
|
52
52
|
} from './settings-modal-types.ts';
|
|
53
53
|
import { enrichRelaySettingDescriptions } from './relay-settings-descriptions.ts';
|
|
54
54
|
import { applyFeatureUnitLayout } from './feature-unit-layout.ts';
|
|
55
|
+
import { FEATURE_SETTINGS_BY_ID } from '../runtime/feature-settings.ts';
|
|
55
56
|
|
|
56
57
|
// ---------------------------------------------------------------------------
|
|
57
58
|
// deepEqual — structural equality for isDefault comparisons
|
|
58
59
|
// ---------------------------------------------------------------------------
|
|
59
60
|
|
|
60
61
|
/**
|
|
61
|
-
* Structural equality
|
|
62
|
-
*
|
|
63
|
-
* circular references or non-plain prototypes — config defaults
|
|
64
|
-
* are always JSON-safe primitives, arrays, or plain objects.
|
|
62
|
+
* Structural equality for setting-default comparisons: scalars, arrays, and
|
|
63
|
+
* plain objects (config defaults are always JSON-safe; no circular refs).
|
|
65
64
|
*/
|
|
66
65
|
export function deepEqual(a: unknown, b: unknown): boolean {
|
|
67
66
|
if (a === b) return true;
|
|
@@ -98,9 +97,8 @@ export function buildSettingGroups(
|
|
|
98
97
|
featureFlagManager?: FeatureFlagManager | null,
|
|
99
98
|
): Map<SettingsCategory, SettingEntry[]> {
|
|
100
99
|
const groups = new Map<SettingsCategory, SettingEntry[]>();
|
|
101
|
-
// Every category starts empty
|
|
102
|
-
//
|
|
103
|
-
// namespaces) and is filled from CONFIG_SCHEMA below.
|
|
100
|
+
// Every category starts empty and is filled from CONFIG_SCHEMA below; the
|
|
101
|
+
// feature-unit layout pass then folds each capability's rows into one unit.
|
|
104
102
|
for (const cat of SETTINGS_CATEGORIES) groups.set(cat, []);
|
|
105
103
|
|
|
106
104
|
for (const setting of CONFIG_SCHEMA) {
|
|
@@ -124,7 +122,7 @@ export function buildSettingGroups(
|
|
|
124
122
|
}
|
|
125
123
|
}
|
|
126
124
|
|
|
127
|
-
//
|
|
125
|
+
// inject the synthetic display.themeMode enum (auto|dark|light). TUI-local
|
|
128
126
|
// key stored under the existing `display` section (see theme-mode-config.ts for why
|
|
129
127
|
// not `appearance`), same rationale as the other synthetic settings below.
|
|
130
128
|
const displayEntries = groups.get('display');
|
|
@@ -181,7 +179,7 @@ export function buildSettingGroups(
|
|
|
181
179
|
behaviorEntries.push(buildBudgetAlertUsdSyntheticEntry(configManager));
|
|
182
180
|
}
|
|
183
181
|
|
|
184
|
-
// Inject the
|
|
182
|
+
// Inject the alert-class toggles + master focus gate. TUI-local
|
|
185
183
|
// synthetic settings, same rationale as notifyAfterSeconds above.
|
|
186
184
|
if (behaviorEntries) {
|
|
187
185
|
for (const entry of buildNotifyAlertSyntheticEntries(configManager)) {
|
|
@@ -240,11 +238,10 @@ export function buildSettingGroups(
|
|
|
240
238
|
// the SDK's own descriptions don't carry (see relay-settings-descriptions.ts).
|
|
241
239
|
enrichRelaySettingDescriptions(groups);
|
|
242
240
|
|
|
243
|
-
// Feature-unit presentation:
|
|
244
|
-
// keys it tunes)
|
|
245
|
-
//
|
|
246
|
-
//
|
|
247
|
-
// flagless test path.
|
|
241
|
+
// Feature-unit presentation: each capability becomes ONE unit (its real
|
|
242
|
+
// enablement row + the settings keys it tunes) in its settings domain,
|
|
243
|
+
// sourced from FEATURE_SETTINGS. Runs AFTER synthetic injection so
|
|
244
|
+
// non-feature-owned rows stay orphans; skipped on the featureless path.
|
|
248
245
|
if (featureFlagManager) applyFeatureUnitLayout(groups, buildFlagEntries(featureFlagManager));
|
|
249
246
|
|
|
250
247
|
// learning.consolidation.* is a real SDK config domain now, so its keys arrive
|
|
@@ -301,7 +298,7 @@ export function buildTtsSpeedSyntheticEntry(configManager: Pick<ConfigManager, '
|
|
|
301
298
|
}
|
|
302
299
|
|
|
303
300
|
// ---------------------------------------------------------------------------
|
|
304
|
-
// display.themeMode synthetic setting (
|
|
301
|
+
// display.themeMode synthetic setting (light theme)
|
|
305
302
|
// ---------------------------------------------------------------------------
|
|
306
303
|
|
|
307
304
|
/**
|
|
@@ -415,11 +412,11 @@ export function buildBudgetAlertUsdSyntheticEntry(configManager: Pick<ConfigMana
|
|
|
415
412
|
}
|
|
416
413
|
|
|
417
414
|
// ---------------------------------------------------------------------------
|
|
418
|
-
//
|
|
415
|
+
// alert-class synthetic settings — behavior.notifyOn* + notifyOnlyWhenUnfocused
|
|
419
416
|
// ---------------------------------------------------------------------------
|
|
420
417
|
|
|
421
418
|
/**
|
|
422
|
-
* The five
|
|
419
|
+
* The five alert-gating booleans, all TUI-local (not yet in the SDK
|
|
423
420
|
* ConfigKey union), all defaulting to on. Read/written generically by
|
|
424
421
|
* core/alert-gating.ts (readBooleanConfig) and the per-alert-class modules
|
|
425
422
|
* (budget-breach-notifier.ts, approval-alert.ts, turn-event-wiring.ts,
|
|
@@ -475,7 +472,7 @@ export function buildNotifyAlertSyntheticEntries(configManager: Pick<ConfigManag
|
|
|
475
472
|
/**
|
|
476
473
|
* The OSC 9 in-terminal desktop-notification toggles + the audible bell.
|
|
477
474
|
* TUI-local synthetic booleans (not in the SDK ConfigKey union), each carrying
|
|
478
|
-
* its OWN default (unlike the always-on
|
|
475
|
+
* its OWN default (unlike the always-on alert toggles above): approval-wait
|
|
479
476
|
* on, turn-end/agent-blocked off, bell off. Read generically by
|
|
480
477
|
* core/terminal-notifier.ts (readBooleanConfig); this is only the
|
|
481
478
|
* settings-modal-visible surface. These are independent of the master
|
|
@@ -537,9 +534,13 @@ export function buildCodeIndexEnabledSyntheticEntry(configManager: Pick<ConfigMa
|
|
|
537
534
|
|
|
538
535
|
export function buildFlagEntries(featureFlagManager: FeatureFlagManager | null): FlagEntry[] {
|
|
539
536
|
if (!featureFlagManager) return [];
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
537
|
+
const entries: FlagEntry[] = [];
|
|
538
|
+
for (const { flag, state, persistedState, pendingRestart } of featureFlagManager.getAll().values()) {
|
|
539
|
+
const feature = FEATURE_SETTINGS_BY_ID.get(flag.id);
|
|
540
|
+
if (!feature) continue; // gate with no settings surface (registry/binding drift is SDK test-guarded)
|
|
541
|
+
entries.push({ feature, flag, state, persistedState, pendingRestart });
|
|
542
|
+
}
|
|
543
|
+
return entries;
|
|
543
544
|
}
|
|
544
545
|
|
|
545
546
|
// ---------------------------------------------------------------------------
|
|
@@ -1,17 +1,18 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* settings-modal-mutations — pure mutation helpers for SettingsModal.
|
|
3
3
|
*
|
|
4
|
-
* These functions encapsulate the side-effectful write operations
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
4
|
+
* These functions encapsulate the side-effectful write operations. Every
|
|
5
|
+
* write — feature-unit headers included — is a plain domain-settings config
|
|
6
|
+
* write; the SDK's settings bridge keeps the runtime gate manager in sync
|
|
7
|
+
* (flipping runtime-toggleable gates live and recording honest
|
|
8
|
+
* pending-restart markers for startup-gated ones). Each function takes its
|
|
9
|
+
* dependencies as explicit arguments rather than accessing class-level state.
|
|
8
10
|
*/
|
|
9
11
|
|
|
10
|
-
import type { ConfigKey
|
|
12
|
+
import type { ConfigKey } from '@pellux/goodvibes-sdk/platform/config';
|
|
11
13
|
import type { ConfigManager } from '@pellux/goodvibes-sdk/platform/config';
|
|
12
14
|
import { logger, summarizeError } from '@pellux/goodvibes-sdk/platform/utils';
|
|
13
15
|
import type { FeatureFlagManager } from '@/runtime/index.ts';
|
|
14
|
-
import type { FeatureFlag, FlagState } from '@/runtime/index.ts';
|
|
15
16
|
import type { FlagEntry, SettingEntry } from './settings-modal-types.ts';
|
|
16
17
|
import { deepEqual } from './settings-modal-data.ts';
|
|
17
18
|
|
|
@@ -83,10 +84,12 @@ export function applySettingValue({
|
|
|
83
84
|
};
|
|
84
85
|
}
|
|
85
86
|
|
|
86
|
-
// Update
|
|
87
|
+
// Update every entry carrying this key: cross-listed rows (the network
|
|
88
|
+
// combined view) and feature-unit headers that share one enablement key
|
|
89
|
+
// (e.g. both compaction features read behavior.compactionStrategy).
|
|
87
90
|
for (const entries of groups.values()) {
|
|
88
|
-
const entry
|
|
89
|
-
|
|
91
|
+
for (const entry of entries) {
|
|
92
|
+
if (entry.setting.key !== key) continue;
|
|
90
93
|
entry.currentValue = configManager.get(key);
|
|
91
94
|
entry.isDefault = deepEqual(entry.currentValue, entry.setting.default);
|
|
92
95
|
}
|
|
@@ -112,65 +115,6 @@ export function applySettingValue({
|
|
|
112
115
|
return { restartDomain, effectMessage, changed: previousValue !== value };
|
|
113
116
|
}
|
|
114
117
|
|
|
115
|
-
// ---------------------------------------------------------------------------
|
|
116
|
-
// persistFlagState — write a flag override to config
|
|
117
|
-
// ---------------------------------------------------------------------------
|
|
118
|
-
|
|
119
|
-
export function persistFlagState(
|
|
120
|
-
configManager: ConfigManager,
|
|
121
|
-
flagId: string,
|
|
122
|
-
newState: FlagState,
|
|
123
|
-
defaultState: FlagState,
|
|
124
|
-
): void {
|
|
125
|
-
if (newState === 'killed') return; // never persist killed state
|
|
126
|
-
|
|
127
|
-
try {
|
|
128
|
-
if (newState === defaultState) {
|
|
129
|
-
// Back at the default: the override must be REMOVED, not merged.
|
|
130
|
-
// getCategory clones and mergeCategory only sets keys, so the old
|
|
131
|
-
// delete-then-merge approach left the stale override on disk and the
|
|
132
|
-
// flag silently reloaded in the overridden state on the next start.
|
|
133
|
-
configManager.removeCategoryKey('featureFlags', flagId);
|
|
134
|
-
} else {
|
|
135
|
-
configManager.mergeCategory('featureFlags', { [flagId]: newState } as Record<string, PersistedFlagState>);
|
|
136
|
-
}
|
|
137
|
-
} catch (e) {
|
|
138
|
-
logger.error('SettingsModal: failed to persist flag state', { flagId, error: summarizeError(e) });
|
|
139
|
-
}
|
|
140
|
-
}
|
|
141
|
-
|
|
142
|
-
// ---------------------------------------------------------------------------
|
|
143
|
-
// applyFlagState — toggle a feature flag (runtime + persist)
|
|
144
|
-
// ---------------------------------------------------------------------------
|
|
145
|
-
|
|
146
|
-
export function applyFlagState(
|
|
147
|
-
flagEntry: FlagEntry,
|
|
148
|
-
newState: FlagState,
|
|
149
|
-
featureFlagManager: FeatureFlagManager,
|
|
150
|
-
configManager: ConfigManager,
|
|
151
|
-
): void {
|
|
152
|
-
const flag: FeatureFlag = flagEntry.flag;
|
|
153
|
-
|
|
154
|
-
try {
|
|
155
|
-
// Persist first so the on-disk override is the source of truth, then apply
|
|
156
|
-
// the same value to the live manager through applyConfigState. That single
|
|
157
|
-
// call keeps both paths consistent with the SDK's config→flag bridge:
|
|
158
|
-
// - runtime-toggleable flags apply immediately (firing flag subscribers
|
|
159
|
-
// exactly as a direct enable()/disable() would), and
|
|
160
|
-
// - startup-gated flags are NOT faked live — the effective state is left
|
|
161
|
-
// untouched and a pending-restart marker is recorded instead.
|
|
162
|
-
persistFlagState(configManager, flag.id, newState, flag.defaultState as FlagState);
|
|
163
|
-
featureFlagManager.applyConfigState(flag.id, newState);
|
|
164
|
-
} catch (e) {
|
|
165
|
-
logger.error('SettingsModal: failed to toggle feature flag', { flag: flag.id, error: summarizeError(e) });
|
|
166
|
-
}
|
|
167
|
-
|
|
168
|
-
// Reflect the manager's real state, never the requested target: for a
|
|
169
|
-
// startup-gated flag `state` stays at the unchanged effective value and
|
|
170
|
-
// `pendingRestart` reports that a restart is needed for `persistedState`.
|
|
171
|
-
syncFlagEntryFromManager(flagEntry, featureFlagManager);
|
|
172
|
-
}
|
|
173
|
-
|
|
174
118
|
/**
|
|
175
119
|
* Refresh a FlagEntry's live/persisted/pending fields from the manager's
|
|
176
120
|
* authoritative snapshot so the settings UI never displays a guessed state.
|