import type { AssetCreateUrlResult, AssetResource, EnvironmentId, PreviewOpenInput, PreviewSessionSnapshot, ScopedThreadRef, } from "@t3tools/contracts"; import { mediaFileReference } from "@t3tools/client-runtime/media-reference"; import { type AtomCommandResult, mapAtomCommandResult, } from "@t3tools/client-runtime/state/runtime"; import * as Cause from "effect/Cause"; import * as Data from "effect/Data"; import { AsyncResult } from "effect/unstable/reactivity"; import { resolveAssetUrl } from "~/assets/assetUrls"; import { applyPreviewServerSnapshot, isPreviewSupportedInRuntime, rememberPreviewUrl, } from "~/previewStateStore"; import { useRightPanelStore } from "~/rightPanelStore"; import { browserDefaultOpenProfileId, browserDefaultOpenViewport, resolveBrowserDefaults, } from "./browserDefaults"; export const isBrowserPreviewFile = (path: string): boolean => /\.(?:html?|pdf)$/i.test(path.split(/[?#]/, 1)[0] ?? ""); export class BrowserPreviewUnavailableError extends Data.TaggedError( "BrowserPreviewUnavailableError", )<{ readonly message: string; }> {} export type OpenPreviewMutation = (input: { readonly environmentId: EnvironmentId; readonly input: PreviewOpenInput; }) => Promise>; export async function openUrlInPreview(input: { readonly threadRef: ScopedThreadRef; readonly url: string; readonly openPreview: OpenPreviewMutation; }): Promise> { const defaults = await resolveBrowserDefaults(); const result = await input.openPreview({ environmentId: input.threadRef.environmentId, input: { threadId: input.threadRef.threadId, url: input.url, // Built here rather than via `openPreviewSession` because this path // maps the result differently, so the configured defaults have to be // applied explicitly or file/link opens would ignore them. viewport: browserDefaultOpenViewport(defaults), profileId: browserDefaultOpenProfileId(defaults), }, }); return mapAtomCommandResult(result, (snapshot) => { applyPreviewServerSnapshot(input.threadRef, snapshot); rememberPreviewUrl(input.threadRef, input.url); useRightPanelStore.getState().openBrowser(input.threadRef, snapshot.tabId); }); } /** * Opens a browser document in the integrated browser. Inside the workspace the * page may load sibling assets; a file outside it is served on its own. */ export async function openFileInPreview(input: { readonly threadRef: ScopedThreadRef; readonly filePath: string; readonly workspaceRoot: string | undefined; readonly httpBaseUrl: string; readonly createAssetUrl: (input: { readonly environmentId: EnvironmentId; readonly input: { readonly resource: AssetResource }; }) => Promise>; readonly openPreview: OpenPreviewMutation; }): Promise> { if (!isPreviewSupportedInRuntime()) { return AsyncResult.failure( Cause.fail( new BrowserPreviewUnavailableError({ message: "The integrated browser is unavailable in this runtime.", }), ), ); } const insideWorkspace = mediaFileReference(input.filePath, input.workspaceRoot).relativePath !== undefined; const assetResult = await input.createAssetUrl({ environmentId: input.threadRef.environmentId, input: { resource: { _tag: insideWorkspace ? "workspace-file" : "media-file", threadId: input.threadRef.threadId, path: input.filePath, }, }, }); if (assetResult._tag === "Failure") { return AsyncResult.failure(assetResult.cause); } const assetUrl = resolveAssetUrl(input.httpBaseUrl, assetResult.value.relativeUrl); if (assetUrl === null) { return AsyncResult.failure( Cause.die(new Error("The environment returned an invalid asset URL.")), ); } return openUrlInPreview({ threadRef: input.threadRef, url: assetUrl, openPreview: input.openPreview, }); }