feat(app): store assistant image previews as assets

This commit is contained in:
Mohamed Boudra
2026-04-22 22:28:56 +07:00
parent 434b28fc8e
commit 4140e640fe
15 changed files with 476 additions and 38 deletions
@@ -55,4 +55,15 @@ describe("assistant image metadata", () => {
),
).toBeGreaterThan(220);
});
it("estimates image-only data-image markdown without caching the full payload as text", () => {
const source = `data:image/png;base64,${"a".repeat(512)}`;
setAssistantImageMetadata({ source }, { width: 1200, height: 800 });
const imageOnlyHeight = estimateAssistantMessageHeightFromCache(`![Screenshot](${source})`);
const mixedHeight = estimateAssistantMessageHeightFromCache(`Text\n\n![Screenshot](${source})`);
expect(imageOnlyHeight).toBeGreaterThan(220);
expect(mixedHeight).toBeGreaterThan(imageOnlyHeight ?? 0);
});
});
@@ -1,5 +1,6 @@
import { MAX_CONTENT_WIDTH } from "@/constants/layout";
import { resolveAssistantImageSource } from "@/utils/assistant-image-source";
import { createImageSourceCacheKey } from "@/attachments/utils";
export interface AssistantImageMetadata {
width: number;
@@ -48,8 +49,25 @@ function normalizeAssistantImageSourceToken(value: string): string | null {
return source || null;
}
function parseAssistantImageMarkdown(markdown: string): {
sources: string[];
hasNonImageText: boolean;
} {
const sources: string[] = [];
for (const match of markdown.matchAll(MARKDOWN_IMAGE_PATTERN)) {
const normalized = normalizeAssistantImageSourceToken(match[1] ?? "");
if (normalized) {
sources.push(normalized);
}
}
return {
sources,
hasNonImageText: markdown.replace(MARKDOWN_IMAGE_PATTERN, "").trim().length > 0,
};
}
function createSourceAliasKey(source: string): string {
return `source:${source}`;
return `source:${createImageSourceCacheKey(source)}`;
}
function createResolutionKey(input: {
@@ -65,7 +83,7 @@ function createResolutionKey(input: {
return null;
}
if (resolution.kind === "direct") {
return `direct:${resolution.uri}`;
return `direct:${createImageSourceCacheKey(resolution.uri)}`;
}
return `file:${input.serverId ?? "unknown-server"}:${resolution.cwd}:${resolution.path}`;
}
@@ -140,7 +158,8 @@ export function setAssistantImageMetadata(
}
export function extractAssistantImageSources(markdown: string): string[] {
const cachedParse = assistantImageParseCache.get(markdown);
const shouldCacheParse = !/data:image\//i.test(markdown);
const cachedParse = shouldCacheParse ? assistantImageParseCache.get(markdown) : undefined;
if (cachedParse) {
touchCacheEntry(
assistantImageParseCache,
@@ -151,32 +170,15 @@ export function extractAssistantImageSources(markdown: string): string[] {
return cachedParse.sources;
}
const sources: string[] = [];
for (const match of markdown.matchAll(MARKDOWN_IMAGE_PATTERN)) {
const normalized = normalizeAssistantImageSourceToken(match[1] ?? "");
if (normalized) {
sources.push(normalized);
}
const parsed = parseAssistantImageMarkdown(markdown);
if (shouldCacheParse) {
touchCacheEntry(assistantImageParseCache, markdown, parsed, ASSISTANT_IMAGE_PARSE_CACHE_LIMIT);
}
const hasNonImageText = markdown.replace(MARKDOWN_IMAGE_PATTERN, "").trim().length > 0;
touchCacheEntry(
assistantImageParseCache,
markdown,
{ sources, hasNonImageText },
ASSISTANT_IMAGE_PARSE_CACHE_LIMIT,
);
return sources;
return parsed.sources;
}
export function estimateAssistantMessageHeightFromCache(markdown: string): number | null {
const cachedParse = assistantImageParseCache.get(markdown);
const parsed =
cachedParse ??
(() => {
const sources = extractAssistantImageSources(markdown);
const nextParsed = assistantImageParseCache.get(markdown);
return nextParsed ?? { sources, hasNonImageText: true };
})();
const parsed = assistantImageParseCache.get(markdown) ?? parseAssistantImageMarkdown(markdown);
if (parsed.sources.length === 0) {
return null;
}
@@ -28,8 +28,10 @@ function createTestStore(): AttachmentStore {
byteSize = input.source.blob.size;
} else if (input.source.kind === "data_url") {
byteSize = input.source.dataUrl.length;
} else {
} else if (input.source.kind === "file_uri") {
byteSize = input.source.uri.length;
} else {
byteSize = input.source.base64.length;
}
return {
id,