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 with this body:
{
"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"
}
]
}- 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.
- 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.
- 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.
The use case this job follows, with its automation and trigger: Steer a run as a team.
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.