mirror of
https://github.com/getpaseo/paseo.git
synced 2026-07-29 12:01:31 +00:00
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.
98 lines
2.7 KiB
TypeScript
98 lines
2.7 KiB
TypeScript
import { afterEach, describe, expect, it } from "vitest";
|
|
import type { AttachmentMetadata, AttachmentStore, SaveAttachmentInput } from "@/attachments/types";
|
|
import { __setAttachmentStoreForTests } from "./store";
|
|
import { encodeAttachmentsForSend, persistAttachmentFromBytes } from "./service";
|
|
|
|
function createAttachment(input: Partial<AttachmentMetadata> = {}): AttachmentMetadata {
|
|
return {
|
|
id: input.id ?? "att_1",
|
|
mimeType: input.mimeType ?? "image/png",
|
|
storageType: input.storageType ?? "web-indexeddb",
|
|
storageKey: input.storageKey ?? "att_1",
|
|
fileName: input.fileName,
|
|
byteSize: input.byteSize,
|
|
createdAt: input.createdAt ?? 1700000000000,
|
|
};
|
|
}
|
|
|
|
function createRecordingStore(): AttachmentStore & {
|
|
savedSources: SaveAttachmentInput[];
|
|
releasedUrls: string[];
|
|
} {
|
|
const savedSources: SaveAttachmentInput[] = [];
|
|
const releasedUrls: string[] = [];
|
|
|
|
return {
|
|
storageType: "web-indexeddb",
|
|
savedSources,
|
|
releasedUrls,
|
|
async save(input) {
|
|
savedSources.push(input);
|
|
return createAttachment({
|
|
id: input.id,
|
|
mimeType: input.mimeType,
|
|
fileName: input.fileName,
|
|
byteSize: 4,
|
|
});
|
|
},
|
|
async encodeBase64({ attachment }) {
|
|
return `${attachment.id}:base64`;
|
|
},
|
|
async resolvePreviewUrl({ attachment }) {
|
|
return `blob:${attachment.id}`;
|
|
},
|
|
async releasePreviewUrl({ url }) {
|
|
releasedUrls.push(url);
|
|
},
|
|
async delete() {},
|
|
async garbageCollect() {},
|
|
};
|
|
}
|
|
|
|
describe("attachment service", () => {
|
|
afterEach(() => {
|
|
__setAttachmentStoreForTests(null);
|
|
});
|
|
|
|
it("persists raw bytes without requiring a base64 wrapper", async () => {
|
|
const store = createRecordingStore();
|
|
__setAttachmentStoreForTests(store);
|
|
const bytes = new Uint8Array([0, 1, 2, 3]);
|
|
|
|
const attachment = await persistAttachmentFromBytes({
|
|
id: "att_bytes",
|
|
bytes,
|
|
mimeType: "image/png",
|
|
fileName: "image.png",
|
|
});
|
|
|
|
expect(attachment).toEqual({
|
|
id: "att_bytes",
|
|
mimeType: "image/png",
|
|
storageType: "web-indexeddb",
|
|
storageKey: "att_1",
|
|
fileName: "image.png",
|
|
byteSize: 4,
|
|
createdAt: 1700000000000,
|
|
});
|
|
expect(store.savedSources).toEqual([
|
|
{
|
|
id: "att_bytes",
|
|
mimeType: "image/png",
|
|
fileName: "image.png",
|
|
source: { kind: "bytes", bytes },
|
|
},
|
|
]);
|
|
});
|
|
|
|
it("keeps provider send output byte-compatible", async () => {
|
|
const store = createRecordingStore();
|
|
__setAttachmentStoreForTests(store);
|
|
const attachment = createAttachment({ id: "att_send", mimeType: "image/jpeg" });
|
|
|
|
await expect(encodeAttachmentsForSend([attachment])).resolves.toEqual([
|
|
{ data: "att_send:base64", mimeType: "image/jpeg" },
|
|
]);
|
|
});
|
|
});
|