agent-orchestrator/ui-electrobun/src/bun/index.ts
Hibryda 88206205fe feat(electrobun): multi-machine relay + OTEL telemetry
Multi-machine relay:
- relay-client.ts: WebSocket client for agor-relay with token auth,
  exponential backoff (1s-30s), TCP probe, heartbeat (15s ping)
- machines-store.svelte.ts: remote machine state tracking
- RemoteMachinesSettings.svelte: machine list, add/connect/disconnect UI
- 7 RPC types (remote.connect/disconnect/list/send/status + events)

Telemetry:
- telemetry.ts: OTEL spans + OTLP/HTTP export to Tempo,
  controlled by AGOR_OTLP_ENDPOINT env var
- telemetry-bridge.ts: tel.info/warn/error frontend convenience API
- telemetry.log RPC for frontend→Bun tracing
2026-03-22 01:46:03 +01:00

1075 lines
36 KiB
TypeScript

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<boolean> {
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<PtyRPCSchema>({
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<string> {
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!");