Repository navigation
fix(desktop): agent preview tabs keep working while T3 is hidden #22743
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Open
juliusmarminge
wants to merge
10
commits into
main
Choose a base branch
from
t3/preview-background-rendering-lease
base: main
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
+740
−71
Open
Changes from all commits
Commits
Show all changes
10 commits
Select commit
Hold shift + click to select a range
6a3a2cd
fix(desktop): agent preview tabs keep working while T3 is hidden
juliusmarminge d22c957
Merge origin/main into t3/preview-background-rendering-lease
juliusmarminge 317bbb9
fix(desktop): settle background preview tabs on main's preview shape
juliusmarminge 9b6bc6a
fix(desktop): background preview windows stay out of app window fallback
juliusmarminge a623078
fix(desktop): a navigation during background window setup wins over i…
juliusmarminge ad8c5a4
fix(desktop): agent tabs use a background window only while T3 is hidden
juliusmarminge 06510f5
fix(desktop): background agent tabs move into the panel once T3 is shown
juliusmarminge 601b0fe
fix(desktop): only a viewed background agent tab moves into the panel
juliusmarminge 4d5013a
Merge remote-tracking branch 'origin/main' into t3/preview-background…
juliusmarminge 45ae805
fix(desktop): background agent tabs keep their host so the page is ne…
juliusmarminge File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
95 changes: 95 additions & 0 deletions
95
apps/desktop/scripts/verify-background-preview.electron.cjs
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,95 @@ | ||
| // Electron entry for verify-background-preview.mjs. CommonJS, because an ESM | ||
| // entry delays `ready` until its top-level await settles. | ||
| const NodeFS = require("node:fs"); | ||
| const NodeOS = require("node:os"); | ||
| const NodePath = require("node:path"); | ||
| const NodeURL = require("node:url"); | ||
| const { app, BrowserWindow, nativeImage } = require("electron"); | ||
|
|
||
| const CAPTURE_TIMEOUT_MS = 5_000; | ||
| const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms)); | ||
| const withTimeout = (promise, ms) => | ||
| Promise.race([ | ||
| promise.then((value) => ({ ok: true, value })), | ||
| sleep(ms).then(() => ({ ok: false })), | ||
| ]); | ||
| const page = (html) => `data:text/html,${encodeURIComponent(html)}`; | ||
|
|
||
| async function captureColor(tab, rgb) { | ||
| await tab.debugger.sendCommand("Runtime.evaluate", { | ||
| expression: `document.body.style.background = 'rgb(${rgb.join(",")})'`, | ||
| }); | ||
| const shot = await withTimeout( | ||
| tab.debugger.sendCommand("Page.captureScreenshot", { format: "png" }), | ||
| CAPTURE_TIMEOUT_MS, | ||
| ); | ||
| if (!shot.ok) return "timed out"; | ||
| const pixels = nativeImage.createFromBuffer(Buffer.from(shot.value.data, "base64")).toBitmap(); | ||
| const got = [pixels[2], pixels[1], pixels[0]]; | ||
| // Color management can shift a channel by a few levels. | ||
| return got.every((value, index) => Math.abs(value - rgb[index]) <= 6) | ||
| ? null | ||
| : `stale: rgb(${got.join(",")})`; | ||
| } | ||
|
|
||
| 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", | ||
| ); | ||
| NodeFS.writeFileSync(preload, ""); | ||
| // Each state closes its windows, which would otherwise quit the app. | ||
| app.on("window-all-closed", () => {}); | ||
| await app.whenReady(); | ||
|
|
||
| const failures = []; | ||
| for (const state of ["hidden", "minimized"]) { | ||
| const appWindow = new BrowserWindow({ width: 480, height: 320, show: true }); | ||
| await appWindow.loadURL(page("<body>T3 Code</body>")); | ||
| await sleep(500); | ||
| if (state === "hidden") appWindow.hide(); | ||
| else appWindow.minimize(); | ||
| await sleep(1_500); | ||
|
|
||
| // Created only after the app window is gone, as an agent's tab would be. | ||
| const tabWindow = new BrowserWindow( | ||
| backgroundTabWindowOptions({ | ||
| partition: `t3-background-preview-${state}`, | ||
| preload, | ||
| passkeyBridgeArgument: undefined, | ||
| }), | ||
| ); | ||
| await tabWindow.loadURL(page("<body style='margin:0'></body>")); | ||
| const tab = tabWindow.webContents; | ||
| tab.debugger.attach("1.3"); | ||
| for (const [step, rgb] of [ | ||
| [1, [0, 200, 0]], | ||
| [2, [200, 200, 0]], | ||
| [3, [0, 200, 200]], | ||
| ]) { | ||
| const failure = await captureColor(tab, rgb); | ||
| if (failure) failures.push(`app window ${state}, capture ${step}: ${failure}`); | ||
| await sleep(1_000); | ||
| } | ||
| tabWindow.destroy(); | ||
| appWindow.destroy(); | ||
| } | ||
|
|
||
| if (failures.length > 0) { | ||
| console.error("Background preview tabs stopped painting:"); | ||
| for (const failure of failures) console.error(` - ${failure}`); | ||
| app.exit(1); | ||
| return; | ||
| } | ||
| console.log("Background preview tabs paint while the app window is hidden or minimized."); | ||
| NodeFS.writeFileSync(process.env.T3_BACKGROUND_PREVIEW_DONE, "ok"); | ||
| app.exit(0); | ||
| } | ||
|
|
||
| main().catch((error) => { | ||
| console.error(error); | ||
| app.exit(1); | ||
| }); | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,38 @@ | ||
| // Proves agent preview tabs still paint while the app window is hidden or minimized, | ||
| // using the window options the preview manager ships. Unit tests mock Electron. | ||
| import * as NodeChildProcess from "node:child_process"; | ||
| import * as NodeFS from "node:fs"; | ||
| import * as NodeModule from "node:module"; | ||
| import * as NodeOS from "node:os"; | ||
| import * as NodePath from "node:path"; | ||
| import * as NodeURL from "node:url"; | ||
|
|
||
| const RUN_TIMEOUT_MS = 90_000; | ||
| const entry = NodePath.join( | ||
| NodePath.dirname(NodeURL.fileURLToPath(import.meta.url)), | ||
| "verify-background-preview.electron.cjs", | ||
| ); | ||
| // The plain binary: the development launcher's renamed app bundle refuses data: URLs. | ||
| const electronPath = NodeModule.createRequire(import.meta.url)("electron"); | ||
| const { ELECTRON_RUN_AS_NODE: _ignored, ...env } = process.env; | ||
| // Only a run that finished every capture writes this, so an early clean quit still fails. | ||
| const done = NodePath.join( | ||
| NodeFS.mkdtempSync(NodePath.join(NodeOS.tmpdir(), "t3-background-preview-")), | ||
| "done", | ||
| ); | ||
| const child = NodeChildProcess.spawn(electronPath, [entry], { | ||
| stdio: "inherit", | ||
| env: { ...env, T3_BACKGROUND_PREVIEW_DONE: done }, | ||
| }); | ||
| const timer = setTimeout(() => { | ||
| console.error(`Background preview check did not finish within ${RUN_TIMEOUT_MS}ms.`); | ||
| child.kill("SIGKILL"); | ||
| }, RUN_TIMEOUT_MS); | ||
| child.on("exit", (code, signal) => { | ||
| clearTimeout(timer); | ||
| if (code === 0 && !NodeFS.existsSync(done)) { | ||
| console.error("Background preview check quit before it finished."); | ||
| process.exit(1); | ||
| } | ||
| process.exit(code ?? (signal ? 1 : 0)); | ||
|
coderabbitai[bot] marked this conversation as resolved.
Resolved
|
||
| }); | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,42 @@ | ||
| import type { BrowserWindowConstructorOptions } from "electron"; | ||
|
|
||
| // Kept free of runtime imports: scripts/verify-background-preview.mjs loads it | ||
| // straight into Electron to prove these options still paint while the app | ||
| // window is hidden. | ||
|
|
||
| // The server sizes the page through emulation; this only seeds the window. | ||
| const BACKGROUND_TAB_WIDTH = 1280; | ||
| const BACKGROUND_TAB_HEIGHT = 800; | ||
|
|
||
| /** | ||
| * An agent tab's never-shown window. A panel `<webview>` 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. | ||
| */ | ||
| export function backgroundTabWindowOptions(input: { | ||
| readonly partition: string; | ||
| readonly preload: string; | ||
| readonly passkeyBridgeArgument: string | undefined; | ||
| }): BrowserWindowConstructorOptions { | ||
| return { | ||
| width: BACKGROUND_TAB_WIDTH, | ||
| height: BACKGROUND_TAB_HEIGHT, | ||
| show: false, | ||
| paintWhenInitiallyHidden: true, | ||
| skipTaskbar: true, | ||
| webPreferences: { | ||
| partition: input.partition, | ||
| preload: input.preload, | ||
| // Mirrors `will-attach-webview` for panel guests. | ||
| sandbox: true, | ||
| nodeIntegration: false, | ||
| nodeIntegrationInSubFrames: false, | ||
| contextIsolation: false, | ||
| disableHtmlFullscreenWindowResize: true, | ||
| backgroundThrottling: false, | ||
| ...(input.passkeyBridgeArgument === undefined | ||
| ? {} | ||
| : { additionalArguments: [input.passkeyBridgeArgument] }), | ||
| }, | ||
| }; | ||
| } |
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.