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:
Mohamed Boudra
2026-05-02 14:02:27 +08:00
committed by GitHub
parent b6c43088fb
commit bb889dae99
44 changed files with 1630 additions and 436 deletions
+8 -12
View File
@@ -98,7 +98,7 @@ import { PlanCard } from "./plan-card";
import { useToolCallSheet } from "./tool-call-sheet";
import { ToolCallDetailsContent } from "./tool-call-details";
import { useAttachmentPreviewUrl } from "@/attachments/use-attachment-preview-url";
import { persistAttachmentFromBase64, persistAttachmentFromDataUrl } from "@/attachments/service";
import { persistAttachmentFromBytes, persistAttachmentFromDataUrl } from "@/attachments/service";
import type { DaemonClient } from "@server/client/daemon-client";
import { isWeb, isNative } from "@/constants/platform";
export type { InlinePathTarget } from "@/utils/inline-path";
@@ -760,25 +760,21 @@ function AssistantMarkdownImage({
return null;
}
const payload = await client.exploreFileSystem(resolution.cwd, resolution.path, "file");
if (payload.error) {
throw new Error(payload.error);
}
const file = payload.file;
if (!file || file.kind !== "image" || !file.content) {
const file = await client.readFile(resolution.cwd, resolution.path);
if (file.kind !== "image") {
throw new Error("Image preview unavailable.");
}
return await persistAttachmentFromBase64({
return await persistAttachmentFromBytes({
id: createPreviewAttachmentId({
mimeType: file.mimeType ?? "image/png",
mimeType: file.mime,
path: file.path || resolution.path,
size: file.size,
modifiedAt: file.modifiedAt,
contentLength: file.content.length,
contentLength: file.bytes.byteLength,
}),
base64: file.content,
mimeType: file.mimeType,
bytes: file.bytes,
mimeType: file.mime,
fileName: getFileNameFromPath(file.path || resolution.path),
});
},