What the agent does
The agent starts from the issue and the repository, cloned into its own sandbox, and reads how the app styles itself before it adds a button. In bulletproof-react's apps/react-vite workspace (Vite 5, React with React Router, Tailwind ^3.4.3, Vitest 2, MSW 2, yarn), src/index.css already defines the theme twice: color variables such as --background and --foreground under :root, and the same variables with dark values under a .dark selector. tailwind.config.cjs maps background, foreground, border and the rest to hsl(var(--...)). What it does not set is darkMode, so Tailwind 3 uses its default, the media strategy, which follows the operating system and ignores a .dark class. Half the work exists already; the agent says so and adds the other half.
The change itself is small and goes where the codebase expects it. It sets darkMode: 'class' in tailwind.config.cjs so the .dark variables and any dark: utility follow a class on <html>. It adds a small theme module that reads localStorage, falls back to window.matchMedia('(prefers-color-scheme: dark)') when nothing is stored, and writes the choice back. It puts the toggle in the shared layout, in the <header> of src/components/layouts/dashboard-layout.tsx next to the existing user menu, built from the project's own Button in src/components/ui/button with a lucide-react icon, as the rest of the header is. File and folder names stay kebab case, because .eslintrc.cjs enforces KEBAB_CASE with check-file, and the module stays out of src/features/ because import/no-restricted-paths limits what features may import from each other.
Two details prevent the bugs a dark mode toggle usually ships with. A choice read in a React effect applies after the first paint, so a user who picked dark sees a white flash on every load; the agent adds a few lines of inline script in index.html, before /src/main.tsx loads, that set the class from localStorage or the media query. And jsdom has no matchMedia, which src/testing/setup-tests.ts does not stub today (it stubs ResizeObserver, btoa and atob), so the agent stubs it in its tests to cover both system preferences. The setup file also calls vi.mock('zustand'), so if the theme lives in a zustand store, its state resets between tests.
Then the tests, in a __tests__ folder beside the component as src/components/ui/dialog/__tests__/dialog.test.tsx does: on first load with nothing stored, the class follows prefers-color-scheme; clicking the toggle switches the class on document.documentElement and stores the choice; a stored choice wins over the system setting. Last, it runs what .github/workflows/react-vite-ci.yml runs in apps/react-vite: mv .env.example .env, yarn install, yarn build (which is tsc && vite build --base=/), yarn test, yarn lint and yarn check-types, and it opens the dashboard in the sandbox's browser to take a screenshot in each theme.
Start it with one API call
POST https://api.prix.dev/o/{org}/sessions with this body:
{
"agent": "claude",
"input": "A Linear issue labeled agent says: \"Add a Dark Mode Toggle to a Vite React App. Add a toggle in the app header that switches the Tailwind theme, remembers the choice in localStorage and respects prefers-color-scheme on first load. Work in apps/react-vite.\" Before writing code, read how the app defines its theme today: the CSS variables, any .dark selector, and the darkMode setting in the Tailwind config, and say what already exists. Use the class strategy, put the toggle in the shared layout's header using the project's own Button and icon library, and follow its lint rules for file names and import boundaries. Apply the stored or system theme before React renders so there is no flash of the wrong theme. Add Vitest and Testing Library tests for the first-load default under both system preferences, the toggle switching the class and storing it, and a stored choice winning, stubbing matchMedia. Run yarn build, yarn test, yarn lint and yarn check-types in apps/react-vite until they pass, take a screenshot of the dashboard in each theme, and open a pull request that links back to the issue.",
"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 is a pull request on its own branch that links the Linear issue. In an app shaped like bulletproof-react, the diff touches tailwind.config.cjs (one line, darkMode: 'class'), index.html (the script that sets the class before the bundle loads), a new theme module and toggle component under src/components/, the header in dashboard-layout.tsx, and a new test file in a __tests__ folder. The description says what already existed (the .dark variables in src/index.css), what was missing (the class strategy and the toggle), and shows the dashboard screenshots in light and dark.
The proof is the checks and the transcript. The session keeps every command with its output: yarn install, each yarn build, yarn test, yarn lint and yarn check-types, including runs that failed before a fix. The repository's react-vite-ci.yml workflow runs the same build, test, lint and type check on the pull request in GitHub Actions.
Before merging, a reviewer loads the preview with the system set to dark and nothing stored, then toggles and reloads, to see that there is no flash; checks that components which hard-code colors instead of using the theme variables still read well in dark; and confirms the new tests fail when the toggle stops writing to localStorage.
Run it on every Linear issue labelled agent
In the console, an automation on the Project with a Linear trigger on linear.issue.labeled and the label agent starts one run per labelled issue in the Project's repository. Product requests like this one, small and well described, are what the label is for: the issue text is the prompt.
From your own server, start the same run with a POST /o/{org}/sessions per issue, listing the repository and the issue's https://linear.app/<workspace>/issue/<ID> URL in resources so Rush reads the issue through your Linear connection. A webhook on the API key posts run.completed or run.failed with the session_url when the turn finishes.
The use case this job follows, with its automation and trigger: Turn a Linear label into a PR.
Why run it on Rush?
A dark mode change is visual, so it is finished when someone has looked at it, not when the tests pass. In a Rush sandbox the agent builds the app, runs it, and opens the dashboard in its own browser in both themes, so the pull request arrives with screenshots instead of a request to check it out locally. It runs the full yarn install, build, test, lint and type check on its own machine without touching anyone's dev server on port 3000.
The label in Linear is the only step a person takes. The transcript shows the agent finding the existing .dark variables before writing new ones and every check with its output, and the pull request links back to the issue. Several labelled issues can run at once, each on its own branch in its own sandbox, up to the key's max_concurrent limit.
Frequently asked questions
Will it use Tailwind's class or media strategy?
The class strategy, because a toggle has to override the system setting. With darkMode: 'class' the .dark class on <html> drives the theme, and the agent's script sets that class from the stored choice or from prefers-color-scheme when nothing is stored.
How does it avoid the flash of the wrong theme on load?
It sets the class in a small inline script in index.html that runs before the React bundle loads, so the first paint already has the right theme. Reading the choice inside a React effect would apply it only after the page had rendered once in light.
Does it work with shadcn/ui-style CSS variables?
Yes, that is the common case. When the theme is defined as CSS variables under :root and .dark, as bulletproof-react's src/index.css does, the agent changes no colors at all: switching the class switches every variable.
How do the tests check prefers-color-scheme in jsdom?
jsdom has no matchMedia, so the tests stub window.matchMedia to answer dark or light and render the app's theme setup under each. They also set and clear localStorage to check that a stored choice wins over the system preference.