The channel number started wherever the preceding text ended: the row was a flex line whose first child was <Checkbox label={displayName}>, so minWidth: 42 reserved width for the number without being able to align it. The name then rendered a second time in the detail column, and the group header above the rows had the same duplicate.
The row is a grid now. What makes the column line up is that everything left of the number is fixed-width — a label-less Checkbox renders one fixed-size control — plus a fixed number track, because each row is its own grid container and an auto/max-content track resolves per row.
No fixed track bounds the value, and the code says so rather than pretending otherwise: AutoTuneNumberAllocator.Allocate emits int.ToString() from a StartingNumber that PreviewAutoTuneChannelsHandler only floors at 1, and a per-proposal override is free text checked against Channel.NumberValidator (^[0-9]+(\.[0-9]{1,2})?$). So the cell clips, with the full value on hover. That is a real trade — the flex box it replaces grew instead of truncating — so the title fallback is part of the fix and is pinned by a test.
Checkbox gains hideLabel, mirroring the prop Switch in the same file already has. label still supplies the accessible name, so every existing getByRole('checkbox', { name }) assertion is untouched.
Neither guard §3b mandates was present, and their triggers differ:
runSearch is reachable twice concurrently — a form submit fanning out over DEFAULT_SEARCH_KINDS, a kind-chip click issuing one request. A superseded response settling later repainted the list with every kind under an active "Movies" chip, wiped a newer successful result with a superseded search's error, or cleared searching while the other request was still in flight.
submit is unmount-reachable mid-request: its Add button is disabled while adding, but Dialog closes on Escape, backdrop click and the header close button, none of which consult adding.
onAdded/onClose are deliberately not guarded — they belong to the parent, which owns its own guarding, and dropping the list reload after a write that succeeded would leave the collection stale.
Verification
Every clause ships with a test that reddens when that clause alone is removed — 14/14, each mutation applied to pristine source, asserted to match exactly once, read back, then re-run.
Two of those were green until the tests were strengthened, and both are the kind of green that means nothing:
The two submit guards masked each other — removing either alone left the whole suite green, because the survivor still read the ref. That test counts the reads now.
The failed-search test asserted queryByText('Unable to search library'), which messageFromCollectionError only produces for a non-Error throw — null-vs-null, passing with the guard removed. It matches role="alert" instead, verified to fail on its own with the test's other assertions removed.
Web suite 115 files / 1206 tests green; npm run typecheck and npm run lint clean; repo guard suite scripts/tests 956 passed / 2 skipped.
Live-E2E not required — docs/e2e-local.md scopes it to API write-path handler changes; this is pure-SPA and read-only. No .cs files touched.
Review
Independent cold adversarial review (worktree-isolated, review-only brief), three rounds: BLOCKED → BLOCKED (narrowly) → MERGEABLE. Round 1 caught a false mutation-proof claim in the commit message plus a genuinely unpinned catch-path guard whose removal reintroduced a worse bug than #740 itself; round 2 caught the vacuous error assertion and two unpinned style clauses. Both rounds' findings were taken in full, none argued away.
Docs
docs/spa-conventions.md §3b — the AddItemsDialog caveat naming #740 as outstanding is replaced by the shipped rule, with the two guards' different triggers stated separately (concurrent reachability vs unmount reachability) and the own-state-only rule spelled out.
Deferred
#830 — a failed add is silently swallowed when the dialog is closed mid-request. Pre-existing; the guard here makes the drop deliberate rather than accidental, but fixing the UX needs a parent-owned surface or a change to the shared Dialog, which is its own blast radius.
Design system
The prototypes already had a label-less proposal checkbox — the SPA had drifted from them — so the mirrored delta is the number's right-align. The prototypes keep their flex row; converting them to the grid is a layout change to a mockup, not part of this fix.
fixes #721
fixes #740
Two `frontend` fixes swept in one session — disjoint files, one shared component change.
## #721 — Auto-Tune proposal rows
The channel number started wherever the preceding text ended: the row was a flex line whose first child was `<Checkbox label={displayName}>`, so `minWidth: 42` reserved width for the number without being able to align it. The name then rendered a second time in the detail column, and the group header above the rows had the same duplicate.
The row is a grid now. What makes the column line up is that everything **left of** the number is fixed-width — a label-less `Checkbox` renders one fixed-size control — plus a fixed number track, because each row is its own grid container and an `auto`/`max-content` track resolves per row.
**No fixed track bounds the value**, and the code says so rather than pretending otherwise: `AutoTuneNumberAllocator.Allocate` emits `int.ToString()` from a `StartingNumber` that `PreviewAutoTuneChannelsHandler` only floors at 1, and a per-proposal override is free text checked against `Channel.NumberValidator` (`^[0-9]+(\.[0-9]{1,2})?$`). So the cell clips, with the full value on hover. That is a real trade — the flex box it replaces *grew* instead of truncating — so the `title` fallback is part of the fix and is pinned by a test.
`Checkbox` gains `hideLabel`, mirroring the prop `Switch` in the same file already has. `label` still supplies the accessible name, so every existing `getByRole('checkbox', { name })` assertion is untouched.
## #740 — AddItemsDialog async guards
Neither guard §3b mandates was present, and **their triggers differ**:
- `runSearch` is reachable **twice concurrently** — a form submit fanning out over `DEFAULT_SEARCH_KINDS`, a kind-chip click issuing one request. A superseded response settling later repainted the list with every kind under an active "Movies" chip, wiped a newer successful result with a superseded search's *error*, or cleared `searching` while the other request was still in flight.
- `submit` is **unmount-reachable** mid-request: its Add button is disabled while adding, but `Dialog` closes on Escape, backdrop click and the header close button, none of which consult `adding`.
`onAdded`/`onClose` are deliberately **not** guarded — they belong to the parent, which owns its own guarding, and dropping the list reload after a write that succeeded would leave the collection stale.
## Verification
Every clause ships with a test that reddens when **that clause alone** is removed — 14/14, each mutation applied to pristine source, asserted to match exactly once, read back, then re-run.
Two of those were green until the tests were strengthened, and both are the kind of green that means nothing:
- The two `submit` guards **masked each other** — removing either alone left the whole suite green, because the survivor still read the ref. That test counts the reads now.
- The failed-search test asserted `queryByText('Unable to search library')`, which `messageFromCollectionError` only produces for a **non-`Error`** throw — null-vs-null, passing with the guard removed. It matches `role="alert"` instead, verified to fail on its own with the test's other assertions removed.
Web suite 115 files / 1206 tests green; `npm run typecheck` and `npm run lint` clean; repo guard suite `scripts/tests` 956 passed / 2 skipped.
**Live-E2E not required** — `docs/e2e-local.md` scopes it to API write-path handler changes; this is pure-SPA and read-only. No `.cs` files touched.
## Review
Independent cold adversarial review (worktree-isolated, review-only brief), three rounds: **BLOCKED** → **BLOCKED (narrowly)** → **MERGEABLE**. Round 1 caught a false mutation-proof claim in the commit message plus a genuinely unpinned catch-path guard whose removal reintroduced a *worse* bug than #740 itself; round 2 caught the vacuous error assertion and two unpinned style clauses. Both rounds' findings were taken in full, none argued away.
## Docs
`docs/spa-conventions.md` §3b — the `AddItemsDialog` caveat naming #740 as outstanding is replaced by the shipped rule, with the two guards' **different triggers** stated separately (concurrent reachability vs unmount reachability) and the own-state-only rule spelled out.
## Deferred
**#830** — a failed add is silently swallowed when the dialog is closed mid-request. Pre-existing; the guard here makes the drop deliberate rather than accidental, but fixing the UX needs a parent-owned surface or a change to the shared `Dialog`, which is its own blast radius.
## Design system
The prototypes already had a label-less proposal checkbox — the SPA had drifted from them — so the mirrored delta is the number's right-align. The prototypes keep their flex row; converting them to the grid is a layout change to a mockup, not part of this fix.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
#721 — the Auto-Tune proposal list was hard to scan because the channel number started
wherever the preceding text ended: the row was a flex line whose first child was a
`<Checkbox label={displayName}>`, so `minWidth: 42` reserved width for the number without
being able to align it. The same name then rendered a second time in the detail column, and
the group header above the rows had the same duplicate (`Select all TV Shows` immediately
followed by `TV Shows`).
The row is now a grid. What makes the column line up is that everything to the LEFT of the
number is fixed-width — a label-less Checkbox renders one fixed-size control — and the number
track is fixed rather than content-sized, because each row is its own grid container and an
`auto`/`max-content` track would resolve per row.
No fixed track is a true upper bound on the value: `AutoTuneNumberAllocator.Allocate` emits
`int.ToString()` from a `StartingNumber` that `PreviewAutoTuneChannelsHandler` only floors at
1, and a per-proposal override is free text checked against `Channel.NumberValidator`
(`^[0-9]+(\.[0-9]{1,2})?$`) — unbounded integer part plus up to two decimals. So the cell
clips: an extreme value degrades to an ellipsis with the full value on hover instead of
overflowing into the checkbox beside it. That is a real trade — the flex box it replaces grew
instead of truncating — so the `title` fallback is part of the fix, not decoration, and is
pinned. The number is right-aligned and tabular so 2-, 3- and 4-digit numbers agree on their
last digit.
`Checkbox` gains `hideLabel`, mirroring the prop `Switch` in the same file already has: the
name lives in its own column and the control keeps `label` as its accessible name, so the
existing `getByRole('checkbox', { name })` assertions are untouched.
#740 — `AddItemsDialog` had neither of the two guards spa-conventions.md §3b mandates for
asynchronous pickers, and their triggers differ. `runSearch` is reachable twice concurrently
(a form submit fanning out over DEFAULT_SEARCH_KINDS, a kind-chip click issuing one request),
so a superseded response could settle after a newer one — repainting the list with every kind
under an active "Movies" chip, wiping a newer successful result with a superseded search's
error, or clearing `searching` while the other request was still in flight. Separately, the
dialog is unmount-reachable mid-request through Escape, a backdrop click and the header close
button, none of which consult the in-flight flag — so `submit`'s post-await writes to its own
state need the is-mounted guard even though its Add button is disabled and nothing can
supersede it. `onAdded`/`onClose` are deliberately NOT guarded: they belong to the parent,
which owns its own guarding, and dropping the list reload after a write that succeeded would
leave the collection stale.
Every clause listed below ships with a test that reddens when THAT clause alone is removed.
Verified by applying each mutation to the pristine source, asserting it matched exactly once,
reading it back, and re-running — 14/14 red:
runSearch success guard -> repaint test submit catch guard -> failed-add test
runSearch catch guard -> failed-search test submit finally guard -> failed-add test
runSearch finally guard -> spinner test row grid track list -> #721 test
isCurrent mounted half -> unmount test textAlign / overflow -> #721 test
isCurrent seq half -> 3 search tests whiteSpace / title -> #721 test
row + group-header hideLabel -> #721 test
Two of those needed the test strengthened before they were pinned at all, and both are the
kind of green that means nothing:
- The two `submit` guards MASKED each other — removing either alone left the suite green,
because the survivor still read the ref. That test now counts the reads rather than
asserting merely that one of them saw `false`.
- The failed-search test asserted `queryByText('Unable to search library')`, which
`messageFromCollectionError` only produces for a non-`Error` throw — so it was null-vs-null
and passed with the guard removed. It matches `role="alert"` instead, verified to fail on
its own with the other assertions in that test removed.
The unmount tests record what the component read out of the is-mounted ref because an
unmounted tree renders nothing either way and no DOM assertion can tell "the guard stopped it"
from "React discarded it"; same construction as schedules/pickers.test.tsx.
The design-system prototypes already had a label-less proposal checkbox — the SPA had drifted
from them — so the mirrored delta is the number's right-align. The prototypes keep their flex
row; converting them to the grid is a layout change to a mockup, not part of this fix.
Verification: web suite 115 files / 1206 tests green; `npm run typecheck` and `npm run lint`
clean; repo guard suite `scripts/tests` 956 passed / 2 skipped. Live-E2E not required —
`docs/e2e-local.md` scopes it to API write-path handler changes and this is pure-SPA and
read-only. No .cs files touched.
fixes#721fixes#740
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Independent cold adversarial review, 3 rounds (BLOCKED, BLOCKED-narrowly, MERGEABLE). Round 1 found a false mutation-proof claim and an unpinned catch-path guard whose removal reintroduced a worse defect than #740; round 2 found a vacuous error assertion and 2 unpinned style clauses. All taken in full. Final: 14/14 clauses redden individually, 0 unpinned, reproduced independently by the reviewer. Verdict posted by the AUTHOR on the reviewer's behalf.
Review-verdict: MERGEABLE @ 4d33462
Independent cold adversarial review, 3 rounds (BLOCKED, BLOCKED-narrowly, MERGEABLE). Round 1 found a false mutation-proof claim and an unpinned catch-path guard whose removal reintroduced a worse defect than #740; round 2 found a vacuous error assertion and 2 unpinned style clauses. All taken in full. Final: 14/14 clauses redden individually, 0 unpinned, reproduced independently by the reviewer. Verdict posted by the AUTHOR on the reviewer's behalf.
timothy
merged commit 7453dd3a82 into main2026-08-25 23:55:24 +02:00
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.
fixes #721
fixes #740
Two
frontendfixes swept in one session — disjoint files, one shared component change.#721 — Auto-Tune proposal rows
The channel number started wherever the preceding text ended: the row was a flex line whose first child was
<Checkbox label={displayName}>, sominWidth: 42reserved width for the number without being able to align it. The name then rendered a second time in the detail column, and the group header above the rows had the same duplicate.The row is a grid now. What makes the column line up is that everything left of the number is fixed-width — a label-less
Checkboxrenders one fixed-size control — plus a fixed number track, because each row is its own grid container and anauto/max-contenttrack resolves per row.No fixed track bounds the value, and the code says so rather than pretending otherwise:
AutoTuneNumberAllocator.Allocateemitsint.ToString()from aStartingNumberthatPreviewAutoTuneChannelsHandleronly floors at 1, and a per-proposal override is free text checked againstChannel.NumberValidator(^[0-9]+(\.[0-9]{1,2})?$). So the cell clips, with the full value on hover. That is a real trade — the flex box it replaces grew instead of truncating — so thetitlefallback is part of the fix and is pinned by a test.CheckboxgainshideLabel, mirroring the propSwitchin the same file already has.labelstill supplies the accessible name, so every existinggetByRole('checkbox', { name })assertion is untouched.#740 — AddItemsDialog async guards
Neither guard §3b mandates was present, and their triggers differ:
runSearchis reachable twice concurrently — a form submit fanning out overDEFAULT_SEARCH_KINDS, a kind-chip click issuing one request. A superseded response settling later repainted the list with every kind under an active "Movies" chip, wiped a newer successful result with a superseded search's error, or clearedsearchingwhile the other request was still in flight.submitis unmount-reachable mid-request: its Add button is disabled while adding, butDialogcloses on Escape, backdrop click and the header close button, none of which consultadding.onAdded/onCloseare deliberately not guarded — they belong to the parent, which owns its own guarding, and dropping the list reload after a write that succeeded would leave the collection stale.Verification
Every clause ships with a test that reddens when that clause alone is removed — 14/14, each mutation applied to pristine source, asserted to match exactly once, read back, then re-run.
Two of those were green until the tests were strengthened, and both are the kind of green that means nothing:
submitguards masked each other — removing either alone left the whole suite green, because the survivor still read the ref. That test counts the reads now.queryByText('Unable to search library'), whichmessageFromCollectionErroronly produces for a non-Errorthrow — null-vs-null, passing with the guard removed. It matchesrole="alert"instead, verified to fail on its own with the test's other assertions removed.Web suite 115 files / 1206 tests green;
npm run typecheckandnpm run lintclean; repo guard suitescripts/tests956 passed / 2 skipped.Live-E2E not required —
docs/e2e-local.mdscopes it to API write-path handler changes; this is pure-SPA and read-only. No.csfiles touched.Review
Independent cold adversarial review (worktree-isolated, review-only brief), three rounds: BLOCKED → BLOCKED (narrowly) → MERGEABLE. Round 1 caught a false mutation-proof claim in the commit message plus a genuinely unpinned catch-path guard whose removal reintroduced a worse bug than #740 itself; round 2 caught the vacuous error assertion and two unpinned style clauses. Both rounds' findings were taken in full, none argued away.
Docs
docs/spa-conventions.md§3b — theAddItemsDialogcaveat naming #740 as outstanding is replaced by the shipped rule, with the two guards' different triggers stated separately (concurrent reachability vs unmount reachability) and the own-state-only rule spelled out.Deferred
#830 — a failed add is silently swallowed when the dialog is closed mid-request. Pre-existing; the guard here makes the drop deliberate rather than accidental, but fixing the UX needs a parent-owned surface or a change to the shared
Dialog, which is its own blast radius.Design system
The prototypes already had a label-less proposal checkbox — the SPA had drifted from them — so the mirrored delta is the number's right-align. The prototypes keep their flex row; converting them to the grid is a layout change to a mockup, not part of this fix.
🤖 Generated with Claude Code
Review-verdict: MERGEABLE @
4d33462Independent cold adversarial review, 3 rounds (BLOCKED, BLOCKED-narrowly, MERGEABLE). Round 1 found a false mutation-proof claim and an unpinned catch-path guard whose removal reintroduced a worse defect than #740; round 2 found a vacuous error assertion and 2 unpinned style clauses. All taken in full. Final: 14/14 clauses redden individually, 0 unpinned, reproduced independently by the reviewer. Verdict posted by the AUTHOR on the reviewer's behalf.