feat(desktop): native desktop enhancements

- Add a minimal application menu with common items and shortcuts:
  New Chat (Cmd+N), Settings (Cmd+,), Reload, etc.
- Add system tray with show window, new chat, and quit; click icon to restore
- Add single-instance lock so relaunching focuses the existing window
- Implement close-to-tray: closing the window hides it; only a real quit destroys it
- Explicitly define Window menu's Close Window bound to Cmd+W / Ctrl+W to reliably trigger hide-to-tray
- Listen for menu-action in the renderer to handle menu-triggered new chat / open settings
This commit is contained in:
zhayujie
2026-06-22 15:32:22 +08:00
parent d1336b872e
commit 49452e035d
6 changed files with 226 additions and 1 deletions

59
desktop/src/main/tray.ts Normal file
View File

@@ -0,0 +1,59 @@
import { app, Tray, Menu, BrowserWindow, nativeImage } from 'electron'
let tray: Tray | null = null
interface TrayDeps {
getWindow: () => BrowserWindow | null
iconPath?: string
// Called when the user picks "Quit" so the app can fully exit.
onQuit: () => void
}
// Build a system tray icon with a minimal menu. The tray lets users restore the
// window after closing it to the background and start a new chat quickly.
export function createTray({ getWindow, iconPath, onQuit }: TrayDeps): Tray | null {
if (tray) return tray
if (!iconPath) return null
let image = nativeImage.createFromPath(iconPath)
if (image.isEmpty()) return null
// Tray icons render small; resize to avoid an oversized image on some platforms.
image = image.resize({ width: 18, height: 18 })
// macOS renders template images correctly in both light/dark menubars.
if (process.platform === 'darwin') image.setTemplateImage(false)
tray = new Tray(image)
tray.setToolTip(app.name)
const showWindow = () => {
const win = getWindow()
if (!win) return
if (win.isMinimized()) win.restore()
win.show()
win.focus()
}
const contextMenu = Menu.buildFromTemplate([
{ label: 'Show CowAgent', click: showWindow },
{
label: 'New Chat',
click: () => {
showWindow()
getWindow()?.webContents.send('menu-action', 'new-chat')
},
},
{ type: 'separator' },
{ label: 'Quit', click: onQuit },
])
tray.setContextMenu(contextMenu)
// Single click restores the window (common Windows/Linux behavior).
tray.on('click', showWindow)
return tray
}
export function destroyTray() {
tray?.destroy()
tray = null
}