playwright-interactive by openai/skills
npx skills add https://github.com/openai/skills --skill playwright-interactive使用持久的 js_repl Playwright 会话来调试本地 Web 或 Electron 应用,在多次迭代中保持相同的句柄存活,并运行功能性和视觉质量保证测试,而无需重新启动整个工具链,除非进程所有权发生变化。
此技能必须启用 js_repl。
如果缺少 js_repl,请在 ~/.codex/config.toml 中启用:
[features] js_repl = true
您也可以使用 --enable js_repl 启动新会话(等同于 -c features.js_repl=true)。
启用 js_repl 后,启动一个新的 Codex 会话,以便工具列表刷新。
目前,请在不启用沙箱的情况下运行此工作流:使用 启动 Codex(或配置 的等效配置)。这是一个临时要求,因为沙箱内的 + Playwright 支持仍在完善中。
广告位招租
在这里展示您的产品或服务
触达数万 AI 开发者,精准高效
--sandbox danger-full-accesssandbox_mode=danger-full-accessjs_repl从您需要调试的同一项目目录运行设置。
将 js_repl_reset 视为恢复工具,而非常规清理。重置内核会销毁您的 Playwright 句柄。
test -f package.json || npm init -y
npm install playwright
# 仅限 Web,用于带界面的 Chromium 或移动设备模拟:
# npx playwright install chromium
# 仅限 Electron,且仅当目标工作区是应用本身时:
# npm install --save-dev electron
node -e "import('playwright').then(() => console.log('playwright import ok')).catch((error) => { console.error(error); process.exit(1); })"
如果稍后切换到不同的工作区,请在该工作区重复设置。
var chromium;
var electronLauncher;
var browser;
var context;
var page;
var mobileContext;
var mobilePage;
var electronApp;
var appWindow;
try {
({ chromium, _electron: electronLauncher } = await import("playwright"));
console.log("Playwright loaded");
} catch (error) {
throw new Error(
`Could not load playwright from the current js_repl cwd. Run the setup commands from this workspace first. Original error: ${error}`
);
}
绑定规则:
var,因为后续的 js_repl 单元格会重用它们。undefined 并重新运行该单元格,而不是到处添加恢复逻辑。page、mobilePage、appWindow),优先使用一个命名的句柄,而不是重复从上下文中重新发现页面。共享的 Web 辅助函数:
var resetWebHandles = function () {
context = undefined;
page = undefined;
mobileContext = undefined;
mobilePage = undefined;
};
var ensureWebBrowser = async function () {
if (browser && !browser.isConnected()) {
browser = undefined;
resetWebHandles();
}
browser ??= await chromium.launch({ headless: false });
return browser;
};
var reloadWebContexts = async function () {
for (const currentContext of [context, mobileContext]) {
if (!currentContext) continue;
for (const p of currentContext.pages()) {
await p.reload({ waitUntil: "domcontentloaded" });
}
}
console.log("Reloaded existing web tabs");
};
对于 Web 应用,默认使用显式视口,并将原生窗口模式视为单独的验证环节。
deviceScaleFactor,而不是直接切换到原生窗口模式。viewport: null)进行单独的带界面验证环节。noDefaultViewport 启动,因此将其视为真实的桌面窗口,并在调整任何大小之前检查启动时的大小和布局。context 重用于原生窗口环节,反之亦然;关闭旧的 page 和 context,然后为新模式创建新的。桌面和移动 Web 会话共享相同的 browser、辅助函数和质量保证流程。主要区别在于您创建的上下文和页面对。
将 TARGET_URL 设置为您正在调试的应用。对于本地服务器,优先使用 127.0.0.1 而不是 localhost。
var TARGET_URL = "http://127.0.0.1:3000";
if (page?.isClosed()) page = undefined;
await ensureWebBrowser();
context ??= await browser.newContext({
viewport: { width: 1600, height: 900 },
});
page ??= await context.newPage();
await page.goto(TARGET_URL, { waitUntil: "domcontentloaded" });
console.log("Loaded:", await page.title());
如果 context 或 page 已过时,请设置 context = page = undefined 并重新运行该单元格。
当 TARGET_URL 已存在时重用;否则直接设置移动目标。
var MOBILE_TARGET_URL = typeof TARGET_URL === "string"
? TARGET_URL
: "http://127.0.0.1:3000";
if (mobilePage?.isClosed()) mobilePage = undefined;
await ensureWebBrowser();
mobileContext ??= await browser.newContext({
viewport: { width: 390, height: 844 },
isMobile: true,
hasTouch: true,
});
mobilePage ??= await mobileContext.newPage();
await mobilePage.goto(MOBILE_TARGET_URL, { waitUntil: "domcontentloaded" });
console.log("Loaded mobile:", await mobilePage.title());
如果 mobileContext 或 mobilePage 已过时,请设置 mobileContext = mobilePage = undefined 并重新运行该单元格。
var TARGET_URL = "http://127.0.0.1:3000";
await ensureWebBrowser();
await page?.close().catch(() => {});
await context?.close().catch(() => {});
page = undefined;
context = undefined;
browser ??= await chromium.launch({ headless: false });
context = await browser.newContext({ viewport: null });
page = await context.newPage();
await page.goto(TARGET_URL, { waitUntil: "domcontentloaded" });
console.log("Loaded native window:", await page.title());
当当前工作区是 Electron 应用且 package.json 的 main 字段指向正确的入口文件时,将 ELECTRON_ENTRY 设置为 .。如果您需要直接定位特定的主进程文件,请使用路径,例如 ./main.js。
var ELECTRON_ENTRY = ".";
if (appWindow?.isClosed()) appWindow = undefined;
if (!appWindow && electronApp) {
await electronApp.close().catch(() => {});
electronApp = undefined;
}
electronApp ??= await electronLauncher.launch({
args: [ELECTRON_ENTRY],
});
appWindow ??= await electronApp.firstWindow();
console.log("Loaded Electron window:", await appWindow.title());
如果 js_repl 尚未从 Electron 应用工作区运行,则在启动时显式传递 cwd。
如果应用进程看起来已过时,请设置 electronApp = appWindow = undefined 并重新运行该单元格。
如果您已经有一个 Electron 会话,但在主进程、预加载或启动更改后需要一个新的进程,请使用下一节中的重启单元格,而不是重新运行此单元格。
尽可能保持同一会话存活。
Web 渲染器重新加载:
await reloadWebContexts();
仅 Electron 渲染器重新加载:
await appWindow.reload({ waitUntil: "domcontentloaded" });
console.log("Reloaded Electron window");
主进程、预加载或启动更改后的 Electron 重启:
await electronApp.close().catch(() => {});
electronApp = undefined;
appWindow = undefined;
electronApp = await electronLauncher.launch({
args: [ELECTRON_ENTRY],
});
appWindow = await electronApp.firstWindow();
console.log("Relaunched Electron window:", await appWindow.title());
如果您的启动需要显式的 cwd,请在此处包含相同的 cwd。
默认姿态:
js_repl 单元格简短并专注于一次交互爆发。browser、context、page、electronApp、appWindow),而不是重新声明它们。electronApp.evaluate(...) 用于主进程检查或专门构建的诊断。js_repl 一次,然后在迭代过程中保持相同的 Playwright 句柄存活。page.evaluate(...) 和 electronApp.evaluate(...) 可以检查或准备状态,但它们不能作为最终确认的输入。如果您计划通过 codex.emitImage(...) 发出截图,默认情况下请使用下一节中的 CSS 标准化路径。这些是用于将被模型解释或用于基于坐标的后续操作的截图的规范示例。仅将原始捕获作为保真度敏感调试的例外情况;原始例外示例出现在标准化指南之后。
如果您将通过 codex.emitImage(...) 发出截图以供模型解释,请在发出之前将其标准化为您捕获的确切区域的 CSS 像素。这可以确保如果回复稍后用于点击,返回的坐标与 Playwright CSS 像素对齐,并且还可以减少图像负载大小和模型令牌成本。
默认情况下不要发出原始的原生窗口截图。仅当您明确需要设备像素保真度时才跳过标准化,例如 Retina 或 DPI 伪影调试、像素级渲染检查,或其他原始像素比负载大小更重要的保真度敏感情况。对于不会发送给模型的仅本地检查,原始捕获是可以的。
不要假设在原生窗口模式(viewport: null)下 page.screenshot({ scale: "css" }) 就足够了。在 macOS Retina 显示器上的 Chromium 中,带界面的原生窗口截图即使请求了 scale: "css",仍可能以设备像素大小返回。同样的注意事项适用于通过 Playwright 启动的 Electron 窗口,因为 Electron 以 noDefaultViewport 运行,并且 appWindow.screenshot({ scale: "css" }) 可能仍返回设备像素输出。
为网页和 Electron 窗口使用单独的标准化路径:
page.screenshot({ scale: "css" })。如果原生窗口 Chromium 仍返回设备像素输出,请使用当前页面内的 canvas 调整大小;不需要临时页面。appWindow.context().newPage() 或 electronApp.context().newPage() 作为临时页面。Electron 上下文不能可靠地支持该路径。使用 BrowserWindow.capturePage(...) 在主进程中捕获,使用 nativeImage.resize(...) 调整大小,并直接发出这些字节。共享辅助函数和约定:
var emitJpeg = async function (bytes) {
await codex.emitImage({
bytes,
mimeType: "image/jpeg",
detail: "original",
});
};
var emitWebJpeg = async function (surface, options = {}) {
await emitJpeg(await surface.screenshot({
type: "jpeg",
quality: 85,
scale: "css",
...options,
}));
};
var clickCssPoint = async function ({ surface, x, y, clip }) {
await surface.mouse.click(
clip ? clip.x + x : x,
clip ? clip.y + y : y
);
};
var tapCssPoint = async function ({ page, x, y, clip }) {
await page.touchscreen.tap(
clip ? clip.x + x : x,
clip ? clip.y + y : y
);
};
page 或 mobilePage,对于 Electron 使用 appWindow 作为 surface。clip 视为渲染器中 getBoundingClientRect() 的 CSS 像素。quality: 85 的 JPEG。{ x, y }。显式视口上下文的优选 Web 路径,通常也适用于 Web 一般情况:
await emitWebJpeg(page);
移动 Web 使用相同路径;将 page 替换为 mobilePage:
await emitWebJpeg(mobilePage);
如果模型返回 { x, y },直接点击它:
await clickCssPoint({ surface: page, x, y });
移动 Web 点击路径:
await tapCssPoint({ page: mobilePage, x, y });
对于此正常路径中的 Web clip 截图或元素截图,scale: "css" 通常直接有效。点击时添加区域原点。
await emitWebJpeg(page, { clip })await emitWebJpeg(mobilePage, { clip })await clickCssPoint({ surface: page, clip, x, y })await tapCssPoint({ page: mobilePage, clip, x, y })await clickCssPoint({ surface: page, clip: box, x, y }) 在 const box = await locator.boundingBox() 之后当 scale: "css" 仍返回设备像素大小时的 Web 原生窗口回退方案:
var emitWebScreenshotCssScaled = async function ({ page, clip, quality = 0.85 } = {}) {
var NodeBuffer = (await import("node:buffer")).Buffer;
const target = clip
? { width: clip.width, height: clip.height }
: await page.evaluate(() => ({
width: window.innerWidth,
height: window.innerHeight,
}));
const screenshotBuffer = await page.screenshot({
type: "png",
...(clip ? { clip } : {}),
});
const bytes = await page.evaluate(
async ({ imageBase64, targetWidth, targetHeight, quality }) => {
const image = new Image();
image.src = `data:image/png;base64,${imageBase64}`;
await image.decode();
const canvas = document.createElement("canvas");
canvas.width = targetWidth;
canvas.height = targetHeight;
const ctx = canvas.getContext("2d");
ctx.imageSmoothingEnabled = true;
ctx.drawImage(image, 0, 0, targetWidth, targetHeight);
const blob = await new Promise((resolve) =>
canvas.toBlob(resolve, "image/jpeg", quality)
);
return new Uint8Array(await blob.arrayBuffer());
},
{
imageBase64: NodeBuffer.from(screenshotBuffer).toString("base64"),
targetWidth: target.width,
targetHeight: target.height,
quality,
}
);
await emitJpeg(bytes);
};
对于完整的视口回退捕获,将返回的 { x, y } 视为直接的 CSS 坐标:
await emitWebScreenshotCssScaled({ page });
await clickCssPoint({ surface: page, x, y });
对于裁剪的回退捕获,添加裁剪原点:
await emitWebScreenshotCssScaled({ page, clip });
await clickCssPoint({ surface: page, clip, x, y });
对于 Electron,在主进程中标准化,而不是打开一个临时的 Playwright 页面。下面的辅助函数返回完整内容区域或裁剪的 CSS 像素区域的 CSS 缩放字节。将 clip 视为内容区域的 CSS 像素,例如从渲染器中的 getBoundingClientRect() 获取的值。
var emitElectronScreenshotCssScaled = async function ({ electronApp, clip, quality = 85 } = {}) {
const bytes = await electronApp.evaluate(async ({ BrowserWindow }, { clip, quality }) => {
const win = BrowserWindow.getAllWindows()[0];
const image = clip ? await win.capturePage(clip) : await win.capturePage();
const target = clip
? { width: clip.width, height: clip.height }
: (() => {
const [width, height] = win.getContentSize();
return { width, height };
})();
const resized = image.resize({
width: target.width,
height: target.height,
quality: "best",
});
return resized.toJPEG(quality);
}, { clip, quality });
await emitJpeg(bytes);
};
完整的 Electron 窗口:
await emitElectronScreenshotCssScaled({ electronApp });
await clickCssPoint({ surface: appWindow, x, y });
使用来自渲染器的 CSS 像素裁剪 Electron 区域:
var clip = await appWindow.evaluate(() => {
const rect = document.getElementById("board").getBoundingClientRect();
return {
x: Math.round(rect.x),
y: Math.round(rect.y),
width: Math.round(rect.width),
height: Math.round(rect.height),
};
});
await emitElectronScreenshotCssScaled({ electronApp, clip });
await clickCssPoint({ surface: appWindow, clip, x, y });
仅当原始像素比 CSS 坐标对齐更重要时才使用这些,例如 Retina 或 DPI 伪影调试、像素级渲染检查或其他保真度敏感的审查。
Web 桌面原始发出:
await codex.emitImage({
bytes: await page.screenshot({ type: "jpeg", quality: 85 }),
mimeType: "image/jpeg",
detail: "original",
});
Electron 原始发出:
await codex.emitImage({
bytes: await appWindow.screenshot({ type: "jpeg", quality: 85 }),
mimeType: "image/jpeg",
detail: "original",
});
移动 Web 上下文已在运行后的移动原始发出:
await codex.emitImage({
bytes: await mobilePage.screenshot({ type: "jpeg", quality: 85 }),
mimeType: "image/jpeg",
detail: "original",
});
不要仅仅因为主部件可见就假设截图是可接受的。在最终确认前,明确验证预期的初始视图是否符合产品要求,同时使用截图审查和数值检查。
Web 或渲染器检查:
console.log(await page.evaluate(() => ({
innerWidth: window.innerWidth,
innerHeight: window.innerHeight,
clientWidth: document.documentElement.clientWidth,
clientHeight: document.documentElement.clientHeight,
scrollWidth: document.documentElement.scrollWidth,
scrollHeight: document.documentElement.scrollHeight,
canScrollX: document.documentElement.scrollWidth > document.documentElement.clientWidth,
canScrollY: document.documentElement.scrollHeight > document.documentElement.clientHeight,
})));
Electron 检查:
console.log(await appWindow.evaluate(() => ({
innerWidth: window.innerWidth,
innerHeight: window.innerHeight,
clientWidth: document.documentElement.clientWidth,
clientHeight: document.documentElement.clientHeight,
scrollWidth: document.documentElement.scrollWidth,
scrollHeight: document.documentElement.scrollHeight,
canScrollX: document.documentElement.scrollWidth > document.documentElement.clientWidth,
canScrollY: document.documentElement.scrollHeight > document.documentElement.clientHeight,
})));
当裁剪是现实的失败模式时,使用 getBoundingClientRect() 检查来补充特定用户界面中必需可见区域的数值检查;仅文档级别的指标不足以应对固定外壳。
对于本地 Web 调试,请将应用保持在持久的 TTY 会话中运行。不要依赖来自短暂 shell 的一次性后台命令。
使用项目的正常启动命令,例如:
npm start
在 page.goto(...) 之前,验证所选端口正在监听且应用有响应。
对于 Electron 调试,通过 _electron.launch(...) 从 js_repl 启动应用,以便同一会话拥有该进程。如果 Electron 渲染器依赖于单独的开发服务器(例如 Vite 或 Next),请将该服务器保持在持久的 TTY 会话中运行,然后从 js_repl 重新启动或重新加载 Electron 应用。
仅在任务实际完成时运行清理:
此清理是手动的。退出 Codex、关闭终端或丢失 js_repl 会话不会隐式运行 electronApp.close()、context.close() 或 browser.close()。
特别是对于 Electron,假设如果您在没有先执行清理单元格的情况下离开会话,应用可能会继续运行。
if (electronApp) { await electronApp.close().catch(() => {}); }
if (mobileContext) { await mobileContext.close().catch(() => {}); }
if (context) { await context.close().catch(() => {}); }
if (browser) { await browser.close().catch(() => {}); }
browser = undefined; context = undefined; page = undefined; mobileContext = undefined; mobilePage = undefined; electronApp = undefined; appWindow = undefined;
console.log("Playwright session closed");
如果您计划在调试后立即退出 Codex,请先运行清理单元格,并在退出前等待 "Playwright session closed" 日志。
Cannot find module 'playwright':在当前工作区运行一次性设置,并在使用 js_repl 前验证导入。npx playwright install chromium。page.goto: net::ERR_CONNECTION_REFUSED:确保开发服务器仍在持久的 TTY 会话中运行,重新检查端口,并优先使用 http://127.0.0.1:<port>。electron.launch 挂起、超时或立即退出:验证本地 electron 依赖项,确认 args 目标,并确保任何渲染器开发服务器在启动前已在运行。Identifier has already been declared:重用现有的顶级绑定,选择新名称,或将代码包装在 { ... } 中。仅在内核真正卡住时才使用 js_repl_reset。browserContext.newPage: Protocol error (Target.createTarget): Not supported:不要使用 appWindow.context().newPage() 或 electronApp.context().newPage() 作为临时页面;使用模型绑定截图部分中的 Electron 特定截图标准化流程。js_repl 超时或重置:重新运行引导单元格,并使用更短、更专注的单元格重新创建会话。--sandbox danger-full-access 启动的,如果需要,请以这种方式重新启动。每周安装数
775
代码仓库
GitHub 星标数
15.3K
首次出现
2026年3月5日
安全审计
已安装于
codex756
cursor412
opencode412
gemini-cli410
github-copilot410
kimi-cli408
Use a persistent js_repl Playwright session to debug local web or Electron apps, keep the same handles alive across iterations, and run functional plus visual QA without restarting the whole toolchain unless the process ownership changed.
js_repl must be enabled for this skill.
If js_repl is missing, enable it in ~/.codex/config.toml:
[features] js_repl = true
You can also start a new session with --enable js_repl (equivalent to -c features.js_repl=true).
After enabling js_repl, start a new Codex session so the tool list refreshes.
For now, run this workflow with sandboxing disabled: start Codex with --sandbox danger-full-access (or the equivalent config for sandbox_mode=danger-full-access). This is a temporary requirement while js_repl + Playwright support inside the sandbox is still being completed.
Run setup from the same project directory you need to debug.
Treat js_repl_reset as a recovery tool, not routine cleanup. Resetting the kernel destroys your Playwright handles.
test -f package.json || npm init -y
npm install playwright
# Web-only, for headed Chromium or mobile emulation:
# npx playwright install chromium
# Electron-only, and only if the target workspace is the app itself:
# npm install --save-dev electron
node -e "import('playwright').then(() => console.log('playwright import ok')).catch((error) => { console.error(error); process.exit(1); })"
If you switch to a different workspace later, repeat setup there.
var chromium;
var electronLauncher;
var browser;
var context;
var page;
var mobileContext;
var mobilePage;
var electronApp;
var appWindow;
try {
({ chromium, _electron: electronLauncher } = await import("playwright"));
console.log("Playwright loaded");
} catch (error) {
throw new Error(
`Could not load playwright from the current js_repl cwd. Run the setup commands from this workspace first. Original error: ${error}`
);
}
Binding rules:
var for the shared top-level Playwright handles because later js_repl cells reuse them.undefined and rerun the cell instead of adding recovery logic everywhere.page, mobilePage, appWindow) over repeatedly rediscovering pages from the context.Shared web helpers:
var resetWebHandles = function () {
context = undefined;
page = undefined;
mobileContext = undefined;
mobilePage = undefined;
};
var ensureWebBrowser = async function () {
if (browser && !browser.isConnected()) {
browser = undefined;
resetWebHandles();
}
browser ??= await chromium.launch({ headless: false });
return browser;
};
var reloadWebContexts = async function () {
for (const currentContext of [context, mobileContext]) {
if (!currentContext) continue;
for (const p of currentContext.pages()) {
await p.reload({ waitUntil: "domcontentloaded" });
}
}
console.log("Reloaded existing web tabs");
};
For web apps, use an explicit viewport by default and treat native-window mode as a separate validation pass.
deviceScaleFactor rather than switching straight to native-window mode.viewport: null) for a separate headed pass when you need to validate launched window size, OS-level DPI behavior, browser chrome interactions, or bugs that may depend on the host display configuration.noDefaultViewport, so treat it like a real desktop window and check the as-launched size and layout before resizing anything.context for a native-window pass or vice versa; close the old page and context, then create a new one for the new mode.Desktop and mobile web sessions share the same browser, helpers, and QA flow. The main difference is which context and page pair you create.
Set TARGET_URL to the app you are debugging. For local servers, prefer 127.0.0.1 over localhost.
var TARGET_URL = "http://127.0.0.1:3000";
if (page?.isClosed()) page = undefined;
await ensureWebBrowser();
context ??= await browser.newContext({
viewport: { width: 1600, height: 900 },
});
page ??= await context.newPage();
await page.goto(TARGET_URL, { waitUntil: "domcontentloaded" });
console.log("Loaded:", await page.title());
If context or page is stale, set context = page = undefined and rerun the cell.
Reuse TARGET_URL when it already exists; otherwise set a mobile target directly.
var MOBILE_TARGET_URL = typeof TARGET_URL === "string"
? TARGET_URL
: "http://127.0.0.1:3000";
if (mobilePage?.isClosed()) mobilePage = undefined;
await ensureWebBrowser();
mobileContext ??= await browser.newContext({
viewport: { width: 390, height: 844 },
isMobile: true,
hasTouch: true,
});
mobilePage ??= await mobileContext.newPage();
await mobilePage.goto(MOBILE_TARGET_URL, { waitUntil: "domcontentloaded" });
console.log("Loaded mobile:", await mobilePage.title());
If mobileContext or mobilePage is stale, set mobileContext = mobilePage = undefined and rerun the cell.
var TARGET_URL = "http://127.0.0.1:3000";
await ensureWebBrowser();
await page?.close().catch(() => {});
await context?.close().catch(() => {});
page = undefined;
context = undefined;
browser ??= await chromium.launch({ headless: false });
context = await browser.newContext({ viewport: null });
page = await context.newPage();
await page.goto(TARGET_URL, { waitUntil: "domcontentloaded" });
console.log("Loaded native window:", await page.title());
Set ELECTRON_ENTRY to . when the current workspace is the Electron app and package.json points main to the right entry file. If you need to target a specific main-process file directly, use a path such as ./main.js instead.
var ELECTRON_ENTRY = ".";
if (appWindow?.isClosed()) appWindow = undefined;
if (!appWindow && electronApp) {
await electronApp.close().catch(() => {});
electronApp = undefined;
}
electronApp ??= await electronLauncher.launch({
args: [ELECTRON_ENTRY],
});
appWindow ??= await electronApp.firstWindow();
console.log("Loaded Electron window:", await appWindow.title());
If js_repl is not already running from the Electron app workspace, pass cwd explicitly when launching.
If the app process looks stale, set electronApp = appWindow = undefined and rerun the cell.
If you already have an Electron session but need a fresh process after a main-process, preload, or startup change, use the restart cell in the next section instead of rerunning this one.
Keep the same session alive whenever you can.
Web renderer reload:
await reloadWebContexts();
Electron renderer-only reload:
await appWindow.reload({ waitUntil: "domcontentloaded" });
console.log("Reloaded Electron window");
Electron restart after main-process, preload, or startup changes:
await electronApp.close().catch(() => {});
electronApp = undefined;
appWindow = undefined;
electronApp = await electronLauncher.launch({
args: [ELECTRON_ENTRY],
});
appWindow = await electronApp.firstWindow();
console.log("Relaunched Electron window:", await appWindow.title());
If your launch requires an explicit cwd, include the same cwd here.
Default posture:
js_repl cell short and focused on one interaction burst.browser, context, page, electronApp, appWindow) instead of redeclaring them.electronApp.evaluate(...) only for main-process inspection or purpose-built diagnostics.js_repl once, then keep the same Playwright handles alive across iterations.page.evaluate(...) and electronApp.evaluate(...) may inspect or stage state, but they do not count as signoff input.If you plan to emit a screenshot through codex.emitImage(...), use the CSS-normalized paths in the next section by default. Those are the canonical examples for screenshots that will be interpreted by the model or used for coordinate-based follow-up actions. Keep raw captures as an exception for fidelity-sensitive debugging only; the raw exception examples appear after the normalization guidance.
If you will emit a screenshot with codex.emitImage(...) for model interpretation, normalize it to CSS pixels for the exact region you captured before emitting. This keeps returned coordinates aligned with Playwright CSS pixels if the reply is later used for clicking, and it also reduces image payload size and model token cost.
Do not emit raw native-window screenshots by default. Skip normalization only when you explicitly need device-pixel fidelity, such as Retina or DPI artifact debugging, pixel-accurate rendering inspection, or another fidelity-sensitive case where raw pixels matter more than payload size. For local-only inspection that will not be emitted to the model, raw capture is fine.
Do not assume page.screenshot({ scale: "css" }) is enough in native-window mode (viewport: null). In Chromium on macOS Retina displays, headed native-window screenshots can still come back at device-pixel size even when scale: "css" is requested. The same caveat applies to Electron windows launched through Playwright because Electron runs with noDefaultViewport, and appWindow.screenshot({ scale: "css" }) may still return device-pixel output.
Use separate normalization paths for web pages and Electron windows:
page.screenshot({ scale: "css" }) directly. If native-window Chromium still returns device-pixel output, resize inside the current page with canvas; no scratch page is required.appWindow.context().newPage() or electronApp.context().newPage() as a scratch page. Electron contexts do not support that path reliably. Capture in the main process with BrowserWindow.capturePage(...), resize with nativeImage.resize(...), and emit those bytes directly.Shared helpers and conventions:
var emitJpeg = async function (bytes) {
await codex.emitImage({
bytes,
mimeType: "image/jpeg",
detail: "original",
});
};
var emitWebJpeg = async function (surface, options = {}) {
await emitJpeg(await surface.screenshot({
type: "jpeg",
quality: 85,
scale: "css",
...options,
}));
};
var clickCssPoint = async function ({ surface, x, y, clip }) {
await surface.mouse.click(
clip ? clip.x + x : x,
clip ? clip.y + y : y
);
};
var tapCssPoint = async function ({ page, x, y, clip }) {
await page.touchscreen.tap(
clip ? clip.x + x : x,
clip ? clip.y + y : y
);
};
page or mobilePage for web, or appWindow for Electron, as the surface.clip as CSS pixels from getBoundingClientRect() in the renderer.quality: 85 unless lossless fidelity is specifically required.{ x, y } directly.Preferred web path for explicit-viewport contexts, and often for web in general:
await emitWebJpeg(page);
Mobile web uses the same path; substitute mobilePage for page:
await emitWebJpeg(mobilePage);
If the model returns { x, y }, click it directly:
await clickCssPoint({ surface: page, x, y });
Mobile web click path:
await tapCssPoint({ page: mobilePage, x, y });
For web clip screenshots or element screenshots in this normal path, scale: "css" usually works directly. Add the region origin back when clicking.
await emitWebJpeg(page, { clip })await emitWebJpeg(mobilePage, { clip })await clickCssPoint({ surface: page, clip, x, y })await tapCssPoint({ page: mobilePage, clip, x, y })await clickCssPoint({ surface: page, clip: box, x, y }) after const box = await locator.boundingBox()Web native-window fallback when scale: "css" still comes back at device-pixel size:
var emitWebScreenshotCssScaled = async function ({ page, clip, quality = 0.85 } = {}) {
var NodeBuffer = (await import("node:buffer")).Buffer;
const target = clip
? { width: clip.width, height: clip.height }
: await page.evaluate(() => ({
width: window.innerWidth,
height: window.innerHeight,
}));
const screenshotBuffer = await page.screenshot({
type: "png",
...(clip ? { clip } : {}),
});
const bytes = await page.evaluate(
async ({ imageBase64, targetWidth, targetHeight, quality }) => {
const image = new Image();
image.src = `data:image/png;base64,${imageBase64}`;
await image.decode();
const canvas = document.createElement("canvas");
canvas.width = targetWidth;
canvas.height = targetHeight;
const ctx = canvas.getContext("2d");
ctx.imageSmoothingEnabled = true;
ctx.drawImage(image, 0, 0, targetWidth, targetHeight);
const blob = await new Promise((resolve) =>
canvas.toBlob(resolve, "image/jpeg", quality)
);
return new Uint8Array(await blob.arrayBuffer());
},
{
imageBase64: NodeBuffer.from(screenshotBuffer).toString("base64"),
targetWidth: target.width,
targetHeight: target.height,
quality,
}
);
await emitJpeg(bytes);
};
For a full viewport fallback capture, treat returned { x, y } as direct CSS coordinates:
await emitWebScreenshotCssScaled({ page });
await clickCssPoint({ surface: page, x, y });
For a clipped fallback capture, add the clip origin back:
await emitWebScreenshotCssScaled({ page, clip });
await clickCssPoint({ surface: page, clip, x, y });
For Electron, normalize in the main process instead of opening a scratch Playwright page. The helper below returns CSS-scaled bytes for the full content area or for a clipped CSS-pixel region. Treat clip as content-area CSS pixels, for example values taken from getBoundingClientRect() in the renderer.
var emitElectronScreenshotCssScaled = async function ({ electronApp, clip, quality = 85 } = {}) {
const bytes = await electronApp.evaluate(async ({ BrowserWindow }, { clip, quality }) => {
const win = BrowserWindow.getAllWindows()[0];
const image = clip ? await win.capturePage(clip) : await win.capturePage();
const target = clip
? { width: clip.width, height: clip.height }
: (() => {
const [width, height] = win.getContentSize();
return { width, height };
})();
const resized = image.resize({
width: target.width,
height: target.height,
quality: "best",
});
return resized.toJPEG(quality);
}, { clip, quality });
await emitJpeg(bytes);
};
Full Electron window:
await emitElectronScreenshotCssScaled({ electronApp });
await clickCssPoint({ surface: appWindow, x, y });
Clipped Electron region using CSS pixels from the renderer:
var clip = await appWindow.evaluate(() => {
const rect = document.getElementById("board").getBoundingClientRect();
return {
x: Math.round(rect.x),
y: Math.round(rect.y),
width: Math.round(rect.width),
height: Math.round(rect.height),
};
});
await emitElectronScreenshotCssScaled({ electronApp, clip });
await clickCssPoint({ surface: appWindow, clip, x, y });
Use these only when raw pixels matter more than CSS-coordinate alignment, such as Retina or DPI artifact debugging, pixel-accurate rendering inspection, or other fidelity-sensitive review.
Web desktop raw emit:
await codex.emitImage({
bytes: await page.screenshot({ type: "jpeg", quality: 85 }),
mimeType: "image/jpeg",
detail: "original",
});
Electron raw emit:
await codex.emitImage({
bytes: await appWindow.screenshot({ type: "jpeg", quality: 85 }),
mimeType: "image/jpeg",
detail: "original",
});
Mobile raw emit after the mobile web context is already running:
await codex.emitImage({
bytes: await mobilePage.screenshot({ type: "jpeg", quality: 85 }),
mimeType: "image/jpeg",
detail: "original",
});
Do not assume a screenshot is acceptable just because the main widget is visible. Before signoff, explicitly verify that the intended initial view matches the product requirement, using both screenshot review and numeric checks.
Web or renderer check:
console.log(await page.evaluate(() => ({
innerWidth: window.innerWidth,
innerHeight: window.innerHeight,
clientWidth: document.documentElement.clientWidth,
clientHeight: document.documentElement.clientHeight,
scrollWidth: document.documentElement.scrollWidth,
scrollHeight: document.documentElement.scrollHeight,
canScrollX: document.documentElement.scrollWidth > document.documentElement.clientWidth,
canScrollY: document.documentElement.scrollHeight > document.documentElement.clientHeight,
})));
Electron check:
console.log(await appWindow.evaluate(() => ({
innerWidth: window.innerWidth,
innerHeight: window.innerHeight,
clientWidth: document.documentElement.clientWidth,
clientHeight: document.documentElement.clientHeight,
scrollWidth: document.documentElement.scrollWidth,
scrollHeight: document.documentElement.scrollHeight,
canScrollX: document.documentElement.scrollWidth > document.documentElement.clientWidth,
canScrollY: document.documentElement.scrollHeight > document.documentElement.clientHeight,
})));
Augment the numeric check with getBoundingClientRect() checks for the required visible regions in your specific UI when clipping is a realistic failure mode; document-level metrics alone are not sufficient for fixed shells.
For local web debugging, keep the app running in a persistent TTY session. Do not rely on one-shot background commands from a short-lived shell.
Use the project's normal start command, for example:
npm start
Before page.goto(...), verify the chosen port is listening and the app responds.
For Electron debugging, launch the app from js_repl through _electron.launch(...) so the same session owns the process. If the Electron renderer depends on a separate dev server (for example Vite or Next), keep that server running in a persistent TTY session and then relaunch or reload the Electron app from js_repl.
Only run cleanup when the task is actually finished:
This cleanup is manual. Exiting Codex, closing the terminal, or losing the js_repl session does not implicitly run electronApp.close(), context.close(), or browser.close().
For Electron specifically, assume the app may keep running if you leave the session without executing the cleanup cell first.
if (electronApp) { await electronApp.close().catch(() => {}); }
if (mobileContext) { await mobileContext.close().catch(() => {}); }
if (context) { await context.close().catch(() => {}); }
if (browser) { await browser.close().catch(() => {}); }
browser = undefined; context = undefined; page = undefined; mobileContext = undefined; mobilePage = undefined; electronApp = undefined; appWindow = undefined;
console.log("Playwright session closed");
If you plan to exit Codex immediately after debugging, run the cleanup cell first and wait for the "Playwright session closed" log before quitting.
Cannot find module 'playwright': run the one-time setup in the current workspace and verify the import before using js_repl.npx playwright install chromium.page.goto: net::ERR_CONNECTION_REFUSED: make sure the dev server is still running in a persistent TTY session, recheck the port, and prefer http://127.0.0.1:<port>.electron.launch hangs, times out, or exits immediately: verify the local electron dependency, confirm the args target, and make sure any renderer dev server is already running before launch.Identifier has already been declared: reuse the existing top-level bindings, choose a new name, or wrap the code in . Use only when the kernel is genuinely stuck.Weekly Installs
775
Repository
GitHub Stars
15.3K
First Seen
Mar 5, 2026
Security Audits
Gen Agent Trust HubPassSocketPassSnykWarn
Installed on
codex756
cursor412
opencode412
gemini-cli410
github-copilot410
kimi-cli408
agent-browser 浏览器自动化工具 - Vercel Labs 命令行网页操作与测试
136,300 周安装
Trigger.dev 实时功能:从前端/后端实时订阅任务运行,流式传输数据
734 周安装
PPTX 文件处理全攻略:Python 脚本创建、编辑、分析 .pptx 文件内容与结构
735 周安装
Dokie AI PPT:AI驱动的专业演示文稿设计工具,支持HTML创意动效
737 周安装
PRD生成器:AI驱动产品需求文档工具,快速创建清晰可执行PRD
737 周安装
Devcontainer 设置技能:一键创建预配置开发容器,集成 Claude Code 和语言工具
739 周安装
Plankton代码质量工具:Claude Code自动格式化与Linter强制执行系统
741 周安装
{ ... }js_repl_resetbrowserContext.newPage: Protocol error (Target.createTarget): Not supported while working with Electron: do not use appWindow.context().newPage() or electronApp.context().newPage() as a scratch page; use the Electron-specific screenshot normalization flow in the model-bound screenshots section.js_repl timed out or reset: rerun the bootstrap cell and recreate the session with shorter, more focused cells.--sandbox danger-full-access and restart that way if needed.