Skip to content

Add the PHP version and the debug flags to the settings, applied at the next server start - #640

Merged
zaerl merged 4 commits into
trunkfrom
feat/server-settings
Oct 6, 2026
Merged

zaerl merged 4 commits into
trunkfrom
feat/server-settings

Conversation

@zaerl

@zaerl zaerl commented Oct 6, 2026 •

Copy link
Copy Markdown
Collaborator

Why

Third part of #559, on the dialog of #638. Every site's development server ran on the PHP the bundled Playground chooses and with WordPress's debug constants fixed on; a contributor whose ticket is about PHP 7.4, or about what a site does with WP_DEBUG off, had no way to see it. This adds the PHP version and the two debug constants that can be turned off to the settings, and shows what a server will start with in the open site's details.

What changes

A Sites tab: Development server. The PHP version, one of the seven the bundled Playground has (7.4 to 8.5), 8.3 unless another is chosen; and two switches, Report notices and deprecations (WP_DEBUG) and Use unminified scripts (SCRIPT_DEBUG). The rest of the constants a server is booted with (the log, the display, the disabled fatal handler and updater) stay as they were and are not anyone's to change.

Applied at the next start. Main reads the three at every server start and hands them to the runner in its config; a server that is running keeps what it started with until it is stopped and started, and the tab says so. A version a release has taken out of the bundle is passed over for 8.3, with a line in the app log, and the control shows 8.3 as chosen with a note saying why.

The details say what a server will start with, as the prototype has them: "WordPress Core · PHP 8.3" on the Checkout row, and a Debugging row with the constants that are on, or "Off".

What WP_DEBUG off does, said for what it is. It takes away notices and deprecations. Warnings and errors still reach debug.log and the browser, and error_log() calls still reach debug.log, because Playground's own PHP keeps logging and display on whatever WP_DEBUG says. The switch, the guide and the constants' module say so.

The guide's pictures are retaken. The details changed here, and the footer's cog from #638 had never been retaken. The three pictures of a site's page are taken in a window 900 high, so the details' Build watch section is whole.

Not in this pull request:

  • The PHP version a running server is on, in the Server section, where the details show the version the next start is given. Follow-up.
  • Per-site values: these are global, like the other settings. A per-site override can go in siteMeta if one is ever wanted.
  • The remaining constants (WP_DEBUG_LOG, WP_DEBUG_DISPLAY, WP_DISABLE_FATAL_ERROR_HANDLER, AUTOMATIC_UPDATER_DISABLED), on purpose: src/wp-debug-constants.js says why each is on.
  • The rest of Settings dialog with what exists and all new settings #559: what starts when a site opens and what happens on quit; the theme (Add dark mode #560).

How to test this

Platforms: any for the dialog and the details; macOS for the real server, below. The journeys drive the dialog and the details on macOS and Windows. From the repository root, after npm ci and npm run build:once:

npx playwright test --project=journeys settings.spec site-header.spec i18n.spec
  • settings.spec.js, the Sites journey: the fallbacks (8.3, both constants on); 8.4 chosen is kept, WP_DEBUG off is kept and SCRIPT_DEBUG left as it was; the open site's details then say "WordPress Core · PHP 8.4" and "SCRIPT_DEBUG"; restarted, the tab shows what was kept.
  • site-header.spec.js: the details name the project with the PHP a server starts on and which constants are on.
  • i18n.spec.js: the tab in the pseudo-locale.

I broke two claims one at a time (main not reading the settings at a start; the switch not writing) and each was caught, the first by ipc-wiring.test.cjs, the second by the journey. tests/unit/runner-wiring.test.cjs proves the runner hands Playground the version and turns the two constants off in the blueprint; wp-debug-constants.test.cjs that nothing else in the set moves; settings.test.cjs and settings-view.test.cjs what is accepted and what the control shows.

The real server, run by me on this Mac (Apple Silicon, the current head), as TESTING.md asks for a change to src/server-runner.js. The app was driven through Playwright with a throwaway profile listing a built wordpress-develop checkout; Start development server was pressed; the served WordPress was logged in to with the app's own credentials and asked, on Site Health's Info tab, which PHP it runs on:

  • PHP version set to 8.4: the server came up, Site Health reported PHP 8.4.25, wp-admin served, Stop ended it.
  • PHP version left at the fallback, 8.3: Site Health reported PHP 8.3.33.

Worth a look by hand (any platform, the current head): Settings → Sites, choose 7.4, close: the details say "PHP 7.4". Start the server: wp-admin → Tools → Site Health → Info → Server says PHP 7.4. Turn WP_DEBUG off, stop and start the server: a page with a deprecated call shows nothing for it, and a page that calls a missing function still shows the fatal.

What must not have happened: a running server changing PHP or constants under the contributor; a change applied to a site other than the next start; WP_DEBUG_LOG or the fatal handler's constant moving with the switches; the details saying a version the server will not start on.

What I could not test: Windows, and a real server with WP_DEBUG off (I checked what the code sends, not a page's output).

Risks and limitations

  • Review: 2 passes, 8 findings fixed here, 1 follow-up taken here (the real boot), 2 left. Details below.
  • The details show the PHP of the next start, not of a server already running. Change the version while a server runs and "PHP 8.4" sits above "Server running" on 8.3. Follow-up: the Server section should name the running server's PHP.
  • Main requires @php-wasm/universal for the list of versions. It is not a declared dependency; it is @wp-playground/cli's, at whatever version that pins, and the comment says so. Declaring it pinned is a package.json decision I have left to the maintainer.
  • The Electron unit suite failed one test once in a run of 2004 and passed on three runs after; I could not see which test it was the first time.
  • A WP_DEBUG switch that is off still leaves WP_DEBUG_LOG on, by design, so the debug.log tab keeps showing warnings and errors.

Related

Part of #559, which is part of #542. Follows #639.


Design decisions and alternatives considered
  • Global, applied at the next start, and read by main at each start. The prototype's "New sites" tab suggests values copied into a site at creation. Here the version and the constants are what the server is booted with, so reading them at each start is simpler and also reaches existing sites.
  • An explicit fallback, always passed. The CLI's own default is 8.3 today; passing it ourselves keeps the behaviour stable across a Playground bump, and a stored version the bump took out falls back to it rather than to whatever the CLI then chooses.
  • The list from the bundle, not written down. SupportedPHPVersions from @php-wasm/universal is what the runner can serve; a list of our own would drift.
  • Only two constants have a switch. The others exist to make a fatal show as itself, the log exist for the tab and the updater stay out of a checkout; a switch for each would be one more thing to get wrong at a Contributor Day.
  • One decision for which version is shown and used, phpVersionChoice in settings-view.cjs: the dialog, the details and the start handler all agree on the fallback.
Review outcome (required — see AGENTS.md)

Pass 1: 3 [fix here] · 3 [follow-up]. Pass 2 (on the fix-up and the details): 5 [fix here] · 1 [follow-up]. All 8 fixed, the real-boot follow-up taken here, 2 left.

  • Review: completed — a separate agent context, read-only; head 9131ed7 / base 77751a1; the runner's options checked against the CLI's runCLI signature and its persistent start command, WordPress's wp_debug_mode() and Playground's php.ini and mu-plugin read for what WP_DEBUG off does, the design system's switch and toggle props confirmed; ESLint and the unit files run; 3 [fix here], 3 [follow-up].
    • Fixed: WP_DEBUG off was said to empty the log and the browser of errors, where it takes away notices and deprecations; which versions the control offers was decided in the component; the real boot TESTING.md asks for was not recorded.
    • Follow-ups: the undeclared require (a comment, left to the maintainer to declare); a stored version the bundle no longer has was shown as chosen but not used (taken here); the journey pins 8.4 (left).
  • Review: completed — a second separate agent context, read-only; head ca84255 / base 77751a1; the four rewordings checked against WordPress and Playground again, the details' new rows and their journeys, the retaken pictures opened against their alt texts; ESLint and the unit files run; 5 [fix here], 1 [follow-up].
    • Fixed: the details showed the version set where the dialog and the start used the fallback; error_log() was said to reach the browser; three pictures cut the details' Build watch section off under the footer; the require had no comment on whose dependency it rides; a failed read of the versions left an enabled, empty control.
    • Follow-up left: the Server section naming the PHP a running server is on.
  • Since review: ca84255 → ba66103 is those five fixes and two style notes, not reviewed again. The commit after it changes one journey: site-header.spec.js's stickiness test failed on both runners, since the Debugging row left the details too tall to fit over the tray at its smallest on their screens; its seeded site now has no creation date and no trunk date, two rows fewer, and the journey was run at windows 700, 680 and 640 high and is red at 640 with the app's rule broken. On the head: npm run lint, npm test (2002), npm run test:electron (2003), all 119 journeys, the packaged smoke and the docs build pass. The twelve pictures were taken four times before the last change and two of them moved by 12 and 30 pixels on a corner between takes, the known noise; the three retaken after it were taken once.
Implementation notes
  • src/settings.cjs: phpVersion, wpDebug, scriptDebug, acceptSwitch. src/wp-debug-constants.js: debugConstants(debug). src/server-runner.js: php and the constants from the config.
  • src/main.js: phpVersions(), playground:php-versions, playground:start reads the store at each start. src/preload.js: listPhpVersions.
  • src/renderer/settings-view.cjs: phpVersionChoice. src/renderer/hooks/use-settings.jsx: php. src/renderer/components/settings-dialog.jsx: SitesTab. src/renderer/site-details.cjs: phpVersion and debug in the rows.
  • scripts/screenshots/shots.cjs: the three site views at 1200×900.
Screenshots or recording

The retaken images are in the diff: docs/public/screenshots/ shows the details before and after.

🤖 Generated with Claude Code

zaerl and others added 3 commits October 6, 2026 12:48
…at the next server start

The third of the settings (#559): a Sites tab with what every site's
development server runs with. The PHP version is one of those the
bundled Playground has, 8.3 unless another is chosen, and WP_DEBUG and
SCRIPT_DEBUG can be turned off; the rest of the constants a server is
booted with stay as they were and are nobody's to change. Main reads the
three at each start and hands them to the runner in its config, so a
change applies to the next server and a running one keeps what it
started with; a version the bundled Playground no longer has, left by a
bump, is passed over for the fallback.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…, and put the server settings in the details

From the review. WP_DEBUG off was said to empty debug.log and the browser
of errors; it takes away notices and deprecations, and warnings, errors
and error_log() calls still reach both, since Playground's own PHP keeps
logging and display on. The switch, the guide and the constants' module
say so. Which versions the control offers, and which it shows as chosen,
are decided in the view module: a version a bump has taken from the
bundle shows the fallback as chosen, with a note, and the start logs why.

The open site's details name the PHP a server starts on beside the
checkout and, under Debugging, which of the two constants are on, as the
prototype has them. The guide's pictures are retaken: the details changed
here, and the footer's cog since #638 had not been taken yet.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
… the details whole in the guide's pictures

From the second review. The details named the PHP version set, where the
dialog and the start used the fallback for one the bundle no longer has;
all three now decide it in one place, and the versions are read once by
the hook, which says when they could not be, where the control offered
nothing. error_log() writes to the log and not the page, and the four
sentences say so. The site views are taken in a window tall enough for
the details' Build watch section, which the Debugging row had pushed
under the footer. The require of the PHP versions says whose dependency
it rides on, and the start handler builds its log scope once.

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: c84d8b70-a717-496f-9f81-536bf4783a01

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.

… on the runners' screens

The facts gained a Debugging row (#559), and with every row the facts
alone no longer fit over the tray at its smallest on the runners'
screens, which the journey's first half needs them to: it failed there
by 36 to 49 pixels and passed on a taller screen. The site it seeds now
has no creation date and no trunk date. Run at windows 700, 680 and 640
high, and red at 640 with the app's rule broken.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
@zaerl
zaerl merged commit c34aa45 into trunk Oct 6, 2026
10 checks passed
@zaerl
zaerl deleted the feat/server-settings branch October 6, 2026 12:02
zaerl added a commit that referenced this pull request Oct 6, 2026
…what a quit stopped (#641)

## 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 (#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:**

- Leaving servers running past the quit. See above.
- Starting a site that is not set up: the setup checklist is what starts
things the first time.
- The theme (#560), the last of #559's rows.

## 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.

---

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

- **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 #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'.

</details>

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

**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 #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.

</details>

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

- `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`.

</details>

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

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.

</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