Files
openwork/apps/desktop/electron/ui-control-server.mjs
T

190 lines
7.7 KiB
JavaScript

// Local UI-control HTTP bridge: a loopback server exposing the legacy
// /snapshot, /actions and /execute routes plus the semantic /context, /query
// and /command surface. Dispatched to the renderer's window.__openworkControl.
// Extracted from main.mjs; state and lifecycle live in this factory
// (createRuntimeManager pattern).
import { randomBytes } from "node:crypto";
import { createServer } from "node:http";
import { rm, writeFile } from "node:fs/promises";
import path from "node:path";
export function createUiControlServer({ app, appName, appIdentifier, getWindow }) {
let uiControlServer = null;
let uiControlDiscoveryPath = null;
const uiControlToken = randomBytes(32).toString("hex");
function sendJsonResponse(response, statusCode, payload) {
response.writeHead(statusCode, {
"Content-Type": "application/json; charset=utf-8",
"Cache-Control": "no-store",
});
response.end(JSON.stringify(payload));
}
function readJsonRequestBody(request) {
return new Promise((resolve, reject) => {
let raw = "";
request.setEncoding("utf8");
request.on("data", (chunk) => {
raw += chunk;
if (raw.length > 128_000) {
reject(new Error("Request body too large"));
request.destroy();
}
});
request.on("end", () => {
if (!raw.trim()) {
resolve({});
return;
}
try {
resolve(JSON.parse(raw));
} catch {
reject(new Error("Request body must be JSON"));
}
});
request.on("error", reject);
});
}
function authorizedUiControlRequest(request) {
const auth = request.headers.authorization ?? "";
return auth === `Bearer ${uiControlToken}`;
}
function jsonForJavaScript(value) {
return JSON.stringify(JSON.stringify(value ?? {}));
}
async function evaluateOpenworkControl(expression) {
const win = await getWindow();
// Commands mutate renderer state directly and do not require the desktop
// window to become active. Foreground activation must be an explicit
// affordance, never an implicit side effect of remote control.
return win.webContents.executeJavaScript(expression, true);
}
async function runOpenworkControlCommand(command, args = {}) {
const argsJsonLiteral = jsonForJavaScript(args);
if (command === "snapshot") {
return evaluateOpenworkControl(`(async () => {
const control = window.__openworkControl;
if (!control) return { ok: false, error: "OpenWork control surface is not available yet." };
control.setEnabled?.(true);
return { ok: true, ...control.snapshot() };
})()`);
}
if (command === "actions") {
return evaluateOpenworkControl(`(async () => {
const control = window.__openworkControl;
if (!control) return { ok: false, error: "OpenWork control surface is not available yet." };
control.setEnabled?.(true);
return { ok: true, actions: control.listActions() };
})()`);
}
if (command === "context") {
return evaluateOpenworkControl(`(async () => {
const control = window.__openworkControl;
if (!control) return { ok: false, error: "OpenWork control surface is not available yet." };
return { ok: true, context: control.context() };
})()`);
}
if (command === "query" || command === "command") {
return evaluateOpenworkControl(`(async () => {
const control = window.__openworkControl;
const input = JSON.parse(${argsJsonLiteral});
if (!control) return { ok: false, error: "OpenWork control surface is not available yet." };
if (!input || typeof input.id !== "string" || !input.id.trim()) {
return { ok: false, error: "Missing OpenWork affordance id." };
}
return control[${JSON.stringify(command)}](input);
})()`);
}
if (command === "execute") {
return evaluateOpenworkControl(`(async () => {
const control = window.__openworkControl;
const input = JSON.parse(${argsJsonLiteral});
if (!control) return { ok: false, error: "OpenWork control surface is not available yet." };
if (!input || typeof input.actionId !== "string" || !input.actionId.trim()) {
return { ok: false, error: "Missing OpenWork actionId." };
}
control.setEnabled?.(true);
return control.execute(input.actionId, input.args ?? {});
})()`);
}
return { ok: false, error: `Unknown OpenWork control command: ${command}` };
}
async function start() {
if (uiControlServer) return;
uiControlServer = createServer(async (request, response) => {
try {
const url = new URL(request.url ?? "/", "http://127.0.0.1");
if (request.method === "GET" && url.pathname === "/health") {
sendJsonResponse(response, 200, { ok: true, app: appName, version: 2 });
return;
}
if (!authorizedUiControlRequest(request)) {
sendJsonResponse(response, 401, { ok: false, error: "Unauthorized" });
return;
}
if (request.method === "GET" && url.pathname === "/snapshot") {
sendJsonResponse(response, 200, await runOpenworkControlCommand("snapshot"));
return;
}
if (request.method === "GET" && url.pathname === "/actions") {
sendJsonResponse(response, 200, await runOpenworkControlCommand("actions"));
return;
}
if (request.method === "GET" && url.pathname === "/context") {
sendJsonResponse(response, 200, await runOpenworkControlCommand("context"));
return;
}
if (request.method === "POST" && url.pathname === "/query") {
sendJsonResponse(response, 200, await runOpenworkControlCommand("query", await readJsonRequestBody(request)));
return;
}
if (request.method === "POST" && url.pathname === "/command") {
sendJsonResponse(response, 200, await runOpenworkControlCommand("command", await readJsonRequestBody(request)));
return;
}
if (request.method === "POST" && url.pathname === "/execute") {
sendJsonResponse(response, 200, await runOpenworkControlCommand("execute", await readJsonRequestBody(request)));
return;
}
sendJsonResponse(response, 404, { ok: false, error: "Not found" });
} catch (error) {
console.error("[ui-control] request failed", error);
sendJsonResponse(response, 500, { ok: false, error: "OpenWork UI control request failed." });
}
});
await new Promise((resolve, reject) => {
uiControlServer.once("error", reject);
uiControlServer.listen(0, "127.0.0.1", () => resolve(undefined));
});
const address = uiControlServer.address();
const port = typeof address === "object" && address ? address.port : null;
if (!port) throw new Error("Could not start OpenWork UI control bridge.");
uiControlDiscoveryPath = path.join(app.getPath("userData"), "openwork-ui-control.json");
await writeFile(
uiControlDiscoveryPath,
`${JSON.stringify({ version: 2, app: appName, identifier: appIdentifier, platform: process.platform, baseUrl: `http://127.0.0.1:${port}`, token: uiControlToken }, null, 2)}\n`,
"utf8",
);
// Make the discovery path available to child processes (server → managed OpenCode → plugin).
process.env.OPENWORK_UI_CONTROL_DISCOVERY = uiControlDiscoveryPath;
}
async function stop() {
if (uiControlDiscoveryPath) {
await rm(uiControlDiscoveryPath, { force: true }).catch(() => undefined);
uiControlDiscoveryPath = null;
}
if (!uiControlServer) return;
await new Promise((resolve) => uiControlServer.close(() => resolve(undefined)));
uiControlServer = null;
}
return { start, stop };
}