From b3a82b88f03116353726a12591cc7f92bc4ff6d3 Mon Sep 17 00:00:00 2001 From: DerpyCrabs Date: Fri, 23 Oct 2026 13:07:43 +0300 Subject: [PATCH] fix(desktop): initialize and paint background tabs on Wayland --- .../verify-background-preview.electron.cjs | 12 ++++-- .../src/preview/BackgroundTabWindow.ts | 4 +- apps/desktop/src/preview/Manager.test.ts | 42 ++++++++++++++----- apps/desktop/src/preview/Manager.ts | 6 +++ 4 files changed, 48 insertions(+), 16 deletions(-) diff --git a/apps/desktop/scripts/verify-background-preview.electron.cjs b/apps/desktop/scripts/verify-background-preview.electron.cjs index 54a847a0189a0..eb28b1b227fb2 100644 --- a/apps/desktop/scripts/verify-background-preview.electron.cjs +++ b/apps/desktop/scripts/verify-background-preview.electron.cjs @@ -6,6 +6,10 @@ const NodePath = require("node:path"); const NodeURL = require("node:url"); const { app, BrowserWindow, nativeImage } = require("electron"); +const testHome = NodeFS.mkdtempSync(NodePath.join(NodeOS.tmpdir(), "t3-background-preview-")); +app.setPath("userData", NodePath.join(testHome, "userdata")); +app.on("quit", () => NodeFS.rmSync(testHome, { recursive: true, force: true })); + const CAPTURE_TIMEOUT_MS = 5_000; const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms)); const withTimeout = (promise, ms) => @@ -36,10 +40,7 @@ async function main() { const { backgroundTabWindowOptions } = await import( NodeURL.pathToFileURL(NodePath.join(__dirname, "../src/preview/BackgroundTabWindow.ts")).href ); - const preload = NodePath.join( - NodeFS.mkdtempSync(NodePath.join(NodeOS.tmpdir(), "t3-background-preview-")), - "preload.cjs", - ); + const preload = NodePath.join(testHome, "preload.cjs"); NodeFS.writeFileSync(preload, ""); // Each state closes its windows, which would otherwise quit the app. app.on("window-all-closed", () => {}); @@ -47,6 +48,7 @@ async function main() { const failures = []; for (const state of ["hidden", "minimized"]) { + console.log(`Checking app window ${state}`); const appWindow = new BrowserWindow({ width: 480, height: 320, show: true }); await appWindow.loadURL(page("T3 Code")); await sleep(500); @@ -63,6 +65,7 @@ async function main() { }), ); await tabWindow.loadURL(page("")); + console.log(`Background tab loaded (${state})`); const tab = tabWindow.webContents; tab.debugger.attach("1.3"); for (const [step, rgb] of [ @@ -71,6 +74,7 @@ async function main() { [3, [0, 200, 200]], ]) { const failure = await captureColor(tab, rgb); + console.log(`App window ${state}, capture ${step}: ${failure ?? "fresh"}`); if (failure) failures.push(`app window ${state}, capture ${step}: ${failure}`); await sleep(1_000); } diff --git a/apps/desktop/src/preview/BackgroundTabWindow.ts b/apps/desktop/src/preview/BackgroundTabWindow.ts index f9ceb472caf97..5b404e74ceab1 100644 --- a/apps/desktop/src/preview/BackgroundTabWindow.ts +++ b/apps/desktop/src/preview/BackgroundTabWindow.ts @@ -11,7 +11,8 @@ const BACKGROUND_TAB_HEIGHT = 800; /** * An agent tab's never-shown window. A panel `` that attaches while * the app window is hidden or minimized never paints, so its screenshots never - * return; a window of its own paints in every app window state. + * return. Offscreen rendering also avoids dependence on a mapped Wayland + * surface: a regular never-shown BrowserWindow can still stall captures there. */ export function backgroundTabWindowOptions(input: { readonly partition: string; @@ -34,6 +35,7 @@ export function backgroundTabWindowOptions(input: { contextIsolation: false, disableHtmlFullscreenWindowResize: true, backgroundThrottling: false, + offscreen: true, ...(input.passkeyBridgeArgument === undefined ? {} : { additionalArguments: [input.passkeyBridgeArgument] }), diff --git a/apps/desktop/src/preview/Manager.test.ts b/apps/desktop/src/preview/Manager.test.ts index 6b67b010a57c9..f3f150a67fb36 100644 --- a/apps/desktop/src/preview/Manager.test.ts +++ b/apps/desktop/src/preview/Manager.test.ts @@ -600,9 +600,9 @@ describe("PreviewManager", () => { ), ); - effectIt.effect( - "hosts an agent tab in a never-shown window that paints and closes with the tab", - () => + effectIt.effect.each([undefined, "https://example.com/agent"])( + "hosts an agent tab in a never-shown window that initializes before attaching (%s)", + (url) => withManager((manager) => Effect.gen(function* () { const tab = makeTestPreviewWebContents( @@ -613,8 +613,20 @@ describe("PreviewManager", () => { 55, null as unknown as TestHostWebContents, ); - const loadURL = vi.fn(async () => undefined); - Object.assign(tab, { getType: () => "window", once: vi.fn(), loadURL, reload: vi.fn() }); + const firstDocument = Promise.withResolvers(); + const debuggerStarted = Promise.withResolvers(); + const loadURL = vi.fn(async () => firstDocument.resolve()); + tab.debugger.sendCommand = vi.fn(async () => { + debuggerStarted.resolve(); + await firstDocument.promise; + }); + Object.assign(tab, { + getType: () => "window", + isDevToolsOpened: () => false, + once: vi.fn(), + loadURL, + reload: vi.fn(), + }); const window = { webContents: tab, once: vi.fn(), @@ -630,11 +642,18 @@ describe("PreviewManager", () => { }); fromId.mockReturnValue(tab); - const state = yield* manager.createTab( - "tab_agent", - { serverTab: { threadId: "thread", tabId: "agent" } }, - { partition: "persist:t3code-preview-test", url: "https://example.com/agent" }, - ); + const opening = yield* manager + .createTab( + "tab_agent", + { serverTab: { threadId: "thread", tabId: "agent" } }, + { partition: "persist:t3code-preview-test", ...(url === undefined ? {} : { url }) }, + ) + .pipe(Effect.forkScoped); + yield* Effect.promise(() => debuggerStarted.promise); + const loadedBeforeDebugger = loadURL.mock.calls.length > 0; + firstDocument.resolve(); + expect(loadedBeforeDebugger).toBe(true); + const state = yield* Fiber.join(opening); expect(options).toMatchObject({ show: false, @@ -642,10 +661,11 @@ describe("PreviewManager", () => { webPreferences: { partition: "persist:t3code-preview-test", backgroundThrottling: false, + offscreen: true, }, }); expect(state.webContentsId).toBe(55); - expect(loadURL).toHaveBeenCalledWith("https://example.com/agent"); + expect(loadURL).toHaveBeenCalledWith(url ?? "about:blank"); yield* manager.closeTab("tab_agent"); expect(window.destroy).toHaveBeenCalledOnce(); }), diff --git a/apps/desktop/src/preview/Manager.ts b/apps/desktop/src/preview/Manager.ts index 70ce945061320..f5cdac5118fd6 100644 --- a/apps/desktop/src/preview/Manager.ts +++ b/apps/desktop/src/preview/Manager.ts @@ -1775,6 +1775,12 @@ const makeNativeOperations = Effect.fn("PreviewManager.makeOperations")(function wc.on("render-process-gone", () => { if (!wc.isDestroyed()) wc.reload(); }); + // A new BrowserWindow has no document. Its debugger's background-color + // command waits for one, so initialize a local page before attaching. + yield* attemptPromise( + { operation: "backgroundWindow.initialize", tabId, webContentsId: wc.id }, + () => wc.loadURL("about:blank"), + ); yield* prepareWebview(wc); yield* registerWebviewUnlocked(tabId, wc.id, tabLifecycleGenerations.get(tabId)); if (background.url !== undefined) {