# Add Dark Mode to an Electron App

Yes. An agent adds a system, light and dark setting to an Electron app through nativeTheme, a menu and a toggle, with Jest tests, steered by follow-ups.

[Run this on Rush](https://cloud.getrush.ai/run/add-dark-mode/electron/), in a Project that has your repository.

## What the agent does

The agent reads how the app is wired before it touches a colour. In an app shaped like electron-react-boilerplate/electron-react-boilerplate, `package.json` declares Electron 44, React 19.3, `react-router-dom` 7, `electron-vite` for the build, Jest 30 with `@testing-library/react`, TypeScript 7 and Node 24 or newer, with scripts `start`, `build`, `package`, `lint`, `test` and `test:smoke`. `src/main/main.ts` creates one `BrowserWindow` of 1024 by 728 with the preload script, answers an `ipc-example` channel, builds the menu through `MenuBuilder` and starts `electron-updater`. `src/main/preload.ts` exposes `window.electron.ipcRenderer` with a `Channels` union that allows only `ipc-example`, and `src/renderer/preload.d.ts` types it. `src/renderer/App.tsx` renders one `Hello` route inside a `MemoryRouter`, and `src/renderer/App.css` paints a fixed gradient background with white text and white buttons.

The setting lives in the main process. A new `src/main/theme.ts` reads and writes a `theme.json` under `app.getPath('userData')` with one of `system`, `light` or `dark`, applies it to `nativeTheme.themeSource`, and answers two new IPC channels, `theme:get` and `theme:set`, while forwarding `nativeTheme`'s `updated` event to the renderer as `theme:changed` with `shouldUseDarkColors`. `preload.ts` adds those three names to the `Channels` union and a small `theme` object with `get`, `set` and `onChange`, so the renderer never imports Electron directly. `src/main/menu.ts` gains an `Appearance` submenu with three `radio` items under the `View` menu in both `buildDarwinTemplate` and `buildDefaultTemplate`, so the menu and the in-app toggle stay in step.

The renderer follows the system. `App.css` moves the gradient and the text colour into variables on `:root` and overrides them under `:root[data-theme='dark']`, and a `ThemeToggle` component sets `data-theme` on the document from `theme:changed` and offers the three choices. The default is `system`, so a user who has never opened the setting sees what their OS asks for, and the choice persists across launches through the JSON file.

Tests follow the repository's own examples. `src/__tests__/updates.test.ts` already mocks `electron` and `electron-log` with `jest.mock` and uses `jest.resetModules`, so a new `theme.test.ts` mocks `nativeTheme` and the `app` path, asserts that `set('dark')` writes the file and sets `themeSource`, and that a corrupt file falls back to `system`. `App.test.tsx` gains a case that the toggle renders and switches `data-theme`. The agent runs `npm run lint`, `npm exec tsc -- --noEmit`, `npm test` and, under `xvfb-run`, `npm run test:smoke`, the sequence `.github/workflows/test.yml` runs on Linux, and takes screenshots of the window in each appearance through the remote debugging port the smoke test already uses, which is where the team's follow-ups come in.

## Start it with one API call

`POST https://api.prix.dev/o/{org}/sessions`

```json
{
  "agent": "codex",
  "input": "Add dark mode to this Electron app with a system, light and dark setting. Read package.json, src/main/main.ts, src/main/menu.ts, src/main/preload.ts, src/renderer/preload.d.ts, src/renderer/App.tsx, src/renderer/App.css, src/__tests__/updates.test.ts and .github/workflows/test.yml first. Keep the setting in the main process with nativeTheme.themeSource and persist it in userData, expose it through the preload with typed channels, add an Appearance submenu with radio items under View, add a renderer toggle and CSS variables with a dark override, add Jest tests that mock electron for the theme module and the toggle, run npm run lint, tsc --noEmit, npm test and the smoke test under xvfb, post a screenshot of the window in light and in dark, and wait for feedback before opening the pull request.",
  "resources": [
    {
      "url": "https://github.com/your-org/your-app"
    }
  ]
}
```

1. **Get an API key.** In the console, open Settings, then API keys, and create a key for your org. Send it in the Authorization header as a Bearer token.
2. **Send the request.** POST the body in request.json to https://api.prix.dev/o/{org}/sessions, with your repository as the resources URL. The run starts in its own cloud sandbox.
3. **Read what came back.** Follow the run on its live stream or wait for the run.completed webhook, then read the result and its trajectory.

## What comes back

What comes back first is a pair of screenshots and a diff to react to, then a pull request. The diff adds `src/main/theme.ts` and its test, changes `src/main/main.ts` to initialise the theme before the window loads, `src/main/menu.ts` for the `Appearance` submenu, `src/main/preload.ts` and `src/renderer/preload.d.ts` for the typed bridge, `src/renderer/App.tsx` and `App.css` for the toggle and the variables, and `src/__tests__/App.test.tsx` for the new case. No dependency is added, because `nativeTheme` ships with Electron and the file is written with `fs`.

The checks are the proof. The `Test` workflow runs on the pull request on macOS, Windows and Ubuntu: `npm ci`, `npm run package`, `npm run lint`, `tsc --noEmit`, `npm test` and the Electron startup smoke test, under `xvfb-run` on Linux. The session's trajectory at `GET /o/{org}/sessions/{id}/messages` keeps each of those commands with its output and the screenshots the team reviewed, so a reviewer sees the window in both appearances rather than imagining it.

Before merging, a reviewer checks that the three menu items and the in-app toggle show the same selection, that `themeSource` is set before the first window loads so there is no flash of the wrong colours, and that a missing or corrupt `theme.json` falls back to `system` rather than crashing at startup. Nothing reaches main until the pull request merges.

## Run it on every design change

Start one run when a design change asks for an appearance pass, with this body and the repository in `resources`. Follow-ups go through `POST /o/{org}/sessions/{id}/messages` with the same key once the turn has finished: a corrected background colour, a different default, a request to recapture both screenshots. While a turn is running the call is `409 TURN_ACTIVE`, so send the next note when `status` leaves `running`. Everyone in the org who can read the run reads the same transcript and the same screenshots.

Give the key a `webhook_url` and it POSTs `run.completed`, `run.failed` or `run.cancelled` with `data.session_url` when each turn finishes, so the designer is told when the next pair of screenshots is ready. The sandbox parks between turns and is kept for 7 idle days, so a review that waits overnight still picks up where it left off, with the build and the window state intact.

## Why run it on Rush?

Dark mode in a desktop app is a conversation, not a ticket: the first palette is never the last, and the way to judge it is the window itself in both appearances, on a build that actually packages. On a laptop that means one person rebuilding and screenshotting for everyone else. On Rush the agent runs `npm run package` and the smoke test in its own sandbox under `xvfb`, captures the window through the debugging port, and the designer answers with a follow-up instead of a meeting.

The run is also its own evidence. The transcript holds every command, every screenshot and every correction, the pull request carries the diff and the Jest tests, and the repository's three-platform workflow runs on it. A second design change is a second run, in parallel, with the same record.

## Frequently asked questions

### Why keep the setting in the main process?

Because `nativeTheme.themeSource` is a main-process API that changes the window chrome, the system dialogs and `prefers-color-scheme` in the renderer at once. The renderer asks for the current value and listens for changes through the preload bridge, which keeps `contextIsolation` as it is; it never imports Electron. The menu and the toggle both call the same `theme:set` channel.

### Where is the choice stored?

In a `theme.json` file under `app.getPath('userData')`, written with `fs` from `src/main/theme.ts`. The repository has no settings store dependency, and one small JSON file does not justify adding one. If your app already uses `electron-store` or similar, the agent reads `package.json`, sees it, and uses it instead.

### What happens on first launch?

The default is `system`, so the app follows the operating system until a user picks otherwise. `main.ts` applies the stored value before `createWindow` loads `index.html`, and the test for a corrupt or missing file asserts the fallback to `system`, so a bad file never blocks startup.

### How does the team see the result before merging?

The agent runs the packaged app under `xvfb-run` the way the smoke test does and captures the window in light and in dark through the remote debugging port, then posts both in the transcript. A follow-up through `POST /o/{org}/sessions/{id}/messages` asks for a change, and the agent recaptures. The pull request opens when the team says the screenshots are right.

### Does it change the auto-updater or the IPC example?

No. `src/main/updates.ts` and its test are untouched, and the existing `ipc-example` channel keeps working; the `Channels` union in `preload.ts` simply gains the three theme channel names, which TypeScript then checks everywhere they are used.
