Repository navigation
Keep Messages selected across its sidebar destinations - #848
Conversation
wesbillman
left a comment
There was a problem hiding this comment.
One change requested: keep keyboard focus on a stable element when retrying a failed Messages opening (inline). Automated source review by Star Lord via Wes’s account, head a9e753b26ec9fbadeda2213e7b8554667bd5472a, base 37dcd836fbcf1613ddd27cece5a99e04d601bd64. Source and both attached images inspected; no tests or app execution performed. The hosted snapshot has required CI failing on a 5-second WorkflowsPage.test.tsx:335 timeout; causality is unestablished, and native acceptance remains unverified.
| !current.readOnly && !requestedMessage && !requestedThread | ||
| // The bound request arrives after the composer's first render. | ||
| (navigation?.focus ?? | ||
| navigator?.snapshot().attempt.focus) !== "preserve" && |
There was a problem hiding this comment.
[P2] Hand off focus when the preserved opening is retried
This suppresses the recovery focus as well as the initial tab-to-page autofocus. If opening Messages from a header tab fails (for example, its channel window errors), App.tsx:253–267 replaces the page with “Retry navigation”. A keyboard user can focus and activate that button, but route.retry() calls navigation.retry(), which retains the original focus: "preserve" (controller.ts:263). The retry button is then unmounted; this new guard prevents the remounted composer from focusing, and the wide shell has no retry focus handoff, leaving focus on the document body rather than the tab or recovered content.
Keep suppression for activation from the persistent header tab, but explicitly hand off focus to a stable main/panel or tab when the retry control itself owns focus, before removing it. Add a failed-opening → keyboard Retry → successful recovery regression asserting that focus remains on a meaningful element; the existing live-update retry already has an explicit handoff. This follows DESIGN.md’s requirement to preserve focus restoration, without changing retry’s stored focus intent.
wesbillman
left a comment
There was a problem hiding this comment.
The existing retry-focus finding remains unresolved. No new findings in the test-only follow-up: bulk fixture input preserves both replacement cycles and their assertions without changing production code or timeouts.
Star Lord’s automated source review via Wes; head b3f8f8d8ced618f3046b80d042ec2f4d8187c559, base 37dcd836fbcf1613ddd27cece5a99e04d601bd64. No tests/app execution; hosted CI was still running and native acceptance is unverified. Dark attachment inspected; the light attachment could not be freshly fetched (HTTP 504), so its previously saved copy was inspected instead.
wpfleger96
left a comment
There was a problem hiding this comment.
🤖 Thanks for this. Blocking: one correctness issue at b3f8f8d8ced618f3046b80d042ec2f4d8187c559. It is the same issue as the open retry-focus thread (r4239536720), confirmed independently from the source.
Blocking: keyboard Retry after a failed Messages opening leaves focus on the page body
- The new guard at
ChannelsPage.tsx:1527-1533turns off composer mount focus for any opening whose intent ispreserve. Header-tab activation and the new Messages reselect both sendpreserve(PageNavigation.tsx:32-35). - If that opening fails,
App.tsx:253-274renders the "Retry navigation" alert. A keyboard user focuses Retry and activates it.route.retry()(navigation.ts:301-318) callsnavigation.retry(), which restarts with the storedpreserveintent (controller.ts:263). - On success the alert and its focused button unmount. The composer mounts without focus, and nothing else takes it, so focus falls to
document.body. Before this change the composer took focus on that path. - Fix: before the alert unmounts, move focus from the Retry control to a stable element (the Messages tab, the shell panel, or
#main-content), or give the retry its own focus intent. Add a test: a failed header opening, then keyboard Retry, then a successful recovery, asserting the final focused element. The existing reconnect test (tests/browser/navigation-repairs.spec.mjs:40-59) checks requests and composer visibility, not focus.
What holds
headerPageSelection(presentation.ts:74-93) maps only registered sidebar/default-placement pages to Messages, and only when Messages is a primary topbar page. Me, Settings, toolbar and independent header pages, and unregistered keys keep exact selection. The sidebar still receives the exactselected, so routes and sidebararia-current="page"are unchanged.onReselectis opt-in.PageNavigationis its only caller, so the otherTabsconsumers get no new behavior.Tabs.tsx:97-99compares against the current render's value, so activating an unselected tab never also reselects. Reselecting the exact Messages destination does nothing.- The panel id follows
panelKeyas an attribute, not a React key, so nothing remounts andaria-controls/aria-labelledbystay paired. Back/forward and channel-row openings keep thecontentintent, so the composer still autofocuses there. Join completion and the live-status Retry keep their explicitfocusRequesthandoffs. - The
WorkflowsPage.test.tsxfixture change still exercises both replacement cycles and every assertion.
Optional
- The compact browser journey (
tests/browser/page-placement.spec.mjs:213-232) checks the Messages area marker in More pages, then dismisses the menu without activating Messages. The RTL compact branch (AppShell.test.tsx:508-516) checks only the callback and immediate main focus against a mocked destination. Activating Messages from compact overflow and asserting the returned conversation, the draft, and the settled focus would cover the real path. - CI at this head: Browser journeys (webkit, 1/6) failed
me-workspace.spec.mjs:110(the activity popup button was never found). The previous head has the same production code and passed that shard, so it looks like a flake. The failed jobs are rerunning. - The description has no
buzz-review-completed.
wesbillman
left a comment
There was a problem hiding this comment.
No actionable changes requested. The retry-focus finding is addressed in source; the keyboard-preview follow-up introduces no new source-demonstrated defect.
Star Lord’s automated source review via Wes’s account; head db46f0202b7fe93dd867b28040a2252606d9341e, base 37dcd836fbcf1613ddd27cece5a99e04d601bd64. Both attachments inspected; no tests or app execution. Required CI still fails on two 5-second ChannelMembersDialog.test.tsx timeouts; causality is unestablished and native acceptance remains unverified.
wesbillman
left a comment
There was a problem hiding this comment.
🤖 No blockers at this head. The b9ad8df retry-focus fix is correct for the reported path. Header selection keeps Me, Settings, independent header pages and unregistered keys exact. onReselect compares against the current render's value, so activating an inactive tab never also fires reselect. The db46f02 popover fix handles a real stale hover-close race. Three non-blocking notes are inline; the first matters most at rebase time.
Merge criteria:
- Rebase required. The branch conflicts with main in
App.tsx,ChannelActivityPopover.tsx,ChannelsPage.tsxandWorkflowsPage.test.tsx. For the workflow test, take main's #911 version and drop b3f8f8d. - After rebasing, re-check header focus with a main-pane thread open (af303df). See the inline note on the composer guard.
- CI rerun after the rebase. The current red is JavaScript 2/2: two 5s
ChannelMembersDialoginvitation-paging timeouts in a file this PR doesn't touch.
Not verified here: the rebased tree, hosted CI and native acceptance.
Carl, an automated reviewer, commenting via Wes's GitHub account. Reviewed head db46f0202b7fe93dd867b28040a2252606d9341e (merge base 37dcd836) against main 55b71bf1.
| !current.readOnly && !requestedMessage && !requestedThread | ||
| // The bound request arrives after the composer's first render. | ||
| (navigation?.focus ?? | ||
| navigator?.snapshot().attempt.focus) !== "preserve" && |
There was a problem hiding this comment.
🤖 Rebase risk (non-blocking at this head): this guard only stops the composer from taking focus. Main's af303df added main-pane thread tabs, and PanelWorkspace focuses the selected tab in a layout effect whenever selected changes (PanelWorkspace.tsx ~178–192 on main). When a new history entry has no saved visit, useChannelTabState resets mainSelected to undefined (line ~290).
Likely failure: thread placement is set to main, or a thread is open in the main pane. The user opens Inbox, then presses Enter on Messages (or goes Me → Messages). The visit reset switches the selection from the thread back to the conversation, the workspace focuses that tab, and focus leaves the Messages header tab. That breaks the "header keeps focus" rule this PR documents.
I traced this in source only; I haven't run it on a rebased tree. Fix while resolving the conflict: also gate the workspace's selection-change focus on the opening's preserve intent, and add a page-placement case with a main-pane thread selected.
| onClick={(event) => { | ||
| const focused = | ||
| event.currentTarget === document.activeElement; | ||
| // Retry removes the control and can briefly unmount the shell. |
There was a problem hiding this comment.
🤖 Non-blocking: focus moves to #main-content before route.retry(), on the assumption that retry removes this button. If retry fails synchronously, the button stays and only focus moves. Example: an invalid address, where controller.ts:156-158 calls finish(... "invalid-target") inside start(). The alert re-renders and the keyboard user's focus is now on main instead of Retry. Before this PR, focus stayed on Retry.
Suggested fix: don't move focus up front. The layout effect above already handles the remount case. Let it move focus to main only once the button is actually gone, e.g. when focus has fallen to document.body or the button is no longer connected.
| if ( | ||
| selected !== "buzz.me/me" && | ||
| page && | ||
| (page.placement === undefined || page.placement === "sidebar") && |
There was a problem hiding this comment.
🤖 Non-blocking (plugin edge case): Page.placement is documented as applying to primary pages only (features/pages/service.ts:35). This check also reads it for non-primary pages. A plugin page registered as { primary: false, placement: "toolbar" } and opened from search therefore loses Messages selection. That contradicts the new shell-design.md text ("including non-primary pages reached through search or links"). Treating !page.primary as sidebar placement here (and adding a helper test) would match the contract.
Signed-off-by: Carl <32a2e2c9d428ee08902cab75d956da2c1d235a22d4766b0dd4138bf6e2e5db1d@buzz.block.builderlab.xyz>
Signed-off-by: Carl <32a2e2c9d428ee08902cab75d956da2c1d235a22d4766b0dd4138bf6e2e5db1d@buzz.block.builderlab.xyz>
Signed-off-by: Carl <32a2e2c9d428ee08902cab75d956da2c1d235a22d4766b0dd4138bf6e2e5db1d@buzz.block.builderlab.xyz>
Signed-off-by: Carl <32a2e2c9d428ee08902cab75d956da2c1d235a22d4766b0dd4138bf6e2e5db1d@buzz.block.builderlab.xyz>
Signed-off-by: Carl <32a2e2c9d428ee08902cab75d956da2c1d235a22d4766b0dd4138bf6e2e5db1d@buzz.block.builderlab.xyz>
Signed-off-by: Carl <32a2e2c9d428ee08902cab75d956da2c1d235a22d4766b0dd4138bf6e2e5db1d@buzz.block.builderlab.xyz>
Signed-off-by: Carl <32a2e2c9d428ee08902cab75d956da2c1d235a22d4766b0dd4138bf6e2e5db1d@buzz.block.builderlab.xyz>
db46f02 to
1eb3b3b
Compare
wesbillman
left a comment
There was a problem hiding this comment.
No new actionable findings; the follow-up addresses the earlier main-pane focus, retry-failure focus, and non-primary placement notes.
Automated source review by Star Lord via Wes’s account: head 1eb3b3be9c97a91a2fdd130dcbf7b5f2e1890ef2, base 53970a71f195df6201ef9983574515d621f77cbc. Source-only: no tests or app execution; the one-time CI snapshot was still in progress, and native/human acceptance remains unverified. This is a non-blocking comment, not approval.
kalvinnchau
left a comment
There was a problem hiding this comment.
🤖 approved after combined am + peon review of head 1eb3b3b. no actionable code findings. peon reports 745 unit tests and 62 chromium/webkit browser checks passed at this head; native acceptance remains unverified.
…nsole * origin/main: (115 commits) test(navigation): isolate the keyboard retry session gate (#981) fix(codex): log failed idle terminal clean instead of swallowing it (#878) feat(agents2): set the clicked conversation's session apart in agent tabs (#846) fix(channels): read the channel type from the first t tag (#958) feat(shell): allow sidebar collapse on all pages (#845) fix(mentions): select an exact member name on Space without waiting for the directory (#935) test(agents): complete local activity test binding (#980) Improve composer attachment previews and eager uploads (#948) fix(inbox): restore message editing in conversation details (#815) fix(codex): queue a differing model/effort pick as a fresh turn instead of steering (#889) fix(channels): focus a moved thread tab in the same task as its placement (#934) fix(messages): neutral thread placeholder when the viewer wrote the root (#930) test(workflows): set the condition value in one change event (#892) fix(agents2): reconnect Retry to native agent-list reload (#921) fix(github): stop claiming PR links the pane cannot show (#920) Keep Messages selected across its sidebar destinations (#848) perf(messages): stop the floating action bar's per-frame rAF loop while a row stays revealed (#938) test(inbox): wait for the exact-target reveal before resizing the detail (#888) perf(panels): measure the resize handle only when its width can change (#937) perf(channels): memoize channel labels and identity name scopes (#939) ... Signed-off-by: Duncan <dcfd242e557282d7a1e2cf2e6877522682f1e5c6156dc92ca7d90eaedd3b0f95@buzz.block.builderlab.xyz>
Opening a page placed in the Messages sidebar (`primary: true` and no `placement`), such as Drafts & sent or a third-party plugin page, cleared the Messages selection in the app dock and hid the conversation sidebar. The page showed bare. The dock move in #2626 deleted the headerPageSelection mapping from #848, and AppShell then showed the Messages sidebar only when the selected page was buzz.channels itself. Carry over the fix from draft #2747 (52445b5, 04baf9f, 03bd763), rebased onto main. navigationOwner in presentation.ts maps the selected destination to the app whose navigation lists it. Sidebar-placed pages belong to Messages, and every other destination owns itself. It decides by placement only and does not special-case any bundled page key. AppShell uses the owner for the dock selection, its tab panel and the Messages sidebar. Activating the selected Messages tab from one of its sidebar pages returns to Messages. A sidebar-placed page that registers its own `sidebar` still keeps Messages selected, and its sidebar replaces the conversation sidebar. Usage moved into Settings in #2742, so it is no longer a sidebar page. Drop it from the AppShell tests and the bundled navigation owner table. Cover the third-party case: a page with `primary: true` and no `placement` or `sidebar`. The AppShell table now asserts that the open page is the only current row in the sidebar, as well as the Messages selection, the sidebar and the return to Messages. The page-placement fixture plugin gains such a page. A browser journey opens it from the conversation sidebar and checks the same four things against real App wiring, including that Messages reopens the conversation. Both fail when the AppShell change is reverted. State the contract in plugin-architecture.md where `primary: true` is introduced. Non-primary pages opened from search or links, such as Repos, are unchanged. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Signed-off-by: mtoohey <mtoohey@squareup.com>
Opening a page placed in the Messages sidebar (`primary: true` and no `placement`), such as Drafts & sent or a third-party plugin page, cleared the Messages selection in the app dock and hid the conversation sidebar. The page showed bare. The dock move in #2626 deleted the headerPageSelection mapping from #848, and AppShell then showed the Messages sidebar only when the selected page was buzz.channels itself. Carry over the fix from draft #2747 (52445b5, 04baf9f, 03bd763), rebased onto main. navigationOwner in presentation.ts maps the selected destination to the app whose navigation lists it. Sidebar-placed pages belong to Messages, and every other destination owns itself. It decides by placement only and does not special-case any bundled page key. AppShell uses the owner for the dock selection, its tab panel and the Messages sidebar. Activating the selected Messages tab from one of its sidebar pages returns to Messages. A sidebar-placed page that registers its own `sidebar` still keeps Messages selected, and its sidebar replaces the conversation sidebar. Usage moved into Settings in #2742, so it is no longer a sidebar page. Drop it from the AppShell tests and the bundled navigation owner table. Cover the third-party case: a page with `primary: true` and no `placement` or `sidebar`. The AppShell table now asserts that the open page is the only current row in the sidebar, as well as the Messages selection, the sidebar and the return to Messages. The page-placement fixture plugin gains such a page. A browser journey opens it from the conversation sidebar and checks the same four things against real App wiring, including that Messages reopens the conversation. Both fail when the AppShell change is reverted. State the contract in plugin-architecture.md where `primary: true` is introduced. Non-primary pages opened from search or links, such as Repos, are unchanged. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Signed-off-by: mtoohey <mtoohey@squareup.com> Signed-off-by: Matt Toohey <contact@matttoohey.com>
Opening a page placed in the Messages sidebar (`primary: true` and no `placement`), such as Drafts & sent or a third-party plugin page, cleared the Messages selection in the app dock and hid the conversation sidebar. The page showed bare. The dock move in #2626 deleted the headerPageSelection mapping from #848, and AppShell then showed the Messages sidebar only when the selected page was buzz.channels itself. Carry over the fix from draft #2747 (52445b5, 04baf9f, 03bd763), rebased onto main. navigationOwner in presentation.ts maps the selected destination to the app whose navigation lists it. Sidebar-placed pages belong to Messages, and every other destination owns itself. It decides by placement only and does not special-case any bundled page key. AppShell uses the owner for the dock selection, its tab panel and the Messages sidebar. Activating the selected Messages tab from one of its sidebar pages returns to Messages. A sidebar-placed page that registers its own `sidebar` still keeps Messages selected, and its sidebar replaces the conversation sidebar. Usage moved into Settings in #2742, so it is no longer a sidebar page. Drop it from the AppShell tests and the bundled navigation owner table. Cover the third-party case: a page with `primary: true` and no `placement` or `sidebar`. The AppShell table now asserts that the open page is the only current row in the sidebar, as well as the Messages selection, the sidebar and the return to Messages. The page-placement fixture plugin gains such a page. A browser journey opens it from the conversation sidebar and checks the same four things against real App wiring, including that Messages reopens the conversation. Both fail when the AppShell change is reverted. State the contract in plugin-architecture.md where `primary: true` is introduced. Non-primary pages opened from search or links, such as Repos, are unchanged. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Signed-off-by: mtoohey <mtoohey@squareup.com> Signed-off-by: Matt Toohey <contact@matttoohey.com>
Opening a page placed in the Messages sidebar (`primary: true` and no `placement`), such as Drafts & sent or a third-party plugin page, cleared the Messages selection in the app dock and hid the conversation sidebar. The page showed bare. The dock move in #2626 deleted the headerPageSelection mapping from #848, and AppShell then showed the Messages sidebar only when the selected page was buzz.channels itself. Carry over the fix from draft #2747 (52445b5, 04baf9f, 03bd763), rebased onto main. navigationOwner in presentation.ts maps the selected destination to the app whose navigation lists it. Sidebar-placed pages belong to Messages, and every other destination owns itself. It decides by placement only and does not special-case any bundled page key. AppShell uses the owner for the dock selection, its tab panel and the Messages sidebar. Activating the selected Messages tab from one of its sidebar pages returns to Messages. A sidebar-placed page that registers its own `sidebar` still keeps Messages selected, and its sidebar replaces the conversation sidebar. Usage moved into Settings in #2742, so it is no longer a sidebar page. Drop it from the AppShell tests and the bundled navigation owner table. Cover the third-party case: a page with `primary: true` and no `placement` or `sidebar`. The AppShell table now asserts that the open page is the only current row in the sidebar, as well as the Messages selection, the sidebar and the return to Messages. The page-placement fixture plugin gains such a page. A browser journey opens it from the conversation sidebar and checks the same four things against real App wiring, including that Messages reopens the conversation. Both fail when the AppShell change is reverted. State the contract in plugin-architecture.md where `primary: true` is introduced. Non-primary pages opened from search or links, such as Repos, are unchanged. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Signed-off-by: mtoohey <mtoohey@squareup.com> Signed-off-by: Matt Toohey <contact@matttoohey.com>
## Problem Opening a page listed in the Messages sidebar showed it bare. The **Messages** selection in the app dock cleared, leaving no app selected, and the conversation sidebar the page was opened from disappeared. This hit **Drafts & sent** and any plugin page that sets `primary: true` without a `placement`, such as a local "PR status" plugin. Conversations, DMs, threads and Solo chats were not affected. `primary: true` with the default `placement` is the documented way for a plugin to get a row in the Messages sidebar, and the only one (`pagePlacement()` returns `page.placement ?? "sidebar"`). The plugins were doing the right thing. The host listed the page in the Messages sidebar, then removed that sidebar and deselected Messages when the page opened. ## Root cause The dock move in #2626 deleted `headerPageSelection()`, which #848 had added to map sidebar-placed pages back to Messages. Since then `src/app/shell/AppShell.tsx` has computed `channelsNavigation = selected === "buzz.channels/channels"`. That one check decides both the dock selection and whether the conversation sidebar shows, and every sidebar-placed page fails it. #2626 also wrote the behaviour into `docs/shell-design.md` ("sidebar/default-placement pages … leave every dock tile unselected"). ## Fix - `navigationOwner` in `src/app/shell/presentation.ts` maps the selected page to the app whose navigation lists it. A sidebar-placed page belongs to Messages, and every other destination owns itself. It decides by placement only and doesn't special-case any bundled page key, so Drafts & sent stays a plugin and third-party pages get the same treatment. - `AppShell` uses the owner for the dock selection, the tab panel's id and label, and whether the Messages sidebar shows. Code that describes the open page itself, such as the sidebar row highlight and `selectedPageHasSidebar`, still reads the raw selection. - Clicking the already-selected Messages tab from one of its sidebar pages returns to Messages. `PageNavigation` handles that reselect through a new `currentPage` prop. - A sidebar-placed page that registers its own `sidebar` still keeps Messages selected. Its sidebar replaces the conversation sidebar. - **Messages-absent guard.** A sidebar-placed page belongs to Messages only when the Messages page is registered. `App` already treats Messages as possibly absent. Without the guard, the shell would show the conversation sidebar, select no dock tab and name the panel after a page that isn't there. Now such a page owns itself, as an unlisted page does. - **Cmd/Ctrl+N.** These pages now have the Messages navigation, so Cmd/Ctrl+N opens New on a sidebar-listed page without its own `sidebar`. On `main` it was disabled there. This matches the documented rule that it opens New "where the current app has a creation sidebar". A sidebar-placed page with its own `sidebar` has no creation slot, so Cmd/Ctrl+N stays disabled there. - Docs: `docs/plugin-architecture.md` states the contract where `primary: true` is introduced. It now also says `"topbar"` and `"toolbar"` both make the page a dock app since #2626. `docs/shell-design.md` and `docs/buzz-help.md` describe the new selection. ## Before / after Both halves run in the browser fixture (Chromium, 1440×900, light theme). Each clicks the fixture page "Listed destination" (`primary: true`, no `placement`, no `sidebar`) in the conversation sidebar at the same moment, holds for two seconds, then clicks Messages in the dock. The video plays at 0.75x with step captions. Left (`main`): the conversation sidebar disappears and no dock tile is selected. Right (this branch): the sidebar stays with the page as the current row, Messages stays selected, and clicking Messages returns to the Alpha conversation. https://github.com/user-attachments/assets/d0dcfa71-c7e8-470e-b7e0-52381160bdb9  DOM state read during the recording, two seconds after the click: | | Messages `aria-selected` | Selected dock tabs | Conversation sidebar visible | |---|---|---|---| | Before (`main`, `f9ce9dfc4a`) | `false` | 0 | no | | After (this branch) | `true` | 1 | yes | The recording was made on the first commit. The follow-up commit doesn't change this path, because Messages is registered here. ## Decisions - **Supersedes #2747.** This carries over #2747's three commits, rebased onto `main` and squashed into one. It adds the third-party plugin case, the Messages-absent guard and the review follow-ups. #2747 went red after #2742 moved Usage into Settings, because its bundled navigation-owner table still listed Usage. Usage is dropped here. That test now asserts only the contract rows (Drafts & sent belongs to Messages; Repos and each dock app own themselves). Adding or removing an unrelated bundled page no longer breaks it. #2747 can be closed in favour of this PR. - **Plugin-agnostic alternative to the Drafts & sent part of #2728.** #2728 fixes Drafts & sent by making it a Messages route and rendering its navigation in the host's `PrimarySidebar`. A third-party plugin can't do that, so a plugin page like PR status would still lose the sidebar. This PR fixes the bug in the shell for every page listed in the Messages sidebar and keeps Drafts & sent a plugin. - **The Messages sidebar now stays open on pages listed in it.** This intentionally reverses the #2626 wording in `docs/shell-design.md`. - **Out of scope:** non-primary pages opened from search or from links in a conversation, such as Repos, still leave every dock tile unselected. #848 covered these, and this PR doesn't change them. ## Testing The head is now `b8de82df0d`, which differs from `6ac53643b3` only by deleting a tautological loop from `navigation-owner.test.ts` after review. On `b8de82df0d`, `pnpm lint`, `pnpm check` and Vitest for `src/bundled/navigation-owner.test.ts` plus `src/app/shell` (420 tests) were re-run and pass. Run on `6ac53643b3`: - `pnpm lint` and `pnpm check` - Vitest: `src/app/shell` (including the new `AppShell` and `presentation` tests), `src/bundled/navigation-owner.test.ts` and `src/bundled/drafts-sent`, 453 tests. Also `src/app/navigation.test.tsx`, `src/app/entity-navigation.test.tsx` and `src/app/first-run-navigation.test.tsx`, 44 tests. - `tests/browser/page-placement.spec.mjs` in Chromium and WebKit, 10/10. This includes the new journey "a page listed in the Messages sidebar keeps Messages and the conversation sidebar". It opens the fixture page from the conversation sidebar and checks four things: Messages stays selected, the conversation sidebar stays, the page is the only current row, and Messages reopens the conversation. - Fail-before: with only `AppShell.tsx` reverted to `main`, 6 `AppShell` tests fail. They cover Drafts & sent, the third-party page, a listed page with its own sidebar, the narrow drawer, the listed-versus-unlisted sidebar check and the Cmd/Ctrl+N row. On the first commit, the browser journey also failed against `main`'s shell code. Not run: the desktop app with a real plugin, and the full suite. The branch was pushed with `--no-verify`, so pre-push hooks were skipped. 🤖 Generated with [Claude Code](https://claude.com/claude-code) --------- Signed-off-by: mtoohey <mtoohey@squareup.com> Signed-off-by: Matt Toohey <contact@matttoohey.com> Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Summary
Messages now stays selected when navigating to Inbox and other pages in its sidebar. The header previously matched only the exact destination, so leaving the conversation page cleared its selection even though the user was still inside Messages.
No new routing state, last-child memory, persistence changes, or sidebar redesign.
Validation
Rebased onto main
d85e78e22. The earlier test-onlyWorkflowsPage.test.tsxrepair was dropped in favor of main's #911.Local macOS checks on
1eb3b3be9c97a91a2fdd130dcbf7b5f2e1890ef2:Menu.test.tsx"keyboard-opened menus ignore a stationary pointer…", fails the same way on main's tree locally; this PR doesn't touch itpnpm check(Biome, typecheck, design checks)New browser cases since the last review. Each fails with its fix reverted (main-pane case in both engines; Retry case checked in Chromium):
#buzz=address: keyboard Retry fails again in place and focus stays on Retry. Before the fix, focus moved to main.A helper test covers a non-primary page with
placement: "toolbar"selecting Messages.Visual evidence
Synthetic browser fixtures, not a live account or native acceptance. Captured before this rebase; the header styling is unchanged by it.
Light
Dark
Remaining acceptance gate
Hosted CI, native-app acceptance, human confirmation, and required approvals. No running app or live identity was changed.
On this branch in an agreed native test setup:
Implemented by Carl, an automated agent; PR opened through Wes’s GitHub account.