OpenWork Desktop Electron App Architecture: Main Process, Renderer, and Runtime Manager Explained
The OpenWork desktop application implements a two-process Electron architecture where the main Node.js process manages native OS integrations and spawns the OpenWork server as a child process, while the Chromium renderer hosts a React UI that communicates via secure IPC channels.
The OpenWork desktop client from the different-ai/openwork repository is a cross-platform Electron application that orchestrates a React-based user interface with an embedded Go backend. Understanding the architecture of the OpenWork desktop Electron app reveals how it handles workspace management, protocol deep-linking, and external automation through a strict separation of concerns between the main process, renderer process, and runtime manager.
Two-Process Electron Architecture Overview
OpenWork follows Electron’s standard multi-process model with distinct responsibilities:
- Main Process (
apps/desktop/electron/main.mjs): Runs Node.js with full OS access, creates theBrowserWindow, registers theopenwork://protocol, and manages child processes. - Renderer Process: A Vite-built React application (
@openwork/app) running in Chromium that renders the UI and communicates with the main process viaipcRenderer.
This separation ensures that UI code remains sandboxed while system-level operations—such as spawning the OpenWork server or handling auto-updates—execute in the privileged main process.
Main Process Native Integration (main.mjs)
The entry point at apps/desktop/electron/main.mjs bootstraps the entire application lifecycle.
Bootstrap and Protocol Registration
On startup, the main process sets the app identifier (APP_IDENTIFIER), configures the user-data path, and registers the custom openwork:// protocol for deep-linking. It initializes the main window via createMainWindow() and loads the renderer bundle:
import { app, BrowserWindow } from "electron";
import { createRuntimeManager } from "./runtime.mjs";
import { createWorkspaceStore } from "./workspace-store.mjs";
import { createUiControlServer } from "./ui-control-server.mjs";
app.whenReady().then(async () => {
const workspaceStore = createWorkspaceStore({ app });
const runtimeManager = createRuntimeManager({ app, workspaceStore });
const uiServer = createUiControlServer({ app });
const win = new BrowserWindow({
width: 1280,
height: 800,
webPreferences: { preload: path.join(__dirname, "preload.mjs") },
});
await win.loadURL(`file://${path.join(__dirname, "../../app/dist/index.html")}`);
});
Core Service Initialization
The main process imports and instantiates four critical utilities at the top of main.mjs:
createRuntimeManager(./runtime.mjs): Manages the lifecycle of the OpenWork server child process.createWorkspaceStore(./workspace-store.mjs): Persists workspace metadata and configuration.createUiControlServer(./ui-control-server.mjs): Exposes a local HTTP/WebSocket endpoint for external automation.createApplicationMenu(./app-menu.mjs): Builds native application menus.
Additionally, registerUpdaterIpc (./updater.mjs) wires the auto-update logic for Windows and macOS, while deep-link handlers listen for openwork:deep-link-native events to route URLs between the OS and the renderer.
Runtime Manager and Sidecar Binaries (runtime.mjs)
Located at apps/desktop/electron/runtime.mjs, the Runtime Manager isolates the OpenWork server—a Go-based HTTP service—from the UI layer.
Port Allocation and Process Spawning
When a user opens a workspace, runtimeManager.engineStart() resolves a free TCP port in the range 48000-51000 using findFreePort, then spawns the server binary:
export async function engineStart(workspacePath, opts = {}) {
const { port } = await resolveOpenworkPort("127.0.0.1", workspaceKey);
const env = await buildChildEnv({ OPENWORK_SERVER_PORT: `${port}` });
const child = spawn(opencodeBinaryPath, [
"serve",
"--workspace", workspacePath,
"--port", `${port}`
], { env });
engineState.child = child;
engineState.port = port;
engineState.baseUrl = `http://127.0.0.1:${port}`;
}
The manager tracks engine states (child, port, baseUrl) and exposes engineStop, engineRestart, and dispose routines for graceful shutdown when the app quits.
System CA and Docker Integration
The Runtime Manager handles enterprise TLS environments by exporting host root certificates to the child process via NODE_EXTRA_CA_CERTS using system-ca.mjs. It also detects Docker binaries and can launch server containers when configured, bridging the gap between the Electron host and containerized sidecars stored in apps/desktop/resources/sidecars/.
Workspace Persistence and Connect-Link Flow
Workspace Store (workspace-store.mjs)
The createWorkspaceStore utility persists data under Electron’s user-data directory, maintaining:
- Local workspace paths and remote Den URLs.
- Desktop bootstrap configuration (branding, activation state).
- The active/selected workspace ID.
It provides listLocalWorkspacePaths() to the Runtime Manager and APIs for Connect-Link acceptance (acceptConnectLink) and branding persistence (persistConnectLinkClaims).
Deep-Link Handling (connect-link.mjs)
The Connect-Link flow at apps/desktop/electron/connect-link.mjs handles openwork://connect?... URLs used by remote MCPs (Model Context Protocols). The main process verifies signatures against bundled public keys, resolves the URL into a Den-hosted connection, and caches brand icons to the user-data folder after validating size and aspect ratio. This ensures the UI immediately reflects new branding without reloading the renderer.
UI-Control Server for External Automation
The createUiControlServer function in apps/desktop/electron/ui-control-server.mjs starts a local HTTP and WebSocket server (default port 9223, overrideable via OPENWORK_ELECTRON_REMOTE_DEBUG_PORT). This server allows external agents—such as the opencode-chrome-devtools plugin—to drive the desktop UI through commands like creating workspaces or triggering model picks. It also forwards Chrome DevTools Protocol (CDP) commands to the embedded browser panel (browser-panel.mjs), enabling programmatic UI automation.
Renderer Process React Application
Vite Build and Entry Point
The renderer is a conventional React codebase built with Vite. The entry point is apps/app/src/app/app.tsx, which renders the full OpenWork UI including workspace lists, chat interfaces, and plugin panels. State management uses Zustand, while OpenWork skills load via the @openwork/ui and @openwork/types packages.
Secure IPC via Preload Script
Communication with the main process flows through apps/desktop/electron/preload.mjs, which exposes a safe API via contextBridge:
const { contextBridge, ipcRenderer } = require("electron");
contextBridge.exposeInMainWorld("electron", {
sendDeepLink: (url) => ipcRenderer.send("openwork:deep-link-native", [url]),
onDeepLink: (cb) => ipcRenderer.on("openwork:deep-link-native", (_, urls) => cb(urls)),
onNotification: (cb) => ipcRenderer.on("desktop-notification", (_, data) => cb(data)),
});
The renderer imports these utilities from apps/app/src/app/lib/electron-alpha.ts, ensuring that OS notifications, deep-link handling, and branding updates remain sandboxed yet functional.
Build Configuration and Distribution
Packaging is handled by Electron Builder using configuration files (electron-builder.yml, electron-builder.base.yml) that define platform-specific signing, icons, and sidecar inclusion. Sidecar binaries reside in apps/desktop/resources/sidecars/ and are referenced by the Runtime Manager during the engineStart sequence. The build pipeline is driven by pnpm scripts defined in apps/app/package.json, with workspace bootstrap constants stored in constants.json (including the OpenCode version for sidecar installation).
Summary
- Main Process (
main.mjs): Bootstraps Electron, registers theopenwork://protocol, manages the application lifecycle, and hosts core services including the Runtime Manager, Workspace Store, and UI-Control Server. - Runtime Manager (
runtime.mjs): Spawns the OpenWork Go server as a child process, handles port allocation (48000-51000), system CA certificates, and Docker integration. - Workspace Store (
workspace-store.mjs): Persists workspace metadata and Connect-Link claims to the user-data directory. - UI-Control Server (
ui-control-server.mjs): Exposes port 9223 for external HTTP/WebSocket automation and CDP forwarding. - Renderer Process: A Vite-built React app using Zustand state management, communicating with the main process via a secure
preload.mjsbridge. - Build Pipeline: Electron Builder packages the app with sidecar binaries from
resources/sidecars/, managed by pnpm scripts in theapps/appworkspace.
Frequently Asked Questions
How does the OpenWork Electron app handle deep-linking from external browsers?
The main process registers the openwork:// protocol during bootstrap. When the OS receives a deep-link, Electron emits an openwork:deep-link-native event in main.mjs, which parses the URL via connect-link.mjs and forwards it to the renderer through the preload.mjs bridge. The renderer then handles workspace selection or Connect-Link verification accordingly.
What is the purpose of the UI-Control Server on port 9223?
The UI-Control Server provides a local HTTP and WebSocket endpoint that enables external automation tools—such as browser extensions or AI agents—to programmatically control the OpenWork UI. It accepts commands to create workspaces, open files, and forward Chrome DevTools Protocol messages to the embedded browser panel, facilitating integration with tools like opencode-chrome-devtools.
How does the Runtime Manager ensure the OpenWork server starts reliably?
The Runtime Manager in runtime.mjs locates an available TCP port in the 48000-51000 range using findFreePort, constructs the appropriate environment variables (including NODE_EXTRA_CA_CERTS for enterprise TLS), and spawns the OpenCode binary as a child process. It maintains engine state to support graceful shutdowns, restarts, and port reassignment without requiring the user to manually configure network settings.
Where are workspace configurations and branding assets stored?
Workspace metadata, selected workspace IDs, and Connect-Link claims are persisted via the Workspace Store to Electron’s user-data directory. Branding assets—such as custom icons downloaded through Connect-Links—are validated for size and aspect ratio, then cached to the same user-data folder and applied to the taskbar, dock, or window frame by the main process.
Have a question about this repo?
These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →