src / mediaHistory.ts
const GENERATED_IMAGE_HISTORY_LIMIT = 20;
const IMAGE_PATH_PATTERN =
/(?:file:\/\/)?(?:[A-Za-z]:)?[A-Za-z0-9_./%:+~-]+\.(?:avif|bmp|gif|jpeg|jpg|png|tif|tiff|webp)\b/gi;
const MARKDOWN_IMAGE_PATTERN = /!\[[^\]]*\]\(([^)]+)\)/g;
type GeneratedImageRecord = {
path: string;
absolutePath?: string;
createdAt: string;
};
const generatedImages: GeneratedImageRecord[] = [];
export function rememberGeneratedImage(record: {
path: string;
absolutePath?: string;
createdAt?: string;
}): void {
const path = cleanPath(record.path);
if (!path) return;
const existingIndex = generatedImages.findIndex((item) => item.path === path);
if (existingIndex >= 0) {
generatedImages.splice(existingIndex, 1);
}
generatedImages.unshift({
path,
absolutePath: cleanPath(record.absolutePath) || undefined,
createdAt: record.createdAt || new Date().toISOString(),
});
generatedImages.splice(GENERATED_IMAGE_HISTORY_LIMIT);
}
export function getRememberedGeneratedImages(maxResults: number): GeneratedImageRecord[] {
return generatedImages.slice(0, Math.max(0, maxResults));
}
export function extractGeneratedImagePathsFromToolResultContent(content: string): string[] {
const found = new Set<string>();
try {
collectGeneratedImageResultPaths(JSON.parse(content), found);
} catch {
// Tool result content is not guaranteed to be JSON.
}
if (found.size === 0) {
collectImagePathStrings(content, found);
}
return [...found];
}
function collectGeneratedImageResultPaths(value: unknown, found: Set<string>): void {
if (Array.isArray(value)) {
for (const item of value) {
collectGeneratedImageResultPaths(item, found);
}
return;
}
if (!value || typeof value !== "object") return;
const record = value as Record<string, unknown>;
if (record.type === "image") {
collectImagePathStrings(record.outputPath, found);
collectImagePathStrings(record.absoluteOutputPath, found);
collectImagePathStrings(record.imageMarkdown, found);
collectImagePathStrings(record.images, found);
return;
}
for (const item of Object.values(record)) {
collectGeneratedImageResultPaths(item, found);
}
}
function collectImagePathStrings(value: unknown, found: Set<string>): void {
if (typeof value === "string") {
collectImagePathMatches(value, found);
return;
}
if (Array.isArray(value)) {
for (const item of value) {
collectImagePathStrings(item, found);
}
return;
}
if (value && typeof value === "object") {
for (const item of Object.values(value)) {
collectImagePathStrings(item, found);
}
}
}
function collectImagePathMatches(value: string, found: Set<string>): void {
for (const match of value.matchAll(MARKDOWN_IMAGE_PATTERN)) {
addCandidate(match[1], found);
}
for (const match of value.matchAll(IMAGE_PATH_PATTERN)) {
addCandidate(match[0], found);
}
}
function addCandidate(candidate: string | undefined, found: Set<string>): void {
const cleaned = cleanPath(candidate)
.replace(/^['"]+/, "")
.replace(/['")\]]+$/, "");
if (cleaned && !/-poster\.png$/i.test(cleaned)) {
found.add(cleaned);
}
}
function cleanPath(value?: string): string {
return typeof value === "string" ? value.trim() : "";
}