import path from "path"; import fs from "fs"; import { BrowserWindow, BrowserView, Updater } from "electrobun/bun"; import { PtyClient } from "./pty-client.ts"; import { settingsDb } from "./settings-db.ts"; import { sessionDb } from "./session-db.ts"; import { btmsgDb } from "./btmsg-db.ts"; import { bttaskDb } from "./bttask-db.ts"; import { SidecarManager } from "./sidecar-manager.ts"; import type { PtyRPCSchema } from "../shared/pty-rpc-schema.ts"; import { randomUUID } from "crypto"; import { SearchDb } from "./search-db.ts"; import { checkForUpdates, getLastCheckTimestamp } from "./updater.ts"; import { RelayClient } from "./relay-client.ts"; import { initTelemetry, telemetry } from "./telemetry.ts"; import { homedir } from "os"; import { join } from "path"; /** Current app version — sourced from electrobun.config.ts at build time. */ const APP_VERSION = "0.0.1"; const DEV_SERVER_PORT = 9760; // Project convention: 9700+ range const DEV_SERVER_URL = `http://localhost:${DEV_SERVER_PORT}`; // ── PTY daemon client ──────────────────────────────────────────────────────── const ptyClient = new PtyClient(); const sidecarManager = new SidecarManager(); const searchDb = new SearchDb(); const relayClient = new RelayClient(); const PLUGINS_DIR = join(homedir(), ".config", "agor", "plugins"); // Initialize telemetry (console-only unless AGOR_OTLP_ENDPOINT is set) initTelemetry(); 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; } // ── Clone helpers ──────────────────────────────────────────────────────────── const BRANCH_RE = /^[a-zA-Z0-9/_.-]+$/; async function gitWorktreeAdd(mainRepoPath: string, worktreePath: string, branchName: string): Promise<{ ok: boolean; error?: string }> { const proc = Bun.spawn( ["git", "worktree", "add", worktreePath, "-b", branchName], { cwd: mainRepoPath, stderr: "pipe", stdout: "pipe" } ); const exitCode = await proc.exited; if (exitCode !== 0) { const errText = await new Response(proc.stderr).text(); return { ok: false, error: errText.trim() || `git exited with code ${exitCode}` }; } return { ok: true }; } // ── RPC definition ──────────────────────────────────────────────────────────── const rpc = BrowserView.defineRPC({ maxRequestTime: 15_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 }); 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) { console.error(`[pty.write] ${sessionId}: daemon not connected`); return { ok: false }; } try { ptyClient.writeInput(sessionId, data); return { ok: true }; } catch (err) { const error = err instanceof Error ? err.message : String(err); console.error(`[pty.write] ${sessionId}: ${error}`); return { ok: false }; } }, "pty.resize": ({ sessionId, cols, rows }) => { if (!ptyClient.isConnected) return { ok: true }; try { ptyClient.resize(sessionId, cols, rows); } catch (err) { console.error(`[pty.resize] ${sessionId}:`, err); } return { ok: true }; }, "pty.unsubscribe": ({ sessionId }) => { try { ptyClient.unsubscribe(sessionId); } catch (err) { console.error(`[pty.unsubscribe] ${sessionId}:`, err); } return { ok: true }; }, "pty.close": ({ sessionId }) => { try { ptyClient.closeSession(sessionId); } catch (err) { console.error(`[pty.close] ${sessionId}:`, err); } 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 }; } }, // ── File I/O handlers ──────────────────────────────────────────────── "files.list": async ({ path: dirPath }) => { try { const dirents = fs.readdirSync(dirPath, { withFileTypes: true }); const entries = dirents .filter((d) => !d.name.startsWith(".")) .map((d) => { let size = 0; if (d.isFile()) { try { size = fs.statSync(path.join(dirPath, d.name)).size; } catch { /* ignore stat errors */ } } return { name: d.name, type: (d.isDirectory() ? "dir" : "file") as "file" | "dir", size, }; }) .sort((a, b) => { // Directories first, then alphabetical if (a.type !== b.type) return a.type === "dir" ? -1 : 1; return a.name.localeCompare(b.name); }); return { entries }; } catch (err) { const error = err instanceof Error ? err.message : String(err); console.error("[files.list]", error); return { entries: [], error }; } }, "files.read": async ({ path: filePath }) => { try { const stat = fs.statSync(filePath); const MAX_SIZE = 10 * 1024 * 1024; // 10MB if (stat.size > MAX_SIZE) { return { encoding: "utf8" as const, size: stat.size, error: `File too large (${(stat.size / 1024 / 1024).toFixed(1)}MB). Maximum is 10MB.` }; } // Detect binary by reading first 8KB const buf = Buffer.alloc(Math.min(8192, stat.size)); const fd = fs.openSync(filePath, "r"); fs.readSync(fd, buf, 0, buf.length, 0); fs.closeSync(fd); const isBinary = buf.includes(0); // null byte = binary if (isBinary) { const content = fs.readFileSync(filePath).toString("base64"); return { content, encoding: "base64" as const, size: stat.size }; } const content = fs.readFileSync(filePath, "utf8"); return { content, encoding: "utf8" as const, size: stat.size }; } catch (err) { const error = err instanceof Error ? err.message : String(err); console.error("[files.read]", error); return { encoding: "utf8" as const, size: 0, error }; } }, "files.write": async ({ path: filePath, content }) => { try { fs.writeFileSync(filePath, content, "utf8"); return { ok: true }; } catch (err) { const error = err instanceof Error ? err.message : String(err); console.error("[files.write]", error); return { ok: false, error }; } }, // ── Groups handlers ────────────────────────────────────────────────── "groups.list": () => { try { return { groups: settingsDb.listGroups() }; } catch (err) { console.error("[groups.list]", err); return { groups: [] }; } }, // ── Project clone handler ──────────────────────────────────────────── "project.clone": async ({ projectId, branchName }) => { try { if (!BRANCH_RE.test(branchName)) { return { ok: false, error: "Invalid branch name. Use only letters, numbers, /, _, -, ." }; } const source = settingsDb.getProject(projectId); if (!source) { return { ok: false, error: `Project not found: ${projectId}` }; } // Determine the authoritative main repo path const mainRepoPath = source.mainRepoPath ?? source.cwd; // Count existing clones const allProjects = settingsDb.listProjects(); const existingClones = allProjects.filter( (p) => p.cloneOf === projectId || (source.cloneOf && p.cloneOf === source.cloneOf) ); if (existingClones.length >= 3) { return { ok: false, error: "Maximum 3 clones per project reached" }; } const cloneIndex = existingClones.length + 1; // Fix #8: Use UUID suffix to prevent race conditions between concurrent clones const wtSuffix = randomUUID().slice(0, 8); const worktreePath = `${mainRepoPath}-wt-${wtSuffix}`; const gitResult = await gitWorktreeAdd(mainRepoPath, worktreePath, branchName); if (!gitResult.ok) { return { ok: false, error: gitResult.error }; } const cloneId = `${projectId}-clone-${cloneIndex}-${randomUUID().slice(0, 8)}`; const cloneConfig = { id: cloneId, name: `${source.name} [${branchName}]`, cwd: worktreePath, accent: source.accent, provider: source.provider, profile: source.profile, model: source.model, groupId: source.groupId ?? "dev", mainRepoPath, cloneOf: projectId, worktreePath, worktreeBranch: branchName, cloneIndex, }; settingsDb.setProject(cloneId, cloneConfig); return { ok: true, project: { id: cloneId, config: JSON.stringify(cloneConfig) }, }; } catch (err) { const error = err instanceof Error ? err.message : String(err); console.error("[project.clone]", err); return { ok: false, error }; } }, // ── Window control handlers ────────────────────────────────────────── "window.minimize": () => { try { mainWindow.minimize(); return { ok: true }; } catch (err) { console.error("[window.minimize]", err); return { ok: false }; } }, "window.maximize": () => { try { const frame = mainWindow.getFrame(); // Heuristic: if window is already near the screen edge, unmaximize if (frame.x <= 0 && frame.y <= 0) { mainWindow.unmaximize(); } else { mainWindow.maximize(); } return { ok: true }; } catch (err) { console.error("[window.maximize]", err); return { ok: false }; } }, "window.close": () => { try { mainWindow.close(); return { ok: true }; } catch (err) { console.error("[window.close]", err); return { ok: false }; } }, "window.getFrame": () => { try { return mainWindow.getFrame(); } catch { return { x: 0, y: 0, width: 1400, height: 900 }; } }, "window.setPosition": ({ x, y }: { x: number; y: number }) => { try { mainWindow.setPosition(x, y); return { ok: true }; } catch { return { ok: false }; } }, // ── Keybinding handlers ────────────────────────────────────────────── "keybindings.getAll": () => { try { return { keybindings: settingsDb.getKeybindings() }; } catch (err) { console.error("[keybindings.getAll]", err); return { keybindings: {} }; } }, "keybindings.set": ({ id, chord }) => { try { settingsDb.setKeybinding(id, chord); return { ok: true }; } catch (err) { console.error("[keybindings.set]", err); return { ok: false }; } }, "keybindings.reset": ({ id }) => { try { settingsDb.deleteKeybinding(id); return { ok: true }; } catch (err) { console.error("[keybindings.reset]", err); return { ok: false }; } }, // ── Agent handlers ────────────────────────────────────────────────── "agent.start": ({ sessionId, provider, prompt, cwd, model, systemPrompt, maxTurns, permissionMode, claudeConfigDir, extraEnv }) => { try { const result = sidecarManager.startSession(sessionId, provider, prompt, { cwd, model, systemPrompt, maxTurns, permissionMode, claudeConfigDir, extraEnv, }); if (result.ok) { // Forward sidecar messages to webview sidecarManager.onMessage(sessionId, (sid, messages) => { try { rpc.send["agent.message"]({ sessionId: sid, messages }); } catch (err) { console.error("[agent.message] forward error:", err); } }); sidecarManager.onStatus(sessionId, (sid, status, error) => { try { rpc.send["agent.status"]({ sessionId: sid, status, error }); } catch (err) { console.error("[agent.status] forward error:", err); } // Send cost update on status change const sessions = sidecarManager.listSessions(); const session = sessions.find((s) => s.sessionId === sid); if (session) { try { rpc.send["agent.cost"]({ sessionId: sid, costUsd: session.costUsd, inputTokens: session.inputTokens, outputTokens: session.outputTokens, }); } catch { /* ignore */ } } }); } return result; } catch (err) { const error = err instanceof Error ? err.message : String(err); console.error("[agent.start]", err); return { ok: false, error }; } }, "agent.stop": ({ sessionId }) => { try { return sidecarManager.stopSession(sessionId); } catch (err) { const error = err instanceof Error ? err.message : String(err); console.error("[agent.stop]", err); return { ok: false, error }; } }, "agent.prompt": ({ sessionId, prompt }) => { try { return sidecarManager.writePrompt(sessionId, prompt); } catch (err) { const error = err instanceof Error ? err.message : String(err); console.error("[agent.prompt]", err); return { ok: false, error }; } }, "agent.list": () => { try { return { sessions: sidecarManager.listSessions() }; } catch (err) { console.error("[agent.list]", err); return { sessions: [] }; } }, // ── Session persistence handlers ────────────────────────────────── "session.save": ({ projectId, sessionId, provider, status, costUsd, inputTokens, outputTokens, model, error, createdAt, updatedAt }) => { try { sessionDb.saveSession({ projectId, sessionId, provider, status, costUsd, inputTokens, outputTokens, model, error, createdAt, updatedAt, }); return { ok: true }; } catch (err) { console.error("[session.save]", err); return { ok: false }; } }, "session.load": ({ projectId }) => { try { return { session: sessionDb.loadSession(projectId) }; } catch (err) { console.error("[session.load]", err); return { session: null }; } }, "session.list": ({ projectId }) => { try { return { sessions: sessionDb.listSessionsByProject(projectId) }; } catch (err) { console.error("[session.list]", err); return { sessions: [] }; } }, "session.messages.save": ({ messages }) => { try { sessionDb.saveMessages(messages.map((m) => ({ sessionId: m.sessionId, msgId: m.msgId, role: m.role, content: m.content, toolName: m.toolName, toolInput: m.toolInput, timestamp: m.timestamp, costUsd: m.costUsd ?? 0, inputTokens: m.inputTokens ?? 0, outputTokens: m.outputTokens ?? 0, }))); return { ok: true }; } catch (err) { console.error("[session.messages.save]", err); return { ok: false }; } }, "session.messages.load": ({ sessionId }) => { try { return { messages: sessionDb.loadMessages(sessionId) }; } catch (err) { console.error("[session.messages.load]", err); return { messages: [] }; } }, // ── btmsg handlers ──────────────────────────────────────────────── "btmsg.registerAgent": ({ id, name, role, groupId, tier, model }) => { try { btmsgDb.registerAgent(id, name, role, groupId, tier, model); return { ok: true }; } catch (err) { console.error("[btmsg.registerAgent]", err); return { ok: false }; } }, "btmsg.getAgents": ({ groupId }) => { try { return { agents: btmsgDb.getAgents(groupId) }; } catch (err) { console.error("[btmsg.getAgents]", err); return { agents: [] }; } }, "btmsg.sendMessage": ({ fromAgent, toAgent, content }) => { try { const messageId = btmsgDb.sendMessage(fromAgent, toAgent, content); return { ok: true, messageId }; } catch (err) { const error = err instanceof Error ? err.message : String(err); console.error("[btmsg.sendMessage]", err); return { ok: false, error }; } }, "btmsg.listMessages": ({ agentId, otherId, limit }) => { try { return { messages: btmsgDb.listMessages(agentId, otherId, limit ?? 50) }; } catch (err) { console.error("[btmsg.listMessages]", err); return { messages: [] }; } }, "btmsg.markRead": ({ agentId, messageIds }) => { try { btmsgDb.markRead(agentId, messageIds); return { ok: true }; } catch (err) { console.error("[btmsg.markRead]", err); return { ok: false }; } }, "btmsg.listChannels": ({ groupId }) => { try { return { channels: btmsgDb.listChannels(groupId) }; } catch (err) { console.error("[btmsg.listChannels]", err); return { channels: [] }; } }, "btmsg.createChannel": ({ name, groupId, createdBy }) => { try { const channelId = btmsgDb.createChannel(name, groupId, createdBy); return { ok: true, channelId }; } catch (err) { console.error("[btmsg.createChannel]", err); return { ok: false }; } }, "btmsg.getChannelMessages": ({ channelId, limit }) => { try { return { messages: btmsgDb.getChannelMessages(channelId, limit ?? 100) }; } catch (err) { console.error("[btmsg.getChannelMessages]", err); return { messages: [] }; } }, "btmsg.sendChannelMessage": ({ channelId, fromAgent, content }) => { try { const messageId = btmsgDb.sendChannelMessage(channelId, fromAgent, content); return { ok: true, messageId }; } catch (err) { console.error("[btmsg.sendChannelMessage]", err); return { ok: false }; } }, "btmsg.heartbeat": ({ agentId }) => { try { btmsgDb.heartbeat(agentId); return { ok: true }; } catch (err) { console.error("[btmsg.heartbeat]", err); return { ok: false }; } }, "btmsg.getDeadLetters": ({ limit }) => { try { return { letters: btmsgDb.getDeadLetters(limit ?? 50) }; } catch (err) { console.error("[btmsg.getDeadLetters]", err); return { letters: [] }; } }, "btmsg.logAudit": ({ agentId, eventType, detail }) => { try { btmsgDb.logAudit(agentId, eventType, detail); return { ok: true }; } catch (err) { console.error("[btmsg.logAudit]", err); return { ok: false }; } }, "btmsg.getAuditLog": ({ limit }) => { try { return { entries: btmsgDb.getAuditLog(limit ?? 100) }; } catch (err) { console.error("[btmsg.getAuditLog]", err); return { entries: [] }; } }, // ── bttask handlers ─────────────────────────────────────────────── "bttask.listTasks": ({ groupId }) => { try { return { tasks: bttaskDb.listTasks(groupId) }; } catch (err) { console.error("[bttask.listTasks]", err); return { tasks: [] }; } }, "bttask.createTask": ({ title, description, priority, groupId, createdBy, assignedTo }) => { try { const taskId = bttaskDb.createTask(title, description, priority, groupId, createdBy, assignedTo); return { ok: true, taskId }; } catch (err) { const error = err instanceof Error ? err.message : String(err); console.error("[bttask.createTask]", err); return { ok: false, error }; } }, "bttask.updateTaskStatus": ({ taskId, status, expectedVersion }) => { try { const newVersion = bttaskDb.updateTaskStatus(taskId, status, expectedVersion); return { ok: true, newVersion }; } catch (err) { const error = err instanceof Error ? err.message : String(err); console.error("[bttask.updateTaskStatus]", err); return { ok: false, error }; } }, "bttask.deleteTask": ({ taskId }) => { try { bttaskDb.deleteTask(taskId); return { ok: true }; } catch (err) { console.error("[bttask.deleteTask]", err); return { ok: false }; } }, "bttask.addComment": ({ taskId, agentId, content }) => { try { const commentId = bttaskDb.addComment(taskId, agentId, content); return { ok: true, commentId }; } catch (err) { console.error("[bttask.addComment]", err); return { ok: false }; } }, "bttask.listComments": ({ taskId }) => { try { return { comments: bttaskDb.listComments(taskId) }; } catch (err) { console.error("[bttask.listComments]", err); return { comments: [] }; } }, "bttask.reviewQueueCount": ({ groupId }) => { try { return { count: bttaskDb.reviewQueueCount(groupId) }; } catch (err) { console.error("[bttask.reviewQueueCount]", err); return { count: 0 }; } }, // ── Search handlers ────────────────────────────────────────────────── "search.query": ({ query, limit }) => { try { const results = searchDb.searchAll(query, limit ?? 20); return { results }; } catch (err) { console.error("[search.query]", err); return { results: [] }; } }, "search.indexMessage": ({ sessionId, role, content }) => { try { searchDb.indexMessage(sessionId, role, content); return { ok: true }; } catch (err) { console.error("[search.indexMessage]", err); return { ok: false }; } }, "search.rebuild": () => { try { searchDb.rebuildIndex(); return { ok: true }; } catch (err) { console.error("[search.rebuild]", err); return { ok: false }; } }, // ── Plugin handlers ────────────────────────────────────────────────── "plugin.discover": () => { try { const plugins: Array<{ id: string; name: string; version: string; description: string; main: string; permissions: string[]; }> = []; if (!fs.existsSync(PLUGINS_DIR)) return { plugins }; const entries = fs.readdirSync(PLUGINS_DIR, { withFileTypes: true }); for (const entry of entries) { if (!entry.isDirectory()) continue; const manifestPath = join(PLUGINS_DIR, entry.name, "plugin.json"); if (!fs.existsSync(manifestPath)) continue; try { const raw = fs.readFileSync(manifestPath, "utf-8"); const manifest = JSON.parse(raw); plugins.push({ id: manifest.id ?? entry.name, name: manifest.name ?? entry.name, version: manifest.version ?? "0.0.0", description: manifest.description ?? "", main: manifest.main ?? "index.js", permissions: Array.isArray(manifest.permissions) ? manifest.permissions : [], }); } catch (parseErr) { console.error(`[plugin.discover] Bad manifest in ${entry.name}:`, parseErr); } } return { plugins }; } catch (err) { console.error("[plugin.discover]", err); return { plugins: [] }; } }, "plugin.readFile": ({ pluginId, filePath }) => { try { // Path traversal protection: resolve and verify within plugins dir const pluginDir = join(PLUGINS_DIR, pluginId); const resolved = path.resolve(pluginDir, filePath); if (!resolved.startsWith(pluginDir + path.sep) && resolved !== pluginDir) { return { ok: false, content: "", error: "Path traversal blocked" }; } if (!fs.existsSync(resolved)) { return { ok: false, content: "", error: "File not found" }; } const content = fs.readFileSync(resolved, "utf-8"); return { ok: true, content }; } catch (err) { const error = err instanceof Error ? err.message : String(err); console.error("[plugin.readFile]", err); return { ok: false, content: "", error }; } }, // ── Updater handlers ────────────────────────────────────────────────── "updater.check": async () => { try { const result = await checkForUpdates(APP_VERSION); return { ...result, error: undefined }; } catch (err) { const error = err instanceof Error ? err.message : String(err); console.error("[updater.check]", err); return { available: false, version: "", downloadUrl: "", releaseNotes: "", checkedAt: Date.now(), error, }; } }, "updater.getVersion": () => { return { version: APP_VERSION, lastCheck: getLastCheckTimestamp(), }; }, // ── Remote machine (relay) handlers ────────────────────────────────── "remote.connect": async ({ url, token, label }) => { try { const machineId = await relayClient.connect(url, token, label); return { ok: true, machineId }; } catch (err) { const error = err instanceof Error ? err.message : String(err); console.error("[remote.connect]", err); return { ok: false, error }; } }, "remote.disconnect": ({ machineId }) => { try { relayClient.disconnect(machineId); return { ok: true }; } catch (err) { const error = err instanceof Error ? err.message : String(err); console.error("[remote.disconnect]", err); return { ok: false, error }; } }, "remote.list": () => { try { return { machines: relayClient.listMachines() }; } catch (err) { console.error("[remote.list]", err); return { machines: [] }; } }, "remote.send": ({ machineId, command, payload }) => { try { relayClient.sendCommand(machineId, command, payload); return { ok: true }; } catch (err) { const error = err instanceof Error ? err.message : String(err); console.error("[remote.send]", err); return { ok: false, error }; } }, "remote.status": ({ machineId }) => { try { const info = relayClient.getStatus(machineId); if (!info) { return { status: "disconnected" as const, latencyMs: null, error: "Machine not found" }; } return { status: info.status, latencyMs: info.latencyMs }; } catch (err) { const error = err instanceof Error ? err.message : String(err); console.error("[remote.status]", err); return { status: "error" as const, latencyMs: null, error }; } }, // ── Telemetry handler ──────────────────────────────────────────────── "telemetry.log": ({ level, message, attributes }) => { try { telemetry.log(level, `[frontend] ${message}`, attributes ?? {}); return { ok: true }; } catch (err) { console.error("[telemetry.log]", err); return { ok: false }; } }, }, messages: {}, }, }); // ── Forward daemon events to WebView ──────────────────────────────────────── ptyClient.on("session_output", (msg) => { if (msg.type !== "session_output") return; try { rpc.send["pty.output"]({ sessionId: msg.session_id, data: msg.data }); } catch (err) { console.error("[pty.output] forward error:", err); } }); 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); } }); // ── Forward relay events to WebView ───────────────────────────────────────── relayClient.onEvent((machineId, event) => { try { rpc.send["remote.event"]({ machineId, eventType: event.type, sessionId: event.sessionId, payload: event.payload, }); } catch (err) { console.error("[remote.event] forward error:", err); } }); relayClient.onStatus((machineId, status, error) => { try { rpc.send["remote.statusChange"]({ machineId, status, error }); } catch (err) { console.error("[remote.statusChange] 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(); // Restore persisted window frame if available const savedX = Number(settingsDb.getSetting("win_x") ?? 100); const savedY = Number(settingsDb.getSetting("win_y") ?? 100); const savedWidth = Number(settingsDb.getSetting("win_width") ?? 1400); const savedHeight = Number(settingsDb.getSetting("win_height") ?? 900); const mainWindow = new BrowserWindow({ title: "Agent Orchestrator", titleBarStyle: "default", // "hidden" breaks clicks on WebKitGTK — testing with default url, rpc, frame: { width: isNaN(savedWidth) ? 1400 : savedWidth, height: isNaN(savedHeight) ? 900 : savedHeight, x: isNaN(savedX) ? 100 : savedX, y: isNaN(savedY) ? 100 : savedY, }, }); console.log("Agent Orchestrator (Electrobun) started!");