Skip to content

fix(ui): stop Cmd+W deleting a thread out from under a dialog (#2474) - #2551

Merged
jonathanKingston merged 2 commits into
mainfrom
claude/dialog-guard-cmd-w-2474
Sep 7, 2026
Merged

fix(ui): stop Cmd+W deleting a thread out from under a dialog (#2474)#2551
jonathanKingston merged 2 commits into
mainfrom
claude/dialog-guard-cmd-w-2474

Conversation

@jonathanKingston

Copy link
Copy Markdown
Collaborator

Closes #2474 (P2, area:ui).

The bug

Cmd/Ctrl+W deletes the active thread. Nothing checked whether a dialog was on screen, so closing Settings with the keystroke that closes things everywhere else destroyed a conversation instead. There is no undo.

if (meta && e.key === 'w') {
  e.preventDefault()
  void confirmDeleteThread()   // ← no idea Settings is open
}

The fix

The guard reads the DOM — is any <dialog> open — rather than asking a list of isXOpen() predicates.

That shape is deliberate. The renderer has seventeen dialogs, and the one hand-maintained list of them sits three lines above this in the Cmd/Ctrl+F handler, naming four: settings, the command palette, file search, keyboard shortcuts. That list is exactly the failure the issue anticipates when it says "or any other dialog for that matter", so the check is built so a new dialog is covered the moment it exists, with nobody having to remember to register it.

A behaviour change worth noticing: the find bar now uses the same check, so it also defers to the thirteen dialogs the list missed — an approval, an SSH passphrase, a confirm. Each is a question it should not open underneath, but it is a change beyond the reported bug, so push back if you'd rather I left the four-item list alone.

show() counts as well as showModal(): the approval prompt appears inline over the chat rather than modally, and it is still something the user is answering.

preventDefault() stays outside the guard

It is what keeps the keystroke from also reaching macOS's File ▸ Close accelerator (app-menu-file-items.ts registers { role: 'close' } there). Moving it inside would hand Cmd+W back to the menu exactly when a dialog is up. So the keystroke is still swallowed; only the delete is skipped.

Doing nothing is the right answer rather than closing the dialog for them — Esc already does that, in the handler immediately below, and every dialog honours it.

Left alone deliberately

The shortcut is matched inline as meta && e.key === 'w' rather than through a matchXShortcut helper like its neighbours in keyboard-shortcuts.ts. One consequence: with Caps Lock on, e.key is 'W' and the shortcut silently does nothing.

Routing it through a matcher would be tidier and testable in the same place as its siblings — but it would also widen which keystrokes delete a thread, and that is the wrong direction to move a destructive shortcut without being asked. Happy to do it if you want it.

Testing

main.ts binds its shortcuts inside a boot function with no seam to call, so the wiring is pinned at the source level — the file's existing test does the same for the layout boot — while the guard itself is unit-tested against real <dialog> elements in happy-dom.

The wiring assertions test the property, not the text: every confirmDeleteThread() call site is guarded, so a second unguarded one added later fails the suite rather than slipping past an assertion that only looked at the first.

Check Result
node scripts/run-tests.mts (whole repo) 8641 tests, 8631 pass, 10 skipped, 0 fail
tsc --noEmit (node + web) clean
pnpm run lint / oxfmt --check . clean
check:dead-code (840 modules) / check:oracle (243 specs, 15 invariants) clean
check:e2e-syntax (269 files) / demo:site:check clean
all three wiring assertions with the guard removed fail, as designed

9 new tests: 5 on isAnyDialogOpen (closed dialog, modal, non-modal, several at once, and one inserted as raw HTML that nothing registered), 3 on the Cmd+W / Cmd+F wiring, and the existing suite unchanged.

Two local-environment notes, neither affecting CI: reaching a full run needs an uncommitted shim over node-pty's native loader (it cannot be rebuilt in this sandbox — the agent proxy denies iojs.org and www.electronjs.org), reverted before committing; and this container's node_modules predates #2383, so patches/@anthropic-ai__sandbox-runtime@0.0.74.patch was not applied and git-commit-signing.test.ts failed until I applied its one-line hunk by hand. That failure reproduces identically on 52c839a with my changes stashed, so it is a stale install here, not a base-branch break.

Refs #2474


🤖 Generated with Claude Code

https://claude.ai/code/session_01Hpk1gEma9LhMUuvr2TwUj6


Generated by Claude Code

claude and others added 2 commits September 7, 2026 19:35
Cmd/Ctrl+W deletes the active thread. Nothing checked whether the user was
looking at a dialog when they pressed it, so closing Settings with the keystroke
that closes things everywhere else destroyed a conversation instead. There is no
undo.

The guard reads the DOM — is any `<dialog>` open — rather than asking a list of
`isXOpen()` predicates. The renderer has seventeen dialogs and the one
hand-maintained list of them, three lines above this in the Cmd/Ctrl+F handler,
named four: settings, the command palette, file search, keyboard shortcuts. That
list is the failure mode the issue anticipates when it says "or any other dialog
for that matter", so the fix is shaped so a new dialog is covered the moment it
exists. The find bar now uses the same check, which is a behaviour change worth
noticing: it also defers to the thirteen the list missed — an approval, an SSH
passphrase, a confirm — each a question it should not open underneath.

`show()` counts as well as `showModal()`. The approval prompt appears inline over
the chat rather than modally, and it is still something the user is answering.

`preventDefault()` deliberately stays outside the guard. It is what keeps the
keystroke from also reaching macOS's File ▸ Close accelerator, and moving it
inside would hand Cmd+W back to the menu exactly when a dialog is up. So the
keystroke is still swallowed; only the delete is skipped. Doing nothing is the
right answer rather than closing the dialog for them — Esc already does that,
in the handler immediately below, and every dialog honours it.

Left alone deliberately: the shortcut is matched inline as `meta && e.key === 'w'`
rather than through a `matchXShortcut` helper like its neighbours, so Caps Lock
(which makes `e.key` `'W'`) silently disables it. Routing it through a matcher
would be tidier, but it would also widen which keystrokes delete a thread, and
that is the wrong direction to move a destructive shortcut without being asked.

main.ts binds its shortcuts inside a boot function with no seam to call, so the
wiring is pinned at the source level — the file's existing test does the same
for the layout boot — while the guard itself is unit-tested against real
`<dialog>` elements. The wiring assertions check the property rather than the
text: every `confirmDeleteThread()` call site is guarded, not just the one that
exists today.

Verified locally: 8641 tests, 8631 pass, 10 skipped, 0 fail, plus typecheck,
lint, format, dead-code, oracle, e2e-syntax and demo:site. All three wiring
assertions were confirmed to fail with the guard removed.

Refs #2474

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Hpk1gEma9LhMUuvr2TwUj6
@jonathanKingston
jonathanKingston force-pushed the claude/dialog-guard-cmd-w-2474 branch from c97d6a0 to 79f212a Compare September 7, 2026 18:41
@jonathanKingston
jonathanKingston marked this pull request as ready for review September 7, 2026 18:41
github-actions Bot added a commit that referenced this pull request Sep 7, 2026
@github-actions

github-actions Bot commented Sep 7, 2026

Copy link
Copy Markdown
Contributor

🖥️ PR preview

@copse-release-bot

Copy link
Copy Markdown
Contributor

Reference screenshots ready for review

Review GitHub’s image diffs in screenshot PR #2562.
Merge it (or enable auto-merge) to apply the accepted PNGs to claude/dialog-guard-cmd-w-2474.

Rendered for 79f212ae878a by CI run 34152645664; the immutable artifact is reference-screenshot-candidates-34152645664.

If this source branch moves, a later successful render closes the stale review PR and replaces this link.

@jonathanKingston
jonathanKingston merged commit ca3c718 into main Sep 7, 2026
24 of 36 checks passed
@jonathanKingston
jonathanKingston deleted the claude/dialog-guard-cmd-w-2474 branch September 7, 2026 19:29
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.

[bug] Cmd+W deletes a thread while a dialog is open

2 participants