Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
34 commits
Select commit Hold shift + click to select a range
f5c634c
feat(macos): add system menu and shortcuts
WilliamWang1721 Sep 24, 2026
cc12b01
style: match Rust formatter for macOS menu
WilliamWang1721 Sep 24, 2026
6b9e224
fix: route macOS menus through platform capability
WilliamWang1721 Sep 24, 2026
bfc1001
fix(macos): use existing fullscreen message in menu
WilliamWang1721 Sep 24, 2026
9a70557
fix(macos): allow settings refresh before global registration
WilliamWang1721 Sep 24, 2026
45fcad4
ci(ui): capture Windows and macOS review screenshots for PR #288
WilliamWang1721 Sep 25, 2026
aca90bd
ci(ui): fix native screenshot capture checks
WilliamWang1721 Sep 25, 2026
bd24f31
ci(ui): publish reviewer screenshots outside PR diff
WilliamWang1721 Sep 25, 2026
a4513de
ci(ui): polish screenshots before PR presentation
WilliamWang1721 Sep 25, 2026
ce9b3b0
Merge current main and align native menus with shared shortcut and qu…
WilliamWang1721 Sep 30, 2026
6a78255
fix(macos): preserve native close on shortcut reset and open About at…
WilliamWang1721 Sep 30, 2026
9e07c9f
Merge latest upstream main for strict PR validation
WilliamWang1721 Sep 30, 2026
7fc57c8
Merge latest upstream main while preserving reviewed feature fixes
WilliamWang1721 Sep 30, 2026
fc2529b
Merge current upstream mobile changes without expanding feature scope
WilliamWang1721 Sep 30, 2026
571e0a3
Merge upstream 2.1.0 and theme improvements while retaining verified …
WilliamWang1721 Sep 30, 2026
d32afe3
Merge current upstream main, preserving reviewed desktop feature beha…
WilliamWang1721 Oct 1, 2026
94dfdf3
Merge upstream main into codex/macos-system-menu
WilliamWang1721 Oct 1, 2026
0b2a335
Merge branch 'main' into codex/macos-system-menu
WilliamWang1721 Oct 3, 2026
f46717a
Merge upstream main into codex/macos-system-menu
WilliamWang1721 Oct 4, 2026
e119052
Merge upstream main into codex/macos-system-menu
WilliamWang1721 Oct 4, 2026
87db773
Merge remote-tracking branch 'origin/main' into HEAD
WilliamWang1721 Oct 6, 2026
1b6e65a
Merge upstream main into codex/macos-system-menu
WilliamWang1721 Oct 11, 2026
4622dd3
Merge upstream main into codex/macos-system-menu
WilliamWang1721 Oct 11, 2026
9a16348
Merge upstream main into codex/macos-system-menu
WilliamWang1721 Oct 13, 2026
a52a039
Merge upstream main into codex/macos-system-menu
WilliamWang1721 Oct 13, 2026
a7f533e
Merge upstream main into codex/macos-system-menu
WilliamWang1721 Oct 14, 2026
2e3465e
Merge upstream main into codex/macos-system-menu
WilliamWang1721 Oct 14, 2026
9a12d34
Merge upstream main into codex/macos-system-menu
WilliamWang1721 Oct 14, 2026
620a716
Merge upstream main into codex/macos-system-menu
WilliamWang1721 Oct 15, 2026
05a4c3e
Merge upstream main into codex/macos-system-menu
WilliamWang1721 Oct 15, 2026
ca11a46
Merge upstream main into codex/macos-system-menu
WilliamWang1721 Oct 16, 2026
9f74d4c
Format keyboard binding test chain
WilliamWang1721 Oct 16, 2026
2edeae5
Merge upstream main into codex/macos-system-menu
WilliamWang1721 Oct 17, 2026
4fd1f0f
Merge upstream main into codex/macos-system-menu
WilliamWang1721 Oct 19, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
# macOS system menu and close shortcuts

## Status

Accepted.

## Context

The GPUI workspace already closes terminal tabs with `cmd-w`, while native window
close must preserve its document, session, and busy-process checks. macOS users
also expect application quit and common editing commands in the system menu.

## Evidence

- [`keyboard_bindings.rs`](../../../../nebula_app/src/gpui_shell/workspace/keyboard_bindings.rs)
registered `cmd-w` for `CloseActiveTerminal`.
- [`closing.rs`](../../../../nebula_app/src/gpui_shell/workspace/closing.rs)
owns the existing window-close checks and persistence path.
- [`shutdown.rs`](../../../../nebula_app/src/gpui_shell/workspace/windowing/shutdown.rs)
owns the save and approval steps before quitting.

## Decision

Use GPUI's native application menus. `cmd-q` calls the shared graceful quit path;
`cmd-w` invokes the workspace close checks; terminal close moves to `cmd-shift-w`.
The app menu exposes About, Settings, Services, Hide, Hide Others, and Quit, with
File, Edit, View, and Window menus alongside it. About explicitly selects the
Application home and clears Settings search. Quit reuses the global `QuitApp`
action. The shared Command alias table moves terminal close to `cmd-shift-w`;
restoring Quit cannot reinstate the legacy `cmd-w` application-quit binding.

## Rejected alternatives

- Keep `cmd-w` for terminal close: conflicts with the expected system window-close
shortcut.
- Call `remove_window()` directly from the menu: bypasses the workspace close
checks and session persistence.
- Call `cx.quit()` directly: bypasses the shared quit approval and save path.

## Consequences

Window-menu close and the native close button share the same close behavior.
The menu is rebuilt when the selected UI language changes. No dependency or saved
format changes are introduced.

## Validation

The production macOS keymap regression checks window close, tab close and Quit
after restoring the configured Quit defaults. Architecture and whitespace checks
pass against the current base. Pinned rustfmt passes; GitHub native compilation,
workspace tests and macOS control acceptance remain required before readiness.

## Supersedes

None.

## Revisit when

The GPUI menu API or workspace shutdown and close flows change.
10 changes: 10 additions & 0 deletions nebula_app/i18n/en-US.json
Original file line number Diff line number Diff line change
Expand Up @@ -378,6 +378,16 @@
"new_folder": "New folder",
"new_tab": "New tab",
"new_window": "New window",
"close_window": "Close Window",
"hide_pebrel": "Hide Pebrel",
"hide_others": "Hide Others",
"quit_pebrel": "Quit Pebrel",
"menu_file": "File",
"menu_edit": "Edit",
"menu_view": "View",
"menu_window": "Window",
"menu_services": "Services",
"menu_minimize": "Minimize",
"close_tab": "Close tab",
"duplicate_tab": "Duplicate tab",
"terminal": "Terminal",
Expand Down
10 changes: 10 additions & 0 deletions nebula_app/i18n/zh-CN.json
Original file line number Diff line number Diff line change
Expand Up @@ -378,6 +378,16 @@
"new_folder": "新建文件夹",
"new_tab": "新建标签",
"new_window": "新建窗口",
"close_window": "关闭窗口",
"hide_pebrel": "隐藏 Pebrel",
"hide_others": "隐藏其他应用",
"quit_pebrel": "退出 Pebrel",
"menu_file": "文件",
"menu_edit": "编辑",
"menu_view": "显示",
"menu_window": "窗口",
"menu_services": "服务",
"menu_minimize": "最小化",
"close_tab": "关闭标签",
"duplicate_tab": "复制标签",
"terminal": "终端",
Expand Down
9 changes: 6 additions & 3 deletions nebula_app/src/display/keymap.rs
Original file line number Diff line number Diff line change
Expand Up @@ -140,9 +140,8 @@ impl ShortcutPolicy {
pub(crate) const MACOS_COMMAND_ALIASES: &[(&str, Action)] = &[
("cmd+t", Action::CreateNewTab),
("cmd+n", Action::CreateNewWindow),
// ⌘W 关标签页是 GPUI 壳的实际行为;配置表的 macOS 段把 ⌘W 记作 Quit,
// 这里必须压过它,否则解绑恢复会把 ⌘W 还给退出。
("cmd+w", Action::CloseTab),
// ⌘W 留给原生关窗;终端标签关闭的注册与设置恢复共用这个别名。
("shift+cmd+w", Action::CloseTab),
("shift+cmd+p", Action::ToggleCommandPalette),
("cmd+k", Action::ToggleShellPicker),
("shift+cmd+f", Action::ToggleFilesPanel),
Expand Down Expand Up @@ -322,6 +321,10 @@ impl ShortcutPolicy {
}
shortcuts.extend(extra);
}
if self.gpui && cfg!(target_os = "macos") {
// 原生关窗没有持久化 Action;恢复 Quit 不得把旧壳的 ⌘W 注入为退出应用。
shortcuts.retain(|(combo, _)| !self.same_combo(combo, "cmd+w"));
}
shortcuts
}

Expand Down
7 changes: 7 additions & 0 deletions nebula_app/src/gpui_shell/settings_pane/about.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,13 @@
use super::*;

impl SettingsPane {
pub(crate) fn show_application_home(&mut self, window: &mut Window, cx: &mut Context<Self>) {
self.active_section = 0;
self.about_sponsor_open = false;
self.settings_search_input.update(cx, |input, cx| input.set_value("", window, cx));
cx.notify();
}

pub(super) fn about_action_row(
id: &'static str,
icon: IconName,
Expand Down
49 changes: 12 additions & 37 deletions nebula_app/src/gpui_shell/workspace.rs
Original file line number Diff line number Diff line change
Expand Up @@ -45,10 +45,12 @@ use gpui_component::input::InputEvent;
use gpui_component::notification::Notification;
use nebula_split::{DIVIDER_GAP, HIT_SLOP, RemoveOutcome, SplitDirection, SplitNav, SplitTree};

mod actions;
mod agents;
mod closing;
mod command_manager;
mod keyboard_bindings;
mod macos_menu;
mod render;
#[cfg(test)]
use keyboard_bindings::{
Expand Down Expand Up @@ -101,49 +103,13 @@ pub(crate) mod windowing;
// 调用点分散在设置页与窗口层,原样再导出以免拆分波及它们。
pub(crate) use update_dialog::{open_update_dialog, show_update_notification};

use actions::*;
use tab_drag::{DockTarget, TabDrag, TabDragAxis};

#[cfg(test)]
use agents::ai_hook_target_pane;
use agents::{ai_session_palette_rows, restored_agent_command};

gpui::actions!(
nebula_workspace,
[
NewTerminal,
NewWindow,
CloseActiveTerminal,
ToggleSidebar,
OpenSettings,
ToggleCommandPalette,
CloseCommandPalette,
ToggleShellPicker,
CommandPaletteUp,
CommandPaletteDown,
ToggleFileTree,
ToggleGitPanel,
SplitRight,
SplitDown,
RenameActiveTab,
ToggleZoom,
FocusPaneLeft,
FocusPaneRight,
FocusPaneUp,
FocusPaneDown,
SelectNextTab,
SelectPreviousTab,
MoveTabLeft,
MoveTabRight,
IncreaseFontSize,
DecreaseFontSize,
ResetFontSize,
CopySelection,
PasteClipboard,
ToggleFullscreen,
OpenQuickJump
]
);

/// 命令面板 / Shell 选择器罩层的 keymap context。Esc 必须挂在这里,
/// 不能挂全局:否则 CC/Codex 的终止对话键到不了 PTY。
const PALETTE_KEY_CONTEXT: &str = "NebulaCommandPalette";
Expand Down Expand Up @@ -180,6 +146,9 @@ fn title_bar_panel_controls() -> gpui::Div {
/// 注册工作区快捷键;在 `gpui_component::init` 之后调用一次。
pub fn init(cx: &mut App) {
keyboard_bindings::init(cx);
if crate::platform::CAPABILITIES.native_application_menu {
macos_menu::init(cx);
}
}

/// 一个终端 pane:视图实体 + 宿主订阅。id 即 `TerminalView::pane_id`
Expand Down Expand Up @@ -1117,7 +1086,13 @@ impl NebulaWorkspace {
/// follow_system 折算)、逐终端刷新、重建 chrome 令牌。
fn apply_runtime_settings(&mut self, cx: &mut Context<Self>) {
let (runtime, settings) = crate::gpui_shell::config::Settings::load_current_snapshot(cx);
let language_changed = cx
.try_global::<crate::gpui_shell::config::Settings>()
.is_none_or(|previous| previous.ui_language != settings.ui_language);
cx.set_global(settings);
if crate::platform::CAPABILITIES.native_application_menu && language_changed {
cx.defer(|cx| macos_menu::init(cx));
}
for tab in &self.tabs {
if let WorkspaceTab::Terminal { panes, .. } = tab {
for pane in panes {
Expand Down
41 changes: 41 additions & 0 deletions nebula_app/src/gpui_shell/workspace/actions.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
gpui::actions!(
nebula_workspace,
[
NewTerminal,
NewWindow,
CloseActiveTerminal,
CloseWindow,
OpenAbout,
HideApplication,
HideOtherApplications,
MinimizeWindow,
ToggleSidebar,
OpenSettings,
ToggleCommandPalette,
CloseCommandPalette,
ToggleShellPicker,
CommandPaletteUp,
CommandPaletteDown,
ToggleFileTree,
ToggleGitPanel,
SplitRight,
SplitDown,
RenameActiveTab,
ToggleZoom,
FocusPaneLeft,
FocusPaneRight,
FocusPaneUp,
FocusPaneDown,
SelectNextTab,
SelectPreviousTab,
MoveTabLeft,
MoveTabRight,
IncreaseFontSize,
DecreaseFontSize,
ResetFontSize,
CopySelection,
PasteClipboard,
ToggleFullscreen,
OpenQuickJump
]
);
11 changes: 8 additions & 3 deletions nebula_app/src/gpui_shell/workspace/keyboard_bindings.rs
Original file line number Diff line number Diff line change
Expand Up @@ -68,7 +68,7 @@ pub(super) fn init(cx: &mut App) {
cx.bind_keys(default_workspace_bindings());
// 平台判定复用已有入口;共享表不意味着给其他平台注册 ⌘ 快捷键。
if crate::platform::Platform::current() == crate::platform::Platform::MacOS {
bind_macos_command_keys(cx);
cx.bind_keys(macos_command_bindings());
}
// 退出不属于任何视图,挂全局兜底:⌘Q 与用户自定义的 `keybind=…:Quit`
// 都落到与托盘退出同一条「先落盘会话与草稿,再停 PTY」的路径。
Expand Down Expand Up @@ -164,23 +164,28 @@ fn default_binding_source(binding: KeyBinding) -> KeyBinding {

/// macOS 使用 ⌘ 承载字符类 GUI 默认键;仅 Control 的字符组合交给终端。
/// 保留现有 Ctrl+Tab、Ctrl+Shift 和 Ctrl+Alt 导航组合及显式用户覆盖。
/// ⌘W 关闭窗口,活动终端改用 ⌘⇧W,避免与系统关闭窗口命令冲突。
///
/// 键位来自 `display::keymap::MACOS_COMMAND_ALIASES`:设置页的反查、解绑与
/// 恢复读的是同一张表,两处不会再各自漂移。注册必须留在这里、且早于用户
/// 自定义键,这样 `clear_action` 注入的 NoAction 才压得住静态默认绑定。
fn bind_macos_command_keys(cx: &mut App) {
fn macos_command_bindings() -> Vec<KeyBinding> {
let mut bindings: Vec<KeyBinding> = crate::display::keymap::MACOS_COMMAND_ALIASES
.iter()
.filter_map(|(combo, action)| workspace_binding_in_context(combo, action, None))
.collect();
// 剩下这些没有对应的 `config::Action`,或者需要单独的作用域。
bindings.extend([
KeyBinding::new("cmd-w", CloseWindow, None),
KeyBinding::new("cmd-h", HideApplication, None),
KeyBinding::new("cmd-alt-h", HideOtherApplications, None),
KeyBinding::new("cmd-m", MinimizeWindow, None),
KeyBinding::new("cmd-b", ToggleSidebar, None),
KeyBinding::new("cmd-,", OpenSettings, None),
// 设置页与对话框的输入框自带 ⌘V,作用域必须和终端分开。
KeyBinding::new("cmd-v", gpui_component::input::Paste, Some("Input")),
]);
cx.bind_keys(bindings.into_iter().map(default_binding_source));
bindings.into_iter().map(default_binding_source).collect()
}

/// Typed GPUI adapter for the shared numbered/last-tab actions. The existing
Expand Down
24 changes: 13 additions & 11 deletions nebula_app/src/gpui_shell/workspace/keyboard_bindings/tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -55,23 +55,25 @@ mod macos_command_keys {
#[test]
fn command_close_and_quit_dispatch_to_distinct_actions() {
use gpui::{KeyContext, Keymap, Keystroke};
// 合并后仍按共享别名表分派,防止配置中的 ⌘W/Quit 抢走关闭标签页动作。
let bindings = crate::display::keymap::MACOS_COMMAND_ALIASES
.iter()
.filter_map(|(combo, action)| workspace_binding_in_context(combo, action, None))
.collect();
let keymap = Keymap::new(bindings);
let mut keymap = Keymap::new(macos_command_bindings());
keymap.add_bindings(
crate::display::keymap::GPUI
.default_shortcuts()
.into_iter()
.filter(|(_, action)| *action == crate::config::Action::Quit)
.filter_map(|(combo, action)| workspace_binding_in_context(&combo, &action, None)),
);
for context in ["Root", crate::gpui_shell::terminal::KEY_CONTEXT] {
let contexts = [KeyContext::parse(context).unwrap()];
for (combo, quit) in [("cmd-w", false), ("cmd-q", true)] {
for combo in ["cmd-w", "cmd-shift-w", "cmd-q"] {
let input = [Keystroke::parse(combo).unwrap()];
let (bindings, pending) = keymap.bindings_for_input(&input, &contexts);
assert!(!pending);
let action = bindings.first().expect("命令键必须有明确动作").action();
if quit {
assert!(action.as_any().is::<QuitApp>());
} else {
assert!(action.as_any().is::<CloseActiveTerminal>());
match combo {
"cmd-q" => assert!(action.as_any().is::<QuitApp>()),
"cmd-w" => assert!(action.as_any().is::<CloseWindow>()),
_ => assert!(action.as_any().is::<CloseActiveTerminal>()),
}
}
}
Expand Down
55 changes: 55 additions & 0 deletions nebula_app/src/gpui_shell/workspace/macos_menu.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
use gpui::{App, Menu, MenuItem, OsAction, SystemMenuType};

use super::*;
use crate::i18n::Message;

pub(super) fn init(cx: &mut App) {
let language = crate::gpui_shell::config::ui_language(cx);
let text = |message| language.text(message);
cx.set_menus([
Menu::new("Pebrel").items([
MenuItem::action(format!("{} Pebrel", text(Message::CommonAbout)), OpenAbout),
MenuItem::separator(),
MenuItem::action(text(Message::CommonSettings), OpenSettings),
MenuItem::separator(),
MenuItem::os_submenu(text(Message::CommonMenuServices), SystemMenuType::Services),
MenuItem::separator(),
MenuItem::action(text(Message::CommonHidePebrel), HideApplication),
MenuItem::action(text(Message::CommonHideOthers), HideOtherApplications),
MenuItem::separator(),
MenuItem::action(text(Message::CommonQuitPebrel), keyboard_bindings::QuitApp),
]),
Menu::new(text(Message::CommonMenuFile)).items([
MenuItem::action(text(Message::CommonNewWindow), NewWindow),
MenuItem::action(text(Message::CommonNewTab), NewTerminal),
MenuItem::separator(),
MenuItem::action(text(Message::CommonCloseWindow), CloseWindow),
]),
Menu::new(text(Message::CommonMenuEdit)).items([
MenuItem::os_action(
text(Message::CommonCut),
gpui_component::input::Cut,
OsAction::Cut,
),
MenuItem::os_action(
text(Message::CommonCopy),
gpui_component::input::Copy,
OsAction::Copy,
),
MenuItem::os_action(
text(Message::CommonPaste),
gpui_component::input::Paste,
OsAction::Paste,
),
MenuItem::os_action(
text(Message::CommonSelectAll),
gpui_component::input::SelectAll,
OsAction::SelectAll,
),
]),
Menu::new(text(Message::CommonMenuView))
.items([MenuItem::action(text(Message::EditorFullscreen), ToggleFullscreen)]),
Menu::new(text(Message::CommonMenuWindow))
.items([MenuItem::action(text(Message::CommonMenuMinimize), MinimizeWindow)]),
]);
}
Loading
Loading