Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
29 changes: 29 additions & 0 deletions skill-src/webcmd-browser/SKILL.src.md
Original file line number Diff line number Diff line change
Expand Up @@ -242,6 +242,27 @@ Use `snapshot --snapshot-mode read` first. If the page is an app shell or needs

Use `run` and inspect `page.frames()`; target the frame by URL/name and keep iframe actions in the same program. If Chrome cannot expose the frame, bind or navigate directly to the iframe URL when safe.

### Open a new tab

When a control should open another page (`target=_blank`, `window.open`, a download-in-tab):

1. In the same `run`, arm `page.waitForEvent('popup')` with a short timeout **before** the click.
2. After the click, inspect `context.pages()` or `webcmd --session <session-id> browser tabs`. A new page is the success path — bind to it and read there.
3. If no popup arrives (blocker, `noopener`, `data:`/`blob:`/`srcdoc` origin, timeout): do **not** `page.goto` the target URL on the current bound page. That replaces the opener (iframes, form state, and the original document are gone). Call `context.newPage()` and navigate the new page if a destination URL is already known (`href`, the task URL).
4. Do not wait out the default `browser run` timeout on a popup that has not fired.

```js
const popupPromise = page.waitForEvent('popup', { timeout: 5000 }).catch(() => null);
await page.getByRole('link', { name: /open/i }).click();
const popup = await popupPromise;
if (popup) {
return { url: popup.url(), title: await popup.title() };
}
const tab = await context.newPage();
await tab.goto(knownUrl);
return { url: tab.url(), title: await tab.title() };
```

---

## Pitfalls
Expand All @@ -251,6 +272,7 @@ Use `run` and inspect `page.frames()`; target the frame by URL/name and keep ifr
- **Do not run a trigger before arming the waiter.** If a request matters, create `page.waitForResponse(...)` before the click/fill/keypress that triggers it.
- **Do not trust autocomplete or masked inputs blindly.** Fill/type can appear to work while the app rejects the value. Verify visible text, `inputValue()`, or post-action state.
- **Do not solve CAPTCHA or auth challenges programmatically.** Use human handoff and verification.
- **Do not recover a missing popup by navigating the current page.** Use `context.newPage()`. Same-tab `page.goto` is not a new tab.
- **Do not turn browser-run code into adapter code.** Preserve evidence and behavior; adapters use `IPage`, fetch/intercept helpers, or existing adapter patterns.
- **Screenshots are for humans, not for agents.** Use snapshots or targeted extraction unless the page is genuinely visual: CAPTCHA, charts, icon-only controls, or layout ambiguity.
- **Large DOM/text dumps are usually a bug.** Scope extraction, cap returned fields, and prefer response samples or visible values.
Expand All @@ -267,6 +289,8 @@ Use `run` and inspect `page.frames()`; target the frame by URL/name and keep ifr
| `webcmd doctor` is red | Fix the browser runtime first. Browser commands depend on it; adapter discovery does not. |
| No suitable adapter appears | Confirm registry output was complete and non-truncated before browser fallback. |
| Bound page is wrong or stale | Run `tabs`, choose the current page id, then `bind --page <id>` again. |
| Click should have opened a tab, but `tabs` still shows one page | Do not `page.goto` on the bound page. `context.newPage()`, then navigate the new page if you have a URL. |
| `waitForEvent('popup')` hangs until `BROWSER_RUN_TIMEOUT` / `BROWSER_RUN_CANCELLED` | Cap the popup wait to a few seconds. After timeout, inspect `tabs` and fall back to `context.newPage()`. |
| `run` times out before returning | Increase `--timeout` only after checking whether the wait condition is wrong. |
| Write may have happened before timeout | Take a fresh snapshot before retrying. Avoid duplicate submissions. |
| `SESSION_REQUIRED` | Create a Session, then retry with root `--session <session-id>`. |
Expand Down Expand Up @@ -294,4 +318,9 @@ Author-only. Stripped by litprompt, so it costs the running agent nothing.
Append one dated line whenever a correction lands, or whenever an approach
is tried and rejected. Record what was tried and why it failed, not just
what won.

- 2026-08-20: A missing popup recovered with `page.goto` on the opener destroyed
iframe and opener state. Same-tab navigation is not a new tab. `context.newPage()`
is. Do not special-case one fixture URL; popup blockers, `noopener`, and opaque
origins all fail the same way.
-->
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,11 @@ do not — each run starts with a fresh scope.
`context.newPage()` works and creates a tab the Webcmd session tracks. You cannot close it
from inside `run` (see below); list tabs with `webcmd --session <session-id> browser tabs`.

When a click does not produce a popup, `context.newPage()` is the recovery — not
`page.goto` on the opener. Popup blockers, `noopener`, and `data:`/`blob:`/`srcdoc`
documents often swallow `window.open`. Cap `waitForEvent('popup')`; do not let it
consume the whole run timeout.

`page.snapshotForAI()` is not available; use `webcmd browser snapshot` instead.

## What is blocked, and what to use instead
Expand Down
24 changes: 24 additions & 0 deletions skills/webcmd-browser/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -242,6 +242,27 @@ Use `snapshot --snapshot-mode read` first. If the page is an app shell or needs

Use `run` and inspect `page.frames()`; target the frame by URL/name and keep iframe actions in the same program. If Chrome cannot expose the frame, bind or navigate directly to the iframe URL when safe.

### Open a new tab

When a control should open another page (`target=_blank`, `window.open`, a download-in-tab):

1. In the same `run`, arm `page.waitForEvent('popup')` with a short timeout **before** the click.
2. After the click, inspect `context.pages()` or `webcmd --session <session-id> browser tabs`. A new page is the success path — bind to it and read there.
3. If no popup arrives (blocker, `noopener`, `data:`/`blob:`/`srcdoc` origin, timeout): do **not** `page.goto` the target URL on the current bound page. That replaces the opener (iframes, form state, and the original document are gone). Call `context.newPage()` and navigate the new page if a destination URL is already known (`href`, the task URL).
4. Do not wait out the default `browser run` timeout on a popup that has not fired.

```js
const popupPromise = page.waitForEvent('popup', { timeout: 5000 }).catch(() => null);
await page.getByRole('link', { name: /open/i }).click();
const popup = await popupPromise;
if (popup) {
return { url: popup.url(), title: await popup.title() };
}
const tab = await context.newPage();
await tab.goto(knownUrl);
return { url: tab.url(), title: await tab.title() };
```

---

## Pitfalls
Expand All @@ -251,6 +272,7 @@ Use `run` and inspect `page.frames()`; target the frame by URL/name and keep ifr
- **Do not run a trigger before arming the waiter.** If a request matters, create `page.waitForResponse(...)` before the click/fill/keypress that triggers it.
- **Do not trust autocomplete or masked inputs blindly.** Fill/type can appear to work while the app rejects the value. Verify visible text, `inputValue()`, or post-action state.
- **Do not solve CAPTCHA or auth challenges programmatically.** Use human handoff and verification.
- **Do not recover a missing popup by navigating the current page.** Use `context.newPage()`. Same-tab `page.goto` is not a new tab.
- **Do not turn browser-run code into adapter code.** Preserve evidence and behavior; adapters use `IPage`, fetch/intercept helpers, or existing adapter patterns.
- **Screenshots are for humans, not for agents.** Use snapshots or targeted extraction unless the page is genuinely visual: CAPTCHA, charts, icon-only controls, or layout ambiguity.
- **Large DOM/text dumps are usually a bug.** Scope extraction, cap returned fields, and prefer response samples or visible values.
Expand All @@ -267,6 +289,8 @@ Use `run` and inspect `page.frames()`; target the frame by URL/name and keep ifr
| `webcmd doctor` is red | Fix the browser runtime first. Browser commands depend on it; adapter discovery does not. |
| No suitable adapter appears | Confirm registry output was complete and non-truncated before browser fallback. |
| Bound page is wrong or stale | Run `tabs`, choose the current page id, then `bind --page <id>` again. |
| Click should have opened a tab, but `tabs` still shows one page | Do not `page.goto` on the bound page. `context.newPage()`, then navigate the new page if you have a URL. |
| `waitForEvent('popup')` hangs until `BROWSER_RUN_TIMEOUT` / `BROWSER_RUN_CANCELLED` | Cap the popup wait to a few seconds. After timeout, inspect `tabs` and fall back to `context.newPage()`. |
| `run` times out before returning | Increase `--timeout` only after checking whether the wait condition is wrong. |
| Write may have happened before timeout | Take a fresh snapshot before retrying. Avoid duplicate submissions. |
| `SESSION_REQUIRED` | Create a Session, then retry with root `--session <session-id>`. |
Expand Down
5 changes: 5 additions & 0 deletions skills/webcmd-browser/references/browser-run-playwright.md
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,11 @@ do not — each run starts with a fresh scope.
`context.newPage()` works and creates a tab the Webcmd session tracks. You cannot close it
from inside `run` (see below); list tabs with `webcmd --session <session-id> browser tabs`.

When a click does not produce a popup, `context.newPage()` is the recovery — not
`page.goto` on the opener. Popup blockers, `noopener`, and `data:`/`blob:`/`srcdoc`
documents often swallow `window.open`. Cap `waitForEvent('popup')`; do not let it
consume the whole run timeout.

`page.snapshotForAI()` is not available; use `webcmd browser snapshot` instead.

## What is blocked, and what to use instead
Expand Down
14 changes: 14 additions & 0 deletions src/skills.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -333,6 +333,20 @@ describe('webcmd skills content', () => {
expect(browser).not.toMatch(/^### (?:Inspect|Get|Interact|Wait|Extract|Network)$/m);
});

it('recovers a blocked popup without replacing the current page', () => {
const browser = bundledSkill('webcmd-browser');
const playwright = fs.readFileSync(
path.join(process.cwd(), 'skills', 'webcmd-browser', 'references', 'browser-run-playwright.md'),
'utf8',
);

expect(browser).toContain('### Open a new tab');
expect(browser).toContain('Do not recover a missing popup by navigating the current page');
expect(browser).toContain('context.newPage()');
expect(browser).toMatch(/waitForEvent\('popup'\)[\s\S]{0,80}timeout/);
expect(playwright).toMatch(/context\.newPage\(\)[\s\S]{0,220}not[\s\S]{0,40}`page\.goto`/);
});

it('adds bundled skills once and refreshes them after package updates', () => {
const firstRoot = makePackageRoot('first');
const secondRoot = makePackageRoot('second');
Expand Down
Loading