pi-browser-harness 0.8.3 → 0.10.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/src/prompt.ts CHANGED
@@ -2,150 +2,126 @@
2
2
  * System prompt guidance for browser usage.
3
3
  *
4
4
  * Injects a compact browser control reference into the pi system prompt.
5
- * Per-tool guidelines are injected via promptGuidelines on each tool registration.
6
- * This prompt provides only the cross-cutting patterns and tool reference table.
5
+ * This prompt carries only cross-cutting knowledge — the mental model, tool
6
+ * routing, workflows, and gotchas. Per-tool specifics (schemas, detailed usage)
7
+ * are injected separately via promptGuidelines on each tool registration, so this
8
+ * file intentionally does NOT restate full tool descriptions.
7
9
  */
8
10
 
9
11
  export function getBrowserSystemPrompt(): string {
10
12
  return `
11
13
  ## Browser Control
12
14
 
13
- You have full control of a real Chrome browser via \`browser_*\` tools.
14
- The browser connects to the user's running Chrome (not a headless instance).
15
-
16
- **These tools coexist with all other standard pi tools** — read, bash, edit,
17
- write, and any other built-in tools are still available. Use browser_* tools
18
- to interact with pages visually, and other tools alongside them.
19
-
20
- ### Browser-Specific Tools
21
-
22
- | Tool | Purpose |
23
- |------|---------|
24
- | browser_snapshot | Accessibility-tree snapshot. Assigns every interactive element a stable **ref** (\`[eN]\`) plus \`@(x,y)\`. Refs are the primary handle for interaction tools — they survive re-renders, coordinates don't. |
25
- | browser_screenshot | Capture a PNG or JPEG screenshot of the current page (rendered inline in the TUI; the LLM reads the image via the saved file path) |
26
- | browser_click | Click an element by **ref** (preferred — re-resolves position, survives re-renders) or viewport x/y. Appends a compact diff of page changes. |
27
- | browser_type | Type text into the focused element (real keystrokes; requires a focused field) |
28
- | browser_fill | Fill an input/textarea by **ref** (preferred) or selector — updates React/Vue controlled components, verifies the value, appends a page-changes diff |
29
- | browser_select_option | Select an option in a native <select> by **ref** (preferred) or selector, choosing by value/label/index (fires change) |
30
- | browser_focus | Focus an element by **ref** (preferred) or selector (deterministic; use before browser_type) |
31
- | browser_press_key | Press a keyboard key (Enter, Tab, Escape, arrows, etc.) |
32
- | browser_scroll | Scroll the page at coordinates |
33
- | browser_navigate | Navigate to a URL (result details.outcome.kind is "in_place" or "new_tab_created") |
34
- | browser_new_tab | Open a new tab, optionally navigate |
35
- | browser_open_urls | Open multiple URLs in new tabs (parallel) |
36
- | browser_go_back / browser_go_forward / browser_reload | History navigation |
37
- | browser_page_info | Get page URL, title, viewport, scroll position, or dialog info |
38
- | browser_list_tabs | List all open browser tabs |
39
- | browser_current_tab | Get current tab info |
40
- | browser_switch_tab | Switch to a different tab by targetId |
41
- | browser_execute_js | Execute JavaScript and return the result |
42
- | browser_http_get | Direct HTTP GET (outside browser, for APIs) |
43
- | browser_console | Read JS errors / console output on the current tab (diagnostic — use when an action looks broken; pass sinceSeq from the previous nextCursor to see only new messages) |
44
- | browser_wait | Wait N seconds |
45
- | browser_wait_for | Wait until a selector/text appears (or a selector disappears) — use for SPA content before interacting |
46
- | browser_wait_for_load | Wait for document.readyState === 'complete' (returns a typed timeout error if the page doesn't reach readyState=complete in N seconds, default 15) |
47
- | browser_handle_dialog | Accept or dismiss a JS dialog |
48
- | browser_run_script | Execute a temporary script file with daemon access (write script to disk, then run) |
49
-
50
- ### Temporary Scripts
51
-
52
- When the built-in tools aren't enough for a multi-step workflow, write a temporary
53
- script to disk and execute it with browser_run_script. The script runs in the
54
- harness process with direct access to the browser daemon and Node.js APIs.
15
+ You drive a **real Chrome** (the user's running instance, not headless) through
16
+ \`browser_*\` tools. These coexist with every standard pi tool — read, bash, edit,
17
+ write — so mix them freely: \`browser_*\` for the page, everything else alongside.
55
18
 
56
- \`\`\`
57
- write("/tmp/scrape-pages.js", \`
58
- const results = [];
59
- for (const url of params.urls) {
60
- await daemon.cdp("Page.navigate", { url });
61
- await new Promise(r => setTimeout(r, 2000));
62
- const data = await daemon.evaluateJS("document.title");
63
- results.push({ url, title: data });
64
- }
65
- return { content: [{ type: "text", text: JSON.stringify(results, null, 2) }] };
66
- \`)
67
- browser_run_script("/tmp/scrape-pages.js", { urls: [...] })
68
- \`\`\`
19
+ ### The core handle: refs
69
20
 
70
- Script bindings: params, daemon, require, signal, onUpdate, ctx, console, fetch, JSON, Buffer, setTimeout, clearTimeout.
21
+ Interaction is ref-first. Call \`browser_snapshot\` to get an accessibility tree
22
+ where every interactive element has a stable ref (\`[eN]\`) and \`@(x,y)\`. **Target
23
+ elements by ref, not by coordinates or guessed selectors** — refs are keyed to
24
+ element identity, so they survive the re-renders that React/Vue forms trigger on
25
+ every edit and save. Coordinates go stale; guessed selectors miss.
71
26
 
72
- ### Parallel Execution
27
+ - Prefer \`browser_fill\` for text — it fires input/change so controlled components
28
+ update, and returns the value for verification. Use \`browser_click\` + \`browser_type\`
29
+ only for keystroke-sensitive widgets (autocomplete, masked inputs).
30
+ - Mutating calls append a compact **"Page changes"** diff — read it to confirm the
31
+ change landed (form closed, new \`*[eN]\` appeared).
32
+ - \`"ref is stale"\` means the page changed: re-run \`browser_snapshot\` for fresh refs.
73
33
 
74
- Observation tools (browser_screenshot, browser_page_info, browser_execute_js, browser_list_tabs, browser_http_get, etc.) can run in parallel with each other and with mutation tools. The harness automatically serializes mutation tools (click, type, scroll, navigate, switch_tab, etc.) so they never race on shared state. When operations are independent, emit them in the same turn for better performance.
34
+ ### Choosing a tool
75
35
 
76
- **Examples of safe parallel calls:**
77
- \`\`\`
78
- browser_screenshot() + browser_page_info() + browser_execute_js("document.title")
79
- browser_http_get("https://api.example.com/data") + browser_click(x, y)
80
- \`\`\`
36
+ | To… | Reach for |
37
+ |-----|-----------|
38
+ | See the page structure (for interaction) | \`browser_snapshot\` (refs) |
39
+ | See the page visually | \`browser_screenshot\` |
40
+ | Search the web | \`browser_web_search\` → ranked {title, url, snippet} |
41
+ | Read an article's clean text | \`browser_read_page\` (reader mode; url or owned tab) |
42
+ | Extract specific DOM values | \`browser_execute_js\` |
43
+ | Hit a JSON/API endpoint | \`browser_http_get\` (raw GET, outside the browser) |
44
+ | Click / fill / type / select / focus / press a key | \`browser_click\` · \`browser_fill\` · \`browser_type\` · \`browser_select_option\` · \`browser_focus\` · \`browser_press_key\` |
45
+ | Upload a file · drag · resize viewport | \`browser_upload_file\` · \`browser_drag_and_drop\` · \`browser_viewport_resize\` |
46
+ | Go to a URL / open many / manage tabs | \`browser_navigate\` · \`browser_new_tab\` · \`browser_open_urls\` · \`browser_list_tabs\` · \`browser_switch_tab\` · \`browser_go_back\`/\`go_forward\`/\`reload\` |
47
+ | Wait for content / load / a fixed delay | \`browser_wait_for\` (selector/text) · \`browser_wait_for_load\` (readyState) · \`browser_wait\` |
48
+ | Inspect page / handle a dialog | \`browser_page_info\` · \`browser_current_tab\` · \`browser_handle_dialog\` |
49
+ | Diagnose a broken action | \`browser_console\` (JS errors) · \`browser_get_network_log\` |
50
+ | Save output / config downloads | \`browser_print_to_pdf\` · \`browser_download\` |
51
+ | Anything the tools can't express | \`browser_run_script\` (see Extending) |
81
52
 
82
- **Multi-agent note**: When multiple subagents use the browser, tab switching by one agent changes the active tab for all agents. Per-tab data (console buffers, network traces, dialogs, page info cache) is isolated — switching tabs does not destroy another agent's collected data. Call \`browser_current_tab\` before mutation tools to confirm you're on the expected tab. If \`browser_page_info\` returns a dialog, handle it with \`browser_handle_dialog\` promptly — dialogs block page interaction and are not queued across agents.
53
+ Each tool carries its own detailed guidelines — this table is only for routing.
83
54
 
84
- ### Common Patterns
55
+ ### Workflows
85
56
 
86
- **Navigation:**
57
+ **Research (search → read):**
87
58
  \`\`\`
88
- browser_new_tab("https://example.com") → browser_wait_for_load() → browser_screenshot()
59
+ browser_web_search("query") // ranked links, no page content
60
+ → browser_read_page(url) // clean article text for the top hits
61
+ // fall back to browser_open_urls + browser_execute_js only when reader mode misses
89
62
  \`\`\`
90
63
 
91
- **Form filling (ref-first — this is the reliable path on React/SPA forms):**
64
+ **Form filling (ref-first — the reliable path on SPA forms):**
92
65
  \`\`\`
93
- browser_wait_for({ selector: "#email" }) // ensure SPA field is rendered
94
- browser_snapshot() // each interactive element gets a [eN] ref
95
- → browser_fill({ ref: "e7", value: "a@b.com" }) // target by ref, not a guessed selector
96
- → browser_select_option({ ref: "e9", label: "India" }) // native <select> by ref
97
- → browser_click({ ref: "e12" }) // e.g. Save — re-resolves position even if the form reflowed
98
- // each mutating call appends a compact "Page changes" diff — read it to confirm
99
- // the change landed (e.g. a save closed the form, a new field appeared as *[eN]).
66
+ browser_wait_for({ selector: "#email" }) // ensure the field is rendered
67
+ browser_snapshot() // interactive elements get [eN] refs
68
+ → browser_fill({ ref: "e7", value: "a@b.com" })
69
+ → browser_select_option({ ref: "e9", label: "India" })
70
+ → browser_click({ ref: "e12" }) // re-resolves position even after reflow
71
+ // read each "Page changes" diff to confirm the step landed
100
72
  \`\`\`
101
- **Why ref over selector/coordinates:** refs are keyed to the element's identity, so
102
- they survive the re-renders that React/Vue forms trigger on every edit and save —
103
- coordinates go stale and selectors you guess often don't match. If a call returns
104
- "ref is stale", the page changed: re-run browser_snapshot for fresh refs.
105
-
106
- Prefer browser_fill for text inputs/textareas — it fires input/change so controlled
107
- components update, and returns the field's value for verification. Use browser_click +
108
- browser_type only for keystroke-sensitive widgets (autocomplete, masked inputs);
109
- browser_type requires an already-focused field and errors if none is focused.
110
73
 
111
- **Data extraction:**
74
+ **Navigate + verify:**
112
75
  \`\`\`
113
- browser_execute_js("document.querySelector('.price').innerText")
114
- // or for APIs:
115
- browser_http_get("https://api.example.com/data")
76
+ browser_new_tab("https://example.com") → browser_wait_for_load() → browser_screenshot()
116
77
  \`\`\`
117
78
 
118
- **Scrolling:**
119
- \`\`\`
120
- browser_screenshot() → browser_scroll({ deltaY: 500 }) → browser_screenshot()
121
- \`\`\`
122
- Note: deltaY follows W3C wheel-event convention: positive=down, negative=up. Default deltaY=300 scrolls down.
79
+ **Scroll:** \`browser_scroll({ deltaY: 500 })\` — W3C wheel convention: positive = down,
80
+ negative = up (default 300, down).
123
81
 
124
- **Research Workflow (search + browser):**
82
+ ### Parallelism
83
+
84
+ Observation tools (\`browser_screenshot\`, \`browser_page_info\`, \`browser_execute_js\`,
85
+ \`browser_list_tabs\`, \`browser_http_get\`, \`browser_web_search\`, \`browser_read_page\`, …)
86
+ run in parallel with each other and with mutations. The harness serializes mutations
87
+ (click, type, scroll, navigate, switch_tab) so they never race. Emit independent calls
88
+ in one turn:
125
89
  \`\`\`
126
- browser_navigate("https://google.com/search?q=...") → search engine in a tab
127
- browser_open_urls(urls: ["url1", "url2", ...]) → open result pages in parallel tabs
128
- browser_list_tabs() → see all open tabs with targetIds
129
- browser_switch_tab(targetId: "...") → switch to a tab
130
- browser_screenshot() → visually inspect the page
131
- browser_execute_js("document.querySelector('.main').innerText") → extract content
90
+ browser_screenshot() + browser_page_info() + browser_execute_js("document.title")
132
91
  \`\`\`
133
92
 
134
- **Temporary Scripts (extending the harness):**
93
+ **Multi-agent:** tab switching by one agent changes the active tab for all agents,
94
+ but per-tab data (console, network, dialogs) stays isolated. Call \`browser_current_tab\`
95
+ before a mutation to confirm you're on the expected tab. Handle any dialog reported by
96
+ \`browser_page_info\` promptly — dialogs block interaction and aren't queued across agents.
97
+
98
+ **Session boundary & tab hygiene:** you operate only inside this session's own Chrome
99
+ window. \`browser_switch_tab\`/\`browser_close_tab\` refuse tabs this session didn't open,
100
+ and \`browser_list_tabs\` defaults to owned tabs — never reach into the user's other tabs
101
+ or windows. Close tabs when you're done with them: call \`browser_close_tab\` on any tab
102
+ you opened (via \`browser_new_tab\`/\`browser_open_urls\`) once you've extracted what you
103
+ need. Don't leave a pile of stale tabs behind — keep only what a later step still needs.
104
+ (Leftover tabs are closed automatically when the session ends, but close them yourself
105
+ as you go.)
106
+
107
+ ### Extending: temporary scripts
108
+
109
+ When built-in tools can't express a multi-step workflow, write a script to a temp file
110
+ and run it with \`browser_run_script\`. It executes in the harness process with direct
111
+ daemon + Node access.
135
112
  \`\`\`
136
- write("/tmp/extract.js", "...script with daemon access...") → browser_run_script("/tmp/extract.js")
113
+ write(<tempfile>.js, \`
114
+ const results = [];
115
+ for (const url of params.urls) {
116
+ await daemon.cdp("Page.navigate", { url });
117
+ await new Promise(r => setTimeout(r, 2000));
118
+ results.push({ url, title: await daemon.evaluateJS("document.title") });
119
+ }
120
+ return { content: [{ type: "text", text: JSON.stringify(results, null, 2) }] };
121
+ \`)
122
+ browser_run_script(<tempfile>.js, { urls: [...] })
137
123
  \`\`\`
138
-
139
- ### Additional Tools
140
-
141
- | Tool | Purpose |
142
- |------|---------|
143
- | browser_upload_file | Upload a file to a file input by **ref** (preferred) or selector (bypasses file picker) |
144
- | browser_dispatch_key | Dispatch a DOM KeyboardEvent on an element by **ref** (preferred) or selector (for React/Vue inputs); returns details.matched |
145
- | browser_download | Configure download directory and disable save-as prompts |
146
- | browser_viewport_resize | Resize the viewport for responsive testing |
147
- | browser_drag_and_drop | Perform drag-and-drop from one coordinate to another |
148
- | browser_print_to_pdf | Print the current page to a PDF file |
149
- | browser_get_network_log | Get buffered network request/response events |
124
+ Script bindings: params, daemon, require, signal, onUpdate, ctx, console, fetch, JSON,
125
+ Buffer, setTimeout, clearTimeout. Use a real temp path from the OS — don't hardcode \`/tmp\`.
150
126
  `;
151
127
  }
package/src/registry.ts CHANGED
@@ -19,6 +19,8 @@ import { consoleTool } from "./domains/console";
19
19
  import { snapshotTool } from "./domains/snapshot";
20
20
  import { executeJsTool, runScriptTool } from "./domains/js";
21
21
  import { setupTool } from "./domains/setup";
22
+ import { webSearchTool } from "./domains/search/web-search";
23
+ import { readPageTool } from "./domains/readpage/read-page";
22
24
 
23
25
  // Mutation tools that modify page / browser state are marked `serialized: true`
24
26
  // so they run through a shared async mutex. Observation / read-only tools are
@@ -61,6 +63,10 @@ const TOOLS: ReadonlyArray<AnyBrowserToolDefinition> = [
61
63
  snapshotTool,
62
64
  executeJsTool,
63
65
  runScriptTool,
66
+ // Opens and owns an isolated tab to scrape a SERP — same class as openUrlsTool.
67
+ { ...webSearchTool, serialized: true },
68
+ // Pure isolated read (own sessionId, own tab) — safe concurrent, like executeJsTool.
69
+ readPageTool,
64
70
  ];
65
71
 
66
72
  export const registerAllTools = (pi: ExtensionAPI, client: BrowserClient): void => {
package/src/setup.ts CHANGED
@@ -4,10 +4,12 @@
4
4
  * users and a tool (browser_setup) for the agent to self-recover.
5
5
  */
6
6
 
7
- import type { ExtensionAPI } from "@mariozechner/pi-coding-agent";
8
- import { execSync } from "node:child_process";
7
+ import type { ExtensionAPI, ExtensionContext } from "@mariozechner/pi-coding-agent";
9
8
  import type { BrowserClient } from "./client";
10
9
  import { ensureDaemon } from "./daemon/spawn";
10
+ import { DAEMON_SOCKET_PATH } from "./daemon/protocol";
11
+ import { isBrowserRunning } from "./profile/browser-process";
12
+ import { promptForProfileIfUnset } from "./profile/command";
11
13
 
12
14
  // ── Public: register the /browser-setup slash command ──────────────────────
13
15
 
@@ -15,7 +17,7 @@ export function registerSetupCommand(pi: ExtensionAPI, client: BrowserClient): v
15
17
  pi.registerCommand("browser-setup", {
16
18
  description: "Connect pi to your Chrome browser",
17
19
  handler: async (_args, ctx) => {
18
- const result = await performSetup(client);
20
+ const result = await performSetup(client, ctx);
19
21
  if (result.success) {
20
22
  ctx.ui.notify(result.data, "info");
21
23
  } else {
@@ -31,9 +33,9 @@ export type SetupResult = { success: true; data: string } | { success: false; er
31
33
 
32
34
  // ── Shared setup logic (used by both the command and the tool) ─────────────
33
35
 
34
- export async function performSetup(client: BrowserClient): Promise<SetupResult> {
36
+ export async function performSetup(client: BrowserClient, ctx?: ExtensionContext): Promise<SetupResult> {
35
37
  // Step 1: Check Chrome is running
36
- const chromeRunning = checkChromeRunning();
38
+ const chromeRunning = await isBrowserRunning();
37
39
  if (!chromeRunning) {
38
40
  return { success: false, error: "No browser instance running. Please open your browser and then run /browser-setup." };
39
41
  }
@@ -41,10 +43,16 @@ export async function performSetup(client: BrowserClient): Promise<SetupResult>
41
43
  // Step 2: Start the browser daemon (spawns if not running, silently reuses if alive)
42
44
  const daemonReady = await ensureDaemon();
43
45
  if (!daemonReady) {
44
- return { success: false, error: "Could not start the browser daemon. Check /tmp/pi-browser-daemon.sock." };
46
+ return { success: false, error: `Could not start the browser daemon. Check ${DAEMON_SOCKET_PATH}.` };
45
47
  }
46
48
 
47
- // Step 3: Connect to Chrome DevTools
49
+ // Step 3: Ask which profile to work in, the first time only. Enumeration
50
+ // reads DevToolsActivePort + Local State from disk, so it works before the
51
+ // connection exists. Declining leaves the pre-profile behavior untouched —
52
+ // this must never block setup.
53
+ const profileNote = await promptForProfileIfUnset(client, ctx);
54
+
55
+ // Step 4: Connect to Chrome DevTools
48
56
  const startResult = await client.start();
49
57
  if (!startResult.success) {
50
58
  const msg = startResult.error.message;
@@ -59,9 +67,12 @@ export async function performSetup(client: BrowserClient): Promise<SetupResult>
59
67
  return {
60
68
  success: false,
61
69
  error:
62
- "Chrome remote debugging needs to be enabled.\n\n" +
63
- 'Open chrome://inspect/#remote-debugging in your browser, tick the\n' +
70
+ "Browser remote debugging needs to be enabled.\n\n" +
71
+ "Open chrome://inspect/#remote-debugging (or brave://inspect,\n" +
72
+ "edge://inspect) in your browser, tick the\n" +
64
73
  '"Discover network targets" / Allow checkbox, then retry.\n\n' +
74
+ "If that doesn't expose DevTools, relaunch the browser with\n" +
75
+ "--remote-debugging-port=9222.\n\n" +
65
76
  "Or set BU_CDP_WS to a remote browser WebSocket URL.",
66
77
  };
67
78
  }
@@ -69,7 +80,7 @@ export async function performSetup(client: BrowserClient): Promise<SetupResult>
69
80
  return { success: false, error: `Connection failed: ${msg}` };
70
81
  }
71
82
 
72
- // Step 4: Verify with test navigation
83
+ // Step 5: Verify with test navigation
73
84
  const tabResult = await client.newTab("https://github.com");
74
85
  if (!tabResult.success) {
75
86
  return { success: false, error: `Browser connected but test navigation failed: ${tabResult.error.message}` };
@@ -81,40 +92,13 @@ export async function performSetup(client: BrowserClient): Promise<SetupResult>
81
92
  }
82
93
 
83
94
  const pageUrl = info.success && !("dialog" in info.data) ? info.data.url : "github.com";
84
- return { success: true, data: `Browser connected ✓\nNavigated to: ${pageUrl}` };
95
+ const lines = ["Browser connected ✓", `Navigated to: ${pageUrl}`];
96
+ if (profileNote) lines.push(profileNote);
97
+ return { success: true, data: lines.join("\n") };
85
98
  }
86
99
 
87
100
  // ── Chrome process detection ───────────────────────────────────────────────
88
-
89
- function checkChromeRunning(): boolean {
90
- try {
91
- if (process.platform === "darwin") {
92
- // ps -o comm= returns the full executable path on modern macOS.
93
- // We check for "Google Chrome" anywhere in the path (case-insensitive),
94
- // excluding helper/renderer subprocesses.
95
- const out = execSync("ps -A -o comm=", { timeout: 5000 }).toString().toLowerCase();
96
- const lines = out.split("\n").map((l) => l.trim()).filter(Boolean);
97
- return lines.some((l) => {
98
- if (l.includes("helper") || l.includes("renderer") || l.includes("crashpad")) return false;
99
- return l.includes("google chrome") || l.includes("chromium") || l.includes("microsoft edge");
100
- });
101
- } else if (process.platform === "linux") {
102
- const out = execSync("ps -A -o comm=,args=", { timeout: 5000 }).toString().toLowerCase();
103
- const lines = out.split("\n").map((l) => l.trim()).filter(Boolean);
104
- const browserComms = ["chrome", "chromium", "chromium-browser", "msedge", "microsoft-edge", "google-chrome"];
105
- return lines.some((line) => {
106
- const parts = line.split(/\s+/);
107
- const comm = parts[0] ?? "";
108
- if (!comm || !browserComms.includes(comm)) return false;
109
- const isSubprocess = line.includes("--type=") && !line.includes("--type=browser");
110
- return !isSubprocess;
111
- });
112
- } else if (process.platform === "win32") {
113
- const out = execSync("tasklist", { timeout: 5000 }).toString().toLowerCase();
114
- return ["chrome.exe", "msedge.exe"].some((n) => out.includes(n));
115
- }
116
- } catch {
117
- // best-effort
118
- }
119
- return false;
120
- }
101
+ //
102
+ // Detection moved to src/profile/browser-process.ts, which answers the same
103
+ // "is a browser running?" question per platform and additionally reports the
104
+ // executable path and any explicit --user-data-dir that profile selection needs.
package/src/state.ts CHANGED
@@ -12,6 +12,7 @@ export type BrowserState = {
12
12
  readonly remoteBrowserId?: string;
13
13
  readonly ownedTargetIds?: ReadonlyArray<string>;
14
14
  readonly harnessWindowTargetId?: string;
15
+ readonly harnessWindowId?: number;
15
16
  };
16
17
 
17
18
  export const defaultState = (namespace = "default"): BrowserState => ({