feat: stream files as binary frames over WebSocket (#659)
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.
This commit is contained in:
@@ -4,6 +4,7 @@ import { createDesktopAttachmentStore } from "./desktop-attachment-store";
|
||||
const {
|
||||
copyDesktopAttachmentFileMock,
|
||||
writeDesktopAttachmentBase64Mock,
|
||||
writeDesktopAttachmentBytesMock,
|
||||
deleteDesktopAttachmentFileMock,
|
||||
garbageCollectDesktopAttachmentFilesMock,
|
||||
readDesktopFileBase64Mock,
|
||||
@@ -18,6 +19,10 @@ const {
|
||||
path: "/managed/att_2.png",
|
||||
byteSize: 4,
|
||||
})),
|
||||
writeDesktopAttachmentBytesMock: vi.fn(async () => ({
|
||||
path: "/managed/att_bytes.png",
|
||||
byteSize: 4,
|
||||
})),
|
||||
deleteDesktopAttachmentFileMock: vi.fn(async () => true),
|
||||
garbageCollectDesktopAttachmentFilesMock: vi.fn(async () => 0),
|
||||
readDesktopFileBase64Mock: vi.fn(async () => "AAECAw=="),
|
||||
@@ -28,6 +33,7 @@ const {
|
||||
vi.mock("./desktop-file-commands", () => ({
|
||||
copyDesktopAttachmentFile: copyDesktopAttachmentFileMock,
|
||||
writeDesktopAttachmentBase64: writeDesktopAttachmentBase64Mock,
|
||||
writeDesktopAttachmentBytes: writeDesktopAttachmentBytesMock,
|
||||
deleteDesktopAttachmentFile: deleteDesktopAttachmentFileMock,
|
||||
garbageCollectDesktopAttachmentFiles: garbageCollectDesktopAttachmentFilesMock,
|
||||
}));
|
||||
@@ -42,6 +48,7 @@ describe("desktop attachment store", () => {
|
||||
beforeEach(() => {
|
||||
copyDesktopAttachmentFileMock.mockClear();
|
||||
writeDesktopAttachmentBase64Mock.mockClear();
|
||||
writeDesktopAttachmentBytesMock.mockClear();
|
||||
deleteDesktopAttachmentFileMock.mockClear();
|
||||
garbageCollectDesktopAttachmentFilesMock.mockClear();
|
||||
readDesktopFileBase64Mock.mockClear();
|
||||
@@ -86,28 +93,30 @@ describe("desktop attachment store", () => {
|
||||
});
|
||||
});
|
||||
|
||||
it("saves raw base64 sources via desktop filesystem writes", async () => {
|
||||
it("saves raw byte sources via desktop filesystem writes", async () => {
|
||||
const store = createDesktopAttachmentStore();
|
||||
const bytes = new Uint8Array([0, 1, 2, 3]);
|
||||
const attachment = await store.save({
|
||||
id: "att_base64",
|
||||
id: "att_bytes",
|
||||
mimeType: "image/png",
|
||||
fileName: "inline.png",
|
||||
source: {
|
||||
kind: "base64",
|
||||
base64: "AAECAw==",
|
||||
kind: "bytes",
|
||||
bytes,
|
||||
},
|
||||
});
|
||||
|
||||
expect(writeDesktopAttachmentBase64Mock).toHaveBeenCalledWith({
|
||||
attachmentId: "att_base64",
|
||||
base64: "AAECAw==",
|
||||
expect(writeDesktopAttachmentBytesMock).toHaveBeenCalledWith({
|
||||
attachmentId: "att_bytes",
|
||||
bytes,
|
||||
extension: ".png",
|
||||
});
|
||||
expect(writeDesktopAttachmentBase64Mock).not.toHaveBeenCalled();
|
||||
expect(attachment).toMatchObject({
|
||||
id: "att_base64",
|
||||
id: "att_bytes",
|
||||
mimeType: "image/png",
|
||||
storageType: "desktop-file",
|
||||
storageKey: "/managed/att_2.png",
|
||||
storageKey: "/managed/att_bytes.png",
|
||||
fileName: "inline.png",
|
||||
byteSize: 4,
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user