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}