diff --git a/apps/web/src/components/WorktreeSourceControl.logic.test.ts b/apps/web/src/components/WorktreeSourceControl.logic.test.ts index b9fc7a65db8..29bf5e611f1 100644 --- a/apps/web/src/components/WorktreeSourceControl.logic.test.ts +++ b/apps/web/src/components/WorktreeSourceControl.logic.test.ts @@ -1,6 +1,57 @@ import { describe, expect, it } from "vite-plus/test"; -import { resolveWorktreeDiffSource } from "./WorktreeSourceControl.logic"; +import { + resolveWorktreeCompatibilityNotice, + resolveWorktreeDiffSource, +} from "./WorktreeSourceControl.logic"; + +describe("resolveWorktreeCompatibilityNotice", () => { + it("does not warn when the environment supports source-control mutations", () => { + expect( + resolveWorktreeCompatibilityNotice({ + supportsMutations: true, + serverVersion: "1.2.3", + versionMismatch: { + clientVersion: "1.2.4", + serverVersion: "1.2.3", + }, + }), + ).toBeNull(); + }); + + it("identifies version drift when a mixed-version environment lacks the capability", () => { + expect( + resolveWorktreeCompatibilityNotice({ + supportsMutations: false, + serverVersion: "1.2.3", + versionMismatch: { + clientVersion: "1.2.4", + serverVersion: "1.2.3", + }, + }), + ).toEqual({ + kind: "version-mismatch", + label: "Version mismatch", + detail: + "Client 1.2.4 · environment 1.2.3. Diffs remain available in read-only compatibility mode; update the environment to restore stage and discard actions.", + }); + }); + + it("reports a missing capability even when version strings match", () => { + expect( + resolveWorktreeCompatibilityNotice({ + supportsMutations: false, + serverVersion: "1.2.3", + versionMismatch: null, + }), + ).toEqual({ + kind: "limited-capability", + label: "Limited compatibility", + detail: + "Environment 1.2.3 does not advertise source-control mutations. Update or restart it to restore stage and discard actions.", + }); + }); +}); describe("resolveWorktreeDiffSource", () => { it("uses the exact index-aware source when available", () => { diff --git a/apps/web/src/components/WorktreeSourceControl.logic.ts b/apps/web/src/components/WorktreeSourceControl.logic.ts index c34df0adebf..0436dbe3419 100644 --- a/apps/web/src/components/WorktreeSourceControl.logic.ts +++ b/apps/web/src/components/WorktreeSourceControl.logic.ts @@ -2,6 +2,37 @@ import type { ReviewDiffPreviewSourceKind } from "@t3tools/contracts"; export type WorktreeChangeScope = "staged" | "unstaged"; +export interface WorktreeCompatibilityNotice { + readonly kind: "version-mismatch" | "limited-capability"; + readonly label: string; + readonly detail: string; +} + +export function resolveWorktreeCompatibilityNotice(input: { + readonly supportsMutations: boolean; + readonly serverVersion: string; + readonly versionMismatch: { + readonly clientVersion: string; + readonly serverVersion: string; + } | null; +}): WorktreeCompatibilityNotice | null { + if (input.supportsMutations) return null; + + if (input.versionMismatch) { + return { + kind: "version-mismatch", + label: "Version mismatch", + detail: `Client ${input.versionMismatch.clientVersion} · environment ${input.versionMismatch.serverVersion}. Diffs remain available in read-only compatibility mode; update the environment to restore stage and discard actions.`, + }; + } + + return { + kind: "limited-capability", + label: "Limited compatibility", + detail: `Environment ${input.serverVersion} does not advertise source-control mutations. Update or restart it to restore stage and discard actions.`, + }; +} + /** * Older environments only return the combined `working-tree` review source. * Treat it as the unstaged source when no index-aware sources are present so diff --git a/apps/web/src/components/WorktreeSourceControl.tsx b/apps/web/src/components/WorktreeSourceControl.tsx index e2e511269ed..80510566f57 100644 --- a/apps/web/src/components/WorktreeSourceControl.tsx +++ b/apps/web/src/components/WorktreeSourceControl.tsx @@ -17,6 +17,7 @@ import { RotateCcwIcon, Rows3Icon, TextWrapIcon, + TriangleAlertIcon, } from "lucide-react"; import { memo, useCallback, useMemo, useState } from "react"; @@ -35,6 +36,7 @@ import { useEnvironmentQuery } from "~/state/query"; import { serverEnvironment } from "~/state/server"; import { useAtomCommand } from "~/state/use-atom-command"; import { vcsEnvironment } from "~/state/vcs"; +import { resolveServerConfigVersionMismatch } from "~/versionSkew"; import { useClientSettings } from "../hooks/useSettings"; import { useTheme } from "../hooks/useTheme"; @@ -45,7 +47,11 @@ import { DIFF_VIEW_UNSAFE_CSS } from "./diffs/diffViewStyles"; import { Toggle, ToggleGroup } from "./ui/toggle-group"; import { Tooltip, TooltipPopup, TooltipTrigger } from "./ui/tooltip"; import { stackedThreadToast, toastManager } from "./ui/toast"; -import { resolveWorktreeDiffSource, type WorktreeChangeScope } from "./WorktreeSourceControl.logic"; +import { + resolveWorktreeCompatibilityNotice, + resolveWorktreeDiffSource, + type WorktreeChangeScope, +} from "./WorktreeSourceControl.logic"; export type { WorktreeChangeScope } from "./WorktreeSourceControl.logic"; @@ -318,6 +324,13 @@ export function WorktreeSourceControl({ const stagedFiles = useMemo(() => files.filter(isStaged), [files]); const unstagedFiles = useMemo(() => files.filter(isUnstaged), [files]); const canMutate = serverConfig?.environment.capabilities.worktreeSourceControl === true; + const compatibilityNotice = serverConfig + ? resolveWorktreeCompatibilityNotice({ + supportsMutations: canMutate, + serverVersion: serverConfig.environment.serverVersion, + versionMismatch: resolveServerConfigVersionMismatch(serverConfig), + }) + : null; const selectedSource = resolveWorktreeDiffSource(preview.data?.sources ?? [], selectedScope); const renderablePatch = useMemo( () => @@ -472,6 +485,25 @@ export function WorktreeSourceControl({
- Read-only compatibility mode. Update this environment to stage or discard - changes. -
+ {compatibilityNotice ? ( +{compatibilityNotice.label}
++ {compatibilityNotice.detail} +
+