- {versionMismatch &&
- (serverUpdateState.status === "idle" || serverUpdateState.status === "failed") ? (
+ {versionMismatch?.outdatedSide === "client" ? (
+
+ ) : versionMismatch &&
+ (serverUpdateState.status === "idle" || serverUpdateState.status === "failed") ? (
Client {primaryVersionMismatch.clientVersion}, server{" "}
- {primaryVersionMismatch.serverVersion}. Sync them if RPC calls or reconnects
- fail.
+ {primaryVersionMismatch.serverVersion}.{" "}
+ {primaryVersionMismatch.outdatedSide === "client"
+ ? clientUpdateGuidance()
+ : "Sync them if RPC calls or reconnects fail."}
) : null
}
control={
- primaryVersionMismatch &&
- primaryEnvironmentId !== null &&
- primaryServerUpdateState.status !== "running" ? (
+ primaryVersionMismatch?.outdatedSide === "client" ? (
+
+ ) : primaryVersionMismatch &&
+ primaryEnvironmentId !== null &&
+ primaryServerUpdateState.status !== "running" ? (
{
});
it("returns a mismatch when the server version differs from the client", () => {
+ const outdatedSide = resolveVersionOutdatedSide(APP_VERSION, "9.9.9");
expect(resolveVersionMismatch("9.9.9")).toEqual({
clientVersion: APP_VERSION,
serverVersion: "9.9.9",
- hint: "Version mismatch. Try syncing the client and server to the same T3 Code version.",
+ outdatedSide,
+ hint:
+ outdatedSide === "client"
+ ? "Version mismatch. Update the client to the same T3 Code version as the server."
+ : outdatedSide === "server"
+ ? "Version mismatch. Update the server to the same T3 Code version as the client."
+ : "Version mismatch. Try syncing the client and server to the same T3 Code version.",
});
});
+ it("marks the client outdated when the server nightly is newer", () => {
+ expect(
+ resolveVersionOutdatedSide("0.0.32-nightly.20260802.980", "0.0.32-nightly.20260803.985"),
+ ).toBe("client");
+ });
+
+ it("marks the server outdated when the client nightly is newer", () => {
+ expect(
+ resolveVersionOutdatedSide("0.0.32-nightly.20260803.985", "0.0.32-nightly.20260802.980"),
+ ).toBe("server");
+ });
+
it("reads the server version from config descriptors", () => {
expect(
resolveServerConfigVersionMismatch({
@@ -70,11 +91,11 @@ describe("versionSkew", () => {
).toBe(false);
});
- it("appends a hint to connection errors when versions differ", () => {
+ it("appends a direction-aware hint to connection errors when versions differ", () => {
const mismatch = resolveVersionMismatch("9.9.9");
expect(appendVersionMismatchHint("Socket closed.", mismatch)).toBe(
- "Socket closed. Hint: Version mismatch. Try syncing the client and server to the same T3 Code version.",
+ `Socket closed. Hint: ${mismatch?.hint}`,
);
});
@@ -106,5 +127,6 @@ describe("versionSkew", () => {
expect(serverUpdateGuidance(null, "Local server")).toBe(
"Relaunch the Local server with the copied command to sync them.",
);
+ expect(clientUpdateGuidance()).toBe("Update this client so they stay in sync.");
});
});
diff --git a/apps/web/src/versionSkew.ts b/apps/web/src/versionSkew.ts
index 6cf2a474269..f78e99b58bb 100644
--- a/apps/web/src/versionSkew.ts
+++ b/apps/web/src/versionSkew.ts
@@ -1,12 +1,17 @@
import type { EnvironmentId, ServerConfig, ServerSelfUpdateCapability } from "@t3tools/contracts";
+import { compareSemverVersions } from "@t3tools/shared/semver";
import * as Schema from "effect/Schema";
import { APP_VERSION } from "./branding";
import { getLocalStorageItem, setLocalStorageItem } from "./hooks/useLocalStorage";
+/** Which side of a version mismatch is behind, or unknown when ordering is inconclusive. */
+export type VersionOutdatedSide = "client" | "server" | "unknown";
+
export interface VersionMismatch {
readonly clientVersion: string;
readonly serverVersion: string;
+ readonly outdatedSide: VersionOutdatedSide;
readonly hint: string;
}
@@ -23,6 +28,31 @@ function normalizeVersion(version: string | null | undefined): string | null {
return trimmed && trimmed.length > 0 ? trimmed : null;
}
+export function resolveVersionOutdatedSide(
+ clientVersion: string,
+ serverVersion: string,
+): VersionOutdatedSide {
+ const comparison = compareSemverVersions(clientVersion, serverVersion);
+ if (comparison < 0) {
+ return "client";
+ }
+ if (comparison > 0) {
+ return "server";
+ }
+ return "unknown";
+}
+
+function versionMismatchHint(outdatedSide: VersionOutdatedSide): string {
+ switch (outdatedSide) {
+ case "client":
+ return "Version mismatch. Update the client to the same T3 Code version as the server.";
+ case "server":
+ return "Version mismatch. Update the server to the same T3 Code version as the client.";
+ default:
+ return "Version mismatch. Try syncing the client and server to the same T3 Code version.";
+ }
+}
+
export function resolveVersionMismatch(
serverVersion: string | null | undefined,
): VersionMismatch | null {
@@ -36,10 +66,13 @@ export function resolveVersionMismatch(
return null;
}
+ const outdatedSide = resolveVersionOutdatedSide(normalizedClientVersion, normalizedServerVersion);
+
return {
clientVersion: normalizedClientVersion,
serverVersion: normalizedServerVersion,
- hint: "Version mismatch. Try syncing the client and server to the same T3 Code version.",
+ outdatedSide,
+ hint: versionMismatchHint(outdatedSide),
};
}
@@ -79,6 +112,11 @@ export function serverUpdateGuidance(
}
}
+/** One sentence telling the user to update this client when it is behind the server. */
+export function clientUpdateGuidance(): string {
+ return "Update this client so they stay in sync.";
+}
+
export function buildVersionMismatchDismissalKey(
environmentId: EnvironmentId,
mismatch: Pick,
diff --git a/docs/user/updating.md b/docs/user/updating.md
index a0cc0e5d1e0..f9e58f97f05 100644
--- a/docs/user/updating.md
+++ b/docs/user/updating.md
@@ -1,7 +1,8 @@
# Keeping T3 Code in Sync
The T3 Code web or desktop app and the server it connects to work best when they use the same
-version. If they do not match, T3 Code shows a warning with the right update option for that server.
+version. If they do not match, T3 Code shows a warning with the right update option for whichever
+side is behind.
## Where to Find the Update
@@ -11,25 +12,30 @@ You may see the warning in either of these places:
- **Settings** → **Connections**, beside the affected connection
Dismissing the conversation warning only hides that reminder for those two versions. It does not
-update the server, and the version difference remains visible in Connections.
+update either side, and the version difference remains visible in Connections.
## Before You Update
-Let active agent work and terminal commands finish first. Updating restarts the server, so the
-connection will disappear briefly and work that is still running may be interrupted.
+If the warning asks you to update the **server**, let active agent work and terminal commands finish
+first. Updating restarts the server, so the connection will disappear briefly and work that is still
+running may be interrupted.
+
+If the warning asks you to update this **client**, finish local work you care about before
+restarting the desktop app.
The update does not remove saved threads, settings, or project files.
## Choose the Action You See
-| Action | What to do |
-| -------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
-| **Update server** | Available for the T3 Code Linux background service. Select the button and leave T3 Code open while it prepares, tests, restarts, and reconnects. |
-| **Update the desktop app** | Open the T3 Code desktop app on the machine that runs the server and install the app update there. Reopen it if needed. |
-| **Copy update command** | Copy the command, open a terminal on the server machine, stop the current T3 Code server, and relaunch it with the copied command and any startup options you normally use. |
+| Action | What to do |
+| -------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
+| **Update client** | Shown when this app is older than the connected server. On the desktop app, select the button to check for, download, and install the newer client build. |
+| **Update server** | Shown when the connected server is older than this client. Available for the T3 Code Linux background service. Leave T3 Code open while it prepares, tests, restarts, and reconnects. |
+| **Update the desktop app** | Shown for a server managed by a desktop app on another machine. Open T3 Code there and install the app update. Reopen it if needed. |
+| **Copy update command** | Copy the command, open a terminal on the server machine, stop the current T3 Code server, and relaunch it with the copied command and any startup options you normally use. |
-The available action depends on how that server was started. T3 Code does not update connected
-servers silently in the background.
+The available action depends on which side is behind and how that server was started. T3 Code does
+not update connected servers silently in the background.
If the requested version includes a database update, remote installation stops before restart and
asks you to run the exact `npx t3@ service update` command on the server machine. This is