Files
claude-code-haha/desktop/electron/services/workspaceBrowserMenu.ts
程序员阿江-Relakkes 7e50c31986 fix(desktop): composite the workspace browser with the app UI (#1437)
The workspace browser was a native WebContentsView, which always paints
above the DOM. Switching to the skills or settings page left the page
floating over it, and menus over the page needed a screenshot swap that
showed up clipped and flashed white.

Pages are now <webview> guests kept in a layer inside the session panel
that is never unmounted, so other pages, menus and dialogs draw over
them like any element. The main process adopts each guest and keeps all
in-page behaviour: navigation, history, find, zoom, capture, PDF,
downloads, shortcuts and annotation.

- Guard every attach in the main window: browser partition only, start
  at about:blank, preload and sandbox pinned, reported ids validated.
- Match the native page: drop the blank history entry, keep page zoom
  independent of app zoom, allow popups so they still become tabs.
- Keep app drags working over a page and close menus on a click into it.
- Tell the side dock it is off screen when the session page is hidden.
- Remove the overlay snapshot machinery and native bounds syncing.
2026-10-04 03:24:50 +08:00

91 lines
3.4 KiB
TypeScript

import type { MenuItemConstructorOptions } from 'electron'
import type { WorkspaceBrowserMenuAction, WorkspaceBrowserMenuOptions } from '../../src/lib/desktopHost/types'
import { MAX_APP_ZOOM, MIN_APP_ZOOM } from './zoom'
export type WorkspaceBrowserNativeMenu = {
popup(options: { x: number; y: number; callback: () => void }): void
closePopup(): void
}
export type WorkspaceBrowserMenuFactory = (template: MenuItemConstructorOptions[]) => WorkspaceBrowserNativeMenu
/** Renderer anchors are CSS pixels; Electron popup coordinates use window DIP. */
export function workspaceBrowserMenuPosition(
anchor: { x: number; y: number },
hostZoom: number,
contentSize: { width: number; height: number },
): { x: number; y: number } {
if (![anchor.x, anchor.y, hostZoom, contentSize.width, contentSize.height].every(Number.isFinite) || hostZoom <= 0) {
throw new Error('Invalid workspace browser menu position')
}
return {
x: Math.round(Math.max(0, Math.min(anchor.x * hostZoom, Math.max(0, contentSize.width - 1)))),
y: Math.round(Math.max(0, Math.min(anchor.y * hostZoom, Math.max(0, contentSize.height - 1)))),
}
}
export function buildWorkspaceBrowserMenuTemplate(
options: WorkspaceBrowserMenuOptions,
select: (action: WorkspaceBrowserMenuAction) => void,
): MenuItemConstructorOptions[] {
const item = (action: WorkspaceBrowserMenuAction, enabled = true): MenuItemConstructorOptions => ({
id: action,
label: options.labels[action],
enabled,
click: () => { if (enabled) select(action) },
})
return [
item('find', options.hasPage),
item('print', options.hasPage),
{ type: 'separator' },
{ id: 'zoom', label: `${options.labels.zoom} · ${Math.round(options.zoomFactor * 100)}%`, enabled: false },
item('zoomOut', options.zoomFactor > MIN_APP_ZOOM),
item('zoomIn', options.zoomFactor < MAX_APP_ZOOM),
item('zoomReset', options.zoomFactor !== 1),
{ type: 'separator' },
item('capture', options.hasPage),
item('pickElement', options.hasPage),
{ type: 'separator' },
item('downloads'),
item('history'),
item('openExternal', options.canOpenExternal),
]
}
/** One popup belongs to one live page; dismissal never hides or reloads that page. */
export class WorkspaceBrowserMenuController {
private pending: { tabId: string; cancel: () => void } | null = null
constructor(private readonly createMenu: WorkspaceBrowserMenuFactory) {}
show(tabId: string, options: WorkspaceBrowserMenuOptions): Promise<WorkspaceBrowserMenuAction | null> {
this.cancel()
return new Promise((resolve, reject) => {
let settled = false
const finish = (action: WorkspaceBrowserMenuAction | null) => {
if (settled) return
settled = true
if (this.pending === pending) this.pending = null
resolve(action)
}
const menu = this.createMenu(buildWorkspaceBrowserMenuTemplate(options, finish))
const pending = { tabId, cancel: () => {
finish(null)
menu.closePopup()
} }
this.pending = pending
try {
menu.popup({ x: options.x, y: options.y, callback: () => finish(null) })
} catch (error) {
settled = true
if (this.pending === pending) this.pending = null
reject(error)
}
})
}
cancel(tabId?: string): void {
if (this.pending && (tabId === undefined || this.pending.tabId === tabId)) this.pending.cancel()
}
}