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
+56
View File
@@ -0,0 +1,56 @@
import type { EntryType, FileEntry } from "../../shared/types";
import type { FileSystemProvider } from "../fs/provider";
export interface WalkedTree {
/** Relative dir paths as segment arrays, top-down. [] is the root itself. */
dirs: string[][];
files: { entry: FileEntry; dirSegments: string[] }[];
totalBytes: number;
skippedSymlinks: number;
}
/**
* Depth-first walk of a directory. Segment arrays (not joined strings) so the
* destination side can re-join them with its own separator. Symlinks are
* skipped — following them risks cycles and surprising copies.
*/
export async function walkTree(
fs: FileSystemProvider,
rootDir: string,
): Promise<WalkedTree> {
const tree: WalkedTree = { dirs: [[]], files: [], totalBytes: 0, skippedSymlinks: 0 };
async function visit(dir: string, segments: string[]): Promise<void> {
for (const entry of await fs.list(dir)) {
if (entry.type === "symlink") {
tree.skippedSymlinks++;
} else if (entry.type === "dir") {
const childSegments = [...segments, entry.name];
tree.dirs.push(childSegments);
await visit(entry.path, childSegments);
} else {
tree.files.push({ entry, dirSegments: segments });
tree.totalBytes += entry.size;
}
}
}
await visit(rootDir, []);
return tree;
}
/** Post-order recursive delete, provider-generic (works locally and over SFTP). */
export async function removeRecursive(
fs: FileSystemProvider,
path: string,
type: EntryType,
): Promise<void> {
if (type !== "dir") {
await fs.deleteFile(path);
return;
}
for (const entry of await fs.list(path)) {
await removeRecursive(fs, entry.path, entry.type);
}
await fs.deleteEmptyDir(path);
}