first-run-wizard dismissed itself as a step in its own flow — handleFinish set finished, closed the dialog and cleared active —
so a library appearing by any other route left the modal up over an app
that was already set up, intercepting every pointer event (#175).
What changed
One subscription. AddLibrary emits LibraryAdded whoever calls it,
so the wizard now waits on the state it exists to wait for:
EventsOn(Events.LibraryAdded, …) in connectedCallback, cancelled
in disconnectedCallback.
It is registered before the initial GetAllLibrariesWithTrackCounts,
and the result of that read is discarded if the event landed while it
was in flight — otherwise a stale empty list raises the wizard a
moment after the library arrived.
Both routes out end in one dismiss(), which sets finishedfirst
because preventClose cancels the hide otherwise.
On the Direction's first question
#175 asks whether a user can reach this before it asks what to do, and
then says the dismissal should follow the library existing either
way. That is what made it takeable without settling the question: the
answer moves the priority, not the diff. What the code says, for
whoever does settle it:
There are exactly two frontend callers of AddLibrary — this wizard
and Settings' Add Library.
Measured on the real app while the wizard is up, document.elementFromPoint(550, 400) is FIRST-RUN-WIZARD; the wa-dialog is a native modal, which is what makes Settings
unreachable.
So on desktop it reads as robustness rather than a lockout. Android's
storage-access flow is the case that needs a device, and nothing here
depends on it.
Verification
make ui-test — 104 files, 1135 tests, green. frontend/test/components/first-run-wizard.test.ts is new: shows on a
fresh install; stays hidden when a library already exists; dismisses
on LibraryAdded; does not raise itself when the event lands during
the initial read; still dismisses through its own Get Started
button (that last one pinning the handleFinish → dismiss()
refactor).
Planted, not read. With the component reverted the two
new-behaviour cases fail (expected true to be false) and the three
pinning existing behaviour pass.
The real app, on a fresh YJ_HOME (make dev-headless-fresh,
port checked free first), driven with this issue's own reproduction — __yjEvents.call('library.Library.AddLibrary', [fixture library]).
Before: dialogRendered: true, nativeDialogOpen: true, elementFromPoint → FIRST-RUN-WIZARD, all unchanged after the call.
After: the dialog is gone and that point is HOME-VIEW. Two builds
from this tree, one with the source change stashed.
make e2e (chromium, 246 passed) against make dev-headless SEED=default, as a regression check only: every
e2e sandbox has a library by construction — global-setup.ts refuses
to start without one — so no spec there can reach the wizard's active
state. The fresh-sandbox probe above is what covers it. WebKit is
CI's tier, not one this machine can run.
npx tsc --noEmit clean; check-css-literals and check-css-nesting clean.
Deliberately not done
No Go, no .sql, no .templ, no bound signature, so make lint, make test, make generate and make bindings were not run.
CLAUDE.md untouched. It says of this component only that it is
eagerly mounted because it is the first paint, which is still true.
Only LibraryAdded is subscribed.LibraryRemoved taking the
last library away does not raise the wizard mid-session; that is the
existing behaviour and not what #175 reports.
No new issues filed — nothing was found that was not this one.
## The issue
`first-run-wizard` dismissed itself as a step in its **own** flow —
`handleFinish` set `finished`, closed the dialog and cleared `active` —
so a library appearing by any other route left the modal up over an app
that was already set up, intercepting every pointer event (#175).
## What changed
One subscription. `AddLibrary` emits `LibraryAdded` whoever calls it,
so the wizard now waits on the state it exists to wait for:
- `EventsOn(Events.LibraryAdded, …)` in `connectedCallback`, cancelled
in `disconnectedCallback`.
- It is registered **before** the initial `GetAllLibrariesWithTrackCounts`,
and the result of that read is discarded if the event landed while it
was in flight — otherwise a stale empty list raises the wizard a
moment after the library arrived.
- Both routes out end in one `dismiss()`, which sets `finished` *first*
because `preventClose` cancels the hide otherwise.
## On the Direction's first question
#175 asks whether a user can reach this before it asks what to do, and
then says the dismissal should follow the library existing **either
way**. That is what made it takeable without settling the question: the
answer moves the priority, not the diff. What the code says, for
whoever does settle it:
- There are exactly two frontend callers of `AddLibrary` — this wizard
and Settings' *Add Library*.
- Measured on the real app while the wizard is up,
`document.elementFromPoint(550, 400)` is `FIRST-RUN-WIZARD`; the
`wa-dialog` is a native modal, which is what makes Settings
unreachable.
So on desktop it reads as robustness rather than a lockout. Android's
storage-access flow is the case that needs a device, and nothing here
depends on it.
## Verification
- **`make ui-test`** — 104 files, 1135 tests, green.
`frontend/test/components/first-run-wizard.test.ts` is new: shows on a
fresh install; stays hidden when a library already exists; dismisses
on `LibraryAdded`; does not raise itself when the event lands during
the initial read; still dismisses through its own *Get Started*
button (that last one pinning the `handleFinish` → `dismiss()`
refactor).
- **Planted, not read.** With the component reverted the two
new-behaviour cases fail (`expected true to be false`) and the three
pinning existing behaviour pass.
- **The real app, on a fresh `YJ_HOME`** (`make dev-headless-fresh`,
port checked free first), driven with this issue's own reproduction —
`__yjEvents.call('library.Library.AddLibrary', [fixture library])`.
Before: `dialogRendered: true`, `nativeDialogOpen: true`,
`elementFromPoint → FIRST-RUN-WIZARD`, all unchanged after the call.
After: the dialog is gone and that point is `HOME-VIEW`. Two builds
from this tree, one with the source change stashed.
- **`make e2e`** (chromium, 246 passed) against
`make dev-headless SEED=default`, as a regression check only: every
e2e sandbox has a library by construction — `global-setup.ts` refuses
to start without one — so no spec there can reach the wizard's active
state. The fresh-sandbox probe above is what covers it. WebKit is
CI's tier, not one this machine can run.
- `npx tsc --noEmit` clean; `check-css-literals` and
`check-css-nesting` clean.
## Deliberately not done
- **No Go, no `.sql`, no `.templ`, no bound signature**, so `make lint`,
`make test`, `make generate` and `make bindings` were not run.
- **`CLAUDE.md` untouched.** It says of this component only that it is
eagerly mounted because it is the first paint, which is still true.
- **Only `LibraryAdded` is subscribed.** `LibraryRemoved` taking the
last library away does not raise the wizard mid-session; that is the
existing behaviour and not what #175 reports.
- No new issues filed — nothing was found that was not this one.
Closes #175
The first-run wizard dismissed itself as a step in its own flow, so a
library appearing by any other route left a full-screen modal up over
an app that was already set up — intercepting every pointer event,
which is also what keeps Settings out of reach while it is there.
AddLibrary emits LibraryAdded whoever calls it, so one subscription
makes the dismissal follow the state the wizard exists to wait for
rather than the button being pressed. It is registered before the
initial read, or a library arriving while that call is in flight is
answered with a stale empty list; both routes out now end in one
dismiss(), which sets `finished` before asking the dialog to close
because preventClose cancels the hide otherwise.
Closes#175
CI green on b9e60bd — run 601, both required jobs.
check (job 29915): commit messages, lint, test, frontend typecheck,
CSS literals, the component and store suite, bindings, skill-check —
every step success.
e2e (job 29916): chromium and webkit both success. Worth naming
because the WebKit step is the one that gets skipped when chromium
fails, and it is the only signal for the renderer that actually
ships.
Not merged, per the run's instructions.
**CI green on `b9e60bd`** — run 601, both required jobs.
- `check` (job 29915): commit messages, lint, test, frontend typecheck,
CSS literals, the component and store suite, bindings, skill-check —
every step success.
- `e2e` (job 29916): **chromium and webkit both success.** Worth naming
because the WebKit step is the one that gets skipped when chromium
fails, and it is the only signal for the renderer that actually
ships.
Not merged, per the run's instructions.
Blocking a user prevents them from interacting with repositories, such as opening or commenting on pull requests or issues. Learn more about blocking a user.
The issue
first-run-wizarddismissed itself as a step in its own flow —handleFinishsetfinished, closed the dialog and clearedactive—so a library appearing by any other route left the modal up over an app
that was already set up, intercepting every pointer event (#175).
What changed
One subscription.
AddLibraryemitsLibraryAddedwhoever calls it,so the wizard now waits on the state it exists to wait for:
EventsOn(Events.LibraryAdded, …)inconnectedCallback, cancelledin
disconnectedCallback.GetAllLibrariesWithTrackCounts,and the result of that read is discarded if the event landed while it
was in flight — otherwise a stale empty list raises the wizard a
moment after the library arrived.
dismiss(), which setsfinishedfirstbecause
preventClosecancels the hide otherwise.On the Direction's first question
#175 asks whether a user can reach this before it asks what to do, and
then says the dismissal should follow the library existing either
way. That is what made it takeable without settling the question: the
answer moves the priority, not the diff. What the code says, for
whoever does settle it:
AddLibrary— this wizardand Settings' Add Library.
document.elementFromPoint(550, 400)isFIRST-RUN-WIZARD; thewa-dialogis a native modal, which is what makes Settingsunreachable.
So on desktop it reads as robustness rather than a lockout. Android's
storage-access flow is the case that needs a device, and nothing here
depends on it.
Verification
make ui-test— 104 files, 1135 tests, green.frontend/test/components/first-run-wizard.test.tsis new: shows on afresh install; stays hidden when a library already exists; dismisses
on
LibraryAdded; does not raise itself when the event lands duringthe initial read; still dismisses through its own Get Started
button (that last one pinning the
handleFinish→dismiss()refactor).
new-behaviour cases fail (
expected true to be false) and the threepinning existing behaviour pass.
YJ_HOME(make dev-headless-fresh,port checked free first), driven with this issue's own reproduction —
__yjEvents.call('library.Library.AddLibrary', [fixture library]).Before:
dialogRendered: true,nativeDialogOpen: true,elementFromPoint → FIRST-RUN-WIZARD, all unchanged after the call.After: the dialog is gone and that point is
HOME-VIEW. Two buildsfrom this tree, one with the source change stashed.
make e2e(chromium, 246 passed) againstmake dev-headless SEED=default, as a regression check only: everye2e sandbox has a library by construction —
global-setup.tsrefusesto start without one — so no spec there can reach the wizard's active
state. The fresh-sandbox probe above is what covers it. WebKit is
CI's tier, not one this machine can run.
npx tsc --noEmitclean;check-css-literalsandcheck-css-nestingclean.Deliberately not done
.sql, no.templ, no bound signature, somake lint,make test,make generateandmake bindingswere not run.CLAUDE.mduntouched. It says of this component only that it iseagerly mounted because it is the first paint, which is still true.
LibraryAddedis subscribed.LibraryRemovedtaking thelast library away does not raise the wizard mid-session; that is the
existing behaviour and not what #175 reports.
Closes #175
CI green on
b9e60bd— run 601, both required jobs.check(job 29915): commit messages, lint, test, frontend typecheck,CSS literals, the component and store suite, bindings, skill-check —
every step success.
e2e(job 29916): chromium and webkit both success. Worth namingbecause the WebKit step is the one that gets skipped when chromium
fails, and it is the only signal for the renderer that actually
ships.
Not merged, per the run's instructions.