diff --git a/apps/web/src/components/files/FileBrowserPanel.tsx b/apps/web/src/components/files/FileBrowserPanel.tsx index dc3cfcef0137..d5a1bf7fca7b 100644 --- a/apps/web/src/components/files/FileBrowserPanel.tsx +++ b/apps/web/src/components/files/FileBrowserPanel.tsx @@ -12,6 +12,7 @@ import { Button } from "~/components/ui/button"; import { InputGroup, InputGroupInput } from "~/components/ui/input-group"; import { toastManager } from "~/components/ui/toast"; import { Tooltip, TooltipPopup, TooltipTrigger } from "~/components/ui/tooltip"; +import { isBrowserPreviewFile } from "~/browser/openFileInPreview"; import { useComposerHandleContext } from "~/composerHandleContext"; import { writeTextToClipboard } from "~/hooks/useCopyToClipboard"; import { useTheme } from "~/hooks/useTheme"; @@ -37,6 +38,8 @@ interface FileBrowserPanelProps { onOpenFile: (relativePath: string) => void; onRefreshSelectedFile?: () => void; workspaceMutationId: string | null; + /** Present when the runtime can open a workspace file in the integrated browser. */ + onOpenInBrowser?: ((relativePath: string) => void) | undefined; } function treePath(entry: ProjectEntry): string { @@ -101,6 +104,7 @@ export default function FileBrowserPanel({ onOpenFile, onRefreshSelectedFile, workspaceMutationId, + onOpenInBrowser, }: FileBrowserPanelProps) { const { resolvedTheme } = useTheme(); const composerRef = useComposerHandleContext(); @@ -144,6 +148,10 @@ export default function FileBrowserPanel({ } const relativePath = item.path.replace(/\/$/, ""); const mention = serializeComposerFileLink(relativePath); + const canOpenInBrowser = + onOpenInBrowser !== undefined && + entryKindsRef.current.get(relativePath) === "file" && + isBrowserPreviewFile(relativePath); const pointer = contextMenuPointerRef.current; const pointerIsFresh = pointer !== null && performance.now() - pointer.at < 1000; const anchorRect = context.anchorElement.getBoundingClientRect(); @@ -153,11 +161,18 @@ export default function FileBrowserPanel({ try { const clicked = await api.contextMenu.show( [ + ...(canOpenInBrowser + ? [{ id: "open-in-browser" as const, label: "Open in browser" }] + : []), { id: "copy-mention", label: "Copy mention" }, { id: "add-to-chat", label: "Add to chat" }, ], position, ); + if (clicked === "open-in-browser") { + onOpenInBrowser?.(relativePath); + return; + } if (clicked === "copy-mention") { try { await writeTextToClipboard(mention); diff --git a/apps/web/src/components/files/FilePreviewPanel.tsx b/apps/web/src/components/files/FilePreviewPanel.tsx index 0ad18434d7e7..a7607b4d092a 100644 --- a/apps/web/src/components/files/FilePreviewPanel.tsx +++ b/apps/web/src/components/files/FilePreviewPanel.tsx @@ -36,7 +36,7 @@ import { DIFF_SURFACE_THEME_UNSAFE_CSS, resolveDiffThemeName } from "~/lib/diffR import { PREFERRED_HIGHLIGHTER } from "~/lib/syntaxHighlighting"; import { cn } from "~/lib/utils"; import { isPreviewSupportedInRuntime } from "~/previewStateStore"; -import { isAbsolutePath, resolvePathLinkTarget } from "~/terminal-links"; +import { isAbsolutePath, resolveWorkspaceFilePath } from "~/terminal-links"; import { ScrollArea } from "~/components/ui/scroll-area"; import { Toggle } from "~/components/ui/toggle"; import { Tooltip, TooltipPopup, TooltipTrigger } from "~/components/ui/tooltip"; @@ -1066,7 +1066,7 @@ export default function FilePreviewPanel({ isPreviewSupportedInRuntime() && isBrowserPreviewFile(relativePath); const absolutePath = - relativePath && attachment === undefined ? resolvePathLinkTarget(relativePath, cwd) : null; + relativePath && attachment === undefined ? resolveWorkspaceFilePath(relativePath, cwd) : null; const onFilePostRender = useFileLineReveal(relativePath, revealLine, revealRequestId); useWorkspaceMutationRefresh({ enabled: @@ -1099,30 +1099,35 @@ export default function FilePreviewPanel({ }); }; - const handleOpenInBrowser = useCallback(() => { - if (!absolutePath || !environmentHttpBaseUrl) return; - void (async () => { - const result = await openFileInPreview({ - threadRef, - filePath: absolutePath, - workspaceRoot: cwd, - httpBaseUrl: environmentHttpBaseUrl, - createAssetUrl, - openPreview, - }); - if (result._tag === "Success" || isAtomCommandInterrupted(result)) { - return; - } - const error = squashAtomCommandFailure(result); - toastManager.add( - stackedThreadToast({ - type: "error", - title: "Unable to open file in browser", - description: error instanceof Error ? error.message : "An error occurred.", - }), - ); - })(); - }, [absolutePath, createAssetUrl, cwd, environmentHttpBaseUrl, openPreview, threadRef]); + const handleOpenInBrowser = useCallback( + (filePath: string) => { + if (!environmentHttpBaseUrl) return; + void (async () => { + const result = await openFileInPreview({ + threadRef, + // Tree and preview-header paths are workspace-relative; resolve them + // against the workspace root without terminal-link `~/` expansion. + filePath: resolveWorkspaceFilePath(filePath, cwd), + workspaceRoot: cwd, + httpBaseUrl: environmentHttpBaseUrl, + createAssetUrl, + openPreview, + }); + if (result._tag === "Success" || isAtomCommandInterrupted(result)) { + return; + } + const error = squashAtomCommandFailure(result); + toastManager.add( + stackedThreadToast({ + type: "error", + title: "Unable to open file in browser", + description: error instanceof Error ? error.message : "An error occurred.", + }), + ); + })(); + }, + [createAssetUrl, cwd, environmentHttpBaseUrl, openPreview, threadRef], + ); return (