Desktop Material

Developer Guide

Avoiding redundant root renders

Compare-form updates flow through the root AppStore, so an identical update is not free: without an equality gate it schedules another full renderer pass. Repository section navigation checks whether the History branch list is actually open before asking to close it, and AppStore._updateCompareForm rejects identical partial updates as the final boundary.

See No-op renderer update suppression for measured baseline timings, failure modes, and the verification contract.

Root renderer resource ownership

Long-lived work created by the root renderer must be released at the same lifecycle boundary. Store/updater/drag/IPC listeners belong in the root CompositeDisposable; polling timers retain explicit handles; document and window handlers are paired with unmount cleanup. Queued idle or animation-frame work checks the mounted state before starting more work.

See Root renderer resource lifecycle for behavior, failure modes, security boundaries, and verification evidence.

The renderer state path from React UI through Dispatcher and AppStore to a fresh state

This page is for contributors. It describes how Desktop Material is put together and how to build and run it. Desktop Material is a fork of desktop/desktop (MIT), so much of the underlying architecture is shared with GitHub Desktop; this guide highlights that foundation plus the pieces this fork adds.

The design contract is MATERIAL_REDESIGN.md at the repo root. It is the source of truth for the Material Design 3 shell — tokens, shape, motion, and the rules the redesign must uphold. Read it before changing anything in the shell, and treat it as the spec your changes are measured against.


Process model — Electron main + renderer

Desktop Material is an Electron app with the standard two-process split:

Supporting trees:


State flow — Store / Dispatcher / AppStore

The UI is a unidirectional data flow. Nothing in the UI mutates application state directly; it dispatches an intent, the store mutates, and the store emits a new immutable snapshot the UI re-renders from.

UI (React, app/src/ui/**)
  → Dispatcher (app/src/ui/dispatcher/dispatcher.ts)
    → AppStore._method(...)  (app/src/lib/stores/app-store.ts)
      → emitUpdate()
        → IAppState  ──►  UI re-renders
  1. UI components call methods on the Dispatcher in response to user actions. They never poke the store's internals.
  2. The Dispatcher (app/src/ui/dispatcher/dispatcher.ts) is the single funnel for intents. It validates/normalizes and forwards to the appropriate store method.
  3. AppStore (app/src/lib/stores/app-store.ts) holds the canonical state. Its internal _method(...) handlers perform the mutation (often after awaiting git or network work).
  4. When a mutation completes, the store calls emitUpdate().
  5. emitUpdate publishes a fresh IAppState snapshot; subscribed UI re-renders from it.

When you add a feature, the pattern is: add a Dispatcher method → add an AppStore._method that does the work and calls emitUpdate → extend IAppState with the new state → render it in the UI. Keep side effects in the store, keep the UI declarative.


Git plumbing — dugite

All Git operations go through dugite, the Git-over-child-process layer that ships a bundled Git and returns structured results. Wrappers live in app/src/lib/git/ (for example add.ts, apply.ts, authentication.ts, and one module per Git command). Higher layers — and the automation features — call these wrappers rather than shelling out ad hoc, which keeps error handling, environment setup, and credential plumbing consistent. New Git functionality should be a typed wrapper here, called from an AppStore method.


Per-account profile git repos

Two isolated account lanes connecting only to their matching repositories and remotes

Desktop Material stores each account's settings, tabs, and notifications as their own local git repositories under Electron's userData directory. This is what powers the fork's versioned settings:

Because these are real git repos, the audit trail and restore semantics come "for free" from Git rather than from a bespoke persistence format. When adding data that should be versioned per account, persist it into the relevant profile repo and commit through the same path.

The shared Git path has one deliberately narrow Windows launcher recovery: withTransientGitLaunchRetry may repeat only git rev-parse --verify HEAD, a hook-free read probe, after 75 ms and 250 ms. Do not broaden that allowlist to a mutating command: stderr text cannot prove that a hook or helper produced no side effects. Repository-indicator refreshes likewise contain failures per repository and always reschedule.

Appearance lives under userData/appearance-elements/<profile>/. Every profile owner, stable feature ID, repository element, and tab title owns an ordinary directory containing only its own .git and versioned setting.json. DedicatedSettingStore serializes writes/history mutations, uses crash-safe persistence, and makes undo/redo/restore append audit commits. Repository elements use a local desktop-material.appearance-id UUID so their separate workspace, toolbar, tabs, list-name, and logo repositories survive a path move. The old desktop-material.appearance value is accepted only as a migration seed/compatibility projection.


MCP / agent server

Desktop Material embeds an MCP server, with a local HTTP + CLI fallback, that lets an AI agent drive the app (accounts/repos/tabs, single or batch clone, status, commit, fetch/pull/push, branches, automation, and workflow dispatch). It binds 127.0.0.1 only, is token-gated and opt-in, and never exposes account tokens.

See Agent API for connection steps, command names, and the security model.


Feature subsystems

These features follow the same Store/Dispatcher rule rather than creating parallel state paths:

The current maintenance additions in this section are implemented. Their exact production, headless, source-publication, and cleanup evidence remains centralized in HANDOFF.md; historical gallery references do not substitute for those receipts.

The Guided Feature Gallery is the machine-checked documentation manifest for 86 user-facing Windows visual targets associated with these subsystems. Each function must own one distinct current PNG; missing, duplicate, and unassigned current assets fail the catalog contract, so the manifest cannot claim publication while a target is absent. Five retained Linux/Xvfb assets are explicitly historical and outside that target set. Keep captures free of personal paths, account identifiers, credentials, signed URLs, and unbounded provider payloads. A tracked image reference does not replace exact-source build, CI, public publication, release, or cleanup evidence.


Verification architecture

Responsive acceptance is catalog-driven. .codex/verification/responsive_surface_catalog.json enumerates every registered repository rail page, Preferences section, Repository Settings section, Clone tab, nested panel, and safe menu dialog together with its owning source and risk. Its viewport matrix covers the normal desktop, 640×480 minimum, narrow portrait, short landscape, wide desktop, 125% and 150% zoom, and 640×480 at 200% zoom.

.codex/verification/verify_responsive_surface_matrix_cdp.js exercises that catalog against the exact built renderer and a deterministic fixture. For every applicable surface it records requested and observed metrics, proves each vertical scroll owner can reach its bottom, and rejects document, root, or required-target horizontal overflow; clipped final controls; unreachable dialog forms/fieldsets/footers; and unnamed buttons. Safe audit wrappers still emit a complete ledger when one row fails, so a partial run cannot be mistaken for full coverage.

Feature-specific verifiers add state assertions that geometry alone cannot prove: verify_repository_logo_cdp.js edits layers and checks the generated tab/list SVG propagation and cleanup; verify_github_api_explorer_cdp.js executes the deterministic provider request and the full add/run/edit/remove function lifecycle; and the notification/navigation verifiers cover bulk state, error notices, context actions, and scroll endpoints. Run them on an off-screen Win32 desktop with an isolated profile and fixture, inspect promoted PNGs at original resolution, and retain the JSON ledger and cleanup receipts with the milestone.

Unit contracts mirror these boundaries: parsers and stores test malformed, oversized, credential-shaped, stale-binding, crash, link/junction, concurrent-resume, and cache-race cases, while style and catalog tests ensure the named scroll/container selectors cannot silently disappear. A screenshot is evidence of one accepted state, not a substitute for exact-source build, typed/unit checks, the catalog ledger, or resource cleanup.


Styling — SCSS token architecture

The Material Design 3 look is built from a layered SCSS system under app/styles/:

Rule of thumb: never hard-code a color — reference an M3 token so light/dark theming and future palette changes stay consistent. New component styling goes in a ui/ partial that consumes _material.scss tokens, matching whatever MATERIAL_REDESIGN.md specifies.


Build & run

Desktop Material uses Yarn and targets Node 24.15.0 (use a version manager such as nvm/ fnm/volta to pin it). Electron and the toolchain are pinned in package.json.

# 1. Use the right Node
node --version            # expect v24.15.0

# 2. Install dependencies
yarn

# 3. Run the app in development
yarn start

yarn start runs the development launcher (script/start.ts), which builds and boots the Electron app with hot-reload for the renderer. From there, standard fork tasks — lint, typecheck, and the packaging scripts — follow the same yarn <script> convention defined in package.json.

Codex/OpenCode build-fix runner

The Build & Run dispatcher owns the provider-neutral lifecycle and the main process owns separate typed IPC handlers for Codex and OpenCode. Codex detection spawns codex --version and codex login status with shell: false. Execution spawns the verified Codex stdin form with the root --ask-for-approval option before exec, workspace-write, an explicit on-request/never policy, --disable hooks, --ephemeral, --ignore-user-config, --ignore-rules, and --color never. The repository is the child cwd; neither its path nor the natural-language prompt is put in argv. A validated nested profile directory is bounded and supplied in stdin context only. Codex CLI 0.144 has no verified blanket MCP-disable override, so trusted project .codex/config.toml remains part of the repository trust boundary; do not claim project MCP isolation.

Keep prompt, line, dialog, and store bounds intact. Cancellation must continue through the shared process-tree teardown. An operation must remain owned by its exact renderer WebContents: reject duplicate IDs, scope cancellation to the owner, and abort-and-await on navigation or destruction. New providers must not bypass the dispatcher's startBuildRun(repository) verification rerun unless the owning renderer operation was cancelled; Stop must fence that rerun. Focused tests live under app/test/unit/lib/build-run, app/test/unit/main-process/build-run, and app/test/unit/ui; the typed IPC inventory is enforced by app/test/unit/ipc-contract-test.ts.


See also: Agent API · Automation · User Guide