All settings wired to SQLite persistence: - AgentSettings: shell, CWD, permissions, providers (JSON blob) - SecuritySettings: branch policies (JSON array) - ProjectSettings: per-project via setProject RPC - OrchestrationSettings: wake, anchors, notifications - AdvancedSettings: logging, OTLP, plugins, import/export JSON Theme Editor: - 26 color pickers (14 Accents + 12 Neutrals) - Live CSS var preview as you pick colors - Save custom theme to SQLite, cancel reverts - Import/export theme as JSON - Custom themes in dropdown with delete button Extensions Marketplace: - 8-plugin demo catalog (Browse/Installed tabs) - Search/filter by name or tag - Install/uninstall with SQLite persistence - Plugin cards with emoji icons, tags, version Terminal font hot-swap: - fontStore.onTermFontChange() → xterm.js options update + fitAddon.fit() - Resize notification to PTY daemon after font change All 7 settings categories functional. Every control persists and takes effect.
252 lines
8.4 KiB
TypeScript
252 lines
8.4 KiB
TypeScript
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<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;
|
|
}
|
|
|
|
// ── 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<PtyRPCSchema>({
|
|
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<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();
|
|
|
|
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!");
|