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
+20 -25
View File
@@ -2,6 +2,10 @@ import { useState, useRef, useEffect } from "react";
import { Platform } from "react-native";
import type { ImageAttachment } from "@/components/message-input";
import { getCurrentTauriWindow, getTauri } from "@/utils/tauri";
import {
persistAttachmentFromBlob,
persistAttachmentFromFileUri,
} from "@/attachments/service";
interface UseFileDropZoneOptions {
onFilesDropped: (files: ImageAttachment[]) => void;
@@ -66,34 +70,17 @@ function isImagePath(path: string): boolean {
return getFileExtension(path) in IMAGE_MIME_BY_EXTENSION;
}
function filePathToImageAttachment(path: string): ImageAttachment {
async function filePathToImageAttachment(path: string): Promise<ImageAttachment> {
const extension = getFileExtension(path);
const mimeType = IMAGE_MIME_BY_EXTENSION[extension] ?? "image/jpeg";
const convertFileSrc = getTauri()?.core?.convertFileSrc;
const uri =
typeof convertFileSrc === "function" ? convertFileSrc(path) : path;
return {
uri,
mimeType,
};
return await persistAttachmentFromFileUri({ uri: path, mimeType });
}
async function fileToImageAttachment(file: File): Promise<ImageAttachment> {
return new Promise((resolve, reject) => {
const reader = new FileReader();
reader.onload = () => {
if (typeof reader.result === "string") {
resolve({
uri: reader.result,
mimeType: file.type || "image/jpeg",
});
} else {
reject(new Error("Failed to read file as data URL"));
}
};
reader.onerror = () => reject(reader.error);
reader.readAsDataURL(file);
return await persistAttachmentFromBlob({
blob: file,
mimeType: file.type || "image/jpeg",
fileName: file.name,
});
}
@@ -177,8 +164,16 @@ export function useFileDropZone({
return;
}
const attachments = imagePaths.map(filePathToImageAttachment);
onFilesDroppedRef.current(attachments);
void Promise.all(imagePaths.map(filePathToImageAttachment))
.then((attachments) => {
if (attachments.length === 0) {
return;
}
onFilesDroppedRef.current(attachments);
})
.catch((error) => {
console.error("[useFileDropZone] Failed to persist dropped files:", error);
});
}
);
@@ -144,11 +144,9 @@ function toAggregatedAgent(params: {
}
export function useSidebarAgentsList(options?: {
isOpen?: boolean;
serverId?: string | null;
selectedProjectFilterKeys?: string[];
}): SidebarAgentsListResult {
const isSidebarOpen = options?.isOpen ?? true;
const { daemons } = useDaemonRegistry();
const runtime = getHostRuntimeStore();
const daemonLabelSignature = useMemo(
@@ -181,7 +179,7 @@ export function useSidebarAgentsList(options?: {
[options?.selectedProjectFilterKeys]
);
const isActive = Boolean(serverId && isSidebarOpen);
const isActive = Boolean(serverId);
const liveAgents = useSessionStore((state) =>
isActive && serverId ? state.sessions[serverId]?.agents ?? null : null
);
@@ -309,7 +307,7 @@ export function useSidebarAgentsList(options?: {
if (!isActive || !serverId || connectionStatus !== "online") {
return;
}
void runtime.refreshAgentDirectory({ serverId }).catch(() => undefined);
void runtime.refreshAgentDirectory({ serverId, page: { limit: 50 } }).catch(() => undefined);
}, [connectionStatus, isActive, runtime, serverId]);
const isDirectoryLoading =