"use client"; import { scopedThreadKey } from "@t3tools/client-runtime/environment";import { isAtomCommandInterrupted } from "@t3tools/client-runtime/state/runtime";import { type ScopedThreadRef } from "@t3tools/contracts";import { useCallback, useEffect, useRef, useState } from "react"; import { useComposerDraftStore } from "~/composerDraftStore";import { previewAnnotationScreenshotFile } from "~/lib/previewAnnotation";import { ensureLocalApi } from "~/localApi";import { rememberPreviewUrl, useThreadPreviewState } from "~/previewStateStore";import { resolveDiscoveredServerUrl } from "~/browser/browserTargetResolver";import { useEnvironment, useEnvironmentHttpBaseUrl } from "~/state/environments";import { previewEnvironment } from "~/state/preview";import { useAtomCommand } from "~/state/use-atom-command"; import { previewBridge } from "./previewBridge";import { subscribePreviewAction } from "./previewActionBus";import { openPreviewSession } from "./openPreviewSession";import { PreviewChromeRow } from "./PreviewChromeRow";import { formatPreviewUrl } from "./previewUrlPresentation";import { PreviewEmptyState } from "./PreviewEmptyState";import { PreviewMoreMenu } from "./PreviewMoreMenu";import { previewUrlFailureContext, reportPreviewActionFailure } from "./reportPreviewActionFailure";import { PreviewUnreachable } from "./PreviewUnreachable";import { revealInFileExplorerLabel } from "./fileExplorerLabel";import { shouldShowPreviewEmptyState } from "./previewEmptyStateLogic";import { BrowserSurfaceSlot } from "~/browser/BrowserSurfaceSlot";import { useLoadingProgress } from "./useLoadingProgress";import { usePreviewSession } from "./usePreviewSession";import { ZoomIndicator } from "./ZoomIndicator";import { AgentBrowserCursor } from "./AgentBrowserCursor";import { startBrowserRecording, stopBrowserRecording, useActiveBrowserRecordingTabId,} from "~/browser/browserRecording";import { stackedThreadToast, toastManager } from "~/components/ui/toast"; interface Props { threadRef: ScopedThreadRef; tabId?: string | null; configuredUrls?: ReadonlyArray<string> | undefined; visible: boolean;} const localApi = typeof window === "undefined" ? null : ensureLocalApi(); /** * Single-tab preview surface: chrome row on top, one webview below, empty * state when no session exists for the thread. */export function PreviewView({ threadRef, tabId: requestedTabId, configuredUrls, visible }: Props) { const [focusUrlNonce, setFocusUrlNonce] = useState<number | undefined>(undefined); const [pickActive, setPickActive] = useState(false); const activeRecordingTabId = useActiveBrowserRecordingTabId(); const pickActiveRef = useRef(false); const isMountedRef = useRef(true); const previewState = useThreadPreviewState(threadRef); const addPreviewAnnotation = useComposerDraftStore((store) => store.addPreviewAnnotation); const addImage = useComposerDraftStore((store) => store.addImage); const environment = useEnvironment(threadRef.environmentId); const environmentHttpBaseUrl = useEnvironmentHttpBaseUrl(threadRef.environmentId); const open = useAtomCommand(previewEnvironment.open); usePreviewSession(threadRef); useEffect(() => { isMountedRef.current = true; return () => { isMountedRef.current = false; }; }, []); const tabId = requestedTabId ?? previewState.activeTabId; const snapshot = tabId ? (previewState.sessions[tabId] ?? null) : null; const desktopOverlay = tabId ? (previewState.desktopByTabId[tabId] ?? null) : null; const navStatus = snapshot?.navStatus ?? { _tag: "Idle" as const }; const url = navStatus._tag === "Idle" ? "" : navStatus.url; const loading = desktopOverlay?.loading ?? navStatus._tag === "Loading"; const canGoBack = desktopOverlay?.canGoBack ?? snapshot?.canGoBack ?? false; const canGoForward = desktopOverlay?.canGoForward ?? snapshot?.canGoForward ?? false; const refreshDisabled = navStatus._tag === "Idle"; const isUnreachable = navStatus._tag === "LoadFailed"; const showEmptyState = shouldShowPreviewEmptyState(snapshot); const controller = desktopOverlay?.controller ?? "none"; const loadProgress = useLoadingProgress(loading); const displayUrl = url && environment && environmentHttpBaseUrl ? (formatPreviewUrl({ url, environmentLabel: environment.label, environmentHttpBaseUrl, }) ?? undefined) : undefined; const threadKey = scopedThreadKey(threadRef); const handleSubmitUrl = useCallback( async (next: string) => { let operation = "resolve-url"; let targetUrl = next; try { const resolvedUrl = resolveDiscoveredServerUrl(threadRef.environmentId, next); targetUrl = resolvedUrl; if (tabId && previewBridge) { // Drive the webview imperatively; `usePreviewBridge` mirrors the // resolved URL back to the server so other clients stay in sync. operation = "navigate"; await previewBridge.navigate(tabId, resolvedUrl); rememberPreviewUrl(threadRef, resolvedUrl); } else { operation = "open-session"; const result = await openPreviewSession({ openPreview: open, threadRef, url: resolvedUrl, }); if (result._tag === "Failure" && !isAtomCommandInterrupted(result)) { reportPreviewActionFailure( { operation, threadKey, ...(tabId ? { tabId } : {}), ...previewUrlFailureContext(targetUrl), }, result.cause, ); } } } catch (cause) { reportPreviewActionFailure( { operation, threadKey, ...(tabId ? { tabId } : {}), ...previewUrlFailureContext(targetUrl), }, cause, ); // Server-side `failed` event renders the unreachable view. } }, [open, tabId, threadKey, threadRef], ); const handleRefresh = useCallback(() => { if (!previewBridge || !tabId) return; void previewBridge.refresh(tabId).catch((cause) => { reportPreviewActionFailure({ operation: "refresh", threadKey, tabId }, cause); }); }, [tabId, threadKey]); const handleZoomIn = useCallback(() => { if (!previewBridge || !tabId) return; void previewBridge.zoomIn(tabId).catch((cause) => { reportPreviewActionFailure({ operation: "zoom-in", threadKey, tabId }, cause); }); }, [tabId, threadKey]); const handleZoomOut = useCallback(() => { if (!previewBridge || !tabId) return; void previewBridge.zoomOut(tabId).catch((cause) => { reportPreviewActionFailure({ operation: "zoom-out", threadKey, tabId }, cause); }); }, [tabId, threadKey]); const handleResetZoom = useCallback(() => { if (!previewBridge || !tabId) return; void previewBridge.resetZoom(tabId).catch((cause) => { reportPreviewActionFailure({ operation: "reset-zoom", threadKey, tabId }, cause); }); }, [tabId, threadKey]); const handleBack = useCallback(() => { if (!previewBridge || !tabId) return; void previewBridge.goBack(tabId).catch((cause) => { reportPreviewActionFailure({ operation: "go-back", threadKey, tabId }, cause); }); }, [tabId, threadKey]); const handleForward = useCallback(() => { if (!previewBridge || !tabId) return; void previewBridge.goForward(tabId).catch((cause) => { reportPreviewActionFailure({ operation: "go-forward", threadKey, tabId }, cause); }); }, [tabId, threadKey]); const handleOpenInBrowser = useCallback(() => { if (!localApi || !url) return; void localApi.shell.openExternal(url).catch((cause) => { reportPreviewActionFailure( { operation: "open-external", threadKey, ...(tabId ? { tabId } : {}), ...previewUrlFailureContext(url), }, cause, ); }); }, [tabId, threadKey, url]); const handleCapture = useCallback( (record: boolean) => { if (!previewBridge || !tabId) return; const bridge = previewBridge; const recordingThisTab = activeRecordingTabId === tabId; if (recordingThisTab) { void stopBrowserRecording(tabId).then( (artifact) => { if (!artifact) return; let pathCopied = false; let toastId: ReturnType<typeof toastManager.add>; const copyPath = () => { if (!navigator.clipboard?.writeText) { toastManager.update( toastId, stackedThreadToast({ type: "error", title: "Unable to copy recording path", description: "Clipboard API unavailable.", actionProps: revealAction, }), ); return; } void navigator.clipboard.writeText(artifact.path).then( () => { pathCopied = true; updateRecordingToast(); window.setTimeout(() => { pathCopied = false; updateRecordingToast(); }, 2_000); }, (error) => { reportPreviewActionFailure( { operation: "copy-recording-path", threadKey, tabId, artifactPath: artifact.path, }, error, ); toastManager.update( toastId, stackedThreadToast({ type: "error", title: "Unable to copy recording path", description: error instanceof Error ? error.message : "An error occurred.", actionProps: revealAction, }), ); }, ); }; const revealAction = { children: revealInFileExplorerLabel(navigator.platform), onClick: () => void bridge.revealArtifact(artifact.path).catch((cause) => { reportPreviewActionFailure( { operation: "reveal-recording", threadKey, tabId, artifactPath: artifact.path, }, cause, ); }), }; const updateRecordingToast = () => { toastManager.update( toastId, stackedThreadToast({ type: "success", title: "Recording saved", actionProps: revealAction, data: { secondaryActionProps: { children: pathCopied ? "Copied!" : "Copy path", disabled: pathCopied, onClick: copyPath, }, secondaryActionVariant: "outline", }, }), ); }; toastId = toastManager.add( stackedThreadToast({ type: "success", title: "Recording saved", actionProps: revealAction, data: { secondaryActionProps: { children: "Copy path", onClick: copyPath, }, secondaryActionVariant: "outline", }, }), ); }, (error) => { reportPreviewActionFailure({ operation: "stop-recording", threadKey, tabId }, error); toastManager.add({ type: "error", title: "Unable to stop recording", description: error instanceof Error ? error.message : "An error occurred.", }); }, ); return; } if (record) { if (activeRecordingTabId !== null) { toastManager.add({ type: "warning", title: "Another preview is recording", description: "Stop the active recording before starting a new one.", }); return; } void startBrowserRecording(tabId).catch((error) => { reportPreviewActionFailure({ operation: "start-recording", threadKey, tabId }, error); toastManager.add({ type: "error", title: "Unable to start recording", description: error instanceof Error ? error.message : "An error occurred.", }); }); return; } void bridge.captureScreenshot(tabId).then( (artifact) => { const revealAction = { children: revealInFileExplorerLabel(navigator.platform), onClick: () => void bridge.revealArtifact(artifact.path).catch((cause) => { reportPreviewActionFailure( { operation: "reveal-screenshot", threadKey, tabId, artifactPath: artifact.path, }, cause, ); }), }; let pathCopied = false; let imageCopied = false; let toastId: ReturnType<typeof toastManager.add>; const updateScreenshotToast = ( type: "success" | "error" = "success", title = "Screenshot saved", description?: string, ) => { toastManager.update( toastId, stackedThreadToast({ type, title, description, actionProps: { children: imageCopied ? "Copied!" : "Copy image", disabled: imageCopied, onClick: copyImage, }, data: { additionalActions: [ { id: "copy-path", props: { children: pathCopied ? "Copied!" : "Copy path", disabled: pathCopied, onClick: copyPath, }, }, ], secondaryActionProps: { ...revealAction, }, secondaryActionVariant: "outline", }, }), ); }; const copyPath = () => { if (!navigator.clipboard?.writeText) { updateScreenshotToast( "error", "Unable to copy screenshot path", "Clipboard API unavailable.", ); return; } void navigator.clipboard.writeText(artifact.path).then( () => { pathCopied = true; updateScreenshotToast(); window.setTimeout(() => { pathCopied = false; updateScreenshotToast(); }, 2_000); }, (error) => { reportPreviewActionFailure( { operation: "copy-screenshot-path", threadKey, tabId, artifactPath: artifact.path, }, error, ); updateScreenshotToast( "error", "Unable to copy screenshot path", error instanceof Error ? error.message : "An error occurred.", ); }, ); }; const copyImage = () => { void bridge.copyArtifactToClipboard(artifact.path).then( () => { imageCopied = true; updateScreenshotToast(); window.setTimeout(() => { imageCopied = false; updateScreenshotToast(); }, 2_000); }, (error) => { reportPreviewActionFailure( { operation: "copy-screenshot-image", threadKey, tabId, artifactPath: artifact.path, }, error, ); updateScreenshotToast( "error", "Unable to copy screenshot", error instanceof Error ? error.message : "An error occurred.", ); }, ); }; toastId = toastManager.add( stackedThreadToast({ type: "success", title: "Screenshot saved", actionProps: { children: "Copy image", onClick: copyImage, }, data: { additionalActions: [ { id: "copy-path", props: { children: "Copy path", onClick: copyPath, }, }, ], secondaryActionProps: { ...revealAction, }, secondaryActionVariant: "outline", }, }), ); }, (error) => { reportPreviewActionFailure({ operation: "capture-screenshot", threadKey, tabId }, error); toastManager.add({ type: "error", title: "Unable to capture screenshot", description: error instanceof Error ? error.message : "An error occurred.", }); }, ); }, [activeRecordingTabId, tabId, threadKey],Lines 1–500 of 710. Load more
"use client"; import { scopedThreadKey } from "@t3tools/client-runtime/environment"; import { isAtomCommandInterrupted } from "@t3tools/client-runtime/state/runtime"; import { type ScopedThreadRef } from "@t3tools/contracts"; import { useCallback, useEffect, useRef, useState } from "react"; import { useComposerDraftStore } from "~/composerDraftStore"; import { previewAnnotationScreenshotFile } from "~/lib/previewAnnotation"; import { ensureLocalApi } from "~/localApi"; import { rememberPreviewUrl, useThreadPreviewState } from "~/previewStateStore"; import { resolveDiscoveredServerUrl } from "~/browser/browserTargetResolver"; import { useEnvironment, useEnvironmentHttpBaseUrl } from "~/state/environments"; import { previewEnvironment } from "~/state/preview"; import { useAtomCommand } from "~/state/use-atom-command"; import { previewBridge } from "./previewBridge"; import { subscribePreviewAction } from "./previewActionBus"; import { openPreviewSession } from "./openPreviewSession"; import { PreviewChromeRow } from "./PreviewChromeRow"; import { formatPreviewUrl } from "./previewUrlPresentation"; import { PreviewEmptyState } from "./PreviewEmptyState"; import { PreviewMoreMenu } from "./PreviewMoreMenu"; import { previewUrlFailureContext, reportPreviewActionFailure } from "./reportPreviewActionFailure"; import { PreviewUnreachable } from "./PreviewUnreachable"; import { revealInFileExplorerLabel } from "./fileExplorerLabel"; import { shouldShowPreviewEmptyState } from "./previewEmptyStateLogic"; import { BrowserSurfaceSlot } from "~/browser/BrowserSurfaceSlot"; import { useLoadingProgress } from "./useLoadingProgress"; import { usePreviewSession } from "./usePreviewSession"; import { ZoomIndicator } from "./ZoomIndicator"; import { AgentBrowserCursor } from "./AgentBrowserCursor"; import { startBrowserRecording, stopBrowserRecording, useActiveBrowserRecordingTabId, } from "~/browser/browserRecording"; import { stackedThreadToast, toastManager } from "~/components/ui/toast"; interface Props { threadRef: ScopedThreadRef; tabId?: string | null; configuredUrls?: ReadonlyArray<string> | undefined; visible: boolean; } const localApi = typeof window === "undefined" ? null : ensureLocalApi(); /** * Single-tab preview surface: chrome row on top, one webview below, empty * state when no session exists for the thread. */ export function PreviewView({ threadRef, tabId: requestedTabId, configuredUrls, visible }: Props) { const [focusUrlNonce, setFocusUrlNonce] = useState<number | undefined>(undefined); const [pickActive, setPickActive] = useState(false); const activeRecordingTabId = useActiveBrowserRecordingTabId(); const pickActiveRef = useRef(false); const isMountedRef = useRef(true); const previewState = useThreadPreviewState(threadRef); const addPreviewAnnotation = useComposerDraftStore((store) => store.addPreviewAnnotation); const addImage = useComposerDraftStore((store) => store.addImage); const environment = useEnvironment(threadRef.environmentId); const environmentHttpBaseUrl = useEnvironmentHttpBaseUrl(threadRef.environmentId); const open = useAtomCommand(previewEnvironment.open); usePreviewSession(threadRef); useEffect(() => { isMountedRef.current = true; return () => { isMountedRef.current = false; }; }, []); const tabId = requestedTabId ?? previewState.activeTabId; const snapshot = tabId ? (previewState.sessions[tabId] ?? null) : null; const desktopOverlay = tabId ? (previewState.desktopByTabId[tabId] ?? null) : null; const navStatus = snapshot?.navStatus ?? { _tag: "Idle" as const }; const url = navStatus._tag === "Idle" ? "" : navStatus.url; const loading = desktopOverlay?.loading ?? navStatus._tag === "Loading"; const canGoBack = desktopOverlay?.canGoBack ?? snapshot?.canGoBack ?? false; const canGoForward = desktopOverlay?.canGoForward ?? snapshot?.canGoForward ?? false; const refreshDisabled = navStatus._tag === "Idle"; const isUnreachable = navStatus._tag === "LoadFailed"; const showEmptyState = shouldShowPreviewEmptyState(snapshot); const controller = desktopOverlay?.controller ?? "none"; const loadProgress = useLoadingProgress(loading); const displayUrl = url && environment && environmentHttpBaseUrl ? (formatPreviewUrl({ url, environmentLabel: environment.label, environmentHttpBaseUrl, }) ?? undefined) : undefined; const threadKey = scopedThreadKey(threadRef); const handleSubmitUrl = useCallback( async (next: string) => { let operation = "resolve-url"; let targetUrl = next; try { const resolvedUrl = resolveDiscoveredServerUrl(threadRef.environmentId, next); targetUrl = resolvedUrl; if (tabId && previewBridge) { // Drive the webview imperatively; `usePreviewBridge` mirrors the // resolved URL back to the server so other clients stay in sync. operation = "navigate"; await previewBridge.navigate(tabId, resolvedUrl); rememberPreviewUrl(threadRef, resolvedUrl); } else { operation = "open-session"; const result = await openPreviewSession({ openPreview: open, threadRef, url: resolvedUrl, }); if (result._tag === "Failure" && !isAtomCommandInterrupted(result)) { reportPreviewActionFailure( { operation, threadKey, ...(tabId ? { tabId } : {}), ...previewUrlFailureContext(targetUrl), }, result.cause, ); } } } catch (cause) { reportPreviewActionFailure( { operation, threadKey, ...(tabId ? { tabId } : {}), ...previewUrlFailureContext(targetUrl), }, cause, ); // Server-side `failed` event renders the unreachable view. } }, [open, tabId, threadKey, threadRef], ); const handleRefresh = useCallback(() => { if (!previewBridge || !tabId) return; void previewBridge.refresh(tabId).catch((cause) => { reportPreviewActionFailure({ operation: "refresh", threadKey, tabId }, cause); }); }, [tabId, threadKey]); const handleZoomIn = useCallback(() => { if (!previewBridge || !tabId) return; void previewBridge.zoomIn(tabId).catch((cause) => { reportPreviewActionFailure({ operation: "zoom-in", threadKey, tabId }, cause); }); }, [tabId, threadKey]); const handleZoomOut = useCallback(() => { if (!previewBridge || !tabId) return; void previewBridge.zoomOut(tabId).catch((cause) => { reportPreviewActionFailure({ operation: "zoom-out", threadKey, tabId }, cause); }); }, [tabId, threadKey]); const handleResetZoom = useCallback(() => { if (!previewBridge || !tabId) return; void previewBridge.resetZoom(tabId).catch((cause) => { reportPreviewActionFailure({ operation: "reset-zoom", threadKey, tabId }, cause); }); }, [tabId, threadKey]); const handleBack = useCallback(() => { if (!previewBridge || !tabId) return; void previewBridge.goBack(tabId).catch((cause) => { reportPreviewActionFailure({ operation: "go-back", threadKey, tabId }, cause); }); }, [tabId, threadKey]); const handleForward = useCallback(() => { if (!previewBridge || !tabId) return; void previewBridge.goForward(tabId).catch((cause) => { reportPreviewActionFailure({ operation: "go-forward", threadKey, tabId }, cause); }); }, [tabId, threadKey]); const handleOpenInBrowser = useCallback(() => { if (!localApi || !url) return; void localApi.shell.openExternal(url).catch((cause) => { reportPreviewActionFailure( { operation: "open-external", threadKey, ...(tabId ? { tabId } : {}), ...previewUrlFailureContext(url), }, cause, ); }); }, [tabId, threadKey, url]); const handleCapture = useCallback( (record: boolean) => { if (!previewBridge || !tabId) return; const bridge = previewBridge; const recordingThisTab = activeRecordingTabId === tabId; if (recordingThisTab) { void stopBrowserRecording(tabId).then( (artifact) => { if (!artifact) return; let pathCopied = false; let toastId: ReturnType<typeof toastManager.add>; const copyPath = () => { if (!navigator.clipboard?.writeText) { toastManager.update( toastId, stackedThreadToast({ type: "error", title: "Unable to copy recording path", description: "Clipboard API unavailable.", actionProps: revealAction, }), ); return; } void navigator.clipboard.writeText(artifact.path).then( () => { pathCopied = true; updateRecordingToast(); window.setTimeout(() => { pathCopied = false; updateRecordingToast(); }, 2_000); }, (error) => { reportPreviewActionFailure( { operation: "copy-recording-path", threadKey, tabId, artifactPath: artifact.path, }, error, ); toastManager.update( toastId, stackedThreadToast({ type: "error", title: "Unable to copy recording path", description: error instanceof Error ? error.message : "An error occurred.", actionProps: revealAction, }), ); }, ); }; const revealAction = { children: revealInFileExplorerLabel(navigator.platform), onClick: () => void bridge.revealArtifact(artifact.path).catch((cause) => { reportPreviewActionFailure( { operation: "reveal-recording", threadKey, tabId, artifactPath: artifact.path, }, cause, ); }), }; const updateRecordingToast = () => { toastManager.update( toastId, stackedThreadToast({ type: "success", title: "Recording saved", actionProps: revealAction, data: { secondaryActionProps: { children: pathCopied ? "Copied!" : "Copy path", disabled: pathCopied, onClick: copyPath, }, secondaryActionVariant: "outline", }, }), ); }; toastId = toastManager.add( stackedThreadToast({ type: "success", title: "Recording saved", actionProps: revealAction, data: { secondaryActionProps: { children: "Copy path", onClick: copyPath, }, secondaryActionVariant: "outline", }, }), ); }, (error) => { reportPreviewActionFailure({ operation: "stop-recording", threadKey, tabId }, error); toastManager.add({ type: "error", title: "Unable to stop recording", description: error instanceof Error ? error.message : "An error occurred.", }); }, ); return; } if (record) { if (activeRecordingTabId !== null) { toastManager.add({ type: "warning", title: "Another preview is recording", description: "Stop the active recording before starting a new one.", }); return; } void startBrowserRecording(tabId).catch((error) => { reportPreviewActionFailure({ operation: "start-recording", threadKey, tabId }, error); toastManager.add({ type: "error", title: "Unable to start recording", description: error instanceof Error ? error.message : "An error occurred.", }); }); return; } void bridge.captureScreenshot(tabId).then( (artifact) => { const revealAction = { children: revealInFileExplorerLabel(navigator.platform), onClick: () => void bridge.revealArtifact(artifact.path).catch((cause) => { reportPreviewActionFailure( { operation: "reveal-screenshot", threadKey, tabId, artifactPath: artifact.path, }, cause, ); }), }; let pathCopied = false; let imageCopied = false; let toastId: ReturnType<typeof toastManager.add>; const updateScreenshotToast = ( type: "success" | "error" = "success", title = "Screenshot saved", description?: string, ) => { toastManager.update( toastId, stackedThreadToast({ type, title, description, actionProps: { children: imageCopied ? "Copied!" : "Copy image", disabled: imageCopied, onClick: copyImage, }, data: { additionalActions: [ { id: "copy-path", props: { children: pathCopied ? "Copied!" : "Copy path", disabled: pathCopied, onClick: copyPath, }, }, ], secondaryActionProps: { ...revealAction, }, secondaryActionVariant: "outline", }, }), ); }; const copyPath = () => { if (!navigator.clipboard?.writeText) { updateScreenshotToast( "error", "Unable to copy screenshot path", "Clipboard API unavailable.", ); return; } void navigator.clipboard.writeText(artifact.path).then( () => { pathCopied = true; updateScreenshotToast(); window.setTimeout(() => { pathCopied = false; updateScreenshotToast(); }, 2_000); }, (error) => { reportPreviewActionFailure( { operation: "copy-screenshot-path", threadKey, tabId, artifactPath: artifact.path, }, error, ); updateScreenshotToast( "error", "Unable to copy screenshot path", error instanceof Error ? error.message : "An error occurred.", ); }, ); }; const copyImage = () => { void bridge.copyArtifactToClipboard(artifact.path).then( () => { imageCopied = true; updateScreenshotToast(); window.setTimeout(() => { imageCopied = false; updateScreenshotToast(); }, 2_000); }, (error) => { reportPreviewActionFailure( { operation: "copy-screenshot-image", threadKey, tabId, artifactPath: artifact.path, }, error, ); updateScreenshotToast( "error", "Unable to copy screenshot", error instanceof Error ? error.message : "An error occurred.", ); }, ); }; toastId = toastManager.add( stackedThreadToast({ type: "success", title: "Screenshot saved", actionProps: { children: "Copy image", onClick: copyImage, }, data: { additionalActions: [ { id: "copy-path", props: { children: "Copy path", onClick: copyPath, }, }, ], secondaryActionProps: { ...revealAction, }, secondaryActionVariant: "outline", }, }), ); }, (error) => { reportPreviewActionFailure({ operation: "capture-screenshot", threadKey, tabId }, error); toastManager.add({ type: "error", title: "Unable to capture screenshot", description: error instanceof Error ? error.message : "An error occurred.", }); }, ); }, [activeRecordingTabId, tabId, threadKey],