#4817 · Inactive secondary-panel fallback remains mounted
Verdict: PARTIALLY REPRODUCED · Root-cause confidence: high
1. TL;DR
The actual secondary-panel component inserts an unavailable-view diagnostic into the DOM even when closed and there is no active panel tab. Both desktop and drawer presentations fail a regression asserting that this diagnostic must be absent when closed. The same authored test fails at the same assertion in two clean checkouts of trusted main. This proves the component DOM defect, not the reported browser innerText behavior or a failure of the main plugin view. No third-party plugin code was needed or executed.
2. Claims vs findings
| Claim | Status | Evidence |
|---|---|---|
| Closed secondary-panel fallback remains in the DOM. | Verified | Real React component rendered with isOpen=false, no tabs, no fixed tabs; two cases fail in both checkouts. |
| The diagnostic is from BB, not third-party content. | Verified | The diagnostic is emitted by ThreadSecondaryPanel; the reproduction loads no plugin. |
| A specific plugin route works while body.innerText includes the diagnostic. | Unverified | Chromium is unavailable; no live route or browser innerText measurement was performed. |
| This implies an accessibility-tree or visible main-panel failure. | Not established | The closed aside already has aria-hidden and inert. Neither attribute removes DOM text. No accessibility-tree leak is claimed here. |
| Plugin secure-context startup behavior. | Out of scope | Unrelated third-party code was not executed or modified. |
3. Environment
- Public repository get-bb/bb, trusted origin/main commit
76a1ebcaf5fa038d8638daaad7aad3dafe2ae709(package bb-app 0.45.0). - Linux x86_64, Node v22.19.0, pnpm 9.15.0, Vitest 4.1.1, jsdom.
- First clean detached worktree: checkout-1. Second clean detached worktree: checkout-2. Both start at exactly the recorded commit.
- Frozen install in each checkout; full Turbo build in checkout-1: 63 successful tasks. Tests use the normal app Vitest worker projects.
- No BB server, host daemon, provider sessions, or runtime database was started. An isolated Vite harness was briefly served on 127.0.0.1:54817, then stopped after Browser Automation reported no Chrome/Chromium. No data directory was used.
4. Minimal reproduction
- Clone trusted main and check out the recorded commit.
- Apply the independently authored regression patch below from the repository root.
- Install and run:
git clone https://github.com/get-bb/bb.git bb-4817
cd bb-4817
git checkout --detach 76a1ebcaf5fa038d8638daaad7aad3dafe2ae709
git apply <<'PATCH'
diff --git a/apps/app/src/components/secondary-panel/ThreadSecondaryPanel.collapseControl.test.tsx b/apps/app/src/components/secondary-panel/ThreadSecondaryPanel.collapseControl.test.tsx
index 30c0f3201..015bd4939 100644
--- a/apps/app/src/components/secondary-panel/ThreadSecondaryPanel.collapseControl.test.tsx
+++ b/apps/app/src/components/secondary-panel/ThreadSecondaryPanel.collapseControl.test.tsx
@@ -14,7 +14,7 @@ import {
} from "@/components/commands/AppCommandProvider";
import type { AppShortcutPresentation } from "@/lib/app-keybindings";
import { SecondaryPanelHostLayoutContext } from "./SecondaryPanelHostLayoutContext";
-import { PanelGroup } from "react-resizable-panels";
+import { Panel, PanelGroup } from "react-resizable-panels";
import { TooltipProvider } from "@bb/shared-ui/tooltip";
import { SidebarProvider } from "@/components/ui/sidebar";
import {
@@ -177,6 +177,50 @@ function renderFixedTabSplit() {
);
}
+describe("ThreadSecondaryPanel unavailable content", () => {
+ it.each([false, true])(
+ "only mounts the unavailable fallback while open (drawer=%s)",
+ (renderAsDrawer) => {
+ const { wrapper: Wrapper } = createQueryClientTestHarness();
+ const panel = (isOpen: boolean) => (
+ <Wrapper>
+ <TooltipProvider>
+ <PanelGroup direction="horizontal">
+ <Panel id="main" order={1}>
+ Working main content
+ </Panel>
+ <ThreadSecondaryPanel
+ activeTab={null}
+ canUseGitUi={false}
+ fixedTabs={[]}
+ tabs={[]}
+ isConversationCollapsed={false}
+ isOpen={isOpen}
+ metadataContent={null}
+ onClose={noop}
+ onCollapse={noop}
+ onTabReorder={noop}
+ onOpenNewTab={noop}
+ onPanelFocus={noop}
+ onToggleConversationCollapse={noop}
+ renderAsDrawer={renderAsDrawer}
+ />
+ </PanelGroup>
+ </TooltipProvider>
+ </Wrapper>
+ );
+ const { unmount } = render(panel(false));
+ expect(screen.queryByText("This panel view is unavailable.")).toBeNull();
+
+ unmount();
+ render(panel(true));
+ expect(
+ screen.getByText("This panel view is unavailable."),
+ ).not.toBeNull();
+ },
+ );
+});
+
describe("ThreadSecondaryPanel compact file content", () => {
it("renders the available tab while persisted active state catches up", () => {
const { wrapper: Wrapper } = createQueryClientTestHarness();
PATCH
pnpm install --frozen-lockfile --prefer-offline
pnpm exec turbo run build
pnpm exec turbo run test --filter=@bb/app -- src/components/secondary-panel/ThreadSecondaryPanel.collapseControl.test.tsx -t 'only mounts the unavailable fallback'
The fixture includes a real main Panel sibling, QueryClient/Jotai and tooltip providers, and the production ThreadSecondaryPanel. No panel component or database is mocked. An existing command-shortcut stub in this test file does not control the fallback. The test checks separate closed and open mounts in both presentations.
Expected: queryByText("This panel view is unavailable.") is null when closed.
Actual: AssertionError: expected <div …(1)></div> to be null
Test Files 1 failed (1)
Tests 2 failed | 22 skipped (24)
Authored regression, extending the existing owning test suite:
describe("ThreadSecondaryPanel unavailable content", () => {
it.each([false, true])(
"only mounts the unavailable fallback while open (drawer=%s)",
(renderAsDrawer) => {
const { wrapper: Wrapper } = createQueryClientTestHarness();
const panel = (isOpen: boolean) => (
<Wrapper>
<TooltipProvider>
<PanelGroup direction="horizontal">
<Panel id="main" order={1}>
Working main content
</Panel>
<ThreadSecondaryPanel
activeTab={null}
canUseGitUi={false}
fixedTabs={[]}
tabs={[]}
isConversationCollapsed={false}
isOpen={isOpen}
metadataContent={null}
onClose={noop}
onCollapse={noop}
onTabReorder={noop}
onOpenNewTab={noop}
onPanelFocus={noop}
onToggleConversationCollapse={noop}
renderAsDrawer={renderAsDrawer}
/>
</PanelGroup>
</TooltipProvider>
</Wrapper>
);
const { unmount } = render(panel(false));
expect(screen.queryByText("This panel view is unavailable.")).toBeNull();
unmount();
render(panel(true));
expect(
screen.getByText("This panel view is unavailable."),
).not.toBeNull();
},
);
});
This is a nonvisual DOM diagnostic investigation; no screenshot is presented as evidence. The concrete browser innerText claim remains unverified.
5. Root cause
The final renderPanelSurface fallback has no open-state guard. Empty active/fixed tabs reach it even when the panel is closed. The aside still mounts panelSurface and sets aria-hidden and inert, neither of which deletes descendant DOM nodes. The component already computes isLayoutOpen from the panel state and optional host suppression; other content, such as diff activity, uses that value.
... : (
<EmptyStatePanel className="m-4 rounded-lg">
This panel view is unavailable.
</EmptyStatePanel>
)
The plugin right-panel host supplies the same panel component, and can supply empty tab lists. Important scope correction: LazyThreadSecondaryPanel does not initially realize a never-opened closed panel. Once realized, it deliberately retains the component after closing. The owner-level test starts at that realized component boundary; this report does not claim every fresh plugin navigation always contains the diagnostic.
6. Proposed fix
Gate only the final unavailable-view fallback on the existing effective isLayoutOpen value. Preserve retained tab/browser content and the panel shell. A genuinely open panel with no supported view must continue showing the diagnostic. This needs no product decision, public API, protocol, storage, dependency, or accessibility-boundary change.
7. Related issues and PRs
No linked open pull request appeared in the issue timeline or open-PR search before implementation. No external issue URLs or linked code were executed.
8. Verification
The same agent repeated the reproduction in checkout-2, a new clean worktree at 76a1ebcaf5fa038d8638daaad7aad3dafe2ae709, with a fresh frozen install and only the authored regression patch. The exact command above exited 1 in both checkouts, with the two same intended null-assertion failures. This is a second clean run, not an independent-agent verification.
Earlier test-authoring mistakes (unsupported matcher, then a resize-library registration error during a simulated toggle) were corrected before capturing the final evidence. The final fixture uses separate mounts to test visibility without asserting resizing behavior. Only the final reproducible fixture and intended failures are published. The report verdict is limited to PARTIALLY REPRODUCED because the full browser route and innerText claim could not be checked.
9. Appendix
First-checkout final regression output
• turbo 2.10.12
• Packages in scope: @bb/app
• Running test in 1 packages
• Remote caching disabled, using shared worktree cache
//:ensure-native-modules: cache bypass, force executing 021c233267db6d6f
@bb/templates:generate:plugin-scaffold: cache hit, replaying logs 57fcb8a3dae79417
@bb/templates:generate:plugin-scaffold:
@bb/templates:generate:plugin-scaffold: > @bb/templates@0.0.1 generate:plugin-scaffold <checkout-1>/packages/templates
@bb/templates:generate:plugin-scaffold: > node ./scripts/generate-plugin-scaffold.mjs
@bb/templates:generate:plugin-scaffold:
@bb/templates:generate:templates: cache hit, replaying logs dea08a2850fb6662
@bb/templates:generate:templates:
@bb/templates:generate:templates: > @bb/templates@0.0.1 generate:templates <checkout-1>/packages/templates
@bb/templates:generate:templates: > node ./scripts/generate-templates.mjs
@bb/templates:generate:templates:
//:ensure-native-modules:
//:ensure-native-modules: > bb@ ensure-native-modules <checkout-1>
//:ensure-native-modules: > node scripts/ensure-native-modules.mjs
//:ensure-native-modules:
@bb/app:test: cache miss, executing 3cf0a230c8dd8a85
@bb/app:test:
@bb/app:test: > @bb/app@0.0.1 test <checkout-1>/apps/app
@bb/app:test: > node scripts/generate-pwa-icons.mjs --check && vitest run --config vitest.config.ts "src/components/secondary-panel/ThreadSecondaryPanel.collapseControl.test.tsx" "-t" "only mounts the unavailable fallback"
@bb/app:test:
@bb/app:test:
@bb/app:test: RUN v4.1.1 <checkout-1>/apps/app
@bb/app:test:
@bb/app:test: stderr | src/components/secondary-panel/ThreadSecondaryPanel.collapseControl.test.tsx > ThreadSecondaryPanel unavailable content > only mounts the unavailable fallback while open (drawer=false)
@bb/app:test: WARNING: Panel defaultSize prop recommended to avoid layout shift after server rendering
@bb/app:test:
@bb/app:test: stderr | src/components/secondary-panel/ThreadSecondaryPanel.collapseControl.test.tsx > ThreadSecondaryPanel unavailable content > only mounts the unavailable fallback while open (drawer=true)
@bb/app:test: WARNING: Panel defaultSize prop recommended to avoid layout shift after server rendering
@bb/app:test:
@bb/app:test: ❯ |@bb/app:isolated| src/components/secondary-panel/ThreadSecondaryPanel.collapseControl.test.tsx (24 tests | 2 failed | 22 skipped) 78ms
@bb/app:test: × only mounts the unavailable fallback while open (drawer=false) 62ms
@bb/app:test: × only mounts the unavailable fallback while open (drawer=true) 14ms
@bb/app:test: ↓ renders the available tab while persisted active state catches up
@bb/app:test: ↓ renders arbitrary fixed-tab content through the shared surface
@bb/app:test: ↓ renders plugin fixed tabs concurrently when they are split
@bb/app:test: ↓ renders each split pane from the descriptor attached to its tab
@bb/app:test: ↓ retains the active file body after the persistent drawer closes
@bb/app:test: ↓ renders one active compact body and restores the saved wide split
@bb/app:test: ↓ is absent when unsplit and appears at the trailing edge of every split pane
@bb/app:test: ↓ keeps Info and Diff open when removing their split from the focused control
@bb/app:test: ↓ falls back from an ineligible active Diff tab to Info
@bb/app:test: ↓ keeps an active Diff tab visible while Git eligibility loads
@bb/app:test: ↓ leaves only the trailing panel toggle on the full-page compact panel
@bb/app:test: ↓ keeps the panel seam visible while the clipped panel surface moves
@bb/app:test: ↓ keeps Full Screen before Hide right panel in the trailing toolbar
@bb/app:test: ↓ expands the panel while the conversation is shown
@bb/app:test: ↓ restores the conversation from the same slot while it is collapsed
@bb/app:test: ↓ offers every existing split position from the right-panel control and moves the active tab
@bb/app:test: ↓ maximizes one stacked pane while keeping both tab rows mounted
@bb/app:test: ↓ advertises the bound command on the Full Screen control
@bb/app:test: ↓ advertises the bound command on the Maximize pane control
@bb/app:test: ↓ keeps the shortcut off hosts that do not handle the command
@bb/app:test: ↓ focuses New tab without opening it and resumes cycling from the button
@bb/app:test: ↓ ignores tab navigation while chat maximize suppresses the panel and resumes after restore
@bb/app:test:
@bb/app:test: ⎯⎯⎯⎯⎯⎯⎯ Failed Tests 2 ⎯⎯⎯⎯⎯⎯⎯
@bb/app:test:
@bb/app:test: FAIL |@bb/app:isolated| src/components/secondary-panel/ThreadSecondaryPanel.collapseControl.test.tsx > ThreadSecondaryPanel unavailable content > only mounts the unavailable fallback while open (drawer=false)
@bb/app:test: FAIL |@bb/app:isolated| src/components/secondary-panel/ThreadSecondaryPanel.collapseControl.test.tsx > ThreadSecondaryPanel unavailable content > only mounts the unavailable fallback while open (drawer=true)
@bb/app:test: AssertionError: expected <div …(1)></div> to be null
@bb/app:test:
@bb/app:test: - Expected:
@bb/app:test: null
@bb/app:test:
@bb/app:test: + Received:
@bb/app:test: <div
@bb/app:test: class="border border-dashed border-border px-3 py-6 text-center text-sm text-muted-foreground m-4 rounded-lg"
@bb/app:test: >
@bb/app:test: This panel view is unavailable.
@bb/app:test: </div>
@bb/app:test:
@bb/app:test: ❯ src/components/secondary-panel/ThreadSecondaryPanel.collapseControl.test.tsx:213:69
@bb/app:test: 211| );
@bb/app:test: 212| const { unmount } = render(panel(false));
@bb/app:test: 213| expect(screen.queryByText("This panel view is unavailable.")).to…
@bb/app:test: | ^
@bb/app:test: 214|
@bb/app:test: 215| unmount();
@bb/app:test:
@bb/app:test: ⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯[1/2]⎯
@bb/app:test:
@bb/app:test:
@bb/app:test: Test Files 1 failed (1)
@bb/app:test: Tests 2 failed | 22 skipped (24)
@bb/app:test: Start at 18:20:31
@bb/app:test: Duration 28.28s (transform 25.80s, setup 1.64s, import 25.73s, tests 78ms, environment 636ms)
@bb/app:test:
@bb/app:test: ELIFECYCLE Test failed. See above for more details.
@bb/app#test: ERROR command (<checkout-1>/apps/app) /usr/local/bin/pnpm run test src/components/secondary-panel/ThreadSecondaryPanel.collapseControl.test.tsx -t only mounts the unavailable fallback exited (1)
Tasks: 3 successful, 4 total
Cached: 2 cached, 4 total
Time: 30.647s
Failed: @bb/app#test
ERROR run failed: command exited (1)
Second-checkout final regression output
• turbo 2.10.12
• Packages in scope: @bb/app
• Running test in 1 packages
• Remote caching disabled, using shared worktree cache
//:ensure-native-modules: cache bypass, force executing 021c233267db6d6f
@bb/templates:generate:templates: cache hit, replaying logs dea08a2850fb6662
@bb/templates:generate:templates:
@bb/templates:generate:templates: > @bb/templates@0.0.1 generate:templates <checkout-1>/packages/templates
@bb/templates:generate:templates: > node ./scripts/generate-templates.mjs
@bb/templates:generate:templates:
@bb/templates:generate:plugin-scaffold: cache hit, replaying logs 57fcb8a3dae79417
@bb/templates:generate:plugin-scaffold:
@bb/templates:generate:plugin-scaffold: > @bb/templates@0.0.1 generate:plugin-scaffold <checkout-1>/packages/templates
@bb/templates:generate:plugin-scaffold: > node ./scripts/generate-plugin-scaffold.mjs
@bb/templates:generate:plugin-scaffold:
//:ensure-native-modules:
//:ensure-native-modules: > bb@ ensure-native-modules <checkout-2>
//:ensure-native-modules: > node scripts/ensure-native-modules.mjs
//:ensure-native-modules:
@bb/app:test: cache miss, executing cf090ac4fd46ecc7
@bb/app:test:
@bb/app:test: > @bb/app@0.0.1 test <checkout-2>/apps/app
@bb/app:test: > node scripts/generate-pwa-icons.mjs --check && vitest run --config vitest.config.ts "src/components/secondary-panel/ThreadSecondaryPanel.collapseControl.test.tsx" "-t" "only mounts the unavailable fallback"
@bb/app:test:
@bb/app:test:
@bb/app:test: RUN v4.1.1 <checkout-2>/apps/app
@bb/app:test:
@bb/app:test: stderr | src/components/secondary-panel/ThreadSecondaryPanel.collapseControl.test.tsx > ThreadSecondaryPanel unavailable content > only mounts the unavailable fallback while open (drawer=false)
@bb/app:test: WARNING: Panel defaultSize prop recommended to avoid layout shift after server rendering
@bb/app:test:
@bb/app:test: stderr | src/components/secondary-panel/ThreadSecondaryPanel.collapseControl.test.tsx > ThreadSecondaryPanel unavailable content > only mounts the unavailable fallback while open (drawer=true)
@bb/app:test: WARNING: Panel defaultSize prop recommended to avoid layout shift after server rendering
@bb/app:test:
@bb/app:test: ❯ |@bb/app:isolated| src/components/secondary-panel/ThreadSecondaryPanel.collapseControl.test.tsx (24 tests | 2 failed | 22 skipped) 83ms
@bb/app:test: × only mounts the unavailable fallback while open (drawer=false) 66ms
@bb/app:test: × only mounts the unavailable fallback while open (drawer=true) 15ms
@bb/app:test: ↓ renders the available tab while persisted active state catches up
@bb/app:test: ↓ renders arbitrary fixed-tab content through the shared surface
@bb/app:test: ↓ renders plugin fixed tabs concurrently when they are split
@bb/app:test: ↓ renders each split pane from the descriptor attached to its tab
@bb/app:test: ↓ retains the active file body after the persistent drawer closes
@bb/app:test: ↓ renders one active compact body and restores the saved wide split
@bb/app:test: ↓ is absent when unsplit and appears at the trailing edge of every split pane
@bb/app:test: ↓ keeps Info and Diff open when removing their split from the focused control
@bb/app:test: ↓ falls back from an ineligible active Diff tab to Info
@bb/app:test: ↓ keeps an active Diff tab visible while Git eligibility loads
@bb/app:test: ↓ leaves only the trailing panel toggle on the full-page compact panel
@bb/app:test: ↓ keeps the panel seam visible while the clipped panel surface moves
@bb/app:test: ↓ keeps Full Screen before Hide right panel in the trailing toolbar
@bb/app:test: ↓ expands the panel while the conversation is shown
@bb/app:test: ↓ restores the conversation from the same slot while it is collapsed
@bb/app:test: ↓ offers every existing split position from the right-panel control and moves the active tab
@bb/app:test: ↓ maximizes one stacked pane while keeping both tab rows mounted
@bb/app:test: ↓ advertises the bound command on the Full Screen control
@bb/app:test: ↓ advertises the bound command on the Maximize pane control
@bb/app:test: ↓ keeps the shortcut off hosts that do not handle the command
@bb/app:test: ↓ focuses New tab without opening it and resumes cycling from the button
@bb/app:test: ↓ ignores tab navigation while chat maximize suppresses the panel and resumes after restore
@bb/app:test:
@bb/app:test: ⎯⎯⎯⎯⎯⎯⎯ Failed Tests 2 ⎯⎯⎯⎯⎯⎯⎯
@bb/app:test:
@bb/app:test: FAIL |@bb/app:isolated| src/components/secondary-panel/ThreadSecondaryPanel.collapseControl.test.tsx > ThreadSecondaryPanel unavailable content > only mounts the unavailable fallback while open (drawer=false)
@bb/app:test: FAIL |@bb/app:isolated| src/components/secondary-panel/ThreadSecondaryPanel.collapseControl.test.tsx > ThreadSecondaryPanel unavailable content > only mounts the unavailable fallback while open (drawer=true)
@bb/app:test: AssertionError: expected <div …(1)></div> to be null
@bb/app:test:
@bb/app:test: - Expected:
@bb/app:test: null
@bb/app:test:
@bb/app:test: + Received:
@bb/app:test: <div
@bb/app:test: class="border border-dashed border-border px-3 py-6 text-center text-sm text-muted-foreground m-4 rounded-lg"
@bb/app:test: >
@bb/app:test: This panel view is unavailable.
@bb/app:test: </div>
@bb/app:test:
@bb/app:test: ❯ src/components/secondary-panel/ThreadSecondaryPanel.collapseControl.test.tsx:215:9
@bb/app:test: 213| expect(
@bb/app:test: 214| screen.queryByText("This panel view is unavailable."),
@bb/app:test: 215| ).toBeNull();
@bb/app:test: | ^
@bb/app:test: 216|
@bb/app:test: 217| unmount();
@bb/app:test:
@bb/app:test:
@bb/app:test: ⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯[1/2]⎯
@bb/app:test:
@bb/app:test: Test Files 1 failed (1)
@bb/app:test: Tests 2 failed | 22 skipped (24)
@bb/app:test: Start at 18:19:43
@bb/app:test: Duration 30.15s (transform 27.53s, setup 1.72s, import 27.49s, tests 83ms, environment 658ms)
@bb/app:test:
@bb/app:test: ELIFECYCLE Test failed. See above for more details.
@bb/app#test: ERROR command (<checkout-2>/apps/app) /usr/local/bin/pnpm run test src/components/secondary-panel/ThreadSecondaryPanel.collapseControl.test.tsx -t only mounts the unavailable fallback exited (1)
Tasks: 3 successful, 4 total
Cached: 2 cached, 4 total
Time: 32.559s
Failed: @bb/app#test
ERROR run failed: command exited (1)
Raw local evidence is retained outside the public reports repository. The complete runnable patch and expected/actual output are embedded in this HTML, following that repository's publication policy.
Browser Automation setup result: Install Chrome/Chromium on this host or link its executable at the plugin data runtime/chrome path.
Trust handling: issue text was treated only as untrusted claims. All executable repository code came from trusted origin/main; the regression and proposed fix were authored from repository evidence. No issue-supplied command, URL, script, patch, or branch was executed.
> AGENT GENERATED