From ad4d910aefe296031e42efe0abfec0b6ccdf14ef Mon Sep 17 00:00:00 2001 From: Mohamed Boudra Date: Thu, 15 Oct 2026 16:55:15 +0200 Subject: [PATCH] fix(markdown): retry transient image decode failures --- .../src/components/markdown/image-decoding.ts | 18 ++++++++++++++++ .../app/src/components/markdown/images.ts | 17 ++++++--------- .../src/components/markdown/renderer.test.ts | 21 +++++++++++++++++++ 3 files changed, 45 insertions(+), 11 deletions(-) create mode 100644 packages/app/src/components/markdown/image-decoding.ts diff --git a/packages/app/src/components/markdown/image-decoding.ts b/packages/app/src/components/markdown/image-decoding.ts new file mode 100644 index 0000000000..9750d7d6c0 --- /dev/null +++ b/packages/app/src/components/markdown/image-decoding.ts @@ -0,0 +1,18 @@ +type ImageDecoder = ( + uri: string, + success: (width: number, height: number) => void, + failure: (error: unknown) => void, +) => void; + +export function decodeMarkdownImage(uri: string, decode: ImageDecoder): Promise { + return new Promise((resolve, reject) => { + decode( + uri, + (width, height) => { + if (width > 0 && height > 0) resolve(true); + else reject(new Error("Image has no decodable dimensions")); + }, + reject, + ); + }); +} diff --git a/packages/app/src/components/markdown/images.ts b/packages/app/src/components/markdown/images.ts index 37559fa3e9..c0832055a6 100644 --- a/packages/app/src/components/markdown/images.ts +++ b/packages/app/src/components/markdown/images.ts @@ -15,6 +15,7 @@ import type { DaemonClient } from "@getpaseo/client/internal/daemon-client"; import type { AttachmentMetadata } from "@/attachments/types"; import { resolveMarkdownPreviewImageSource } from "./image-source"; import { prepareMarkdownSource } from "./source"; +import { decodeMarkdownImage } from "./image-decoding"; const urls = createAssistantImageAcquisitionCache<{ uri: string; attachment: AttachmentMetadata }>({ capacity: 500, @@ -99,11 +100,15 @@ export function useMarkdownSource(text: string, context: MarkdownImageContext) { const decoded = useFetchQueries( displayUrls.map((uri, index) => ({ queryKey: ["markdownImageDecode", occurrenceKey, uri ?? sources[index]], + refetchOnReconnect: true, enabled: uri !== undefined, immutableWhen: () => true, dataShape: "value", gcTime: 0, - queryFn: () => (uri ? canDecodeImage(uri) : Promise.resolve(false)), + queryFn: () => { + if (!uri) throw new Error("No image URL to decode"); + return decodeMarkdownImage(uri, Image.getSize); + }, })), ); const unavailableImages = new Set( @@ -118,13 +123,3 @@ export function useMarkdownSource(text: string, context: MarkdownImageContext) { imageSources: Object.fromEntries([...resolved].map(([source, url]) => [url, source])), }; } - -function canDecodeImage(uri: string): Promise { - return new Promise((resolve) => { - Image.getSize( - uri, - (width, height) => resolve(width > 0 && height > 0), - () => resolve(false), - ); - }); -} diff --git a/packages/app/src/components/markdown/renderer.test.ts b/packages/app/src/components/markdown/renderer.test.ts index 214193d850..560ae23973 100644 --- a/packages/app/src/components/markdown/renderer.test.ts +++ b/packages/app/src/components/markdown/renderer.test.ts @@ -1,3 +1,5 @@ +import { QueryClient } from "@tanstack/react-query"; +import { decodeMarkdownImage } from "./image-decoding"; import { createElement } from "react"; import { MarkdownRendererFrame } from "./frame"; import { describe, expect, it } from "vitest"; @@ -348,3 +350,22 @@ it("preserves literal Markdown punctuation in unsupported and failed image label ), ).toBe(label); }); + +it("retries transient image decoding failures instead of caching an unavailable image", async () => { + const client = new QueryClient(); + let attempts = 0; + const decoded = await client.fetchQuery({ + queryKey: ["markdownImageDecode", "transient.png"], + retry: 1, + retryDelay: 0, + queryFn: () => + decodeMarkdownImage("transient.png", (_uri, success, failure) => { + attempts += 1; + if (attempts === 1) failure(new Error("temporarily unavailable")); + else success(20, 20); + }), + }); + expect(decoded).toBe(true); + expect(attempts).toBe(2); + client.clear(); +});