diff --git a/apps/studio/src/client/atoms/studio-modal.ts b/apps/studio/src/client/atoms/studio-modal.ts index e47104620..a15c2b0eb 100644 --- a/apps/studio/src/client/atoms/studio-modal.ts +++ b/apps/studio/src/client/atoms/studio-modal.ts @@ -9,6 +9,14 @@ const openModalAtom = atom(null); +/** + * Whether any app-wide studio modal holds the slot. Read by hosts of a + * body-mounted browser guest, which is not inside the dialog's subtree and so + * would otherwise keep painting straight through its overlay (see + * use-guest-covered). + */ +export const isStudioModalOpenAtom = atom((get) => get(openModalAtom) !== null); + type StudioModalAtom = WritableAtom; /** diff --git a/apps/studio/src/client/components/file-viewer.tsx b/apps/studio/src/client/components/file-viewer.tsx index be85f29a1..c1d14d6e3 100644 --- a/apps/studio/src/client/components/file-viewer.tsx +++ b/apps/studio/src/client/components/file-viewer.tsx @@ -17,7 +17,6 @@ import { type FileType, getFileType } from "@/client/lib/get-file-type"; import { cn, getRevealInFolderLabel } from "@/client/lib/utils"; import { rpcClient } from "@/client/rpc/client"; import { - ArrowClockwiseIcon, ArrowLineDownIcon, ArrowsOutSimpleIcon, CheckIcon, @@ -40,10 +39,10 @@ import { ViewerSurface } from "./document-viewers/viewer-surface"; import { FileActionsMenuItems } from "./file-actions-menu"; import { FileLoading } from "./file-loading"; import { FilePreviewFallback } from "./file-preview-fallback"; +import { HtmlArtifactPreview } from "./html-artifact-preview"; import { RevealInFolderIcon } from "./icons/reveal-in-folder"; import { ImageViewer } from "./image-viewer"; import { OpenTaskFileButton } from "./open-task-file-button"; -import { SandboxedHtmlIframe } from "./sandboxed-html-iframe"; import { SessionMarkdown } from "./session-markdown"; import { Alert, AlertDescription, AlertTitle } from "./ui/alert"; import { Button } from "./ui/button"; @@ -184,7 +183,9 @@ export const fileViewerClassName = interface ViewerContext { fallback: ReactNode; file: TaskFileViewerFile; - htmlReloadNonce: number; + // Bumped when the user asks to return to the start of the file they are + // already looking at; see TaskView. + goHomeNonce?: number; imageLoadError: boolean; onImageError: () => void; onMediaError: (fallbackExtension: string) => void; @@ -273,11 +274,10 @@ const VIEWERS = { context.viewMode === "raw" ? ( renderText(context) ) : ( - ), scrolls: "container", @@ -457,19 +457,18 @@ const fileViewerHeaderOpenWithTriggerClassName = toolbarClassName({ export function FileViewer({ file, + goHomeNonce, onClose, onExpand, }: { file: TaskFileViewerFile; + // Passed through to a viewer that hosts a browser guest; see ViewerContext. + goHomeNonce?: number; onClose: () => void; onExpand?: () => void; }) { const { filename, filePath, mimeType, taskId, url } = file; const [viewMode, setViewMode] = useState<"preview" | "raw">("preview"); - // Remounts the sandboxed HTML iframe back to its entry page. The iframe is a - // cross-origin, opaque-origin sandbox, so we can't read or drive its history; - // reloading `src` is the only way to escape an in-page link navigation. - const [htmlReloadNonce, setHtmlReloadNonce] = useState(0); const [mediaLoadError, setMediaLoadError] = useState(false); const [mediaErrorType, setMediaErrorType] = useState(); const [imageErrorUrl, setImageErrorUrl] = useState(null); @@ -496,13 +495,20 @@ export function FileViewer({ const fileType = getFileType(file); const hasPreview = fileType === "markdown" || fileType === "html"; const fileActions = useFileActionVisibility(file); + // An HTML preview is a `` guest, and there is exactly one per task. + // Expanding would mount a second host for it, and two hosts sharing one guest + // have to agree on which file is on screen, who owns Cmd+F, and which of them + // may paint -- none of which a viewer can answer from its own props. The panel + // already gives an artifact the full height of the pane, so the affordance is + // withheld rather than arbitrated. Every other file type keeps it. + const canExpand = onExpand != null && fileType !== "html"; const hasHeaderMenuActions = - onExpand != null || fileActions.showDownload || fileActions.showReveal; + canExpand || fileActions.showDownload || fileActions.showReveal; const showOverflowMenu = fileActions.showDownload || fileActions.showReveal || hasPreview || - Boolean(onExpand); + canExpand; const handleDownload = async () => { await downloadFile(file); @@ -549,7 +555,7 @@ export function FileViewer({ /> ), file, - htmlReloadNonce, + goHomeNonce, imageLoadError, onImageError: () => { setImageErrorUrl(url); @@ -576,24 +582,6 @@ export function FileViewer({ size="sm" variant="ghost" /> - {fileType === "html" && viewMode === "preview" && ( - - - - - Reload - - )} {fileActions.showCopy && !imageLoadError && (