import { useContext, useEffect, useState, type JSX } from "react"; import { ZoomableImage } from "../app/renderer/components/ZoomableImage"; import { cn } from "../app/renderer/lib/utils"; import { useTranslation } from "../i18n/index"; import { getOptionalAgentHostApi, useOptionalAgentHostApi } from "../agentActivityHost"; import { workspaceFileName as basenameWorkspacePath } from "@tutti-os/workspace-file-manager/services"; import type { MarkdownDomProps } from "./AgentMessageMarkdown"; import { MarkdownLinkContext } from "./agentMessageMarkdownContext"; import { cacheMarkdownMedia, canRenderMarkdownVideoFallback, peekCachedMarkdownMediaState, releaseCachedMarkdownMedia, resetCachedMarkdownMediaForTests, resolveMarkdownMediaKind, resolveMarkdownMediaType, resolveMarkdownWorkspaceMediaPath, resolveRenderableMarkdownMediaSrc, retainCachedMarkdownMedia, type MarkdownMediaState } from "./agentMessageMarkdownLinks"; export function resetCachedMarkdownImagesForTests(): void { resetCachedMarkdownMediaForTests(); } export function MarkdownMedia({ node: _node, src, alt, className, title, enableZoom = false, ...props }: MarkdownDomProps<"img"> & { enableZoom?: boolean; }): JSX.Element { "use memo"; const { t } = useTranslation(); const isInsideLink = useContext(MarkdownLinkContext); const agentHostApi = useOptionalAgentHostApi() ?? getOptionalAgentHostApi(); const workspacePath = typeof src === "string" ? resolveMarkdownWorkspaceMediaPath(src) : null; const readWorkspaceImage = workspacePath ? agentHostApi?.workspace?.readFile : undefined; const canReadWorkspaceImage = Boolean(workspacePath && readWorkspaceImage); const shouldEnableZoom = enableZoom && !isInsideLink; const fallbackMediaKind = typeof src === "string" ? resolveMarkdownMediaKind(src) : null; const resolvedSrc = typeof src === "string" ? resolveRenderableMarkdownMediaSrc(src) : src; const [state, setState] = useState(() => canReadWorkspaceImage && workspacePath ? (peekCachedMarkdownMediaState(workspacePath) ?? { status: "loading" }) : null ); useEffect(() => { if (!workspacePath || !readWorkspaceImage) { setState(null); return; } const resolvedWorkspacePath = workspacePath; const resolvedReadWorkspaceImage = readWorkspaceImage; const cached = retainCachedMarkdownMedia(resolvedWorkspacePath); if (cached) { setState({ kind: cached.kind, status: "ready", src: cached.src }); return () => { releaseCachedMarkdownMedia(resolvedWorkspacePath, cached.src); }; } const mediaType = resolveMarkdownMediaType(resolvedWorkspacePath); if (!mediaType) { setState({ status: "error", reason: "unsupported" }); return; } const mediaKind = mediaType.kind; const mediaMimeType = mediaType.mimeType; let canceled = false; let objectUrl: string | null = null; setState({ status: "loading" }); async function loadWorkspaceMedia(): Promise { try { const result = await resolvedReadWorkspaceImage({ path: resolvedWorkspacePath }); if (canceled) { return; } const bytes = result.bytes instanceof Uint8Array ? result.bytes : new Uint8Array(result.bytes); const arrayBuffer = bytes.buffer.slice( bytes.byteOffset, bytes.byteOffset + bytes.byteLength ) as ArrayBuffer; objectUrl = cacheMarkdownMedia( resolvedWorkspacePath, mediaKind, new Blob([arrayBuffer], { type: mediaMimeType }) ); setState({ kind: mediaKind, status: "ready", src: objectUrl }); } catch (error) { if (!canceled) { setState({ status: "error", reason: "read-failed", detail: error instanceof Error ? error.message : String(error) }); } } } void loadWorkspaceMedia(); return () => { canceled = true; if (objectUrl) { releaseCachedMarkdownMedia(resolvedWorkspacePath, objectUrl); } }; }, [canReadWorkspaceImage, workspacePath]); if (!workspacePath || !readWorkspaceImage) { if (fallbackMediaKind === "video") { if (!canRenderMarkdownVideoFallback(src)) { return ; } return (