pi-browser-harness 0.3.2 → 0.5.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.
@@ -0,0 +1,213 @@
1
+ /**
2
+ * In-memory aggregator for CDP Network.* events.
3
+ *
4
+ * The CDP event stream is consumed by a single internal loop in session.ts.
5
+ * That loop hands Network.* event params to this module, which joins them into
6
+ * one record per requestId (request → response → finished/failed). Tools then
7
+ * read records via drain(), filtered to what the caller asked for.
8
+ *
9
+ * Bounded by a fixed capacity (default 500): on overflow the oldest record is
10
+ * evicted FIFO. The overflow flag is reported once per drain so tools can warn
11
+ * the LLM that data may be missing.
12
+ *
13
+ * This module is pure (no CDP dependency) so the joining/filtering logic stays
14
+ * trivially testable in isolation.
15
+ */
16
+
17
+ export type NetworkRecord = {
18
+ requestId: string;
19
+ method: string;
20
+ url: string;
21
+ status?: number;
22
+ statusText?: string;
23
+ type: string;
24
+ mimeType?: string;
25
+ /** Wall-clock ms when requestWillBeSent fired, in Date.now() units. */
26
+ requestStartedMs: number;
27
+ durationMs?: number;
28
+ requestBodySize: number;
29
+ responseBodySize?: number;
30
+ failed?: boolean;
31
+ errorText?: string;
32
+ /** Populated by the tool layer via Network.getResponseBody, never by the aggregator. */
33
+ body?: string | null;
34
+ };
35
+
36
+ export type NetworkFilter = {
37
+ /** Substring match by default. Wrap in slashes (`/foo/`) for regex. */
38
+ urlPattern?: string;
39
+ methodFilter?: string[];
40
+ statusFilter?: { min?: number; max?: number };
41
+ resourceTypes?: string[];
42
+ /** Only requests whose start is at most N ms ago. */
43
+ sinceMs?: number;
44
+ /** Cap on the returned records. */
45
+ limit?: number;
46
+ };
47
+
48
+ export type DrainResult = {
49
+ readonly records: NetworkRecord[];
50
+ /** Total matches before `limit` was applied. */
51
+ readonly total: number;
52
+ /** True if the ring buffer evicted at least one record since the last drain. */
53
+ readonly bufferOverflowed: boolean;
54
+ };
55
+
56
+ export type NetworkBuffer = {
57
+ ingestRequestWillBeSent(p: unknown): void;
58
+ ingestResponseReceived(p: unknown): void;
59
+ ingestLoadingFinished(p: unknown): void;
60
+ ingestLoadingFailed(p: unknown): void;
61
+ drain(filter: NetworkFilter): DrainResult;
62
+ clear(): void;
63
+ };
64
+
65
+ const compileUrlMatcher = (pattern: string): ((url: string) => boolean) => {
66
+ if (pattern.length >= 2 && pattern.startsWith("/") && pattern.endsWith("/")) {
67
+ try {
68
+ const re = new RegExp(pattern.slice(1, -1));
69
+ return (url) => re.test(url);
70
+ } catch {
71
+ // Fall through to substring match if the pattern is not a valid regex.
72
+ }
73
+ }
74
+ return (url) => url.includes(pattern);
75
+ };
76
+
77
+ export const createNetworkBuffer = (capacity = 500): NetworkBuffer => {
78
+ // Insertion-ordered map keyed by requestId. Map preserves insertion order in JS,
79
+ // so iteration gives us oldest-first eviction without a separate index.
80
+ const records = new Map<string, NetworkRecord>();
81
+ let overflowed = false;
82
+
83
+ const evictOldestIfFull = (): void => {
84
+ while (records.size >= capacity) {
85
+ const oldest = records.keys().next();
86
+ if (oldest.done) return;
87
+ records.delete(oldest.value);
88
+ overflowed = true;
89
+ }
90
+ };
91
+
92
+ return {
93
+ ingestRequestWillBeSent(p) {
94
+ // CDP boundary cast: Network.requestWillBeSent
95
+ const params = p as
96
+ | {
97
+ requestId?: string;
98
+ request?: {
99
+ url?: string;
100
+ method?: string;
101
+ postData?: string;
102
+ headers?: Record<string, string>;
103
+ };
104
+ type?: string;
105
+ timestamp?: number;
106
+ wallTime?: number;
107
+ }
108
+ | undefined;
109
+ const id = params?.requestId;
110
+ const url = params?.request?.url;
111
+ const method = params?.request?.method;
112
+ if (!id || !url || !method) return;
113
+ // Re-fired requestWillBeSent for the same id (e.g. redirect) — refresh in place.
114
+ records.delete(id);
115
+ evictOldestIfFull();
116
+ const postData = params.request?.postData;
117
+ records.set(id, {
118
+ requestId: id,
119
+ method,
120
+ url,
121
+ type: params.type ?? "Other",
122
+ requestStartedMs: Date.now(),
123
+ requestBodySize: postData ? Buffer.byteLength(postData, "utf8") : 0,
124
+ });
125
+ },
126
+
127
+ ingestResponseReceived(p) {
128
+ // CDP boundary cast: Network.responseReceived
129
+ const params = p as
130
+ | {
131
+ requestId?: string;
132
+ response?: {
133
+ status?: number;
134
+ statusText?: string;
135
+ mimeType?: string;
136
+ };
137
+ type?: string;
138
+ }
139
+ | undefined;
140
+ const id = params?.requestId;
141
+ if (!id) return;
142
+ const r = records.get(id);
143
+ if (!r) return;
144
+ if (params?.response?.status !== undefined) r.status = params.response.status;
145
+ if (params?.response?.statusText !== undefined && params.response.statusText !== "") r.statusText = params.response.statusText;
146
+ if (params?.response?.mimeType !== undefined) r.mimeType = params.response.mimeType;
147
+ // type is more accurate on responseReceived (CDP sometimes refines it)
148
+ if (params?.type !== undefined) r.type = params.type;
149
+ },
150
+
151
+ ingestLoadingFinished(p) {
152
+ // CDP boundary cast: Network.loadingFinished
153
+ const params = p as
154
+ | { requestId?: string; encodedDataLength?: number }
155
+ | undefined;
156
+ const id = params?.requestId;
157
+ if (!id) return;
158
+ const r = records.get(id);
159
+ if (!r) return;
160
+ if (params?.encodedDataLength !== undefined) r.responseBodySize = params.encodedDataLength;
161
+ r.durationMs = Date.now() - r.requestStartedMs;
162
+ },
163
+
164
+ ingestLoadingFailed(p) {
165
+ // CDP boundary cast: Network.loadingFailed
166
+ const params = p as
167
+ | { requestId?: string; errorText?: string; canceled?: boolean }
168
+ | undefined;
169
+ const id = params?.requestId;
170
+ if (!id) return;
171
+ const r = records.get(id);
172
+ if (!r) return;
173
+ r.failed = true;
174
+ if (params?.errorText !== undefined) r.errorText = params.errorText;
175
+ r.durationMs = Date.now() - r.requestStartedMs;
176
+ },
177
+
178
+ drain(filter) {
179
+ const matchUrl = filter.urlPattern !== undefined ? compileUrlMatcher(filter.urlPattern) : undefined;
180
+ const methods = filter.methodFilter ? new Set(filter.methodFilter.map((m) => m.toUpperCase())) : undefined;
181
+ const types = filter.resourceTypes ? new Set(filter.resourceTypes.map((t) => t.toLowerCase())) : undefined;
182
+ const minStatus = filter.statusFilter?.min;
183
+ const maxStatus = filter.statusFilter?.max;
184
+ const cutoff = filter.sinceMs !== undefined ? Date.now() - filter.sinceMs : undefined;
185
+
186
+ const matched: NetworkRecord[] = [];
187
+ for (const r of records.values()) {
188
+ if (matchUrl && !matchUrl(r.url)) continue;
189
+ if (methods && !methods.has(r.method.toUpperCase())) continue;
190
+ if (types && !types.has(r.type.toLowerCase())) continue;
191
+ if (cutoff !== undefined && r.requestStartedMs < cutoff) continue;
192
+ if (minStatus !== undefined && (r.status === undefined || r.status < minStatus)) continue;
193
+ if (maxStatus !== undefined && (r.status === undefined || r.status > maxStatus)) continue;
194
+ // Clone so callers can mutate (e.g. attach body) without polluting the buffer.
195
+ matched.push({ ...r });
196
+ }
197
+
198
+ const total = matched.length;
199
+ const limit = Math.min(filter.limit ?? 50, 500);
200
+ const limited = matched.slice(-limit); // most recent N
201
+
202
+ const bufferOverflowed = overflowed;
203
+ overflowed = false;
204
+
205
+ return { records: limited, total, bufferOverflowed };
206
+ },
207
+
208
+ clear() {
209
+ records.clear();
210
+ overflowed = false;
211
+ },
212
+ };
213
+ };
@@ -0,0 +1,72 @@
1
+ /**
2
+ * Tracks which tab targetIds belong to this harness session.
3
+ *
4
+ * The harness attaches to the user's running Chrome, which is also full of
5
+ * the user's own tabs. Without an ownership boundary the agent could attach
6
+ * to (or close) personal tabs by accident. The registry is the single source
7
+ * of truth for "did this session open that tab?".
8
+ *
9
+ * Mutations notify a single change listener so the orchestrator can persist
10
+ * to disk via the existing BrowserState flow without the registry needing to
11
+ * know about the pi storage API.
12
+ */
13
+
14
+ export type OwnershipRegistry = {
15
+ add(targetId: string): void;
16
+ remove(targetId: string): void;
17
+ has(targetId: string): boolean;
18
+ list(): ReadonlyArray<string>;
19
+ /** Replace the full set — used on restoreState to re-hydrate from disk. */
20
+ replaceAll(ids: ReadonlyArray<string>): void;
21
+ setHarnessWindow(targetId: string | undefined): void;
22
+ harnessWindow(): string | undefined;
23
+ onChange(cb: () => void): void;
24
+ };
25
+
26
+ export const createOwnershipRegistry = (
27
+ initial?: { readonly ownedTargetIds?: ReadonlyArray<string>; readonly harnessWindowTargetId?: string },
28
+ ): OwnershipRegistry => {
29
+ const owned = new Set<string>(initial?.ownedTargetIds ?? []);
30
+ let harnessWindow: string | undefined = initial?.harnessWindowTargetId;
31
+ let listener: (() => void) | null = null;
32
+
33
+ const notify = (): void => {
34
+ if (listener) listener();
35
+ };
36
+
37
+ return {
38
+ add(targetId) {
39
+ if (owned.has(targetId)) return;
40
+ owned.add(targetId);
41
+ notify();
42
+ },
43
+ remove(targetId) {
44
+ if (!owned.has(targetId) && harnessWindow !== targetId) return;
45
+ owned.delete(targetId);
46
+ if (harnessWindow === targetId) harnessWindow = undefined;
47
+ notify();
48
+ },
49
+ has(targetId) {
50
+ return owned.has(targetId);
51
+ },
52
+ list() {
53
+ return [...owned];
54
+ },
55
+ replaceAll(ids) {
56
+ owned.clear();
57
+ for (const id of ids) owned.add(id);
58
+ notify();
59
+ },
60
+ setHarnessWindow(targetId) {
61
+ if (harnessWindow === targetId) return;
62
+ harnessWindow = targetId;
63
+ notify();
64
+ },
65
+ harnessWindow() {
66
+ return harnessWindow;
67
+ },
68
+ onChange(cb) {
69
+ listener = cb;
70
+ },
71
+ };
72
+ };
@@ -1,8 +1,23 @@
1
- import { type Result, err, ok } from "../util/result";
2
- import { type CdpError, cdpError } from "./errors";
1
+ import { type Result, ok } from "../util/result";
2
+ import type { CdpError } from "./errors";
3
3
  import type { DialogInfo } from "./types";
4
- import { isInternalUrl } from "./types";
4
+ import type { OwnershipRegistry } from "./ownership";
5
5
  import type { CdpTransport } from "./transport";
6
+ import { createNetworkBuffer, type DrainResult, type NetworkFilter } from "./network-buffer";
7
+ import { createConsoleBuffer, type ConsoleDrainResult, type ConsoleFilter } from "./console-buffer";
8
+
9
+ // Per-tab state. One TabSession exists per known targetId.
10
+ // Dialog, page-info dirty flag, and CDP buffers are per-tab — switching
11
+ // tabs preserves the previous tab's data in its TabSession instead of
12
+ // clearing it.
13
+ type TabSession = {
14
+ sessionId: string;
15
+ targetId: string;
16
+ dialog: DialogInfo | null;
17
+ pageInfoDirty: boolean;
18
+ networkBuffer: ReturnType<typeof createNetworkBuffer>;
19
+ consoleBuffer: ReturnType<typeof createConsoleBuffer>;
20
+ };
6
21
 
7
22
  export type CdpSession = {
8
23
  attachFirstPage(): Promise<Result<{ readonly targetId: string; readonly sessionId: string }, CdpError>>;
@@ -13,21 +28,43 @@ export type CdpSession = {
13
28
  callBrowser(method: string, params?: Record<string, unknown>, opts?: { timeoutMs?: number }): Promise<Result<unknown, CdpError>>;
14
29
  takeDialog(): DialogInfo | null;
15
30
  drainPageInfoInvalidations(): boolean;
31
+ drainNetworkBuffer(filter: NetworkFilter): DrainResult;
32
+ drainConsoleBuffer(filter: ConsoleFilter): ConsoleDrainResult;
16
33
  };
17
34
 
18
- export const createCdpSession = (transport: CdpTransport): CdpSession => {
35
+ export const createCdpSession = (
36
+ transport: CdpTransport,
37
+ ownership?: OwnershipRegistry,
38
+ ): CdpSession => {
19
39
  let sessionId: string | null = null;
20
40
  let targetId: string | null = null;
21
- let dialog: DialogInfo | null = null;
22
- let pageInfoDirty = false;
41
+
42
+ // Per-tab tracking: maps targetId → TabSession and sessionId → targetId for
43
+ // event routing. Lazy: a TabSession is created on the first visit to a tab.
44
+ const tabs = new Map<string, TabSession>();
45
+ const sessionIdToTargetId = new Map<string, string>();
23
46
 
24
47
  let activeConsumer: Promise<void> = Promise.resolve();
25
48
 
49
+ // Event → TabSession resolver. Uses sessionId from CDP events to find the
50
+ // correct TabSession, falling back to the current targetId when no sessionId.
51
+ const resolveTab = (evSessionId?: string): TabSession | undefined => {
52
+ const tid = evSessionId ? sessionIdToTargetId.get(evSessionId) : targetId;
53
+ return tid ? tabs.get(tid) : undefined;
54
+ };
55
+
26
56
  const consumeEvents = async (): Promise<void> => {
27
57
  for await (const ev of transport.events()) {
58
+ // Filter: skip events from sessions we're not currently tracking.
59
+ // Target.targetDestroyed is browser-level (no sessionId) — always process.
60
+ if (ev.method !== "Target.targetDestroyed") {
61
+ if (ev.sessionId && ev.sessionId !== sessionId) continue;
62
+ }
28
63
  if (ev.method === "Page.javascriptDialogOpening") {
64
+ const tab = targetId ? tabs.get(targetId) : undefined;
65
+ if (!tab) continue;
29
66
  const params = ev.params as Partial<DialogInfo> | undefined;
30
- dialog = {
67
+ tab.dialog = {
31
68
  type: (params?.type as DialogInfo["type"]) ?? "alert",
32
69
  message: params?.message ?? "",
33
70
  ...(params?.defaultPrompt !== undefined ? { defaultPrompt: params.defaultPrompt } : {}),
@@ -39,7 +76,39 @@ export const createCdpSession = (transport: CdpTransport): CdpSession => {
39
76
  // This prevents fast dismiss flows from dropping a dialog the agent
40
77
  // was about to read. (Fix for spec §7 predictability bug #2.)
41
78
  if (ev.method === "Page.frameNavigated" || ev.method === "Page.loadEventFired") {
42
- pageInfoDirty = true;
79
+ const tab = targetId ? tabs.get(targetId) : undefined;
80
+ if (tab) tab.pageInfoDirty = true;
81
+ }
82
+ if (ev.method === "Target.targetDestroyed" && ownership) {
83
+ const params = ev.params as { targetId?: string } | undefined;
84
+ if (params?.targetId) {
85
+ ownership.remove(params.targetId);
86
+ // Prune per-tab state for the destroyed target
87
+ const tab = tabs.get(params.targetId);
88
+ if (tab) {
89
+ sessionIdToTargetId.delete(tab.sessionId);
90
+ tabs.delete(params.targetId);
91
+ }
92
+ }
93
+ }
94
+ if (ev.method === "Network.requestWillBeSent") {
95
+ const tab = resolveTab(ev.sessionId);
96
+ if (tab) tab.networkBuffer.ingestRequestWillBeSent(ev.params);
97
+ } else if (ev.method === "Network.responseReceived") {
98
+ const tab = resolveTab(ev.sessionId);
99
+ if (tab) tab.networkBuffer.ingestResponseReceived(ev.params);
100
+ } else if (ev.method === "Network.loadingFinished") {
101
+ const tab = resolveTab(ev.sessionId);
102
+ if (tab) tab.networkBuffer.ingestLoadingFinished(ev.params);
103
+ } else if (ev.method === "Network.loadingFailed") {
104
+ const tab = resolveTab(ev.sessionId);
105
+ if (tab) tab.networkBuffer.ingestLoadingFailed(ev.params);
106
+ } else if (ev.method === "Runtime.consoleAPICalled") {
107
+ const tab = resolveTab(ev.sessionId);
108
+ if (tab) tab.consoleBuffer.ingestConsoleApi(ev.params);
109
+ } else if (ev.method === "Log.entryAdded") {
110
+ const tab = resolveTab(ev.sessionId);
111
+ if (tab) tab.consoleBuffer.ingestLogEntry(ev.params);
43
112
  }
44
113
  }
45
114
  };
@@ -58,7 +127,8 @@ export const createCdpSession = (transport: CdpTransport): CdpSession => {
58
127
  transport.onClose(() => {
59
128
  sessionId = null;
60
129
  targetId = null;
61
- pageInfoDirty = false;
130
+ tabs.clear();
131
+ sessionIdToTargetId.clear();
62
132
  // Do NOT clear `dialog` here — same rationale as inside consumeEvents:
63
133
  // the agent may have a pending takeDialog() call that should still see it.
64
134
  restartConsumer();
@@ -68,7 +138,7 @@ export const createCdpSession = (transport: CdpTransport): CdpSession => {
68
138
  // Switching to Promise.all over a single WS pipelines the round-trips and
69
139
  // saves ~3× on tab-switch latency. Defer until session.ts has tests.
70
140
  const enableDomains = async (sid: string): Promise<void> => {
71
- for (const d of ["Page", "DOM", "Runtime", "Network"]) {
141
+ for (const d of ["Page", "DOM", "Runtime", "Network", "Accessibility", "Log"]) {
72
142
  await transport.request(`${d}.enable`, {}, { sessionId: sid });
73
143
  }
74
144
  };
@@ -81,25 +151,69 @@ export const createCdpSession = (transport: CdpTransport): CdpSession => {
81
151
  // as a clear Error in normal use.
82
152
  return {
83
153
  async attachFirstPage() {
154
+ // Subscribe to Target.* events so we can react to targetDestroyed.
155
+ // Best-effort: failing to enable discovery is not fatal for attach.
156
+ await transport.request("Target.setDiscoverTargets", { discover: true }, { sessionId: null });
157
+
84
158
  const targets = await transport.request("Target.getTargets", {}, { sessionId: null });
85
159
  if (!targets.success) return targets;
86
160
  const data = targets.data as { targetInfos: ReadonlyArray<{ targetId: string; type: string; url: string }> };
87
- let pages = data.targetInfos.filter((t) => t.type === "page" && !isInternalUrl(t.url));
88
- if (pages.length === 0) {
89
- const created = await transport.request("Target.createTarget", { url: "about:blank" }, { sessionId: null });
161
+ const allPages = data.targetInfos.filter((t) => t.type === "page");
162
+ // Reconcile the persisted ownership set against live targets — drop dead IDs.
163
+ if (ownership) {
164
+ const live = new Set(allPages.map((p) => p.targetId));
165
+ const survivors = ownership.list().filter((id) => live.has(id));
166
+ if (survivors.length !== ownership.list().length) ownership.replaceAll(survivors);
167
+ const hw = ownership.harnessWindow();
168
+ if (hw && !live.has(hw)) ownership.setHarnessWindow(undefined);
169
+ }
170
+ // Prune per-tab state for pages that no longer exist
171
+ const liveTargetIds = new Set(allPages.map((p) => p.targetId));
172
+ for (const tid of tabs.keys()) {
173
+ if (!liveTargetIds.has(tid)) {
174
+ const tab = tabs.get(tid);
175
+ if (tab) sessionIdToTargetId.delete(tab.sessionId);
176
+ tabs.delete(tid);
177
+ }
178
+ }
179
+
180
+ // Prefer attaching to a tab this session already owns. Falls back to
181
+ // creating a fresh harness-owned tab in a dedicated window — never
182
+ // grabs the user's foreground tab.
183
+ let pickTargetId: string | undefined;
184
+ if (ownership) {
185
+ const ownedLive = ownership.list().filter((id) => allPages.some((p) => p.targetId === id));
186
+ pickTargetId = ownedLive[0];
187
+ }
188
+ if (!pickTargetId) {
189
+ const createParams: Record<string, unknown> = { url: "about:blank" };
190
+ if (ownership) createParams["newWindow"] = true;
191
+ const created = await transport.request("Target.createTarget", createParams, { sessionId: null });
90
192
  if (!created.success) return created;
91
193
  const c = created.data as { targetId: string };
92
- pages = [{ targetId: c.targetId, type: "page", url: "about:blank" }];
194
+ pickTargetId = c.targetId;
195
+ if (ownership) {
196
+ ownership.setHarnessWindow(c.targetId);
197
+ ownership.add(c.targetId);
198
+ }
93
199
  }
94
- const first = pages[0];
95
- if (!first) return err(cdpError("invalid_response", "no page targets after creation"));
96
- const attached = await transport.request("Target.attachToTarget", { targetId: first.targetId, flatten: true }, { sessionId: null });
200
+
201
+ const attached = await transport.request("Target.attachToTarget", { targetId: pickTargetId, flatten: true }, { sessionId: null });
97
202
  if (!attached.success) return attached;
98
203
  const a = attached.data as { sessionId: string };
99
204
  sessionId = a.sessionId;
100
- targetId = first.targetId;
205
+ targetId = pickTargetId;
101
206
  await enableDomains(a.sessionId);
102
- return ok({ targetId: first.targetId, sessionId: a.sessionId });
207
+ tabs.set(pickTargetId, {
208
+ sessionId: a.sessionId,
209
+ targetId: pickTargetId,
210
+ dialog: null,
211
+ pageInfoDirty: false,
212
+ networkBuffer: createNetworkBuffer(),
213
+ consoleBuffer: createConsoleBuffer(),
214
+ });
215
+ sessionIdToTargetId.set(a.sessionId, pickTargetId);
216
+ return ok({ targetId: pickTargetId, sessionId: a.sessionId });
103
217
  },
104
218
  async switchTo(tid) {
105
219
  const activated = await transport.request("Target.activateTarget", { targetId: tid }, { sessionId: null });
@@ -107,10 +221,29 @@ export const createCdpSession = (transport: CdpTransport): CdpSession => {
107
221
  const attached = await transport.request("Target.attachToTarget", { targetId: tid, flatten: true }, { sessionId: null });
108
222
  if (!attached.success) return attached;
109
223
  const a = attached.data as { sessionId: string };
110
- sessionId = a.sessionId;
224
+ // Reuse existing TabSession or create one on first visit
225
+ const existing = tabs.get(tid);
226
+ const tab: TabSession = existing ?? {
227
+ sessionId: a.sessionId,
228
+ targetId: tid,
229
+ dialog: null,
230
+ pageInfoDirty: true,
231
+ networkBuffer: createNetworkBuffer(),
232
+ consoleBuffer: createConsoleBuffer(),
233
+ };
234
+ if (existing) {
235
+ // Each Target.attachToTarget produces a new sessionId — update it.
236
+ sessionIdToTargetId.delete(existing.sessionId);
237
+ existing.sessionId = a.sessionId;
238
+ sessionIdToTargetId.set(a.sessionId, tid);
239
+ } else {
240
+ tabs.set(tid, tab);
241
+ await enableDomains(a.sessionId);
242
+ sessionIdToTargetId.set(a.sessionId, tid);
243
+ }
244
+ // Update global pointers to point at the new active tab
245
+ sessionId = tab.sessionId;
111
246
  targetId = tid;
112
- pageInfoDirty = true;
113
- await enableDomains(a.sessionId);
114
247
  return ok(undefined);
115
248
  },
116
249
  current() {
@@ -126,14 +259,28 @@ export const createCdpSession = (transport: CdpTransport): CdpSession => {
126
259
  return transport.request(method, params, { sessionId: null, ...(opts.timeoutMs !== undefined ? { timeoutMs: opts.timeoutMs } : {}) });
127
260
  },
128
261
  takeDialog() {
129
- const d = dialog;
130
- dialog = null;
262
+ const tab = targetId ? tabs.get(targetId) : undefined;
263
+ if (!tab) return null;
264
+ const d = tab.dialog;
265
+ tab.dialog = null;
131
266
  return d;
132
267
  },
133
268
  drainPageInfoInvalidations() {
134
- const dirty = pageInfoDirty;
135
- pageInfoDirty = false;
269
+ const tab = targetId ? tabs.get(targetId) : undefined;
270
+ if (!tab) return false;
271
+ const dirty = tab.pageInfoDirty;
272
+ tab.pageInfoDirty = false;
136
273
  return dirty;
137
274
  },
275
+ drainNetworkBuffer(filter) {
276
+ const tab = targetId ? tabs.get(targetId) : undefined;
277
+ if (!tab) return { records: [], total: 0, bufferOverflowed: false };
278
+ return tab.networkBuffer.drain(filter);
279
+ },
280
+ drainConsoleBuffer(filter) {
281
+ const tab = targetId ? tabs.get(targetId) : undefined;
282
+ if (!tab) return { records: [], total: 0, bufferOverflowed: false };
283
+ return tab.consoleBuffer.drain(filter);
284
+ },
138
285
  };
139
286
  };
package/src/cdp/types.ts CHANGED
@@ -23,6 +23,7 @@ export type TabInfo = {
23
23
  readonly targetId: string;
24
24
  readonly title: string;
25
25
  readonly url: string;
26
+ readonly owned?: boolean;
26
27
  };
27
28
 
28
29
  export type PageInfo = {