feat(desktop): Settings › Experimental, a switch per experiment and the empty Inbox's empty state between them (experimental 3/3)
Merged
DA
6d ago+747+747+012345678901234567890123456789-123-123-012345678901234567890123456789
Summary
PR 3 of Settings › Experimental (docs/proposals/experimental-settings.md, decided by Dean on 2026-10-14). This is the page, on the storage (#2692) and gates (#2699) beneath it.
The page
- Place. Experimental is a Settings page after This Mac, so About and Keyboard shortcuts stay last. It sits under lucide's flask and is counted by
settings:view_openunder its own name (PRODUCT_SETTINGS_VIEW.EXPERIMENTAL, bridged by the totalSETTINGS_VIEW_COUNTED_AS). - No intro text (Dean's change). The page is its title, then one
SwitchRowper entry inEXPERIMENTS: the name, its one line, and "Send feedback". - The switch writes the account's choice through
ACT_KIND.EXPERIMENT_SETat once, with no confirm. A refusal is said under the row and the switch stays. - Empty state. With no experiment standing, the page draws the empty Inbox's own
EmptyState(Dean's change): the flask glyph, the title "No experiments right now", and "New features show up here to try before they ship."EmptyStategains an optionaltitle, drawn asConversationEmptyState, the component beneath it, draws its own. - Signed out, the page says "Sign in to try experiments."
Search
- Each row is found by Settings search and ⌘K's settings rows: by its name, its line, and "experiment preview beta labs early try".
- Its anchor is
EXPERIMENT_ANCHOR(experiment-factory), kept apart from the bare key a gated feature's command is likely to use on the Keyboard shortcuts page. A test holds every search id unique.
Feedback (decision 7)
- "Send feedback" opens the window's feedback dialog about that experiment through
useOpenFeedback, a context likeuseOpenSettings. The dialog is titled "Feedback on Factory". FeedbackSubmission.experimentcarries the experiment's key.@luke/featuressits below the settings vocabulary, so both readers (feedbackSubmissionandapps/web/server/feedback.mjs) hold it to a key's shape and refuse prose. The mail's subject reads "Luke feedback on the factory experiment".feedback.d.mtstypes the plain-JavaScript endpoint for its first test, and is registered as a knip entry beside it.
Guides
- The renderer AGENTS.md gains an "Experiments" paragraph (approved by Dean): read a gate through
useExperiment, tag a list entry, what "off" means, and how graduation deletes the gates. - The proposal gains an "As built" section.
Evidence
- Platform-independent checks:
pnpm check:affectedexit 0 on the restacked top of the stack. - New tests (+12):
apps/desktop/src/renderer/settings/experimental-page.test.ts: 4. The switch is on as chosen and writes the pressed choice; Send feedback opens the dialog about its experiment; the empty state draws its title and line; the signed-out note.apps/desktop/src/renderer/settings-search.test.ts: 2. An experiment is found by its name, its line, and "Experimental", "beta" and "labs" while signed in, and not while signed out; no two search ids are equal.apps/desktop/src/renderer/feedback-dialog.test.ts: 2. Opened about Factory, the dialog is titled for it and its send carriesexperiment: "factory". A draft kept from one experiment's row starts fresh when the dialog opens about another place; this one was added after Devin's review.packages/features/src/feedback/submission.test.ts: 2. The key is kept; prose and non-strings are refused.apps/web/tests/feedback-endpoint.test.ts: 2. The mail service is scripted at its HTTP boundary. The subject names the experiment; a malformed experiment is refused and nothing is mailed.
- Updated: the approved page order pinned in
desktop-settings.test.tsandcommand-palette.test.tsnow lists Experimental after This Mac.
CI could not verify the Mac. This is a desktop UI change. CI is Linux only, and the release rehearsal on a tag is the only Mac gate. I had no Mac in this workspace to run the app on. The design show (docs/proposals/experimental-settings/index.html) draws this page from the same renderer components and stylesheet, in light and dark, but it is not the running app.
Invariants
-
./scripts/check.shgreen. (CI) - JSON Schema goldens unchanged.
- Every behavior change has, in this PR, the test that fails without it; no change-detector test added.
- Gateway envelope goldens unchanged.
- No new
astype assertion outside the allowlisted files. (Oneas conston a test fixture.) - No
Effect.runPromise/runSync/runForkoutside the runtime edges. - No new
setTimeout/setInterval/new Promise/AbortController/fs.watch. - No Effect, Stream, or Layer written as a statement and dropped.
- Test count delta: +12 (listed above).
- Each hand-rolled file the PR replaces is deleted. (None.)
- Every
CLAUDE.md/AGENTS.mdsentence naming a changed part is edited. The renderer guide gains "Experiments";empty-state.tsx's header names the new place it draws; root AGENTS.md is untouched. -
PRIVACY.mdunchanged. A feedback note now says which experiment it is about, as its title already shows the writer; nothing else rides along. - Package
package.jsondeps match. - Barrel/door rule.
Notes
- Blockers or follow-up verification: a
verify.sh-style pass on a Mac before the next release (LUKE-159's manual pass). Factory's view (#2608) stacks on this PR, the only way to turn Factory on in the app.
🤖 Generated with Claude Code