From 23a7019a2736165f4d9dc13874998260537b4ed3 Mon Sep 17 00:00:00 2001 From: maria-rcks <254055478+maria-rcks@users.noreply.github.com> Date: Sat, 10 Oct 2026 04:35:36 +0000 Subject: [PATCH] fix(server): show diffs for projects outside the server cwd Packaged desktop builds run the server from the home directory, so a project on another Windows drive failed the review workspace guard and the diff panel silently showed the server cwd's repository instead. Accept registered project roots, drop the web fallback, and read repository instructions for projects at a drive root. --- apps/server/src/git/GitManager.ts | 5 +- apps/server/src/review/ReviewService.test.ts | 56 ++++++++++++++++++++ apps/server/src/review/ReviewService.ts | 15 ++++++ apps/server/src/server.ts | 1 + apps/web/src/components/DiffPanel.tsx | 22 +------- 5 files changed, 77 insertions(+), 22 deletions(-) diff --git a/apps/server/src/git/GitManager.ts b/apps/server/src/git/GitManager.ts index 8ca0b6b976a9..63a8d0d8bac5 100644 --- a/apps/server/src/git/GitManager.ts +++ b/apps/server/src/git/GitManager.ts @@ -764,7 +764,10 @@ export const make = Effect.gen(function* () { Effect.gen(function* () { const root = yield* fileSystem.realPath(cwd); const instructionPath = yield* fileSystem.realPath(path.join(root, fileName)); - if (!instructionPath.startsWith(`${root}${path.sep}`)) { + // A drive root such as `D:\` already ends with a separator, so compare + // with path.relative instead of a `${root}${sep}` prefix. + const relative = path.relative(root, instructionPath); + if (relative === "" || relative.startsWith("..") || path.isAbsolute(relative)) { return ""; } const info = yield* fileSystem.stat(instructionPath); diff --git a/apps/server/src/review/ReviewService.test.ts b/apps/server/src/review/ReviewService.test.ts index 0802bac2d73a..384b8ad40d12 100644 --- a/apps/server/src/review/ReviewService.test.ts +++ b/apps/server/src/review/ReviewService.test.ts @@ -4,8 +4,10 @@ import * as Effect from "effect/Effect"; import * as FileSystem from "effect/FileSystem"; import * as Layer from "effect/Layer"; import * as PlatformError from "effect/PlatformError"; +import { ProjectId } from "@t3tools/contracts"; import * as ServerConfig from "../config.ts"; +import * as ProjectStore from "../orchestration-v2/ProjectStore.ts"; import * as ServerSettings from "../serverSettings.ts"; import * as GitVcsDriver from "../vcs/GitVcsDriver.ts"; import * as VcsDriverRegistry from "../vcs/VcsDriverRegistry.ts"; @@ -17,8 +19,30 @@ function layer(input: { readonly detectCalls?: Array<{ readonly cwd: string }>; readonly worktreesDirectory?: string; readonly previousWorktreesDirectories?: ReadonlyArray; + readonly projectRoots?: ReadonlyArray | "unavailable"; }) { + const projectRoots = input.projectRoots ?? []; return ReviewService.layer.pipe( + Layer.provide( + Layer.mock(ProjectStore.ProjectStoreV2)({ + listShells: () => + projectRoots === "unavailable" + ? Effect.fail( + new ProjectStore.ProjectStoreV2Error({ operation: "list", cause: "offline" }), + ) + : Effect.succeed( + projectRoots.map((workspaceRoot, index) => ({ + id: ProjectId.make(`project-${index}`), + title: `Project ${index}`, + workspaceRoot, + defaultModelSelection: null, + scripts: [], + createdAt: "2026-01-01T00:00:00.000Z", + updatedAt: "2026-01-01T00:00:00.000Z", + })), + ), + }), + ), Layer.provide( Layer.mock(VcsDriverRegistry.VcsDriverRegistry)({ get: () => Effect.die("unexpected VCS registry get"), @@ -134,6 +158,38 @@ describe("ReviewService", () => { }).pipe(Effect.provide(NodeServices.layer)), ); + it.effect("allows registered project roots outside the server cwd", () => + Effect.gen(function* () { + const fs = yield* FileSystem.FileSystem; + const workspaceRoot = yield* fs.makeTempDirectoryScoped({ prefix: "t3-review-workspace-" }); + const baseDir = yield* fs.makeTempDirectoryScoped({ prefix: "t3-review-base-" }); + // Stands in for a project on another drive than the server's home cwd. + const projectRoot = yield* fs.makeTempDirectoryScoped({ prefix: "t3-review-project-" }); + const projectChild = `${projectRoot}/packages/app`; + yield* fs.makeDirectory(projectChild, { recursive: true }); + const sibling = `${projectRoot}-sibling`; + yield* fs.makeDirectory(sibling); + yield* Effect.addFinalizer(() => fs.remove(sibling, { recursive: true }).pipe(Effect.ignore)); + + const review = (cwd: string, projectRoots: ReadonlyArray | "unavailable") => + Effect.gen(function* () { + const service = yield* ReviewService.ReviewService; + return yield* service.getDiffPreview({ cwd }); + }).pipe(Effect.provide(layer({ workspaceRoot, baseDir, projectRoots }))); + + assert.strictEqual((yield* review(projectRoot, [projectRoot])).cwd, projectRoot); + assert.strictEqual((yield* review(projectChild, [projectRoot])).cwd, projectChild); + for (const [cwd, projectRoots] of [ + [sibling, [projectRoot]], + [projectRoot, []], + [projectRoot, "unavailable"], + ] as const) { + const error = yield* review(cwd, projectRoots).pipe(Effect.flip); + assert.strictEqual(error._tag, "VcsRepositoryDetectionError"); + } + }).pipe(Effect.provide(NodeServices.layer)), + ); + it.effect("allows diff preview cwd inside the configured workspace root", () => Effect.gen(function* () { const fs = yield* FileSystem.FileSystem; diff --git a/apps/server/src/review/ReviewService.ts b/apps/server/src/review/ReviewService.ts index d8e27c9d6dd7..1bde38b589b4 100644 --- a/apps/server/src/review/ReviewService.ts +++ b/apps/server/src/review/ReviewService.ts @@ -16,6 +16,7 @@ import { } from "@t3tools/contracts"; import * as ServerConfig from "../config.ts"; +import * as ProjectStore from "../orchestration-v2/ProjectStore.ts"; import * as GitVcsDriver from "../vcs/GitVcsDriver.ts"; import * as VcsDriverRegistry from "../vcs/VcsDriverRegistry.ts"; import * as ServerSettings from "../serverSettings.ts"; @@ -42,6 +43,7 @@ export const make = Effect.gen(function* () { const vcsRegistry = yield* VcsDriverRegistry.VcsDriverRegistry; const git = yield* GitVcsDriver.GitVcsDriver; const settings = yield* ServerSettings.ServerSettingsService; + const projectStore = yield* ProjectStore.ProjectStoreV2; const canonicalizePath = (value: string) => { const resolvedPath = path.resolve(value); @@ -97,6 +99,19 @@ export const make = Effect.gen(function* () { return; } + // Registered projects can live outside the server cwd, which is the home + // directory in packaged desktop builds, e.g. a repository on another + // Windows drive. Unreadable or unresolvable project roots grant nothing. + const projects = yield* projectStore.listShells().pipe(Effect.orElseSucceed(() => [])); + for (const project of projects) { + const root = yield* canonicalizePath(project.workspaceRoot).pipe( + Effect.orElseSucceed(() => null), + ); + if (root !== null && isWithinRoot(candidate, root)) { + return; + } + } + return yield* new VcsRepositoryDetectionError({ operation, cwd, diff --git a/apps/server/src/server.ts b/apps/server/src/server.ts index 98a541409380..0707410e065e 100644 --- a/apps/server/src/server.ts +++ b/apps/server/src/server.ts @@ -373,6 +373,7 @@ const layerProjectCloneTracker = ProjectCloneTracker.layer.pipe( ); const layerReview = ReviewService.layer.pipe( + Layer.provide(ProjectStore.layer), Layer.provideMerge(GitVcsDriver.layer), Layer.provideMerge(layerVcsDriverRegistry), ); diff --git a/apps/web/src/components/DiffPanel.tsx b/apps/web/src/components/DiffPanel.tsx index 633094738e9e..0c7a4a28988d 100644 --- a/apps/web/src/components/DiffPanel.tsx +++ b/apps/web/src/components/DiffPanel.tsx @@ -339,7 +339,7 @@ export default function DiffPanel({ }, { enabled: isGitRepo && selectedTurn !== undefined }, ); - const primaryBranchDiffPreview = useEnvironmentQuery( + const branchDiffPreview = useEnvironmentQuery( canReadFiles && selectedRunId === null && activeThread && activeCwd ? reviewEnvironment.diffPreview({ environmentId: activeThread.environmentId, @@ -351,26 +351,6 @@ export default function DiffPanel({ }) : null, ); - const shouldRetryBranchDiffAtEnvironmentCwd = - selectedRunId === null && - primaryBranchDiffPreview.error?.includes("configured workspace root") === true && - serverConfig?.cwd !== undefined && - serverConfig.cwd !== activeCwd; - const fallbackBranchDiffPreview = useEnvironmentQuery( - canReadFiles && shouldRetryBranchDiffAtEnvironmentCwd && activeThread && serverConfig - ? reviewEnvironment.diffPreview({ - environmentId: activeThread.environmentId, - input: { - cwd: serverConfig.cwd, - ...(selectedBaseRef ? { baseRef: selectedBaseRef } : {}), - ignoreWhitespace: diffIgnoreWhitespace, - }, - }) - : null, - ); - const branchDiffPreview = shouldRetryBranchDiffAtEnvironmentCwd - ? fallbackBranchDiffPreview - : primaryBranchDiffPreview; const canRefreshGitDiff = isGitRepo && selectedRunId === null && activeThread != null && activeCwd != null; const activeThreadRefreshKey = routeThreadRef