Desktop Internals
Use this page when you are changing the Electron shell, renderer-to-main contracts, BrowserView behavior, or the embedded runtime launch path.
Desktop execution model
npm run desktop:dev is a four-process loop:
vitefor the renderertsup --watchfor Electron main and preload outputnode scripts/watch-runtime-bundle.mjsto restage the embedded runtime whenruntime/changeselectronmon out/dist-electron/main.cjsto run the app and restart it when the built Electron entrypoint changes
That means desktop work is usually spread across four boundaries: React renderer code, preload bridge code, Electron main-process code, and the staged runtime bundle under desktop/out/runtime-<platform>.
Main code seams
desktop/src/components/layout/AppShell.tsx: the shell composition and top-level product routing. This is where the top-toolbar shell navigation, agent pane, browser panes, file explorer, applications explorer, app surfaces, operations drawer, task-proposal toasts, settings overlays, and reported non-browser operator surfaces are coordinated.desktop/src/components/layout/TopTabsBar.tsx: the shared desktop toolbar, including workspace switching and the shell-levelSpace,Automations, andMarketplaceentry points.desktop/src/components/onboarding/FirstWorkspacePane.tsx,ConfigureStep.tsx,BrowserProfileStep.tsx, andCreatingView.tsx: first-workspace flow, including empty/template workspace creation and browser-profile bootstrap choices.desktop/shared/model-catalog.ts: shipped fallback model metadata for direct providers and Holaboss Proxy model mapping, including reasoning support, allowed thinking values, and input modalities.desktop/src/lib/workspaceDesktop.tsxanddesktop/src/lib/workspaceSelection.tsx: renderer-side workspace state and shell coordination.desktop/src/components/auth/AuthPanel.tsx: provider settings, catalog-backed defaults, background-task selection, recall embeddings, and image-generation configuration.desktop/src/components/panes/ChatPane.tsx: model picker, per-model reasoning-effort selector, queued chat inputs, and stream/timeline rendering.desktop/src/components/panes/BrowserPane.tsx,SpaceBrowserExplorerPane.tsx, andSpaceBrowserDisplayPane.tsx: the browser-space UI for theuserandagentbrowser surfaces.desktop/src/components/panes/FileExplorerPane.tsx: workspace file explorer, previews, editing, bookmarking, drag-and-drop imports, and file-watch behavior.desktop/src/components/panes/SpaceApplicationsExplorerPane.tsx: workspace app list for the space explorer, parallel to files and browser, including theAdd appaction that routes into the Marketplace apps tab.desktop/src/components/layout/NotificationToastStack.tsx: runtime-backed notification rendering and activation behavior.desktop/electron/preload.ts: the renderer-to-main bridge exposed onwindow.electronAPI.desktop/src/types/electron.d.ts: the typed source of truth for the preload bridge. If the renderer can call it, it should be declared here.desktop/electron/main.ts: the main-process implementation for IPC, BrowserView orchestration, embedded runtime startup, workspace actions, file system actions, and browser state.
Embedded runtime lifecycle
The desktop does not talk to the runtime through a vague helper. desktop/electron/main.ts owns the launch path:
- Resolve the staged runtime bundle under
desktop/out/runtime-<platform>or an override such asHOLABOSS_RUNTIME_ROOT. - Validate that the bundle includes the executable, packaged Node runtime, packaged Python runtime,
package-metadata.json, andruntime/api-server/dist/index.mjs. - Spawn
bin/sandbox-runtimewith desktop-owned env such asHB_SANDBOX_ROOT,SANDBOX_AGENT_BIND_HOST=127.0.0.1,SANDBOX_AGENT_BIND_PORT=5160,SANDBOX_AGENT_HARNESS,HOLABOSS_RUNTIME_DB_PATH, and the bridge settings. - Wait for the embedded runtime health check to pass.
- Stream stdout and stderr into
runtime.logunder the ElectronuserDatadirectory. - On app quit, block final Electron exit until desktop-owned cleanup tears down the embedded runtime and browser service.
If you are debugging runtime startup from desktop, inspect runtime.log and the sandbox-host directory under Electron userData before you change UI code.
Representative launch shape from desktop/electron/main.ts:
const child = spawn(launchSpec.command, launchSpec.args, {
cwd: runtimeRoot,
env: {
...process.env,
HB_SANDBOX_ROOT: sandboxRoot,
SANDBOX_AGENT_BIND_HOST: "127.0.0.1",
SANDBOX_AGENT_BIND_PORT: String(RUNTIME_API_PORT),
HOLABOSS_EMBEDDED_RUNTIME: "1",
SANDBOX_AGENT_HARNESS: harness,
HOLABOSS_RUNTIME_DB_PATH: runtimeDatabasePath(),
PROACTIVE_ENABLE_REMOTE_BRIDGE: "1",
HOLABOSS_AUTH_COOKIE: authCookieHeader() ?? "",
},
});HOLABOSS_RUNTIME_ROOT can override the staged bundle location when you need the desktop to boot a different packaged runtime tree.
Renderer-to-main contract
The desktop renderer does not talk to Electron internals directly. It goes through namespaced bridge contracts exposed by electronAPI.
Important namespaces include:
fs: directory listing, previews, writes, file watches, and bookmarksbrowser: browser workspace selection, tab state, navigation, history, downloads, suggestions, and bounds syncingworkspace: workspace lifecycle, sessions, apps, outputs, cronjobs, notifications, memory proposals, integrations, packaging flows, and reported operator-surface contextruntime: runtime status, runtime config, profile, binding exchange, and restart flowsauth: desktop sign-in and runtime binding exchangebilling: subscription and usage surfacesdiagnostics: local runtime and environment inspection helpersappUpdate: desktop update state and install flowappSurface: embedded app-surface navigation and bounds controlui: theme, settings routing, and external-link helpersworkbench: browser-opening handoff from workbench surfaces into the main shell
If you change the desktop contract, update all three layers together:
desktop/electron/preload.tsdesktop/src/types/electron.d.ts- the
ipcMainhandler indesktop/electron/main.ts, usually wired throughhandleTrustedIpc(...)
Minimal bridge example:
// preload
runtime: {
getConfig: () => ipcRenderer.invoke("runtime:getConfig"),
setConfig: (payload) => ipcRenderer.invoke("runtime:setConfig", payload),
},
workspace: {
queueSessionInput: (payload) =>
ipcRenderer.invoke("workspace:queueSessionInput", payload),
}// main
handleTrustedIpc("runtime:getConfig", ["main", "auth-popup"], () =>
getRuntimeConfig(),
);
handleTrustedIpc(
"runtime:setConfig",
["main", "auth-popup"],
async (_event, payload) => {
const currentConfig = await readRuntimeConfigFile();
const nextConfig = await writeRuntimeConfigFile(payload);
await restartEmbeddedRuntimeIfNeeded(currentConfig, nextConfig);
return getRuntimeConfig();
},
);
handleTrustedIpc("workspace:queueSessionInput", ["main"], async (_event, payload) =>
queueSessionInput(payload),
);Workspace bootstrap flow
Workspace bootstrap now has two phases:
workspace:createWorkspacein Electron main materializes template/empty content, writes workspace files, and activates the runtime record.- renderer-side workspace bootstrap in
workspaceDesktop.tsxcan then copy browser profile state from another workspace or import browser data from Chrome/Edge/Arc/Safari.
Representative post-create browser bootstrap calls from the renderer:
await window.electronAPI.workspace.copyBrowserWorkspaceProfile({
sourceWorkspaceId,
targetWorkspaceId: createdWorkspaceId,
});
await window.electronAPI.workspace.importBrowserProfile({
workspaceId: createdWorkspaceId,
source: browserImportSource,
profileDir: browserImportProfileDir.trim() || undefined,
});createWorkspace() in desktop/electron/main.ts now also emits structured stage logs with the [holaboss.createWorkspace] prefix for debugging create failures across materialization, runtime record creation, template apply, and workspace activation.
Browser protocol
The browser system is not just a webview dropped into React. The current path uses BrowserView orchestration in the main process and synchronizes the visible viewport from the renderer using browser.setBounds.
Important behavior to understand:
- browser state is workspace-aware
- browser spaces are explicit:
userandagent - the renderer activates tabs and navigation through
electronAPI.browser - the main process owns actual BrowserView attachment, persistence, downloads, history, popup windows, and the desktop browser service
The desktop browser service is now more than a browser-tool bridge. In addition to page and tab routes, it exposes /api/v1/browser/operator-surface-context, which lets the embedded runtime load the current active operator surfaces for the workspace. That payload combines browser-owned surfaces with the non-browser surfaces AppShell reports through workspace.setOperatorSurfaceContext(...).
This split is why browser behavior belongs to desktop internals, not to generic UI code alone.
The non-browser half of that context comes from the renderer:
await window.electronAPI.workspace.setOperatorSurfaceContext(
workspaceId,
reportedOperatorSurfaceContext,
);
// runtime later reads GET /api/v1/browser/operator-surface-contextModel catalog and reasoning controls
The desktop model path is now catalog-driven rather than a flat model string list.
desktop/electron/main.tsrefreshes a managed runtime model catalog from the control plane when a signed-in Holaboss session is active, caches that catalog, and merges it with the locally persistedruntime-config.json.- The merged runtime-config snapshot exposes
providerModelGroups,catalogVersion, and managed defaults for background tasks, embeddings, and image generation. AuthPanel.tsxuses that snapshot to show the actual configured provider/model surface instead of a blind text field.ChatPane.tsxuses the same provider groups plus the local fallback catalog to decide whether the selected model supports reasoning and whichthinking_valuechoices to show.- The selected
thinking_valueis a chat-composer preference, not aruntime-config.jsonfield. On submit the renderer queues it with the session input so the runtime can apply it per run.
Representative catalog metadata and queue handoff:
const entry = {
model_id: "gpt-5.4",
reasoning: true,
thinking_values: ["none", "low", "medium", "high", "xhigh"],
default_thinking_value: "medium",
input_modalities: ["text", "image"],
};const selectedModelSupportsReasoning = selectedConfiguredModel
? selectedConfiguredModel.reasoning === true
: Boolean(selectedFallbackModelMetadata?.reasoning);
await window.electronAPI.workspace.queueSessionInput({
model: resolvedChatModel || null,
thinking_value: effectiveThinkingValue,
});Adding or modifying shipped catalog entries
Use desktop/shared/model-catalog.ts when you need the desktop to know more than a raw model id:
- display label
- whether the model supports reasoning
- which
thinking_valueoptions the composer should show - default thinking value
- supported input modalities
Do not edit the shipped catalog just to connect a provider or type a custom model into runtime-config.json. Configured models can still appear through providerModelGroups; the catalog is the fallback metadata layer that makes those models feel first-class in the desktop UI.
Representative local catalog entry:
export const PROVIDER_MODEL_CATALOG = {
openrouter_direct: {
source: "local",
models: [
{
model_id: "qwen/qwen3.6-plus",
label: "Qwen 3.6 Plus",
reasoning: true,
thinking_values: ["minimal", "low", "medium", "high"],
default_thinking_value: "medium",
input_modalities: ["text", "image"],
},
],
},
};When AuthPanel.tsx writes configured models back into runtime-config.json, it projects the catalog metadata onto that model entry:
nextModels[token] = {
provider: providerId,
model: modelId,
...(modelCatalog.catalogConfigShapeForProviderModel(
providerId,
modelId,
) ?? {}),
};If a Holaboss-managed proxy model should inherit local fallback metadata, also update the proxy mapping rules in mappedHolabossProxyProviderModel(...):
if (/^gpt-5(?:[.-]|$)/i.test(normalizedModelId)) {
return { providerId: "openai_direct", modelId: normalizedModelId };
}
if (/^claude-/i.test(normalizedModelId)) {
return { providerId: "anthropic_direct", modelId: normalizedModelId };
}That is how a managed proxy model like gpt-5.4 or claude-sonnet-4-6 can still light up the desktop reasoning selector even if the control plane catalog did not provide explicit thinking_values.
Composer skill entry points
Skill usage is now composer-first rather than a standalone pane:
- slash commands in the composer (
/skill_id) - quoted skill chips attached to the pending message
Use Skillspicker inside composer actions
ChatPane.tsx serializes selected skills into a leading slash block before queueing:
const serializedPrompt = serializeQuotedSkillPrompt(trimmed, quotedSkillIds);
await window.electronAPI.workspace.queueSessionInput({
text: serializedPrompt,
// other fields omitted
});If you change skill discoverability or prompt shaping, inspect both the desktop serialization path and the harness-host prompt-expansion path.
File explorer contract
The file explorer goes through the fs:* IPC namespace rather than reading files directly from the renderer.
The current contract includes:
fs:listDirectoryfs:readFilePreviewfs:writeTextFilefs:writeTableFilefs:watchFilefs:createPathfs:renamePathfs:movePathfs:deletePath- bookmark and file-change events
That keeps file access centralized in the main process and makes workspace-relative behavior auditable.
The explorer is also no longer files-only. In space mode the left explorer rail now has three modes:
filesfor workspace filesystem inspection and editingbrowserfor user and agent browser surfacesapplicationsfor installed workspace apps
App outputs that carry app-resource presentation metadata now route into that applications explorer path and open through AppSurfacePane rather than resolving into the browser first.
Notification and runtime-backed product state
Desktop notifications are runtime-backed, not purely renderer-local.
The current path is:
- runtime persists notification records in
runtime/state-store - runtime exposes them through
/api/v1/notifications - Electron routes that through
workspace:listNotificationsandworkspace:updateNotification AppShellpolls and hydrates the toast stackNotificationToastStackrenders activation and dismissal behavior
So if you are changing notification behavior, inspect both the desktop shell and the runtime notification model.
Task-proposal toasts are the current exception. The proposals themselves remain runtime-backed and are polled from workspace:listTaskProposals, but the "new proposal ready" toast is synthesized in AppShell by diffing newly seen pending proposal ids per workspace and feeding that local toast record into the same stack. Activating that toast opens the inbox for the corresponding workspace instead of updating runtime notification state.
Display-surface model
The shell maintains a central display surface that can project:
- browser content
- app content, including app-resource deep links from outputs and artifacts
- internal surfaces
That routing currently lives in AppShell.tsx through spaceDisplayView. If you are adding a new display mode, start there and trace the corresponding pane/component path.
Verification after desktop changes
npm run desktop:typecheckis the minimum validation for every desktop change.- Run
npm run desktop:e2ewhen the change crosses renderer, preload, main-process, or embedded-runtime boundaries. - Use
desktop/electron/browser-operator-surface-context.test.mjsanddesktop/electron/runtime-quit-cleanup.test.mjsas the fastest regression checks when you are touching browser-service context or app-quit cleanup. - Use
bash desktop/scripts/check-runtime-status.shwhen the embedded runtime fails to start, a workspace looks corrupted, or app lifecycle behavior diverges from the desktop UI. - Preserve the renderer/main boundary. Do not move file system access, BrowserView ownership, or runtime process management back into React state.