Direct reproduction and root-cause report

Right-panel tabs remain stable on the current main branch

Issue #2699 · Report date: 2026-08-29 · Base: 3414fda

Bug Priority: Medium Effort: Medium ui side-chat ALREADY FIXED · medium confidence
Verdict: ALREADY FIXED. The reported tab cycle did not occur on trusted current code. Two clean checkouts passed the focused test. A direct browser run also kept each requested tab stable.

Result

New-tab sample
101 samples during five seconds; the tab appeared once and stayed present.
Reply sample
250 samples during five seconds; the side-chat tab appeared once and stayed present.
Focused test
Two tests passed in each clean checkout.
Existing pane suite
All 33 tests passed.
Application build
The full Turbo build passed.

Claim and finding

ClaimCurrent-main finding
A closed side chat returns during a new-tab action.The side chat stayed closed. One new tab appeared and stayed present.
A Reply action starts a repeated tab cycle.One side-chat tab appeared. The browser found no repeated add or remove cycle.
A reload can clear the state.The clean runs did not require a reload or a recovery action.

Direct browser reproduction

  1. Build trusted commit 3414fda with Turbo.
  2. Start an isolated application, server, daemon, and data directory.
  3. Create one isolated main thread.
  4. Open a side chat and wait for its tab.
  5. Close the side chat, then open a new tab.
  6. Sample the right-panel toolbar every 20 milliseconds for five seconds.
  7. Close the new tab, then select Reply.
  8. Sample the toolbar again for five seconds.

The expected failure was a repeated tab add and remove cycle. The actual result was one stable tab for each action. The sample output follows.

{
  "sampleIntervalMs": 20,
  "newTabRun": {
    "sampleCount": 101,
    "changes": [
      {"ms": 59, "labels": ["Open new tab"]},
      {"ms": 539, "labels": ["New tab", "Open new tab"]}
    ],
    "finalState": {"newTabPresent": true, "newTabSearchPresent": true}
  },
  "replyRun": {
    "sampleCount": 250,
    "changes": [
      {"ms": 92, "labels": ["Open new tab"]},
      {"ms": 814, "labels": ["Side chat", "Close Side chat", "Open new tab"]}
    ],
    "finalState": {"sideChatTabPresent": true}
  }
}
The isolated bb application with one stable side-chat tab in the right panel
The initial side chat reached a stable state.
The isolated bb application with one stable new tab in the right panel
The new tab stayed present after the side chat closed.

Focused reproduction test

The test creates separate side-chat and new-tab panes. It leaves ownership on the side-chat pane before each action.

One child control stops pointer propagation. The other child receives keyboard focus. Each path must activate the new-tab pane exactly once.

it("activates the new-tab pane before a control stops pointer propagation", async () => {
  const split = createTwoPaneState();
  const [sideChatPaneId, newTabPaneId] = paneIds(split);
  window.localStorage.setItem(
    sidebarSplitStorageKey(PANEL_STATE_ID),
    serializeSidebarSplitState(focusSidebarPane(split, sideChatPaneId)),
  );
  const activate = vi.fn();

  renderContainer({
    onActivateTab: activate,
    renderPane: ({ paneId }) => (
      <button
        type="button"
        aria-label={paneId === newTabPaneId ? "Open new tab" : "Close Side chat"}
        onPointerDown={(event) => event.stopPropagation()}
      />
    ),
  });
  fireEvent.pointerDown(screen.getByRole("button", { name: "Open new tab" }));

  await waitFor(() => expect(activate).toHaveBeenCalledWith(NEW_TAB_ID));
  expect(activate).toHaveBeenCalledTimes(1);
});

it("activates the new-tab pane when keyboard focus enters it", async () => {
  const split = createTwoPaneState();
  const [sideChatPaneId, newTabPaneId] = paneIds(split);
  window.localStorage.setItem(
    sidebarSplitStorageKey(PANEL_STATE_ID),
    serializeSidebarSplitState(focusSidebarPane(split, sideChatPaneId)),
  );
  const activate = vi.fn();

  renderContainer({
    onActivateTab: activate,
    renderPane: ({ paneId }) => (
      <button
        type="button"
        aria-label={paneId === newTabPaneId ? "Open new tab" : "Close Side chat"}
      />
    ),
  });
  fireEvent.focus(screen.getByRole("button", { name: "Open new tab" }));

  await waitFor(() => expect(activate).toHaveBeenCalledWith(NEW_TAB_ID));
  expect(activate).toHaveBeenCalledTimes(1);
});
pnpm exec turbo run test --filter=@bb/app --force -- src/components/secondary-panel/SidebarSplitContainer.issue-2699.test.tsx

Test Files  1 passed (1)
Tests       2 passed (2)

Root cause

Version 0.40.0 used a bubble-phase pointer handler for pane ownership. A child control could stop that event before the pane received it.

Keyboard focus also did not update pane ownership. A later global tab action could therefore target the prior pane.

The release source shows the bubble handler at lines 539–542.

Post-release commit c05dba2 moved pointer ownership to the capture phase and added focus capture.

Current main has both handlers at lines 648–653. Its regression tests cover both paths at lines 190–275.

Confidence: medium. The original intermittent session was unavailable. The release defect and the post-release fix match the reported action path.

Verification

Checkout oneTrusted base plus the focused test. The full build passed. Both focused tests passed.
Checkout twoA separate detached checkout at the same trusted base. Both focused tests passed again.
Relevant suiteSidebarSplitContainer.test.tsx passed all 33 tests.
BrowsermacOS 26.6.1, arm64, headless Chrome at 1280 × 720.
ToolchainNode 22.22.3 and pnpm 9.15.0.

Fix decision

No new patch is necessary. Current main already contains the pane-ownership fix and regression coverage.

A new pull request would duplicate that code. The simple-fix gate also requires a failure on current main.

Scope and trust

The investigation used only trusted repository code and changes created for this report. It did not fetch any issue attachment.

The report used issue text only as an untrusted claim. Repository code, test results, and direct browser state support each finding.