feat(desktop): close Buzz window with Cmd+W (#6314)

## Summary

- close the current Buzz window with `Cmd-W` on macOS
- keep Buzz Term's existing `Cmd-W` behavior when the terminal owns
input, where it closes the active terminal tab
- cover the close-window chord and its modifier/composition guards with
unit tests

## Why

Buzz removes macOS's native Close Window menu accelerator so the webview
can conditionally route `Cmd-W` to Buzz Term. That also left normal Buzz
mode without the standard close-window shortcut. The new app-level
bubble-phase handler restores window closing while allowing the
terminal's capture-phase handler to retain priority.

For the main window, the existing Rust close-request handler keeps the
webview alive and hides it to the tray, matching the titlebar close
button.

## Validation

- desktop unit tests: 5,093 passed
- focused close-window and terminal shortcut tests: 28 passed at
`71a2cce42013440e297c7b237d45b20e9638078d`
- desktop TypeScript: passed
- desktop checks: passed (pre-existing warnings only)
- workspace Rust clippy: passed
- Tauri Rust clippy/check: passed
- desktop build: passed
- mobile format/analyze: passed
- Tauri tests: 2,592 desktop tests and 26/27 terminal tests passed; the
existing `default_prog_child_observes_the_login_argv0` PTY fixture
repeatedly timed out waiting for `/bin/sh`, including when run alone,
and is unrelated to this TypeScript shortcut change

---------

Signed-off-by: Matt Kursmark <kursmark@squareup.com>
Co-authored-by: Codex <noreply@openai.com>
This commit is contained in:
Matt Kursmark
2026-08-20 11:31:39 -04:00
committed by GitHub
parent 2a236e4137
commit e5d1dfef7b
4 changed files with 106 additions and 10 deletions

View File

@@ -5,23 +5,19 @@
//! `close_window` item in both the File and Window submenus, and muda gives
//! that item a Cmd+W key equivalent bound to `performClose:`.
//!
//! Two consequences, both wrong for Buzz:
//! That default cannot express Buzz's context-dependent behavior:
//!
//! 1. `CloseRequested` on the main window is intercepted in `lib.rs` and turned
//! into hide-to-tray, so Cmd+W never closed a window -- it hid the whole
//! app. That is already redundant with Cmd+H (Hide), which stays.
//! into hide-to-tray. Cmd+W should take that path in normal Buzz mode.
//! 2. macOS resolves a menu key equivalent before the webview receives any key
//! event, so Buzz Term could never bind Cmd+W to "close this terminal tab"
//! while the accelerator was claimed here.
//!
//! So this module builds the standard menu minus both `close_window` items.
//! Everything else matches `Menu::default()` deliberately: the goal is to drop
//! one item, not to design a menu.
//!
//! If hide-on-Cmd+W is ever wanted back in Buzz mode, the revisit path is to
//! restore the item and disable it while the terminal owns input (a disabled
//! item does not consume its key equivalent) -- at the cost of an owner->Rust
//! IPC hop this approach does not need.
//! Everything else matches `Menu::default()` deliberately. The webview routes
//! Cmd+W conditionally instead: Buzz Term consumes it in capture phase while
//! it owns input, and `useCloseWindowShortcut` closes the current window in
//! normal Buzz mode.
#[cfg(target_os = "macos")]
use tauri::menu::{

View File

@@ -21,6 +21,7 @@ import { deriveShellRoute } from "@/app/AppShell.helpers";
import { ThemeGrainientBackground } from "@/app/ThemeGrainientBackground";
import { CommunityThemeController } from "@/shared/theme/CommunityThemeController";
import { useReloadShortcut } from "@/app/useReloadShortcut";
import { useCloseWindowShortcut } from "@/app/useCloseWindowShortcut";
import { KnownAgentPubkeysProvider } from "@/features/agents/useKnownAgentPubkeys";
import { huddleWindowChannelId } from "@/features/huddle/lib/huddleWindow";
import { useAppOnboardingState } from "@/features/onboarding/hooks";
@@ -769,6 +770,7 @@ function MachineBootstrap({ sharedIdentity }: { sharedIdentity: boolean }) {
export function App() {
useReloadShortcut();
useCloseWindowShortcut();
useInitialRenderReady();
const [sharedIdentity, setSharedIdentity] = useState<boolean | null>(null);
const [queryClient] = useState(createBuzzQueryClient);

View File

@@ -0,0 +1,43 @@
import assert from "node:assert/strict";
import test from "node:test";
import { isCloseWindowShortcut } from "./useCloseWindowShortcut.ts";
function chord(overrides = {}) {
return {
altKey: false,
code: "KeyW",
ctrlKey: false,
defaultPrevented: false,
isComposing: false,
metaKey: true,
repeat: false,
shiftKey: false,
...overrides,
};
}
test("Cmd+W closes a window on macOS", () => {
assert.equal(isCloseWindowShortcut(chord(), true), true);
});
test("the close-window shortcut rejects other platforms and modified chords", () => {
assert.equal(isCloseWindowShortcut(chord(), false), false);
assert.equal(isCloseWindowShortcut(chord({ metaKey: false }), true), false);
assert.equal(isCloseWindowShortcut(chord({ ctrlKey: true }), true), false);
assert.equal(isCloseWindowShortcut(chord({ altKey: true }), true), false);
assert.equal(isCloseWindowShortcut(chord({ shiftKey: true }), true), false);
assert.equal(isCloseWindowShortcut(chord({ code: "KeyQ" }), true), false);
});
test("handled, composing, and repeated events are left alone", () => {
assert.equal(
isCloseWindowShortcut(chord({ defaultPrevented: true }), true),
false,
);
assert.equal(
isCloseWindowShortcut(chord({ isComposing: true }), true),
false,
);
assert.equal(isCloseWindowShortcut(chord({ repeat: true }), true), false);
});

View File

@@ -0,0 +1,55 @@
import * as React from "react";
import { isTauri } from "@tauri-apps/api/core";
import { getCurrentWindow } from "@tauri-apps/api/window";
import { isMacPlatform } from "@/shared/lib/platform";
type CloseWindowChord = Pick<
KeyboardEvent,
| "altKey"
| "code"
| "ctrlKey"
| "defaultPrevented"
| "isComposing"
| "metaKey"
| "repeat"
| "shiftKey"
>;
export function isCloseWindowShortcut(
event: CloseWindowChord,
isMac: boolean,
): boolean {
return (
isMac &&
event.code === "KeyW" &&
event.metaKey &&
!event.ctrlKey &&
!event.altKey &&
!event.shiftKey &&
!event.defaultPrevented &&
!event.isComposing &&
!event.repeat
);
}
/**
* Restores the standard macOS Cmd+W behavior without reclaiming the native
* menu accelerator. Buzz Term handles the chord first in capture phase while
* it owns input; otherwise this bubble-phase listener closes the current
* window. The main window's Rust close handler turns that into hide-to-tray.
*/
export function useCloseWindowShortcut() {
React.useEffect(() => {
if (!isTauri()) return;
function handleKeyDown(event: KeyboardEvent) {
if (!isCloseWindowShortcut(event, isMacPlatform())) return;
event.preventDefault();
void getCurrentWindow().close();
}
window.addEventListener("keydown", handleKeyDown);
return () => window.removeEventListener("keydown", handleKeyDown);
}, []);
}