-
Notifications
You must be signed in to change notification settings - Fork 0
Developer Guide
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.mdat 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.
Desktop Material is an Electron app with the standard two-process split:
-
Main process (
app/src/main-process/) — owns the app lifecycle, native windows and menus, IPC, and privileged operations. It is the only side allowed to touch the OS directly. -
Renderer process (
app/src/ui/) — the React UI that draws the workspace. It talks to the main process over IPC and never performs privileged work itself.
Supporting trees:
-
app/src/lib/— shared, process-agnostic logic (git, stores, models helpers). -
app/src/models/— plain data models shared across both processes. -
app/src/cli/— the command-line entry points.
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
- UI components call methods on the Dispatcher in response to user actions. They never poke the store's internals.
- The Dispatcher (
app/src/ui/dispatcher/dispatcher.ts) is the single funnel for intents. It validates/normalizes and forwards to the appropriate store method. -
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). - When a mutation completes, the store calls
emitUpdate(). -
emitUpdatepublishes a freshIAppStatesnapshot; 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.
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.
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:
- Every settings or tab change auto-commits to that account's profile repo.
- The history manager (Settings → History) is
git logover that repo — undo/redo walk the commits, and restore checks out an earlier state (seesettings-history-manager.png). - The notification centre is backed by its own repo in the same way.
- The strict
appearance-customization-v1value is allowlisted byapp/src/lib/profiles/profile-settings-registry.ts, captured in the active profile'ssettings.json, and described as an appearance-customization change in Git history. - Per-tab title/background styling is written with the tab to
tabs.json; the bounded recent-color list is another allowlisted profile setting. - Optional
isPinnedandopenedAtvalues share that serialized tab model. Missing legacy values keep migration-safe defaults and profile serialization preserves unknown newer fields. Close and arrange mutations must useRepositoryTabsStoreso they remain ordered on the same profile queue and isolated by account/window scope.
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.
Repository appearance overrides are deliberately outside that profile repository. The six
allowlisted fields — accent palette, surface palette, toolbar labels, toolbar density, tab density,
and tab width — are serialized under desktop-material.appearance in the selected repository's
local .git/config. Missing fields inherit the active-profile defaults. Never move this value into
a tracked repository file or treat per-tab background color as a repository override.
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.
-
app/src/main-process/agent-server/owns the loopback server, MCP/REST parsing, token lifecycle, request limits, and command queue. -
app/src/lib/agent-commands.tsis the versioned command/schema source of truth shared by both Electron processes. -
app/src/lib/agent-command-executor.tsresolves repository targets and sends allowed operations through the same Dispatcher/AppStore paths used by the UI. -
app/src/ui/preferences/agent-access.tsxcontrols opt-in lifecycle and token rotation. -
script/agent/mcp-stdio-proxy.jsandscript/agent/desktop-agent.jsare the shipped stdio and CLI clients. They read the app's restricted connection file instead of embedding a port or token.
See Agent API for connection steps, command names, and the security model.
These features follow the same Store/Dispatcher rule rather than creating parallel state paths:
The current maintenance additions in this section are implemented but remain subject to their integrated production/headless/publication gate. Historical gallery references do not imply that new acceptance has already completed.
The Guided Feature Gallery is the machine-checked documentation manifest for 55 synthetic, user-facing visual functions and states associated with these subsystems. Each function owns one distinct tracked PNG; missing, duplicate, and unassigned assets fail the catalog contract. 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.
-
Accounts, organizations, and providers — account state and organization loading live in
app/src/lib/stores/accounts-store.ts; provider credentials are modelled in the account/auth layer;app/src/ui/clone-repository/merges personal and organization repositories and hosts the GitLab/Bitbucket browser; publish ownership is selected inapp/src/ui/publish-repository/.app/src/lib/github-oauth-scopes.tsis the reviewed GitHub browser-authorization allowlist; keep feature scope additions explicit and never infer destructive/admin families. -
Appearance and adaptive Material shell —
app/src/models/appearance-customization.tsowns the strict versioned model for the 12 profile defaults: accent palette, surface palette, elevation, interface font, monospace font, motion, toolbar labels, toolbar density, repository-list density, tab density, tab width, and tab close buttons.app/src/lib/appearance-customization.tsresolves the six repository-local overrides;app/src/ui/app-theme.tsxapplies only normalized data attributes and tokens; Preferences and Repository Settings expose the two scopes.app/src/ui/toolbar/toolbar-overflow-layout.tskeeps the width/priority calculation pure whiletoolbar.tsxowns ResizeObserver, More-surface focus, and restoration. The first-run React surface lives inapp/src/ui/welcome/and retains the existing sign-in/configure-Git state machine beneath the Material presentation. -
Repository tab actions —
app/src/lib/stores/repository-tabs-store.tsowns pinned protection, literal inverse-close matching, pin-constrained moves, and stable one-shot sorts.app/src/ui/repository-tabs/close-tabs-containing-popover.tsxkeeps the original regex close and inverse close behind review/count/preview semantics;app/src/ui/repository-tabs/arrange-tabs-popover.tsxowns drag, labelled keyboard moves, pin changes, live announcements, and focus return. Never let an empty or zero-match inverse query become close-all, move across a pin boundary implicitly, or continuously sort on status updates. -
Automation — typed settings and safety predicates live in
app/src/lib/automation/, the scheduler isapp/src/lib/stores/helpers/automation-scheduler.ts, global/account controls are inapp/src/ui/preferences/automation.tsx, repository overrides are inapp/src/ui/repository-settings/automation-overrides.tsx, and merge-all/pull-all surfaces live inapp/src/ui/merge-all/andapp/src/ui/pull-all/. -
GitHub Actions and logs —
app/src/lib/stores/actions-store.tsowns API state; the run list, run details, workflow-dispatch dialog, and searchable log viewer live inapp/src/ui/actions/;app/src/lib/actions-log-parser/parses log markup without coupling it to React.app/src/lib/actions-artifacts.tsandapp/src/lib/actions-branch-rules.tsown bounded artifact and effective-rule projections; transfer code must keep redirect credentials stripped and stale account/repository generations cancelable.app/src/lib/actions-workflow-runs.tsis the bounded cancellable/terminal status contract. Cancellation must GET/revalidate the exact repository/account/run immediately before one normal POST, deduplicate in-flight submission, and poll a terminal state; do not surface force-cancel as the primary action. -
Guided Git administration — named Repository Tools panels live in
app/src/ui/repository-tools/; bounded models and operations live inapp/src/lib/git/format-patch.ts,app/src/lib/git/structured-commit-rewrite.ts,app/src/lib/repository-signing.ts,app/src/lib/repository-lfs.ts,app/src/lib/repository-bisect.ts, andapp/src/lib/hooks/repository-hooks-manager.ts. Preserve review fingerprints, exact source/destination identity checks, and cancel/uncertain boundaries; never turn this layer into a raw command editor. Current-branch rebase continues to useapp/src/lib/rebase.tsand the existing multi-commit conflict state; the chooser adds only searched target selection, bounded preview, fresh dirty/conflict/operation checks, and exact ref revalidation. No code path may infer or perform an automatic force push. -
GitHub lifecycle workspaces — pull-request state lives in
app/src/lib/stores/pull-request-lifecycle-store.ts; Releases and Issues use their dedicated stores underapp/src/lib/stores/and views underapp/src/ui/github-releases/andapp/src/ui/github-issues/. Keep all writes account/repository/item/operation/payload-bound and cap streamed API and asset responses before parsing or writing. -
Provider-neutral triage —
app/src/lib/provider-triage.tscontains provider adapters,app/src/lib/provider-triage-json.tsvalidates bounded projections,app/src/lib/stores/provider-triage-store.tsowns cancelable account/repository generations, andapp/src/ui/repository-tools/provider-triage.tsxrenders safe neutral states. The store resolves the same canonicalendpoint#idpersisted by Repository Settings and subscribes to repository replacement/binding changes; unique-match auto-bind is valid only for an unassigned repository, while multiple matches require an explicit save. Revalidate generations before data load/save, never overwrite a valid explicit binding, and do not retain raw provider payloads, tokens, or repository paths in the store. -
History search and graph — the pure matching helper is
app/src/lib/commit-search.ts; the lane model and renderer areapp/src/ui/history/commit-graph-model.tsandapp/src/ui/history/commit-graph.tsx. Keep graph construction independent from filtered list row indices. -
Stashes, remotes, worktrees, and branch visibility — Git operations remain in
app/src/lib/git/stash.ts,app/src/lib/git/remote-manager.ts, andapp/src/lib/git/worktree.ts; the complete manager surfaces live inapp/src/ui/stashing/,app/src/ui/repository-settings/remote.tsx, andapp/src/ui/worktrees/.app/src/lib/branch-visibility.tsowns persisted pin/hide/solo state. Mutations must revalidate the exact reviewed identity and leave partial/uncertain results explicit. Remote Manager styling lives inapp/styles/ui/dialogs/_repository-settings.scss; preserve usable field/control minima, limit arbitrary wrapping to long names/URLs, and stack before semantic columns collapse. -
Multi-window and CLI routing —
app/src/main-process/window-routing.tschooses a destination window,app/src/main-process/app-window.tsowns each native window, andapp/src/lib/window-scope.tsplusapp/src/lib/profiles/profile-tabs-file.tskeep tab state isolated by window scope.app/src/lib/cli-action.tscontains the open/clone launch contract; do not route an action by assuming the first window is active. - Desktop-plus parity controls — repository pinning/grouping, Pull all, branch presets/default branch, repository editor overrides, SVG diff controls, and pushed-history safety confirmations are integrated into their existing repository, branch, diff, and undo/reset/tag surfaces rather than a separate compatibility layer.
The Material Design 3 look is built from a layered SCSS system under app/styles/:
-
_material.scss— the M3 design tokens: color roles (light on:root, dark under[data-theme="dark"]/prefers-color-scheme: dark), shape corners (small 8px, medium 12px, large 16px, full 999px), type, and motion. This is the token layer everything else consumes. -
_material-shell.scss— the application shell built from those tokens: the tabbed workspace chrome, surfaces, elevation, and layout that give the app its M3 structure. Normalized appearance values becomedata-dm-*attributes on the document body; selectors map those finite values back to tokens instead of accepting arbitrary CSS. -
app/styles/ui/partials — one partial per component (_changes.scss,_dialog.scss,_branches.scss,_ci-status.scss, …). Components pull colors and shape from the token layer rather than hard-coding hex values. -
app/styles/ui/_welcome.scssandapp/styles/ui/toolbar/_toolbar.scss— the responsive Material first-run composition and measured More-action layout. Keep moved toolbar actions mounted but out of layout so their state survives; preserve the compact-window and reduced-motion fallbacks. -
app/styles/ui/_repository-tools.scss,app/styles/ui/dialogs/_repository-settings.scss, andapp/styles/ui/_regex-builder.scss— own the compact-height vertical scroll chain, readable Remote Manager grid-to-stack threshold, and viewport-bounded Regex Builder reflow. Applymin-width: 0through the actual flex/grid ancestry, keep named controls and keyboard order, and reserve horizontal scrolling for genuinely spatial content rather than task-page recovery.
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.
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 startyarn 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.
See also: Agent API · Automation · User Guide