Relakkes Yang 98b79ce51a fix: restore Windows custom desktop chrome
Use frameless Electron chrome only on Windows and remove the native Windows application menu so the packaged app matches the previous Tauri-style desktop surface.

Add a Windows-only manual drag fallback for desktop drag regions, while excluding tab reorder targets and preserving macOS/Linux native chrome and menu behavior.

Tested: cd desktop && bun run test --run electron/services/menu.test.ts electron/services/windows.test.ts src/hooks/useElectronWindowDragRegions.test.tsx src/components/layout/TabBar.test.tsx src/components/layout/Sidebar.test.tsx src/lib/desktopHost/electronHost.test.ts electron/ipc/capabilities.test.ts

Tested: cd desktop && SKIP_INSTALL=1 bun run build:windows-x64

Tested: Computer Use Windows packaged app smoke verified no native menu, custom controls, drag regions, tab reorder, close-to-background, and deepseek-v4-pro provider response FINAL_WINDOWS_ELECTRON_REAL_PROVIDER_OK.

Not-tested: full bun run verify.

Constraint: keep custom frameless behavior scoped to win32 so macOS and Linux native chrome paths remain intact.

Confidence: high

Scope-risk: moderate
2026-06-03 23:24:30 +08:00

236 lines
6.6 KiB
TypeScript

import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs'
import path from 'node:path'
import type { App, BrowserWindow, BrowserWindowConstructorOptions, Display } from 'electron'
export const WINDOW_STATE_FILE = 'window-state.json'
export const DEFAULT_WINDOW_WIDTH = 1280
export const DEFAULT_WINDOW_HEIGHT = 820
export const MIN_WINDOW_WIDTH = 960
export const MIN_WINDOW_HEIGHT = 640
const MIN_VISIBLE_PIXELS = 80
export type StoredWindowState = {
x: number
y: number
width: number
height: number
maximized: boolean
}
export type WindowStateBounds = Pick<StoredWindowState, 'x' | 'y' | 'width' | 'height'>
export type WindowCreateBounds =
& Partial<Pick<StoredWindowState, 'x' | 'y'>>
& Pick<StoredWindowState, 'width' | 'height'>
export type WindowChromeOptions = Pick<
BrowserWindowConstructorOptions,
'autoHideMenuBar' | 'frame' | 'fullscreenable' | 'titleBarStyle'
>
export function windowStatePath(app: App, env: NodeJS.ProcessEnv = process.env): string {
return path.join(env.CLAUDE_CONFIG_DIR || app.getPath('userData'), WINDOW_STATE_FILE)
}
export function isPersistableWindowState(state: StoredWindowState): boolean {
return Number.isFinite(state.x)
&& Number.isFinite(state.y)
&& state.width >= MIN_WINDOW_WIDTH
&& state.height >= MIN_WINDOW_HEIGHT
}
export function hasMeaningfulIntersection(
state: WindowStateBounds,
displayBounds: WindowStateBounds,
): boolean {
const stateRight = state.x + state.width
const stateBottom = state.y + state.height
const displayRight = displayBounds.x + displayBounds.width
const displayBottom = displayBounds.y + displayBounds.height
return stateRight > displayBounds.x + MIN_VISIBLE_PIXELS
&& stateBottom > displayBounds.y + MIN_VISIBLE_PIXELS
&& state.x < displayRight - MIN_VISIBLE_PIXELS
&& state.y < displayBottom - MIN_VISIBLE_PIXELS
}
export function isWindowStateVisibleOnAnyDisplay(
state: StoredWindowState,
displays: Array<Pick<Display, 'bounds' | 'workArea'>>,
): boolean {
if (displays.length === 0) return true
return displays.some(display =>
hasMeaningfulIntersection(state, display.workArea ?? display.bounds),
)
}
function clamp(value: number, min: number, max: number): number {
return Math.min(Math.max(value, min), max)
}
export function clampWindowStateToVisibleWorkArea(
state: StoredWindowState,
displays: Array<Pick<Display, 'bounds' | 'workArea'>>,
): StoredWindowState {
const display = displays.find(candidate =>
hasMeaningfulIntersection(state, candidate.workArea ?? candidate.bounds),
)
if (!display) return state
const workArea = display.workArea ?? display.bounds
const maxX = workArea.x + Math.max(0, workArea.width - state.width)
const maxY = workArea.y + Math.max(0, workArea.height - state.height)
return {
...state,
x: clamp(state.x, workArea.x, maxX),
y: clamp(state.y, workArea.y, maxY),
}
}
export function readWindowState(
app: App,
displays: Array<Pick<Display, 'bounds' | 'workArea'>>,
env: NodeJS.ProcessEnv = process.env,
platform = process.platform,
): StoredWindowState | null {
const statePath = windowStatePath(app, env)
if (!existsSync(statePath)) return null
try {
const parsed = JSON.parse(readFileSync(statePath, 'utf-8')) as StoredWindowState
if (!isPersistableWindowState(parsed)) return null
if (!isWindowStateVisibleOnAnyDisplay(parsed, displays)) return null
return platform === 'darwin'
? clampWindowStateToVisibleWorkArea(parsed, displays)
: parsed
} catch (error) {
console.error(`[desktop] failed to read Electron window state ${statePath}:`, error)
return null
}
}
export function writeWindowState(
app: App,
state: StoredWindowState,
env: NodeJS.ProcessEnv = process.env,
) {
if (!isPersistableWindowState(state)) return
const statePath = windowStatePath(app, env)
mkdirSync(path.dirname(statePath), { recursive: true })
writeFileSync(statePath, `${JSON.stringify(state, null, 2)}\n`)
}
export function captureWindowState(window: BrowserWindow): StoredWindowState | null {
if (window.isMinimized()) return null
const bounds = window.getBounds()
const state = {
x: bounds.x,
y: bounds.y,
width: bounds.width,
height: bounds.height,
maximized: window.isMaximized(),
}
return isPersistableWindowState(state) ? state : null
}
export function windowOptionsFromState(state: StoredWindowState | null): WindowCreateBounds {
return state
? { x: state.x, y: state.y, width: state.width, height: state.height }
: { width: DEFAULT_WINDOW_WIDTH, height: DEFAULT_WINDOW_HEIGHT }
}
export function windowChromeOptionsForPlatform(
platform: NodeJS.Platform = process.platform,
): WindowChromeOptions {
if (platform === 'darwin') {
return {
titleBarStyle: 'hiddenInset',
fullscreenable: false,
}
}
if (platform === 'win32') {
return {
frame: false,
autoHideMenuBar: true,
fullscreenable: true,
}
}
return {
titleBarStyle: 'default',
fullscreenable: true,
}
}
export function restoreWindowMaximized(window: BrowserWindow, state: StoredWindowState | null) {
if (state?.maximized) window.maximize()
}
export function saveWindowState(app: App, window: BrowserWindow) {
const state = captureWindowState(window)
if (state) writeWindowState(app, state)
}
export function hideWindowSafely(window: BrowserWindow, afterHide?: () => void) {
if (window.isSimpleFullScreen()) {
window.setSimpleFullScreen(false)
window.hide()
afterHide?.()
return
}
if (!window.isFullScreen()) {
window.hide()
afterHide?.()
return
}
window.once('leave-full-screen', () => {
if (!window.isDestroyed()) {
window.hide()
afterHide?.()
}
})
window.setFullScreen(false)
}
export function toggleWindowFullScreen(window: BrowserWindow, platform = process.platform) {
if (platform === 'darwin') {
window.setSimpleFullScreen(!window.isSimpleFullScreen())
return
}
window.setFullScreen(!window.isFullScreen())
}
type MacOsWindowVisibilityApp = {
show?: () => void
}
export function showMainWindow(window: BrowserWindow | null, app?: MacOsWindowVisibilityApp) {
if (!window) return
app?.show?.()
if (!window.isVisible()) window.show()
if (window.isMinimized()) window.restore()
window.focus()
}
export function installWindowLifecycle({
app,
window,
shouldQuit,
}: {
app: App
window: BrowserWindow
shouldQuit: () => boolean
}) {
window.on('close', (event) => {
saveWindowState(app, window)
if (shouldQuit()) return
event.preventDefault()
hideWindowSafely(window)
})
window.on('move', () => saveWindowState(app, window))
window.on('resize', () => saveWindowState(app, window))
}