Skip to content

Cover drawer replacement cross-fades two overlays for ~210ms (visual; Escape half fixed) #6734

Description

@baxen

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:

  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.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't working

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions