with one click
take-screenshots
// Capture screenshots of the Exo Electron app workflows using Playwright in demo mode. Use when the user asks for screenshots, workflow documentation, or visual captures of the app.
// Capture screenshots of the Exo Electron app workflows using Playwright in demo mode. Use when the user asks for screenshots, workflow documentation, or visual captures of the app.
Test the Electron app interactively using Chrome DevTools Protocol. Use when the user asks to test, verify, or interact with the running app via browser automation.
Clear extension caches, analysis data, and drafts from the database. Use when testing sender lookups, analysis, or draft generation from a clean state.
Run GitHub Actions CI workflows locally using nektos/act in Docker. Use when testing CI before pushing or debugging workflow failures.
Pre-commit code review for production-critical issues. Use when reviewing staged changes, before committing, or when asked to review code for bugs and consistency issues.
Iteratively improves a PR until all review bots (Greptile, Devin, and others) are satisfied with zero unresolved comments, then fixes any CI failures. Triggers reviews, fixes all actionable comments, pushes, re-triggers, and repeats. Use when the user wants to fully optimize a PR against all automated code review feedback.
| name | take-screenshots |
| description | Capture screenshots of the Exo Electron app workflows using Playwright in demo mode. Use when the user asks for screenshots, workflow documentation, or visual captures of the app. |
| disable-model-invocation | true |
Captures screenshots of the Exo Electron app by running Playwright tests in demo mode.
App must be built first — screenshots run against the compiled output:
node_modules/.bin/electron-vite build
Virtual display required (headless Linux) — the app needs Xvfb since Electron requires a display server:
# Xvfb is already installed at /usr/bin/xvfb-run
EXO_DEMO_MODE=true (no real Gmail API calls)capturePage() API for screenshots (not Playwright's page.screenshot(), which hangs in headless Electron)./screenshots/out/main/index.js (the built output)xvfb-run --auto-servernum --server-args="-screen 0 1920x1080x24" \
npx playwright test tests/screenshots/take-screenshots.spec.ts --timeout 120000
Screenshots are saved to ./screenshots/ with numbered filenames (e.g., 01-inbox-view.png).
Create new .spec.ts files in tests/screenshots/. Use this template:
import { test, _electron as electron, Page, ElectronApplication } from "@playwright/test";
import path from "path";
import fs from "fs";
import { fileURLToPath } from "url";
const __dirname = path.dirname(fileURLToPath(import.meta.url));
const SCREENSHOT_DIR = path.join(__dirname, "../../screenshots");
fs.mkdirSync(SCREENSHOT_DIR, { recursive: true });
let electronApp: ElectronApplication;
let page: Page;
async function launchApp(): Promise<{ app: ElectronApplication; page: Page }> {
const app = await electron.launch({
args: [
path.join(__dirname, "../../out/main/index.js"),
"--disable-gpu",
"--disable-software-rasterizer",
],
env: {
...process.env,
NODE_ENV: "test",
EXO_DEMO_MODE: "true",
ELECTRON_DISABLE_GPU: "1",
},
});
const window = await app.firstWindow();
await window.waitForLoadState("domcontentloaded");
await window.waitForSelector("text=Exo", { timeout: 30000 });
await window.waitForTimeout(2000);
return { app, page: window };
}
// IMPORTANT: Use Electron's native capturePage(), NOT page.screenshot()
async function screenshot(name: string) {
const filepath = path.join(SCREENSHOT_DIR, `${name}.png`);
const imageBuffer = await electronApp.evaluate(async ({ BrowserWindow }) => {
const win = BrowserWindow.getAllWindows()[0];
const image = await win.webContents.capturePage();
return image.toPNG().toString("base64");
});
fs.writeFileSync(filepath, Buffer.from(imageBuffer, "base64"));
}
test.describe("My Workflow", () => {
test.setTimeout(120000);
test.beforeAll(async () => {
const result = await launchApp();
electronApp = result.app;
page = result.page;
});
test.afterAll(async () => {
if (electronApp) await electronApp.close();
});
test("capture screenshots", async () => {
await screenshot("01-step-name");
// ... interact with the app, then take more screenshots
});
});
These selectors are useful for navigating the app during screenshot capture:
| Element | Selector |
|---|---|
| Compose button | button:has-text('Compose') |
| New Message header | text=New Message |
| To field | [data-testid='address-input-to'] input[type='text'] |
| Subject field | input[placeholder='Subject'] |
| Rich text editor | .ProseMirror, [contenteditable='true'] |
| Send button | button filtered by hasText: /^Send/ |
| Bold button | button[title='Bold (Cmd+B)'] |
| Discard button | button:has-text('Discard') |
| Reply All button | button[title='Reply All'] |
| Forward button | button[title='Forward (F)'] |
| Settings button | settings gear icon in top bar |
| Email list items | button elements in the left sidebar |
| Address chips | [data-testid='address-chip'] |
page.screenshot() — it hangs indefinitely in headless Electron. Always use the electronApp.evaluate + capturePage() pattern shown above.xvfb-run on headless Linux. Without a display server, Electron exits with "Missing X server or $DISPLAY".out/main/index.js, not the dev server.EXO_DEMO_MODE=true means no real Gmail API calls, no real emails sent. All data is mock.--disable-gpu and --disable-software-rasterizer args to Electron launch for headless compatibility.waitForTimeout() after interactions to let animations/transitions complete before capturing.