authorgravatar for julius0216@outlook.comJulius Marminge <julius0216@outlook.com> 2026-10-08 14:18:17-07:00
committergravatar for noreply@github.comGitHub <noreply@github.com> 2026-10-08 14:18:17-07:00
log2c1160783ea4b4dcc1c1f1ab04639ffc0aaa289d
tree5ac0dbc9259df0bfabebc8f285ddab66e3360ad9
parent9eea28bacd27b2ee6d5183e195c397bba8b515a0
signature Signed by PGP key B5690EEEBB952194

refactor(settings): drop the legacy per-driver providers map (#17300)

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>

32 files changed, 602 insertions(+), 1073 deletions(-)

apps/server/src/orchestration-v2/CursorOrchestratorV2.live.test.ts+7-2
......@@ -7,6 +7,8 @@ import {
77 CommandId,
88 MessageId,
99 ProjectId,
10 ProviderDriverKind,
11 ProviderInstanceId,
1012 ThreadId,
1113 type OrchestrationV2ThreadProjection,
1214} from "@t3tools/contracts";
......@@ -60,8 +62,11 @@ const layerVcsDriverRegistry = VcsDriverRegistry.layer.pipe(
6062const layerCheckpointStore = CheckpointStore.layer.pipe(Layer.provide(layerVcsDriverRegistry));
6163
6264const layerServerSettings = ServerSettings.layerTest({
63 providers: {
64 cursor: { enabled: true },
65 providerInstances: {
66 [ProviderInstanceId.make("cursor")]: {
67 driver: ProviderDriverKind.make("cursor"),
68 enabled: true,
69 },
6570 },
6671});
6772const layerBackgroundPolicy = BackgroundPolicy.layer.pipe(
apps/server/src/orchestration-v2/GrokOrchestratorV2.live.test.ts+7-2
......@@ -8,6 +8,8 @@ import {
88 MessageId,
99 type OrchestrationV2ThreadProjection,
1010 ProjectId,
11 ProviderDriverKind,
12 ProviderInstanceId,
1113 ThreadId,
1214} from "@t3tools/contracts";
1315import * as Console from "effect/Console";
......@@ -59,8 +61,11 @@ const layerVcsDriverRegistry = VcsDriverRegistry.layer.pipe(
5961const layerCheckpointStore = CheckpointStore.layer.pipe(Layer.provide(layerVcsDriverRegistry));
6062
6163const layerServerSettings = ServerSettings.layerTest({
62 providers: {
63 grok: { enabled: true },
64 providerInstances: {
65 [ProviderInstanceId.make("grok")]: {
66 driver: ProviderDriverKind.make("grok"),
67 enabled: true,
68 },
6469 },
6570});
6671const layerBackgroundPolicy = BackgroundPolicy.layer.pipe(
apps/server/src/project/AgentSessionScanner.test.ts+13-7
......@@ -59,13 +59,19 @@ const layerScannerTest = (input: ScannerTestInput) =>
5959 Layer.provide(
6060 Layer.mergeAll(
6161 ServerSettings.layerTest({
62 providers: {
63 claudeAgent: { homePath: input.claudeHomePath },
64 codex: { homePath: input.codexHomePath },
62 // Default slots point at the fixture homes; explicit test instances
63 // replace a slot with the same id.
64 providerInstances: {
65 [ProviderInstanceId.make("claudeAgent")]: {
66 driver: ProviderDriverKind.make("claudeAgent"),
67 config: { homePath: input.claudeHomePath },
68 },
69 [ProviderInstanceId.make("codex")]: {
70 driver: ProviderDriverKind.make("codex"),
71 config: { homePath: input.codexHomePath },
72 },
73 ...input.providerInstances,
6574 },
66 ...(input.providerInstances === undefined
67 ? {}
68 : { providerInstances: input.providerInstances }),
6975 }),
7076 ServerConfig.layerTest(
7177 input.claudeHomePath,
......@@ -533,7 +539,7 @@ it.layer(NodeServices.layer)("AgentSessionScanner", (it) => {
533539 }),
534540 );
535541
536 it.effect("uses explicit provider instance homes instead of overridden legacy homes", () =>
542 it.effect("uses explicit default-slot homes instead of the fixture homes", () =>
537543 Effect.gen(function* () {
538544 const path = yield* Path.Path;
539545 const claudeHomePath = yield* makeTempDir("t3code-claude-legacy-");
apps/server/src/project/AgentSessionScanner.ts+6-7
......@@ -1102,16 +1102,15 @@ export const make = Effect.gen(function* () {
11021102 instanceId: ProviderInstanceId.make(instanceId),
11031103 config,
11041104 }));
1105 // The built-in default instance runs with default config when settings
1106 // have no entry for it.
11051107 if (!Object.hasOwn(settings.providerInstances, source)) {
1106 const legacyInstance = {
1108 const defaultInstance = {
11071109 instanceId: ProviderInstanceId.make(source),
1108 config: {
1109 driver: ProviderDriverKind.make(source),
1110 config: settings.providers[source],
1111 },
1110 config: { driver: ProviderDriverKind.make(source) },
11121111 };
1113 if (resolveProviderInstanceEnabled(legacyInstance.config)) {
1114 instances.push(legacyInstance);
1112 if (resolveProviderInstanceEnabled(defaultInstance.config)) {
1113 instances.push(defaultInstance);
11151114 }
11161115 }
11171116
apps/server/src/provider/Drivers/AcpRegistryDriver.ts+1
......@@ -467,6 +467,7 @@ export const AcpRegistryDriver: ProviderDriver<AcpRegistrySettings, AcpRegistryD
467467 metadata: {
468468 displayName: "ACP Registry",
469469 supportsMultipleInstances: true,
470 hasDefaultInstance: false,
470471 },
471472 configSchema: AcpRegistrySettings,
472473 defaultConfig: () => decodeSettings({}),
apps/server/src/provider/ProviderInstanceRegistryHydration.ts+10-54
......@@ -2,28 +2,10 @@
22 * ProviderInstanceRegistryHydration — derive a `ProviderInstanceConfigMap`
33 * from `ServerSettings` and keep `ProviderInstanceRegistry` in sync with it.
44 *
5 * The server still reads two shapes:
6 *
7 * 1. `settings.providerInstances` — the new driver-agnostic map the
8 * registry expects. Keyed by `ProviderInstanceId`, values are
9 * `ProviderInstanceConfig` envelopes.
10 * 2. `settings.providers.<kind>` — the legacy single-instance-per-driver
11 * fields (`providers.codex`, `providers.claudeAgent`, …). These are
12 * the source of truth for every deployment that hasn't been migrated
13 * yet to an explicit `providerInstances` entry.
14 *
15 * This module bridges (2) into (1) and wires the resulting map into a
16 * mutable registry. For every built-in driver whose id is not already
17 * present in `providerInstances` (keyed on
18 * `defaultInstanceIdForDriver(driverKind)` — literally the driver kind as a
19 * routing slug), we synthesize an envelope from the legacy field. The
20 * registry decodes both flavours through the same `configSchema` and ends
21 * up with one uniform `ProviderInstance` per entry.
22 *
23 * Explicit `providerInstances` entries always win — users can already
24 * override the legacy `providers.<kind>` blob by authoring a
25 * `providerInstances.codex` entry with a matching driver, and we don't
26 * want the synthesized envelope to silently stomp their config.
5 * `settings.providerInstances` is the source of truth. Every built-in driver
6 * with a default instance also runs at `defaultInstanceIdForDriver(kind)`
7 * when that slot has no entry, using the driver's default config, so a fresh
8 * install shows its built-in providers without writing settings first.
279 *
2810 * Hot-reload
2911 * ----------
......@@ -76,17 +58,9 @@ type ProviderInstanceRegistryHydrationEnv =
7658 | ServerConfig.ServerConfig;
7759
7860/**
79 * Synthesize a `ProviderInstanceConfigMap` from a `ServerSettings` snapshot.
80 *
81 * Strategy:
82 * 1. Copy all explicit `settings.providerInstances` entries verbatim.
83 * 2. For each built-in driver whose `defaultInstanceIdForDriver(id)` key
84 * is *not* already in the explicit map, synthesize an entry from the
85 * matching legacy `settings.providers.<kind>` blob.
86 *
87 * The returned map is the input the registry consumes; pure & exported
88 * separately so the hydration logic can be exercised by unit tests
89 * without layering.
61 * Explicit `providerInstances` entries plus an implicit default instance for
62 * each built-in driver whose default slot is empty. Pure so the hydration
63 * rule can be tested without layers.
9064 */
9165export const deriveProviderInstanceConfigMap = (
9266 settings: ServerSettings,
......@@ -94,28 +68,10 @@ export const deriveProviderInstanceConfigMap = (
9468 const merged: Record<string, ProviderInstanceConfig> = { ...settings.providerInstances };
9569
9670 for (const driver of BUILT_IN_DRIVERS) {
71 if (driver.metadata.hasDefaultInstance === false) continue;
9772 const instanceId = defaultInstanceIdForDriver(driver.driverKind);
98 if (instanceId in merged) {
99 // Explicit `providerInstances` entry for this slot — user-authored
100 // config always wins over the legacy mirror.
101 continue;
102 }
103
104 // Only built-in drivers have a legacy mirror; the registry's
105 // `providers` struct is keyed on the same literal slug as
106 // `driverKind`. Access is dynamic (the driver kind is a branded string),
107 // but it's constrained to `keyof settings.providers` by the union of
108 // built-in driver kinds.
109 const legacyKey = driver.driverKind as keyof ServerSettings["providers"];
110 const legacyConfig = settings.providers[legacyKey];
111 if (legacyConfig === undefined) {
112 continue;
113 }
114
115 merged[instanceId] = {
116 driver: driver.driverKind,
117 config: legacyConfig,
118 };
73 if (instanceId in merged) continue;
74 merged[instanceId] = { driver: driver.driverKind };
11975 }
12076
12177 return merged as ProviderInstanceConfigMap;
apps/server/src/provider/ProviderRegistry.test.ts+33-35
......@@ -68,6 +68,17 @@ const decodeServerSettings = Schema.decodeSync(ServerSettings);
6868const encodeServerSettings = Schema.encodeSync(ServerSettings);
6969const encodedDefaultServerSettings = encodeServerSettings(DEFAULT_SERVER_SETTINGS);
7070
71/** Default slots that settings mark disabled, so their probes never spawn. */
72const disabledDefaultSlots = (
73 ...drivers: ReadonlyArray<string>
74): ContractServerSettings["providerInstances"] =>
75 Object.fromEntries(
76 drivers.map((driver) => [
77 ProviderInstanceId.make(driver),
78 { driver: ProviderDriverKind.make(driver), enabled: false },
79 ]),
80 );
81
7182const defaultClaudeSettings: ClaudeSettings = Schema.decodeSync(ClaudeSettings)({});
7283const defaultCodexSettings: CodexSettings = Schema.decodeSync(CodexSettings)({});
7384const decodeCodexSettings = Schema.decodeSync(CodexSettings);
......@@ -2663,24 +2674,18 @@ it.layer(
26632674 const serverSettings = yield* makeMutableServerSettingsService(
26642675 decodeServerSettings(
26652676 deepMerge(encodedDefaultServerSettings, {
2666 providers: {
2667 // Disable every built-in probe that would otherwise spawn
2668 // on the CI host. `enabled: false` short-circuits each
2669 // driver's probe *before* it touches the spawner, so the
2670 // test environment stays isolated from the dev
2671 // machine's PATH.
2672 codex: { enabled: false },
2673 claudeAgent: { enabled: false },
2674 cursor: { enabled: false },
2675 grok: { enabled: false },
2676 opencode: { enabled: false },
2677 },
26782677 // `providerInstances` keys are branded `ProviderInstanceId`;
26792678 // the branded index signature rejects plain string literals
26802679 // at the TS level even though the runtime schema happily
26812680 // accepts + decodes them. Cast the patch to `unknown` so
26822681 // the `Schema.decodeSync` below does the real validation.
26832682 providerInstances: {
2683 // Disable every built-in probe that would otherwise spawn
2684 // on the CI host. `enabled: false` short-circuits each
2685 // driver's probe *before* it touches the spawner, so the
2686 // test environment stays isolated from the dev
2687 // machine's PATH.
2688 ...disabledDefaultSlots("codex", "claudeAgent", "cursor", "grok", "opencode"),
26842689 // Matches the shape the user had in `.t3/dev/settings.json`
26852690 // when the bug was reported: a custom enabled Codex instance
26862691 // pointing at a binary the server has to actually spawn.
......@@ -2781,12 +2786,13 @@ it.layer(
27812786 const mutableServerSettings = yield* makeMutableServerSettingsService(
27822787 decodeServerSettings(
27832788 deepMerge(encodedDefaultServerSettings, {
2784 providers: {
2785 codex: { enabled: true, binaryPath: firstMissing },
2786 claudeAgent: { enabled: false },
2787 cursor: { enabled: false },
2788 grok: { enabled: false },
2789 opencode: { enabled: false },
2789 providerInstances: {
2790 ...disabledDefaultSlots("claudeAgent", "cursor", "grok", "opencode"),
2791 [ProviderInstanceId.make("codex")]: {
2792 driver: ProviderDriverKind.make("codex"),
2793 enabled: true,
2794 config: { binaryPath: firstMissing },
2795 },
27902796 },
27912797 }),
27922798 ),
......@@ -2871,8 +2877,13 @@ it.layer(
28712877 ),
28722878 );
28732879 yield* serverSettings.updateSettings({
2874 providers: {
2875 codex: { enabled: true, binaryPath: secondMissing },
2880 providerInstances: {
2881 ...disabledDefaultSlots("claudeAgent", "cursor", "grok", "opencode"),
2882 [ProviderInstanceId.make("codex")]: {
2883 driver: ProviderDriverKind.make("codex"),
2884 enabled: true,
2885 config: { binaryPath: secondMissing },
2886 },
28762887 },
28772888 });
28782889 // Start the lazy stream only after publishing. A watcher that did
......@@ -2900,14 +2911,8 @@ it.layer(
29002911 const serverSettings = yield* makeMutableServerSettingsService(
29012912 decodeServerSettings(
29022913 deepMerge(encodedDefaultServerSettings, {
2903 providers: {
2904 codex: { enabled: false },
2905 claudeAgent: { enabled: false },
2906 cursor: { enabled: false },
2907 grok: { enabled: false },
2908 opencode: { enabled: false },
2909 },
29102914 providerInstances: {
2915 ...disabledDefaultSlots("codex", "claudeAgent", "cursor", "grok", "opencode"),
29112916 ghost_main: {
29122917 driver: "ghostDriver",
29132918 displayName: "A fork-only driver we don't ship",
......@@ -2971,14 +2976,7 @@ it.layer(
29712976 const serverSettings = yield* makeMutableServerSettingsService(
29722977 decodeServerSettings(
29732978 deepMerge(encodedDefaultServerSettings, {
2974 providers: {
2975 codex: {
2976 enabled: false,
2977 },
2978 grok: {
2979 enabled: false,
2980 },
2981 },
2979 providerInstances: disabledDefaultSlots("codex", "grok"),
29822980 }),
29832981 ),
29842982 );
apps/server/src/provider/providerInstallation.test.ts+24-3
......@@ -147,7 +147,14 @@ describe("provider installation routing", () => {
147147 it.effect("keeps external installs manual without hiding shared install status", () =>
148148 Effect.gen(function* () {
149149 const harness = yield* makeHarness({
150 settings: { providers: { antigravity: { binaryPath: "/external/agy" } } },
150 settings: {
151 providerInstances: {
152 [ProviderInstanceId.make("antigravity")]: {
153 driver: ProviderDriverKind.make("antigravity"),
154 config: { binaryPath: "/external/agy" },
155 },
156 },
157 },
151158 });
152159 const start = yield* Effect.flip(harness.router.start({ instanceId }));
153160 const remove = yield* Effect.flip(harness.router.remove({ instanceId }));
......@@ -164,7 +171,14 @@ describe("provider installation routing", () => {
164171 const codexId = ProviderInstanceId.make("codex");
165172 const harness = yield* makeHarness({
166173 instance: instance(ProviderDriverKind.make("codex"), codexId),
167 settings: { providers: { codex: { setupMode: "managed" } } },
174 settings: {
175 providerInstances: {
176 [ProviderInstanceId.make("codex")]: {
177 driver: ProviderDriverKind.make("codex"),
178 config: { setupMode: "managed" },
179 },
180 },
181 },
168182 });
169183 assert.equal((yield* harness.router.start({ instanceId: codexId })).driver, "codex");
170184 yield* harness.router.cancel({ instanceId: codexId, operationId: "operation" });
......@@ -180,7 +194,14 @@ describe("provider installation routing", () => {
180194 const codexId = ProviderInstanceId.make("codex");
181195 const harness = yield* makeHarness({
182196 instance: instance(ProviderDriverKind.make("codex"), codexId),
183 settings: { providers: { codex: { setupMode: "existing" } } },
197 settings: {
198 providerInstances: {
199 [ProviderInstanceId.make("codex")]: {
200 driver: ProviderDriverKind.make("codex"),
201 config: { setupMode: "existing" },
202 },
203 },
204 },
184205 });
185206 assert.include(
186207 (yield* Effect.flip(harness.router.start({ instanceId: codexId }))).detail,
apps/server/src/server.ts+1-2
......@@ -583,8 +583,7 @@ const layerRuntimeCoreDependenciesBase = Layer.mergeAll(
583583 // The instance registry is the new routing keystone — text generation,
584584 // adapter lookup, and runtime ingestion all resolve `ProviderInstanceId`
585585 // through this layer. Built-in drivers come from `BUILT_IN_DRIVERS`;
586 // `providerInstances` hydration merges `settings.providers.<kind>`
587 // with explicit `providerInstances` entries on boot.
586 // hydration adds their default instances to `providerInstances` on boot.
588587 Layer.provideMerge(ProviderInstanceRegistryHydration.layer),
589588 Layer.provideMerge(
590589 Layer.mergeAll(
apps/server/src/serverSettings.test.ts+107-306
......@@ -327,13 +327,6 @@ it.layer(NodeServices.layer)("server settings", (it) => {
327327
328328 it.effect("decodes nested settings patches", () =>
329329 Effect.gen(function* () {
330 assert.deepEqual(
331 yield* decodeSettingsPatch({ providers: { codex: { binaryPath: "/tmp/codex" } } }),
332 {
333 providers: { codex: { binaryPath: "/tmp/codex" } },
334 },
335 );
336
337330 assert.deepEqual(
338331 yield* decodeSettingsPatch({
339332 textGenerationModelSelection: {
......@@ -374,16 +367,7 @@ it.layer(NodeServices.layer)("server settings", (it) => {
374367 const serverSettings = yield* ServerSettingsModule.ServerSettingsService;
375368
376369 yield* serverSettings.updateSettings({
377 providers: {
378 codex: {
379 binaryPath: "/usr/local/bin/codex",
380 homePath: "/Users/julius/.codex",
381 },
382 claudeAgent: {
383 binaryPath: "/usr/local/bin/claude",
384 customModels: ["claude-custom"],
385 },
386 },
370 observability: { otlpTracesUrl: "http://localhost:4318/v1/traces" },
387371 textGenerationModelSelection: {
388372 instanceId: ProviderInstanceId.make("codex"),
389373 model: DEFAULT_SERVER_SETTINGS.textGenerationModelSelection.model,
......@@ -399,32 +383,14 @@ it.layer(NodeServices.layer)("server settings", (it) => {
399383 });
400384
401385 const next = yield* serverSettings.updateSettings({
402 providers: {
403 codex: {
404 binaryPath: "/opt/homebrew/bin/codex",
405 },
406 },
386 observability: { otlpMetricsUrl: "http://localhost:4318/v1/metrics" },
407387 textGenerationModelSelection: {
408388 options: [{ id: "fastMode", value: false }],
409389 },
410390 });
411391
412 assert.deepEqual(next.providers.codex, {
413 enabled: true,
414 binaryPath: "/opt/homebrew/bin/codex",
415 homePath: "/Users/julius/.codex",
416 shadowHomePath: "",
417 launchArgs: "",
418 customModels: [],
419 });
420 assert.deepEqual(next.providers.claudeAgent, {
421 enabled: true,
422 binaryPath: "/usr/local/bin/claude",
423 homePath: "",
424 customModels: ["claude-custom"],
425 launchArgs: "",
426 autoCompactWindow: "",
427 });
392 assert.equal(next.observability.otlpTracesUrl, "http://localhost:4318/v1/traces");
393 assert.equal(next.observability.otlpMetricsUrl, "http://localhost:4318/v1/metrics");
428394 assert.deepEqual(
429395 next.textGenerationModelSelection,
430396 createModelSelection(
......@@ -521,18 +487,11 @@ it.layer(NodeServices.layer)("server settings", (it) => {
521487 const changes = yield* serverSettings.subscribeChanges;
522488
523489 yield* serverSettings.updateSettings({
524 providers: {
525 codex: {
526 binaryPath: "/usr/local/bin/codex-next",
527 },
528 },
490 addProjectBaseDirectory: "~/next",
529491 });
530492
531493 const firstChange = yield* changes.pipe(Stream.runHead, Effect.timeout("1 second"));
532 assert.equal(
533 Option.getOrUndefined(firstChange)?.providers.codex.binaryPath,
534 "/usr/local/bin/codex-next",
535 );
494 assert.equal(Option.getOrUndefined(firstChange)?.addProjectBaseDirectory, "~/next");
536495 }),
537496 ).pipe(Effect.provide(layerServerSettings())),
538497 );
......@@ -654,19 +613,18 @@ it.layer(NodeServices.layer)("server settings", (it) => {
654613 );
655614
656615 it.effect(
657 "uses explicit provider instance enabled state over legacy provider enabled state",
616 "keeps a custom instance's selection when the driver's default instance is disabled",
658617 () =>
659618 Effect.gen(function* () {
660619 const serverSettings = yield* ServerSettingsModule.ServerSettingsService;
661620 const instanceId = ProviderInstanceId.make("claude_openrouter");
662621
663622 const next = yield* serverSettings.updateSettings({
664 providers: {
665 claudeAgent: {
623 providerInstances: {
624 [ProviderInstanceId.make("claudeAgent")]: {
625 driver: ProviderDriverKind.make("claudeAgent"),
666626 enabled: false,
667627 },
668 },
669 providerInstances: {
670628 [instanceId]: {
671629 driver: ProviderDriverKind.make("claudeAgent"),
672630 enabled: true,
......@@ -700,6 +658,10 @@ it.layer(NodeServices.layer)("server settings", (it) => {
700658 const fallbackInstanceId = ProviderInstanceId.make(fallbackId);
701659 const selection = { instanceId: writerId, model: "claude-sonnet-4-6" };
702660 const providerInstances = {
661 [ProviderInstanceId.make("claudeAgent")]: {
662 driver: ProviderDriverKind.make("claudeAgent"),
663 enabled: false,
664 },
703665 [fallbackInstanceId]: {
704666 driver: ProviderDriverKind.make("codex"),
705667 enabled: true,
......@@ -713,11 +675,6 @@ it.layer(NodeServices.layer)("server settings", (it) => {
713675 };
714676
715677 yield* serverSettings.updateSettings({
716 providers: Object.fromEntries(
717 Object.keys(DEFAULT_SERVER_SETTINGS.providers).map(
718 (provider) => [provider, { enabled: false }] as const,
719 ),
720 ),
721678 providerInstances,
722679 textGenerationModelSelection: selection,
723680 });
......@@ -754,7 +711,7 @@ it.layer(NodeServices.layer)("server settings", (it) => {
754711 ).pipe(Effect.provide(layerServerSettings())),
755712 );
756713
757 it.effect("skips explicitly disabled instances when choosing a legacy fallback", () =>
714 it.effect("skips explicitly disabled default instances when choosing a fallback", () =>
758715 Effect.gen(function* () {
759716 const serverSettings = yield* ServerSettingsModule.ServerSettingsService;
760717 const next = yield* serverSettings.updateSettings({
......@@ -933,242 +890,188 @@ it.layer(NodeServices.layer)("server settings", (it) => {
933890 }).pipe(Effect.provide(layerServerSettings())),
934891 );
935892
936 it.effect("enables previously used providers from sparse settings files", () =>
893 it.effect("moves customized legacy provider settings into default instances on load", () =>
937894 Effect.gen(function* () {
938895 const serverConfig = yield* ServerConfig.ServerConfig;
939896 const fileSystem = yield* FileSystem.FileSystem;
940897 const serverSettings = yield* ServerSettingsModule.ServerSettingsService;
941898 yield* fileSystem.writeFileString(
942899 serverConfig.settingsPath,
943 '{"providers":{"opencode":{"serverUrl":"http://127.0.0.1:4096"}}}',
900 JSON.stringify({
901 addProjectBaseDirectory: "~/Development",
902 providers: {
903 codex: { binaryPath: "/opt/guard/bin/codex" },
904 claudeAgent: { enabled: false },
905 cursor: { enabled: false },
906 grok: { enabled: false },
907 opencode: { enabled: false },
908 },
909 }),
944910 );
945 yield* recordProviderUsage("opencode");
946911
947912 const settings = yield* serverSettings.getSettings;
948913
949 assert.isFalse(settings.providers.grok.enabled);
950 assert.isTrue(settings.providers.opencode.enabled);
951 assert.isFalse(settings.providers.cursor.enabled);
952 assert.equal(settings.providers.opencode.serverUrl, "http://127.0.0.1:4096");
914 assert.deepEqual(settings.providerInstances, {
915 [ProviderInstanceId.make("codex")]: {
916 driver: ProviderDriverKind.make("codex"),
917 config: { binaryPath: "/opt/guard/bin/codex" },
918 },
919 [ProviderInstanceId.make("claudeAgent")]: {
920 driver: ProviderDriverKind.make("claudeAgent"),
921 enabled: false,
922 config: {},
923 },
924 });
925 // The file is rewritten once: instances persist and the retired map is gone.
926 const persisted = JSON.parse(yield* fileSystem.readFileString(serverConfig.settingsPath));
927 assert.isUndefined(persisted.providers);
928 assert.equal(persisted.addProjectBaseDirectory, "~/Development");
929 assert.deepEqual(persisted.providerInstances.codex, {
930 driver: "codex",
931 config: { binaryPath: "/opt/guard/bin/codex" },
932 });
953933 }).pipe(Effect.provide(layerServerSettings())),
954934 );
955935
956 it.effect("preserves existing provider instances without explicit enabled flags", () =>
936 it.effect("keeps an explicit default instance over the legacy blob for its driver", () =>
957937 Effect.gen(function* () {
958938 const serverConfig = yield* ServerConfig.ServerConfig;
959939 const fileSystem = yield* FileSystem.FileSystem;
960940 const serverSettings = yield* ServerSettingsModule.ServerSettingsService;
961941 yield* fileSystem.writeFileString(
962942 serverConfig.settingsPath,
963 '{"providerInstances":{"cursor_work":{"driver":"cursor","config":{}},"grok":{"driver":"grok","config":{}},"opencode_work":{"driver":"opencode","config":{"serverUrl":"http://127.0.0.1:4096"}},"opencode_unused":{"driver":"opencode","config":{}}}}',
943 JSON.stringify({
944 providers: { codex: { binaryPath: "/legacy/codex" } },
945 providerInstances: {
946 codex: { driver: "codex", enabled: true, config: { binaryPath: "/explicit/codex" } },
947 },
948 }),
964949 );
965 yield* recordProviderUsage("cursor", "cursor_work");
966 yield* recordProviderUsage("grok", null);
967 yield* recordProviderUsage("opencode", "opencode_work");
968950
969951 const settings = yield* serverSettings.getSettings;
970952
971 assert.isTrue(settings.providers.cursor.enabled);
972 assert.isTrue(settings.providerInstances[ProviderInstanceId.make("cursor_work")]?.enabled);
973 assert.isTrue(settings.providerInstances[ProviderInstanceId.make("grok")]?.enabled);
974 assert.isTrue(settings.providerInstances[ProviderInstanceId.make("opencode_work")]?.enabled);
975 const unused = settings.providerInstances[ProviderInstanceId.make("opencode_unused")];
976 assert.isDefined(unused);
977 assert.isFalse(resolveProviderInstanceEnabled(unused));
953 assert.deepEqual(settings.providerInstances[ProviderInstanceId.make("codex")], {
954 driver: ProviderDriverKind.make("codex"),
955 enabled: true,
956 config: { binaryPath: "/explicit/codex" },
957 });
978958 }).pipe(Effect.provide(layerServerSettings())),
979959 );
980960
981 it.effect("preserves explicit provider disables in existing settings files", () =>
961 it.effect("enables previously used optional providers while migrating legacy settings", () =>
982962 Effect.gen(function* () {
983963 const serverConfig = yield* ServerConfig.ServerConfig;
984964 const fileSystem = yield* FileSystem.FileSystem;
985965 const serverSettings = yield* ServerSettingsModule.ServerSettingsService;
986966 yield* fileSystem.writeFileString(
987967 serverConfig.settingsPath,
988 '{"providers":{"grok":{"enabled":false},"opencode":{"enabled":false},"cursor":{"enabled":false}},"providerInstances":{"grok":{"driver":"grok","enabled":false,"config":{}},"opencode":{"driver":"opencode","config":{"enabled":false}},"cursor":{"driver":"cursor","enabled":false,"config":{}}}}',
968 JSON.stringify({
969 providers: { opencode: { serverUrl: "http://127.0.0.1:4096" }, cursor: {} },
970 providerInstances: {
971 cursor_work: { driver: "cursor", config: {} },
972 opencode_unused: { driver: "opencode", config: {} },
973 },
974 }),
989975 );
990 yield* recordProviderUsage("grok");
991976 yield* recordProviderUsage("opencode");
992 yield* recordProviderUsage("cursor");
977 yield* recordProviderUsage("grok", null);
978 yield* recordProviderUsage("cursor", "cursor_work");
993979
994980 const settings = yield* serverSettings.getSettings;
995981
996 assert.isFalse(settings.providers.grok.enabled);
997 assert.isFalse(settings.providers.opencode.enabled);
998 assert.isFalse(settings.providers.cursor.enabled);
999 assert.isFalse(settings.providerInstances[ProviderInstanceId.make("grok")]?.enabled);
1000 assert.isFalse(settings.providerInstances[ProviderInstanceId.make("opencode")]?.enabled);
1001 assert.isFalse(settings.providerInstances[ProviderInstanceId.make("cursor")]?.enabled);
982 assert.deepEqual(settings.providerInstances[ProviderInstanceId.make("opencode")], {
983 driver: ProviderDriverKind.make("opencode"),
984 enabled: true,
985 config: { serverUrl: "http://127.0.0.1:4096" },
986 });
987 // Used without any legacy blob or instance: the slot is created enabled.
988 assert.isTrue(settings.providerInstances[ProviderInstanceId.make("grok")]?.enabled);
989 assert.isTrue(settings.providerInstances[ProviderInstanceId.make("cursor_work")]?.enabled);
990 // Using any cursor instance counts as opting into the driver.
991 assert.isTrue(settings.providerInstances[ProviderInstanceId.make("cursor")]?.enabled);
992 const unused = settings.providerInstances[ProviderInstanceId.make("opencode_unused")];
993 assert.isDefined(unused);
994 assert.isFalse(resolveProviderInstanceEnabled(unused));
1002995 }).pipe(Effect.provide(layerServerSettings())),
1003996 );
1004997
1005 it.effect("skips a disabled provider instance when picking the text generation fallback", () =>
998 it.effect("keeps explicit legacy disables even when provider history shows use", () =>
1006999 Effect.gen(function* () {
10071000 const serverConfig = yield* ServerConfig.ServerConfig;
10081001 const fileSystem = yield* FileSystem.FileSystem;
10091002 const serverSettings = yield* ServerSettingsModule.ServerSettingsService;
1010 // The Providers UI writes providerInstances only, so the legacy providers
1011 // map decodes to defaults where codex is enabled and listed first.
10121003 yield* fileSystem.writeFileString(
10131004 serverConfig.settingsPath,
1014 '{"providerInstances":{"codex":{"driver":"codex","enabled":false,"config":{}}}}',
1005 JSON.stringify({ providers: { grok: { enabled: false, binaryPath: "/opt/grok" } } }),
10151006 );
1007 yield* recordProviderUsage("grok");
10161008
10171009 const settings = yield* serverSettings.getSettings;
10181010
1019 assert.equal(settings.textGenerationModelSelection.instanceId, "claudeAgent");
1011 assert.deepEqual(settings.providerInstances[ProviderInstanceId.make("grok")], {
1012 driver: ProviderDriverKind.make("grok"),
1013 enabled: false,
1014 config: { binaryPath: "/opt/grok" },
1015 });
10201016 }).pipe(Effect.provide(layerServerSettings())),
10211017 );
10221018
1023 it.effect("keeps unused providers disabled in existing sparse settings files", () =>
1019 it.effect("runs provider history only for files that still carry the legacy map", () =>
10241020 Effect.gen(function* () {
10251021 const serverConfig = yield* ServerConfig.ServerConfig;
10261022 const fileSystem = yield* FileSystem.FileSystem;
10271023 const serverSettings = yield* ServerSettingsModule.ServerSettingsService;
10281024 yield* fileSystem.writeFileString(serverConfig.settingsPath, "{}");
1029
1030 const settings = yield* serverSettings.getSettings;
1031
1032 assert.isFalse(settings.providers.grok.enabled);
1033 assert.isFalse(settings.providers.opencode.enabled);
1034 assert.isFalse(settings.providers.cursor.enabled);
1035 }).pipe(Effect.provide(layerServerSettings())),
1036 );
1037
1038 it.effect("preserves provider history when no settings file exists", () =>
1039 Effect.gen(function* () {
1040 const serverSettings = yield* ServerSettingsModule.ServerSettingsService;
10411025 yield* recordProviderUsage("grok");
10421026
10431027 const settings = yield* serverSettings.getSettings;
10441028
1045 assert.isTrue(settings.providers.grok.enabled);
1046 assert.isFalse(settings.providers.opencode.enabled);
1047 assert.isFalse(settings.providers.cursor.enabled);
1029 assert.deepEqual(settings.providerInstances, {});
10481030 }).pipe(Effect.provide(layerServerSettings())),
10491031 );
10501032
1051 it.effect("preserves provider history when the settings file is invalid", () =>
1033 it.effect("migrates legacy providers from an invalid file without rewriting it", () =>
10521034 Effect.gen(function* () {
10531035 const serverConfig = yield* ServerConfig.ServerConfig;
10541036 const fileSystem = yield* FileSystem.FileSystem;
10551037 const serverSettings = yield* ServerSettingsModule.ServerSettingsService;
1056 yield* fileSystem.writeFileString(serverConfig.settingsPath, "{invalid json");
1057 yield* recordProviderUsage("cursor");
1038 const raw = '{"addProjectBaseDirectory":42,"providers":{"codex":{"binaryPath":"/x"}}}';
1039 yield* fileSystem.writeFileString(serverConfig.settingsPath, raw);
10581040
10591041 const settings = yield* serverSettings.getSettings;
10601042
1061 assert.isTrue(settings.providers.cursor.enabled);
1062 assert.isFalse(settings.providers.grok.enabled);
1063 assert.isFalse(settings.providers.opencode.enabled);
1043 // The readable provider settings still apply, but the file stays for the user to repair.
1044 assert.deepEqual(settings.providerInstances[ProviderInstanceId.make("codex")], {
1045 driver: ProviderDriverKind.make("codex"),
1046 config: { binaryPath: "/x" },
1047 });
1048 assert.equal(yield* fileSystem.readFileString(serverConfig.settingsPath), raw);
10641049 }).pipe(Effect.provide(layerServerSettings())),
10651050 );
10661051
1067 it.effect("preserves valid provider flags when another settings field is invalid", () =>
1052 it.effect("skips a disabled provider instance when picking the text generation fallback", () =>
10681053 Effect.gen(function* () {
10691054 const serverConfig = yield* ServerConfig.ServerConfig;
10701055 const fileSystem = yield* FileSystem.FileSystem;
10711056 const serverSettings = yield* ServerSettingsModule.ServerSettingsService;
10721057 yield* fileSystem.writeFileString(
10731058 serverConfig.settingsPath,
1074 '{"addProjectBaseDirectory":42,"providers":{"cursor":{"enabled":false},"grok":{"enabled":true}}}',
1059 '{"providerInstances":{"codex":{"driver":"codex","enabled":false,"config":{}}}}',
10751060 );
1076 yield* recordProviderUsage("cursor");
1077
1078 const settings = yield* serverSettings.getSettings;
1079
1080 assert.isFalse(settings.providers.cursor.enabled);
1081 assert.isTrue(settings.providers.grok.enabled);
1082 assert.isFalse(settings.providers.opencode.enabled);
1083 }).pipe(Effect.provide(layerServerSettings())),
1084 );
1085
1086 it.effect("restores providers from persisted runtime sessions", () =>
1087 Effect.gen(function* () {
1088 const serverSettings = yield* ServerSettingsModule.ServerSettingsService;
1089 const sql = yield* SqlClient.SqlClient;
1090 yield* sql`
1091 INSERT INTO provider_session_runtime (
1092 thread_id,
1093 provider_name,
1094 provider_instance_id,
1095 adapter_key,
1096 status,
1097 last_seen_at
1098 )
1099 VALUES (
1100 ${"thread-opencode-runtime"},
1101 ${"opencode"},
1102 ${"opencode"},
1103 ${"opencode"},
1104 ${"ready"},
1105 ${"2026-08-25T00:00:00.000Z"}
1106 )
1107 `;
11081061
11091062 const settings = yield* serverSettings.getSettings;
11101063
1111 assert.isFalse(settings.providers.grok.enabled);
1112 assert.isTrue(settings.providers.opencode.enabled);
1113 assert.isFalse(settings.providers.cursor.enabled);
1114 }).pipe(Effect.provide(layerServerSettings())),
1115 );
1116
1117 it.effect("persists explicit disables after a provider has been used", () =>
1118 Effect.gen(function* () {
1119 const serverConfig = yield* ServerConfig.ServerConfig;
1120 const fileSystem = yield* FileSystem.FileSystem;
1121 const serverSettings = yield* ServerSettingsModule.ServerSettingsService;
1122 yield* recordProviderUsage("grok");
1123
1124 assert.isTrue((yield* serverSettings.getSettings).providers.grok.enabled);
1125
1126 const settings = yield* serverSettings.updateSettings({
1127 providers: { grok: { enabled: false } },
1128 });
1129 assert.isFalse(settings.providers.grok.enabled);
1130
1131 const raw = yield* fileSystem.readFileString(serverConfig.settingsPath);
1132 assert.isFalse(JSON.parse(raw).providers.grok.enabled);
1133 }).pipe(Effect.provide(layerServerSettings())),
1134 );
1135
1136 it.effect("persists explicit provider enables before their first use", () =>
1137 Effect.gen(function* () {
1138 const serverConfig = yield* ServerConfig.ServerConfig;
1139 const fileSystem = yield* FileSystem.FileSystem;
1140 const serverSettings = yield* ServerSettingsModule.ServerSettingsService;
1141
1142 yield* serverSettings.updateSettings({
1143 providers: {
1144 cursor: { enabled: true },
1145 grok: { enabled: true },
1146 opencode: { enabled: true },
1147 },
1148 });
1149 yield* serverSettings.updateSettings({ addProjectBaseDirectory: "~/Development" });
1150
1151 const raw = yield* fileSystem.readFileString(serverConfig.settingsPath);
1152 const persisted = JSON.parse(raw);
1153 assert.isTrue(persisted.providers.cursor.enabled);
1154 assert.isTrue(persisted.providers.grok.enabled);
1155 assert.isTrue(persisted.providers.opencode.enabled);
1064 assert.equal(settings.textGenerationModelSelection.instanceId, "claudeAgent");
11561065 }).pipe(Effect.provide(layerServerSettings())),
11571066 );
11581067
1159 it.effect("keeps optional providers disabled after a new installation writes settings", () =>
1068 it.effect("keeps a default-off provider's new instance disabled and sparse on disk", () =>
11601069 Effect.gen(function* () {
11611070 const serverConfig = yield* ServerConfig.ServerConfig;
11621071 const fileSystem = yield* FileSystem.FileSystem;
11631072 const serverSettings = yield* ServerSettingsModule.ServerSettingsService;
11641073
1165 const initial = yield* serverSettings.getSettings;
1166 assert.isFalse(initial.providers.grok.enabled);
1167 assert.isFalse(initial.providers.opencode.enabled);
1168 assert.isFalse(initial.providers.cursor.enabled);
1169
11701074 const next = yield* serverSettings.updateSettings({
1171 addProjectBaseDirectory: "~/Development",
11721075 providerInstances: {
11731076 [ProviderInstanceId.make("grok")]: {
11741077 driver: ProviderDriverKind.make("grok"),
......@@ -1177,18 +1080,11 @@ it.layer(NodeServices.layer)("server settings", (it) => {
11771080 },
11781081 });
11791082
1180 assert.isFalse(next.providers.grok.enabled);
1181 assert.isFalse(next.providers.opencode.enabled);
1182 assert.isFalse(next.providers.cursor.enabled);
11831083 const grok = next.providerInstances[ProviderInstanceId.make("grok")];
11841084 assert.isDefined(grok);
11851085 assert.isFalse(resolveProviderInstanceEnabled(grok));
1186
1187 const raw = yield* fileSystem.readFileString(serverConfig.settingsPath);
1188 const persisted = JSON.parse(raw);
1189 assert.isFalse(persisted.providers.cursor.enabled);
1190 assert.isFalse(persisted.providers.grok.enabled);
1191 assert.isFalse(persisted.providers.opencode.enabled);
1086 const persisted = JSON.parse(yield* fileSystem.readFileString(serverConfig.settingsPath));
1087 assert.isUndefined(persisted.providers);
11921088 assert.isUndefined(persisted.providerInstances.grok.enabled);
11931089 }).pipe(Effect.provide(layerServerSettings())),
11941090 );
......@@ -1252,54 +1148,6 @@ it.layer(NodeServices.layer)("server settings", (it) => {
12521148 }).pipe(Effect.provide(layerServerSettings())),
12531149 );
12541150
1255 it.effect("trims provider path settings when updates are applied", () =>
1256 Effect.gen(function* () {
1257 const serverSettings = yield* ServerSettingsModule.ServerSettingsService;
1258
1259 const next = yield* serverSettings.updateSettings({
1260 providers: {
1261 codex: {
1262 binaryPath: " /opt/homebrew/bin/codex ",
1263 homePath: " ",
1264 },
1265 claudeAgent: {
1266 binaryPath: " /opt/homebrew/bin/claude ",
1267 },
1268 opencode: {
1269 binaryPath: " /opt/homebrew/bin/opencode ",
1270 serverUrl: " http://127.0.0.1:4096 ",
1271 serverPassword: " secret-password ",
1272 },
1273 },
1274 });
1275
1276 assert.deepEqual(next.providers.codex, {
1277 enabled: true,
1278 binaryPath: "/opt/homebrew/bin/codex",
1279 homePath: "",
1280 shadowHomePath: "",
1281 launchArgs: "",
1282 customModels: [],
1283 });
1284 assert.deepEqual(next.providers.claudeAgent, {
1285 enabled: true,
1286 binaryPath: "/opt/homebrew/bin/claude",
1287 homePath: "",
1288 customModels: [],
1289 launchArgs: "",
1290 autoCompactWindow: "",
1291 });
1292 assert.deepEqual(next.providers.opencode, {
1293 // OpenCode is disabled by default; this update only touches paths.
1294 enabled: false,
1295 binaryPath: "/opt/homebrew/bin/opencode",
1296 serverUrl: "http://127.0.0.1:4096",
1297 serverPassword: "secret-password",
1298 customModels: [],
1299 });
1300 }).pipe(Effect.provide(layerServerSettings())),
1301 );
1302
13031151 it.effect("trims observability settings when updates are applied", () =>
13041152 Effect.gen(function* () {
13051153 const serverSettings = yield* ServerSettingsModule.ServerSettingsService;
......@@ -1322,51 +1170,20 @@ it.layer(NodeServices.layer)("server settings", (it) => {
13221170 }).pipe(Effect.provide(layerServerSettings())),
13231171 );
13241172
1325 it.effect("defaults blank binary paths to provider executables", () =>
1326 Effect.gen(function* () {
1327 const serverSettings = yield* ServerSettingsModule.ServerSettingsService;
1328
1329 const next = yield* serverSettings.updateSettings({
1330 providers: {
1331 codex: {
1332 binaryPath: " ",
1333 },
1334 claudeAgent: {
1335 binaryPath: "",
1336 },
1337 },
1338 });
1339
1340 assert.equal(next.providers.codex.binaryPath, "codex");
1341 assert.equal(next.providers.claudeAgent.binaryPath, "claude");
1342 }).pipe(Effect.provide(layerServerSettings())),
1343 );
1344
1345 it.effect("writes non-default settings and explicit optional provider defaults to disk", () =>
1173 it.effect("writes only non-default settings to disk", () =>
13461174 Effect.gen(function* () {
13471175 const serverSettings = yield* ServerSettingsModule.ServerSettingsService;
13481176 const serverConfig = yield* ServerConfig.ServerConfig;
13491177 const fileSystem = yield* FileSystem.FileSystem;
1350 const next = yield* serverSettings.updateSettings({
1178 yield* serverSettings.updateSettings({
13511179 addProjectBaseDirectory: "~/Development",
13521180 observability: {
13531181 otlpTracesUrl: "http://localhost:4318/v1/traces",
13541182 otlpMetricsUrl: "http://localhost:4318/v1/metrics",
13551183 },
1356 providers: {
1357 codex: {
1358 binaryPath: "/opt/homebrew/bin/codex",
1359 },
1360 opencode: {
1361 serverUrl: "http://127.0.0.1:4096",
1362 serverPassword: "secret-password",
1363 },
1364 },
13651184 automaticGitFetchInterval: Duration.seconds(10),
13661185 });
13671186
1368 assert.equal(next.providers.codex.binaryPath, "/opt/homebrew/bin/codex");
1369
13701187 const raw = yield* fileSystem.readFileString(serverConfig.settingsPath);
13711188 assert.deepEqual(JSON.parse(raw), {
13721189 addProjectBaseDirectory: "~/Development",
......@@ -1374,22 +1191,6 @@ it.layer(NodeServices.layer)("server settings", (it) => {
13741191 otlpTracesUrl: "http://localhost:4318/v1/traces",
13751192 otlpMetricsUrl: "http://localhost:4318/v1/metrics",
13761193 },
1377 providers: {
1378 codex: {
1379 binaryPath: "/opt/homebrew/bin/codex",
1380 },
1381 cursor: {
1382 enabled: false,
1383 },
1384 grok: {
1385 enabled: false,
1386 },
1387 opencode: {
1388 enabled: false,
1389 serverUrl: "http://127.0.0.1:4096",
1390 serverPassword: "secret-password",
1391 },
1392 },
13931194 backgroundActivity: {
13941195 schemaVersion: 1,
13951196 profile: "custom",
apps/server/src/serverSettings.ts+102-67
......@@ -23,6 +23,8 @@ import {
2323 type ProviderInstanceEnvironmentVariable,
2424 type UsageLimitSourceConfig,
2525 type ProviderInstanceMutation,
26 defaultInstanceIdForDriver,
27 isProviderDriverKind,
2628 ProviderDriverKind,
2729 ProviderInstanceId,
2830 resolveProviderInstanceEnabled,
......@@ -384,69 +386,110 @@ const ServerSettingsJson = fromLenientJson(
384386 }),
385387);
386388const decodeServerSettingsJsonExit = Schema.decodeUnknownExit(ServerSettingsJson);
387const PersistedOptionalProviderSettings = Schema.Struct({
388 providers: Schema.optionalKey(
389 Schema.Struct({
390 cursor: Schema.optionalKey(Schema.Struct({ enabled: Schema.optionalKey(Schema.Boolean) })),
391 grok: Schema.optionalKey(Schema.Struct({ enabled: Schema.optionalKey(Schema.Boolean) })),
392 opencode: Schema.optionalKey(Schema.Struct({ enabled: Schema.optionalKey(Schema.Boolean) })),
393 }),
394 ),
395});
396const decodePersistedOptionalProviderSettingsJsonExit = Schema.decodeUnknownExit(
397 fromLenientJson(PersistedOptionalProviderSettings),
389/**
390 * The retired `providers.<kind>` map, read without its old schemas. Before
391 * `providerInstances` existed each built-in driver had one blob there; it
392 * now only feeds `migrateLegacyProviderSettings`.
393 */
394const LegacyProviderSettingsJson = fromLenientJson(
395 Schema.Struct({
396 providers: Schema.optionalKey(Schema.Record(Schema.String, Schema.Unknown)),
397 }),
398398);
399const decodeLegacyProviderSettingsJsonExit = Schema.decodeUnknownExit(LegacyProviderSettingsJson);
399400
400function restoreUsedProviders(
401// Drivers that start disabled, so a session in the history means the user
402// turned them on before the instance kept an explicit flag.
403const HISTORY_RESTORED_DRIVERS: ReadonlySet<ProviderDriverKind> = new Set(
404 ["cursor", "grok", "opencode"].map((driver) => ProviderDriverKind.make(driver)),
405);
406
407/**
408 * Move each customized legacy `providers.<kind>` blob into the driver's
409 * default `providerInstances` slot. An explicit instance already in that slot
410 * wins. Blobs that match a fresh install (empty, or only an `enabled` flag
411 * equal to the driver default) carry nothing and are dropped.
412 *
413 * A cursor/grok/opencode slot without an `enabled` flag is enabled when
414 * provider history shows the driver was used, which is how those drivers
415 * were opted into before they had an explicit flag. Callers run this only
416 * while the settings file still has the retired map, so it happens once.
417 */
418function migrateLegacyProviderSettings(
401419 settings: ServerSettings,
402 persisted: typeof PersistedOptionalProviderSettings.Type,
420 legacyProviders: Readonly<Record<string, unknown>>,
403421 providerHistory: ReadonlyArray<{
404422 readonly providerName: string;
405423 readonly providerInstanceId: string | null;
406424 }>,
407425): ServerSettings {
408 const usedProviders = new Set(providerHistory.map(({ providerName }) => providerName));
409 const usedProviderInstances = new Set(
426 // History rows are raw SQL text; compare them as strings.
427 const usedProviders = new Set<string>(providerHistory.map(({ providerName }) => providerName));
428 const usedProviderInstances = new Set<string>(
410429 providerHistory.map(
411430 ({ providerName, providerInstanceId }) => providerInstanceId ?? providerName,
412431 ),
413432 );
414 const providerInstances = Object.fromEntries(
415 Object.entries(settings.providerInstances).map(([instanceId, instance]) => [
416 instanceId,
433
434 const providerInstances: Record<ProviderInstanceId, ProviderInstanceConfig> = {};
435 for (const [instanceId, instance] of Object.entries(settings.providerInstances) as Array<
436 [ProviderInstanceId, ProviderInstanceConfig]
437 >) {
438 providerInstances[instanceId] =
417439 instance.enabled === undefined &&
418 (instance.driver === "cursor" ||
419 instance.driver === "grok" ||
420 instance.driver === "opencode") &&
440 HISTORY_RESTORED_DRIVERS.has(instance.driver) &&
421441 usedProviderInstances.has(instanceId)
422442 ? { ...instance, enabled: true }
423 : instance,
424 ]),
425 );
443 : instance;
444 }
445
446 // History restores a never-configured cursor/grok/opencode slot too.
447 const legacyEntries = new Map(Object.entries(legacyProviders));
448 for (const driver of HISTORY_RESTORED_DRIVERS) {
449 if (!legacyEntries.has(driver)) legacyEntries.set(driver, {});
450 }
451 for (const [kind, blob] of legacyEntries) {
452 if (!isProviderDriverKind(kind) || blob === null || typeof blob !== "object") continue;
453 if (Array.isArray(blob)) continue;
454 const driver = kind;
455 const instanceId = defaultInstanceIdForDriver(driver);
456 if (Object.hasOwn(providerInstances, instanceId)) continue;
457
458 const { enabled: rawEnabled, ...config } = blob as Record<string, unknown>;
459 const explicitEnabled = typeof rawEnabled === "boolean" ? rawEnabled : undefined;
460 const enabled =
461 explicitEnabled ??
462 (HISTORY_RESTORED_DRIVERS.has(driver) && usedProviders.has(driver) ? true : undefined);
463 const driverDefault = resolveProviderInstanceEnabled({ driver, config: {} });
464 if (Object.keys(config).length === 0 && (enabled === undefined || enabled === driverDefault)) {
465 continue;
466 }
467 providerInstances[instanceId] = {
468 driver,
469 ...(enabled === undefined ? {} : { enabled }),
470 config,
471 } satisfies ProviderInstanceConfig;
472 }
426473
427474 return {
428475 ...settings,
429 providers: {
430 ...settings.providers,
431 cursor: {
432 ...settings.providers.cursor,
433 enabled: persisted.providers?.cursor?.enabled ?? usedProviders.has("cursor"),
434 },
435 grok: {
436 ...settings.providers.grok,
437 enabled: persisted.providers?.grok?.enabled ?? usedProviders.has("grok"),
438 },
439 opencode: {
440 ...settings.providers.opencode,
441 enabled: persisted.providers?.opencode?.enabled ?? usedProviders.has("opencode"),
442 },
443 },
444476 providerInstances,
445477 };
446478}
447479
448480const ACP_REGISTRY_DRIVER = ProviderDriverKind.make("acpRegistry");
449481
482const TEXT_GENERATION_FALLBACK_DRIVERS = [
483 "codex",
484 "claudeAgent",
485 "cursor",
486 "grok",
487 "muse",
488 "pi",
489 "opencode",
490 "antigravity",
491].map((driver) => ProviderDriverKind.make(driver));
492
450493/** ACP Registry instances reject every application text-generation operation. */
451494function selectionSupportsTextGeneration(
452495 settings: ServerSettings,
......@@ -463,14 +506,13 @@ function resolveTextGenerationProvider(settings: ServerSettings): ServerSettings
463506}
464507
465508function fallbackTextGenerationProvider(settings: ServerSettings): ServerSettings {
466 // Same precedence as isModelSelectionProviderEnabled: an explicit provider
467 // instance wins over the legacy providers map, which decodes to defaults
468 // (codex enabled) when the Providers UI has only written providerInstances.
469 const fallbackEntry = Object.entries(settings.providers).find(([driver, provider]) => {
470 const instance = settings.providerInstances[ProviderInstanceId.make(driver)];
471 return instance === undefined ? provider.enabled : resolveProviderInstanceEnabled(instance);
472 });
473 const fallback = fallbackEntry ? ProviderDriverKind.make(fallbackEntry[0]) : undefined;
509 // The built-in default instances in preference order. A slot without an
510 // explicit instance uses its driver's default enabled state.
511 const fallback = TEXT_GENERATION_FALLBACK_DRIVERS.find((driver) =>
512 resolveProviderInstanceEnabled(
513 settings.providerInstances[defaultInstanceIdForDriver(driver)] ?? { driver, config: {} },
514 ),
515 );
474516 if (!fallback) {
475517 return settings;
476518 }
......@@ -497,17 +539,6 @@ const ATOMIC_SETTINGS_KEYS: ReadonlySet<string> = new Set([
497539 "pullRequestMergeMethod",
498540]);
499541
500// Preserve both enabled states because provider history cannot recover a new opt-in.
501const PERSISTED_SERVER_SETTINGS_DEFAULTS = {
502 ...DEFAULT_SERVER_SETTINGS,
503 providers: {
504 ...DEFAULT_SERVER_SETTINGS.providers,
505 cursor: { ...DEFAULT_SERVER_SETTINGS.providers.cursor, enabled: undefined },
506 grok: { ...DEFAULT_SERVER_SETTINGS.providers.grok, enabled: undefined },
507 opencode: { ...DEFAULT_SERVER_SETTINGS.providers.opencode, enabled: undefined },
508 },
509};
510
511542function stripDefaultServerSettings(current: unknown, defaults: unknown): unknown | undefined {
512543 if (Array.isArray(current) || Array.isArray(defaults)) {
513544 return Equal.equals(current, defaults) ? undefined : current;
......@@ -671,7 +702,7 @@ const make = Effect.gen(function* () {
671702 const writeSettingsAtomically = Effect.fnUntraced(
672703 function* (settings: ServerSettings) {
673704 const sparseSettingsJson = yield* encodeServerSettingsJson(
674 stripDefaultServerSettings(settings, PERSISTED_SERVER_SETTINGS_DEFAULTS) ?? {},
705 stripDefaultServerSettings(settings, DEFAULT_SERVER_SETTINGS) ?? {},
675706 );
676707
677708 return yield* writeFileStringAtomically({
......@@ -740,7 +771,7 @@ const make = Effect.gen(function* () {
740771
741772 const loadSettingsFromDisk = Effect.gen(function* () {
742773 let settings = DEFAULT_SERVER_SETTINGS;
743 let persisted: typeof PersistedOptionalProviderSettings.Type = {};
774 let legacyProviders: Readonly<Record<string, unknown>> | undefined;
744775 // A file that failed to decode must stay on disk for the user to repair;
745776 // the fold below only writes when it started from the file's real contents.
746777 let settingsFileTrusted = true;
......@@ -748,12 +779,12 @@ const make = Effect.gen(function* () {
748779 if (yield* readConfigExists) {
749780 const raw = yield* readRawConfig;
750781 const decoded = decodeServerSettingsJsonExit(raw);
751 const persistedSettings = decodePersistedOptionalProviderSettingsJsonExit(raw);
752 if (persistedSettings._tag === "Success") {
753 persisted = persistedSettings.value;
782 const legacySettings = decodeLegacyProviderSettingsJsonExit(raw);
783 if (legacySettings._tag === "Success") {
784 legacyProviders = legacySettings.value.providers;
754785 }
755 if (decoded._tag === "Failure" || persistedSettings._tag === "Failure") {
756 const failure = decoded._tag === "Failure" ? decoded : persistedSettings;
786 if (decoded._tag === "Failure" || legacySettings._tag === "Failure") {
787 const failure = decoded._tag === "Failure" ? decoded : legacySettings;
757788 settingsFileTrusted = false;
758789 if (failure._tag === "Failure") {
759790 yield* Effect.logWarning("failed to parse settings.json, using defaults", {
......@@ -817,14 +848,18 @@ const make = Effect.gen(function* () {
817848 );
818849
819850 const loaded = foldProviderInstanceEnabledFlags(
820 restoreUsedProviders(settings, persisted, providerHistory),
851 legacyProviders === undefined
852 ? settings
853 : migrateLegacyProviderSettings(settings, legacyProviders, providerHistory),
821854 );
822855 const folded = settingsFileTrusted
823856 ? foldLegacyProjectSettings(loaded, legacyProjectRows)
824857 : loaded;
825858 // Only rewrite a file that decoded cleanly; an untrusted one stays for the user to repair.
826859 const migrated = settingsFileTrusted ? yield* moveInlineBitbucketTokens(folded) : folded;
827 if (migrated !== loaded) {
860 // A file still carrying the retired `providers` map is rewritten once so
861 // the migrated instances persist and the old key disappears.
862 if (migrated !== loaded || (settingsFileTrusted && legacyProviders !== undefined)) {
828863 yield* writeSettingsAtomically(migrated);
829864 }
830865 return migrated;
apps/server/src/terminal/Manager.test.ts+21-23
......@@ -2272,7 +2272,7 @@ it.layer(
22722272 ),
22732273 );
22742274
2275 it.effect("resolves the legacy Codex default instance", () =>
2275 it.effect("resolves the Codex default slot", () =>
22762276 Effect.gen(function* () {
22772277 const path = yield* Path.Path;
22782278 const serverSettings = yield* ServerSettings.ServerSettingsService;
......@@ -2283,18 +2283,22 @@ it.layer(
22832283 env: undefined,
22842284 });
22852285
2286 expect(environment.CODEX_HOME).toMatch(/[\\/][.]codex-legacy$/);
2286 expect(environment.CODEX_HOME).toMatch(/[\\/][.]codex-default$/);
22872287 }).pipe(
22882288 Effect.provide(
22892289 ServerSettings.ServerSettingsService.layerTest({
2290 providerInstances: {},
2291 providers: { codex: { homePath: "~/.codex-legacy" } },
2290 providerInstances: {
2291 [ProviderInstanceId.make("codex")]: {
2292 driver: ProviderDriverKind.make("codex"),
2293 config: { homePath: "~/.codex-default" },
2294 },
2295 },
22922296 }),
22932297 ),
22942298 ),
22952299 );
22962300
2297 it.effect("resolves the legacy Claude default instance", () =>
2301 it.effect("resolves the Claude default slot", () =>
22982302 Effect.gen(function* () {
22992303 const path = yield* Path.Path;
23002304 const serverSettings = yield* ServerSettings.ServerSettingsService;
......@@ -2305,18 +2309,22 @@ it.layer(
23052309 env: undefined,
23062310 });
23072311
2308 expect(environment.CLAUDE_CONFIG_DIR).toMatch(/[\\/][.]claude-legacy$/);
2312 expect(environment.CLAUDE_CONFIG_DIR).toMatch(/[\\/][.]claude-default$/);
23092313 }).pipe(
23102314 Effect.provide(
23112315 ServerSettings.ServerSettingsService.layerTest({
2312 providerInstances: {},
2313 providers: { claudeAgent: { homePath: "~/.claude-legacy" } },
2316 providerInstances: {
2317 [ProviderInstanceId.make("claudeAgent")]: {
2318 driver: ProviderDriverKind.make("claudeAgent"),
2319 config: { homePath: "~/.claude-default" },
2320 },
2321 },
23142322 }),
23152323 ),
23162324 ),
23172325 );
23182326
2319 it.effect("prefers an explicit default instance over legacy provider settings", () =>
2327 it.effect("resolves an empty Codex default slot with default config", () =>
23202328 Effect.gen(function* () {
23212329 const path = yield* Path.Path;
23222330 const serverSettings = yield* ServerSettings.ServerSettingsService;
......@@ -2324,26 +2332,16 @@ it.layer(
23242332 serverSettings,
23252333 path,
23262334 rawProviderInstanceId: "codex",
2327 env: undefined,
2335 env: { CODEX_HOME: "/inherited/codex-home" },
23282336 });
23292337
2330 expect(environment.CODEX_HOME).toMatch(/[\\/][.]codex-explicit$/);
2338 expect(environment.CODEX_HOME).toBe("/inherited/codex-home");
23312339 }).pipe(
2332 Effect.provide(
2333 ServerSettings.ServerSettingsService.layerTest({
2334 providers: { codex: { homePath: "~/.codex-legacy" } },
2335 providerInstances: {
2336 [ProviderInstanceId.make("codex")]: {
2337 driver: "codex",
2338 config: { homePath: "~/.codex-explicit" },
2339 },
2340 },
2341 }),
2342 ),
2340 Effect.provide(ServerSettings.ServerSettingsService.layerTest({ providerInstances: {} })),
23432341 ),
23442342 );
23452343
2346 it.effect("keeps unknown provider instance ids unavailable after legacy hydration", () =>
2344 it.effect("keeps unknown provider instance ids unavailable after default-slot hydration", () =>
23472345 Effect.gen(function* () {
23482346 const path = yield* Path.Path;
23492347 const serverSettings = yield* ServerSettings.ServerSettingsService;
apps/server/src/usage/UsageService.test.ts+97-92
......@@ -73,9 +73,15 @@ const setup = Effect.gen(function* () {
7373 home,
7474 transcript: NodePath.join(transcriptDir, "session.jsonl"),
7575 settings: {
76 providers: {
77 claudeAgent: { homePath: NodePath.join(home, "claude") },
78 codex: { homePath: NodePath.join(home, "codex") },
76 providerInstances: {
77 [ProviderInstanceId.make("claudeAgent")]: {
78 driver: ProviderDriverKind.make("claudeAgent"),
79 config: { homePath: NodePath.join(home, "claude") },
80 },
81 [ProviderInstanceId.make("codex")]: {
82 driver: ProviderDriverKind.make("codex"),
83 config: { homePath: NodePath.join(home, "codex") },
84 },
7985 },
8086 },
8187 };
......@@ -227,98 +233,86 @@ function cursorSource(summary: { readonly sources: readonly UsageSource[] }) {
227233}
228234
229235describe("UsageService", () => {
230 it.live.each([
231 { explicitDefault: true, label: "explicit" },
232 { explicitDefault: false, label: "legacy" },
233 ])(
234 "reads shared managed $label default and disabled extra account history once",
235 ({ explicitDefault }) =>
236 Effect.gen(function* () {
237 const { home, settings } = yield* setup;
238 const summary = yield* Effect.gen(function* () {
239 for (const [id, output] of [
240 ["codex", 17],
241 ["codex-personal", 23],
242 ] as const) {
243 const sessions = NodePath.join(home, "shared-codex", "sessions");
244 yield* Effect.promise(async () => {
245 await NodeFSP.mkdir(sessions, { recursive: true });
246 await NodeFSP.writeFile(
247 NodePath.join(sessions, `${id}-rollout.jsonl`),
248 [
249 { type: "session_meta", payload: { id } },
250 { type: "turn_context", payload: { model: "gpt-5.6-sol" } },
251 {
252 type: "event_msg",
253 timestamp: "2026-08-01T10:00:00Z",
254 payload: {
255 type: "token_count",
256 info: { last_token_usage: { input_tokens: 10, output_tokens: output } },
257 },
236 it.live("reads shared managed default and disabled extra account history once", () =>
237 Effect.gen(function* () {
238 const { home, settings } = yield* setup;
239 const summary = yield* Effect.gen(function* () {
240 for (const [id, output] of [
241 ["codex", 17],
242 ["codex-personal", 23],
243 ] as const) {
244 const sessions = NodePath.join(home, "shared-codex", "sessions");
245 yield* Effect.promise(async () => {
246 await NodeFSP.mkdir(sessions, { recursive: true });
247 await NodeFSP.writeFile(
248 NodePath.join(sessions, `${id}-rollout.jsonl`),
249 [
250 { type: "session_meta", payload: { id } },
251 { type: "turn_context", payload: { model: "gpt-5.6-sol" } },
252 {
253 type: "event_msg",
254 timestamp: "2026-08-01T10:00:00Z",
255 payload: {
256 type: "token_count",
257 info: { last_token_usage: { input_tokens: 10, output_tokens: output } },
258258 },
259 ]
260 .map((line) => encodeUnknownJsonString(line))
261 .join("\n") + "\n",
262 );
263 });
264 }
265 const service = yield* UsageService.make;
266 return yield* service.readSummary(WINDOW);
267 }).pipe(
268 // Scoped inside the state directory, so pending cache writes land
269 // before it is removed.
270 Effect.scoped,
271 Effect.provide(
272 layerService({
273 prefix: "usage-managed-accounts",
274 home,
275 settings: {
276 ...settings,
277 providers: {
278 ...settings.providers,
279 codex: { setupMode: "managed", homePath: NodePath.join(home, "shared-codex") },
280259 },
281 providerInstances: {
282 ...(explicitDefault
283 ? {
284 [ProviderInstanceId.make("codex")]: {
285 driver: ProviderDriverKind.make("codex"),
286 config: {
287 setupMode: "managed",
288 homePath: NodePath.join(home, "shared-codex"),
289 },
290 },
291 }
292 : {}),
293 [ProviderInstanceId.make("codex-personal")]: {
294 driver: ProviderDriverKind.make("codex"),
295 enabled: false,
296 config: {
297 setupMode: "managed",
298 homePath: NodePath.join(home, "shared-codex"),
299 shadowHomePath: NodePath.join(home, "personal-shadow"),
300 },
301 environment: [
302 {
303 name: "CODEX_HOME",
304 value: NodePath.join(home, "ignored-environment"),
305 sensitive: false,
306 },
307 ],
260 ]
261 .map((line) => encodeUnknownJsonString(line))
262 .join("\n") + "\n",
263 );
264 });
265 }
266 const service = yield* UsageService.make;
267 return yield* service.readSummary(WINDOW);
268 }).pipe(
269 // Scoped inside the state directory, so pending cache writes land
270 // before it is removed.
271 Effect.scoped,
272 Effect.provide(
273 layerService({
274 prefix: "usage-managed-accounts",
275 home,
276 settings: {
277 ...settings,
278 providerInstances: {
279 ...settings.providerInstances,
280 [ProviderInstanceId.make("codex")]: {
281 driver: ProviderDriverKind.make("codex"),
282 config: {
283 setupMode: "managed",
284 homePath: NodePath.join(home, "shared-codex"),
285 },
286 },
287 [ProviderInstanceId.make("codex-personal")]: {
288 driver: ProviderDriverKind.make("codex"),
289 enabled: false,
290 config: {
291 setupMode: "managed",
292 homePath: NodePath.join(home, "shared-codex"),
293 shadowHomePath: NodePath.join(home, "personal-shadow"),
308294 },
295 environment: [
296 {
297 name: "CODEX_HOME",
298 value: NodePath.join(home, "ignored-environment"),
299 sensitive: false,
300 },
301 ],
309302 },
310303 },
311 }),
312 ),
313 );
314 assert.strictEqual(totalOutputTokens(summary), 40);
315 assert.strictEqual(
316 summary.sources.filter(
317 (source) => source.fingerprint.provider === "codex" && source.status === "ok",
318 ).length,
319 1,
320 );
321 }).pipe(Effect.scoped),
304 },
305 }),
306 ),
307 );
308 assert.strictEqual(totalOutputTokens(summary), 40);
309 assert.strictEqual(
310 summary.sources.filter(
311 (source) => source.fingerprint.provider === "codex" && source.status === "ok",
312 ).length,
313 1,
314 );
315 }).pipe(Effect.scoped),
322316 );
323317 it.live("omits Cursor account usage when no file login is saved", () =>
324318 Effect.gen(function* () {
......@@ -788,6 +782,7 @@ describe("UsageService", () => {
788782 settings: {
789783 ...settings,
790784 providerInstances: {
785 ...settings.providerInstances,
791786 [ProviderInstanceId.make("claude-work")]: {
792787 driver: ProviderDriverKind.make("claudeAgent"),
793788 enabled: false,
......@@ -847,7 +842,7 @@ describe("UsageService", () => {
847842 );
848843
849844 it.live(
850 "uses explicit account settings before environment and legacy homes, then refreshes them",
845 "uses explicit account settings before environment and default homes, then refreshes them",
851846 () =>
852847 Effect.gen(function* () {
853848 const { transcript, settings, home } = yield* setup;
......@@ -884,6 +879,7 @@ describe("UsageService", () => {
884879 );
885880 yield* settingsService.updateSettings({
886881 providerInstances: {
882 ...settings.providerInstances,
887883 [ProviderInstanceId.make("claudeAgent")]: {
888884 driver: ProviderDriverKind.make("claudeAgent"),
889885 config: { homePath: "" },
......@@ -912,6 +908,7 @@ describe("UsageService", () => {
912908 settings: {
913909 ...settings,
914910 providerInstances: {
911 ...settings.providerInstances,
915912 [ProviderInstanceId.make("claudeAgent")]: {
916913 driver: ProviderDriverKind.make("claudeAgent"),
917914 config: { homePath: configured },
......@@ -1271,7 +1268,15 @@ describe("UsageService", () => {
12711268 layerService({
12721269 prefix: "usage-service-cleanup-test",
12731270 home,
1274 settings: { providers: { ...settings.providers, claudeAgent: { homePath: alias } } },
1271 settings: {
1272 providerInstances: {
1273 ...settings.providerInstances,
1274 [ProviderInstanceId.make("claudeAgent")]: {
1275 driver: ProviderDriverKind.make("claudeAgent"),
1276 config: { homePath: alias },
1277 },
1278 },
1279 },
12751280 ratesDocument: {
12761281 "claude-fable-5": { input_cost_per_token: 1e-5, output_cost_per_token: 5e-5 },
12771282 },
apps/server/src/usage/UsageService.ts+3-6
......@@ -351,18 +351,15 @@ export const make = Effect.gen(function* () {
351351 }> = [];
352352 const seen = new Set<string>();
353353 for (const driver of ["claudeAgent", "codex", "grok"] as const) {
354 // Disabled accounts still have history. Explicit default slots replace
355 // the legacy settings, just as they do in the provider registry.
354 // Disabled accounts still have history. An unconfigured default slot
355 // runs with default config, just as it does in the provider registry.
356356 const instances: Array<
357357 Pick<ProviderInstanceConfig, "config" | "environment"> & { instanceId: ProviderInstanceId }
358358 > = Object.entries(settings.providerInstances)
359359 .filter(([, instance]) => instance.driver === driver)
360360 .map(([id, instance]) => ({ ...instance, instanceId: ProviderInstanceId.make(id) }));
361361 if (!Object.hasOwn(settings.providerInstances, driver)) {
362 instances.push({
363 config: settings.providers[driver],
364 instanceId: ProviderInstanceId.make(driver),
365 });
362 instances.push({ instanceId: ProviderInstanceId.make(driver) });
366363 }
367364 for (const instance of instances) {
368365 const environment = mergeProviderInstanceEnvironment(instance.environment, hostEnvironment);
apps/web/src/components/onboarding/WelcomeWizard.tsx+1-4
......@@ -898,8 +898,7 @@ function OnboardingCodexSetup({
898898 const settings = serverConfig.settings;
899899 const instance = settings.providerInstances[instanceId] ?? {
900900 driver: ProviderDriverKind.make("codex"),
901 enabled: settings.providers.codex.enabled,
902 config: createdAccount ? { enabled: true, setupMode: "managed" } : settings.providers.codex,
901 config: createdAccount ? { enabled: true, setupMode: "managed" } : {},
903902 };
904903 const mode = readCodexSetupMode(instance.config);
905904 const existingChosen =
......@@ -915,8 +914,6 @@ function OnboardingCodexSetup({
915914 patch: buildProviderInstanceUpdatePatch({
916915 settings,
917916 instanceId,
918 driver: ProviderDriverKind.make("codex"),
919 isDefault: instanceId === defaultInstanceIdForDriver(ProviderDriverKind.make("codex")),
920917 instance: {
921918 ...instance,
922919 enabled: true,
apps/web/src/components/settings/AddProviderInstanceDialog.tsx+6-11
......@@ -7,7 +7,6 @@ import { CheckIcon } from "lucide-react";
77import { useMemo, useState } from "react";
88import { squashAtomCommandFailure } from "@t3tools/client-runtime/state/runtime";
99import {
10 DEFAULT_UNIFIED_SETTINGS,
1110 type AcpRegistrySearchAgent,
1211 AuthProvidersManageScope,
1312 ProviderInstanceId,
......@@ -21,7 +20,6 @@ import {
2120 useEnvironmentSettings,
2221 usePersistEnvironmentProviderInstanceMutation,
2322} from "../../hooks/useSettings";
24import * as Equal from "effect/Equal";
2523
2624import { cn } from "../../lib/utils";
2725import { normalizeProviderAccentColor } from "../../providerInstances";
......@@ -139,15 +137,12 @@ export function AddProviderInstanceDialog({
139137 const [isSaving, setIsSaving] = useState(false);
140138 const [createdInstanceId, setCreatedInstanceId] = useState<ProviderInstanceId | null>(null);
141139
142 const existingIds = useMemo(() => {
143 const ids = new Set(["codex", "claudeAgent", ...Object.keys(settings.providerInstances ?? {})]);
144 const defaults = DEFAULT_UNIFIED_SETTINGS.providers as Record<string, unknown>;
145 // Reserve configured legacy slots too, so adding an account cannot replace them.
146 for (const [kind, config] of Object.entries(settings.providers ?? {})) {
147 if (!Equal.equals(config, defaults[kind])) ids.add(kind);
148 }
149 return ids;
150 }, [settings.providerInstances, settings.providers]);
140 // Codex and Claude run at their default slots before they are configured, so
141 // those ids stay reserved; other unconfigured default slots are free to take.
142 const existingIds = useMemo(
143 () => new Set(["codex", "claudeAgent", ...Object.keys(settings.providerInstances ?? {})]),
144 [settings.providerInstances],
145 );
151146
152147 const driverOption = providerClients.get(driver) ?? DEFAULT_DRIVER_OPTION;
153148 const isAcpRegistry = driver === ACP_REGISTRY_DRIVER_KIND;
apps/web/src/components/settings/ProviderSettingsPanel.environment.test.tsx+8-8
......@@ -276,12 +276,13 @@ describe("EnvironmentProviderSettings routing", () => {
276276 it("keeps legacy provider configuration visible when disabled", () => {
277277 settingsState.value = {
278278 ...DEFAULT_UNIFIED_SETTINGS,
279 providers: {
280 ...DEFAULT_UNIFIED_SETTINGS.providers,
281 grok: {
282 ...DEFAULT_UNIFIED_SETTINGS.providers.grok,
279 providerInstances: {
280 [ProviderInstanceId.make("grok")]: {
281 driver: ProviderDriverKind.make("grok"),
283282 enabled: false,
284 binaryPath: "/custom/grok",
283 config: {
284 binaryPath: "/custom/grok",
285 },
285286 },
286287 },
287288 };
......@@ -516,9 +517,8 @@ describe("EnvironmentProviderSettings routing", () => {
516517
517518 const [resetMutation, resetPatch] = settingsState.mutateProviderInstance.mock.lastCall ?? [];
518519 expect(resetMutation).toEqual({ operation: "remove", instanceId: codexId });
519 expect(Object.keys(resetPatch ?? {}).sort()).toEqual(["providers"]);
520 expect(resetPatch).not.toHaveProperty("favorites");
521 expect(resetPatch).not.toHaveProperty("providerModelPreferences");
520 // Removing the instance is the whole reset; shared preferences stay untouched.
521 expect(resetPatch ?? {}).toEqual({});
522522 });
523523
524524 it("updates one provider instance without sending a stale whole map", async () => {
apps/web/src/components/settings/ProviderSettingsPanel.tsx+22-64
......@@ -147,6 +147,7 @@ function providerConfigString(config: unknown, key: string): string | null {
147147
148148const PROVIDER_SETTINGS = providerClients.definitions.map((definition) => ({
149149 provider: definition.driverKind,
150 hasDefaultInstance: definition.hasDefaultInstance !== false,
150151}));
151152
152153function configuredBinaryPath(config: unknown): string {
......@@ -757,57 +758,29 @@ export function EnvironmentProviderSettings({
757758 );
758759
759760 for (const providerSettings of visibleProviderSettings) {
760 type LegacyProviderSettings = (typeof settings.providers)[keyof typeof settings.providers];
761 const legacyProviders = settings.providers as Record<string, LegacyProviderSettings>;
762 const defaultLegacyProviders = DEFAULT_UNIFIED_SETTINGS.providers as Record<
763 string,
764 LegacyProviderSettings
765 >;
766761 const driver = providerSettings.provider;
767762 const defaultInstanceId = defaultInstanceIdForDriver(driver);
768763 const explicitInstance = settings.providerInstances?.[defaultInstanceId];
769 // A remote device may run a server version whose settings predate this
770 // driver, so the legacy mirror can be absent. Without either an explicit
771 // instance or a legacy blob there is nothing to render for the slot.
772 const legacyConfig = legacyProviders[providerSettings.provider];
773 const defaultLegacyConfig = defaultLegacyProviders[providerSettings.provider];
774 // The envelope is the single enabled flag: keep the legacy in-config
775 // flag out of the synthesized blob, or an explicit `enabled: false`
776 // would keep winning over the envelope and the Switch could never
777 // turn a default-off provider on.
778 const synthesizedInstance = (): ProviderInstanceConfig | undefined => {
779 if (legacyConfig === undefined) {
780 return undefined;
781 }
782 const { enabled: legacyEnabled, ...legacyConfigRest } = legacyConfig;
783 return {
784 driver,
785 enabled: legacyEnabled,
786 config: legacyConfigRest,
787 } satisfies ProviderInstanceConfig;
788 };
789 const effectiveInstance: ProviderInstanceConfig | undefined =
790 explicitInstance ?? synthesizedInstance();
791 // Only the default slot depends on the legacy blob; custom instances for
792 // the driver must still render even when the slot has nothing to show.
793 if (effectiveInstance !== undefined) {
794 const isDirty =
795 explicitInstance !== undefined || !Equal.equals(legacyConfig, defaultLegacyConfig);
796 if (
797 driver === "codex" ||
764 // An unconfigured default slot runs with the driver's default config.
765 const effectiveInstance: ProviderInstanceConfig = explicitInstance ?? { driver };
766 const isDirty = explicitInstance !== undefined;
767 // Drivers without a default instance list only their configured instances.
768 const hasDefaultSlot = providerSettings.hasDefaultInstance || explicitInstance !== undefined;
769 if (
770 hasDefaultSlot &&
771 (driver === "codex" ||
798772 driver === "claudeAgent" ||
799773 isDirty ||
800774 resolveProviderInstanceEnabled(effectiveInstance) ||
801 defaultInstanceId === targetInstanceId
802 ) {
803 rows.push({
804 instanceId: defaultInstanceId,
805 instance: effectiveInstance,
806 driver,
807 isDefault: true,
808 isDirty,
809 });
810 }
775 defaultInstanceId === targetInstanceId)
776 ) {
777 rows.push({
778 instanceId: defaultInstanceId,
779 instance: effectiveInstance,
780 driver,
781 isDefault: true,
782 isDirty,
783 });
811784 }
812785 for (const [id, instance] of instancesByDriver.get(providerSettings.provider) ?? []) {
813786 if (id === defaultInstanceId) continue;
......@@ -847,8 +820,6 @@ export function EnvironmentProviderSettings({
847820 settings,
848821 instanceId: row.instanceId,
849822 instance: next,
850 driver: row.driver,
851 isDefault: row.isDefault,
852823 textGenerationModelSelection: options?.textGenerationModelSelection,
853824 });
854825 const result = await persistProviderInstance(
......@@ -947,23 +918,10 @@ export function EnvironmentProviderSettings({
947918 };
948919
949920 const resetDefaultInstance = async (driverKind: ProviderDriverKind) => {
950 type LegacyProviderSettings = (typeof settings.providers)[keyof typeof settings.providers];
951 const defaultLegacyProviders = DEFAULT_UNIFIED_SETTINGS.providers as Record<
952 string,
953 LegacyProviderSettings | undefined
954 >;
955 const defaultInstanceId = defaultInstanceIdForDriver(driverKind);
956 const defaultLegacyProvider = defaultLegacyProviders[driverKind];
957 if (defaultLegacyProvider === undefined) return;
958 const result = await persistProviderInstance(
959 { operation: "remove", instanceId: defaultInstanceId },
960 {
961 providers: {
962 ...settings.providers,
963 [driverKind]: defaultLegacyProvider,
964 } as typeof settings.providers,
965 },
966 );
921 const result = await persistProviderInstance({
922 operation: "remove",
923 instanceId: defaultInstanceIdForDriver(driverKind),
924 });
967925 if (result._tag === "Failure" && !isAtomCommandInterrupted(result)) {
968926 const error = squashAtomCommandFailure(result);
969927 toastManager.add({
apps/web/src/components/settings/SettingsPanels.logic.test.ts+8-38
......@@ -190,57 +190,27 @@ describe("formatDiagnosticsDescription", () => {
190190});
191191
192192describe("buildProviderInstanceUpdatePatch", () => {
193 it("promotes an edited default provider into providerInstances and resets the legacy provider", () => {
193 it("replaces one instance and keeps the others", () => {
194194 const instanceId = ProviderInstanceId.make("codex");
195 const nextInstance = {
195 const otherId = ProviderInstanceId.make("codex_personal");
196 const other = {
196197 driver: ProviderDriverKind.make("codex"),
197 enabled: true,
198 config: {
199 binaryPath: "/opt/t3/codex",
200 },
198 config: { homePath: "/Users/example/.codex-personal" },
201199 } satisfies ProviderInstanceConfig;
202
203 const patch = buildProviderInstanceUpdatePatch({
204 settings: {
205 ...DEFAULT_SERVER_SETTINGS,
206 providers: {
207 ...DEFAULT_SERVER_SETTINGS.providers,
208 codex: {
209 ...DEFAULT_SERVER_SETTINGS.providers.codex,
210 binaryPath: "/legacy/codex",
211 },
212 },
213 },
214 instanceId,
215 instance: nextInstance,
216 driver: ProviderDriverKind.make("codex"),
217 isDefault: true,
218 });
219
220 expect(patch.providerInstances?.[instanceId]).toEqual(nextInstance);
221 expect(patch.providers?.codex).toEqual(DEFAULT_SERVER_SETTINGS.providers.codex);
222 });
223
224 it("updates custom instances without touching legacy provider settings", () => {
225 const instanceId = ProviderInstanceId.make("codex_personal");
226200 const nextInstance = {
227201 driver: ProviderDriverKind.make("codex"),
228202 enabled: true,
229 config: {
230 homePath: "/Users/example/.codex-personal",
231 },
203 config: { binaryPath: "/opt/t3/codex" },
232204 } satisfies ProviderInstanceConfig;
233205
234206 const patch = buildProviderInstanceUpdatePatch({
235 settings: DEFAULT_SERVER_SETTINGS,
207 settings: { providerInstances: { [otherId]: other } },
236208 instanceId,
237209 instance: nextInstance,
238 driver: ProviderDriverKind.make("codex"),
239 isDefault: false,
240210 });
241211
242 expect(patch.providerInstances?.[instanceId]).toEqual(nextInstance);
243 expect(patch.providers).toBeUndefined();
212 expect(patch.providerInstances).toEqual({ [otherId]: other, [instanceId]: nextInstance });
213 expect(patch).not.toHaveProperty("textGenerationModelSelection");
244214 });
245215});
246216
apps/web/src/components/settings/SettingsPanels.logic.ts+1-18
......@@ -1,7 +1,6 @@
11import type {
22 BackgroundActivityProfile,
33 BackgroundActivitySettings,
4 ProviderDriverKind,
54 ProviderInstanceConfig,
65 PreviewViewportSetting,
76 ProviderInstanceId,
......@@ -255,30 +254,14 @@ export function formatDiagnosticsDescription(input: {
255254}
256255
257256export function buildProviderInstanceUpdatePatch(input: {
258 readonly settings: Pick<ServerSettings, "providers" | "providerInstances">;
257 readonly settings: Pick<ServerSettings, "providerInstances">;
259258 readonly instanceId: ProviderInstanceId;
260259 readonly instance: ProviderInstanceConfig;
261 readonly driver: ProviderDriverKind;
262 readonly isDefault: boolean;
263260 readonly textGenerationModelSelection?:
264261 | ServerSettings["textGenerationModelSelection"]
265262 | undefined;
266263}): Partial<UnifiedSettings> {
267 type LegacyProviderSettings = ServerSettings["providers"][keyof ServerSettings["providers"]];
268 const legacyProviderDefaults = DEFAULT_UNIFIED_SETTINGS.providers as Record<
269 string,
270 LegacyProviderSettings | undefined
271 >;
272 const legacyProviderDefault = input.isDefault ? legacyProviderDefaults[input.driver] : undefined;
273264 return {
274 ...(legacyProviderDefault !== undefined
275 ? {
276 providers: {
277 ...input.settings.providers,
278 [input.driver]: legacyProviderDefault,
279 } as ServerSettings["providers"],
280 }
281 : {}),
282265 providerInstances: {
283266 ...input.settings.providerInstances,
284267 [input.instanceId]: input.instance,
apps/web/src/modelSelection.test.ts+4-7
......@@ -636,14 +636,11 @@ describe("instance-scoped model selection", () => {
636636 const nativeModel = "gemini-3.1-pro";
637637 const settings: UnifiedSettings = {
638638 ...DEFAULT_UNIFIED_SETTINGS,
639 providers: {
640 ...DEFAULT_UNIFIED_SETTINGS.providers,
641 antigravity: {
642 ...DEFAULT_UNIFIED_SETTINGS.providers.antigravity,
643 customModels: ["api-only-model"],
644 },
645 },
646639 providerInstances: {
640 [ProviderInstanceId.make("antigravity")]: {
641 driver,
642 config: { customModels: ["api-only-model"] },
643 },
647644 [customId]: { driver, config: { customModels: ["unknown-model"] } },
648645 },
649646 };
apps/web/src/modelSelection.ts+3-14
......@@ -66,15 +66,7 @@ function readInstanceCustomModels(
6666 return readCustomModelEntries(value);
6767 }
6868 }
69 const defaultInstanceId = defaultInstanceIdForDriver(driverKind);
70 if (instanceId !== defaultInstanceId) {
71 return [];
72 }
73 const legacyProviders = settings.providers as Record<
74 string,
75 { readonly customModels: ReadonlyArray<unknown> } | undefined
76 >;
77 return readCustomModelEntries(legacyProviders[driverKind]?.customModels ?? []);
69 return [];
7870}
7971
8072export interface AppModelOption {
......@@ -228,11 +220,8 @@ function getAppModelOptions(
228220 * come from the instance's own `entry.models` snapshot (rather than the
229221 * first-matching-kind fallback in `getProviderModels`), so each custom
230222 * instance gets the precise model list its driver reported. Custom model
231 * slugs come from the instance's own `providerInstances[id].config.customModels`
232 * when present, falling back to the legacy per-kind
233 * `settings.providers[driverKind].customModels` bucket for default
234 * instances only. This keeps two instances of the same kind from leaking
235 * custom slugs into each other. Custom rows reported by the server are
223 * slugs come from the instance's own `providerInstances[id].config.customModels`,
224 * so two instances of the same kind never leak custom slugs into each other. Custom rows reported by the server are
236225 * ignored so a slug removed in Settings disappears without waiting for the
237226 * next provider probe.
238227 */
apps/web/src/onboarding/providerReadiness.logic.test.ts+36-30
......@@ -196,11 +196,12 @@ describe("resolveOnboardingProviderLoginCommand", () => {
196196 readyCodex,
197197 {
198198 ...DEFAULT_SERVER_SETTINGS,
199 providers: {
200 ...DEFAULT_SERVER_SETTINGS.providers,
201 codex: {
202 ...DEFAULT_SERVER_SETTINGS.providers.codex,
203 binaryPath: "C:\\Program Files\\Codex & Tools\\codex.exe",
199 providerInstances: {
200 [ProviderInstanceId.make("codex")]: {
201 driver: ProviderDriverKind.make("codex"),
202 config: {
203 binaryPath: "C:\\Program Files\\Codex & Tools\\codex.exe",
204 },
204205 },
205206 },
206207 },
......@@ -221,11 +222,12 @@ describe("resolveOnboardingProviderLoginCommand", () => {
221222 provider,
222223 {
223224 ...DEFAULT_SERVER_SETTINGS,
224 providers: {
225 ...DEFAULT_SERVER_SETTINGS.providers,
226 claudeAgent: {
227 ...DEFAULT_SERVER_SETTINGS.providers.claudeAgent,
228 binaryPath: "/opt/Claude Tools/$current/claude",
225 providerInstances: {
226 [ProviderInstanceId.make("claude")]: {
227 driver: ProviderDriverKind.make("claudeAgent"),
228 config: {
229 binaryPath: "/opt/Claude Tools/$current/claude",
230 },
229231 },
230232 },
231233 },
......@@ -246,11 +248,12 @@ describe("resolveOnboardingProviderLoginCommand", () => {
246248 readyCodex,
247249 {
248250 ...DEFAULT_SERVER_SETTINGS,
249 providers: {
250 ...DEFAULT_SERVER_SETTINGS.providers,
251 codex: {
252 ...DEFAULT_SERVER_SETTINGS.providers.codex,
253 binaryPath,
251 providerInstances: {
252 [ProviderInstanceId.make("codex")]: {
253 driver: ProviderDriverKind.make("codex"),
254 config: {
255 binaryPath,
256 },
254257 },
255258 },
256259 },
......@@ -265,11 +268,12 @@ describe("resolveOnboardingProviderLoginCommand", () => {
265268 readyCodex,
266269 {
267270 ...DEFAULT_SERVER_SETTINGS,
268 providers: {
269 ...DEFAULT_SERVER_SETTINGS.providers,
270 codex: {
271 ...DEFAULT_SERVER_SETTINGS.providers.codex,
272 binaryPath: "/opt/codex\\work/codex",
271 providerInstances: {
272 [ProviderInstanceId.make("codex")]: {
273 driver: ProviderDriverKind.make("codex"),
274 config: {
275 binaryPath: "/opt/codex\\work/codex",
276 },
273277 },
274278 },
275279 },
......@@ -284,11 +288,12 @@ describe("resolveOnboardingProviderLoginCommand", () => {
284288 readyCodex,
285289 {
286290 ...DEFAULT_SERVER_SETTINGS,
287 providers: {
288 ...DEFAULT_SERVER_SETTINGS.providers,
289 codex: {
290 ...DEFAULT_SERVER_SETTINGS.providers.codex,
291 binaryPath: "C:\\Tools\\codex.exe",
291 providerInstances: {
292 [ProviderInstanceId.make("codex")]: {
293 driver: ProviderDriverKind.make("codex"),
294 config: {
295 binaryPath: "C:\\Tools\\codex.exe",
296 },
292297 },
293298 },
294299 },
......@@ -303,11 +308,12 @@ describe("resolveOnboardingProviderLoginCommand", () => {
303308 readyCodex,
304309 {
305310 ...DEFAULT_SERVER_SETTINGS,
306 providers: {
307 ...DEFAULT_SERVER_SETTINGS.providers,
308 codex: {
309 ...DEFAULT_SERVER_SETTINGS.providers.codex,
310 binaryPath: "/opt/Codex Tools/codex",
311 providerInstances: {
312 [ProviderInstanceId.make("codex")]: {
313 driver: ProviderDriverKind.make("codex"),
314 config: {
315 binaryPath: "/opt/Codex Tools/codex",
316 },
311317 },
312318 },
313319 },
apps/web/src/onboarding/providerReadiness.logic.ts+2-6
......@@ -112,17 +112,13 @@ export function resolveOnboardingProviderLoginCommand(
112112 const instance = settings.providerInstances[provider.instanceId];
113113
114114 if (provider.driver === "claudeAgent") {
115 const config = decodeClaudeSettings(
116 instance ? (instance.config ?? {}) : settings.providers.claudeAgent,
117 );
115 const config = decodeClaudeSettings(instance?.config ?? {});
118116 const binaryPath = Option.isSome(config) ? config.value.binaryPath : "claude";
119117 return `${quoteProviderBinary(binaryPath, "claude", platform)} auth login`;
120118 }
121119
122120 if (provider.driver === "codex") {
123 const config = decodeCodexSettings(
124 instance ? (instance.config ?? {}) : settings.providers.codex,
125 );
121 const config = decodeCodexSettings(instance?.config ?? {});
126122 const binaryPath = Option.isSome(config) ? config.value.binaryPath : "codex";
127123 return `${quoteProviderBinary(binaryPath, "codex", platform)} login`;
128124 }
apps/web/src/providerInstances.test.ts+7-19
......@@ -143,7 +143,6 @@ describe("applyProviderInstanceSettings", () => {
143143 enabled: false,
144144 },
145145 },
146 providers: {} as never,
147146 });
148147
149148 expect(entry?.enabled).toBe(false);
......@@ -158,7 +157,6 @@ describe("applyProviderInstanceSettings", () => {
158157 ]);
159158 const [entry] = applyProviderInstanceSettings(entries, {
160159 providerInstances: {},
161 providers: {} as never,
162160 });
163161
164162 expect(entry?.enabled).toBe(false);
......@@ -175,7 +173,6 @@ describe("applyProviderInstanceSettings", () => {
175173 ]);
176174 const [entry] = applyProviderInstanceSettings(entries, {
177175 providerInstances: {},
178 providers: {} as never,
179176 });
180177
181178 expect(entry?.enabled).toBe(false);
......@@ -197,7 +194,6 @@ describe("applyProviderInstanceSettings", () => {
197194 enabled: false,
198195 },
199196 },
200 providers: {} as never,
201197 });
202198
203199 expect(entry?.enabled).toBe(false);
......@@ -213,26 +209,22 @@ describe("applyProviderInstanceSettings", () => {
213209 ]);
214210 const [entry] = applyProviderInstanceSettings(entries, {
215211 providerInstances: {},
216 providers: {} as never,
217212 });
218213
219214 expect(entry?.isDefault).toBe(true);
220215 expect(entry?.enabled).toBe(false);
221216 });
222217
223 it("uses legacy settings for a built-in default instance", () => {
218 it("uses the driver default for an unconfigured built-in default instance", () => {
224219 const entries = deriveProviderInstanceEntries([
225 provider({
226 provider: ProviderDriverKind.make("codex"),
227 instanceId: "codex",
228 }),
220 provider({ provider: ProviderDriverKind.make("codex"), instanceId: "codex", enabled: false }),
221 provider({ provider: ProviderDriverKind.make("grok"), instanceId: "grok", enabled: true }),
229222 ]);
230 const [entry] = applyProviderInstanceSettings(entries, {
231 providerInstances: {},
232 providers: { codex: { enabled: false } } as never,
233 });
223 const [codex, grok] = applyProviderInstanceSettings(entries, { providerInstances: {} });
234224
235 expect(entry?.enabled).toBe(false);
225 // Settings decide over a stale probe: Codex starts on, Grok starts off.
226 expect(codex?.enabled).toBe(true);
227 expect(grok?.enabled).toBe(false);
236228 });
237229});
238230
......@@ -261,7 +253,6 @@ describe("deriveProviderEntriesByEnvironment", () => {
261253 agentId,
262254 [snapshot],
263255 {
264 providers: {} as never,
265256 providerInstances: {
266257 [instanceId]: {
267258 driver: ProviderDriverKind.make("acpRegistry"),
......@@ -629,7 +620,6 @@ describe("provider icon metadata", () => {
629620 const registryConfig = { agentId: "swe-agent", registryIconUrl: iconUrl };
630621 const brandedEntries = applyProviderInstanceSettings(snapshots, {
631622 providerInstances: { [instanceId]: { driver, enabled: true, config: registryConfig } },
632 providers: {} as never,
633623 });
634624 expect(brandedEntries[0]?.acpRegistryAgentId).toBe("swe-agent");
635625 expect(brandedEntries[0]?.acpRegistryIconUrl).toBe(iconUrl);
......@@ -642,7 +632,6 @@ describe("provider icon metadata", () => {
642632 config: { ...registryConfig, source: "local", commandPath: "dsh" },
643633 },
644634 },
645 providers: {} as never,
646635 });
647636 expect(localEntry?.acpRegistryAgentId).toBeUndefined();
648637 expect(localEntry?.acpRegistryIconUrl).toBeUndefined();
......@@ -651,7 +640,6 @@ describe("provider icon metadata", () => {
651640
652641 const [restoredEntry] = applyProviderInstanceSettings(localEntry ? [localEntry] : [], {
653642 providerInstances: { [instanceId]: { driver, enabled: true, config: registryConfig } },
654 providers: {} as never,
655643 });
656644 expect(restoredEntry?.acpRegistryAgentId).toBe("swe-agent");
657645 expect(restoredEntry?.acpRegistryIconUrl).toBe(iconUrl);
apps/web/src/providerInstances.ts+9-19
......@@ -15,6 +15,7 @@
1515import {
1616 DEFAULT_MODEL_BY_PROVIDER,
1717 defaultInstanceIdForDriver,
18 isUnconfiguredDefaultInstanceEnabled,
1819 resolveProviderInstanceEnabled,
1920 type ModelSelection,
2021 type ProviderDriverKind,
......@@ -153,11 +154,7 @@ export function deriveProviderInstanceEntries(
153154 */
154155export function deriveProviderEntriesByEnvironment(
155156 providersByEnvironment: Iterable<
156 readonly [
157 string,
158 ReadonlyArray<ServerProvider>,
159 Pick<ServerSettings, "providerInstances" | "providers">?,
160 ]
157 readonly [string, ReadonlyArray<ServerProvider>, Pick<ServerSettings, "providerInstances">?]
161158 >,
162159): ReadonlyMap<string, ReadonlyMap<string, ProviderInstanceEntry>> {
163160 const byEnvironment = new Map<string, ReadonlyMap<string, ProviderInstanceEntry>>();
......@@ -178,30 +175,23 @@ export function deriveProviderEntriesByEnvironment(
178175 * settings write, so picker visibility must follow settings rather than waiting
179176 * for probe reconciliation.
180177 *
181 * Only built-in default instances have a legacy `providers` entry. Every
182 * other instance exists through `providerInstances`; if it is absent there,
183 * its streamed snapshot is stale (for example immediately after deletion)
184 * and is treated as disabled.
178 * A built-in default instance without a `providerInstances` entry keeps its
179 * streamed state, since the server runs it with default config. Any other
180 * instance missing from `providerInstances` is stale (for example
181 * immediately after deletion) and is treated as disabled.
185182 */
186183export function applyProviderInstanceSettings(
187184 entries: ReadonlyArray<ProviderInstanceEntry>,
188 settings: Pick<ServerSettings, "providerInstances" | "providers">,
185 settings: Pick<ServerSettings, "providerInstances">,
189186): ReadonlyArray<ProviderInstanceEntry> {
190 const legacyProviders = settings.providers as Readonly<
191 Record<string, { readonly enabled?: boolean } | undefined>
192 >;
193
194187 return entries.map((entry) => {
195188 const explicitInstance = Object.hasOwn(settings.providerInstances, entry.instanceId)
196189 ? settings.providerInstances[entry.instanceId]
197190 : undefined;
198 const legacyProvider = Object.hasOwn(legacyProviders, entry.driverKind)
199 ? legacyProviders[entry.driverKind]
200 : undefined;
201191 const enabled = explicitInstance
202192 ? resolveProviderInstanceEnabled(explicitInstance)
203 : entry.isDefault && legacyProvider
204 ? (legacyProvider.enabled ?? entry.enabled)
193 : entry.isDefault
194 ? isUnconfiguredDefaultInstanceEnabled(entry.instanceId)
205195 : false;
206196 if (entry.driverKind !== "acpRegistry" || explicitInstance === undefined) {
207197 return enabled === entry.enabled ? entry : { ...entry, enabled };
docs/internals/adding-a-provider.md+3-2
......@@ -103,8 +103,9 @@ produce catches them.
103103
104104## Where a driver plugs in
105105
106- **Contracts:** settings schema and patch, default model, and display name in
107 [`packages/contracts`](../../packages/contracts/src). New providers are off by default.
106- **Contracts:** settings schema, default model, and display name in
107 [`packages/contracts`](../../packages/contracts/src). Instances store that config in
108 `providerInstances`. New providers are off by default.
108109- **Server:** the driver in [`provider/Drivers`](../../apps/server/src/provider/Drivers) and its entry
109110 in [`builtInDrivers.ts`](../../apps/server/src/provider/builtInDrivers.ts). Also its position in
110111 the [status order](../../apps/server/src/provider/providerStatusCache.ts), a compatibility policy
packages/contracts/src/settings.test.ts+7-100
......@@ -208,17 +208,6 @@ describe("custom model settings", () => {
208208 { slug: "named", name: "Named", capabilities },
209209 ]);
210210 });
211
212 it("accepts entries at the settings patch boundary", () => {
213 expect(
214 decodeServerSettingsPatch({
215 providers: { codex: { customModels: [{ slug: "x", capabilities }] } },
216 }).providers?.codex?.customModels,
217 ).toEqual([{ slug: "x", capabilities }]);
218 expect(() =>
219 decodeServerSettingsPatch({ providers: { codex: { customModels: [{ name: "no slug" }] } } }),
220 ).toThrow();
221 });
222211});
223212
224213describe("ClaudeSettings auto-compaction", () => {
......@@ -239,15 +228,6 @@ describe("ClaudeSettings auto-compaction", () => {
239228 expect(() => decodeClaudeSettings({ autoCompactWindow: value })).toThrow();
240229 },
241230 );
242
243 it("rejects an unsupported threshold at the settings patch boundary", () => {
244 expect(() =>
245 decodeServerSettingsPatch({ providers: { claudeAgent: { autoCompactWindow: "300k" } } }),
246 ).toThrow();
247 expect(
248 decodeServerSettingsPatch({ providers: { claudeAgent: { autoCompactWindow: "300000" } } }),
249 ).toBeDefined();
250 });
251231});
252232
253233describe("ClientSettings notifications", () => {
......@@ -752,9 +732,6 @@ describe("ServerSettings.providerInstances (slice-2 invariant)", () => {
752732 it("decodes a fully empty config (legacy on-disk shape) without complaint", () => {
753733 const decoded = decodeServerSettings({});
754734 expect(decoded.providerInstances).toEqual({});
755 // Legacy `providers` struct is still hydrated with its per-driver defaults
756 // so existing call sites keep working through the migration.
757 expect(decoded.providers.codex.enabled).toBe(true);
758735 });
759736
760737 it("decodes a multi-instance map mixing first-party and fork drivers", () => {
......@@ -803,7 +780,6 @@ describe("ServerSettings.providerInstances (slice-2 invariant)", () => {
803780describe("provider enabled defaults", () => {
804781 it("keeps Muse disabled until a configured instance opts in", () => {
805782 const muse = ProviderDriverKind.make("muse");
806 expect(decodeServerSettings({}).providers.muse.enabled).toBe(false);
807783 expect(resolveProviderInstanceEnabled({ driver: muse, config: {} })).toBe(false);
808784 expect(resolveProviderInstanceEnabled({ driver: muse, enabled: true, config: {} })).toBe(true);
809785 expect(
......@@ -812,26 +788,13 @@ describe("provider enabled defaults", () => {
812788 });
813789
814790 it("enables only the stable bindings by default", () => {
815 const decoded = decodeServerSettings({});
816 expect(decoded.providers.codex.enabled).toBe(true);
817 expect(decoded.providers.claudeAgent.enabled).toBe(true);
818 expect(decoded.providers.cursor.enabled).toBe(false);
819 expect(decoded.providers.grok.enabled).toBe(false);
820 expect(decoded.providers.opencode.enabled).toBe(false);
821 });
822
823 it("keeps Cursor enabled when an existing user explicitly opted in", () => {
824 const cursor = ProviderDriverKind.make("cursor");
825 const cursorId = ProviderInstanceId.make("cursor");
826 const decoded = decodeServerSettings({
827 providers: { cursor: { enabled: true } },
828 providerInstances: {
829 [cursorId]: { driver: cursor, enabled: true, config: {} },
830 },
831 });
832
833 expect(decoded.providers.cursor.enabled).toBe(true);
834 expect(resolveProviderInstanceEnabled(decoded.providerInstances[cursorId]!)).toBe(true);
791 const enabledByDefault = (driver: string) =>
792 resolveProviderInstanceEnabled({ driver: ProviderDriverKind.make(driver), config: {} });
793 expect(enabledByDefault("codex")).toBe(true);
794 expect(enabledByDefault("claudeAgent")).toBe(true);
795 for (const driver of ["cursor", "grok", "muse", "pi", "opencode", "antigravity"]) {
796 expect(enabledByDefault(driver)).toBe(false);
797 }
835798 });
836799
837800 it("resolves instance enabled state with explicit false winning", () => {
......@@ -903,42 +866,6 @@ describe("ServerSettings worktree defaults", () => {
903866 });
904867});
905868
906describe("ServerSettings Cursor legacy settings", () => {
907 it("preserves V1 Cursor CLI settings when reading and writing shared settings", () => {
908 const decoded = decodeServerSettings({
909 providers: {
910 cursor: {
911 enabled: true,
912 binaryPath: "cursor-agent",
913 apiEndpoint: "http://127.0.0.1:3774",
914 },
915 },
916 });
917
918 expect(decoded.providers.cursor.enabled).toBe(true);
919 expect(encodeServerSettings(decoded).providers?.cursor).toMatchObject({
920 binaryPath: "cursor-agent",
921 apiEndpoint: "http://127.0.0.1:3774",
922 });
923 });
924
925 it("ignores obsolete Cursor CLI settings in patches", () => {
926 const patch = decodeServerSettingsPatch({
927 providers: {
928 cursor: {
929 enabled: true,
930 binaryPath: "cursor-agent",
931 apiEndpoint: "http://127.0.0.1:3774",
932 },
933 },
934 });
935
936 expect(patch.providers?.cursor?.enabled).toBe(true);
937 expect(patch.providers?.cursor).not.toHaveProperty("binaryPath");
938 expect(patch.providers?.cursor).not.toHaveProperty("apiEndpoint");
939 });
940});
941
942869describe("ServerSettings.sourceControlWritingStyle", () => {
943870 it("defaults all style settings for legacy configs", () => {
944871 const settings = decodeServerSettings({});
......@@ -1011,13 +938,6 @@ describe("ServerSettingsPatch string normalization", () => {
1011938 observability: {
1012939 otlpTracesUrl: " http://localhost:4318/v1/traces ",
1013940 },
1014 providers: {
1015 codex: {
1016 binaryPath: " /opt/homebrew/bin/codex ",
1017 homePath: " ~/.codex ",
1018 launchArgs: " --strict-config --enable foo ",
1019 },
1020 },
1021941 providerInstances: {
1022942 codex_personal: {
1023943 driver: " codex ",
......@@ -1030,9 +950,6 @@ describe("ServerSettingsPatch string normalization", () => {
1030950 expect(patch.addProjectBaseDirectory).toBe("~/Development");
1031951 expect(patch.textGenerationModelSelection?.model).toBe("gpt-5.4-mini");
1032952 expect(patch.observability?.otlpTracesUrl).toBe("http://localhost:4318/v1/traces");
1033 expect(patch.providers?.codex?.binaryPath).toBe("/opt/homebrew/bin/codex");
1034 expect(patch.providers?.codex?.homePath).toBe("~/.codex");
1035 expect(patch.providers?.codex?.launchArgs).toBe("--strict-config --enable foo");
1036953 expect(patch.providerInstances?.[ProviderInstanceId.make("codex_personal")]?.driver).toBe(
1037954 "codex",
1038955 );
......@@ -1049,19 +966,9 @@ describe("ServerSettingsPatch string normalization", () => {
1049966 const encoded = encodeServerSettings({
1050967 ...defaultSettings,
1051968 addProjectBaseDirectory: " ~/Development ",
1052 providers: {
1053 ...defaultSettings.providers,
1054 codex: {
1055 ...defaultSettings.providers.codex,
1056 binaryPath: " /opt/homebrew/bin/codex ",
1057 launchArgs: " --strict-config ",
1058 },
1059 },
1060969 });
1061970
1062971 expect(encoded.addProjectBaseDirectory).toBe("~/Development");
1063 expect(encoded.providers?.codex?.binaryPath).toBe("/opt/homebrew/bin/codex");
1064 expect(encoded.providers?.codex?.launchArgs).toBe("--strict-config");
1065972 });
1066973});
1067974
packages/contracts/src/settings.ts+30-106
......@@ -40,7 +40,7 @@ import {
4040import {
4141 ProviderInstanceConfig,
4242 ProviderInstanceId,
43 type ProviderDriverKind,
43 ProviderDriverKind,
4444} from "./providerInstance.ts";
4545import { PullRequestMergeMethod } from "./pullRequest.ts";
4646
......@@ -1457,22 +1457,6 @@ export const ServerSettings = Schema.Struct({
14571457 Schema.withDecodingDefault(Effect.succeed(null)),
14581458 ),
14591459
1460 // Legacy single-instance-per-driver settings. Continues to be the source
1461 // of truth until `providerInstances` (below) lands per-driver migration
1462 // shims and the server starts hydrating instances from it. Driver-specific
1463 // schemas live here for the duration of the migration; once each driver
1464 // owns its config in its own package, this struct shrinks to nothing and
1465 // is removed entirely.
1466 providers: Schema.Struct({
1467 codex: CodexSettings.pipe(Schema.withDecodingDefault(Effect.succeed({}))),
1468 claudeAgent: ClaudeSettings.pipe(Schema.withDecodingDefault(Effect.succeed({}))),
1469 cursor: CursorSettings.pipe(Schema.withDecodingDefault(Effect.succeed({}))),
1470 grok: GrokSettings.pipe(Schema.withDecodingDefault(Effect.succeed({}))),
1471 muse: MuseSettings.pipe(Schema.withDecodingDefault(Effect.succeed({}))),
1472 pi: PiSettings.pipe(Schema.withDecodingDefault(Effect.succeed({}))),
1473 opencode: OpenCodeSettings.pipe(Schema.withDecodingDefault(Effect.succeed({}))),
1474 antigravity: AntigravitySettings.pipe(Schema.withDecodingDefault(Effect.succeed({}))),
1475 }).pipe(Schema.withDecodingDefault(Effect.succeed({}))),
14761460 // New driver-agnostic instance map. Keyed by `ProviderInstanceId`; values
14771461 // are `ProviderInstanceConfig` envelopes. The driver-specific config blob
14781462 // is `Schema.Unknown` at this layer so envelopes with unknown drivers
......@@ -1512,8 +1496,8 @@ export const DEFAULT_SERVER_SETTINGS: ServerSettings = Schema.decodeSync(ServerS
15121496/**
15131497 * Read the legacy `enabled` flag embedded in a provider instance config
15141498 * blob. The envelope-level `ProviderInstanceConfig.enabled` is the single
1515 * flag going forward; this reader exists for legacy `providers.<kind>`
1516 * blobs and old settings files that still carry the flag in-config.
1499 * flag going forward; this reader exists for old settings files that still
1500 * carry the flag in-config.
15171501 */
15181502export const providerInstanceConfigEnabledFlag = (config: unknown): boolean | undefined => {
15191503 if (config === null || typeof config !== "object" || Array.isArray(config)) {
......@@ -1524,18 +1508,34 @@ export const providerInstanceConfigEnabledFlag = (config: unknown): boolean | un
15241508};
15251509
15261510/**
1527 * Default enabled state for a built-in driver when neither the envelope nor
1528 * the config blob carries a flag. Derived from the driver's settings schema
1529 * through `DEFAULT_SERVER_SETTINGS`, so the schema's decoding default stays
1530 * the single source of truth. Unknown (fork) drivers default to enabled.
1511 * Built-in drivers that stay off until the user turns them on. Matches the
1512 * `enabled` decoding default of each driver's settings schema.
15311513 */
1532const defaultEnabledForDriver = (driver: ProviderDriverKind): boolean => {
1533 const legacyDefaults = DEFAULT_SERVER_SETTINGS.providers as Record<
1534 string,
1535 { readonly enabled?: boolean } | undefined
1536 >;
1537 return legacyDefaults[driver]?.enabled ?? true;
1538};
1514const DEFAULT_DISABLED_PROVIDER_DRIVERS: ReadonlySet<ProviderDriverKind> = new Set(
1515 ["cursor", "grok", "muse", "pi", "opencode", "antigravity"].map((driver) =>
1516 ProviderDriverKind.make(driver),
1517 ),
1518);
1519
1520/** Built-in drivers whose default instance runs before the user configures it. */
1521const DEFAULT_ENABLED_DEFAULT_INSTANCES: ReadonlySet<ProviderInstanceId> = new Set(
1522 ["codex", "claudeAgent"].map((instanceId) => ProviderInstanceId.make(instanceId)),
1523);
1524
1525/**
1526 * Whether the built-in default instance at `instanceId` is enabled while
1527 * settings have no `providerInstances` entry for it. Only Codex and Claude
1528 * start on; any other id without an entry has no running instance.
1529 */
1530export const isUnconfiguredDefaultInstanceEnabled = (instanceId: ProviderInstanceId): boolean =>
1531 DEFAULT_ENABLED_DEFAULT_INSTANCES.has(instanceId);
1532
1533/**
1534 * Default enabled state for a driver when neither the envelope nor the config
1535 * blob carries a flag. Unknown (fork) drivers default to enabled.
1536 */
1537const defaultEnabledForDriver = (driver: ProviderDriverKind): boolean =>
1538 !DEFAULT_DISABLED_PROVIDER_DRIVERS.has(driver);
15391539
15401540/**
15411541 * Resolve whether a configured provider instance is enabled. An explicit
......@@ -1608,70 +1608,6 @@ const ModelSelectionPatch = Schema.Struct({
16081608 options: Schema.optionalKey(ProviderOptionSelections),
16091609});
16101610
1611const CodexSettingsPatch = Schema.Struct({
1612 enabled: Schema.optionalKey(Schema.Boolean),
1613 binaryPath: Schema.optionalKey(TrimmedString),
1614 homePath: Schema.optionalKey(TrimmedString),
1615 shadowHomePath: Schema.optionalKey(TrimmedString),
1616 launchArgs: Schema.optionalKey(TrimmedString),
1617 customModels: Schema.optionalKey(Schema.Array(CustomModelSetting)),
1618});
1619
1620const ClaudeSettingsPatch = Schema.Struct({
1621 enabled: Schema.optionalKey(Schema.Boolean),
1622 binaryPath: Schema.optionalKey(TrimmedString),
1623 homePath: Schema.optionalKey(TrimmedString),
1624 customModels: Schema.optionalKey(Schema.Array(CustomModelSetting)),
1625 launchArgs: Schema.optionalKey(TrimmedString),
1626 // Validated at the patch boundary so a typo fails the one update with a
1627 // schema error instead of a generic whole-settings failure.
1628 autoCompactWindow: Schema.optionalKey(
1629 TrimmedString.check(Schema.isPattern(CLAUDE_AUTO_COMPACT_WINDOW_PATTERN)),
1630 ),
1631});
1632
1633const CursorSettingsPatch = Schema.Struct({
1634 enabled: Schema.optionalKey(Schema.Boolean),
1635 customModels: Schema.optionalKey(Schema.Array(CustomModelSetting)),
1636});
1637
1638const GrokSettingsPatch = Schema.Struct({
1639 enabled: Schema.optionalKey(Schema.Boolean),
1640 binaryPath: Schema.optionalKey(TrimmedString),
1641 customModels: Schema.optionalKey(Schema.Array(CustomModelSetting)),
1642});
1643
1644const AntigravitySettingsPatch = Schema.Struct({
1645 enabled: Schema.optionalKey(Schema.Boolean),
1646 authMethod: Schema.optionalKey(AntigravityAuthMethod),
1647 apiKey: Schema.optionalKey(TrimmedString),
1648 gcpProject: Schema.optionalKey(TrimmedString),
1649 gcpLocation: Schema.optionalKey(TrimmedString),
1650 binaryPath: Schema.optionalKey(TrimmedString),
1651 customModels: Schema.optionalKey(Schema.Array(CustomModelSetting)),
1652});
1653
1654const MuseSettingsPatch = Schema.Struct({
1655 enabled: Schema.optionalKey(Schema.Boolean),
1656 binaryPath: Schema.optionalKey(TrimmedString),
1657 customModels: Schema.optionalKey(Schema.Array(CustomModelSetting)),
1658});
1659
1660const PiSettingsPatch = Schema.Struct({
1661 enabled: Schema.optionalKey(Schema.Boolean),
1662 binaryPath: Schema.optionalKey(TrimmedString),
1663 launchArgs: Schema.optionalKey(TrimmedString),
1664 customModels: Schema.optionalKey(Schema.Array(CustomModelSetting)),
1665});
1666
1667const OpenCodeSettingsPatch = Schema.Struct({
1668 enabled: Schema.optionalKey(Schema.Boolean),
1669 binaryPath: Schema.optionalKey(TrimmedString),
1670 serverUrl: Schema.optionalKey(TrimmedString),
1671 serverPassword: Schema.optionalKey(TrimmedString),
1672 customModels: Schema.optionalKey(Schema.Array(CustomModelSetting)),
1673});
1674
16751611export const ServerSettingsPatch = Schema.Struct({
16761612 worktreeCleanup: Schema.optionalKey(
16771613 Schema.NullOr(
......@@ -1791,18 +1727,6 @@ export const ServerSettingsPatch = Schema.Struct({
17911727 tokens: Schema.optionalKey(Schema.Record(GitHubHost, TrimmedString)),
17921728 }),
17931729 ),
1794 providers: Schema.optionalKey(
1795 Schema.Struct({
1796 codex: Schema.optionalKey(CodexSettingsPatch),
1797 claudeAgent: Schema.optionalKey(ClaudeSettingsPatch),
1798 cursor: Schema.optionalKey(CursorSettingsPatch),
1799 grok: Schema.optionalKey(GrokSettingsPatch),
1800 muse: Schema.optionalKey(MuseSettingsPatch),
1801 pi: Schema.optionalKey(PiSettingsPatch),
1802 opencode: Schema.optionalKey(OpenCodeSettingsPatch),
1803 antigravity: Schema.optionalKey(AntigravitySettingsPatch),
1804 }),
1805 ),
18061730 // Whole-map replacement for the new instance config. Patching individual
18071731 // entries is intentionally out of scope: the map is small, and partial
18081732 // patches risk leaving driver-specific config in a half-merged state.
packages/provider-core/src/server/driver.ts+8-5
......@@ -58,6 +58,12 @@ export interface ProviderDriverMetadata {
5858 * rejects multi-instance configurations with a clear error.
5959 */
6060 readonly supportsMultipleInstances?: boolean;
61 /**
62 * Whether the server runs an instance at `defaultInstanceIdForDriver(kind)`
63 * even when settings have no entry for it. Defaults to `true`; drivers that
64 * need user input before they can run (such as the ACP Registry) opt out.
65 */
66 readonly hasDefaultInstance?: boolean;
6167}
6268
6369export type ProviderWorkspaceSnapshot = ServerProvider &
......@@ -188,11 +194,8 @@ export interface ProviderDriver<Config, R = never> {
188194 */
189195 readonly configSchema: Schema.Codec<Config, unknown>;
190196 /**
191 * Default config payload used when the legacy
192 * `ServerSettings.providers.<kind>` entry is empty or when the driver
193 * is auto-bootstrapped without user configuration. Returning a typed
194 * default keeps the migration path simple — no special-casing needed
195 * to construct a "blank" instance.
197 * Config used when an instance has no config blob, such as the default
198 * instance of a driver the user never configured.
196199 */
197200 readonly defaultConfig: () => Config;
198201 /**
packages/shared/src/projectSettings.test.ts+13-2
......@@ -2,6 +2,7 @@ import {
22 DEFAULT_SERVER_SETTINGS,
33 PROJECT_SCOPED_SERVER_SETTING_KEYS,
44 ProjectId,
5 ProviderDriverKind,
56 ProviderInstanceId,
67} from "@t3tools/contracts";
78import { describe, expect, it } from "vite-plus/test";
......@@ -78,7 +79,12 @@ describe("resolveProjectSettings", () => {
7879 it("keeps the environment text generation model when the override's provider is disabled", () => {
7980 const disabledSelection = createModelSelection(ProviderInstanceId.make("claudeAgent"), "opus");
8081 const settings = applyServerSettingsPatch(DEFAULT_SERVER_SETTINGS, {
81 providers: { claudeAgent: { enabled: false } },
82 providerInstances: {
83 [ProviderInstanceId.make("claudeAgent")]: {
84 driver: ProviderDriverKind.make("claudeAgent"),
85 enabled: false,
86 },
87 },
8288 projectSettingsOverrides: {
8389 [projectId]: { textGenerationModelSelection: disabledSelection },
8490 },
......@@ -128,7 +134,12 @@ describe("resolveProjectSettings", () => {
128134 it("keeps the environment default model when the override's provider is disabled", () => {
129135 const disabledSelection = createModelSelection(ProviderInstanceId.make("claudeAgent"), "opus");
130136 const settings = applyServerSettingsPatch(DEFAULT_SERVER_SETTINGS, {
131 providers: { claudeAgent: { enabled: false } },
137 providerInstances: {
138 [ProviderInstanceId.make("claudeAgent")]: {
139 driver: ProviderDriverKind.make("claudeAgent"),
140 enabled: false,
141 },
142 },
132143 projectSettingsOverrides: { [projectId]: { defaultModelSelection: disabledSelection } },
133144 });
134145 const resolved = resolveProjectSettings(settings, projectId);
packages/shared/src/serverSettings.ts+2-14
......@@ -1,13 +1,12 @@
11import {
2 isProviderDriverKind,
32 isProviderAvailable,
3 isUnconfiguredDefaultInstanceEnabled,
44 resolveProviderInstanceEnabled,
55 isProviderTextGenerationCapable,
66 type ModelSelection,
77 type ProjectId,
88 type ProjectScopedServerSettingKey,
99 type ProjectSettingsOverrides,
10 type ProviderDriverKind,
1110 type ServerProvider,
1211 ServerSettings,
1312 type ServerSettingsPatch,
......@@ -59,14 +58,6 @@ export function resolveProjectAutoPull(
5958 );
6059}
6160
62type LegacyProviderSettings = ServerSettings["providers"][keyof ServerSettings["providers"]];
63
64const getLegacyProviderSettings = (
65 settings: ServerSettings,
66 provider: ProviderDriverKind,
67): LegacyProviderSettings | undefined =>
68 (settings.providers as Record<string, LegacyProviderSettings | undefined>)[provider];
69
7061export function isModelSelectionProviderEnabled(
7162 settings: ServerSettings,
7263 selection: ModelSelection,
......@@ -76,10 +67,7 @@ export function isModelSelectionProviderEnabled(
7667 return resolveProviderInstanceEnabled(instanceConfig);
7768 }
7869
79 return (
80 isProviderDriverKind(selection.instanceId) &&
81 getLegacyProviderSettings(settings, selection.instanceId)?.enabled === true
82 );
70 return isUnconfiguredDefaultInstanceEnabled(selection.instanceId);
8371}
8472
8573export function resolveSourceControlWriterModelSelection(