From 5384abdbaaf69db588ff0d924929bda31bd19d94 Mon Sep 17 00:00:00 2001 From: "paseo-bot[bot]" <266920839+paseo-bot[bot]@users.noreply.github.com> Date: Sat, 17 Oct 2026 14:49:56 +0200 Subject: [PATCH] fix(app): open Markdown preview folder links in Files (#7229) Preview links bypassed the directory-aware opener and opened folder destinations as file tabs. Use the existing opener and explorer reveal actions, retaining preferred file placement and canceling pending checks when leaving the source tab. Co-authored-by: paseo-bot[bot] <266920839+paseo-bot[bot]@users.noreply.github.com> --- packages/app/e2e/browser/folder-links.spec.ts | 57 +++++++++++++++++++ .../screens/workspace/workspace-screen.tsx | 47 +++++++++++++-- 2 files changed, 100 insertions(+), 4 deletions(-) diff --git a/packages/app/e2e/browser/folder-links.spec.ts b/packages/app/e2e/browser/folder-links.spec.ts index 8c1339a8c6..a893fbcb68 100644 --- a/packages/app/e2e/browser/folder-links.spec.ts +++ b/packages/app/e2e/browser/folder-links.spec.ts @@ -2,6 +2,63 @@ import { mkdir, writeFile } from "node:fs/promises"; import path from "node:path"; import { expect, test, type Page } from "../support/fixtures"; import { openAgentRoute, seedMockAgentWorkspace } from "../support/helpers/mock-agent"; +import { + expectFileTabOpen, + openFileExplorer, + openFileFromExplorer, +} from "../support/helpers/file-explorer"; +import { ensureWorkspaceAgentPaneVisible } from "../support/helpers/workspace-tabs"; + +async function openMarkdownPreview(page: Page) { + await openFileExplorer(page); + await openFileFromExplorer(page, "README.md"); + await expectFileTabOpen(page, "README.md"); + await page + .getByTestId("file-panel-bar") + .getByRole("button", { name: "Preview", exact: true }) + .click(); + await ensureWorkspaceAgentPaneVisible(page); +} + +async function followPreviewLink(page: Page, name: string) { + await page + .getByTestId("workspace-file-pane") + .filter({ visible: true }) + .getByRole("link", { name, exact: true }) + .click(); +} + +test("Markdown preview folder links reveal Files and file links still open a file", async ({ + page, + withWorkspace, +}) => { + const workspace = await withWorkspace({ prefix: "preview-folder-links-" }); + await mkdir(path.join(workspace.repoPath, "docs/guides"), { recursive: true }); + await writeFile( + path.join(workspace.repoPath, "docs/guides/guide.md"), + "# Folder link destination\n", + ); + await writeFile( + path.join(workspace.repoPath, "README.md"), + "[Dot](./docs/guides)\n\n[Bare](docs/guides)\n\n[Guide](docs/guides/guide.md)\n", + ); + await workspace.navigateTo(); + + await openMarkdownPreview(page); + await followPreviewLink(page, "Guide"); + await expect(page.getByText("Folder link destination", { exact: true })).toBeVisible(); + + await openMarkdownPreview(page); + await followPreviewLink(page, "Dot"); + await expectFolderContents(page); + await expect(page.getByTestId("workspace-tab-file_docs/guides")).toHaveCount(0); + + await openMarkdownPreview(page); + await followPreviewLink(page, "Bare"); + await expectFolderContents(page); + await expect(page.getByTestId("workspace-tab-file_docs/guides")).toHaveCount(0); + await page.screenshot({ path: test.info().outputPath("preview-folder-links.png") }); +}); async function openReportedFolderLink(page: Page, name: string) { const link = page.getByTestId("assistant-message").getByRole("link", { name, exact: true }); diff --git a/packages/app/src/screens/workspace/workspace-screen.tsx b/packages/app/src/screens/workspace/workspace-screen.tsx index 0872c0dc92..48e123c26a 100644 --- a/packages/app/src/screens/workspace/workspace-screen.tsx +++ b/packages/app/src/screens/workspace/workspace-screen.tsx @@ -1,4 +1,9 @@ -import { AssistantFileLinkResolverProvider, type InlinePathTarget } from "@/assistant-file-links"; +import { + AssistantFileLinkResolverProvider, + createInlinePathOpener, + type InlinePathTarget, +} from "@/assistant-file-links"; +import { useFileExplorerActions } from "@/hooks/use-file-explorer-actions"; import { WorkspaceComposerEditingSessionsProvider, useComposerTabFullscreen, @@ -203,6 +208,7 @@ import { PluginHeaderButtons } from "@/plugins"; import { createWorkspaceFileTabTarget, normalizeWorkspaceFileLocation, + type OpenFileDisposition, type WorkspaceFileLocation, type WorkspaceFileOpenRequest, } from "@/workspace/file-open"; @@ -3658,9 +3664,42 @@ function WorkspaceScreenContent({ ); - const openMarkdownFile = useStableEvent((target: InlinePathTarget) => { - handleOpenPreferredAssistantFile({ location: target, parentTabId: activeTabId }); + const { revealDirectory } = useFileExplorerActions({ + serverId: normalizedServerId, + workspaceId: normalizedWorkspaceId, + workspaceRoot: workspaceDirectory, }); + const openMarkdownFile = useStableEvent((location: WorkspaceFileLocation) => { + handleOpenPreferredAssistantFile({ location, parentTabId: activeTabId }); + }); + const openMarkdownDirectory = useStableEvent((directoryPath: string) => { + void revealDirectory(directoryPath); + openExplorerSidebarView({ + isCompact: isMobile, + workspaceKey: persistenceKey, + checkout: activeExplorerCheckout, + view: "files", + }); + }); + const markdownPathOpener = useMemo( + () => + createInlinePathOpener({ + workspaceRoot: workspaceDirectory ?? "", + client, + openFile: openMarkdownFile, + openDirectory: openMarkdownDirectory, + }), + [workspaceDirectory, client, openMarkdownFile, openMarkdownDirectory], + ); + useEffect(() => { + if (!isRouteFocused) markdownPathOpener.cancel(); + return () => markdownPathOpener.cancel(); + }, [markdownPathOpener, isRouteFocused, activeTabId]); + const openMarkdownPath = useStableEvent( + (target: InlinePathTarget, disposition: OpenFileDisposition) => { + if (isRouteFocused) void markdownPathOpener.open(target, disposition); + }, + ); const renderedWorkspaceScreen = ( @@ -3705,7 +3744,7 @@ function WorkspaceScreenContent({ client={client} serverId={normalizedServerId} workspaceRoot={workspaceDirectory ?? undefined} - onOpenWorkspaceFile={openMarkdownFile} + onOpenWorkspaceFile={openMarkdownPath} toast={toast} > {renderedWorkspaceScreen}