# Files changed · PR \#23859 · pingdotgg/t3code

[View on GitCafe](https://git.cafe/pingdotgg/t3code/pull/23859/diffs)

Repository: [pingdotgg/t3code](https://git.cafe/pingdotgg/t3code)

Visibility: public

[fix\(opencode\): leave configured server updates to their owner](https://git.cafe/pingdotgg/t3code/pull/23859.md)

Version: 6

Head commit: e7ea6f5d2b45ccd64b3d4520f51459fb09aee420

Base commit: 1316d48b0c68151afd8b47eb0c11a114cf0b4daa

Comparison base: 1316d48b0c68151afd8b47eb0c11a114cf0b4daa

Showing up to 20 entries from this revision. If the pull request changes, reopen the first page before continuing.

## apps/mobile/src/features/settings/SettingsEnvironmentDetailRouteScreen.tsx

Status: modified; +2; −1

```diff
@@ -356,7 +356,8 @@
                           {provider.versionAdvisory?.status === "behind_latest" &&
                           !provider.versionAdvisory.canUpdate ? (
                             <Text className="text-sm text-foreground-muted">
+                              {provider.versionAdvisory.message ??
+                                "Update this provider on the environment's machine."}
-                              Update this provider on the environment's machine.
                             </Text>
                           ) : null}
                         </View>
```

## docs/user/providers-opencode.md

Status: modified; +8; −2

```diff
@@ -13,8 +13,8 @@
 
 OpenCode 2 is a separate package, `@opencode/cli`. To move from 1.x, install it
 yourself, for example `npm install -g @opencode/cli`. Then refresh provider status.
+When T3 Code manages the local server, its update button updates whichever package
+you have installed. It never switches a 1.x install to 2.x.
-T3 Code's update button updates whichever package you have installed. It never
-switches a 1.x install to 2.x.
 
 OpenCode 2 converts the shared OpenCode database to its own format the first time it
 runs. Don't run OpenCode 1.x and 2.x side by side on the same machine. Threads you
@@ -34,6 +34,12 @@
 does not forward a local `OPENCODE_SERVER_PASSWORD`. If connection or version checks
 fail, check the URL, credentials, and OpenCode version, then refresh provider status.
 
+With **Server URL** set, update the server through the app or tool that started it,
+then restart it when active sessions can safely be interrupted. This also applies
+to localhost: that server may use a different installation from the local CLI.
+T3 Code reports the running server's version and leaves its updates to you. After
+updating it, refresh provider status to check the new version.
+
 After a lost connection, send another prompt to reconnect to the same OpenCode
 session.
 
```

## packages/provider-opencode/src/server/driver.test.ts

Status: modified; +92; −1

```diff
@@ -11,6 +11,7 @@
 import * as Deferred from "effect/Deferred";
 import * as Effect from "effect/Effect";
 import * as Fiber from "effect/Fiber";
+import * as FileSystem from "effect/FileSystem";
 import * as Layer from "effect/Layer";
 import * as Logger from "effect/Logger";
 import * as References from "effect/References";
@@ -26,8 +27,8 @@
 import * as ProviderEventLoggers from "@t3tools/provider-core/server/ProviderEventLoggers";
 import * as ProviderMaintenance from "@t3tools/provider-core/server/maintenanceResolver";
 import * as OpenCodeRuntime from "./OpenCodeRuntime.ts";
+import { OpenCodeDriver, openCodeUpdateFor } from "./driver.ts";
 import { OPENCODE_2_RESPONSES, replayOpenCodeServer } from "./probeResponses.fixture.ts";
-import { OpenCodeDriver, openCodeUpdateFor } from "./driver.ts";
 import * as TestProviderHost from "@t3tools/provider-testing/TestProviderHost";
 
 const serverStarts: Array<string> = [];
@@ -71,7 +72,97 @@
 
 const noHttp = HttpClient.make(() => Effect.die("A local binary must not be probed over HTTP"));
 
+const nativeInstall = Effect.gen(function* () {
+  const fileSystem = yield* FileSystem.FileSystem;
+  const root = yield* fileSystem.makeTempDirectoryScoped({ prefix: "t3-opencode-update-owner-" });
+  const directory = NodePath.join(root, ".opencode", "bin");
+  yield* fileSystem.makeDirectory(directory, { recursive: true });
+  const binaryPath = NodePath.join(
+    directory,
+    HostProcess.Platform.defaultValue() === "win32" ? "opencode.exe" : "opencode",
+  );
+  yield* fileSystem.writeFileString(binaryPath, "fixture");
+  yield* fileSystem.chmod(binaryPath, 0o755);
+  return binaryPath;
+});
+
 it.layer(layer)("OpenCodeDriver runtime selection", (it) => {
+  it.effect.each(["http://127.0.0.1:4096", "https://server.example.test"])(
+    "keeps updates owned by the configured server at %s",
+    (serverUrl) =>
+      Effect.gen(function* () {
+        const binaryPath = yield* nativeInstall;
+        const cliProbes: string[] = [];
+        const runtime = {
+          ...openCode2Runtime,
+          runOpenCodeCommand: () => {
+            cliProbes.push("--version");
+            return Effect.succeed({ stdout: "opencode v2.1.3\n", stderr: "", code: 0 });
+          },
+        } as OpenCodeRuntime.OpenCodeRuntime["Service"];
+        const responses = {
+          ...OPENCODE_2_RESPONSES,
+          "/api/info": {
+            ...OPENCODE_2_RESPONSES["/api/info"],
+            body: OPENCODE_2_RESPONSES["/api/info"].body.replace("2.0.18", "2.0.32"),
+          },
+        };
+        const latestVersions = yield* ProviderLatestVersions.make([["@opencode/cli", "2.1.3"]]);
+        const instance = yield* create(
+          { binaryPath, serverUrl },
+          replayOpenCodeServer(responses, ""),
+        ).pipe(
+          Effect.provideService(OpenCodeRuntime.OpenCodeRuntime, runtime),
+          Effect.provideService(ProviderLatestVersions.ProviderLatestVersions, latestVersions),
+        );
+        const maintenance = yield* instance.snapshot.resolveMaintenance({ fresh: true });
+        assert.strictEqual(maintenance.packageName, "@opencode/cli");
+        assert.isNull(maintenance.update);
+
+        for (const runningVersion of ["2.0.32", "2.0.32", "2.1.3"]) {
+          responses["/api/info"].body = OPENCODE_2_RESPONSES["/api/info"].body.replace(
+            "2.0.18",
+            runningVersion,
+          );
+          const enriched = yield* instance.snapshot.streamChanges.pipe(
+            Stream.filter(
+              (snapshot) => snapshot.versionAdvisory?.currentVersion === runningVersion,
+            ),
+            Stream.take(1),
+            Stream.runCollect,
+            Effect.forkChild({ startImmediately: true }),
+          );
+          yield* instance.snapshot.refresh;
+          const [snapshot] = yield* Fiber.join(enriched);
+          assert.ok(snapshot);
+          assert.strictEqual(snapshot.version, runningVersion);
+          assert.strictEqual(snapshot.versionAdvisory?.canUpdate, false);
+          assert.strictEqual(snapshot.versionAdvisory?.canInstallVersion, false);
+          assert.isNull(snapshot.versionAdvisory?.updateCommand);
+          assert.strictEqual(
+            snapshot.versionAdvisory?.status,
+            runningVersion === "2.1.3" ? "current" : "behind_latest",
+          );
+          if (runningVersion !== "2.1.3") {
+            assert.include(snapshot.versionAdvisory?.message ?? "", "Version 2.1.3 is out.");
+            assert.include(snapshot.versionAdvisory?.message ?? "", "configured OpenCode server");
+            assert.include(snapshot.versionAdvisory?.message ?? "", "refresh provider status");
+          }
+        }
+        assert.deepStrictEqual(cliProbes, []);
+      }).pipe(Effect.scoped),
+  );
+
+  it.effect("still offers native CLI updates when T3 manages the local server", () =>
+    Effect.gen(function* () {
+      const binaryPath = yield* nativeInstall;
+      const instance = yield* create({ binaryPath, serverUrl: " " }, noHttp);
+      const maintenance = yield* instance.snapshot.resolveMaintenance({ fresh: true });
+      assert.strictEqual(maintenance.update?.executable, binaryPath);
+      assert.deepStrictEqual(maintenance.update?.args, ["upgrade"]);
+    }).pipe(Effect.scoped),
+  );
+
   it.effect("never starts a 1.x server for an OpenCode 2 instance", () =>
     Effect.gen(function* () {
       serverStarts.length = 0;
```

## packages/provider-opencode/src/server/driver.ts

Status: modified; +21; −5

```diff
@@ -207,6 +207,7 @@
       continuationGroupKey: continuationIdentity.continuationKey,
     });
     const effectiveConfig = { ...config, enabled } satisfies OpenCodeSettings;
+    const isExternalServer = effectiveConfig.serverUrl.trim().length > 0;
     const runtimeProbe = yield* makeOpenCodeRuntimeProbe(
       probeOpenCodeRuntime(effectiveConfig, processEnv).pipe(
         Effect.provideService(HttpClient.HttpClient, httpClient),
@@ -224,10 +225,13 @@
         Effect.flatMap((generation) =>
           Option.isNone(generation)
             ? Effect.succeed(noUpdate)
+            : isExternalServer
+              ? // Even loopback URLs can point to another installation or a forwarded server.
+                openCodeUpdateFor(generation.value).resolve(null)
+              : resolveProviderMaintenanceCapabilitiesEffect(openCodeUpdateFor(generation.value), {
+                  binaryPath: effectiveConfig.binaryPath,
+                  env: processEnv,
-            : resolveProviderMaintenanceCapabilitiesEffect(openCodeUpdateFor(generation.value), {
-                binaryPath: effectiveConfig.binaryPath,
-                env: processEnv,
-              }),
+                }),
         ),
         Effect.provideService(ChildProcessSpawner.ChildProcessSpawner, spawner),
         Effect.provideService(FileSystem.FileSystem, fileSystem),
@@ -454,7 +458,19 @@
           ),
           Effect.provideService(HttpClient.HttpClient, httpClient),
           Effect.provideService(ProviderLatestVersions.ProviderLatestVersions, latestVersions),
-          Effect.flatMap((enrichedSnapshot) => publishSnapshot(enrichedSnapshot)),
+          Effect.flatMap((enrichedSnapshot) =>
+            publishSnapshot(
+              isExternalServer && enrichedSnapshot.versionAdvisory?.status === "behind_latest"
+                ? {
+                    ...enrichedSnapshot,
+                    versionAdvisory: {
+                      ...enrichedSnapshot.versionAdvisory,
+                      message: `Version ${enrichedSnapshot.versionAdvisory.latestVersion} is out. Update the configured OpenCode server with the app or tool that started it. Restart that server when active sessions can be interrupted, then refresh provider status.`,
+                    },
+                  }
+                : enrichedSnapshot,
+            ),
+          ),
         ),
     }).pipe(
       Effect.mapError(
```
