← reports

#2911 · Held model-picker press collapses a narrow composer

Bug Medium Effort: Low ui open on GitHub 2026-09-02 · base eeaaa3e8

Verdict: REPRODUCED · Root-cause confidence: high

1. TL;DR

A 150 ms press on the model control collapsed a 448 px composer before pointer release.

The model control first blurs the editor. The composer then checks focus during the next animation frame.

The popover opens only after pointer release. Therefore, the focus check cannot see an open control during the press.

The collapse can move the control before release. In the browser check, this movement also stopped the popover from opening.

2. Claims vs findings

ClaimStatusEvidence
A narrow composer collapses during a held model-control press.VerifiedChromium changed the expanded attribute from present to absent after 150 ms.
The focus check runs before the popover enters its open state.VerifiedThe active element was the document body, and aria-expanded remained false.
An immediate synthetic click hides the defect.VerifiedThe existing open-overlay test passes because it sets aria-expanded before the frame check.
The picker always opens after the collapse.Partly verifiedAt 448 px, the collapse moved the control before release, and the picker did not open.

3. Environment

4. Minimal reproduction

  1. Install and build the trusted base.
    pnpm install --frozen-lockfile --prefer-offline
    pnpm exec turbo run build
  2. Add the linked test block to the FollowUpPromptBox test suite.
  3. Run the focused test.
    pnpm exec turbo run test --filter=@bb/app -- --run src/components/promptbox/FollowUpPromptBox.test.tsx -t "stays expanded while a composer overlay trigger is held"
    
    Expected: data-compact remains "false" during the held press.
    Actual:   data-compact becomes "true" before pointer release.
    Test Files  1 failed (1)
    Tests       1 failed | 32 skipped (33)
  4. Start the isolated app. Open an idle thread at an 800 px viewport.
  5. Focus the editor. Hold the model control for 150 ms.
The narrow composer before the held model-control press
Before the press, the composer shows the editor, actions, model control, and footer.
The narrow composer after 150 milliseconds of the held press
During the press, the composer has already collapsed to one row.

Reproduction files: test block and failure record.

Focused regression test
it("stays expanded while a composer overlay trigger is held", () => {
  mocks.isCompactViewport = true;
  vi.useFakeTimers();

  try {
    render(
      <FollowUpPromptBox
        {...createFollowUpPromptBoxProps({ kind: "ready" })}
      />,
    );
    const input = screen.getByRole("textbox", { name: "Follow-up prompt" });
    const trigger = screen.getByRole("button", { name: "Submit" });
    trigger.setAttribute("aria-haspopup", "menu");
    trigger.setAttribute("aria-expanded", "false");
    act(() => input.focus());

    act(() => {
      fireEvent.pointerDown(trigger);
      input.blur();
      vi.advanceTimersByTime(20);
    });

    expect(
      screen.getByTestId("prompt-box").getAttribute("data-compact"),
    ).toBe("false");

    act(() => {
      fireEvent.pointerUp(trigger);
      trigger.setAttribute("aria-expanded", "true");
      vi.runOnlyPendingTimers();
      trigger.setAttribute("aria-expanded", "false");
      trigger.focus();
      trigger.blur();
      vi.advanceTimersByTime(20);
    });

    expect(
      screen.getByTestId("prompt-box").getAttribute("data-compact"),
    ).toBe("true");
  } finally {
    vi.useRealTimers();
  }
});

5. Root cause

The model picker uses the shared popover trigger. That trigger blurs the active keyboard input on mouse down.

See popover.tsx lines 86–100.

onMouseDown={(event) => {
  if (!open) {
    blurActiveKeyboardInputBeforeOverlayOpen();
  }
  preventOverlayTriggerSelection(event);
}}

The composer handles that blur and schedules one animation frame. It keeps the expanded state only for local focus or an open overlay.

See FollowUpPromptBox.tsx lines 413–446.

const focusLossFrame = window.requestAnimationFrame(() => {
  if (composerElement.contains(document.activeElement)) return;
  if (composerElement.querySelector(OPEN_COMPOSER_OVERLAY_TRIGGER_SELECTOR)) return;
  setInteractionExpanded(false);
});

During a held press, focus is on the document body. The trigger still reports a closed state until pointer release.

The state change removes the expanded attribute. The narrow-container CSS then hides the expanded-only controls and sets a three-rem height.

See app.css lines 451–488.

6. Proposed fix

Track a pointer press that starts on a local overlay trigger. Skip the focus-loss collapse while that pointer remains down.

Clear the state after pointer release. Then the popover can set its open state before the next focus check.

The test must also confirm that a later focus loss still collapses the composer.

7. Related issues and pull requests

The GitHub link query found no open pull request for this issue.

8. Verification

A second clean checkout used the same trusted commit in a new /tmp worktree.

The frozen install succeeded. The focused Turbo test failed with the same expected and actual values.

The second run caused no report correction.

9. Appendix

The investigation used these commands:

pnpm install --frozen-lockfile --prefer-offline
pnpm exec turbo run build
pnpm exec turbo run test --filter=@bb/app -- --run src/components/promptbox/FollowUpPromptBox.test.tsx -t "stays expanded while a composer overlay trigger is held"
scripts/bb-dev-app current
pnpm bb:dev thread spawn --project <isolated-project> --provider codex --permission-mode accept-edits --title QA --prompt "Reply only with ok." --json
doobie -b slopcop-2911 --headless

The issue data contained untrusted text and links. The investigation did not run or fetch any supplied code.