mirror of
https://github.com/NanmiCoder/claude-code-haha.git
synced 2026-10-10 11:53:10 +08:00
7e50c31986
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.
91 lines
3.4 KiB
TypeScript
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()
|
|
}
|
|
}
|