Skip to content

Start the server or the watch when a site is opened, and start again what a quit stopped - #641

Merged
zaerl merged 3 commits into
trunkfrom
feat/auto-start-and-quit
Oct 6, 2026
Merged

zaerl merged 3 commits into
trunkfrom
feat/auto-start-and-quit

Conversation

@zaerl

@zaerl zaerl commented Oct 6, 2026

Copy link
Copy Markdown
Collaborator

Why

Fourth part of #559, on the dialog of #638. Every site started cold: its development server and its build watch waited for a press, every time the site was opened and every time the app was. A contributor who works on one site all afternoon, and a mentor who opens the app to find the sites they left running, each had a press to make first.

What changes

Opening and quitting, on the General tab.

  • Start the server when I open a site and Start the build watch when I open a site, both off unless turned on. On, opening a site that is set up starts its server, its watch or both, as a press of Start would; on WordPress Core the server's start brings the watch with it. Nothing starts for a site whose setup, update or deletion is under way, and nothing already running or starting is started again.
  • When I quit, running servers and build watches: Stop them, which is what the app has always done, or Stop them, and start them again next time. Under the second, main writes down at quit which sites had a server or their project's watch running, and the next launch starts them again, whichever site it opens on, then forgets the list. The prototype's third answer, leaving them running past the quit, is deliberately not offered: the quit sweep exists to end every child the app started (Quitting the app leaves dev servers and watchers running #83).

How a site decides. Opening a site is an edge, and so is the list a quit left: each is consumed once, when the site is ready for it, in a pure module (src/renderer/auto-start.cjs) the row asks. The row keeps the gates and the calls: the server's start first, awaited, then the watch where that start did not bring it up. A watch the server's start was refused, because the terminal is held, is not asked for twice.

How the quit remembers. before-quit runs synchronously and is not awaited, so the write goes through the store's synchronous accessor, which the first read at startup has made; a store that cannot be written is logged and does not stop the sweep. A site with both its server and its watch running is listed under both.

Not in this pull request:

How to test this

Platforms: any. The journeys drive this on macOS and Windows. From the repository root, after npm ci and npm run build:once:

npx playwright test --project=journeys auto-start.spec settings.spec i18n.spec site-header.spec
  • auto-start.spec.js: with the server set to start, the site the window opens on asks for its server once, and on Core the watch with it; opening another site asks for that site's; coming back to one whose server runs asks for nothing and does not stop it. With the watch set to start, the watch is asked for and no server; turned off, a relaunch starts nothing. With a list a quit left, the served site's server and the watched site's watch are asked for, whichever site the window opens on, the served site's watch once; the list is forgotten as it is read; a relaunch with nothing left starts nothing, and a list left under 'restart' is not followed under 'stop'. The quit setting is chosen and kept.
  • settings.spec.js, i18n.spec.js: the dialog, in English and in the pseudo-locale.
  • site-header.spec.js: the details' stickiness journey now sizes its window itself.

Nothing is run: the handlers that start the server and a script are stood in, and the journeys read what they were asked. The quit's own write is ipc-wiring.test.cjs's subject: under 'restart' it writes the sites with a server and those running their project's watch, under 'stop' nothing, and a store that cannot be written is logged and the sweep still reaches every child. I broke the open-site gate (every row started) and the quit's write, and each was caught.

Worth a look by hand (any platform, the current head, a site that is set up): Settings → General, turn on Start the server when I open a site, close, open another site and come back: the server starts, and the Logs tray shows its lines. Set When I quit to Stop them, and start them again next time, leave the server running, quit, open the app: the server starts again on that site, whichever site the window opens on. Quit again with it running and set the quit back to Stop them before you open the app: nothing starts.

What must not have happened: a server started for a site whose setup or update is under way; a server stopped by the setting (it only starts); a second watch started on a site whose server's start started one; a server or watch running past the quit; the list followed twice after a relaunch that was not a quit.

What I could not test: the real quit path end to end, since the journeys stand in for the server; the wiring test drives the handler with recorded children.

Risks and limitations

  • Review: 2 passes, 12 findings fixed here, none left. Details below.
  • A start asked for cannot be taken back: a site deactivated, or an update pressed, in the moment between the plan and the server's first answer still gets its start. The window is one request to main.
  • Two servers starting at once (on a relaunch with two sites listed) can both find Playground's first port free and one fall back to another port; pressing Start on two sites quickly has the same race today.
  • On Gutenberg a relaunch with both listed starts the watch, which removes build/ under the just-started server for its rebuild, as both settings on do.
  • The stickiness journey's second half held only on screens that leave the tray at its largest too little room; on a tall screen it did not, so the journey sizes its window to the smallest the suite runs on.

Related

Part of #559, which is part of #542. Follows #640. The quit sweep: #83.


Design decisions and alternatives considered
  • Edges, consumed once, in a pure module. The first version decided inline in the component, which the suite cannot load, and predicted whether the server's start would bring the watch from a copy of "built" that the start re-reads from disk. Now the server's start is awaited and the watch's state read after it.
  • The quit writes through a synchronous accessor. before-quit is not awaited by Electron; the store is an ESM import made at startup for the locale, so it is there by the quit, and the sweep must not wait on it.
  • Two answers to the quit question, not three. Leaving children past the quit undoes Quitting the app leaves dev servers and watchers running #83.
  • The list is read once and forgotten, and only while the setting still says 'restart': a launch that ends badly does not start everything twice, and a list left under 'restart' is not followed under 'stop'.
Review outcome (required — see AGENTS.md)

Pass 1: 5 [fix here] · 2 [follow-up]. Pass 2 (on the fix-up): 3 [fix here] · 2 [follow-up]. All 12 applied here.

  • Review: completed — a separate agent context, read-only; head e5a0ba4 / base c34aa45; the quit path read against the store rule and the sweep, the effect's edges and gates, Playground's port choice for two servers at once, the journeys against the standard; ESLint, the unit files and the pot extraction run, Playwright not; 5 [fix here], 2 [follow-up].
    • Fixed: an error writing the list at quit would have skipped the sweep; a site with both its server and its watch was remembered under the servers only, and on Gutenberg with a build the watch was not started again; the start decision lived in the component, with a prediction from a stale copy of "built"; a Server section and process menus, ticket card, apply patch and others #557 comment sat over the wrong code; the journeys slept.
    • Follow-ups, taken here: the stickiness journey's comment and its wait for the resize; a dead check in the effect.
  • Review: completed — a second separate agent context, read-only; head c75017f / base c34aa45; the awaited server start traced through the hook's two paths for whether the watch's state is set before it resolves, the throwing-store test checked to be red on the first commit; ESLint and the unit files run; 3 [fix here], 2 [follow-up].
    • Fixed: a round trip to main was not a sound sign that nothing was started, since a start asks main only after awaits of its own, so the journeys now read the controls after the settings are in the page and the effects have run; the resume journey did not seed the list a quit now writes; the effect's catch was silent.
    • Follow-ups, taken here: a watch the server's start was refused was asked for a second time; the throwing-store test did not read the log.
  • Since review: c75017f → 5117052 is those five, not reviewed again. On the head: npm run lint, npm test (2018), npm run test:electron (2019), all 123 journeys, the packaged smoke and the docs build pass.
Implementation notes
  • src/settings.cjs: autoStartServer, autoStartWatch, quitBehavior. src/resume-sites.cjs: sitesToResume, readResume. src/settings-store.js: peekStore. src/main.js: scriptByRunId, rememberRunningSites in before-quit, sites:resume. src/preload.js: takeResumeList.
  • src/renderer/auto-start.cjs: autoStartPlan. src/renderer/settings-view.cjs: quitItems, resumeFor. src/renderer/index.jsx: the row's effect; App reads the list once. src/renderer/components/settings-dialog.jsx: OpeningAndQuitting.
  • Tests: tests/unit/auto-start.test.cjs, resume-sites.test.cjs, settings.test.cjs, settings-view.test.cjs, ipc-wiring.test.cjs; tests/e2e/journeys/auto-start.spec.js.
Screenshots or recording

Not attached: I have no way to upload images from the command line. The journeys drive everything that changed, and the guide page says what each control does.

🤖 Generated with Claude Code

zaerl and others added 3 commits October 6, 2026 15:56
…what a quit stopped

The fourth of the settings (#559). Two switches start a site's development
server, its build watch or both when the site is opened, as a press of
Start would: the row becoming the open one is the edge, consumed once it
is ready for it, and nothing starts under a setup, an update or a
deletion, nor what is already running. A choice for the quit remembers
which sites had a server or a watch running: the quit stops them, as it
always has, and the next launch starts them again, whichever site it
opens on. Main writes the list in the quit handler, through the store's
synchronous accessor, from what it tracks of its children; the window
reads it once as it opens, and main forgets it as it is read.

The stickiness journey sizes its window itself, as its neighbour does:
on a tall screen the tray at its largest still leaves the details room,
which its second half needs it not to.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…server, and keep the sweep from a store that cannot be written

From the review. Which edge to consume and what to start on opening a
site is a pure module with its own tests, and the watch is started after
the server's start has answered where that start did not bring it up,
instead of predicting from a copy of whether the site is built. A site
that had both its server and its watch running is remembered under
both, which the guide promised. A store that cannot be written at quit
is logged and does not stop the sweep. The journeys' sleeps are the
round trip to main that TESTING.md names as the signal for nothing
asked; the stickiness journey waits for its window to be resized and
says what size the window opens at.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…ould not be started

From the second review. A start asks main only after awaits of its own,
so a round trip to main was not a sign that nothing was asked; what a
start does first is set the site's controls to starting, so the journeys
wait for the settings to be in the page and the effects to have run, and
then read the controls. The resume journey seeds the list a quit now
writes, a Core site's watch beside its server, and holds that the watch
is asked for once. A start that could not be made is said in the log,
where it was swallowed; a watch the server's start was refused is not
asked for a second time; and the quit's test reads the log as well.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
@coderabbitai

coderabbitai Bot commented Oct 6, 2026

Copy link
Copy Markdown

Important

Review skipped

Auto reviews are disabled on this repository. Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration
  • Configuration used: Repository: WordPress/contributor-toolkit/.coderabbit.yaml
  • Review profile: CHILL
  • Plan: Advanced
  • Run ID: 44d2edfa-4d6c-49c8-a59d-042c4aaf6d08

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review
  • Autopilot · Keep fixing CodeRabbit findings and required CI, and resolving merge conflicts

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@zaerl
zaerl merged commit 3866e1a into trunk Oct 6, 2026
10 checks passed
@zaerl
zaerl deleted the feat/auto-start-and-quit branch October 6, 2026 14:48
zaerl added a commit that referenced this pull request Oct 6, 2026
## Why

#560, and the last row of #559's dialog. The window painted light only,
and said so: its colour-scheme declaration was held to `light` by a
test, since the older component library ships light styles and a promise
of dark that only reached the native form controls gave charcoal inputs
on white cards. A contributor whose machine is in dark mode got a white
window all evening, with no way to say otherwise. This adds a dark
theme, chosen in the settings or following the system.

## What changes

**Theme, on the General tab**, under Appearance: **Light**, **Dark** or
**System**, which is the default and follows the operating system. A
change is on screen as it is made; nothing running is touched. The
prototype's fourth answer, a custom pair of colours, is not offered.

**One source of truth.** Main gives the choice to Electron's
`nativeTheme`, read from the store before the window is made and set
again as the setting changes. Chromium answers the page's
`prefers-color-scheme` from it and paints the window's chrome and the
native form controls to match, so the window only reads that answer: a
wrapper around the design system's provider seeds the dark ramp from the
prototype's dark background when the scheme is dark, and passes no
colour when it is light, so the light theme is pixel for pixel what it
was and no picture in the guide moves. The provider builds every token
from the seed, for its own components, for the older library's through
the variables it maps for them, and for the app's own styles; the diff
and log panes follow through their tokens.

**What the provider does not reach.** The terminal takes its colours as
values and reads them again when the scheme changes. The older library's
dialogs and popovers are painted white in its own stylesheet, with no
token behind the colour, and are painted here with tokens; the popover's
ring and shadow are restated in the dark scheme only.

**No white before the page paints.** A dark window is made in the dark
colour and kept in it as the theme changes, and the page paints its body
the same colour under the dark scheme until the provider has mounted,
which it marks on the document. `color-scheme.test.cjs` now holds the
declaration to `light dark` and that colour to the theme module.

**The pictures and the journeys.** Playwright holds a page to the light
scheme whatever the machine says, so the guide's pictures are pinned to
light through the app's own setting (`SHOTS_THEME=dark` for a dark one),
and every journey stays light; the theme journey alone lets the page
follow the app.

**Not in this pull request:**

- The patch and Trac windows, which are made without a theme and paint
their own bar white. Follow-up.
- A custom theme.
- Mail rendering is unchanged here and tracked separately.

## How to test this

Platforms: any. The journeys drive this on macOS and Windows. From the
repository root, after `npm ci` and `npm run build:once`:

```
npx playwright test --project=journeys settings.spec i18n.spec terminal.spec logs.spec
```

- `settings.spec.js`, the theme journey: stored dark, the window starts
dark, Electron is told, the body and the terminal are painted with the
dark tokens; Light chosen is kept, Electron is told, the page, the body,
the terminal and the window's own colour are light at once with no
relaunch; System is kept and the window is the colour of whichever theme
Electron then says; started again with dark kept, the window is made
dark and the control says so.
- `i18n.spec.js`: the control in the pseudo-locale.
- `terminal.spec.js`, `logs.spec.js`: the terminal's and the panes'
colours are the tokens, as before.

I broke six claims one at a time and each was caught: the stored theme
not applied at startup, the setting not telling Electron, the window not
made dark, the terminal not re-reading its colours, the page not seeded
dark, the window not painted on a setting change (the last by
`ipc-wiring.test.cjs`; the journey is kept green by Electron's own
`updated` for that case). `tests/unit/theme.test.cjs`,
`settings.test.cjs`, `settings-view.test.cjs` and
`color-scheme.test.cjs` hold the model, the control's entries and the
page's declaration.

**Worth a look by hand** (any platform, the current head): Settings →
General → Theme. Press **Dark**: the whole window, the dialog you are in
and the footer's **Give feedback** popover are dark at once; the site
menu, the create-site dialog and the review dialog's diff are dark when
opened. Open the Terminal tray before and after: it follows. Press
**System** and change your machine's appearance: the window follows.
Quit with **Dark** kept and open the app: it opens dark, with no white
flash.

**What must not have happened:** the light theme looking any different
from before (the guide's pictures are the reference); a white window or
page in the moment before a dark page paints; the terminal keeping the
old colours after a switch; a server or build stopped or started by the
change.

**What I could not test:** Windows, where the journeys run in CI; the
system's theme changing under **System** by hand, which the unit test
drives through Electron's `updated` with a stubbed answer, and which I
could not stage on the Mac without changing its appearance under the
running suite. The pictures of the dark theme in this description were
taken with `SHOTS_THEME=dark` and are not in the diff: the guide shows
the light theme.

## Risks and limitations

- Review: 2 passes, 6 findings fixed here, 1 follow-up left (the patch
and Trac windows), every style note applied. Details below.
- **The patch and Trac windows stay white** under Dark, as they were.
Follow-up.
- **The dark theme is the design system's ramp from one seed**, not a
hand-made palette: a surface or a stroke that reads wrong in dark is the
ramp's to fix, and the one token the app sets by hand is the pre-mount
body colour, held to the seed by a test.
- The older library's buttons keep a handful of literal colours for a
disabled or a destructive state, readable in dark but not the ramp's.

## Related

Closes #560. Part of #559, which is part of #542. Follows
#641.

---

<details>
<summary>Design decisions and alternatives considered</summary>

- **`nativeTheme` as the one place the choice is made.** The prototype
resolves 'system' in the page with `matchMedia`. Here main resolves
nothing: it gives Electron the setting, and the page reads
`prefers-color-scheme`, which Electron answers from it. One chain,
setting → Electron → page → tokens, and the window's chrome and the
native controls come with it.
- **No seed in the light scheme.** Generating the light ramp from a seed
moves every colour by a hair; the tokens stylesheet already holds the
light values, and the guide's pictures show them.
- **The window's own colour set from the setting, not left to
`updated`.** Electron promises the event for a change to light or dark,
not to system; the listener stays for the system's theme changing under
'system'.
- **A pre-mount body rule in index.html, scoped to before the provider
mounts.** The page's first paint is before its script runs; the rule
stops at the attribute the provider sets, so the ramp's surface is the
body from then on. The colour is the seed, and a test holds the two
together.
- **Three themes, not four.** A custom pair of colours is a feature the
prototype has and nobody has asked for.

</details>

<details>
<summary>Review outcome (required — see AGENTS.md)</summary>

**Pass 1: 3 [fix here] · 1 [follow-up] · 5 style notes. Pass 2 (on the
fix-up): 3 [fix here] · 0 [follow-up] · 1 style note. All 6 fixed here,
every style note applied, 1 follow-up left.**

- **Review:** completed — a separate agent context, read-only; head
`8a7457b` / base `3866e1a`; the store rule checked against the startup
read and the handler, the provider's effect ordering against React's
commit phases, the pre-mount rule's specificity, the deep-link path that
can open the window before the ready path, the old library's rules the
shell overrides, translation extraction; ESLint and the unit files run;
3 [fix here], 1 [follow-up].
- Fixed: the window's colour was set once and went stale on a theme
change, an OS change and a deep link's early window; the journey read
the light body before the app had repainted; the popover's ring and
shadow were restated in the light theme too, and a menu-group rule
matched nothing.
  - Follow-up left: the patch and Trac windows are not themed.
- Style notes, applied: a stale comment on the store's first read;
translator context for the theme's words; `SHOTS_THEME` checked; the
tests naming the colours by their constants; a unit test that restated
constants.
- **Review:** completed — a second separate agent context, read-only;
head `64cad1a` / base `3866e1a`; the `updated` listener against Electron
43's documentation, the stub's assumptions, the light theme against the
library's and the tokens' values; ESLint and the unit files run; 3 [fix
here].
- Fixed: nothing showed Electron emits `updated` for a change to
'system', so the window is painted from the setting itself and the
journey reads the window's colour; a test's name claimed a deep link's
window it did not exercise; `SHOTS_THEME=system` passed the check that
exists to keep the system's theme out of the pictures.
- Style note, applied: the dark popover's soft layer is a glow, and the
comment says so.
- **Since review:** `64cad1a → HEAD` is those three and the note, not
reviewed again. On the head: `npm run lint`, `npm test`, `npm run
test:electron` (2029), all 124 journeys, the packaged smoke and the docs
build pass.

</details>

<details>
<summary>Implementation notes</summary>

- `src/theme.cjs`: `THEMES`, `themeColorSeeds`, `windowBackground`, the
two seeds. `src/settings.cjs`: `theme`. `src/main.js`:
`applyStoredTheme`, `paintWindowForTheme`, the `updated` listener,
`backgroundColor` on the window, `settings:set`.
- `src/renderer/components/app-theme.jsx`: `AppTheme`, `useDarkScheme`.
`src/renderer/index.jsx`: mounted under it.
`src/renderer/hooks/use-site-terminal.jsx`: the re-read.
`src/renderer/terminal-theme.cjs`: the header. `src/renderer/shell.css`:
the old library's dialogs and popovers. `src/renderer/index.html`:
`color-scheme` and the pre-mount rule.
- `src/renderer/settings-view.cjs`: `themeItems`.
`src/renderer/components/settings-dialog.jsx`: `ThemeControl`.
- `scripts/screenshots/fixtures.cjs`: `SHOTS_THEME`.
`scripts/screenshots/capture.cjs`, `tests/e2e/helpers/app.cjs`:
`colorScheme`.
- Tests: `tests/unit/theme.test.cjs`, `color-scheme.test.cjs`,
`settings.test.cjs`, `settings-view.test.cjs`, `ipc-wiring.test.cjs`
(the electron stub's `nativeTheme`);
`tests/e2e/journeys/settings.spec.js`.

</details>

<details>
<summary>Screenshots or recording</summary>

Not attached: I have no way to upload images from the command line.
`SHOTS_THEME=dark npm run shots` takes the guide's pictures in the dark
theme into `docs/public/screenshots/` for a look (and leaves them
changed; restore them after).

</details>

🤖 Generated with [Claude Code](https://claude.com/claude-code)

---------

Co-authored-by: Claude Fable 5.1 <noreply@anthropic.com>
@zaerl zaerl added this to the v2.0.0 milestone Oct 7, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant