From 6632e5256fe9771cf626da329949bbd634df3f16 Mon Sep 17 00:00:00 2001 From: Johan Vaz Date: Mon, 2 Nov 2026 21:09:16 +0800 Subject: [PATCH] fix(web): open files from Claude and ACP tool-call diffs Claude and ACP providers record tool-call edits with absolute file paths. resolveDiffPathForWorkspace rejected every absolute path, so clicking a file name in a tool-call diff did nothing and its context menu was empty. Absolute paths inside the workspace now resolve to workspace-relative paths; paths outside it are still rejected. Co-Authored-By: Claude Opus 5.5 --- apps/web/src/diffFileActions.test.ts | 54 +++++++++++++++++++--------- apps/web/src/diffFileActions.ts | 14 +++++++- 2 files changed, 51 insertions(+), 17 deletions(-) diff --git a/apps/web/src/diffFileActions.test.ts b/apps/web/src/diffFileActions.test.ts index b4fdb71f8d89a..f7faf72ef8bd3 100644 --- a/apps/web/src/diffFileActions.test.ts +++ b/apps/web/src/diffFileActions.test.ts @@ -69,23 +69,45 @@ describe("openDiffFilePrimaryAction", () => { ).toBe("index.ts"); }); - it.each(["backend/server.ts", "frontend2/app.ts", "frontend/../secret.ts", "C:secret.ts"])( - "does not open an out-of-project diff path: %s", - (filePath) => { - const openInEditor = vi.fn(); - - openDiffFilePrimaryAction({ - threadRef: THREAD_REF, - filePath, - activeCwd: "/repo/frontend", - repositoryRoot: "/repo", - openInEditor, - }); - + it.each([ + { + filePath: "/repo/frontend/src/index.ts", + workspaceRoot: "/repo/frontend", + expected: "src/index.ts", + }, + { filePath: "c:\\Repo\\src\\index.ts", workspaceRoot: "C:\\repo", expected: "src/index.ts" }, + ])( + "opens absolute diff paths inside the workspace: $filePath", + ({ filePath, workspaceRoot, expected }) => { expect( - selectThreadRightPanelState(useRightPanelStore.getState().byThreadKey, THREAD_REF), - ).toMatchObject({ isOpen: false }); - expect(openInEditor).not.toHaveBeenCalled(); + resolveDiffPathForWorkspace({ filePath, workspaceRoot, repositoryRoot: workspaceRoot }), + ).toBe(expected); }, ); + + it.each([ + "backend/server.ts", + "frontend2/app.ts", + "frontend/../secret.ts", + "C:secret.ts", + "/repo/backend/server.ts", + "/repo/frontend/../backend/server.ts", + "/repo/frontend", + "/etc/passwd", + ])("does not open an out-of-project diff path: %s", (filePath) => { + const openInEditor = vi.fn(); + + openDiffFilePrimaryAction({ + threadRef: THREAD_REF, + filePath, + activeCwd: "/repo/frontend", + repositoryRoot: "/repo", + openInEditor, + }); + + expect( + selectThreadRightPanelState(useRightPanelStore.getState().byThreadKey, THREAD_REF), + ).toMatchObject({ isOpen: false }); + expect(openInEditor).not.toHaveBeenCalled(); + }); }); diff --git a/apps/web/src/diffFileActions.ts b/apps/web/src/diffFileActions.ts index 99baf13c94160..c6a3fa3d436fc 100644 --- a/apps/web/src/diffFileActions.ts +++ b/apps/web/src/diffFileActions.ts @@ -1,5 +1,10 @@ import type { ScopedThreadRef } from "@t3tools/contracts"; -import { isWindowsAbsolutePath, normalizeProjectPathForComparison } from "@t3tools/shared/path"; +import { + isAbsolutePath, + isWindowsAbsolutePath, + normalizeProjectPathForComparison, + workspaceRelativeFilePath, +} from "@t3tools/shared/path"; import { useRightPanelStore } from "./rightPanelStore"; import { resolvePathLinkTarget } from "@t3tools/shared/fileLinks"; @@ -52,6 +57,13 @@ export function resolveDiffPathForWorkspace(input: { readonly workspaceRoot: string | undefined; readonly repositoryRoot: string | undefined; }): string | null { + // Claude and ACP tool-call diffs name files by absolute path. + if (isAbsolutePath(input.filePath)) { + const relativePath = workspaceRelativeFilePath(input.filePath, input.workspaceRoot); + const segments = relativePath === null ? null : normalizedRelativePathSegments(relativePath); + return segments ? segments.join("/") : null; + } + const fileSegments = normalizedRelativePathSegments(input.filePath); if (!fileSegments) return null;