Skip to content

Keep app shortcuts alive next to a retained closed drawer - #1829

Merged
ymichael merged 1 commit into
mainfrom
bb/fix-global-new-thread-shortcut-routing-1812-thr_jiwn6msgbt
Aug 18, 2026
Merged

ymichael merged 1 commit into
mainfrom
bb/fix-global-new-thread-shortcut-routing-1812-thr_jiwn6msgbt

Conversation

@ymichael

Copy link
Copy Markdown
Collaborator

Root cause

AppCommandProvider.hasOpenModal() treated any [aria-modal="true"] node as an open modal. bb's compact sidebar drawer (SidebarMobilePanel) intentionally stays mounted across open/close and only marks its panel inert while closed (#1261), so that panel matched the selector permanently.

Result: on any window narrower than the 767px compact breakpoint — reachable on desktop, where MIN_WINDOW_WIDTH is 500 — modalOpen was stuck true for the whole session, and every mainSurface binding that excludes modalOpen (thread.new, panel.toggle, terminal.open, sidebar.toggle, …) silently declined, exactly the "observed behavior is narrower than the declared context" symptom reported in #1812.

Fix

In the shared app-command path only: a candidate counts as an open modal only when it is neither inside an [inert] subtree nor data-state="closed". A drawer or dialog that is genuinely open still suppresses commands.

Validation

Live in the dev app (scripts/bb-dev-app, 700px-wide window, thread route), before the fix:

  • focus in the timeline/body: thread.new, panel.toggle, terminal.open all no-ops (defaultPrevented === false), the retained node reporting role=dialog state=closed inert=true
  • focus in the composer: also a no-op

After the fix, all of them run with focus in the timeline, on a sidebar button, and in the composer, and with the drawer actually open thread.new is still suppressed.

Unit coverage added in AppCommandProvider.test.tsx (fails without the fix): thread.new and panel.toggle dispatch from a timeline row, a sidebar entry, and a contenteditable composer while a closed, inert drawer is mounted; a genuinely open drawer still suppresses.

turbo run test --filter=@bb/app (commands, promptbox shortcuts, app-keybindings, root-compose handlers: 41 passed), turbo run typecheck lint --filter=@bb/app (0 errors).

Note for the reporter

I could not reproduce the composer-only asymmetry described in the issue on a wide window: with the real desktop shell (Electron, CDP-driven), on web, and with a Linux-platform-emulated renderer, thread.new and panel.toggle already dispatched from body, sidebar-button, and timeline focus. The defect above is the one path that does kill those exact commands while the effective keybindings still advertise them. If the original report was on a wide window, please reopen with the window size and whether the sidebar was a drawer.

Two adjacent gaps found while tracing, left out as separate concerns:

  • the desktop native-menu path dispatches thread.new/panel.newTab/settings.open via IPC with no context evaluation and a null target, unlike every renderer-dispatched chord;
  • when the desktop browser panel's WebContentsView has focus, resolveDesktopBrowserAppCommand only resolves browserFocus-scoped bindings, so global chords are dropped there.

Closes #1812

AGENT GENERATED: by Claude Opus 5

@bb-slop-cop

bb-slop-cop Bot commented Aug 18, 2026

Copy link
Copy Markdown
Contributor

🚨 SLOP COP 🚨 · review

I am SlopCop. I am reviewing this pull request now.

This change keeps app shortcuts active after a closed drawer stays mounted for faster future use.

I will check security, code quality, performance, architecture, tests, and the user flow.

renderProvider(
<>
<Handler command={command} name={command} result={true} />
<div role="dialog" aria-modal="true" data-state="closed" inert>

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🚨 slopcop/review — The test combines both new modal exclusions.

This fixture sets both data-state="closed" and inert.

If either branch fails, the other branch keeps the test green.

Could you split this into one closed-state case and one inert-ancestor case?

@bb-slop-cop bb-slop-cop Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🚨 SLOP COP 🚨 · review

Plain-English summary: This change keeps app shortcuts active when the compact sidebar remains in the document after it closes.

It still blocks those shortcuts while the drawer is open.

I found no blocking security, correctness, or performance issue.

I left one non-blocking inline note. The new test combines both modal exclusions, so one exclusion can hide a failure in the other.

The modal check now scans all matching elements after a shortcut match. Chrome measured 0.392 ms with 5,000 synthetic nodes.

Chrome measured 1.899 ms with 20,000 synthetic nodes. This small cost does not block the change.

The architecture scan found no duplicate modal detector. The sidebar and shared drawer use different closed-state accessibility attributes.

A later change can align those attributes and reduce future contract drift.

Validation completed:

  • The command-provider test passed all 18 tests.

  • The retained-sidebar test passed all 13 tests.

  • The @bb/app type check passed.

  • git diff --check passed.

  • All active GitHub checks passed.

  • A browser test used a 390 by 844 viewport.

  • The closed drawer stayed mounted with inert, aria-modal, and data-state="closed".

  • Meta+Shift+O opened the new-thread composer while that drawer stayed closed.

  • The same shortcut stayed blocked after the drawer opened.

The security, quality, performance, and final review agents used GPT-5.6-Sol with high reasoning.

`hasOpenModal` matched any `[aria-modal="true"]` node, but the compact
sidebar drawer stays mounted across open/close and is only `inert` while
closed. On a window narrower than the 767px compact breakpoint (desktop
windows go down to 500px), that retained panel kept `modalOpen` on for
the whole session, so every `mainSurface` chord that excludes `modalOpen`
— thread.new, panel.toggle, terminal.open, sidebar.toggle — silently
declined even though the effective keybindings said otherwise.

Treat a candidate as an open modal only when it is neither `inert` nor
`data-state="closed"`, so a genuinely open drawer still suppresses.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@ymichael
ymichael force-pushed the bb/fix-global-new-thread-shortcut-routing-1812-thr_jiwn6msgbt branch from 240ffe3 to 49b9443 Compare August 18, 2026 22:55
@ymichael
ymichael merged commit 2b66b17 into main Aug 18, 2026
10 checks passed
@ymichael
ymichael deleted the bb/fix-global-new-thread-shortcut-routing-1812-thr_jiwn6msgbt branch August 18, 2026 22:59
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.

New-thread shortcuts only work when the prompt composer is focused

1 participant