import path from "path"; import { BrowserWindow, BrowserView, Updater } from "electrobun/bun"; import { PtyClient } from "./pty-client.ts"; import { settingsDb } from "./settings-db.ts"; import type { PtyRPCSchema } from "../shared/pty-rpc-schema.ts"; const DEV_SERVER_PORT = 9760; // Project convention: 9700+ range const DEV_SERVER_URL = `http://localhost:${DEV_SERVER_PORT}`; // ── PTY daemon client ──────────────────────────────────────────────────────── // Resolve daemon socket directory. agor-ptyd writes ptyd.sock and ptyd.token // into $XDG_RUNTIME_DIR/agor or ~/.local/share/agor/run. const ptyClient = new PtyClient(); async function connectToDaemon(retries = 5, delayMs = 500): Promise { for (let attempt = 1; attempt <= retries; attempt++) { try { await ptyClient.connect(); console.log("[agor-ptyd] Connected to PTY daemon"); return true; } catch (err) { const msg = err instanceof Error ? err.message : String(err); if (attempt < retries) { console.warn(`[agor-ptyd] Connect attempt ${attempt}/${retries} failed: ${msg}. Retrying in ${delayMs}ms…`); await new Promise((r) => setTimeout(r, delayMs)); delayMs = Math.min(delayMs * 2, 4000); } else { console.error(`[agor-ptyd] Could not connect after ${retries} attempts: ${msg}`); console.error("[agor-ptyd] Terminals will not work. Start agor-ptyd and restart the app."); } } } return false; } // ── RPC definition ──────────────────────────────────────────────────────────── /** * BrowserView.defineRPC defines handlers that the WebView can call. * The schema type parameter describes what the Bun side handles (requests from * WebView) and what messages Bun sends to the WebView. * * Pattern: handlers.requests = WebView→Bun calls * handlers.messages = Bun→WebView fire-and-forget * To push a message to the WebView: rpc.send["pty.output"](payload) */ const rpc = BrowserView.defineRPC({ maxRequestTime: 10_000, handlers: { requests: { "pty.create": async ({ sessionId, cols, rows, cwd }) => { if (!ptyClient.isConnected) { return { ok: false, error: "PTY daemon not connected" }; } try { ptyClient.createSession({ id: sessionId, cols, rows, cwd }); // Don't call subscribe() — CreateSession already auto-subscribes // the creating client and starts a fanout task. return { ok: true }; } catch (err) { const error = err instanceof Error ? err.message : String(err); console.error(`[pty.create] ${sessionId}: ${error}`); return { ok: false, error }; } }, "pty.write": ({ sessionId, data }) => { if (!ptyClient.isConnected) return { ok: false }; try { ptyClient.writeInput(sessionId, data); return { ok: true }; } catch (err) { console.error(`[pty.write] ${sessionId}:`, err); return { ok: false }; } }, "pty.resize": ({ sessionId, cols, rows }) => { if (!ptyClient.isConnected) return { ok: true }; // Best-effort try { ptyClient.resize(sessionId, cols, rows); } catch { /* ignore */ } return { ok: true }; }, "pty.unsubscribe": ({ sessionId }) => { try { ptyClient.unsubscribe(sessionId); } catch { /* ignore */ } return { ok: true }; }, "pty.close": ({ sessionId }) => { try { ptyClient.closeSession(sessionId); } catch { /* ignore */ } return { ok: true }; }, // ── Settings handlers ───────────────────────────────────────────────── "settings.get": ({ key }) => { try { return { value: settingsDb.getSetting(key) }; } catch (err) { console.error("[settings.get]", err); return { value: null }; } }, "settings.set": ({ key, value }) => { try { settingsDb.setSetting(key, value); return { ok: true }; } catch (err) { console.error("[settings.set]", err); return { ok: false }; } }, "settings.getAll": () => { try { return { settings: settingsDb.getAll() }; } catch (err) { console.error("[settings.getAll]", err); return { settings: {} }; } }, "settings.getProjects": () => { try { const projects = settingsDb.listProjects().map((p) => ({ id: p.id, config: JSON.stringify(p), })); return { projects }; } catch (err) { console.error("[settings.getProjects]", err); return { projects: [] }; } }, "settings.setProject": ({ id, config }) => { try { const parsed = JSON.parse(config); settingsDb.setProject(id, { id, ...parsed }); return { ok: true }; } catch (err) { console.error("[settings.setProject]", err); return { ok: false }; } }, // ── Custom Themes handlers ─────────────────────────────────────────── "themes.getCustom": () => { try { return { themes: settingsDb.getCustomThemes() }; } catch (err) { console.error("[themes.getCustom]", err); return { themes: [] }; } }, "themes.saveCustom": ({ id, name, palette }) => { try { settingsDb.saveCustomTheme(id, name, palette); return { ok: true }; } catch (err) { console.error("[themes.saveCustom]", err); return { ok: false }; } }, "themes.deleteCustom": ({ id }) => { try { settingsDb.deleteCustomTheme(id); return { ok: true }; } catch (err) { console.error("[themes.deleteCustom]", err); return { ok: false }; } }, }, messages: { // Messages section defines what the WebView can *send* to Bun (fire-and-forget). // We don't expect any inbound messages from the WebView in this direction. }, }, }); // ── Forward daemon events to WebView ──────────────────────────────────────── // session_output: forward each chunk to the WebView as a "pty.output" message. ptyClient.on("session_output", (msg) => { if (msg.type !== "session_output") return; try { // data is already base64 from the daemon — pass it straight through. rpc.send["pty.output"]({ sessionId: msg.session_id, data: msg.data }); } catch (err) { console.error("[pty.output] forward error:", err); } }); // session_closed: notify the WebView so it can display "[Process exited]". ptyClient.on("session_closed", (msg) => { if (msg.type !== "session_closed") return; try { rpc.send["pty.closed"]({ sessionId: msg.session_id, exitCode: msg.exit_code }); } catch (err) { console.error("[pty.closed] forward error:", err); } }); // ── App window ─────────────────────────────────────────────────────────────── async function getMainViewUrl(): Promise { const channel = await Updater.localInfo.channel(); if (channel === "dev") { try { await fetch(DEV_SERVER_URL, { method: "HEAD" }); console.log(`HMR enabled: Using Vite dev server at ${DEV_SERVER_URL}`); return DEV_SERVER_URL; } catch { console.log( "Vite dev server not running. Run 'bun run dev:hmr' for HMR support.", ); } } return "views://mainview/index.html"; } // Connect to daemon (non-blocking — window opens regardless). connectToDaemon(); const url = await getMainViewUrl(); const mainWindow = new BrowserWindow({ title: "Agent Orchestrator — Electrobun", url, rpc, frame: { width: 1400, height: 900, x: 100, y: 100, }, }); console.log("Agent Orchestrator (Electrobun) started!");