Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 15 additions & 0 deletions apps/web/src/components/files/FileBrowserPanel.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand All @@ -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 {
Expand Down Expand Up @@ -101,6 +104,7 @@ export default function FileBrowserPanel({
onOpenFile,
onRefreshSelectedFile,
workspaceMutationId,
onOpenInBrowser,
}: FileBrowserPanelProps) {
const { resolvedTheme } = useTheme();
const composerRef = useComposerHandleContext();
Expand Down Expand Up @@ -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();
Expand All @@ -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);
Expand Down
64 changes: 37 additions & 27 deletions apps/web/src/components/files/FilePreviewPanel.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand Down Expand Up @@ -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:
Expand Down Expand Up @@ -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 (
<div className="flex min-h-0 flex-1 flex-col overflow-hidden bg-background">
Expand Down Expand Up @@ -1205,7 +1210,7 @@ export default function FilePreviewPanel({
<Toggle
className="shrink-0"
pressed={false}
onPressedChange={handleOpenInBrowser}
onPressedChange={() => handleOpenInBrowser(relativePath)}
aria-label="Open file in preview browser"
variant="ghost"
size="sm"
Expand Down Expand Up @@ -1368,6 +1373,11 @@ export default function FilePreviewPanel({
{...(relativePath && !isMedia && !isPdf
? { onRefreshSelectedFile: file.refresh }
: {})}
onOpenInBrowser={
isPreviewSupportedInRuntime() && environmentHttpBaseUrl
? (browserPath) => handleOpenInBrowser(browserPath)
: undefined
}
/>
</aside>
) : null}
Expand Down
11 changes: 11 additions & 0 deletions apps/web/src/terminal-links.ts
Original file line number Diff line number Diff line change
Expand Up @@ -264,3 +264,14 @@ export function resolvePathLinkTarget(rawPath: string, cwd: string): string {

return formatFilePathPosition({ ...position, path: resolvedPath });
}

/**
* Resolves a workspace-relative file path against the workspace root. Unlike
* terminal links, a leading `~/` is a literal folder name inside the
* workspace, not the user's home directory.
*/
export function resolveWorkspaceFilePath(rawPath: string, cwd: string): string {
if (isAbsolutePath(rawPath)) return rawPath;
const separator: "/" | "\\" = isWindowsPathStyle(cwd) ? "\\" : "/";
return joinPath(cwd, rawPath, separator);
}
Loading