feat(electrobun): wire PTY daemon into terminal tabs via Electrobun RPC

- Bun process connects to agor-ptyd via PtyClient (5 retries, exponential backoff)
- RPC bridge: 5 request handlers (create/write/resize/unsubscribe/close)
- Daemon output forwarded to WebView as pty.output messages (base64 passthrough)
- Terminal.svelte: real PTY sessions via RPC instead of echo mode
- Shared RPC schema at src/shared/pty-rpc-schema.ts
- Fixed pty-client.ts protocol: base64 string for data (was number array)
- TerminalTabs passes sessionId to Terminal component
This commit is contained in:
Hibryda 2026-03-20 03:20:13 +01:00
parent f3456bd09d
commit 4676fc2c94
6 changed files with 343 additions and 86 deletions

View file

@ -25,7 +25,8 @@ export interface SessionInfo {
export type DaemonEvent =
| { type: "auth_result"; ok: boolean }
| { type: "session_created"; session_id: string; pid: number }
| { type: "session_output"; session_id: string; data: number[] }
/** data is base64-encoded bytes from the PTY. */
| { type: "session_output"; session_id: string; data: string }
| { type: "session_closed"; session_id: string; exit_code: number | null }
| { type: "session_list"; sessions: SessionInfo[] }
| { type: "pong" }
@ -131,13 +132,15 @@ export class PtyClient extends EventEmitter {
});
}
/** Write input to a session's PTY. */
/** Write input to a session's PTY. data is encoded as base64 for transport. */
writeInput(sessionId: string, data: string | Uint8Array): void {
const bytes =
typeof data === "string"
? Array.from(new TextEncoder().encode(data))
: Array.from(data);
this.send({ type: "write_input", session_id: sessionId, data: bytes });
? new TextEncoder().encode(data)
: data;
// Daemon expects base64 string per protocol.rs WriteInput { data: String }
const b64 = btoa(String.fromCharCode(...bytes));
this.send({ type: "write_input", session_id: sessionId, data: b64 });
}
/** Resize a session's terminal. */
@ -199,10 +202,18 @@ export class PtyClient extends EventEmitter {
* terminal.write(new Uint8Array(chunk));
* }
*/
/**
* Connect to daemon, create a session, and return output as an async iterator.
* Each yielded chunk is a Uint8Array of raw PTY bytes (decoded from base64).
* Usage:
* for await (const chunk of ptySession("my-shell", { cols: 120, rows: 40 })) {
* terminal.write(chunk);
* }
*/
export async function* ptySession(
sessionId: string,
opts?: { shell?: string; cwd?: string; cols?: number; rows?: number; socketDir?: string }
): AsyncGenerator<number[], void, void> {
): AsyncGenerator<Uint8Array, void, void> {
const client = new PtyClient(opts?.socketDir);
await client.connect();
@ -222,7 +233,11 @@ export async function* ptySession(
});
if (msg.type === "session_output" && msg.session_id === sessionId) {
yield msg.data;
// Decode base64 → raw bytes
const binary = atob(msg.data);
const bytes = new Uint8Array(binary.length);
for (let i = 0; i < binary.length; i++) bytes[i] = binary.charCodeAt(i);
yield bytes;
} else if (msg.type === "session_closed" && msg.session_id === sessionId) {
break;
}