refactor(app): introduce attachment storage layer and draft lifecycle management

Replace inline data-URL image handling with a persistent attachment storage service, add draft lifecycle states (active/abandoned/sent) with generation tracking, and update fetchAgent to return project placement alongside agent snapshots.
This commit is contained in:
Mohamed Boudra
2026-02-22 16:22:21 +07:00
parent 2652bdda2e
commit 0c710b09f5
55 changed files with 2381 additions and 350 deletions
@@ -1,3 +1,6 @@
import type { AttachmentMetadata } from "@/attachments/types";
import { persistAttachmentFromBlob } from "@/attachments/service";
export interface ClipboardItemLike {
kind?: string;
type?: string;
@@ -8,23 +11,7 @@ export interface ClipboardDataLike {
items?: ArrayLike<ClipboardItemLike> | null;
}
export interface ImageAttachmentFromFile {
uri: string;
mimeType: string;
}
function toBase64(buffer: ArrayBuffer): string {
const bytes = new Uint8Array(buffer);
const chunkSize = 0x8000;
let binary = "";
for (let index = 0; index < bytes.length; index += chunkSize) {
const chunk = bytes.subarray(index, index + chunkSize);
binary += String.fromCharCode(...chunk);
}
return btoa(binary);
}
export type ImageAttachmentFromFile = AttachmentMetadata;
export function collectImageFilesFromClipboardData(
clipboardData?: ClipboardDataLike | null
@@ -54,14 +41,25 @@ export function collectImageFilesFromClipboardData(
export async function filesToImageAttachments(
files: readonly File[]
): Promise<ImageAttachmentFromFile[]> {
return Promise.all(
const attachments = await Promise.all(
files.map(async (file) => {
const mimeType = file.type || "image/jpeg";
const base64 = toBase64(await file.arrayBuffer());
return {
uri: `data:${mimeType};base64,${base64}`,
mimeType,
};
try {
return await persistAttachmentFromBlob({
blob: file,
mimeType: file.type || "image/jpeg",
fileName: file.name,
});
} catch (error) {
console.error("[attachments] Failed to persist file attachment", {
fileName: file.name,
error,
});
return null;
}
})
);
return attachments.filter(
(entry): entry is ImageAttachmentFromFile => entry !== null
);
}