Explorar o código

fix(desktop): complete the macOS menu defaults

Add the standard application hide commands (Cmd+H, Option+Cmd+H, Show All) and
pin them with the window commands in the startup test, drop the redundant mock
cast that failed the lint gate, move the Agent Note to bug-fix, and state the
English role labels and the Dock-activation condition in both READMEs.
Turtle hai 2 semanas
pai
achega
b9fde8d063

+ 3 - 3
.agents/notes/implemented/feature/2026-09-16-desktop-window-menus.i18n.yaml → .agents/notes/implemented/bug-fix/2026-09-16-desktop-window-menus.i18n.yaml

@@ -1,6 +1,6 @@
 # Bilingual-pair consistency record (docs/i18n/README.md): the git blob hash of each
 # side as of the last confirmed-consistent state. Both languages carry equal authority;
 # after editing either side, bring the other along and re-record with:
-#   pnpm run verify-translation-pairing --write .agents/notes/implemented/feature/2026-09-16-desktop-window-menus.md
-2026-09-16-desktop-window-menus.md: 4e7110a45b1f3ceaa119555405079127700c2c70
-2026-09-16-desktop-window-menus.zh.md: 5b53fb0e87fad025b61c3b94e9fe86c5b318874a
+#   pnpm run verify-translation-pairing --write .agents/notes/implemented/bug-fix/2026-09-16-desktop-window-menus.md
+2026-09-16-desktop-window-menus.md: 1285e5cb9cf02a7ad914925d7c8ab45901671b7d
+2026-09-16-desktop-window-menus.zh.md: 0c3fe5ba55c9532d89c77b42f1a13663281bb720

+ 35 - 0
.agents/notes/implemented/bug-fix/2026-09-16-desktop-window-menus.md

@@ -0,0 +1,35 @@
+# Agent Note: Desktop standard macOS window menus
+
+Status: implemented
+
+English | [中文](2026-09-16-desktop-window-menus.zh.md)
+
+## Problem
+
+The Desktop shell replaces Electron's default application menu with a custom template that listed only the application and Edit menus. Electron builds only the roles a template declares, so macOS lost the File and Window menus and the application hide commands the default template supplies, including Close Window (⌘W), Minimize (⌘M), and Hide (⌘H). None of those shortcuts did anything in the Desktop application while every comparable macOS application responds to them (issue #4374).
+
+## Decision
+
+On macOS the template declares `{ role: 'fileMenu' }` before the Edit menu and `{ role: 'windowMenu' }` after it, and a separator-delimited run of `hide`, `hideOthers`, and `unhide` before Quit in the application submenu. Close Window (⌘W) closes the focused window, Minimize (⌘M) and Zoom act on it, Bring All to Front raises every application window, and Hide (⌘H), Hide Others (⌥⌘H), and Show All hide the application with focus handed to the previous one. The File menu holds only Close Window, and the Window menu holds only Minimize, Zoom, and Bring All to Front. Windows and Linux keep the application and Edit menus.
+
+Electron supplies the role labels as English literals rather than from Desktop's locale dictionaries, as the existing Edit menu does; a zh-CN Electron 44 run still reports "Close Window" and "Minimize". No custom close, minimize, or hide code is added: ⌘W destroys the window through Electron's own role, and the Dock icon recreates it only when no Desktop window remains open, because `activate` calls `focusPrimaryWindow` only with zero windows.
+
+## Alternatives considered
+
+**Bind ⌘W to minimizing or hiding the window.** macOS reserves Minimize for ⌘M and Hide for ⌘H, and comparable applications close the front window with ⌘W. Binding another command to the reported shortcut would contradict the platform convention the change follows.
+
+**Declare only the Window menu.** That menu supplies Minimize and Zoom but no Close, leaving ⌘W unbound.
+
+**Add a bare `{ role: 'close' }` item to the application submenu.** It avoids a File menu containing a single command, but places a window command among the app-wide Plugins, Updates, Hide, and Quit items where no macOS application puts it.
+
+**Declare the File and Window menus on every platform.** The same template declares Ctrl+W there; with one window, closing it invokes `window-all-closed` and quits the application, turning a window shortcut into an unrequested quit path.
+
+**Recreate the main window on Dock activation regardless of other windows.** The plugin window can outlive the main window, so gating `activate` on `mainWindow` instead of `BrowserWindow.getAllWindows()` would make the Dock behavior hold in every case. That changes window lifecycle beyond restoring the suppressed platform commands, so the README instead states the condition under which the Dock icon reopens the window.
+
+## Consequences
+
+macOS regains the window and application commands the custom menu suppressed, at the cost of four menu roles the [Desktop README](../../../../apps/desktop/README.md) must keep explaining. The labels stay English on a non-English Desktop, which the README states.
+
+## Testing
+
+A `apps/desktop/tests/main-startup.spec.ts` case pins the declared menu roles per platform, including the macOS hide commands. Role-based menu items execute natively, so a programmatic `click()` and the vitest Electron mock cannot exercise the shortcuts; a real Electron 44 run of the same template showed Close Window (⌘W), Minimize (⌘M), Hide (⌘H), Hide Others (⌥⌘H), and Show All present only after this change, with the role labels unchanged under `--lang=zh-CN`.

+ 35 - 0
.agents/notes/implemented/bug-fix/2026-09-16-desktop-window-menus.zh.md

@@ -0,0 +1,35 @@
+# Agent Note: Desktop standard macOS window menus
+
+Status: implemented
+
+[English](2026-09-16-desktop-window-menus.md) | 中文
+
+## 问题
+
+Desktop shell 用自定义模板替换了 Electron 的默认应用菜单,该模板此前只列出应用菜单和 Edit 菜单。Electron 只构建模板中声明的 role,因此 macOS 失去了默认模板提供的 File、Window 菜单和应用隐藏命令,包括 Close Window(⌘W)、Minimize(⌘M)和 Hide(⌘H)。这些快捷键在 Desktop 应用中都没有任何效果,而同类的 macOS 应用都会响应它们(#4374)。
+
+## 决策
+
+macOS 上的模板在 Edit 菜单之前声明 `{ role: 'fileMenu' }`,在其之后声明 `{ role: 'windowMenu' }`,并在应用子菜单的 Quit 之前声明由分隔符隔开的 `hide`、`hideOthers` 和 `unhide`。Close Window(⌘W)关闭当前窗口,Minimize(⌘M)和 Zoom 作用于当前窗口,Bring All to Front 抬高应用的每个窗口,Hide(⌘H)、Hide Others(⌥⌘H)和 Show All 隐藏应用并把焦点交还前一个应用。File 菜单只有 Close Window,Window 菜单只有 Minimize、Zoom 和 Bring All to Front。Windows 和 Linux 保留应用菜单和 Edit 菜单。
+
+这些 role 的标签由 Electron 以英文常量提供,不来自 Desktop 的语言词典,现有的 Edit 菜单也是如此;在 zh-CN 下运行 Electron 44 仍然得到 "Close Window" 和 "Minimize"。这里不新增任何自定义的关闭、最小化或隐藏代码:⌘W 通过 Electron 自身的 role 销毁窗口;`activate` 只在没有任何 Desktop 窗口时才调用 `focusPrimaryWindow`,因此只有此时点击 Dock 图标才会重建窗口。
+
+## 考虑过的替代方案
+
+**把 ⌘W 绑定为最小化或隐藏窗口。** macOS 把最小化留给 ⌘M、把隐藏留给 ⌘H,同类应用都用 ⌘W 关闭最前窗口。给这个快捷键绑定其他命令会违背本改动所遵循的平台惯例。
+
+**只声明 Window 菜单。** 该菜单提供 Minimize 和 Zoom,但不提供关闭,⌘W 仍然没有绑定。
+
+**在应用子菜单里直接加一个 `{ role: 'close' }` 项。** 这样可以避免只有一个命令的 File 菜单,但会把窗口命令放到 Plugins、Updates、Hide、Quit 这些应用级命令中间,没有 macOS 应用这样排布。
+
+**在所有平台都声明 File 和 Window 菜单。** 同一份模板在那些平台会把 Ctrl+W 声明为关闭;应用只有一个窗口,关闭它会触发 `window-all-closed` 并退出应用,把窗口快捷键变成用户没有要求的退出路径。
+
+**让 Dock 激活时无论其他窗口是否存在都重建主窗口。** 插件窗口可以比主窗口存活更久,因此把 `activate` 的判断从 `BrowserWindow.getAllWindows()` 改为 `mainWindow` 能让 Dock 行为在任何情况下都成立。但这超出了恢复被压掉的平台命令,属于窗口生命周期的行为变更,因此 README 改为说明 Dock 图标在什么条件下重新打开窗口。
+
+## 影响
+
+macOS 恢复了自定义菜单压掉的窗口和应用命令,代价是 [Desktop README](../../../../apps/desktop/README.zh.md) 需要持续解释这四个菜单 role。在非英文的 Desktop 上这些标签仍是英文,README 已说明这一点。
+
+## 测试
+
+`apps/desktop/tests/main-startup.spec.ts` 中的一个用例固定了各平台声明的菜单 role,包括 macOS 的隐藏命令。基于 role 的菜单项由本机执行,因此程序化的 `click()` 和 vitest 的 Electron mock 都无法触达这些快捷键;用真实 Electron 44 运行同一模板显示,只有在本改动之后才出现 Close Window(⌘W)、Minimize(⌘M)、Hide(⌘H)、Hide Others(⌥⌘H)和 Show All,并且在 `--lang=zh-CN` 下 role 标签保持不变。

+ 0 - 33
.agents/notes/implemented/feature/2026-09-16-desktop-window-menus.md

@@ -1,33 +0,0 @@
-# Agent Note: Desktop standard macOS window menus
-
-Status: implemented
-
-English | [中文](2026-09-16-desktop-window-menus.zh.md)
-
-## Problem
-
-The Desktop shell replaces Electron's default application menu with a custom template that listed only the application and Edit menus. Electron builds only the roles a template declares, so macOS lost the File and Window menus the default template supplies, including Close Window (⌘W) and Minimize (⌘M). Both shortcuts did nothing in the Desktop application while other macOS applications respond to them, so users could not make the main window leave the screen with the shortcut they already knew.
-
-## Decision
-
-On macOS the template declares `{ role: 'fileMenu' }` before the Edit menu and `{ role: 'windowMenu' }` after it. Close Window (⌘W), Minimize (⌘M), Zoom, Bring All to Front, and the open-window list come from those roles; Electron localizes their labels from the application locale, so the Desktop dictionaries stay unchanged. Windows and Linux keep the application and Edit menus.
-
-Closing the main window takes the same path as the window's traffic-light button: the dsh Host keeps running, and the Dock icon or `activate` recreates the window. `window-all-closed` still quits only on non-macOS platforms.
-
-## Alternatives considered
-
-**Bind ⌘W to minimizing the window.** The feedback asked for ⌘W, but macOS reserves Minimize for ⌘M and every comparable application closes the front window with ⌘W. Binding minimization to ⌘W would contradict the platform convention the report appealed to.
-
-**Declare only the Window menu.** That menu supplies Minimize and Zoom but no Close, so the reported ⌘W would stay dead.
-
-**Add a bare `{ role: 'close' }` item to the application submenu.** It avoids a File menu containing a single command, but places a window command among the app-wide Plugins, Updates, and Quit items where no macOS application puts it.
-
-**Declare the File and Window menus on every platform.** The same template declares Ctrl+W there; with one window, closing it invokes `window-all-closed` and quits the application, turning a window shortcut into an unrequested quit path.
-
-## Consequences
-
-macOS regains the window commands the custom menu suppressed, at the cost of two menu roles the [Desktop README](../../../../apps/desktop/README.md) must keep explaining. Role labels follow Electron's locale rather than the Desktop dictionaries, which is also true of the existing Edit menu.
-
-## Testing
-
-A `apps/desktop/tests/main-startup.spec.ts` case pins the declared menu roles on macOS and on Windows and Linux. Role-based menu items execute natively, so a programmatic `click()` and the vitest Electron mock cannot exercise the shortcuts; a real Electron 44 run of the same template showed Close Window (⌘W) and Minimize (⌘M) present only after this change.

+ 0 - 33
.agents/notes/implemented/feature/2026-09-16-desktop-window-menus.zh.md

@@ -1,33 +0,0 @@
-# Agent Note: Desktop standard macOS window menus
-
-Status: implemented
-
-[English](2026-09-16-desktop-window-menus.md) | 中文
-
-## 问题
-
-Desktop shell 用自定义模板替换了 Electron 的默认应用菜单,该模板此前只列出应用菜单和“编辑”菜单。Electron 只构建模板中声明的 role,因此 macOS 失去了默认模板提供的“文件”和“窗口”菜单,包括“关闭窗口”(⌘W)和“最小化”(⌘M)。这两个快捷键在 Desktop 应用中都没有任何效果,而其他 macOS 应用会响应它们,用户无法用自己熟悉的快捷键让主窗口离开屏幕。
-
-## 决策
-
-macOS 上的模板在“编辑”菜单之前声明 `{ role: 'fileMenu' }`,在其之后声明 `{ role: 'windowMenu' }`。“关闭窗口”(⌘W)、“最小化”(⌘M)、“缩放”、“全部置于顶层”和打开窗口列表都由这两个 role 提供;Electron 按应用语言本地化它们的标签,因此 Desktop 词典无需改动。Windows 和 Linux 仍然只有应用菜单和“编辑”菜单。
-
-关闭主窗口与点击窗口红绿灯按钮走同一条路径:dsh Host 继续运行,点击 Dock 图标或触发 `activate` 会重新创建窗口。`window-all-closed` 仍只在非 macOS 平台退出应用。
-
-## 考虑过的替代方案
-
-**把 ⌘W 绑定为最小化窗口。** 反馈提到的是 ⌘W,但 macOS 把最小化留给 ⌘M,所有同类应用都用 ⌘W 关闭最前窗口。把最小化绑到 ⌘W 会违背这份反馈所依据的平台惯例。
-
-**只声明“窗口”菜单。** 该菜单提供最小化和缩放,但不提供关闭,反馈中的 ⌘W 仍然无效。
-
-**在应用子菜单里直接加一个 `{ role: 'close' }` 项。** 这样可以避免只有一个命令的“文件”菜单,但会把窗口命令放到 Plugins、Updates、Quit 这些应用级命令中间,没有 macOS 应用这样排布。
-
-**在所有平台都声明“文件”和“窗口”菜单。** 同一份模板在那些平台会把 Ctrl+W 声明为关闭;应用只有一个窗口,关闭它会触发 `window-all-closed` 并退出应用,把窗口快捷键变成用户没有要求的退出路径。
-
-## 影响
-
-macOS 恢复了自定义菜单压掉的窗口命令,代价是 [Desktop README](../../../../apps/desktop/README.zh.md) 需要持续解释这两个菜单 role。role 标签跟随 Electron 的语言环境而不是 Desktop 词典,现有的“编辑”菜单也是如此。
-
-## 测试
-
-`apps/desktop/tests/main-startup.spec.ts` 中的一个用例固定了 macOS 与 Windows、Linux 上声明的菜单 role。基于 role 的菜单项由本机执行,因此程序化的 `click()` 和 vitest 的 Electron mock 都无法触达这些快捷键;用真实 Electron 44 运行同一模板显示,只有在本改动之后才出现“关闭窗口”(⌘W)和“最小化”(⌘M)。

+ 2 - 2
apps/desktop/README.i18n.yaml

@@ -2,5 +2,5 @@
 # side as of the last confirmed-consistent state. Both languages carry equal authority;
 # after editing either side, bring the other along and re-record with:
 #   pnpm run verify-translation-pairing --write apps/desktop/README.md
-README.md: 5e47dcca178ed3a6481bbd5dca39943331afa2ad
-README.zh.md: 48572537b1729c2a09ba25e0e102ea68e6004f63
+README.md: fbb1249752a1957c61bb20c89cb76271e48a302f
+README.zh.md: b3841813c14c2823d28258df12d90479fde7597f

+ 1 - 1
apps/desktop/README.md

@@ -52,7 +52,7 @@ Electron chooses typed English or Chinese shell copy from its application locale
 
 Electron's native Edit menu supplies undo, redo, cut, copy, paste, and select-all commands and platform shortcuts for the focused window. Right-clicking an editable field opens these commands without shortcut labels, with availability supplied by Chromium; selected read-only text offers Copy.
 
-macOS also receives the standard File and Window menus, which the custom application menu must declare because it replaces Electron's default menu: Close Window (⌘W) closes the focused window, and Minimize (⌘M), Zoom, and Bring All to Front act on it. Closing the main window leaves the dsh Host running; the Dock icon opens the window again. Windows and Linux keep the application and Edit menus only.
+macOS also receives the standard File and Window menus and the application hide commands, which the custom application menu must declare because it replaces Electron's default menu: Close Window (⌘W) closes the focused window, Minimize (⌘M) and Zoom act on it, Bring All to Front raises every application window, and the application menu offers Hide (⌘H), Hide Others (⌥⌘H), and Show All. Electron supplies these labels in English rather than from Desktop's locale-owned copy, as the existing Edit menu does. Closing the last Desktop window leaves the dsh Host running, and the Dock icon then opens the main window again. Windows and Linux keep the application and Edit menus only.
 
 ### Runtime and plugin activation
 

+ 1 - 1
apps/desktop/README.zh.md

@@ -52,7 +52,7 @@ Electron 根据应用语言选择类型化的英文或中文 shell 文案,并
 
 Electron 原生“编辑”菜单为当前聚焦窗口提供撤销、重做、剪切、复制、粘贴和全选命令及平台快捷键。右键点击可编辑输入区域会打开不带快捷键标注的这些命令,其可用状态由 Chromium 提供;选中的只读文本提供“复制”命令。
 
-macOS 还会获得标准的“文件”和“窗口”菜单;由于自定义应用菜单会替换 Electron 的默认菜单,这两项必须显式声明:“关闭窗口”(⌘W)关闭当前窗口,“最小化”(⌘M)、“缩放”和“全部置于顶层”作用于该窗口。关闭主窗口后 dsh Host 继续运行,点击 Dock 图标会重新打开窗口。Windows 和 Linux 只保留应用菜单和“编辑”菜单。
+macOS 还会获得标准的 File 和 Window 菜单以及应用隐藏命令;由于自定义应用菜单会替换 Electron 的默认菜单,这些必须显式声明:Close Window(⌘W)关闭当前窗口,Minimize(⌘M)和 Zoom 作用于当前窗口,Bring All to Front 抬高应用的每个窗口,应用菜单提供 Hide(⌘H)、Hide Others(⌥⌘H)和 Show All。这些标签由 Electron 以英文提供,不使用 Desktop 的语言词典,现有的 Edit 菜单也是如此。关闭最后一个 Desktop 窗口后 dsh Host 继续运行,点击 Dock 图标会重新打开主窗口。Windows 和 Linux 只保留应用菜单和 Edit 菜单。
 
 ### 运行时与插件激活
 

+ 9 - 4
apps/desktop/src/main.ts

@@ -428,12 +428,16 @@ async function main(): Promise<void> {
   }
 
   // A custom application menu replaces Electron's default menu, so macOS needs
-  // its standard File and Window menus declared explicitly.
-  const standardMenus: MenuItemConstructorOptions[] = process.platform === 'darwin'
+  // its standard menus and application hide commands declared explicitly.
+  const darwin = process.platform === 'darwin'
+  const platformMenus: MenuItemConstructorOptions[] = darwin
     ? [{ role: 'fileMenu' }, { role: 'editMenu' }, { role: 'windowMenu' }]
     : [{ role: 'editMenu' }]
+  const hideCommands: MenuItemConstructorOptions[] = darwin
+    ? [{ role: 'hide' }, { role: 'hideOthers' }, { role: 'unhide' }, { type: 'separator' }]
+    : []
   Menu.setApplicationMenu(Menu.buildFromTemplate([{
-    label: process.platform === 'darwin' ? app.name : messages.application,
+    label: darwin ? app.name : messages.application,
     submenu: [
       {
         label: messages.pluginsMenu,
@@ -442,9 +446,10 @@ async function main(): Promise<void> {
       },
       { label: messages.checkUpdatesMenu, click: () => { void checkAndPrompt(true) } },
       { type: 'separator' },
+      ...hideCommands,
       { role: 'quit' },
     ],
-  }, ...standardMenus]))
+  }, ...platformMenus]))
 
   const createMainWindow = (): BrowserWindow => {
     const window = createWindow(appPreload, true)

+ 10 - 3
apps/desktop/tests/main-startup.spec.ts

@@ -3,6 +3,7 @@ import type { IpcMainInvokeEvent, MenuItemConstructorOptions } from 'electron'
 import { join } from 'node:path'
 import type { MessageBoxOptions, MessageBoxReturnValue } from 'electron'
 import { DESKTOP_IPC } from '../src/ipc.ts'
+import { en } from '../src/locale.ts'
 
 vi.mock('../src/web-document.ts', () => ({ authenticateWebHost: async () => 'test-cookie', serveWebDocument: vi.fn(), forwardWebRequest: vi.fn() }))
 
@@ -205,17 +206,23 @@ describe('desktop main startup', () => {
     expect(harness.hosts).toHaveLength(0)
   })
 
-  it.each(['darwin', 'win32', 'linux'] as const)('adds the standard window menus only on macOS (%s)', async (platform) => {
+  it.each(['darwin', 'win32', 'linux'] as const)('adds the standard macOS window commands only on macOS (%s)', async (platform) => {
     vi.spyOn(process, 'platform', 'get').mockReturnValue(platform)
     await import('../src/main.ts')
     await harness.preparing.promise
+    const describeItem = (item: MenuItemConstructorOptions): string | undefined =>
+      item.role ?? (item.type === 'separator' ? 'separator' : item.label)
     const template = harness.menu.buildFromTemplate.mock.calls
-      .map(call => call[0] as MenuItemConstructorOptions[])
+      .map(call => call[0])
       .find(items => items.some(item => item.role === 'editMenu'))
     if (template === undefined) throw new Error('application menu missing')
-    expect(template.map(item => item.role ?? item.label)).toEqual(platform === 'darwin'
+    expect(template.map(describeItem)).toEqual(platform === 'darwin'
       ? ['Desktop test', 'fileMenu', 'editMenu', 'windowMenu']
       : ['Application', 'editMenu'])
+    const application = template[0]!.submenu as MenuItemConstructorOptions[]
+    expect(application.map(describeItem)).toEqual(platform === 'darwin'
+      ? [en.pluginsMenu, en.checkUpdatesMenu, 'separator', 'hide', 'hideOthers', 'unhide', 'separator', 'quit']
+      : [en.pluginsMenu, en.checkUpdatesMenu, 'separator', 'quit'])
     expect(harness.menu.setApplicationMenu).toHaveBeenCalledOnce()
   })