Skip to content

Restore plugin pane chrome and the Docs plugin sidebar - #1036

Merged
SawyerHood merged 2 commits into
mainfrom
bb/investigate-broken-docs-plugin-thr_i558bwu73m
Aug 6, 2026
Merged

Restore plugin pane chrome and the Docs plugin sidebar#1036
SawyerHood merged 2 commits into
mainfrom
bb/investigate-broken-docs-plugin-thr_i558bwu73m

Conversation

@SawyerHood

Copy link
Copy Markdown
Collaborator

Summary

Four regressions left plugin panes with broken chrome. Three came from #991; one predates it. Each was reproduced in the dev app before the fix.

Docs sidebar looked like the host's shared sidebar. #991 restyled it to bg-sidebar and painted a fake background up into the app header. #1019 then gave every header its seam, which that background cut short. The sidebar sits below the header again, with the seam edge to edge and the plugin's own bg-muted/20 tint.

The Docs header toggle did nothing. #991 deleted the shared sidebar store once its mounts hit zero. The header and the sidebar live in separate React subtrees and find each other only through that module-level map, so a deleted entry left each subtree on a different store object: two toggles rendered, and the header one was dead (measured 288px to 288px on click). Entries now live for the session. A reopened pane still starts clean, because the view resets the state on its first mount.

Plugin panes lost full screen, move-to-side, and their corner reserve. #991 excluded plugin-panel content from all three. Without the reserve, the host's pinned panel toggle landed on Close pane whenever a plugin pane held the workspace corner — measured overlap of 22 of 28px, with the toggle winning the click. Position alone decides the reserve now, and plugin panes carry the same pane controls as every other pane.

A pane with no right panel lost its toggle and placeholder. #736 shipped both. #962 disabled the button and deleted the placeholder; #991 removed the control outright. Both are restored: the toggle always works (including Ctrl+J), and a pane that publishes no panel shows This pane has no right panel. in a resizable panel whose width persists. Since that empty state has no chrome of its own, the corner button stays visible while it is open — otherwise no button could close it. The host exposes this as pinsCornerToggle, and pane headers reserve the corner from the same flag.

Two smaller fixes in the same area:

  • The reserve no longer leaves an empty 28px slot when the host hides its toggle. This showed as dead space beside Exit Full Screen and predates Paginate skill detail content #991 (it came in with BB-45: Maximize and restore individual split panes #781).
  • The pinned toggle moved from right-2.5 to right-4, so it shares the pane header's px-4 action axis. Measured after: Full Screen 1652–1680, Close pane 1684–1712, toggle 1716–1744 — an even 4px rhythm, with the toggle landing exactly in the reserved slot.

Tests

  • turbo run test --filter=@bb/app: 2364 pass (315 files).
  • turbo run test --filter=bb-plugin-simple-notes: 59 pass.
  • turbo run typecheck passes for @bb/app and bb-plugin-simple-notes. Lint reports 0 errors.
  • New regression tests: the corner reserve for a plugin pane at the top right (verified failing against the old condition), the reserve dropping while a full-screen pane hides the toggle, and the empty panel state with a live toggle.
  • Updated the tests from Polish pane controls and browser chrome #962 and Paginate skill detail content #991 that locked in the removed controls.

QA

Checked in the branch dev app with the Docs, Tasks, and GitHub plugins installed: the header seam, a single working Docs toggle, plugin full screen and restore, no button collision, the empty-state panel opening and collapsing to 0px, and even header spacing.

Risks

Restoring move-to-side for plugin panes was not requested directly. It came out in the same #991 block as full screen, so I put it back with the rest. Say the word if it should stay off.

🤖 Generated with Claude Code

Four regressions landed together and left plugin panes with broken
chrome. Three came from #991, one predates it.

Docs sidebar. #991 restyled the sidebar to bg-sidebar and painted a
fake background up into the app header. #1019 then gave every header a
seam, which that background cut short, so the plugin's own panel read
as the host's shared sidebar. The sidebar sits below the header again
and the seam runs edge to edge.

Docs header toggle. #991 also deleted the shared sidebar store when its
mounts hit zero. The header and the sidebar live in separate React
subtrees and find each other only through that map, so a deleted entry
left each on its own store: two toggles, and the header one did
nothing. Entries now live for the session; a reopened pane still starts
clean because the view resets state on its first mount.

Plugin pane controls. #991 stripped full screen and move-to-side from
plugin panes, and dropped their reserve for the host's pinned panel
toggle. That toggle then landed on Close pane whenever a plugin pane
held the workspace corner. Position alone decides the reserve now, and
plugin panes carry the same pane controls as any other pane.

Empty right panel. #962 disabled the panel toggle for panes that
publish no panel and deleted the placeholder; #991 removed the control
outright. Both are back from #736: the toggle always works, and a pane
without a panel shows "This pane has no right panel." The corner button
stays visible while that empty state is open, since it has no chrome of
its own to close from.

Also: the reserve no longer trails an empty slot when the host hides
its toggle (a full-screen pane), and the corner button moved to right-4
so it shares the pane header's action axis.

Tests: 2364 pass in @bb/app; 59 in bb-plugin-simple-notes. Typecheck
clean, lint reports no errors. Verified in the dev app: header seam,
one working Docs toggle, plugin full screen and restore, no button
collision, the empty-state panel, and even header spacing.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@SawyerHood

SawyerHood commented Aug 5, 2026

Copy link
Copy Markdown
Collaborator Author

🚨 SLOP COP 🚨 · review

I am the SlopCop. I am reviewing this pull request for security, code quality, performance, architecture, and end-to-end behavior.

The stack contains PR #1036 only. I will post a comment after each review phase.

@SawyerHood

Copy link
Copy Markdown
Collaborator Author

🚨 SLOP COP 🚨 · review

Security phase complete.

I found no security issues. The new controls use host-owned pane IDs. The empty panel adds no untrusted HTML, URL, or filesystem input.

I also checked plugin trust boundaries, composer scope, store keys, and denial-of-service risks. git diff --check passed.

@SawyerHood

Copy link
Copy Markdown
Collaborator Author

🚨 SLOP COP 🚨 · review

Performance phase complete.

I found one low-severity issue. notesSidebarStores keeps inactive pane and vault entries for the full application session.

The map can grow across many Docs vaults. A bounded cache or safe delayed cleanup would prevent this long-session heap growth.

I found no other performance issue. All GitHub checks passed.

@SawyerHood

Copy link
Copy Markdown
Collaborator Author

🚨 SLOP COP 🚨 · review

Code quality and architecture phase complete.

I found one blocking lifecycle defect. An empty panel starts at zero size for a focused plugin pane.

The panel can call onCollapse during its first mount. That call changes the visibility sentinel from null to false.

A later thread pane then closes its persisted-open panel. The current panel mock does not reproduce this library callback.

The code should ignore collapse until the empty panel first reports a positive size. A regression test should cover the plugin-first path.

@SawyerHood

Copy link
Copy Markdown
Collaborator Author

🚨 SLOP COP 🚨 · review

End-to-end phase complete on the top PR.

I installed the Docs plugin in an isolated development instance. I opened Docs beside a new-thread pane.

Both panes showed Full Screen and Close pane controls. The Docs sidebar expanded to 288 pixels and showed its action toolbar.

The plugin pane entered and left full-screen mode. The host toggle opened and closed the empty right-panel state.

The visual check showed no control overlap. This path passed.

defaultSize={isOpen ? panelWidthPercent : 0}
minSize={THREAD_SECONDARY_PANEL_MIN_SIZE_PERCENT}
maxSize={THREAD_SECONDARY_PANEL_MAX_SIZE_PERCENT}
onCollapse={handleEmptyPanelCollapse}

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🚨 slopcop/review — The initial collapse overwrites the visibility sentinel.

This panel starts at zero size when a plugin pane has focus. React Resizable Panels calls onCollapse for that initial size.

The callback changes isPanelVisible from null to false. A later persisted-open thread then closes instead of adopting its saved state.

Ignore collapse until this panel first reports a positive size. Add a test that calls onCollapse for the initial zero size.

// deleted entry lets one subtree keep the old store while the other creates a
// new one, which silently breaks the header toggle. A reopened pane starts
// clean anyway, because the view resets the state on its first mount.
const notesSidebarStores = new Map<string, NotesSidebarStore>();

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🚨 slopcop/review — This map keeps inactive stores for the full application session.

Each distinct pane and vault key adds a state object and listener set. Unmounting both React subtrees no longer removes the entry.

Use safe delayed cleanup or a bounded cache. Cancel cleanup when either subtree mounts again.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks — I looked at this one and am leaving it as is. Deleting entries is what broke the toggle in the first place, so I would rather pay a small bound than reintroduce that failure mode.

The key is ${paneScope}:${vaultKey}. Pane scopes come from live split-pane IDs, capped at 8 panes, and vault keys come from the user's own vault list. So the map tops out at a few dozen entries per session, each a small object plus an empty listener set. It grows only when the user opens new panes or vaults, not on a loop.

Delayed cleanup with cancellation would restore the race in a subtler form: the header and the sidebar mount in separate React subtrees with no ordering guarantee, so any window where the entry is gone can hand the two subtrees different store objects again. That is exactly the desync this PR fixes — a dead header toggle and a duplicate control, with no error to point at it.

If the map ever needs bounding, the safe shape is a cap on total entries with eviction of keys whose panes no longer exist in the split layout, checked against the live layout rather than against mount counts. That is worth doing when there is evidence of real growth; today it would add machinery and risk for a few dozen small objects.

const toggleLabel = isOpen ? "Hide right panel" : "Show right panel";
// An open pane panel carries the toggle in its own chrome, and a full-screen
// pane hides it. The empty state has no chrome, so it keeps the corner.
const pinsCornerToggle = !isPaneMaximized && !(isOpen && model !== null);

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🚨 slopcop/review — An open empty panel leaves a blank header action slot.

The empty panel contains the visible toggle. The separate right-edge plugin pane still reserves 28 pixels for that toggle.

Reserve this slot only when the closed panel places the toggle over the main pane. Add an open-empty-panel test.

@SawyerHood SawyerHood left a comment

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🚨 SLOP COP 🚨 · review

Review complete for the one-PR stack: #1036.

I found one blocking defect and two low-severity defects.

  1. The initial empty-panel collapse clears the null visibility sentinel.
  2. The Docs sidebar store map grows for the full application session.
  3. An open empty panel leaves a blank 28-pixel plugin header slot.

The first defect can close a persisted-open thread panel after a plugin pane first gains focus.

The fix should ignore empty-panel collapse until that panel first reports a positive size. A regression test must simulate the initial collapse callback.

I found no security issue.

The empty panel repeats collapse and resize behavior from ThreadSecondaryPanel. A shared lifecycle helper could prevent more behavior differences.

Local validation passed:

  • The app test passed 38 tests.
  • The Docs test passed 24 tests.
  • Both package typechecks passed.
  • All GitHub checks passed.
  • The browser test passed the Docs sidebar, split chrome, full-screen, and empty-panel flows.

The GPT-5.6 final gate returned REQUEST CHANGES. I posted a comment-only review and did not use GitHub approval controls.

Initial collapse. The empty panel mounts at zero width for a focused
plugin pane, and react-resizable-panels reports that first layout as a
collapse. Honoring it turned the "adopt the first publisher's state"
sentinel into a hard closed, so the next thread lost its persisted-open
panel. A collapse now counts only after the panel has reported a real
width.

Corner reserve. An open empty panel holds the toggle over itself, but
the right-edge pane header still reserved 28px for it, which left the
blank slot this branch set out to remove. The reserve now follows
whether the button lands on a pane header, which an open panel of
either kind ends.

The panel mock records each panel's lifecycle callbacks, so a test can
fire the initial collapse the real library fires on its own. Both new
tests fail against the previous code.

Tests: 2366 pass in @bb/app. Typecheck clean, lint reports no errors.
Verified in the dev app: closed keeps the reserve, open ends the header
at Close pane with the toggle over the panel.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@SawyerHood
SawyerHood merged commit 43efc6d into main Aug 6, 2026
10 checks passed
@SawyerHood
SawyerHood deleted the bb/investigate-broken-docs-plugin-thr_i558bwu73m branch August 6, 2026 00:16
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant