P1: typed RPC bridge + FileSystemProvider + dual-panel local browsing

- shared/types + shared/rpc: full wire contract (Result envelope, no exceptions cross the bridge)
- FileSystemProvider interface, LocalProvider, SessionRegistry with built-in 'local'
- walk.ts: provider-generic recursive walk + removeRecursive
- rpc-handlers wired into BrowserWindow; 10min request timeout for slow SFTP ops
- Angular: RpcService (Electroview bridge + plain-browser guard), PanelsService with
  signals-based PanelState (sort/cursor/selection), FilePanel component with
  keyboard nav (arrows/Space/Enter/Backspace/Tab), path bar, toasts
- verified: both panels listDir home over the bridge in the running app

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
Jokin
2026-07-11 13:35:02 +02:00
co-authored by Claude Fable 5
parent 8092cb0c08
commit a150c5ee2e
17 changed files with 1062 additions and 15 deletions
+80
View File
@@ -0,0 +1,80 @@
import type {
ConnFolder,
ConnectionConfig,
ConnectionsFile,
EntryType,
FileEntry,
Result,
SavedConnection,
TransferSnapshot,
} from "./types";
// Structurally equivalent to electrobun's ElectrobunRPCSchema, written in
// resolved form so this file stays importable from the Angular bundle without
// pulling in bun-only type modules.
//
// bun.requests = calls the webview makes, handled in src/bun/rpc-handlers.ts
// webview.messages = push events from bun, handled in ui rpc.service.ts
export type AppRPC = {
bun: {
requests: {
// sessions
connect: {
params: { config: ConnectionConfig };
response: Result<{ sessionId: string; homeDir: string }>;
};
disconnect: { params: { sessionId: string }; response: Result };
// filesystem (uniform across "local" and sftp sessions)
listDir: {
params: { sessionId: string; path: string | null }; // null -> home dir
response: Result<{ path: string; entries: FileEntry[] }>;
};
mkdir: { params: { sessionId: string; path: string }; response: Result };
createFile: { params: { sessionId: string; path: string }; response: Result };
rename: {
params: { sessionId: string; oldPath: string; newPath: string };
response: Result;
};
deleteEntries: {
params: { sessionId: string; entries: { path: string; type: EntryType }[] };
response: Result;
};
chmod: {
params: { sessionId: string; paths: string[]; mode: number };
response: Result;
};
// transfers
enqueueTransfers: {
params: {
srcSessionId: string;
items: { path: string; type: EntryType }[];
dstSessionId: string;
dstDir: string;
};
response: Result<{ jobIds: string[] }>;
};
cancelTransfer: { params: { jobId: string }; response: Result };
dismissTransfer: { params: { jobId: string }; response: Result };
// connection store
getConnections: {
params: Record<string, never>;
response: Result<{ store: ConnectionsFile }>;
};
upsertConnection: {
params: { connection: SavedConnection };
response: Result<{ id: string }>;
};
deleteConnection: { params: { id: string }; response: Result };
upsertFolder: { params: { folder: ConnFolder }; response: Result<{ id: string }> };
deleteFolder: { params: { id: string }; response: Result };
};
messages: Record<never, never>;
};
webview: {
requests: Record<never, never>;
messages: {
transferUpdate: TransferSnapshot;
sessionClosed: { sessionId: string; reason: string };
};
};
};