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:
@@ -1,5 +1,6 @@
|
||||
import { useCallback, useMemo } from "react";
|
||||
import { useSessionStore, type AgentFileExplorerState } from "@/stores/session-store";
|
||||
import { explorerFileFromReadResult } from "@/file-explorer/read-result";
|
||||
|
||||
function createExplorerState(): AgentFileExplorerState {
|
||||
return {
|
||||
@@ -130,26 +131,20 @@ export function useFileExplorerActions(params: { serverId: string } & FileExplor
|
||||
}
|
||||
|
||||
try {
|
||||
const payload = await client.exploreFileSystem(
|
||||
normalizedWorkspaceRoot,
|
||||
normalizedPath,
|
||||
"list",
|
||||
);
|
||||
const directory = await client.listDirectory(normalizedWorkspaceRoot, normalizedPath);
|
||||
updateExplorerState((state) => {
|
||||
const nextState: AgentFileExplorerState = {
|
||||
...state,
|
||||
isLoading: false,
|
||||
lastError: payload.error ?? null,
|
||||
lastError: null,
|
||||
pendingRequest: null,
|
||||
directories: state.directories,
|
||||
files: state.files,
|
||||
};
|
||||
|
||||
if (!payload.error && payload.directory) {
|
||||
const directories = new Map(state.directories);
|
||||
directories.set(payload.directory.path, payload.directory);
|
||||
nextState.directories = directories;
|
||||
}
|
||||
const directories = new Map(state.directories);
|
||||
directories.set(directory.path, directory);
|
||||
nextState.directories = directories;
|
||||
|
||||
return nextState;
|
||||
});
|
||||
@@ -201,34 +196,29 @@ export function useFileExplorerActions(params: { serverId: string } & FileExplor
|
||||
}
|
||||
|
||||
try {
|
||||
const payload = await client.exploreFileSystem(
|
||||
normalizedWorkspaceRoot,
|
||||
normalizedPath,
|
||||
"file",
|
||||
);
|
||||
const file = await client.readFile(normalizedWorkspaceRoot, normalizedPath);
|
||||
updateExplorerState((state) => {
|
||||
const nextState: AgentFileExplorerState = {
|
||||
...state,
|
||||
isLoading: false,
|
||||
lastError: null,
|
||||
pendingRequest: null,
|
||||
directories: state.directories,
|
||||
files: state.files,
|
||||
};
|
||||
|
||||
if (!payload.error && payload.file) {
|
||||
const files = new Map(state.files);
|
||||
files.set(payload.file.path, payload.file);
|
||||
nextState.files = files;
|
||||
} else if (payload.error) {
|
||||
nextState.lastError = payload.error;
|
||||
}
|
||||
const files = new Map(state.files);
|
||||
const explorerFile = explorerFileFromReadResult(file);
|
||||
files.set(explorerFile.path, explorerFile);
|
||||
nextState.files = files;
|
||||
|
||||
return nextState;
|
||||
});
|
||||
} catch {
|
||||
} catch (error) {
|
||||
updateExplorerState((state) => ({
|
||||
...state,
|
||||
isLoading: false,
|
||||
lastError: error instanceof Error ? error.message : "Failed to load file preview",
|
||||
pendingRequest: null,
|
||||
}));
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user