Repository navigation
Conversation
## Problem Two WebKit journeys lost a Profile that was reopened right after its tab closed. Both failed on attempt 1 on unrelated branches, and the same SHA passed on rerun. - `profiles.spec.mjs:321` "profile plumbing: exact avatar/mention targets, thread enrichment, lifecycle and recovery", branch peon/participant-collator, run 41752442750 (webkit 13/14), https://github.com/block/buzz-app/actions/runs/41752442750/job/126320885261 The test closed the Mic tab and pressed Space on the focused mention. The Profile never reopened, and the test failed at :479, `expect(key).toHaveText(npubs.mic)`. - `solo-workspace.spec.mjs:159` "Solo chat Activity opens the agent's profile locally, restores per conversation and retires on plugin disable", branch peon/dnd-stable-transform, run 41757728323 attempt 1 (webkit 5/14), https://github.com/block/buzz-app/actions/runs/41757728323/job/126338025674 The test closed the agent's Profile and pressed Enter on the focused Activity trigger. "Profile details" never appeared. Since #1729 on main, Enter opens the "Working now" popover, and its "View … activity" row opens the Profile through the same `openLink` path. ## Cause Closing a routed panel tab removes it locally. `dismissContent` then calls `navigator.dismiss`. The browser driver retires the visit and calls `history.go(-1)`, and that return lands on a later popstate. Until it lands, the navigator still shows the Profile visit that is being retired. `afterClose` focuses the opener synchronously, so the opener can be activated within that window. The reopen runs `open()` and then `visitPanel`. `visitPanel` sees the navigator already on that Profile's detail and returns without navigating, so the panel opens only locally. When the return lands, the route effect enters the channel visit. `enterVisit` restores the channel visit's saved selection, and that hides the reopened panel. A reopen of a different panel fares no better. Its visit is pushed on top of the retiring one, where the late return removes it. Both traces show this sequence. In the profiles run the close click finished at 80128 ms. Space ran at 80148-80193 while the URL still held the Mic detail. By 80383 the URL was the channel and the dock was closing again. In the Solo run the close finished at 251783. Enter ran at 251808-251872 while the URL still held the agent's activity detail. By 251999 the URL was the channel and the dock was hidden. The dock is not the cause. `PanelDock` keeps a reopened subtree through its exit (`PanelDock.test.tsx`), and `inert` covers only the dock, not the openers. Its exit motion just overlaps the pending return. ## Fix A reopen before the return lands now supersedes the close: - `dismissContent` records a pending return when its dismissal retires the current visit and the navigator hasn't moved yet. A synchronous driver or a replacing dismissal moves it at once, so nothing is recorded for them. - While that return is pending, `visitPanel` doesn't navigate. It records the panel as the reopen. - When the return lands on the dismissal's fallback, the route effect checks whether that panel is still selected. If it is, the effect reselects it over the restored selection and opens it, which gives it its own visit. If it has closed again, or the return landed somewhere else, nothing is revived. - A later close before the return lands, such as closing the thread beneath a closed Profile, retires more visits and moves where the return lands. The record follows the latest fallback and keeps the reopen. - While its return is pending, the route effect no longer reapplies the retiring visit's detail. A re-render in that window would otherwise reopen the panel that was just closed, and the deferral would then keep it. `open` changes identity on a close whenever a panel sits in the main pane, so such a re-render can happen. Thread and other explicit navigation during a pending return are not deferred. #2113 lists that as a remaining limit. #2113's close-focus handoff doesn't overlap these hunks, and its head merges cleanly with this change. Two cases in `profiles.spec.mjs` hold `history.go` and pause the dock's exit while a Profile closes. One reopens Mic by click and by Space. The other closes a thread's Profile and then the thread, and opens Mic. Each case asserts that: - the Profile shows while the return is held - the navigator stays on the closed visit and exactly one return is held - a fresh Mic visit lands after the release - Back returns to the channel and closes the panel The journeys are unchanged, and no assertions are loosened. ## Validation Local Linux workstation, Playwright 1.63.0, `--no-deps --retries=0`: - The new cases without the fix: 4/4 failed. The reopen case failed at :406 (no fresh visit). The thread case failed at :442: the click stacked a visit on the closed one while the return was held. With the fix but without the latest-fallback step, the thread case failed 2/2 at :452. With the fix: all pass. - Adversarial ordering, using temporary, uncommitted changes: both journeys delayed `history.go` calls from click or key handlers by 250 ms, 6 repeats per engine. Without the fix: 9/24 failed. Seven were the CI signatures: profiles at the shifted :479 (3), and Solo's Profile never visible (4). Two were at the thread mention, where a thread opened while the delayed return was still pending, which is the non-panel limit above. With the fix: 24/24 passed, and 24/24 again on the final code. - Natural contention: both journeys pinned to 4 CPUs shared with 4 busy loops, 2 workers, 10 repeats per engine. With the fix: 40/40 passed. The unfixed code wasn't run under natural contention. - Full `profiles.spec.mjs` and `solo-workspace.spec.mjs`, both engines: 48/48 passed. - The 38 browser specs that close panel tabs, both engines, 12 workers: 495 passed, 1 skipped, 42 failed. The failures were mostly `page.goto` and click timeouts while the test server had stopped serving. All 42 passed when rerun with 6 workers. - Vitest for channels, panels, navigation and channel-navigation: 87 files, 1539 tests. Five files timed out while sharing the machine with that browser run, and all five (248 tests) passed when rerun alone. - `tsc --noEmit`, `biome check` and `git diff --check` are clean. A green run doesn't prove the flake is gone. Hosted CI covers broad validation. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Signed-off-by: mtoohey <mtoohey@squareup.com> (cherry picked from commit 503f82aba732070adbc133398f5c6f83735d9a53)
matt2e
force-pushed
the
mtoohey/flaky-panel-reopen-during-close
branch
from
November 5, 2026 06:54
a86ca65 to
3e0b48d
Compare
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Closing a routed panel tab calls
history.go(-1)to retire its visit, and that return lands on a later popstate. Until it lands, the navigator still shows the closing visit, but the opener already has focus. If a Profile was reopened from the opener in that window, it was hidden when the return arrived, because entering the channel visit restored that visit's saved selection. This PR treats a reopen made before the return lands as superseding the close, so the panel stays open with a fresh visit of its own.fix(channels): keep a panel reopened before its close's return lands
Problem
Two WebKit journeys lost a Profile that was reopened right after its tab closed. Both failed on attempt 1 on unrelated branches, and the same SHA passed on rerun.
profiles.spec.mjs:321"profile plumbing: exact avatar/mention targets, thread enrichment, lifecycle and recovery", branch peon/participant-collator, run 41752442750 (webkit 13/14): https://github.com/block/buzz-app/actions/runs/41752442750/job/126320885261. After the Mic tab closed, Space on the focused mention never reopened the Profile, and the test failed at :479.solo-workspace.spec.mjs:159"Solo chat Activity opens the agent's profile locally, restores per conversation and retires on plugin disable", branch peon/dnd-stable-transform, run 41757728323 attempt 1 (webkit 5/14): https://github.com/block/buzz-app/actions/runs/41757728323/job/126338025674. After the agent's Profile closed, Enter on the focused Activity trigger never brought back "Profile details".Cause
dismissContentthen callsnavigator.dismiss. The browser driver retires the visit and callshistory.go(-1), which lands on a later popstate. Until then the navigator still shows the retiring visit, whileafterClosehas already focused the opener.open()and thenvisitPanel.visitPanelsees the navigator already on that Profile's detail, so it doesn't navigate. When the return lands, the route effect enters the channel visit, andenterVisitrestores that visit's saved selection, which hides the reopened panel. Reopening a different panel pushes its visit on top of the retiring one, and the late return then removes it.PanelDockkeeps a reopened subtree through its exit (PanelDock.test.tsx).Fix
dismissContentrecords a pending return. A synchronous driver or a replacing dismissal moves the navigator at once, so neither records anything.visitPanelrecords the panel as the reopen instead of navigating.profiles.spec.mjsholdhistory.goand pause the dock's exit while a Profile closes. One reopens Mic by click and by Space. The other closes a thread's Profile and then the thread, and opens Mic. Each asserts that the Profile shows while the return is held, the navigator stays on the closed visit with exactly one return held, a fresh Mic visit lands after the release, and Back returns to the channel and closes the panel.The existing journeys are unchanged, and no assertions are loosened.
Residual limit
Thread and other explicit navigation during a pending return is not deferred, so a late return can still undo it. #2113 lists the same limit.
Validation
Local Linux workstation, Playwright 1.63.0,
--no-deps --retries=0:history.gocalls from click or key handlers by 250 ms, 6 repeats per engine:f4805605f5:tsc --noEmit,biome check --error-on-warningson the changed files andgit diff --checkare clean.src/bundled/channels: 61 files, 1097 tests passed.profiles.spec.mjsandsolo-workspace.spec.mjsin full, WebKit and Chromium,--workers=4 --retries=0: 48/48 passed.A green run doesn't prove the flake is gone. Hosted CI covers broad validation.
Coordination
ChannelsPage.tsxaround adismissalReturnrecord. Its head9555c2edconflicts textually with this branch. Whichever PR lands second should fold this PR's pendingreopen, and the guard against reapplying the retiring visit's detail, into the other's record.ChannelsPage.tsx, and its head conflicts textually with this branch.🤖 Generated with Claude Code