mirror of
https://gitee.com/zhangsken/opencode.git
synced 2026-10-16 10:30:01 +08:00
fix(desktop): use Win+Alt+O and plain tray options on Windows
This commit is contained in:
@@ -56,10 +56,13 @@ highlight and click target, with compact system spacing and default styling. Tra
|
||||
positions the visible favicon alongside the title without changing its size, horizontal padding, or shared hover target.
|
||||
Platforms without native secondary labels
|
||||
retain separate title/detail rows and an invisible alignment spacer. Menu icons have an extra four-point trailing gutter.
|
||||
Footer shortcuts use the operating system's separate, subdued shortcut column:
|
||||
On macOS and Linux, footer shortcuts use the operating system's separate, subdued shortcut column:
|
||||
**Cmd/Ctrl+N** for New Agent, **Cmd/Ctrl+,** for Settings, and **Cmd/Ctrl+Q** for Quit. Docs has no shortcut.
|
||||
They work while the tray menu is open. Windows popup menus do not respond to these shortcuts, so on Windows the footer
|
||||
items are plain options without shortcut labels.
|
||||
|
||||
**Cmd+Option+O** opens the tray globally on macOS (**Ctrl+Alt+O** on Windows/Linux), even while another app is focused.
|
||||
**Cmd+Option+O** opens the tray globally on macOS (**Ctrl+Alt+O** on Linux, **Win+Alt+O** on Windows), even while another
|
||||
app is focused. Windows avoids Ctrl+Alt because many keyboard layouts use it as AltGr to type characters.
|
||||
It is registered when the tray icon appears and released when the tray is disabled or the app quits. If another application
|
||||
already owns the combination, the tray remains usable by mouse and a shortcut-registration warning is written to the log.
|
||||
|
||||
@@ -175,4 +178,4 @@ written to `dist/bench-startup`.
|
||||
|
||||
A packaged beta or prod build registers itself as the `opencode://` handler when it starts, even from the bench; the
|
||||
installed app takes the registration back on its next launch. Those channels run with `HTTPS_PROXY` pointed at a
|
||||
closed port (`--offline` forces it for dev) so the updater's first check fails fast instead of reaching GitHub.
|
||||
closed port (`--offline` forces it for dev) so the updater's first check fails fast instead of reaching GitHub.
|
||||
|
||||
@@ -164,7 +164,7 @@ describe("tray menu", () => {
|
||||
|
||||
test("uses the native shortcut column instead of adding shortcuts to action labels", () => {
|
||||
const actions = { open() {}, trigger() {}, docs() {}, quit() {}, session() {} }
|
||||
const menu = trayMenu(actions, empty)
|
||||
const menu = trayMenu(actions, empty, undefined, true, "darwin")
|
||||
expect(
|
||||
menu
|
||||
.filter((item) => item.click)
|
||||
@@ -177,6 +177,14 @@ describe("tray menu", () => {
|
||||
])
|
||||
})
|
||||
|
||||
test("shows plain options without shortcuts on Windows", () => {
|
||||
const actions = { open() {}, trigger() {}, docs() {}, quit() {}, session() {} }
|
||||
const menu = trayMenu(actions, empty, undefined, false, "win32")
|
||||
const options = menu.filter((item) => item.click)
|
||||
expect(options.map((item) => item.label)).toEqual(["New Agent…", "Settings", "Docs", "Quit"])
|
||||
expect(options.some((item) => "accelerator" in item)).toBe(false)
|
||||
})
|
||||
|
||||
test("does not claim stale sessions are live after a connection failure", () => {
|
||||
const actions = { open() {}, trigger() {}, docs() {}, quit() {}, session() {} }
|
||||
const menu = trayMenu(actions, {
|
||||
|
||||
@@ -18,7 +18,15 @@ export type TrayActions = {
|
||||
session: (id: string) => void
|
||||
}
|
||||
|
||||
export function trayMenu(actions: TrayActions, snapshot: TraySessions, icons?: TrayMenuIcons, sublabels = true) {
|
||||
export function trayMenu(
|
||||
actions: TrayActions,
|
||||
snapshot: TraySessions,
|
||||
icons?: TrayMenuIcons,
|
||||
sublabels = true,
|
||||
platform: NodeJS.Platform = process.platform,
|
||||
) {
|
||||
// Windows popup menus do not respond to accelerators, so showing them would advertise shortcuts that do nothing.
|
||||
const accelerator = (key: string) => (platform === "win32" ? {} : { accelerator: key })
|
||||
const items = snapshot.sessions.flatMap((session, index) => {
|
||||
const group = traySessionGroup(session.status)
|
||||
const previous = snapshot.sessions[index - 1]
|
||||
@@ -85,13 +93,13 @@ export function trayMenu(actions: TrayActions, snapshot: TraySessions, icons?: T
|
||||
{
|
||||
label: nativeT("desktop.tray.newAgent"),
|
||||
icon: icons?.action("newAgent"),
|
||||
accelerator: "CommandOrControl+N",
|
||||
...accelerator("CommandOrControl+N"),
|
||||
click: () => actions.trigger("tab.new"),
|
||||
},
|
||||
{
|
||||
label: nativeT("desktop.menu.settings"),
|
||||
icon: icons?.action("settings"),
|
||||
accelerator: "CommandOrControl+,",
|
||||
...accelerator("CommandOrControl+,"),
|
||||
click: () => actions.trigger("settings.open"),
|
||||
},
|
||||
{ label: nativeT("desktop.tray.docs"), icon: icons?.action("docs"), click: actions.docs },
|
||||
@@ -99,7 +107,7 @@ export function trayMenu(actions: TrayActions, snapshot: TraySessions, icons?: T
|
||||
{
|
||||
label: nativeT("desktop.tray.quit"),
|
||||
icon: icons?.action("quit"),
|
||||
accelerator: "CommandOrControl+Q",
|
||||
...accelerator("CommandOrControl+Q"),
|
||||
click: actions.quit,
|
||||
},
|
||||
] satisfies MenuItemConstructorOptions[]
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { expect, test } from "bun:test"
|
||||
import { registerTrayShortcut, TRAY_SHORTCUT } from "./tray-shortcut"
|
||||
import { registerTrayShortcut, trayShortcut } from "./tray-shortcut"
|
||||
|
||||
test("registers Command/Control+Alt+O and invokes the tray opener", () => {
|
||||
const callbacks = new Map<string, () => void>()
|
||||
@@ -19,8 +19,10 @@ test("registers Command/Control+Alt+O and invokes the tray opener", () => {
|
||||
() => {
|
||||
opened.push("tray")
|
||||
},
|
||||
"darwin",
|
||||
)
|
||||
expect(TRAY_SHORTCUT).toBe("CommandOrControl+Alt+O")
|
||||
const TRAY_SHORTCUT = "CommandOrControl+Alt+O"
|
||||
expect(shortcut.key).toBe(TRAY_SHORTCUT)
|
||||
expect(shortcut.registered).toBe(true)
|
||||
callbacks.get(TRAY_SHORTCUT)?.()
|
||||
expect(opened).toEqual(["tray"])
|
||||
@@ -46,3 +48,22 @@ test("does not unregister a shortcut it could not acquire", () => {
|
||||
shortcut.dispose()
|
||||
expect(released).toEqual([])
|
||||
})
|
||||
|
||||
test("uses Win+Alt+O on Windows so AltGr characters remain typeable", () => {
|
||||
expect(trayShortcut("win32")).toBe("Super+Alt+O")
|
||||
expect(trayShortcut("darwin")).toBe("CommandOrControl+Alt+O")
|
||||
expect(trayShortcut("linux")).toBe("CommandOrControl+Alt+O")
|
||||
const registered: string[] = []
|
||||
registerTrayShortcut(
|
||||
{
|
||||
register: (key) => {
|
||||
registered.push(key)
|
||||
return true
|
||||
},
|
||||
unregister: () => {},
|
||||
},
|
||||
() => {},
|
||||
"win32",
|
||||
)
|
||||
expect(registered).toEqual(["Super+Alt+O"])
|
||||
})
|
||||
|
||||
@@ -1,17 +1,23 @@
|
||||
export const TRAY_SHORTCUT = "CommandOrControl+Alt+O"
|
||||
// Ctrl+Alt is AltGr on many Windows keyboard layouts, so a Ctrl+Alt shortcut would block typed characters.
|
||||
export function trayShortcut(platform: NodeJS.Platform = process.platform) {
|
||||
return platform === "win32" ? "Super+Alt+O" : "CommandOrControl+Alt+O"
|
||||
}
|
||||
|
||||
export function registerTrayShortcut(
|
||||
registry: { register: (key: string, callback: () => void) => boolean; unregister: (key: string) => void },
|
||||
open: () => void,
|
||||
platform: NodeJS.Platform = process.platform,
|
||||
) {
|
||||
const state = { registered: registry.register(TRAY_SHORTCUT, open) }
|
||||
const key = trayShortcut(platform)
|
||||
const state = { registered: registry.register(key, open) }
|
||||
return {
|
||||
key,
|
||||
get registered() {
|
||||
return state.registered
|
||||
},
|
||||
dispose() {
|
||||
if (!state.registered) return
|
||||
registry.unregister(TRAY_SHORTCUT)
|
||||
registry.unregister(key)
|
||||
state.registered = false
|
||||
},
|
||||
}
|
||||
|
||||
@@ -9,7 +9,7 @@ import { traySessionNotification, type TraySessions } from "./tray-sessions"
|
||||
import type { TrayIcons } from "./tray-icons"
|
||||
import { trayTabs } from "./tray-tabs"
|
||||
import { trayStartupPixels } from "./tray-icon-art"
|
||||
import { registerTrayShortcut, TRAY_SHORTCUT } from "./tray-shortcut"
|
||||
import { registerTrayShortcut } from "./tray-shortcut"
|
||||
|
||||
let tray: Tray | undefined
|
||||
let actions: TrayActions | undefined
|
||||
@@ -132,7 +132,7 @@ function createTray(image: NativeImage) {
|
||||
if (!menuOpen) item.popUpContextMenu()
|
||||
})
|
||||
if (!shortcut.registered)
|
||||
log.warn("Unable to register tray shortcut; it may be in use by another application", { shortcut: TRAY_SHORTCUT })
|
||||
log.warn("Unable to register tray shortcut; it may be in use by another application", { shortcut: shortcut.key })
|
||||
return item
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user