Cover drawer replacement cross-fades two overlays for ~210ms (visual; Escape half fixed)
- Dominant language
- Rust
- Stars
- 32.7k
- Forks
- 4.3k
- Avg merge
- 1d 13h
- Merged PRs (30d)
- 253
Description
Found while answering an outdated Codex review thread on #6542 (https://github.com/block/buzz/pull/6542#discussion_r3848180442). Measured at `0d7b21b356c842ad167d54a874bc9f378fe3c706`. Not a regression against main — the cover drawer is new in #6542.
> **Scope narrowed.** This issue originally covered two symptoms of one root cause: a swallowed Escape press and a transient double overlay. **The Escape half is fixed** in #6542 (commit `d833c54ee`); it was a real two-press bug and shipping the new surface with it was not acceptable. What remains here is the **visual half only**, which is a design question rather than a defect with an obvious correct answer. See the note at the bottom for what the fix taught us about the mechanism.
## Symptom
When one cover drawer replaces another (agent activity opening over a focus-mode thread, or the reverse), both drawers are mounted and cross-fading for **~210 ms**. Both `z-41` overlays and both scrims are in the DOM, and a screenshot taken mid-crossfade shows the outgoing panel's content ghosting through the incoming one.
Measured opacities during the window:
```
thread over activity: agentOpacity=0.46 threadOpacity=0
agentOpacity=0.34 threadOpacity=0.57
activity over thread: agentOpacity=0.23 threadOpacity=0.99
agentOpacity=1 threadOpacity=0.37
```
## Mechanism
`AnimatePresence` in `ChannelPane` keeps the replaced drawer mounted through its exit animation, and nothing serializes the transition — the successor mounts immediately, so for the duration of the exit two `CoverDrawer` instances are live. The focus-slot coordinator handles focus restoration only; it does not gate mounting.
`expectExactlyOneCoverDrawer` in `tests/e2e/agent-activity-cover.spec.ts` asserts post-settle overlay counts, so it holds with this present.
## The question
There is no obviously-correct answer here, which is why it is not being fixed alongside the Escape half:
1. **Keep the cross-fade.** Replacement stays as fast as it is now; two surfaces are briefly visible through each other.
2. **Serialize the hand-off** — a shared cover-slot key, or `AnimatePresence mode="wait"` for the covered slot — so the successor does not mount until the previous drawer has left. Only one surface is ever visible, at the cost of a slower replacement (roughly the exit duration added before the new panel appears).
This is a call about how drawer replacement should *feel*, so it wants a product decision rather than an implementation preference.
## What the Escape fix established about the mechanism
Worth recording, because the mechanism was more than one layer deep and the first diagnosis was incomplete:
- The swallowed press had **two independent causes**, and fixing either alone left the bug in place. `CoverDrawer`'s capture-phase claim consumed the press with `stopImmediatePropagation`; separately, `useEscapeKey` ignores an already-`defaultPrevented` event, so an exiting *panel*'s `preventDefault` swallowed the press from its successor's panel. Agent activity takes that second path exclusively (`ownsEscape={false}`), so no `CoverDrawer` Escape code runs for it at all.
- **The focus slot is not a usable proxy for "owns the covered slot."** Suggestion 2 in the original write-up — scope the claim to the drawer holding the focus slot — was implemented and falsified in the browser: the slot is claimed only by a drawer that *captures focus*, and a successor whose content takes focus instead never claims it, leaving the outgoing drawer's claim current. A slot check therefore passes for exactly the drawer that must stand down. The fix gates on `useIsPresent` instead.
Neither of those changes the visual half, and #6542 deliberately does not touch presence mode or the cross-fade.
Contributor guide
Assessment
This issue has not been assessed yet.