You are a Sonnet worker on the Bookshelf Android app (~/bookshelf). Read `docs/SPEC.md` first — it is the authoritative product contract and it wins over anything you infer from the code. Do not restate it, do not let it drift. ## Ground rules (violating these fails the wave) - Build ONLY with `./tasks/gw ` — never `./gradlew`. Another worker shares this Gradle project dir and concurrent invocations clobber each other. `tasks/gw` is a flock-serialized wrapper. - Run builds in the FOREGROUND. Never background a Gradle build and end your turn saying you'll report later — `claude -p` kills background tasks and you will never report at all. Builds take up to 10 minutes; just wait. - You own EXACTLY these files: ui/components/ShelfPickerSheet.kt (new file, create it) ui/scan/ScanScreen.kt ui/scan/ScanViewModel.kt ui/detail/DetailScreen.kt ui/detail/DetailViewModel.kt data/prefs/SettingsStore.kt app/app/src/test/** (tests you add) (paths relative to app/app/src/main/java/org/modg/bookshelf/) Touch NOTHING else. Specifically forbidden: any build file (`app/build.gradle.kts`, `gradle/libs.versions.toml`), `data/metadata/**` (the ORCHESTRATOR is editing that right now, in this same working tree), `ui/locations/**`, `ui/library/**`, `ui/nav/**`, `ui/settings/**`, `ui/setup/**`, `AppContainer.kt`, and every file in `ui/components/` EXCEPT the new `ShelfPickerSheet.kt` you create. - Do not change any public composable signature. `ui/nav/BookshelfNavHost.kt` calls these screens and you may not edit it. - `AppContainer.settingsStore` is already a public val — you do NOT need to change AppContainer to reach it. ## Background: what the user actually reported They are shelving books a box at a time, scanning a run of books that all belong on the SAME shelf. Two complaints: 1. The shelf dropdown lists every bookcase×shelf pair in one flat menu. With more than a couple of bookcases that is unusable. 2. Re-picking the same shelf for every book in the box is tedious. ## Task 1 — a grouped shelf picker, replacing the flat dropdown There are currently TWO near-duplicate flat pickers: - `ShelfPicker` in `ScanScreen.kt` (~line 477) - the location picker inside `DetailScreen.kt` (~line 273-310) Both build a `DropdownMenu` containing "Not shelved" followed by one flat item per (bookcase, shelf) pair, labelled "Bookcase • Shelf". Replace BOTH with ONE new shared composable in `ui/components/ShelfPickerSheet.kt`, presented as a Material 3 `ModalBottomSheet` rather than a dropdown. Contents, top to bottom: a. A "Recent" section — see task 2. Omit the whole section when there is no remembered shelf, or when the remembered shelf no longer exists. b. "Not shelved" as an always-available choice. c. One section per bookcase, in `position` order. The bookcase name is a section header (non-selectable — a bookcase is not a location a book can sit in; only shelves are). Its shelves are listed under it in `position` order, labelled with just the shelf label, since the header already gives the bookcase. Make the currently-selected shelf visually distinct. d. A bookcase with no shelves shows its header and an inline "No shelves yet" hint, so an empty bookcase does not look like a rendering bug. The sheet must scroll. Do NOT add a search field — the user explicitly deferred that; we will add it later if the grouping alone proves insufficient. Design language is in SPEC.md ("feels like books") and the existing components in `ui/components/` are your reference for type, spacing and the gold hairline rules. Reuse `GoldDivider` for section separation rather than inventing a new rule. ## Task 2 — remember the most recently used shelf Add a `LAST_SHELF_ID` key to `SettingsStore` (a nullable String preference, with a `Flow` reader, a setter, and a way to clear it), following the exact shape of the keys already there. Clear it in `clearAuth()` alongside the rest — signing out of a shared library should not leak the other account's shelf. Write it whenever a book's shelf is set to a non-null value: - `ScanViewModel` — on save, both from a metadata hit and from manual entry. - `DetailViewModel` — when the user changes a book's location. Setting a book to "Not shelved" must NOT overwrite the remembered shelf; it isn't a shelf, and clobbering the memory with it would defeat the whole feature. Surface it as the "Recent" section at the top of the picker, labelled with the full "Bookcase • Shelf" text (the section has no bookcase header to lean on). **Do NOT pre-select it.** The user considered and explicitly rejected pre-selection: the risk of silently mis-shelving a book, when the user forgets to change it, outweighs saving one tap. A new scan still starts with no shelf chosen; the remembered shelf is one tap away at the top of the sheet, and that is all. Note `ScanViewModel._selectedShelfId` already survives across saves within a single scanning session (it is deliberately not reset in `recordSave`). Keep that. What you are adding is persistence ACROSS sessions and screens. ## Task 3 — three small fixes in ScanScreen.kt 3a. In `SearchingSheet` (~line 268), delete the third Text, the one reading "Barcode read — you can lower the book." The user says "Searching…" plus the ISBN already carries it. Update the composable's KDoc, which currently justifies that line — do not leave a comment explaining code that is gone. 3b. `LookupFailedSheet` (~line 435) takes a `reason` parameter and never renders it. `ScanSheetState.LookupFailed(isbn13, reason)` already carries a diagnostic string, and `MetadataRepository` already builds it as e.g. "open library: network error; google books: http 429". It is currently dead — it reaches the UI and is dropped on the floor. Render it, below the existing explanatory paragraph, in `MaterialTheme.typography.bodySmall` and `onSurfaceVariant`. This is the only diagnostic channel we have from a real phone, so it must actually appear. Keep the existing headline and paragraph as they are — the reason is supplementary detail, not a replacement for plain-language copy. NOTE: the orchestrator is concurrently making those reason strings more specific. Do not depend on their exact wording — render whatever you are given, and do not parse, match on, or reformat the string. 3c. `ManualIsbnDialog` (~line 512) opens with an unfocused text field. Auto-focus it and raise the keyboard, via `FocusRequester` + `LaunchedEffect(Unit) { focusRequester.requestFocus() }`. Guard the call so it cannot throw if the node is not attached yet. ## Verify before you report (all in the FOREGROUND) ./tasks/gw assembleDebug ./tasks/gw testDebugUnitTest ./tasks/gw recordPaparazziDebug git status --porcelain - assembleDebug and testDebugUnitTest must exit 0. The test count is 138 today and must not go DOWN. Add real tests for the logic you introduce — at minimum the SettingsStore round-trip, that "Not shelved" does not overwrite the memory, and that a remembered shelf which no longer exists is not offered. Assertion-free tests are explicitly forbidden by SPEC's quality bar. - Add a Paparazzi snapshot of the new picker sheet with at least two bookcases, one of them empty, and a recent shelf present — that is the state the whole change exists for, and no one has ever trusted a worker's word on how this app looks. - Grep your build output for the string `always 'false'`. That Kotlin warning class silently blanked every book cover in this app for months by making a `when` branch dead code that still compiled. Zero hits on files you touched. - `git status --porcelain` will show files the orchestrator and the other worker are editing (`data/metadata/**`, `ui/locations/**`, `ui/library/**`, `res/drawable/**`). That is EXPECTED. Confirm only that no file outside your own list was changed BY YOU. Never revert or "fix" someone else's work. - Do not commit. The orchestrator commits after verifying. ## Report Finish with a plain report: what you changed per task, exact exit codes and test counts, what the new Paparazzi PNG shows, and anything you could NOT do. Do not claim success you did not verify — several previous workers on this project over-claimed and were caught.