History

feat: allow disabling provider update checks (#3130)

Ulises Britos committed 4 months ago

Commit 5d4e2fa1 parent 3e01c4b14 files · +239 −57Browse files

Unified · Split

14 files
  1. apps/server/src/provider/Drivers/ClaudeDriver.ts +19 −9
  2. apps/server/src/provider/Drivers/CodexDriver.ts +19 −9
  3. apps/server/src/provider/Drivers/CursorDriver.ts +17 −8
  4. apps/server/src/provider/Drivers/GrokDriver.ts +17 −8
  5. apps/server/src/provider/Drivers/OpenCodeDriver.ts +29 −17
  6. apps/server/src/provider/Layers/CursorProvider.ts +4 −1
  7. apps/server/src/provider/Layers/GrokProvider.ts +4 −1
  8. apps/server/src/provider/Layers/ProviderInstanceRegistryLive.test.ts +3 −0
  9. apps/server/src/provider/makeManagedServerProvider.ts +1 −1
  10. apps/server/src/provider/providerMaintenance.test.ts +41 −1
  11. apps/server/src/provider/providerMaintenance.ts +13 −2
  12. apps/server/src/provider/providerUpdateSettings.ts +43 −0
  13. apps/web/src/components/settings/SettingsPanels.tsx +27 −0
  14. packages/contracts/src/settings.ts +2 −0
apps/server/src/provider/Drivers/ClaudeDriver.ts+19−9
@@ -20,12 +20,12 @@ import * as Effect from "effect/Effect"; · 19 unmodified lines
2020 import * as FileSystem from "effect/FileSystem";
2121 import * as Path from "effect/Path";
2222 import * as Schema from "effect/Schema";
23−import * as Stream from "effect/Stream";
2423 import { HttpClient } from "effect/unstable/http";
2524 import { ChildProcessSpawner } from "effect/unstable/process";
2625
2726 import { makeClaudeTextGeneration } from "../../textGeneration/ClaudeTextGeneration.ts";
2827 import { ServerConfig } from "../../config.ts";
28+import { ServerSettingsService } from "../../serverSettings.ts";
2929 import { ProviderDriverError } from "../Errors.ts";
3030 import { makeClaudeAdapter } from "../Layers/ClaudeAdapter.ts";
3131 import {
@@ -48,6 +48,11 @@ import { · 16 unmodified lines
4848 normalizeCommandPath,
4949 resolveProviderMaintenanceCapabilitiesEffect,
5050 } from "../providerMaintenance.ts";
51+import {
52+ haveProviderSnapshotSettingsChanged,
53+ makeProviderSnapshotSettingsSource,
54+ type ProviderSnapshotSettings,
55+} from "../providerUpdateSettings.ts";
5156 import { makeClaudeCapabilitiesCacheKey, makeClaudeContinuationGroupKey } from "./ClaudeHome.ts";
5257 const decodeClaudeSettings = Schema.decodeSync(ClaudeSettings);
5358
@@ -83,7 +88,8 @@ export type ClaudeDriverEnv = · 29 unmodified lines
8388 | HttpClient.HttpClient
8489 | Path.Path
8590 | ProviderEventLoggers
86− | ServerConfig;
91+ | ServerConfig
92+ | ServerSettingsService;
8793
8894 const withInstanceIdentity =
8995 (input: {
@@ -114,6 +120,7 @@ export const ClaudeDriver: ProviderDriver<ClaudeSettings, ClaudeDriverEnv> = { · 24 unmodified lines
114120 const spawner = yield* ChildProcessSpawner.ChildProcessSpawner;
115121 const path = yield* Path.Path;
116122 const httpClient = yield* HttpClient.HttpClient;
123+ const serverSettings = yield* ServerSettingsService;
117124 const eventLoggers = yield* ProviderEventLoggers;
118125 const processEnv = mergeProviderInstanceEnvironment(environment);
119126 const fallbackContinuationIdentity = defaultProviderContinuationIdentity({
@@ -163,16 +170,19 @@ export const ClaudeDriver: ProviderDriver<ClaudeSettings, ClaudeDriverEnv> = { · 43 unmodified lines
163170 Effect.provideService(Path.Path, path),
164171 );
165172
166− const snapshot = yield* makeManagedServerProvider<ClaudeSettings>({
173+ const snapshotSettings = makeProviderSnapshotSettingsSource(effectiveConfig, serverSettings);
174+ const snapshot = yield* makeManagedServerProvider<ProviderSnapshotSettings<ClaudeSettings>>({
167175 maintenanceCapabilities,
168− getSettings: Effect.succeed(effectiveConfig),
169− streamSettings: Stream.never,
170− haveSettingsChanged: () => false,
176+ getSettings: snapshotSettings.getSettings,
177+ streamSettings: snapshotSettings.streamSettings,
178+ haveSettingsChanged: haveProviderSnapshotSettingsChanged,
171179 initialSnapshot: (settings) =>
172− makePendingClaudeProvider(settings).pipe(Effect.map(stampIdentity)),
180+ makePendingClaudeProvider(settings.provider).pipe(Effect.map(stampIdentity)),
173181 checkProvider,
174− enrichSnapshot: ({ snapshot, publishSnapshot }) =>
175− enrichProviderSnapshotWithVersionAdvisory(snapshot, maintenanceCapabilities).pipe(
182+ enrichSnapshot: ({ settings, snapshot, publishSnapshot }) =>
183+ enrichProviderSnapshotWithVersionAdvisory(snapshot, maintenanceCapabilities, {
184+ enableProviderUpdateChecks: settings.enableProviderUpdateChecks,
185+ }).pipe(
176186 Effect.provideService(HttpClient.HttpClient, httpClient),
177187 Effect.flatMap((enrichedSnapshot) => publishSnapshot(enrichedSnapshot)),
178188 ),
apps/server/src/provider/Drivers/CodexDriver.ts+19−9
@@ -28,12 +28,12 @@ import * as Effect from "effect/Effect"; · 27 unmodified lines
2828 import * as FileSystem from "effect/FileSystem";
2929 import * as Path from "effect/Path";
3030 import * as Schema from "effect/Schema";
31−import * as Stream from "effect/Stream";
3231 import { HttpClient } from "effect/unstable/http";
3332 import { ChildProcessSpawner } from "effect/unstable/process";
3433
3534 import { makeCodexTextGeneration } from "../../textGeneration/CodexTextGeneration.ts";
3635 import { ServerConfig } from "../../config.ts";
36+import { ServerSettingsService } from "../../serverSettings.ts";
3737 import { ProviderDriverError } from "../Errors.ts";
3838 import { makeCodexAdapter } from "../Layers/CodexAdapter.ts";
3939 import { checkCodexProviderStatus, makePendingCodexProvider } from "../Layers/CodexProvider.ts";
@@ -47,6 +47,11 @@ import { · 7 unmodified lines
4747 makePackageManagedProviderMaintenanceResolver,
4848 resolveProviderMaintenanceCapabilitiesEffect,
4949 } from "../providerMaintenance.ts";
50+import {
51+ haveProviderSnapshotSettingsChanged,
52+ makeProviderSnapshotSettingsSource,
53+ type ProviderSnapshotSettings,
54+} from "../providerUpdateSettings.ts";
5055 import {
5156 codexContinuationIdentity,
5257 materializeCodexShadowHome,
@@ -75,7 +80,8 @@ export type CodexDriverEnv = · 22 unmodified lines
7580 | HttpClient.HttpClient
7681 | Path.Path
7782 | ProviderEventLoggers
78− | ServerConfig;
83+ | ServerConfig
84+ | ServerSettingsService;
7985
8086 /**
8187 * Stamp instance identity onto a `ServerProvider` snapshot produced by the
@@ -111,6 +117,7 @@ export const CodexDriver: ProviderDriver<CodexSettings, CodexDriverEnv> = { · 29 unmodified lines
111117 Effect.gen(function* () {
112118 const spawner = yield* ChildProcessSpawner.ChildProcessSpawner;
113119 const httpClient = yield* HttpClient.HttpClient;
120+ const serverSettings = yield* ServerSettingsService;
114121 const eventLoggers = yield* ProviderEventLoggers;
115122 const processEnv = mergeProviderInstanceEnvironment(environment);
116123 const homeLayout = yield* resolveCodexHomeLayout(config);
@@ -163,16 +170,19 @@ export const CodexDriver: ProviderDriver<CodexSettings, CodexDriverEnv> = { · 46 unmodified lines
163170 Effect.map(stampIdentity),
164171 Effect.provideService(ChildProcessSpawner.ChildProcessSpawner, spawner),
165172 );
166− const snapshot = yield* makeManagedServerProvider<CodexSettings>({
173+ const snapshotSettings = makeProviderSnapshotSettingsSource(effectiveConfig, serverSettings);
174+ const snapshot = yield* makeManagedServerProvider<ProviderSnapshotSettings<CodexSettings>>({
167175 maintenanceCapabilities,
168− getSettings: Effect.succeed(effectiveConfig),
169− streamSettings: Stream.never,
170− haveSettingsChanged: () => false,
176+ getSettings: snapshotSettings.getSettings,
177+ streamSettings: snapshotSettings.streamSettings,
178+ haveSettingsChanged: haveProviderSnapshotSettingsChanged,
171179 initialSnapshot: (settings) =>
172− makePendingCodexProvider(settings).pipe(Effect.map(stampIdentity)),
180+ makePendingCodexProvider(settings.provider).pipe(Effect.map(stampIdentity)),
173181 checkProvider,
174− enrichSnapshot: ({ snapshot, publishSnapshot }) =>
175− enrichProviderSnapshotWithVersionAdvisory(snapshot, maintenanceCapabilities).pipe(
182+ enrichSnapshot: ({ settings, snapshot, publishSnapshot }) =>
183+ enrichProviderSnapshotWithVersionAdvisory(snapshot, maintenanceCapabilities, {
184+ enableProviderUpdateChecks: settings.enableProviderUpdateChecks,
185+ }).pipe(
176186 Effect.provideService(HttpClient.HttpClient, httpClient),
177187 Effect.flatMap((enrichedSnapshot) => publishSnapshot(enrichedSnapshot)),
178188 ),
apps/server/src/provider/Drivers/CursorDriver.ts+17−8
@@ -18,11 +18,11 @@ import * as Effect from "effect/Effect"; · 17 unmodified lines
1818 import * as FileSystem from "effect/FileSystem";
1919 import * as Path from "effect/Path";
2020 import * as Schema from "effect/Schema";
21−import * as Stream from "effect/Stream";
2221 import { HttpClient } from "effect/unstable/http";
2322 import { ChildProcessSpawner } from "effect/unstable/process";
2423
2524 import { ServerConfig } from "../../config.ts";
25+import { ServerSettingsService } from "../../serverSettings.ts";
2626 import { makeCursorTextGeneration } from "../../textGeneration/CursorTextGeneration.ts";
2727 import { ProviderDriverError } from "../Errors.ts";
2828 import { makeCursorAdapter } from "../Layers/CursorAdapter.ts";
@@ -45,6 +45,11 @@ import { · 16 unmodified lines
4545 makeStaticProviderMaintenanceResolver,
4646 resolveProviderMaintenanceCapabilitiesEffect,
4747 } from "../providerMaintenance.ts";
48+import {
49+ haveProviderSnapshotSettingsChanged,
50+ makeProviderSnapshotSettingsSource,
51+ type ProviderSnapshotSettings,
52+} from "../providerUpdateSettings.ts";
4853 const decodeCursorSettings = Schema.decodeSync(CursorSettings);
4954
5055 const DRIVER_KIND = ProviderDriverKind.make("cursor");
@@ -66,7 +71,8 @@ export type CursorDriverEnv = · 15 unmodified lines
6671 | HttpClient.HttpClient
6772 | Path.Path
6873 | ProviderEventLoggers
69− | ServerConfig;
74+ | ServerConfig
75+ | ServerSettingsService;
7076
7177 const withInstanceIdentity =
7278 (input: {
@@ -98,6 +104,7 @@ export const CursorDriver: ProviderDriver<CursorSettings, CursorDriverEnv> = { · 25 unmodified lines
98104 const fileSystem = yield* FileSystem.FileSystem;
99105 const path = yield* Path.Path;
100106 const httpClient = yield* HttpClient.HttpClient;
107+ const serverSettings = yield* ServerSettingsService;
101108 const eventLoggers = yield* ProviderEventLoggers;
102109 const processEnv = mergeProviderInstanceEnvironment(environment);
103110 const continuationIdentity = defaultProviderContinuationIdentity({
@@ -130,21 +137,23 @@ export const CursorDriver: ProviderDriver<CursorSettings, CursorDriverEnv> = { · 26 unmodified lines
130137 Effect.provideService(Path.Path, path),
131138 );
132139
133− const snapshot = yield* makeManagedServerProvider<CursorSettings>({
140+ const snapshotSettings = makeProviderSnapshotSettingsSource(effectiveConfig, serverSettings);
141+ const snapshot = yield* makeManagedServerProvider<ProviderSnapshotSettings<CursorSettings>>({
134142 maintenanceCapabilities,
135− getSettings: Effect.succeed(effectiveConfig),
136− streamSettings: Stream.never,
137− haveSettingsChanged: () => false,
143+ getSettings: snapshotSettings.getSettings,
144+ streamSettings: snapshotSettings.streamSettings,
145+ haveSettingsChanged: haveProviderSnapshotSettingsChanged,
138146 initialSnapshot: (settings) =>
139− buildInitialCursorProviderSnapshot(settings).pipe(Effect.map(stampIdentity)),
147+ buildInitialCursorProviderSnapshot(settings.provider).pipe(Effect.map(stampIdentity)),
140148 checkProvider,
141149 // Model catalog and capabilities come exclusively from Cursor's
142150 // list_available_models extension method during provider checks.
143151 enrichSnapshot: ({ settings, snapshot: currentSnapshot, publishSnapshot }) =>
144152 enrichCursorSnapshot({
145− settings,
153+ settings: settings.provider,
146154 snapshot: currentSnapshot,
147155 maintenanceCapabilities,
156+ enableProviderUpdateChecks: settings.enableProviderUpdateChecks,
148157 publishSnapshot,
149158 stampIdentity,
150159 httpClient,
apps/server/src/provider/Drivers/GrokDriver.ts+17−8
@@ -5,11 +5,11 @@ import * as Effect from "effect/Effect"; · 4 unmodified lines
55 import * as FileSystem from "effect/FileSystem";
66 import * as Path from "effect/Path";
77 import * as Schema from "effect/Schema";
8−import * as Stream from "effect/Stream";
98 import { HttpClient } from "effect/unstable/http";
109 import { ChildProcessSpawner } from "effect/unstable/process";
1110
1211 import { ServerConfig } from "../../config.ts";
12+import { ServerSettingsService } from "../../serverSettings.ts";
1313 import { makeGrokTextGeneration } from "../../textGeneration/GrokTextGeneration.ts";
1414 import { ProviderDriverError } from "../Errors.ts";
1515 import { makeGrokAdapter } from "../Layers/GrokAdapter.ts";
@@ -32,6 +32,11 @@ import { · 16 unmodified lines
3232 makeStaticProviderMaintenanceResolver,
3333 resolveProviderMaintenanceCapabilitiesEffect,
3434 } from "../providerMaintenance.ts";
35+import {
36+ haveProviderSnapshotSettingsChanged,
37+ makeProviderSnapshotSettingsSource,
38+ type ProviderSnapshotSettings,
39+} from "../providerUpdateSettings.ts";
3540 const decodeGrokSettings = Schema.decodeSync(GrokSettings);
3641
3742 const DRIVER_KIND = ProviderDriverKind.make("grok");
@@ -50,7 +55,8 @@ export type GrokDriverEnv = · 12 unmodified lines
5055 | HttpClient.HttpClient
5156 | Path.Path
5257 | ProviderEventLoggers
53− | ServerConfig;
58+ | ServerConfig
59+ | ServerSettingsService;
5460
5561 const withInstanceIdentity =
5662 (input: {
@@ -80,6 +86,7 @@ export const GrokDriver: ProviderDriver<GrokSettings, GrokDriverEnv> = { · 23 unmodified lines
8086 Effect.gen(function* () {
8187 const spawner = yield* ChildProcessSpawner.ChildProcessSpawner;
8288 const httpClient = yield* HttpClient.HttpClient;
89+ const serverSettings = yield* ServerSettingsService;
8390 const eventLoggers = yield* ProviderEventLoggers;
8491 const processEnv = mergeProviderInstanceEnvironment(environment);
8592 const continuationIdentity = defaultProviderContinuationIdentity({
@@ -110,18 +117,20 @@ export const GrokDriver: ProviderDriver<GrokSettings, GrokDriverEnv> = { · 24 unmodified lines
110117 Effect.provideService(ChildProcessSpawner.ChildProcessSpawner, spawner),
111118 );
112119
113− const snapshot = yield* makeManagedServerProvider<GrokSettings>({
120+ const snapshotSettings = makeProviderSnapshotSettingsSource(effectiveConfig, serverSettings);
121+ const snapshot = yield* makeManagedServerProvider<ProviderSnapshotSettings<GrokSettings>>({
114122 maintenanceCapabilities,
115− getSettings: Effect.succeed(effectiveConfig),
116− streamSettings: Stream.never,
117− haveSettingsChanged: () => false,
123+ getSettings: snapshotSettings.getSettings,
124+ streamSettings: snapshotSettings.streamSettings,
125+ haveSettingsChanged: haveProviderSnapshotSettingsChanged,
118126 initialSnapshot: (settings) =>
119− buildInitialGrokProviderSnapshot(settings).pipe(Effect.map(stampIdentity)),
127+ buildInitialGrokProviderSnapshot(settings.provider).pipe(Effect.map(stampIdentity)),
120128 checkProvider,
121− enrichSnapshot: ({ snapshot: currentSnapshot, publishSnapshot }) =>
129+ enrichSnapshot: ({ settings, snapshot: currentSnapshot, publishSnapshot }) =>
122130 enrichGrokSnapshot({
123131 snapshot: currentSnapshot,
124132 maintenanceCapabilities,
133+ enableProviderUpdateChecks: settings.enableProviderUpdateChecks,
125134 publishSnapshot,
126135 httpClient,
127136 }),
apps/server/src/provider/Drivers/OpenCodeDriver.ts+29−17
@@ -19,12 +19,12 @@ import * as Effect from "effect/Effect"; · 18 unmodified lines
1919 import * as FileSystem from "effect/FileSystem";
2020 import * as Path from "effect/Path";
2121 import * as Schema from "effect/Schema";
22−import * as Stream from "effect/Stream";
2322 import { HttpClient } from "effect/unstable/http";
2423 import { ChildProcessSpawner } from "effect/unstable/process";
2524
2625 import { makeOpenCodeTextGeneration } from "../../textGeneration/OpenCodeTextGeneration.ts";
2726 import { ServerConfig } from "../../config.ts";
27+import { ServerSettingsService } from "../../serverSettings.ts";
2828 import { ProviderDriverError } from "../Errors.ts";
2929 import { makeOpenCodeAdapter } from "../Layers/OpenCodeAdapter.ts";
3030 import {
@@ -47,6 +47,11 @@ import { · 16 unmodified lines
4747 normalizeCommandPath,
4848 resolveProviderMaintenanceCapabilitiesEffect,
4949 } from "../providerMaintenance.ts";
50+import {
51+ haveProviderSnapshotSettingsChanged,
52+ makeProviderSnapshotSettingsSource,
53+ type ProviderSnapshotSettings,
54+} from "../providerUpdateSettings.ts";
5055 const decodeOpenCodeSettings = Schema.decodeSync(OpenCodeSettings);
5156
5257 const DRIVER_KIND = ProviderDriverKind.make("opencode");
@@ -80,7 +85,8 @@ export type OpenCodeDriverEnv = · 27 unmodified lines
8085 | OpenCodeRuntime
8186 | Path.Path
8287 | ProviderEventLoggers
83− | ServerConfig;
88+ | ServerConfig
89+ | ServerSettingsService;
8490
8591 const withInstanceIdentity =
8692 (input: {
@@ -111,6 +117,7 @@ export const OpenCodeDriver: ProviderDriver<OpenCodeSettings, OpenCodeDriverEnv> · 24 unmodified lines
111117 const openCodeRuntime = yield* OpenCodeRuntime;
112118 const serverConfig = yield* ServerConfig;
113119 const httpClient = yield* HttpClient.HttpClient;
120+ const serverSettings = yield* ServerSettingsService;
114121 const eventLoggers = yield* ProviderEventLoggers;
115122 const processEnv = mergeProviderInstanceEnvironment(environment);
116123 const continuationIdentity = defaultProviderContinuationIdentity({
@@ -142,21 +149,26 @@ export const OpenCodeDriver: ProviderDriver<OpenCodeSettings, OpenCodeDriverEnv> · 25 unmodified lines
142149 processEnv,
143150 ).pipe(Effect.map(stampIdentity), Effect.provideService(OpenCodeRuntime, openCodeRuntime));
144151
145− const snapshot = yield* makeManagedServerProvider<OpenCodeSettings>({
146− maintenanceCapabilities,
147− getSettings: Effect.succeed(effectiveConfig),
148− streamSettings: Stream.never,
149− haveSettingsChanged: () => false,
150− initialSnapshot: (settings) =>
151− makePendingOpenCodeProvider(settings).pipe(Effect.map(stampIdentity)),
152− checkProvider,
153− enrichSnapshot: ({ snapshot, publishSnapshot }) =>
154− enrichProviderSnapshotWithVersionAdvisory(snapshot, maintenanceCapabilities).pipe(
155− Effect.provideService(HttpClient.HttpClient, httpClient),
156− Effect.flatMap((enrichedSnapshot) => publishSnapshot(enrichedSnapshot)),
157− ),
158− refreshInterval: SNAPSHOT_REFRESH_INTERVAL,
159− }).pipe(
152+ const snapshotSettings = makeProviderSnapshotSettingsSource(effectiveConfig, serverSettings);
153+ const snapshot = yield* makeManagedServerProvider<ProviderSnapshotSettings<OpenCodeSettings>>(
154+ {
155+ maintenanceCapabilities,
156+ getSettings: snapshotSettings.getSettings,
157+ streamSettings: snapshotSettings.streamSettings,
158+ haveSettingsChanged: haveProviderSnapshotSettingsChanged,
159+ initialSnapshot: (settings) =>
160+ makePendingOpenCodeProvider(settings.provider).pipe(Effect.map(stampIdentity)),
161+ checkProvider,
162+ enrichSnapshot: ({ settings, snapshot, publishSnapshot }) =>
163+ enrichProviderSnapshotWithVersionAdvisory(snapshot, maintenanceCapabilities, {
164+ enableProviderUpdateChecks: settings.enableProviderUpdateChecks,
165+ }).pipe(
166+ Effect.provideService(HttpClient.HttpClient, httpClient),
167+ Effect.flatMap((enrichedSnapshot) => publishSnapshot(enrichedSnapshot)),
168+ ),
169+ refreshInterval: SNAPSHOT_REFRESH_INTERVAL,
170+ },
171+ ).pipe(
160172 Effect.mapError(
161173 (cause) =>
162174 new ProviderDriverError({
apps/server/src/provider/Layers/CursorProvider.ts+4−1
@@ -1106,6 +1106,7 @@ export const enrichCursorSnapshot = (input: { · 1105 unmodified lines
11061106 readonly settings: CursorSettings;
11071107 readonly snapshot: ServerProvider;
11081108 readonly maintenanceCapabilities: ProviderMaintenanceCapabilities;
1109+ readonly enableProviderUpdateChecks?: boolean;
11091110 readonly publishSnapshot: (snapshot: ServerProvider) => Effect.Effect<void>;
11101111 readonly stampIdentity?: (snapshot: ServerProvider) => ServerProvider;
11111112 readonly httpClient: HttpClient.HttpClient;
@@ -1117,7 +1118,9 @@ export const enrichCursorSnapshot = (input: { · 5 unmodified lines
11171118 return Effect.void;
11181119 }
11191120
1120− return enrichProviderSnapshotWithVersionAdvisory(snapshot, input.maintenanceCapabilities).pipe(
1121+ return enrichProviderSnapshotWithVersionAdvisory(snapshot, input.maintenanceCapabilities, {
1122+ enableProviderUpdateChecks: input.enableProviderUpdateChecks,
1123+ }).pipe(
11211124 Effect.provideService(HttpClient.HttpClient, input.httpClient),
11221125 Effect.flatMap((enrichedSnapshot) =>
11231126 publishSnapshot(stampIdentity(enrichedSnapshot)).pipe(Effect.as(enrichedSnapshot)),
apps/server/src/provider/Layers/GrokProvider.ts+4−1
@@ -311,12 +311,15 @@ export const checkGrokProviderStatus = Effect.fn("checkGrokProviderStatus")(func · 310 unmodified lines
311311 export const enrichGrokSnapshot = (input: {
312312 readonly snapshot: ServerProvider;
313313 readonly maintenanceCapabilities: ProviderMaintenanceCapabilities;
314+ readonly enableProviderUpdateChecks?: boolean;
314315 readonly publishSnapshot: (snapshot: ServerProvider) => Effect.Effect<void>;
315316 readonly httpClient: HttpClient.HttpClient;
316317 }): Effect.Effect<void> => {
317318 const { snapshot, publishSnapshot } = input;
318319
319− return enrichProviderSnapshotWithVersionAdvisory(snapshot, input.maintenanceCapabilities).pipe(
320+ return enrichProviderSnapshotWithVersionAdvisory(snapshot, input.maintenanceCapabilities, {
321+ enableProviderUpdateChecks: input.enableProviderUpdateChecks,
322+ }).pipe(
320323 Effect.provideService(HttpClient.HttpClient, input.httpClient),
321324 Effect.flatMap((enrichedSnapshot) => publishSnapshot(enrichedSnapshot)),
322325 Effect.catchCause((cause) =>
apps/server/src/provider/Layers/ProviderInstanceRegistryLive.test.ts+3−0
@@ -39,6 +39,7 @@ import * as Layer from "effect/Layer"; · 38 unmodified lines
3939 import { HttpClient, HttpClientResponse } from "effect/unstable/http";
4040
4141 import { ServerConfig } from "../../config.ts";
42+import { ServerSettingsService } from "../../serverSettings.ts";
4243 import { ClaudeDriver } from "../Drivers/ClaudeDriver.ts";
4344 import { CodexDriver } from "../Drivers/CodexDriver.ts";
4445 import { CursorDriver } from "../Drivers/CursorDriver.ts";
@@ -107,6 +108,7 @@ describe("ProviderInstanceRegistryLive — multi-instance codex slice", () => { · 62 unmodified lines
107108 prefix: "provider-instance-registry-test",
108109 }).pipe(
109110 Layer.provideMerge(NodeServices.layer),
111+ Layer.provideMerge(ServerSettingsService.layerTest()),
110112 Layer.provideMerge(TestHttpClientLive),
111113 Layer.provideMerge(Layer.succeed(ProviderEventLoggers, NoOpProviderEventLoggers)),
112114 );
@@ -244,6 +246,7 @@ describe("ProviderInstanceRegistryLive — all drivers slice", () => { · 131 unmodified lines
244246 prefix: "provider-instance-registry-all-drivers-test",
245247 }).pipe(
246248 Layer.provideMerge(infraLayer),
249+ Layer.provideMerge(ServerSettingsService.layerTest()),
247250 Layer.provideMerge(TestHttpClientLive),
248251 Layer.provideMerge(Layer.succeed(ProviderEventLoggers, NoOpProviderEventLoggers)),
249252 );
apps/server/src/provider/makeManagedServerProvider.ts+1−1
@@ -21,7 +21,7 @@ export const makeManagedServerProvider = Effect.fn("makeManagedServerProvider")( · 20 unmodified lines
2121 Settings,
2222 >(input: {
2323 readonly maintenanceCapabilities: ServerProviderShape["maintenanceCapabilities"];
24− readonly getSettings: Effect.Effect<Settings>;
24+ readonly getSettings: Effect.Effect<Settings, ServerSettingsError>;
2525 readonly streamSettings: Stream.Stream<Settings>;
2626 readonly haveSettingsChanged: (previous: Settings, next: Settings) => boolean;
2727 readonly initialSnapshot: (settings: Settings) => Effect.Effect<ServerProvider>;
apps/server/src/provider/providerMaintenance.test.ts+41−1
@@ -4,13 +4,14 @@ import { chmodSync, mkdirSync, symlinkSync, writeFileSync } from "node:fs"; · 3 unmodified lines
44 import * as NodeServices from "@effect/platform-node/NodeServices";
55 import * as NodeOS from "node:os";
66 import path from "node:path";
7−import { ProviderDriverKind } from "@t3tools/contracts";
7+import { ProviderDriverKind, ProviderInstanceId, type ServerProvider } from "@t3tools/contracts";
88 import { HostProcessPlatform } from "@t3tools/shared/hostProcess";
99 import * as Crypto from "effect/Crypto";
1010 import * as Effect from "effect/Effect";
1111 import { HttpClient } from "effect/unstable/http";
1212 import {
1313 createProviderVersionAdvisory,
14+ enrichProviderSnapshotWithVersionAdvisory,
1415 makePackageManagedProviderMaintenanceResolver,
1516 makeProviderMaintenanceCapabilities,
1617 makeStaticProviderMaintenanceResolver,
@@ -67,6 +68,19 @@ const staticToolUpdate = makeStaticProviderMaintenanceResolver( · 50 unmodified lines
6768 updateLockKey: "static-tool",
6869 }),
6970 );
71+const installedPackageToolProvider: ServerProvider = {
72+ instanceId: ProviderInstanceId.make("packageTool"),
73+ driver: driver("packageTool"),
74+ enabled: true,
75+ installed: true,
76+ version: "1.0.0",
77+ status: "ready",
78+ auth: { status: "authenticated" },
79+ checkedAt: "2026-04-10T00:00:00.000Z",
80+ models: [],
81+ slashCommands: [],
82+ skills: [],
83+};
7084
7185 it.layer(NodeServices.layer)("providerMaintenance", (it) => {
7286 it.effect("reads cached versions through the injectable cache reference", () =>
@@ -95,6 +109,32 @@ it.layer(NodeServices.layer)("providerMaintenance", (it) => { · 22 unmodified lines
95109 ),
96110 );
97111
112+ it.effect("does not fetch latest provider versions when update checks are disabled", () =>
113+ enrichProviderSnapshotWithVersionAdvisory(
114+ installedPackageToolProvider,
115+ packageToolUpdate.resolve(),
116+ {
117+ enableProviderUpdateChecks: false,
118+ },
119+ ).pipe(
120+ Effect.provideService(ProviderVersionCache, new Map()),
121+ Effect.provideService(
122+ HttpClient.HttpClient,
123+ HttpClient.make(() =>
124+ Effect.die("disabled provider update checks should not make an HTTP request"),
125+ ),
126+ ),
127+ Effect.map((provider) => {
128+ expect(provider.versionAdvisory).toMatchObject({
129+ status: "unknown",
130+ currentVersion: "1.0.0",
131+ latestVersion: null,
132+ checkedAt: "2026-04-10T00:00:00.000Z",
133+ });
134+ }),
135+ ),
136+ );
137+
98138 it("marks providers with unknown current versions as unknown", () => {
99139 expect(
100140 createProviderVersionAdvisory({
apps/server/src/provider/providerMaintenance.ts+13−2
@@ -468,10 +468,21 @@ export const resolveLatestProviderVersion = Effect.fn("resolveLatestProviderVers · 467 unmodified lines
468468
469469 export const enrichProviderSnapshotWithVersionAdvisory = Effect.fn(
470470 "enrichProviderSnapshotWithVersionAdvisory",
471−)(function* (snapshot: ServerProvider, maintenanceCapabilities?: ProviderMaintenanceCapabilities) {
471+)(function* (
472+ snapshot: ServerProvider,
473+ maintenanceCapabilities?: ProviderMaintenanceCapabilities,
474+ options?: {
475+ readonly enableProviderUpdateChecks: boolean | undefined;
476+ },
477+) {
472478 const capabilities =
473479 maintenanceCapabilities ?? makeManualProviderMaintenanceCapabilities(snapshot.driver);
474− if (!snapshot.enabled || !snapshot.installed || !snapshot.version) {
480+ const shouldResolveLatestVersion =
481+ options?.enableProviderUpdateChecks !== false &&
482+ snapshot.enabled &&
483+ snapshot.installed &&
484+ Boolean(snapshot.version);
485+ if (!shouldResolveLatestVersion) {
475486 return {
476487 ...snapshot,
477488 versionAdvisory: createProviderVersionAdvisory({
apps/server/src/provider/providerUpdateSettings.ts+43−0
@@ -0,0 +1,43 @@
1+import type { ServerSettings, ServerSettingsError } from "@t3tools/contracts";
2+import * as Effect from "effect/Effect";
3+import * as Equal from "effect/Equal";
4+import * as Stream from "effect/Stream";
5+
6+import type { ServerSettingsShape } from "../serverSettings.ts";
7+
8+export interface ProviderSnapshotSettings<Settings> {
9+ readonly provider: Settings;
10+ readonly enableProviderUpdateChecks: boolean;
11+}
12+
13+export function makeProviderSnapshotSettings<Settings>(
14+ provider: Settings,
15+ settings: ServerSettings,
16+): ProviderSnapshotSettings<Settings> {
17+ return {
18+ provider,
19+ enableProviderUpdateChecks: settings.enableProviderUpdateChecks,
20+ };
21+}
22+
23+export function haveProviderSnapshotSettingsChanged<Settings>(
24+ previous: ProviderSnapshotSettings<Settings>,
25+ next: ProviderSnapshotSettings<Settings>,
26+): boolean {
27+ return !Equal.equals(previous, next);
28+}
29+
30+export function makeProviderSnapshotSettingsSource<Settings>(
31+ provider: Settings,
32+ serverSettings: ServerSettingsShape,
33+): {
34+ readonly getSettings: Effect.Effect<ProviderSnapshotSettings<Settings>, ServerSettingsError>;
35+ readonly streamSettings: Stream.Stream<ProviderSnapshotSettings<Settings>>;
36+} {
37+ const mapSettings = (settings: ServerSettings) =>
38+ makeProviderSnapshotSettings(provider, settings);
39+ return {
40+ getSettings: serverSettings.getSettings.pipe(Effect.map(mapSettings)),
41+ streamSettings: serverSettings.streamChanges.pipe(Stream.map(mapSettings)),
42+ };
43+}
apps/web/src/components/settings/SettingsPanels.tsx+27−0
@@ -668,6 +668,33 @@ export function GeneralSettingsPanel() { · 667 unmodified lines
668668 }
669669 />
670670
671+ <SettingsRow
672+ title="Provider update checks"
673+ description="Check installed provider CLIs for newer available versions."
674+ resetAction={
675+ settings.enableProviderUpdateChecks !==
676+ DEFAULT_UNIFIED_SETTINGS.enableProviderUpdateChecks ? (
677+ <SettingResetButton
678+ label="provider update checks"
679+ onClick={() =>
680+ updateSettings({
681+ enableProviderUpdateChecks: DEFAULT_UNIFIED_SETTINGS.enableProviderUpdateChecks,
682+ })
683+ }
684+ />
685+ ) : null
686+ }
687+ control={
688+ <Switch
689+ checked={settings.enableProviderUpdateChecks}
690+ onCheckedChange={(checked) =>
691+ updateSettings({ enableProviderUpdateChecks: Boolean(checked) })
692+ }
693+ aria-label="Check provider versions"
694+ />
695+ }
696+ />
697+
671698 <SettingsRow
672699 title="Auto-open task panel"
673700 description="Open the right-side plan and task panel automatically when steps appear."
packages/contracts/src/settings.ts+2−0
@@ -365,6 +365,7 @@ export const DEFAULT_AUTOMATIC_GIT_FETCH_INTERVAL = Duration.seconds(30); · 364 unmodified lines
365365
366366 export const ServerSettings = Schema.Struct({
367367 enableAssistantStreaming: Schema.Boolean.pipe(Schema.withDecodingDefault(Effect.succeed(false))),
368+ enableProviderUpdateChecks: Schema.Boolean.pipe(Schema.withDecodingDefault(Effect.succeed(true))),
368369 automaticGitFetchInterval: Schema.DurationFromMillis.pipe(
369370 Schema.withDecodingDefault(
370371 Effect.succeed(Duration.toMillis(DEFAULT_AUTOMATIC_GIT_FETCH_INTERVAL)),
@@ -482,6 +483,7 @@ const OpenCodeSettingsPatch = Schema.Struct({ · 111 unmodified lines
482483 export const ServerSettingsPatch = Schema.Struct({
483484 // Server settings
484485 enableAssistantStreaming: Schema.optionalKey(Schema.Boolean),
486+ enableProviderUpdateChecks: Schema.optionalKey(Schema.Boolean),
485487 automaticGitFetchInterval: Schema.optionalKey(Schema.DurationFromMillis),
486488 defaultThreadEnvMode: Schema.optionalKey(ThreadEnvMode),
487489 newWorktreesStartFromOrigin: Schema.optionalKey(Schema.Boolean),