From 1211cceaa10e9238e4e0396f63473c088e03c299 Mon Sep 17 00:00:00 2001 From: "paseo-bot[bot]" <266920839+paseo-bot[bot]@users.noreply.github.com> Date: Mon, 2 Nov 2026 13:41:29 +0000 Subject: [PATCH] fix(app): reuse file tabs opened from nested-project Changes Changes paths are relative to the current worktree. Resolve file actions from that root, then give file tabs and chat attachments the existing workspace-relative target for files inside the project. Keep absolute targets for changed files outside it so both paths remain addressable. The contributor's root-path fix opened inside-project files with absolute targets, which differed from Files and created duplicate retained tabs. A failing-first browser journey covers navigation between both views. Co-authored-by: 3ae3ae --- packages/app/e2e/browser/changes-pane.spec.ts | 39 +++++++++++++++++++ packages/app/src/git/diff-pane.tsx | 11 ++++-- 2 files changed, 46 insertions(+), 4 deletions(-) diff --git a/packages/app/e2e/browser/changes-pane.spec.ts b/packages/app/e2e/browser/changes-pane.spec.ts index 3e6fecc934..51add27945 100644 --- a/packages/app/e2e/browser/changes-pane.spec.ts +++ b/packages/app/e2e/browser/changes-pane.spec.ts @@ -8,6 +8,7 @@ import { daemonWsRoutePattern } from "../support/helpers/daemon-port"; import { getServerId } from "../support/helpers/server-id"; import { connectSeedClient } from "../support/helpers/seed-client"; import { createTempGitRepo } from "../support/helpers/workspace"; +import { openFileExplorer, openFileFromExplorer } from "../support/helpers/file-explorer"; import { openChangesPanel, waitForWorkspaceTabsVisible } from "../support/helpers/workspace-tabs"; interface DirtyWorkspace { @@ -506,6 +507,44 @@ for (const { workspaceSubdirectory, filePath, content, visibleContent } of [ }); } +test("Changes and Files reuse a retained file tab in a subfolder project", async ({ page }) => { + const workspace = await createWorkspaceWithMountedTabDiff({ workspaceSubdirectory: "src" }); + await openWorkspaceChanges(page, workspace); + await openChangedFile(page, "src/use-mounted-tab-set.ts"); + await editAndSaveChangedFile(page, workspace); + await openChangesPanel(page); + await openFileExplorer(page); + await openFileFromExplorer(page, "use-mounted-tab-set.ts"); + + await expect(page.getByTestId(/^workspace-tab-file_/).filter({ visible: true })).toHaveCount(1); + await expect(page.getByTestId("workspace-tab-file_use-mounted-tab-set.ts")).toHaveAttribute( + "aria-selected", + "true", + ); + await expect(visibleFileEditor(page)).toContainText("// retained file tab"); +}); + +async function openChangedFile(page: Page, filePath: string): Promise { + const panel = page.getByTestId("working-diff-panel").filter({ visible: true }); + await diffHeaderForPath(panel, filePath) + .getByTestId(/^diff-file-\d+-toggle$/) + .click({ button: "right" }); + await page.getByRole("menuitem", { name: "Open file", exact: true }).click(); + await expect(visibleFileEditor(page)).toContainText("interface DeriveRenderMountedTabIdsInput"); +} + +function visibleFileEditor(page: Page): Locator { + return page.getByTestId("workspace-file-pane").filter({ visible: true }).locator(".cm-content"); +} + +async function editAndSaveChangedFile(page: Page, workspace: DirtyWorkspace): Promise { + const content = "// retained file tab\n" + AFTER; + await visibleFileEditor(page).fill(content); + await expect + .poll(() => readFile(path.join(workspace.repoPath, "src/use-mounted-tab-set.ts"), "utf8")) + .toBe(content); +} + test("canvas file headers select without toggling for context menu and long press", async ({ page, }) => { diff --git a/packages/app/src/git/diff-pane.tsx b/packages/app/src/git/diff-pane.tsx index aef17bc47a..9f9e509257 100644 --- a/packages/app/src/git/diff-pane.tsx +++ b/packages/app/src/git/diff-pane.tsx @@ -56,7 +56,7 @@ import * as Clipboard from "expo-clipboard"; import { useFileDownload } from "@/hooks/use-file-download"; import { useIsLocalDaemon } from "@/hooks/use-is-local-daemon"; import { buildAbsoluteExplorerPath } from "@/utils/explorer-paths"; -import { isWorkspaceRootPath } from "@/workspace/file-open"; +import { resolveWorkspaceFilePaths } from "@/workspace/file-open"; import { Tooltip, TooltipTrigger, TooltipContent } from "@/components/ui/tooltip"; import { GitActionsSplitButton } from "@/git/actions-split-button"; import type { GitActions } from "@/git/policy"; @@ -1637,14 +1637,17 @@ export function ChangesSurface({ [appSettings.monoFontFamily, codeFontSize, effectiveLayout, wrapLines], ); const fileRoot = resolveChangesFileRoot(status, cwd); - const isRootWorkspace = isWorkspaceRootPath({ path: fileRoot, workspaceRoot: cwd }); const resolveFilePath = useCallback( (path: string) => buildAbsoluteExplorerPath({ workspaceRoot: fileRoot, entryPath: path }), [fileRoot], ); const resolveFileTargetPath = useCallback( - (path: string) => (isRootWorkspace ? path : resolveFilePath(path)), - [isRootWorkspace, resolveFilePath], + (path: string) => { + const absolutePath = resolveFilePath(path); + const resolved = resolveWorkspaceFilePaths({ path: absolutePath, workspaceRoot: cwd }); + return resolved?.relativePath ?? absolutePath; + }, + [cwd, resolveFilePath], ); const handleOpenFile = useCallback( (path: string) => onOpenFile?.(resolveFileTargetPath(path)),