Skip to content
Open
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
1 change: 1 addition & 0 deletions SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -591,6 +591,7 @@ Refs are invalidated on navigation — run `snapshot` again after `goto`.
| `cookie <name>=<value>` | Set cookie on current page domain |
| `cookie-import <json>` | Import cookies from JSON file |
| `cookie-import-browser [browser] [--domain d]` | Import cookies from Comet, Chrome, Arc, Brave, or Edge (opens picker, or use --domain for direct import) |
| `device <name|list|clear>` | Apply Playwright device profile (viewport, user-agent, DPR, touch). Use "list" to show 143 profiles, "clear" to reset |
| `dialog-accept [text]` | Auto-accept next alert/confirm/prompt. Optional text is sent as the prompt response |
| `dialog-dismiss` | Auto-dismiss next dialog |
| `fill <sel> <val>` | Fill input |
Expand Down
1 change: 1 addition & 0 deletions browse/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -488,6 +488,7 @@ Refs are invalidated on navigation — run `snapshot` again after `goto`.
| `cookie <name>=<value>` | Set cookie on current page domain |
| `cookie-import <json>` | Import cookies from JSON file |
| `cookie-import-browser [browser] [--domain d]` | Import cookies from Comet, Chrome, Arc, Brave, or Edge (opens picker, or use --domain for direct import) |
| `device <name|list|clear>` | Apply Playwright device profile (viewport, user-agent, DPR, touch). Use "list" to show 143 profiles, "clear" to reset |
| `dialog-accept [text]` | Auto-accept next alert/confirm/prompt. Optional text is sent as the prompt response |
| `dialog-dismiss` | Auto-dismiss next dialog |
| `fill <sel> <val>` | Fill input |
Expand Down
45 changes: 41 additions & 4 deletions browse/src/browser-manager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@ export class BrowserManager {
private nextTabId: number = 1;
private extraHeaders: Record<string, string> = {};
private customUserAgent: string | null = null;
private activeDevice: { viewport: { width: number; height: number }; userAgent: string; deviceScaleFactor: number; isMobile: boolean; hasTouch: boolean } | null = null;

/** Server port — set after server starts, used by cookie-import-browser command */
public serverPort: number = 0;
Expand Down Expand Up @@ -72,11 +73,16 @@ export class BrowserManager {
});

const contextOptions: BrowserContextOptions = {
viewport: { width: 1280, height: 720 },
viewport: this.activeDevice?.viewport ?? { width: 1280, height: 720 },
};
if (this.customUserAgent) {
contextOptions.userAgent = this.customUserAgent;
}
if (this.activeDevice) {
contextOptions.deviceScaleFactor = this.activeDevice.deviceScaleFactor;
contextOptions.isMobile = this.activeDevice.isMobile;
contextOptions.hasTouch = this.activeDevice.hasTouch;
}
this.context = await this.browser.newContext(contextOptions);

if (Object.keys(this.extraHeaders).length > 0) {
Expand Down Expand Up @@ -275,6 +281,22 @@ export class BrowserManager {
await this.getPage().setViewportSize({ width, height });
}

// ─── Device Emulation ─────────────────────────────────────
async setDevice(descriptor: { viewport: { width: number; height: number }; userAgent: string; deviceScaleFactor: number; isMobile: boolean; hasTouch: boolean } | null) {
this.activeDevice = descriptor;
if (descriptor) {
this.customUserAgent = descriptor.userAgent;
await this.getPage().setViewportSize(descriptor.viewport);
} else {
this.customUserAgent = null;
await this.getPage().setViewportSize({ width: 1280, height: 720 });
}
}

getDevice() {
return this.activeDevice;
}

// ─── Extra Headers ─────────────────────────────────────────
async setExtraHeader(name: string, value: string) {
this.extraHeaders[name] = value;
Expand Down Expand Up @@ -401,11 +423,16 @@ export class BrowserManager {

// 3. Create new context with updated settings
const contextOptions: BrowserContextOptions = {
viewport: { width: 1280, height: 720 },
viewport: this.activeDevice?.viewport ?? { width: 1280, height: 720 },
};
if (this.customUserAgent) {
contextOptions.userAgent = this.customUserAgent;
}
if (this.activeDevice) {
contextOptions.deviceScaleFactor = this.activeDevice.deviceScaleFactor;
contextOptions.isMobile = this.activeDevice.isMobile;
contextOptions.hasTouch = this.activeDevice.hasTouch;
}
this.context = await this.browser.newContext(contextOptions);

if (Object.keys(this.extraHeaders).length > 0) {
Expand All @@ -423,11 +450,16 @@ export class BrowserManager {
if (this.context) await this.context.close().catch(() => {});

const contextOptions: BrowserContextOptions = {
viewport: { width: 1280, height: 720 },
viewport: this.activeDevice?.viewport ?? { width: 1280, height: 720 },
};
if (this.customUserAgent) {
contextOptions.userAgent = this.customUserAgent;
}
if (this.activeDevice) {
contextOptions.deviceScaleFactor = this.activeDevice.deviceScaleFactor;
contextOptions.isMobile = this.activeDevice.isMobile;
contextOptions.hasTouch = this.activeDevice.hasTouch;
}
this.context = await this.browser!.newContext(contextOptions);
await this.newTab();
this.clearRefs();
Expand Down Expand Up @@ -473,11 +505,16 @@ export class BrowserManager {
// 3. Create context and restore state into new headed browser
try {
const contextOptions: BrowserContextOptions = {
viewport: { width: 1280, height: 720 },
viewport: this.activeDevice?.viewport ?? { width: 1280, height: 720 },
};
if (this.customUserAgent) {
contextOptions.userAgent = this.customUserAgent;
}
if (this.activeDevice) {
contextOptions.deviceScaleFactor = this.activeDevice.deviceScaleFactor;
contextOptions.isMobile = this.activeDevice.isMobile;
contextOptions.hasTouch = this.activeDevice.hasTouch;
}
const newContext = await newBrowser.newContext(contextOptions);

if (Object.keys(this.extraHeaders).length > 0) {
Expand Down
3 changes: 2 additions & 1 deletion browse/src/commands.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ export const READ_COMMANDS = new Set([
export const WRITE_COMMANDS = new Set([
'goto', 'back', 'forward', 'reload',
'click', 'fill', 'select', 'hover', 'type', 'press', 'scroll', 'wait',
'viewport', 'cookie', 'cookie-import', 'cookie-import-browser', 'header', 'useragent',
'viewport', 'device', 'cookie', 'cookie-import', 'cookie-import-browser', 'header', 'useragent',
'upload', 'dialog-accept', 'dialog-dismiss',
]);

Expand Down Expand Up @@ -71,6 +71,7 @@ export const COMMAND_DESCRIPTIONS: Record<string, { category: string; descriptio
'wait': { category: 'Interaction', description: 'Wait for element, network idle, or page load (timeout: 15s)', usage: 'wait <sel|--networkidle|--load>' },
'upload': { category: 'Interaction', description: 'Upload file(s)', usage: 'upload <sel> <file> [file2...]' },
'viewport':{ category: 'Interaction', description: 'Set viewport size', usage: 'viewport <WxH>' },
'device': { category: 'Interaction', description: 'Apply Playwright device profile (viewport, user-agent, DPR, touch). Use "list" to show 143 profiles, "clear" to reset', usage: 'device <name|list|clear>' },
'cookie': { category: 'Interaction', description: 'Set cookie on current page domain', usage: 'cookie <name>=<value>' },
'cookie-import': { category: 'Interaction', description: 'Import cookies from JSON file', usage: 'cookie-import <json>' },
'cookie-import-browser': { category: 'Interaction', description: 'Import cookies from Comet, Chrome, Arc, Brave, or Edge (opens picker, or use --domain for direct import)', usage: 'cookie-import-browser [browser] [--domain d]' },
Expand Down
34 changes: 27 additions & 7 deletions browse/src/meta-commands.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import { handleSnapshot } from './snapshot';
import { getCleanText } from './read-commands';
import { READ_COMMANDS, WRITE_COMMANDS, META_COMMANDS } from './commands';
import { validateNavigationUrl } from './url-validation';
import { devices } from 'playwright';
import * as Diff from 'diff';
import * as fs from 'fs';
import * as path from 'path';
Expand Down Expand Up @@ -158,24 +159,43 @@ export async function handleMetaCommand(
const page = bm.getPage();
const prefix = args[0] || `${TEMP_DIR}/browse-responsive`;
validateOutputPath(prefix);
const mobileDevice = devices['iPhone 15 Pro'];
const tabletDevice = devices['iPad (gen 7)'];
const viewports = [
{ name: 'mobile', width: 375, height: 812 },
{ name: 'tablet', width: 768, height: 1024 },
{ name: 'desktop', width: 1280, height: 720 },
{ name: 'mobile', device: mobileDevice, width: mobileDevice.viewport.width, height: mobileDevice.viewport.height },
{ name: 'tablet', device: tabletDevice, width: tabletDevice.viewport.width, height: tabletDevice.viewport.height },
{ name: 'desktop', device: null, width: 1280, height: 720 },
];
const originalViewport = page.viewportSize();
const savedDevice = bm.getDevice();
const results: string[] = [];

for (const vp of viewports) {
await page.setViewportSize({ width: vp.width, height: vp.height });
if (vp.device) {
await bm.setDevice({
viewport: vp.device.viewport,
userAgent: vp.device.userAgent,
deviceScaleFactor: vp.device.deviceScaleFactor,
isMobile: vp.device.isMobile,
hasTouch: vp.device.hasTouch,
});
} else {
await bm.setDevice(null);
await page.setViewportSize({ width: vp.width, height: vp.height });
}
const path = `${prefix}-${vp.name}.png`;
await page.screenshot({ path, fullPage: true });
results.push(`${vp.name} (${vp.width}x${vp.height}): ${path}`);
}

// Restore original viewport
if (originalViewport) {
await page.setViewportSize(originalViewport);
// Restore previous device/viewport
if (savedDevice) {
await bm.setDevice(savedDevice);
} else {
await bm.setDevice(null);
if (originalViewport) {
await page.setViewportSize(originalViewport);
}
}

return results.join('\n');
Expand Down
37 changes: 37 additions & 0 deletions browse/src/write-commands.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
import type { BrowserManager } from './browser-manager';
import { findInstalledBrowsers, importCookies } from './cookie-import-browser';
import { validateNavigationUrl } from './url-validation';
import { devices } from 'playwright';
import * as fs from 'fs';
import * as path from 'path';
import { TEMP_DIR, isPathWithin } from './platform';
Expand Down Expand Up @@ -196,6 +197,42 @@ export async function handleWriteCommand(
return `Viewport set to ${w}x${h}`;
}

case 'device': {
const subcommand = args[0];
if (!subcommand) throw new Error('Usage: browse device <name|list|clear>');

if (subcommand === 'list') {
const names = Object.keys(devices);
return `${names.length} devices available:\n${names.join('\n')}`;
}

if (subcommand === 'clear') {
await bm.setDevice(null);
return 'Device profile cleared. Using default viewport (1280x720).';
}

const name = args.join(' ');
const descriptor = devices[name];
if (!descriptor) {
const match = Object.keys(devices).find(k =>
k.toLowerCase().includes(name.toLowerCase())
);
if (match) {
throw new Error(`Device "${name}" not found. Did you mean "${match}"?`);
}
throw new Error(`Device "${name}" not found. Run "device list" to see available profiles.`);
}

await bm.setDevice({
viewport: descriptor.viewport,
userAgent: descriptor.userAgent,
deviceScaleFactor: descriptor.deviceScaleFactor,
isMobile: descriptor.isMobile,
hasTouch: descriptor.hasTouch,
});
return `Device: ${name}\n Viewport: ${descriptor.viewport.width}x${descriptor.viewport.height}\n DPR: ${descriptor.deviceScaleFactor}\n Mobile: ${descriptor.isMobile}\n Touch: ${descriptor.hasTouch}\n UA: ${descriptor.userAgent.slice(0, 80)}...`;
}

case 'cookie': {
const cookieStr = args[0];
if (!cookieStr || !cookieStr.includes('=')) throw new Error('Usage: browse cookie <name>=<value>');
Expand Down
Binary file added docs/images/device-after-iphone.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added docs/images/device-before-desktop.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.