-
Notifications
You must be signed in to change notification settings - Fork 0
Renderer Bootstrap & App Composition
The renderer has exactly one mount point and one composition root. src/renderer/src/main.tsx owns the browser-level bootstrap: it imports the two global stylesheets, installs window-level error filters, and mounts <App /> into #root. src/renderer/src/App.tsx is the composition root: it fetches the three pieces of top-level state (app version, app settings, workspace projects), derives theme/accent/active-project values, and assembles the toolbar, transient overlays, the React Flow provider, the canvas, and the boot overlay. src/renderer/src/env.d.ts is the compile-time contract that tells the renderer what window.termsprawl looks like; it contains no runtime code and is satisfied in practice by the preload bridge (desktop) or the server shim (web).
Everything below assumes those three files as the ground truth; internals of Canvas, the stores, and the component kit are only described where App's imports reveal the call contract.
main.tsx mounts immediately — there is no async gate before React starts. App then renders its shell on the first pass and lets three fire-and-forget reads resolve in the background.
flowchart TD
M["main.tsx: createRoot(#root).render(App)"] --> A["App renders shell (toolbar + stage)"]
A --> E1["effect: appVersion() -> version"]
A --> E2["effect: settings.get() -> setSettings + applyTheme"]
A --> E3["effect: useProjects.load()"]
E3 --> G{"useProjects.loaded"}
G -- false --> P["placeholder .canvas div"]
G -- true --> RF["ReactFlowProvider -> Canvas(cwd, remote, invertWheelZoom)"]
E3 --> B["useBootOverlay(loaded)"]
E2 --> BH["useBrowserHome.setHomeUrl(settings.browserHomeUrl)"]
B --> O["boot-overlay + TesseractSpinner"]
Key nodes:
-
The three reads are independent.
appVersion(),settings.get(), andload()are issued from one effect keyed on the stableloadaction, and each updates a different piece of state. Nothing waits on anything else, so a slow workspace load cannot delay the theme or the version string. -
loadedgates the canvas, not the app. Until the workspace store reports loaded, App renders<div className="canvas" />— a same-sized placeholder — so layout does not jump when the real canvas arrives. -
The boot overlay is a sibling of the canvas, not a replacement.
useBootOverlay(loaded)drivesboot.visible/boot.leaving, and the overlay is rendered after the canvas block inside.canvas-stage. The canvas therefore mounts at the exact moment loading finishes and the tesseract cross-fades away on top of it; making the overlay a conditional alternative to the canvas would add startup time. -
The overlay is scoped to the canvas area.
.canvas-stageis the shared positioning context; the toolbar above it stays visible and interactive while booting.
App holds four pieces of local state — version, settings, error, settingsOpen — and reads loaded, load, activeProjectId, and projects from the workspace store. From those it derives activeCwd, activeRemote, and activeAccent, plus accent (via resolveAccent) and theme (via resolveTheme(settings?.theme ?? 'system')).
Two rules fall out of this shape:
-
App is a fan-out point for settings, not the owner of domain state.
settings.get()resolves once, then the same value is pushed into four different channels:applyTheme(...)mutates the document outside React so CSS variables take effect immediately;useBrowserHome.getState().setHomeUrl(...)copiesbrowserHomeUrlinto a store so canvas nodes can read it;settings?.invertWheelZoomand the theme/accent values are passed as props intoCanvasandTesseractSpinner; andonSettingsChange={setSettings}lets the settings panel write back so every consumer re-renders. The comment "canvas nodes can't take props" is the architectural rule to remember: anything a node needs globally goes into a store, anything the canvas shell needs goes in as a prop. -
Accent and theme resolve in different scopes. Theme is global (
AppSettings.theme); accent is per-project (projects.find(p => p.id === activeProjectId)?.settings?.accent).resolveAccentis the "never-purple guard": non-hex or legacy purple values resolve toundefined, the--accentinline CSS variable is omitted, and the stylesheet default applies. The resolved accent feeds both the shell's CSS variable andTesseractSpinner, so the boot spinner and the chrome never disagree. Changing accent plumbing means changing both consumers.
activeCwd / activeRemote are computed from the live project list (.find), so renaming, closing, or switching a project re-passes new props into Canvas on the next render without any extra subscription.
There are two independent layers, and they must stay independent:
-
main.tsxregisterswindowerrorandunhandledrejectionlisteners whose only job isevent.preventDefault()for benign ResizeObserver loop reports, matched throughisResizeObserverNoisefrom./ro-noise. This suppresses the DevTools console report. -
App.tsxregisters its ownerror/unhandledrejectionlisteners (empty deps, cleaned up on unmount) that seterror, rendered as a click-to-dismiss.error-banner.
The comments are explicit that preventDefault does not stop other listeners, so App's handler must re-apply the same predicate or the banner reappears on every node resize. Any new global error reporter (Sentry-like hook, telemetry) must call isResizeObserverNoise too, or it will treat the resize noise as a real failure.
The rendered tree is a .shell div carrying the optional --accent override, containing:
-
.toolbar— brand text plusHelpBadge,TabBar,OrganizeButton(disabled when there is noactiveProjectId),CogMenu(whose only job issetSettingsOpen(true)), and the version string. This is the place to add a new global action. -
Conditional panels —
AppSettingsPanelwhensettingsOpen, andOnboardingonly whensettingshas loaded andShouldShowOnboarding(settings, <count of non-closed, non-archived projects>)returns true. Dismissal writesonboardedAtthroughwindow.termsprawl.settings.setand feeds the result straight back intosetSettings. -
Transient overlays —
UpdateToastandAnnouncementBannerrender unconditionally and decide internally whether to show. -
.canvas-stage—ReactFlowProviderwrappingCanvaswhenloaded, otherwise the placeholder div; then the boot overlay.
ReactFlowProvider is the only app-level context provider, and it must stay inside .canvas-stage so the boot overlay still covers the canvas region. Canvas receives cwd, remote, and invertWheelZoom; everything else it needs comes from stores.
env.d.ts declares Window.termsprawl in a declare global block, importing all payload types from @shared/types, @shared/agent-tools, @shared/agent-status, @shared/update-status, and ../../core/chat/types so the renderer namespaces are structurally identical to the shared domain model rather than re-declared locally.
Properties worth knowing before editing:
-
agentToolsis optional (agentTools?), unlike the other namespaces. Consumers must feature-detect it; the hint is that it is only wired when the in-process tool server is active. -
runtime: { kind: 'desktop' | 'server' }is the discriminator the settings panel uses to hide desktop-only surfaces; the Server Edition shim reports'server'. Any new capability that cannot exist in the browser must be gated on this. -
The namespaces are the renderer-visible IPC inventory:
settings,updates,announcements,workspace,pty,diff,files,agent,contextLinks,git,cloud,github,browser,chat,links,relay, plus the standaloneappVersion,openExternal,runtimeInfo. Adding a channel means touching the preload bridge and this file in lockstep; this file alone produces no runtime behavior, so drift is silent until a call site compiles against the wrong shape. -
Subscription methods return unsubscribe functions (
onStatus,onEvent,onData,onExit,onFrame,onStatus,onAgentOpen), which is the pattern everyuseEffectin the renderer must follow.
-
Do not add
StrictMode. The comment inmain.tsxis a hard constraint: a dev double-mount would spawn two PTYs per terminal node. If StrictMode is ever reintroduced, terminal session creation must first become idempotent per node id. -
document.getElementById('root')!assumesindex.htmlprovides#root; that is the only DOM contract in the bootstrap. -
New toolbar control → add inside
.toolbar, mirrorOrganizeButton'sdisabled={!activeProjectId}convention when the action needs a project. -
New global overlay/toast → render alongside
UpdateToast/AnnouncementBanner, and keep it outside.canvas-stageunless it should be covered by the boot overlay. -
New per-node global fact → follow
useBrowserHome: land it in a store inApp's settings effect, because canvas nodes cannot receive props. -
New IPC capability → implement in preload, declare in
env.d.ts, then consume. Keep additions backwards-tolerant so the Server Edition shim does not have to match every optional surface at once.
Limitations: only main.tsx, App.tsx, and env.d.ts were available for this page. The internal behavior of Canvas, useProjects, useBootOverlay, ro-noise, and the imported UI components is described only to the extent their call sites in these three files reveal it.
Sources: src/renderer/src/main.tsx, src/renderer/src/App.tsx, src/renderer/src/env.d.ts
Generated from termsprawl at 0d4393be54c6200beedd91bb636e5296c30472c5.
App Shell & Platform Foundations
- Electron Main Process & Window Lifecycle
- Preload Bridge & IPC Contract
- Shared Domain Types and File/URL Helpers
- Renderer Bootstrap & App Composition
- Build Targets & TypeScript Configuration
Canvas, Nodes & Renderer State
- Infinite Canvas Surface & Viewport Interaction
- Workspace, Project & Tab State
- Node Links, Edges & Link Inspector
- Sticky, Group, Editor & Diff Nodes
- Keyboard Canvas Navigation & Cross-Panel Requests
- Theme, Accent & Visual Language
- Boot Overlay, Onboarding & Shared UI Kit
Terminals & Session Continuity
- PTY Lifecycle & Terminal Sessions
- tmux Session Naming & Reattach
- Scrollback Snapshots & Cold Replay
- Terminal Node Rendering (xterm.js)
- SSH Remote Projects, Terminals & Files
Persistence, Projects & Files
- Workspace Store & Project File Layout
- Project Scope, Deletion & Worktree Registry
- Workspace Bundle Export/Import
- File Service & File Tree UI
Agent Runtime & Tooling
- Agent Status Model & Hook Normalization
- Hook Server & CLI Hook Installers
- Agent Launch, CLI Probing & Managed Accounts
- Agent Tool Protocol & In-Process Server
- Agent Tool Client, CLI & MCP Entry
- Transcripts, Context Discovery & Context CLI
- Agent Canvas State & Status Badges
Chat Nodes & Model Providers
- Chat Runtime, Conversation & Cost
- Model Provider Adapters & Streaming
- Chat Tool Calling & Project Tools
- Chat Node UI
Git & Source Control
Embedded Browser Nodes
- Browser Manager & Guest Runtime
- CDP Facade & Browser Agent Server
- Browser Navigation Policy & Node UI
Server Edition
- Server Bootstrap & HTTP/WebSocket Entry
- RPC Dispatch, Handlers & Service Bridges
- Renderer Shim & Server Boundary
- Server Auth & Security Boundary
Relay & Remote Access
- Relay Hub & WebSocket Frame Routing
- Relay End-to-End Cryptography
- Relay Auth, Invites, Store & Admin API
- Relay Client, Pairing & Terminal Tunneling
- Relay Trust UI
Integrations & Secondary Surfaces
- Telegram Bot, Commands & Pairing
- A2A Peers: Protocol, Client & Server
- Node Link Engine, Registry & Scheduler
- Cloud Spaces, Snapshots & Sync
Settings, Updates & Maintenance