import { app, BrowserWindow, ipcMain, nativeImage, safeStorage, screen, WebContentsView } from 'electron' import assert from 'node:assert/strict' import { randomUUID } from 'node:crypto' import { createRequire } from 'node:module' import { createServer } from 'node:http' import { mkdir, writeFile } from 'node:fs/promises' import { join } from 'node:path' import { registerIpcHandlers } from '../../src/main/ipc' import { AssistantDatabase } from '../../src/main/assistant/assistant-database' import { ApplicationSettingsStore } from '../../src/main/application-settings-store' import { BrowserService } from '../../src/main/browser/browser-service' import { CapabilityService } from '../../src/main/capabilities/capability-service' import { defaultRuntimeSettings } from '../../src/shared/contracts' import { ipcChannels } from '../../src/shared/ipc-channels' const directory = process.env.GB_MCP_DIRECTORY! app.setPath('userData', join(directory, 'profile')) app.commandLine.appendSwitch('force-device-scale-factor', '1') app.commandLine.appendSwitch('disable-features', 'CalculateNativeWinOcclusion') app.whenReady().then(async () => { const win = new BrowserWindow({ show: true, frame: false, width: 1280, height: 800, useContentSize: true, webPreferences: { sandbox: true, contextIsolation: true, nodeIntegration: false, backgroundThrottling: false, preload: join(directory, 'preload.cjs') } }) const database = new AssistantDatabase(join(directory, 'assistant.sqlite')) database.initialize(directory) const project = database.listProjects()[0]! const archivedProject = database.createProject({ name: 'Archived workspace', description: '', rootPath: directory }) const archivedConversationId = randomUUID() database.saveLocalConversations([{ header: { id: archivedConversationId, projectId: archivedProject.id, title: 'Archived history', updatedAt: 1 }, messages: [{ id: randomUUID(), role: 'assistant', state: 'complete', createdAt: 1, content: 'Preserved archived message' }] }]) database.setProjectArchived(archivedProject.id, true) const conversationId = randomUUID() const resultId = randomUUID() const html = 'Jump to end' + '

Long preview text

'.repeat(80) + '

End

' database.saveLocalConversations([{ header: { id: conversationId, projectId: project.id, title: 'Overlay paths', updatedAt: Date.now() }, messages: [{ id: randomUUID(), role: 'assistant', state: 'complete', createdAt: Date.now(), content: '```html\n' + html + '\n```', attachments: [{ id: randomUUID(), kind: 'text', name: 'review.pdf', size: 20, preview: 'Saved result', resultId }], sourceReferences: [{ libraryId: randomUUID(), libraryName: 'Review', documentName: 'Review source', sourceName: 'Fixture', snippet: 'Saved citation', rank: 1, external: { kind: 'external', provider: 'fastgpt', instanceId: randomUUID(), remoteKnowledgeBaseId: 'review' } }] }, { id: randomUUID(), role: 'assistant', state: 'complete', createdAt: Date.now(), content: '```html\n

Plain keyboard preview

' + '

Long plain content

'.repeat(80) + '\n```' }] }]) database.saveLocalConversations(Array.from({ length: 14 }, (_, i) => ({ header: { id: randomUUID(), projectId: project.id, title: `Earlier workspace conversation with a long title ${i}`, updatedAt: new Date(2025, 8, 2, 10, i).getTime() }, messages: [] }))) const settings = { ...defaultRuntimeSettings, workspacePath: directory, modelProfiles: [], embeddingConnections: [] } const applicationSettings = new ApplicationSettingsStore(join(directory, 'application.json')) await applicationSettings.update({ checkUpdatesOnStartup: false }) const browser = new BrowserService({ parentWindow: win }) const cipher = { isAvailable: () => safeStorage.isEncryptionAvailable(), encrypt: (value: string) => safeStorage.encryptString(value), decrypt: (value: Buffer) => safeStorage.decryptString(value) } const capabilities = new CapabilityService(join(directory, 'capabilities.json'), join(directory, 'builtin-skills'), join(directory, 'imported-skills'), cipher) const dispose = registerIpcHandlers(win, { capability: 'text', getStatus: async () => ({ available: true, name: 'Overlay fixture', capability: 'text' }) } as never, 'CommandOrControl+Shift+Space', { getResolvedSettings: async () => settings, getPublicSettings: async () => settings } as never, capabilities, { clear() {}, cancelImport() {}, getDraft: () => [] } as never, { snapshot: () => ({ libraries: [], sources: [], documents: [], entities: [], relations: [], evidence: [], tasks: [] }), database: { externalStore: { listBindings: () => [] } }, external: { listInstances: () => [] } } as never, database, {} as never, async () => {}, undefined, browser, undefined, undefined, applicationSettings, undefined, undefined, undefined, undefined, undefined, undefined, undefined, undefined, undefined, undefined, { getPending: async () => undefined } as never) // Parsing payloads are fixtures; App, preload, window keyboard IPC and browser services are production. const redImage = nativeImage.createFromBitmap(Buffer.from([0, 0, 255, 255]), { width: 1, height: 1 }).resize({ width: 200, height: 100 }).toDataURL() ipcMain.removeHandler(ipcChannels.documentParsingResult) ipcMain.removeHandler(ipcChannels.documentParsingImage) ipcMain.handle(ipcChannels.documentParsingResult, () => ({ id: resultId, fileName: 'review.pdf', sourceFormat: '.pdf', sections: [{ locator: 'Page 1', content: '![Review](asset:image)' }], images: [{ id: 'image', pageNumber: 1, width: 200, height: 100, size: 100, mimeType: 'image/png' }], missingImages: [], warnings: [], completeness: 'complete', parsedAt: new Date().toISOString(), durationMs: 1, settings: {} })) ipcMain.handle(ipcChannels.documentParsingImage, () => redImage) const js = (code: string): Promise => win.webContents.executeJavaScript(code) const settle = (): Promise => js('new Promise(r=>requestAnimationFrame(()=>requestAnimationFrame(r)))') const wait = async (code: string): Promise => { for (let i = 0; i < 400; i++) { if (await js(code)) return await new Promise(r => setTimeout(r, 25)) } throw new Error(`Timeout: ${code}\nFocus: ${await js('document.activeElement?.outerHTML')}\n${await js('document.body.innerText')}`) } const click = async (selector: string): Promise => { await wait(`!!document.querySelector(${JSON.stringify(selector)})`) // The frosted header overlays the top of the chat viewport; nearest may leave a control underneath it. await js(`document.querySelector(${JSON.stringify(selector)}).scrollIntoView({block:'center'})`) await settle() const p = await js<{ x: number; y: number; hit: boolean; cover?: string }>(`(()=>{const e=document.querySelector(${JSON.stringify(selector)}),r=e.getBoundingClientRect(),x=Math.round(r.x+r.width/2),y=Math.round(r.y+r.height/2),cover=document.elementFromPoint(x,y);return {x,y,hit:e.contains(cover),cover:cover?.outerHTML.slice(0,500)}})()`) assert(p.hit, `Occluded ${selector}: ${JSON.stringify(p)}`) win.focus() win.webContents.sendInputEvent({ type: 'mouseDown', x: p.x, y: p.y, button: 'left', clickCount: 1 }) win.webContents.sendInputEvent({ type: 'mouseUp', x: p.x, y: p.y, button: 'left', clickCount: 1 }) await settle() } const key = async (keyCode: string, modifiers: Electron.KeyboardInputEvent['modifiers'] = []): Promise => { if (!win.isFocused()) win.focus() // Do not refocus WebContents here: it would reset a focused child frame. win.webContents.sendInputEvent({ type: 'keyDown', keyCode, modifiers }) win.webContents.sendInputEvent({ type: 'keyUp', keyCode, modifiers }) await settle() } const evidence: Record = {} const artifacts = process.env.GB_OVERLAY_PATHS_ARTIFACTS if (artifacts) await mkdir(artifacts, { recursive: true }) const screenshot = async (name: string): Promise => { if (artifacts) await writeFile(join(artifacts, name + '.png'), (await win.webContents.capturePage()).toPNG()) } const server = createServer((_q, r) => { r.setHeader('Content-Type', 'text/html'); r.end('

Native overlay page

') }) await new Promise(r => server.listen(0, '127.0.0.1', r)) const origin = `http://127.0.0.1:${(server.address() as { port: number }).port}/` const native = (): WebContentsView | undefined => win.contentView.children.find(v => v instanceof WebContentsView) as WebContentsView | undefined const waitNative = async (visible: boolean): Promise => { for (let i = 0; i < 400; i++) { if (native()?.getVisible() === visible) return await new Promise(r => setTimeout(r, 25)) } assert.equal(native()?.getVisible(), visible) } try { await win.loadURL(process.env.GB_MCP_URL!) await wait('!!document.querySelector(".message-html-preview")') for (const theme of ['light', 'dark']) { await js(`document.documentElement.dataset.theme='${theme}'`) await click('[aria-label="查看解析结果:review.pdf"]') await click('.document-result-image button') await wait('!!document.querySelector(".image-viewer-dialog")') assert(await js(`getComputedStyle(document.querySelector('.image-viewer-backdrop')).zIndex === getComputedStyle(document.querySelector('.document-parsing-diagnostic-backdrop')).zIndex && document.querySelector('.document-parsing-diagnostic-backdrop').inert`)) const imageRect = await js<{ x: number; y: number }>(`(()=>{const r=document.querySelector('.image-viewer-dialog img').getBoundingClientRect();return {x:Math.round(r.x+r.width/2),y:Math.round(r.y+r.height/2)}})()`) const pixel = (await win.webContents.capturePage({ ...imageRect, width: 1, height: 1 })).toBitmap() assert(pixel[2]! > 240 && pixel[1]! < 10 && pixel[0]! < 10, 'Image pixels must paint above the inert parent') await screenshot(theme + '-nested-image') await key('Escape') await wait('!document.querySelector(".image-viewer-dialog") && !document.querySelector(".document-parsing-diagnostic-backdrop").inert') await wait('document.activeElement.matches(".document-result-image button")') await key('Escape') await wait('!document.querySelector(".document-parsing-diagnostic")') } evidence.image = true await click('.message-html-preview__actions button:last-child') await wait('!!document.querySelector(".html-preview-viewer")') assert(await js(`document.querySelector('.html-preview-viewer iframe').getAttribute('sandbox') === '' && document.querySelector('.html-preview-viewer iframe').srcdoc.includes("script-src 'none'")`)) await key('Tab') await wait('document.activeElement.matches(".html-preview-viewer iframe")') assert(await js(`!document.querySelector('.html-preview-viewer iframe').srcdoc.includes('