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
+42
View File
@@ -0,0 +1,42 @@
import { LocalProvider } from "./local";
import type { FileSystemProvider } from "./provider";
export const LOCAL_SESSION_ID = "local";
/**
* Maps sessionId -> provider. "local" is always available; each SFTP connect
* adds a new session (one SSH connection per panel-connect, no refcounting).
*/
export class SessionRegistry {
private sessions = new Map<string, FileSystemProvider>([
[LOCAL_SESSION_ID, new LocalProvider()],
]);
private counter = 0;
get(sessionId: string): FileSystemProvider {
const provider = this.sessions.get(sessionId);
if (!provider) throw new Error(`Session "${sessionId}" is not connected`);
return provider;
}
add(provider: FileSystemProvider): string {
const id = `sftp-${++this.counter}`;
this.sessions.set(id, provider);
return id;
}
async remove(sessionId: string): Promise<void> {
if (sessionId === LOCAL_SESSION_ID)
throw new Error("The local session cannot be disconnected");
const provider = this.sessions.get(sessionId);
if (!provider) return;
this.sessions.delete(sessionId);
await provider.dispose();
}
/** Remove without dispose — for sessions that died on their own. */
drop(sessionId: string): void {
if (sessionId === LOCAL_SESSION_ID) return;
this.sessions.delete(sessionId);
}
}