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:
@@ -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 };
|
||||
};
|
||||
};
|
||||
};
|
||||
@@ -0,0 +1,68 @@
|
||||
export type EntryType = "file" | "dir" | "symlink";
|
||||
|
||||
export interface FileEntry {
|
||||
name: string;
|
||||
path: string; // absolute path on its filesystem
|
||||
type: EntryType;
|
||||
size: number; // bytes
|
||||
mtime: number; // epoch milliseconds
|
||||
mode: number; // permission bits only (masked & 0o7777)
|
||||
}
|
||||
|
||||
/** Every RPC response uses this envelope; errors never cross the bridge as exceptions. */
|
||||
export type Result<T = object> = ({ ok: true } & T) | { ok: false; error: string };
|
||||
|
||||
export type AuthConfig =
|
||||
| { kind: "password"; password: string }
|
||||
| { kind: "key"; privateKeyPath: string; passphrase?: string };
|
||||
|
||||
export interface ConnectionConfig {
|
||||
host: string;
|
||||
port: number;
|
||||
username: string;
|
||||
auth: AuthConfig;
|
||||
initialPath?: string | null;
|
||||
}
|
||||
|
||||
export interface SavedConnection extends ConnectionConfig {
|
||||
id: string;
|
||||
folderId: string | null;
|
||||
name: string;
|
||||
}
|
||||
|
||||
export interface ConnFolder {
|
||||
id: string;
|
||||
name: string;
|
||||
}
|
||||
|
||||
export interface ConnectionsFile {
|
||||
version: 1;
|
||||
folders: ConnFolder[];
|
||||
connections: SavedConnection[];
|
||||
}
|
||||
|
||||
export type TransferState =
|
||||
| "queued"
|
||||
| "scanning"
|
||||
| "running"
|
||||
| "completed"
|
||||
| "failed"
|
||||
| "cancelled";
|
||||
|
||||
export interface TransferSnapshot {
|
||||
id: string;
|
||||
name: string; // display name (basename of source)
|
||||
srcSessionId: string;
|
||||
srcPath: string;
|
||||
dstSessionId: string;
|
||||
dstDir: string;
|
||||
state: TransferState;
|
||||
totalFiles: number;
|
||||
totalBytes: number;
|
||||
doneFiles: number;
|
||||
doneBytes: number;
|
||||
currentFile: string;
|
||||
currentFileBytes: number;
|
||||
currentFileTotal: number;
|
||||
error?: string;
|
||||
}
|
||||
Reference in New Issue
Block a user