Replace base64-in-JSON file transfers with binary WebSocket frames to
unblock the JS thread when assistant markdown messages contain images.
Receive cost (JSON.parse over multi-MB base64) and persist cost (base64
re-encode to disk) both removed.
- Split daemon-client `exploreFileSystem` into `listDirectory` + `readFile`;
`readFile` returns `{ bytes, mime, size, modifiedAt }`. Encoding stays
private to the seam.
- New `packages/server/src/shared/binary-frames/` module hosts terminal
frames (moved from `terminal-stream-protocol.ts`) and new file-transfer
opcodes (`FileBegin` / `FileChunk` / `FileEnd`) with request-id correlation.
- Add optional `acceptBinary` to `FileExplorerRequestSchema`. New daemons
emit binary frames when set; legacy JSON path remains for old clients.
Compat fallback lives only inside `readFile`.
- New `persistAttachmentFromBytes` writes bytes directly: native via
`expo-file-system` `File.write(Uint8Array)`, web via Blob -> IndexedDB,
desktop via new `write_attachment_bytes` IPC. `persistAttachmentFromBase64`
removed.
51 lines
1.6 KiB
TypeScript
51 lines
1.6 KiB
TypeScript
import type { AttachmentMetadata } from "@/attachments/types";
|
|
import { fileUriToPath } from "@/attachments/utils";
|
|
import { invokeDesktopCommand } from "@/desktop/electron/invoke";
|
|
import { Buffer } from "buffer";
|
|
|
|
function base64ToUint8Array(base64: string): Uint8Array {
|
|
return new Uint8Array(Buffer.from(base64, "base64"));
|
|
}
|
|
|
|
const activeObjectUrls = new Set<string>();
|
|
|
|
export async function readDesktopFileBase64(pathOrUri: string): Promise<string> {
|
|
return await invokeDesktopCommand<string>("read_file_base64", {
|
|
path: fileUriToPath(pathOrUri),
|
|
});
|
|
}
|
|
|
|
export async function resolveDesktopPreviewUrl(attachment: AttachmentMetadata): Promise<string> {
|
|
const base64 = await readDesktopFileBase64(attachment.storageKey);
|
|
|
|
if (typeof URL !== "undefined" && typeof URL.createObjectURL === "function") {
|
|
const bytes = base64ToUint8Array(base64);
|
|
const buffer = new ArrayBuffer(bytes.byteLength);
|
|
new Uint8Array(buffer).set(bytes);
|
|
const blob = new Blob([buffer], { type: attachment.mimeType });
|
|
const objectUrl = URL.createObjectURL(blob);
|
|
activeObjectUrls.add(objectUrl);
|
|
return objectUrl;
|
|
}
|
|
|
|
return `data:${attachment.mimeType};base64,${base64}`;
|
|
}
|
|
|
|
export async function releaseDesktopPreviewUrl(input: { url: string }): Promise<void> {
|
|
if (!activeObjectUrls.has(input.url)) {
|
|
return;
|
|
}
|
|
activeObjectUrls.delete(input.url);
|
|
|
|
if (typeof URL !== "undefined" && typeof URL.revokeObjectURL === "function") {
|
|
URL.revokeObjectURL(input.url);
|
|
}
|
|
}
|
|
|
|
export const __desktopPreviewUrlTestUtils = {
|
|
base64ToUint8Array,
|
|
clearActiveObjectUrls: () => {
|
|
activeObjectUrls.clear();
|
|
},
|
|
};
|