程序员阿江(Relakkes) 16f4137954 fix(desktop): stabilize Electron proxy and fullscreen handling
Electron desktop runs network-sensitive OpenAI OAuth token exchange in the sidecar process, so the sidecar now receives system proxy env derived from Electron's cross-platform proxy resolver and the OAuth token client uses the existing proxy fetch options. General manual proxy settings also document and preserve authenticated proxy URLs.

The macOS fullscreen black-screen path is addressed by avoiding native fullscreen Spaces for app fullscreen toggles and by leaving fullscreen before hiding or closing the window.

Constraint: Electron packaged apps may not inherit shell HTTPS_PROXY env when launched from Finder.
Constraint: Manual authenticated proxies must remain standard HTTP(S) proxy URLs for Bun/undici compatibility.
Rejected: Store proxy username and password as separate fields | would require new secret-storage semantics and migration beyond this bugfix.
Rejected: Use native macOS fullscreen Spaces for the desktop app | reproduced black-screen behavior when hiding/closing from fullscreen.
Confidence: high
Scope-risk: moderate
Directive: Do not remove sidecar proxy env injection without retesting OpenAI OAuth from a packaged app launched outside a shell.
Tested: bun test src/services/openaiAuth/client.test.ts src/server/__tests__/haha-openai-oauth-service.test.ts
Tested: bun test src/server/__tests__/network-settings.test.ts
Tested: cd desktop && bun run test -- src/__tests__/generalSettings.test.tsx --run
Tested: cd desktop && bun run check:electron
Tested: git diff --check
Not-tested: bun run check:server blocked by expired quarantine entries server:cron-scheduler, server:providers-real, server:tasks, server:e2e:business-flow, server:e2e:full-flow
Not-tested: live OpenAI OAuth through a corporate authenticated proxy
2026-06-02 21:37:35 +08:00

102 lines
3.0 KiB
TypeScript

import type { App, BrowserWindow, MenuItemConstructorOptions } from 'electron'
import { ELECTRON_EVENT_CHANNELS } from '../ipc/channels'
import { hideWindowSafely, toggleWindowFullScreen } from './windows'
export type NativeMenuDestination = 'about' | 'settings'
type ApplicationMenuActions = {
hide?: () => void
close?: () => void
toggleFullScreen?: () => void
}
export function buildApplicationMenuTemplate(
appName: string,
onNavigate: (destination: NativeMenuDestination) => void,
platform = process.platform,
actions: ApplicationMenuActions = {},
): MenuItemConstructorOptions[] {
const appMenu: MenuItemConstructorOptions[] = platform === 'darwin'
? [{
label: appName,
submenu: [
{ label: `About ${appName}`, click: () => onNavigate('about') },
{ type: 'separator' },
{ label: 'Settings...', accelerator: 'CmdOrCtrl+,', click: () => onNavigate('settings') },
{ type: 'separator' },
{ role: 'services' },
{ type: 'separator' },
{ label: `Hide ${appName}`, accelerator: 'Command+H', click: () => actions.hide?.() },
{ role: 'hideOthers' },
{ role: 'unhide' },
{ type: 'separator' },
{ role: 'quit' },
],
}]
: [{
label: 'File',
submenu: [
{ label: 'Settings...', accelerator: 'Ctrl+,', click: () => onNavigate('settings') },
{ type: 'separator' },
{ role: 'quit' },
],
}]
return [
...appMenu,
{
label: 'Edit',
submenu: [
{ role: 'undo' },
{ role: 'redo' },
{ type: 'separator' },
{ role: 'cut' },
{ role: 'copy' },
{ role: 'paste' },
{ role: 'selectAll' },
],
},
{
label: 'View',
submenu: [
{
label: 'Toggle Full Screen',
accelerator: platform === 'darwin' ? 'Ctrl+Command+F' : 'F11',
click: () => actions.toggleFullScreen?.(),
},
],
},
{
label: 'Window',
submenu: [
{ role: 'minimize' },
{ role: 'zoom' },
{ label: 'Close Window', accelerator: 'CmdOrCtrl+W', click: () => actions.close?.() },
],
},
]
}
export async function installApplicationMenu(app: App, getMainWindow: () => BrowserWindow | null) {
const { Menu } = await import('electron')
const template = buildApplicationMenuTemplate(app.name || 'Claude Code Haha', destination => {
getMainWindow()?.webContents.send(ELECTRON_EVENT_CHANNELS.nativeMenuNavigate, destination)
}, process.platform, {
hide: () => {
const window = getMainWindow()
if (!window) {
app.hide?.()
return
}
hideWindowSafely(window, () => app.hide?.())
},
close: () => {
getMainWindow()?.close()
},
toggleFullScreen: () => {
const window = getMainWindow()
if (window) toggleWindowFullScreen(window)
},
})
Menu.setApplicationMenu(Menu.buildFromTemplate(template))
}