feat(emulate): add mobile device emulation via CDP Emulation domain (#60)

* feat(emulate): add mobile device emulation via CDP Emulation domain

Add `bsk emulate` so agents can debug mobile page behaviour by
emulating a device environment on a session tab:

- Viewport metrics (Emulation.setDeviceMetricsOverride), User-Agent
  (Emulation.setUserAgentOverride, optional accept-language and UA
  client-hints metadata), and touch (Emulation.setTouchEmulationEnabled
  with maxTouchPoints).
- Seven CLI-side device presets (iphone-14, iphone-14-pro-max,
  iphone-se, pixel-7, galaxy-s23, ipad-mini, galaxy-tab-s8) via
  --device; manual flags (--width/--height/--dpr/--mobile/--ua/
  --accept-language/--touch/--max-touch-points) work standalone or
  override individual preset fields.
- `bsk emulate --off` clears every override (clearDeviceMetricsOverride
  + touch disabled + empty UA override) and restores the real
  environment.
- Overrides are per-tab (CDP per-target) and not inherited by new tabs;
  the result JSON carries a note reminding agents to re-apply.

Wire: new tool.emulate method end-to-end (bsk-protocol types + JSON
schemas + Method::ToolEmulate classified BrowserMutation, daemon
forwarding, CLI subcommand, extension tool handler + ChromiumCdp
Emulation wrappers, dispatcher registration), docs in both SKILL.md
copies, and unit tests on every layer.

* refine(emulate): merge per-tab state and align preset override semantics

- extension: remember the applied emulation state per tab and merge each new request onto it field by field before applying, so a later --width/--height no longer resets an earlier preset's dpr/mobile; --off clears the stored state along with the CDP overrides
- extension: cdp_failed errors now note that already-applied overrides were not rolled back and can be reset with --off
- cli: validate width/height pairing and metric requirements on the merged preset+flags result, so --device iphone-14 --width 500 keeps the preset height
- cli: add --no-mobile/--no-touch (conflicting with --mobile/--touch) to turn preset mobile/touch fields back off
- docs: describe the merge behaviour, --no-mobile/--no-touch and --off state clearing in both SKILL.md copies
This commit is contained in:
BB-fat
2026-08-05 18:10:39 +08:00
committed by GitHub
parent 49378d70de
commit 0f264c5df1
21 changed files with 2579 additions and 1 deletions
@@ -85,6 +85,23 @@ export const CDP_PROTOCOL_VERSION = "1.3";
/** Per-tab monotonic sequence returned by [`ChromiumCdp.dialogCursor`]. */
export type DialogCursor = number;
/** CDP `Emulation.setDeviceMetricsOverride` payload. */
export interface DeviceMetricsOverride {
width: number;
height: number;
/** `0` asks Chrome to use the display's native factor. */
deviceScaleFactor: number;
mobile: boolean;
}
/** CDP `Emulation.setUserAgentOverride` payload. */
export interface UserAgentOverride {
userAgent: string;
acceptLanguage?: string;
/** CDP `Emulation.UserAgentMetadata` (already camelCase). */
userAgentMetadata?: Record<string, unknown>;
}
const MAX_DIALOG_BUFFER = 32;
const MAX_DIALOG_FIELD_LENGTH = 4096;
const MAX_CONSOLE_BUFFER = 200;
@@ -194,6 +211,33 @@ export class ChromiumCdp {
return this.dialogSequences.get(tabId) ?? 0;
}
/** `Emulation.setDeviceMetricsOverride` — pin the tab's viewport metrics. */
async setDeviceMetricsOverride(tabId: number, metrics: DeviceMetricsOverride): Promise<void> {
await this.send(tabId, "Emulation.setDeviceMetricsOverride", { ...metrics });
}
/** `Emulation.clearDeviceMetricsOverride` — restore native viewport metrics. */
async clearDeviceMetricsOverride(tabId: number): Promise<void> {
await this.send(tabId, "Emulation.clearDeviceMetricsOverride", {});
}
/** `Emulation.setUserAgentOverride` — an empty `userAgent` restores the real UA. */
async setUserAgentOverride(tabId: number, override: UserAgentOverride): Promise<void> {
await this.send(tabId, "Emulation.setUserAgentOverride", { ...override });
}
/** `Emulation.setTouchEmulationEnabled`. */
async setTouchEmulationEnabled(
tabId: number,
enabled: boolean,
maxTouchPoints?: number,
): Promise<void> {
await this.send(tabId, "Emulation.setTouchEmulationEnabled", {
enabled,
...(maxTouchPoints !== undefined ? { maxTouchPoints } : {}),
});
}
/** Dialogs observed on `tabId` with sequence strictly greater than `cursor`. */
dialogsSince(tabId: number, cursor: DialogCursor): JavaScriptDialogInfo[] {
const buf = this.dialogBuffers.get(tabId) ?? [];