Found while answering an outdated Codex review thread on #6542 (#6542 (comment)). 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:
- Keep the cross-fade. Replacement stays as fast as it is now; two surfaces are briefly visible through each other.
- 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.
Found while answering an outdated Codex review thread on #6542 (#6542 (comment)). Measured at
0d7b21b356c842ad167d54a874bc9f378fe3c706. Not a regression against main — the cover drawer is new in #6542.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-41overlays 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:
Mechanism
AnimatePresenceinChannelPanekeeps the replaced drawer mounted through its exit animation, and nothing serializes the transition — the successor mounts immediately, so for the duration of the exit twoCoverDrawerinstances are live. The focus-slot coordinator handles focus restoration only; it does not gate mounting.expectExactlyOneCoverDrawerintests/e2e/agent-activity-cover.spec.tsasserts 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:
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:
CoverDrawer's capture-phase claim consumed the press withstopImmediatePropagation; separately,useEscapeKeyignores an already-defaultPreventedevent, so an exiting panel'spreventDefaultswallowed the press from its successor's panel. Agent activity takes that second path exclusively (ownsEscape={false}), so noCoverDrawerEscape code runs for it at all.useIsPresentinstead.Neither of those changes the visual half, and #6542 deliberately does not touch presence mode or the cross-fade.