diff --git a/apps/web/src/components/files/FileBrowserPanel.tsx b/apps/web/src/components/files/FileBrowserPanel.tsx index 49894db3c8cf..25ac3cfbb27e 100644 --- a/apps/web/src/components/files/FileBrowserPanel.tsx +++ b/apps/web/src/components/files/FileBrowserPanel.tsx @@ -30,7 +30,7 @@ interface FileBrowserPanelProps { environmentId: EnvironmentId; cwd: string; projectName: string; - /** File currently open in the preview pane; revealed and selected in the tree. */ + /** Entry currently open in the surface; revealed and selected in the tree. A directory is expanded. */ selectedPath: string | null; /** Bumped when the same path should be revealed again (e.g. re-opened from search). */ selectedPathRevealId: number; @@ -299,8 +299,11 @@ export default function FileBrowserPanel({ ) { return; } - if (entryKinds.get(selectedPath) !== "file") return; - const selectedItem = model.getItem(selectedPath); + const selectedKind = entryKinds.get(selectedPath); + if (selectedKind === undefined) return; + // Directory rows are registered with a trailing slash (see treePath). + const selectedTreePath = selectedKind === "directory" ? `${selectedPath}/` : selectedPath; + const selectedItem = model.getItem(selectedTreePath); if (!selectedItem) return; // A selection that originated inside the tree (clicking a row, possibly @@ -334,8 +337,12 @@ export default function FileBrowserPanel({ if (item && "expand" in item) item.expand(); } + if ("expand" in selectedItem) selectedItem.expand(); selectedItem.select(); - model.scrollToPath(selectedPath, { focus: true, offset: "center" }); + model.scrollToPath(selectedTreePath, { + focus: true, + offset: "center", + }); queueMicrotask(() => { syncingSelectionRef.current = false; }); diff --git a/apps/web/src/components/files/FilePreviewPanel.tsx b/apps/web/src/components/files/FilePreviewPanel.tsx index b739d120da63..15153badea99 100644 --- a/apps/web/src/components/files/FilePreviewPanel.tsx +++ b/apps/web/src/components/files/FilePreviewPanel.tsx @@ -989,15 +989,22 @@ export default function FilePreviewPanel({ // A file outside the workspace (an absolute path) is shown, never edited. const isHostFile = attachment !== undefined || (relativePath !== null && isAbsolutePath(relativePath)); - const file = useProjectFileQuery( - environmentId, - cwd, - relativePath, - attachment === undefined && !isMedia && !isPdf, - ); + // Media and PDFs render from their absolute path, so their contents are never + // shown. The read still runs: a folder named `assets.png` is only knowable as a + // folder from the read failure, and the server stats before reading, so a folder + // costs an open and a stat and returns no body. + const file = useProjectFileQuery(environmentId, cwd, relativePath, attachment === undefined); + // A chat link cannot tell a folder from a file, so a folder arrives here as + // a file surface and the read fails. Keep the breadcrumbs, drop the preview + // pane, and let the tree fill the surface with the folder revealed. Mutation + // refresh stays on so the surface notices if the path becomes a file. A host + // path cannot be revealed in the workspace tree, so it keeps the read error. + const isDirectory = file.isNotFile && !isHostFile; + // Everything preview-related keys off previewPath; a folder has no preview. + const previewPath = isDirectory ? null : relativePath; const [explorerOpen, setExplorerOpen] = useState(initialExplorerOpen); const showExplorer = shouldShowFileExplorer({ - relativePath, + relativePath: previewPath, explorerOpen, attachmentOpen: attachment !== undefined, }); @@ -1020,7 +1027,7 @@ export default function FilePreviewPanel({ null, ); const breadcrumbRef = useRef(null); - const isMarkdown = relativePath ? isMarkdownPreviewFile(relativePath) : false; + const isMarkdown = previewPath ? isMarkdownPreviewFile(previewPath) : false; // A reveal still wins over the preference: the line only exists in the source. const revealHandled = revealLine === null || @@ -1033,11 +1040,11 @@ export default function FilePreviewPanel({ ? setRenderMarkdownPreferred : setRenderBrowserFilePreferred; const canOpenInBrowser = - relativePath !== null && + previewPath !== null && attachment === undefined && !isVideo && isPreviewSupportedInRuntime() && - isBrowserPreviewFile(relativePath); + isBrowserPreviewFile(previewPath); const absolutePath = relativePath && attachment === undefined ? resolvePathLinkTarget(relativePath, cwd) : null; const onFilePostRender = useFileLineReveal(relativePath, revealLine, revealRequestId); @@ -1045,8 +1052,10 @@ export default function FilePreviewPanel({ enabled: attachment === undefined && relativePath !== null && - !isMedia && - !isPdf && + // Media and PDFs never show their contents, so re-reading them on every + // workspace mutation is waste. A folder named like one still re-reads, so + // it notices when the path becomes a file. + (isDirectory || (!isMedia && !isPdf)) && !selectedFilePending, mutationId: workspaceMutationId, refresh: file.refresh, @@ -1190,7 +1199,7 @@ export default function FilePreviewPanel({ Open file in preview browser ) : null} - {!isHostFile ? ( + {!isHostFile && previewPath !== null ? ( ) : null} - {relativePath && !isMedia && !renderBrowserFile && file.data?.truncated ? ( + {previewPath && !isMedia && !renderBrowserFile && file.data?.truncated ? (
Preview limited to the first 1 MB of a {file.data.byteLength.toLocaleString()} byte file.
) : null}
{relativePath && attachment ? ( @@ -1332,7 +1338,7 @@ export default function FilePreviewPanel({