@youtyan/browser-pilot 0.7.1

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,2332 @@
1
+ // apps/extension/src/background/tab-utils.ts
2
+ function waitForTabLoad(tabId, timeoutMs = 15000) {
3
+ return new Promise((resolve, reject) => {
4
+ const timeout = setTimeout(() => {
5
+ chrome.tabs.onUpdated.removeListener(listener);
6
+ reject(new Error("Page load timeout"));
7
+ }, timeoutMs);
8
+ function listener(updatedTabId, changeInfo) {
9
+ if (updatedTabId === tabId && changeInfo.status === "complete") {
10
+ clearTimeout(timeout);
11
+ chrome.tabs.onUpdated.removeListener(listener);
12
+ setTimeout(resolve, 500);
13
+ }
14
+ }
15
+ chrome.tabs.onUpdated.addListener(listener);
16
+ });
17
+ }
18
+ function reinjectContentScripts() {
19
+ chrome.tabs.query({}, (tabs) => {
20
+ for (const tab of tabs) {
21
+ if (tab.id && tab.url && (tab.url.startsWith("http://") || tab.url.startsWith("https://"))) {
22
+ chrome.scripting.executeScript({
23
+ target: { tabId: tab.id },
24
+ files: ["content.js"]
25
+ }).catch(() => {});
26
+ }
27
+ }
28
+ });
29
+ }
30
+
31
+ // apps/extension/src/background/download.ts
32
+ function waitForDownload(timeoutMs) {
33
+ return new Promise((resolve, reject) => {
34
+ let downloadId = null;
35
+ const timeout = setTimeout(() => {
36
+ chrome.downloads.onCreated.removeListener(onCreated);
37
+ chrome.downloads.onChanged.removeListener(onChanged);
38
+ reject(new Error(`Download timeout after ${timeoutMs}ms`));
39
+ }, timeoutMs);
40
+ function cleanup() {
41
+ clearTimeout(timeout);
42
+ chrome.downloads.onCreated.removeListener(onCreated);
43
+ chrome.downloads.onChanged.removeListener(onChanged);
44
+ }
45
+ function onCreated(item) {
46
+ downloadId = item.id;
47
+ }
48
+ function onChanged(delta) {
49
+ if (downloadId === null && delta.state) {
50
+ downloadId = delta.id;
51
+ }
52
+ if (delta.id !== downloadId)
53
+ return;
54
+ if (delta.state?.current === "complete") {
55
+ cleanup();
56
+ chrome.downloads.search({ id: downloadId }, (results) => {
57
+ const item = results[0];
58
+ resolve({
59
+ data: {
60
+ completed: true,
61
+ id: item?.id,
62
+ filename: item?.filename,
63
+ fileSize: item?.fileSize,
64
+ mime: item?.mime,
65
+ url: item?.url
66
+ }
67
+ });
68
+ });
69
+ } else if (delta.state?.current === "interrupted") {
70
+ cleanup();
71
+ chrome.downloads.search({ id: downloadId }, (results) => {
72
+ const item = results[0];
73
+ reject(new Error(`Download interrupted: ${item?.error ?? "unknown"}`));
74
+ });
75
+ }
76
+ }
77
+ chrome.downloads.onCreated.addListener(onCreated);
78
+ chrome.downloads.onChanged.addListener(onChanged);
79
+ });
80
+ }
81
+
82
+ // apps/extension/src/background/event-buffer.ts
83
+ class EventBuffer {
84
+ maxSize;
85
+ buffer = [];
86
+ dropped = 0;
87
+ constructor(maxSize) {
88
+ this.maxSize = maxSize;
89
+ }
90
+ push(item) {
91
+ if (this.buffer.length >= this.maxSize) {
92
+ this.buffer.shift();
93
+ this.dropped++;
94
+ }
95
+ this.buffer.push(item);
96
+ }
97
+ drain(filter) {
98
+ const dropped = this.dropped;
99
+ const items = filter ? this.buffer.filter(filter) : [...this.buffer];
100
+ this.buffer = [];
101
+ this.dropped = 0;
102
+ return { items, dropped };
103
+ }
104
+ peek(filter) {
105
+ return filter ? this.buffer.filter(filter) : [...this.buffer];
106
+ }
107
+ peekWithDropped(filter) {
108
+ const items = filter ? this.buffer.filter(filter) : [...this.buffer];
109
+ return { items, dropped: this.dropped };
110
+ }
111
+ clear() {
112
+ this.buffer = [];
113
+ this.dropped = 0;
114
+ }
115
+ }
116
+
117
+ // apps/extension/src/background/debugger.ts
118
+ var screencastFrameHandler = null;
119
+ function setScreencastFrameHandler(handler) {
120
+ screencastFrameHandler = handler;
121
+ }
122
+ var BUFFER_MAX = 1000;
123
+ var PENDING_REQUEST_TTL_MS = 60000;
124
+ var DETAIL_RETAIN_MAX = 500;
125
+ var EXTRA_INFO_MAX = 200;
126
+ var NAVIGATION_HISTORY_MAX = 3;
127
+ var tabStates = new Map;
128
+ var navigationCounter = 0;
129
+ function getState(tabId) {
130
+ return tabStates.get(tabId);
131
+ }
132
+ function createState(tabId) {
133
+ const state = {
134
+ consoleBuffer: new EventBuffer(BUFFER_MAX),
135
+ networkBuffer: new EventBuffer(BUFFER_MAX),
136
+ exceptionBuffer: new EventBuffer(BUFFER_MAX),
137
+ pendingRequests: new Map,
138
+ completedRequests: new Map,
139
+ pendingExtraInfo: new Map,
140
+ mainFrameId: null,
141
+ currentNavigation: null,
142
+ navigationHistory: [],
143
+ orphanCleanupTimer: null,
144
+ nextMsgId: 1,
145
+ scriptMetaCache: new Map,
146
+ staleSourceMapUrls: [],
147
+ traceState: null,
148
+ cssEnabled: false,
149
+ attachedAt: Date.now()
150
+ };
151
+ tabStates.set(tabId, state);
152
+ return state;
153
+ }
154
+ function rotateNavigation(state, tabId, url, type) {
155
+ if (state.currentNavigation) {
156
+ const snapshot = {
157
+ navigation: state.currentNavigation,
158
+ requests: state.networkBuffer.peek().map((r) => ({
159
+ url: r.url,
160
+ method: r.method,
161
+ status: r.status,
162
+ navigationId: r.navigationId
163
+ })),
164
+ consoleEntries: state.consoleBuffer.peek().map((m) => ({
165
+ msgId: m.msgId,
166
+ level: m.level,
167
+ text: m.text
168
+ }))
169
+ };
170
+ state.navigationHistory.push(snapshot);
171
+ if (state.navigationHistory.length > NAVIGATION_HISTORY_MAX)
172
+ state.navigationHistory.shift();
173
+ }
174
+ state.currentNavigation = {
175
+ navigationId: `nav-${tabId}-${++navigationCounter}`,
176
+ url,
177
+ timestamp: Date.now(),
178
+ type
179
+ };
180
+ if (type === "full") {
181
+ state.scriptMetaCache.clear();
182
+ state.staleSourceMapUrls = [];
183
+ }
184
+ }
185
+ function evictOldestExtraInfo(state) {
186
+ if (state.pendingExtraInfo.size > EXTRA_INFO_MAX) {
187
+ const firstKey = state.pendingExtraInfo.keys().next().value;
188
+ if (firstKey)
189
+ state.pendingExtraInfo.delete(firstKey);
190
+ }
191
+ }
192
+ function removeState(tabId) {
193
+ tabStates.delete(tabId);
194
+ }
195
+ function estimateBodySafe(encodedDataLength, responseHeaders) {
196
+ const MAX_INLINE_SIZE = 1048576;
197
+ const rawCL = responseHeaders?.["content-length"] ? parseInt(responseHeaders["content-length"], 10) : undefined;
198
+ const contentLength = rawCL != null && !Number.isNaN(rawCL) ? rawCL : undefined;
199
+ const encoding = responseHeaders?.["content-encoding"];
200
+ let estimatedSize = encodedDataLength;
201
+ if (encoding && encoding !== "identity") {
202
+ estimatedSize = contentLength ?? encodedDataLength * 10;
203
+ } else if (contentLength) {
204
+ estimatedSize = contentLength;
205
+ }
206
+ return { safe: estimatedSize <= MAX_INLINE_SIZE, estimatedSize };
207
+ }
208
+ async function freezeRequestDetail(tabId, req, encodedDataLength) {
209
+ const { safe } = estimateBodySafe(encodedDataLength, req.responseHeaders);
210
+ let requestBody = null;
211
+ let responseBody = null;
212
+ let base64Encoded = false;
213
+ let bodyTruncated = false;
214
+ if (req.postDataLength) {
215
+ try {
216
+ const result = await chrome.debugger.sendCommand({ tabId }, "Network.getRequestPostData", { requestId: req.requestId });
217
+ requestBody = result.postData ?? null;
218
+ } catch {}
219
+ }
220
+ if (safe) {
221
+ try {
222
+ const result = await chrome.debugger.sendCommand({ tabId }, "Network.getResponseBody", { requestId: req.requestId });
223
+ const body = result;
224
+ responseBody = body.body ?? null;
225
+ base64Encoded = body.base64Encoded ?? false;
226
+ } catch {
227
+ bodyTruncated = true;
228
+ }
229
+ } else {
230
+ bodyTruncated = true;
231
+ }
232
+ req.frozen = { requestBody, responseBody, base64Encoded, bodyTruncated };
233
+ }
234
+ async function attachDebugger(tabId) {
235
+ if (tabStates.has(tabId))
236
+ return;
237
+ await chrome.debugger.attach({ tabId }, "1.3");
238
+ createState(tabId);
239
+ try {
240
+ await chrome.debugger.sendCommand({ tabId }, "Runtime.enable");
241
+ await chrome.debugger.sendCommand({ tabId }, "Network.enable", {
242
+ maxPostDataSize: 65536,
243
+ maxTotalBufferSize: 10485760,
244
+ maxResourceBufferSize: 5242880
245
+ });
246
+ await chrome.debugger.sendCommand({ tabId }, "Performance.enable");
247
+ await chrome.debugger.sendCommand({ tabId }, "Page.enable");
248
+ await chrome.debugger.sendCommand({ tabId }, "Debugger.enable");
249
+ await chrome.debugger.sendCommand({ tabId }, "Debugger.setSkipAllPauses", { skip: true });
250
+ await chrome.debugger.sendCommand({ tabId }, "Accessibility.enable");
251
+ const state = tabStates.get(tabId);
252
+ const frameTreeResult = await chrome.debugger.sendCommand({ tabId }, "Page.getFrameTree");
253
+ const frameTree = frameTreeResult.frameTree;
254
+ state.mainFrameId = frameTree.frame.id;
255
+ state.currentNavigation = {
256
+ navigationId: `nav-${tabId}-${++navigationCounter}`,
257
+ url: frameTree.frame.url,
258
+ timestamp: Date.now(),
259
+ type: "full"
260
+ };
261
+ state.orphanCleanupTimer = setInterval(() => {
262
+ const cutoff = Date.now() - 30000;
263
+ for (const [id, info] of state.pendingExtraInfo) {
264
+ if (info.timestamp < cutoff)
265
+ state.pendingExtraInfo.delete(id);
266
+ }
267
+ }, 1e4);
268
+ } catch (e) {
269
+ try {
270
+ await chrome.debugger.detach({ tabId });
271
+ } catch {}
272
+ removeState(tabId);
273
+ throw e;
274
+ }
275
+ }
276
+ async function detachDebugger(tabId) {
277
+ if (!tabStates.has(tabId))
278
+ return;
279
+ const state = getState(tabId);
280
+ if (state?.orphanCleanupTimer) {
281
+ clearInterval(state.orphanCleanupTimer);
282
+ }
283
+ try {
284
+ await chrome.debugger.detach({ tabId });
285
+ } catch {}
286
+ removeState(tabId);
287
+ }
288
+ function onEvent(source, method, params) {
289
+ try {
290
+ const tabId = source.tabId;
291
+ if (tabId == null)
292
+ return;
293
+ const p = params;
294
+ if (method === "Page.screencastFrame" && p) {
295
+ if (screencastFrameHandler)
296
+ screencastFrameHandler(tabId, p);
297
+ return;
298
+ }
299
+ const state = getState(tabId);
300
+ if (!state)
301
+ return;
302
+ if (method === "Tracing.dataCollected" && p) {
303
+ if (state.traceState?.recording) {
304
+ const value = p.value;
305
+ if (Array.isArray(value)) {
306
+ state.traceState.events.push(...value);
307
+ }
308
+ }
309
+ return;
310
+ }
311
+ if (method === "Tracing.tracingComplete") {
312
+ if (state.traceState?.resolve) {
313
+ state.traceState.resolve(state.traceState.events);
314
+ state.traceState = null;
315
+ }
316
+ return;
317
+ }
318
+ if (method === "Page.frameNavigated" && p) {
319
+ const frame = p.frame;
320
+ if (frame && !frame.parentId) {
321
+ state.mainFrameId = frame.id;
322
+ rotateNavigation(state, tabId, frame.url, "full");
323
+ }
324
+ return;
325
+ }
326
+ if (method === "Page.navigatedWithinDocument" && p) {
327
+ const frameId = p.frameId;
328
+ const url = p.url;
329
+ if (frameId && url && frameId === state.mainFrameId) {
330
+ rotateNavigation(state, tabId, url, "same-document");
331
+ }
332
+ return;
333
+ }
334
+ if (method === "Debugger.scriptParsed" && p) {
335
+ const scriptUrl = p.url;
336
+ const scriptId = p.scriptId;
337
+ if (!scriptUrl)
338
+ return;
339
+ const existingForUrl = [...state.scriptMetaCache.values()].find((m) => m.url === scriptUrl && m.scriptId !== scriptId);
340
+ if (existingForUrl) {
341
+ state.scriptMetaCache.delete(existingForUrl.scriptId);
342
+ state.staleSourceMapUrls.push(scriptUrl);
343
+ }
344
+ state.scriptMetaCache.set(scriptId, {
345
+ scriptId,
346
+ url: scriptUrl,
347
+ sourceMapURL: p.sourceMapURL ?? null,
348
+ startLine: p.startLine ?? 0,
349
+ startColumn: p.startColumn ?? 0
350
+ });
351
+ return;
352
+ }
353
+ if (method === "Runtime.consoleAPICalled" && p) {
354
+ const rawArgs = p.args ?? [];
355
+ const text = rawArgs.map((a) => {
356
+ if (a.type === "object" && a.preview?.properties) {
357
+ const entries = a.preview.properties.map((prop) => `${prop.name}: ${prop.value}`).join(", ");
358
+ return `{${entries}}`;
359
+ }
360
+ return a.description ?? String(a.value ?? "");
361
+ }).join(" ");
362
+ const consoleArgs = rawArgs.map((a) => ({
363
+ type: a.type,
364
+ subtype: a.subtype,
365
+ value: a.value,
366
+ description: a.description,
367
+ objectId: a.objectId
368
+ }));
369
+ const rawStack = p.stackTrace?.callFrames;
370
+ state.consoleBuffer.push({
371
+ msgId: state.nextMsgId++,
372
+ level: p.type ?? "log",
373
+ text,
374
+ url: rawStack?.[0]?.url,
375
+ lineNumber: rawStack?.[0]?.lineNumber,
376
+ columnNumber: rawStack?.[0]?.columnNumber,
377
+ scriptId: rawStack?.[0]?.scriptId,
378
+ timestamp: Date.now(),
379
+ navigationId: state.currentNavigation?.navigationId,
380
+ args: consoleArgs,
381
+ stackTrace: rawStack
382
+ });
383
+ }
384
+ if (method === "Runtime.exceptionThrown" && p) {
385
+ const exDetails = p.exceptionDetails;
386
+ if (exDetails) {
387
+ const description = exDetails.exception?.description ?? exDetails.text ?? "Unknown exception";
388
+ state.exceptionBuffer.push({
389
+ exceptionId: exDetails.exceptionId ?? 0,
390
+ text: description,
391
+ timestamp: Date.now(),
392
+ navigationId: state.currentNavigation?.navigationId,
393
+ revoked: false,
394
+ stackTrace: exDetails.stackTrace?.callFrames
395
+ });
396
+ }
397
+ }
398
+ if (method === "Runtime.exceptionRevoked" && p) {
399
+ const exceptionId = p.exceptionId;
400
+ const reason = p.reason ?? undefined;
401
+ if (exceptionId != null) {
402
+ for (const ex of state.exceptionBuffer.peek()) {
403
+ if (ex.exceptionId === exceptionId) {
404
+ ex.revoked = true;
405
+ ex.revokeReason = reason;
406
+ break;
407
+ }
408
+ }
409
+ }
410
+ }
411
+ if (method === "Network.requestWillBeSent" && p) {
412
+ const request = p.request;
413
+ if (!request)
414
+ return;
415
+ const requestId = p.requestId;
416
+ const redirectResponse = p.redirectResponse;
417
+ const existingPending = state.pendingRequests.get(requestId);
418
+ let redirectHops;
419
+ if (redirectResponse && existingPending) {
420
+ const hop = {
421
+ url: existingPending.url,
422
+ method: existingPending.method,
423
+ status: redirectResponse.status,
424
+ startTime: existingPending.startTime,
425
+ endTime: Date.now()
426
+ };
427
+ redirectHops = [...existingPending.redirectHops ?? [], hop];
428
+ }
429
+ if (redirectResponse) {
430
+ state.pendingExtraInfo.delete(requestId);
431
+ }
432
+ const req = {
433
+ requestId,
434
+ url: request.url,
435
+ method: request.method,
436
+ type: p.type,
437
+ startTime: Date.now(),
438
+ navigationId: state.currentNavigation?.navigationId
439
+ };
440
+ if (redirectHops)
441
+ req.redirectHops = redirectHops;
442
+ const initiator = p.initiator;
443
+ if (initiator?.type) {
444
+ let iUrl = initiator.url;
445
+ let iLine = initiator.lineNumber;
446
+ if (!iUrl && initiator.stack?.callFrames?.[0]) {
447
+ const frame = initiator.stack.callFrames[0];
448
+ iUrl = frame.url || undefined;
449
+ iLine = frame.lineNumber;
450
+ }
451
+ req.initiator = {
452
+ type: initiator.type,
453
+ ...iUrl && { url: iUrl },
454
+ ...iLine != null && { lineNumber: iLine }
455
+ };
456
+ }
457
+ const reqData = request;
458
+ if (reqData.hasPostData) {
459
+ req.postDataLength = reqData.postDataLength ?? 1;
460
+ }
461
+ const headers = reqData.headers;
462
+ if (headers)
463
+ req.requestHeaders = headers;
464
+ const buffered = state.pendingExtraInfo.get(req.requestId);
465
+ if (buffered) {
466
+ if (buffered.requestHeaders)
467
+ req.requestHeaders = buffered.requestHeaders;
468
+ if (buffered.associatedCookies)
469
+ req.associatedCookies = buffered.associatedCookies;
470
+ if (buffered.responseHeaders)
471
+ req.responseHeaders = buffered.responseHeaders;
472
+ state.pendingExtraInfo.delete(req.requestId);
473
+ }
474
+ state.pendingRequests.set(req.requestId, req);
475
+ }
476
+ if (method === "Network.responseReceived" && p) {
477
+ const id = p.requestId;
478
+ const pending = state.pendingRequests.get(id);
479
+ if (pending) {
480
+ const response = p.response;
481
+ if (response) {
482
+ pending.status = response.status;
483
+ pending.statusText = response.statusText;
484
+ pending.mimeType = response.mimeType;
485
+ }
486
+ const timing = response.timing;
487
+ if (timing) {
488
+ pending.timing = {
489
+ dnsStart: timing.dnsStart ?? -1,
490
+ dnsEnd: timing.dnsEnd ?? -1,
491
+ connectStart: timing.connectStart ?? -1,
492
+ connectEnd: timing.connectEnd ?? -1,
493
+ sslStart: timing.sslStart ?? -1,
494
+ sslEnd: timing.sslEnd ?? -1,
495
+ sendStart: timing.sendStart ?? -1,
496
+ sendEnd: timing.sendEnd ?? -1,
497
+ receiveHeadersStart: timing.receiveHeadersStart ?? -1,
498
+ receiveHeadersEnd: timing.receiveHeadersEnd ?? -1
499
+ };
500
+ }
501
+ pending.type = pending.type ?? p.type;
502
+ }
503
+ }
504
+ if (method === "Network.loadingFinished" && p) {
505
+ const id = p.requestId;
506
+ const pending = state.pendingRequests.get(id);
507
+ if (pending) {
508
+ pending.endTime = Date.now();
509
+ pending.size = p.encodedDataLength ?? undefined;
510
+ freezeRequestDetail(tabId, pending, p.encodedDataLength ?? 0).catch(() => {});
511
+ state.networkBuffer.push(pending);
512
+ state.pendingRequests.delete(id);
513
+ state.completedRequests.set(id, pending);
514
+ if (state.completedRequests.size > DETAIL_RETAIN_MAX) {
515
+ const firstKey = state.completedRequests.keys().next().value;
516
+ if (firstKey)
517
+ state.completedRequests.delete(firstKey);
518
+ }
519
+ }
520
+ }
521
+ if (method === "Network.loadingFailed" && p) {
522
+ const id = p.requestId;
523
+ const pending = state.pendingRequests.get(id);
524
+ if (pending) {
525
+ pending.error = p.errorText ?? "Failed";
526
+ pending.endTime = Date.now();
527
+ state.networkBuffer.push(pending);
528
+ state.pendingRequests.delete(id);
529
+ state.completedRequests.set(id, pending);
530
+ if (state.completedRequests.size > DETAIL_RETAIN_MAX) {
531
+ const firstKey = state.completedRequests.keys().next().value;
532
+ if (firstKey)
533
+ state.completedRequests.delete(firstKey);
534
+ }
535
+ }
536
+ }
537
+ if (method.startsWith("Network.")) {
538
+ const now = Date.now();
539
+ for (const [id, req] of state.pendingRequests) {
540
+ if (now - req.startTime > PENDING_REQUEST_TTL_MS) {
541
+ req.error = "stale (no completion event)";
542
+ req.endTime = now;
543
+ state.networkBuffer.push(req);
544
+ state.pendingRequests.delete(id);
545
+ state.completedRequests.set(id, req);
546
+ if (state.completedRequests.size > DETAIL_RETAIN_MAX) {
547
+ const firstKey = state.completedRequests.keys().next().value;
548
+ if (firstKey)
549
+ state.completedRequests.delete(firstKey);
550
+ }
551
+ }
552
+ }
553
+ }
554
+ if (method === "Network.requestWillBeSentExtraInfo" && p) {
555
+ const id = p.requestId;
556
+ const headers = p.headers;
557
+ const cookies = p.associatedCookies;
558
+ const parsedCookies = cookies?.map((c) => ({
559
+ name: c.cookie.name,
560
+ value: c.cookie.value,
561
+ domain: c.cookie.domain,
562
+ path: c.cookie.path,
563
+ httpOnly: c.cookie.httpOnly,
564
+ secure: c.cookie.secure,
565
+ blocked: (c.blockedReasons?.length ?? 0) > 0,
566
+ blockedReasons: c.blockedReasons?.length ? c.blockedReasons : undefined
567
+ }));
568
+ const pending = state.pendingRequests.get(id);
569
+ if (pending) {
570
+ if (headers)
571
+ pending.requestHeaders = headers;
572
+ if (parsedCookies)
573
+ pending.associatedCookies = parsedCookies;
574
+ } else {
575
+ const existing = state.pendingExtraInfo.get(id) ?? { timestamp: Date.now() };
576
+ if (headers)
577
+ existing.requestHeaders = headers;
578
+ if (parsedCookies)
579
+ existing.associatedCookies = parsedCookies;
580
+ state.pendingExtraInfo.set(id, existing);
581
+ evictOldestExtraInfo(state);
582
+ }
583
+ }
584
+ if (method === "Network.responseReceivedExtraInfo" && p) {
585
+ const id = p.requestId;
586
+ const headers = p.headers;
587
+ const completed = state.completedRequests.get(id);
588
+ if (completed) {
589
+ if (headers && !completed.responseHeaders)
590
+ completed.responseHeaders = headers;
591
+ } else {
592
+ const pending = state.pendingRequests.get(id);
593
+ if (pending) {
594
+ if (headers)
595
+ pending.responseHeaders = headers;
596
+ } else {
597
+ const existing = state.pendingExtraInfo.get(id) ?? { timestamp: Date.now() };
598
+ if (headers)
599
+ existing.responseHeaders = headers;
600
+ state.pendingExtraInfo.set(id, existing);
601
+ evictOldestExtraInfo(state);
602
+ }
603
+ }
604
+ }
605
+ } catch (e) {
606
+ console.warn("[Debugger] Error processing CDP event:", method, e);
607
+ }
608
+ }
609
+ function initDebuggerListeners() {
610
+ if (typeof chrome === "undefined" || !chrome.debugger)
611
+ return;
612
+ chrome.debugger.onEvent.addListener(onEvent);
613
+ chrome.debugger.onDetach.addListener((source) => {
614
+ if (source.tabId != null) {
615
+ const state = getState(source.tabId);
616
+ if (state?.orphanCleanupTimer)
617
+ clearInterval(state.orphanCleanupTimer);
618
+ removeState(source.tabId);
619
+ }
620
+ });
621
+ }
622
+ function getConsoleMessages(tabId, filter) {
623
+ const state = getState(tabId);
624
+ if (!state)
625
+ return { items: [], dropped: 0 };
626
+ const predicate = filter ? (msg) => {
627
+ if (filter.level && msg.level !== filter.level)
628
+ return false;
629
+ if (filter.textPattern && !msg.text.includes(filter.textPattern))
630
+ return false;
631
+ return true;
632
+ } : undefined;
633
+ return state.consoleBuffer.peekWithDropped(predicate);
634
+ }
635
+ function getRuntimeExceptions(tabId) {
636
+ const state = getState(tabId);
637
+ if (!state)
638
+ return { items: [], dropped: 0 };
639
+ return state.exceptionBuffer.peekWithDropped();
640
+ }
641
+ function getNetworkCaptureStatus(tabId) {
642
+ const state = getState(tabId);
643
+ if (!state)
644
+ return { attached: false, pendingCount: 0, captureStartedAt: null };
645
+ return {
646
+ attached: true,
647
+ pendingCount: state.pendingRequests.size,
648
+ captureStartedAt: state.attachedAt
649
+ };
650
+ }
651
+ function getNetworkRequests(tabId, filter, options) {
652
+ const state = getState(tabId);
653
+ if (!state)
654
+ return { items: [], dropped: 0 };
655
+ const predicate = filter ? (req) => {
656
+ if (filter.urlPattern && !req.url.includes(filter.urlPattern))
657
+ return false;
658
+ if (filter.method && req.method !== filter.method.toUpperCase())
659
+ return false;
660
+ if (filter.statusCode != null && req.status !== filter.statusCode)
661
+ return false;
662
+ return true;
663
+ } : undefined;
664
+ const result = state.networkBuffer.peekWithDropped(predicate);
665
+ if (options?.includePending) {
666
+ const pendingItems = [...state.pendingRequests.values()];
667
+ const filtered = predicate ? pendingItems.filter(predicate) : pendingItems;
668
+ result.items = [...result.items, ...filtered].sort((a, b) => a.startTime - b.startTime);
669
+ }
670
+ return result;
671
+ }
672
+ async function getPerformanceMetrics(tabId) {
673
+ if (!tabStates.has(tabId))
674
+ throw new Error("Debugger not attached to tab " + tabId);
675
+ const result = await chrome.debugger.sendCommand({ tabId }, "Performance.getMetrics");
676
+ const metrics = {};
677
+ for (const m of result.metrics) {
678
+ metrics[m.name] = m.value;
679
+ }
680
+ return metrics;
681
+ }
682
+ async function getAccessibilityTree(tabId) {
683
+ if (!tabStates.has(tabId))
684
+ throw new Error("Debugger not attached to tab " + tabId);
685
+ const result = await chrome.debugger.sendCommand({ tabId }, "Accessibility.getFullAXTree");
686
+ return result;
687
+ }
688
+ async function getNetworkRequestDetail(tabId, requestId) {
689
+ const state = getState(tabId);
690
+ if (!state)
691
+ return { request: null, requestBody: null, responseBody: null, base64Encoded: false, bodyUnavailable: true };
692
+ const req = state.completedRequests.get(requestId) ?? state.pendingRequests.get(requestId) ?? null;
693
+ if (!req)
694
+ return { request: null, requestBody: null, responseBody: null, base64Encoded: false, bodyUnavailable: true };
695
+ if (req.frozen) {
696
+ return {
697
+ request: req,
698
+ requestBody: req.frozen.requestBody,
699
+ responseBody: req.frozen.responseBody,
700
+ base64Encoded: req.frozen.base64Encoded,
701
+ bodyUnavailable: req.frozen.bodyTruncated
702
+ };
703
+ }
704
+ let requestBody = null;
705
+ let responseBody = null;
706
+ let base64Encoded = false;
707
+ let bodyUnavailable = false;
708
+ if (req.postDataLength) {
709
+ try {
710
+ const result = await chrome.debugger.sendCommand({ tabId }, "Network.getRequestPostData", { requestId });
711
+ requestBody = result.postData ?? null;
712
+ } catch {}
713
+ }
714
+ try {
715
+ const result = await chrome.debugger.sendCommand({ tabId }, "Network.getResponseBody", { requestId });
716
+ const body = result;
717
+ responseBody = body.body ?? null;
718
+ base64Encoded = body.base64Encoded ?? false;
719
+ } catch {
720
+ bodyUnavailable = true;
721
+ }
722
+ return { request: req, requestBody, responseBody, base64Encoded, bodyUnavailable };
723
+ }
724
+ function clearNetworkBuffer(tabId) {
725
+ const state = getState(tabId);
726
+ if (!state)
727
+ return;
728
+ state.networkBuffer.clear();
729
+ state.completedRequests.clear();
730
+ state.pendingRequests.clear();
731
+ state.pendingExtraInfo.clear();
732
+ }
733
+ function clearExceptionBuffer(tabId) {
734
+ const state = getState(tabId);
735
+ if (!state)
736
+ return;
737
+ state.exceptionBuffer.clear();
738
+ }
739
+ function getScriptMetaCache(tabId) {
740
+ const state = getState(tabId);
741
+ if (!state)
742
+ return { scripts: [], staleSourceMapUrls: [], topFrameUrl: null };
743
+ const staleUrls = [...state.staleSourceMapUrls];
744
+ state.staleSourceMapUrls = [];
745
+ return {
746
+ scripts: Array.from(state.scriptMetaCache.values()),
747
+ staleSourceMapUrls: staleUrls,
748
+ topFrameUrl: state.currentNavigation?.url ?? null
749
+ };
750
+ }
751
+ function getSentCookies(tabId, requestId) {
752
+ const state = getState(tabId);
753
+ if (!state)
754
+ return [];
755
+ if (requestId) {
756
+ const req = state.completedRequests.get(requestId) ?? state.pendingRequests.get(requestId);
757
+ if (!req?.associatedCookies)
758
+ return [];
759
+ return req.associatedCookies.map((c) => ({ ...c, requestId: req.requestId, url: req.url }));
760
+ }
761
+ const seen = new Map;
762
+ const result = [];
763
+ for (const req of state.completedRequests.values()) {
764
+ if (!req.associatedCookies)
765
+ continue;
766
+ for (const c of req.associatedCookies) {
767
+ const key = `${c.name}|${c.domain}|${c.path}`;
768
+ if (!seen.has(key)) {
769
+ const entry = { ...c, requestId: req.requestId, url: req.url };
770
+ seen.set(key, entry);
771
+ result.push(entry);
772
+ }
773
+ }
774
+ }
775
+ return result;
776
+ }
777
+ async function findElementsByAccessibility(tabId, filters) {
778
+ if (!tabStates.has(tabId))
779
+ throw new Error("Debugger not attached to tab " + tabId);
780
+ const result = await chrome.debugger.sendCommand({ tabId }, "Accessibility.getFullAXTree");
781
+ const nodes = result.nodes ?? [];
782
+ const results = [];
783
+ for (const node of nodes) {
784
+ if (node.ignored)
785
+ continue;
786
+ if (!node.backendDOMNodeId)
787
+ continue;
788
+ const role = node.role?.value ?? "";
789
+ const name = node.name?.value ?? "";
790
+ const desc = node.description?.value;
791
+ if (filters.role && role.toLowerCase() !== filters.role.toLowerCase())
792
+ continue;
793
+ if (filters.name && !name.toLowerCase().includes(filters.name.toLowerCase()))
794
+ continue;
795
+ if (filters.description && desc && !desc.toLowerCase().includes(filters.description.toLowerCase()))
796
+ continue;
797
+ if (!filters.role && ["generic", "none", "InlineTextBox", "StaticText"].includes(role))
798
+ continue;
799
+ const props = {};
800
+ if (node.properties) {
801
+ for (const p of node.properties) {
802
+ props[p.name] = p.value?.value;
803
+ }
804
+ }
805
+ results.push({
806
+ backendDOMNodeId: node.backendDOMNodeId,
807
+ role,
808
+ name,
809
+ description: desc,
810
+ value: node.value?.value,
811
+ properties: props
812
+ });
813
+ }
814
+ return results;
815
+ }
816
+ async function getPartialAccessibilityTree(tabId, backendNodeId) {
817
+ if (!tabStates.has(tabId))
818
+ throw new Error("Debugger not attached to tab " + tabId);
819
+ const result = await chrome.debugger.sendCommand({ tabId }, "Accessibility.getPartialAXTree", {
820
+ backendNodeId,
821
+ fetchRelatives: true
822
+ });
823
+ return result.nodes ?? [];
824
+ }
825
+ async function queryAccessibilitySubtree(tabId, backendNodeId) {
826
+ if (!tabStates.has(tabId))
827
+ throw new Error("Debugger not attached to tab " + tabId);
828
+ const result = await chrome.debugger.sendCommand({ tabId }, "Accessibility.queryAXTree", {
829
+ backendNodeId
830
+ });
831
+ return result.nodes ?? [];
832
+ }
833
+ async function resolveSelector(tabId, selector) {
834
+ if (!tabStates.has(tabId))
835
+ throw new Error("Debugger not attached to tab " + tabId);
836
+ const docResult = await chrome.debugger.sendCommand({ tabId }, "DOM.getDocument", { depth: 0 });
837
+ const rootNodeId = docResult.root.nodeId;
838
+ const queryResult = await chrome.debugger.sendCommand({ tabId }, "DOM.querySelector", {
839
+ nodeId: rootNodeId,
840
+ selector
841
+ });
842
+ const nodeId = queryResult.nodeId;
843
+ if (!nodeId)
844
+ throw new Error(`No element matches selector: ${selector}`);
845
+ const descResult = await chrome.debugger.sendCommand({ tabId }, "DOM.describeNode", { nodeId });
846
+ const backendNodeId = descResult.node.backendNodeId;
847
+ return backendNodeId;
848
+ }
849
+ async function resolveSelectorToNodeId(tabId, selector) {
850
+ if (!tabStates.has(tabId))
851
+ throw new Error("Debugger not attached to tab " + tabId);
852
+ const docResult = await chrome.debugger.sendCommand({ tabId }, "DOM.getDocument", { depth: 0 });
853
+ const rootNodeId = docResult.root.nodeId;
854
+ const queryResult = await chrome.debugger.sendCommand({ tabId }, "DOM.querySelector", {
855
+ nodeId: rootNodeId,
856
+ selector
857
+ });
858
+ const nodeId = queryResult.nodeId;
859
+ if (!nodeId)
860
+ throw new Error(`No element matches selector: ${selector}`);
861
+ return nodeId;
862
+ }
863
+ async function ensureCssDomain(tabId) {
864
+ const state = tabStates.get(tabId);
865
+ if (!state)
866
+ throw new Error("Debugger not attached to tab " + tabId);
867
+ if (state.cssEnabled)
868
+ return;
869
+ await chrome.debugger.sendCommand({ tabId }, "DOM.enable");
870
+ await chrome.debugger.sendCommand({ tabId }, "CSS.enable");
871
+ state.cssEnabled = true;
872
+ }
873
+ var DEFAULT_INSPECT_PROPERTIES = [
874
+ "display",
875
+ "position",
876
+ "top",
877
+ "right",
878
+ "bottom",
879
+ "left",
880
+ "width",
881
+ "height",
882
+ "min-width",
883
+ "min-height",
884
+ "max-width",
885
+ "max-height",
886
+ "margin-top",
887
+ "margin-right",
888
+ "margin-bottom",
889
+ "margin-left",
890
+ "padding-top",
891
+ "padding-right",
892
+ "padding-bottom",
893
+ "padding-left",
894
+ "box-sizing",
895
+ "overflow",
896
+ "overflow-x",
897
+ "overflow-y",
898
+ "transform",
899
+ "rotate",
900
+ "scale",
901
+ "translate",
902
+ "z-index",
903
+ "contain",
904
+ "visibility",
905
+ "opacity",
906
+ "filter",
907
+ "backdrop-filter",
908
+ "perspective",
909
+ "will-change",
910
+ "isolation",
911
+ "clip-path",
912
+ "mix-blend-mode",
913
+ "flex-grow",
914
+ "flex-shrink",
915
+ "flex-basis",
916
+ "align-self",
917
+ "justify-self",
918
+ "order",
919
+ "grid-column-start",
920
+ "grid-column-end",
921
+ "grid-row-start",
922
+ "grid-row-end",
923
+ "grid-template-columns",
924
+ "grid-template-rows",
925
+ "grid-auto-columns",
926
+ "grid-auto-rows",
927
+ "gap",
928
+ "row-gap",
929
+ "column-gap",
930
+ "vertical-align",
931
+ "line-height",
932
+ "white-space",
933
+ "pointer-events"
934
+ ];
935
+ var ANCESTOR_EXTRA_PROPERTIES = [
936
+ "flex-direction",
937
+ "flex-wrap",
938
+ "align-items",
939
+ "justify-content",
940
+ "grid-template-columns",
941
+ "grid-template-rows",
942
+ "grid-auto-flow",
943
+ "gap",
944
+ "row-gap",
945
+ "column-gap"
946
+ ];
947
+ function parseCdpAttributes(attrs) {
948
+ const result = {};
949
+ for (let i = 0;i < attrs.length; i += 2) {
950
+ result[attrs[i]] = attrs[i + 1] ?? "";
951
+ }
952
+ return result;
953
+ }
954
+ async function cssInspect(tabId, options) {
955
+ await attachDebugger(tabId);
956
+ await ensureCssDomain(tabId);
957
+ const props = new Set(options.properties ?? DEFAULT_INSPECT_PROPERTIES);
958
+ const ancestorDepth = options.ancestorDepth ?? 5;
959
+ let nodeId;
960
+ if (options.selector) {
961
+ nodeId = await resolveSelectorToNodeId(tabId, options.selector);
962
+ } else if (options.backendNodeId != null) {
963
+ const objectId = await resolveBackendNode(tabId, options.backendNodeId);
964
+ const reqResult = await chrome.debugger.sendCommand({ tabId }, "DOM.requestNode", {
965
+ objectId
966
+ });
967
+ nodeId = reqResult.nodeId;
968
+ if (!nodeId)
969
+ throw new Error("Could not resolve backendNodeId to nodeId");
970
+ } else {
971
+ throw new Error("selector or backendNodeId is required");
972
+ }
973
+ const descResult = await chrome.debugger.sendCommand({ tabId }, "DOM.describeNode", { nodeId });
974
+ const nodeDesc = descResult.node;
975
+ const attrs = parseCdpAttributes(nodeDesc.attributes ?? []);
976
+ const target = {
977
+ tagName: nodeDesc.localName,
978
+ id: attrs.id ?? "",
979
+ classList: (attrs.class ?? "").split(/\s+/).filter(Boolean),
980
+ nodeId
981
+ };
982
+ let boxModel = null;
983
+ try {
984
+ const boxResult = await chrome.debugger.sendCommand({ tabId }, "DOM.getBoxModel", { nodeId });
985
+ const model = boxResult.model;
986
+ boxModel = {
987
+ content: model.content,
988
+ padding: model.padding,
989
+ border: model.border,
990
+ margin: model.margin,
991
+ width: model.width,
992
+ height: model.height
993
+ };
994
+ } catch {}
995
+ const computedResult = await chrome.debugger.sendCommand({ tabId }, "CSS.getComputedStyleForNode", { nodeId });
996
+ const allComputed = computedResult.computedStyle;
997
+ const computedStyle = options.includeComputed ? allComputed : allComputed.filter((p) => props.has(p.name));
998
+ const matchedResult = await chrome.debugger.sendCommand({ tabId }, "CSS.getMatchedStylesForNode", { nodeId });
999
+ const matched = matchedResult;
1000
+ const inlineStyle = (matched.inlineStyle?.cssProperties ?? []).filter((p) => p.value && !p.disabled).map((p) => ({ name: p.name, value: p.value, important: p.important ?? false }));
1001
+ const matchedRules = [];
1002
+ for (const entry of matched.matchedCSSRules ?? []) {
1003
+ const rule = entry.rule;
1004
+ if (rule.origin === "user-agent")
1005
+ continue;
1006
+ matchedRules.push({
1007
+ selector: rule.selectorList.text,
1008
+ origin: rule.origin,
1009
+ styleSheetId: rule.styleSheetId ?? "",
1010
+ sourceLine: rule.style.range?.startLine ?? 0,
1011
+ declarations: rule.style.cssProperties.filter((p) => p.value && !p.disabled).map((p) => ({
1012
+ name: p.name,
1013
+ value: p.value,
1014
+ important: p.important ?? false
1015
+ }))
1016
+ });
1017
+ }
1018
+ const ancestorChain = [];
1019
+ const ancestorProps = new Set([...props, ...ANCESTOR_EXTRA_PROPERTIES]);
1020
+ const resolveResult = await chrome.debugger.sendCommand({ tabId }, "DOM.resolveNode", { nodeId });
1021
+ let currentObjectId = resolveResult.object?.objectId;
1022
+ for (let level = 0;level < ancestorDepth && currentObjectId; level++) {
1023
+ try {
1024
+ const parentResult = await chrome.debugger.sendCommand({ tabId }, "Runtime.callFunctionOn", {
1025
+ objectId: currentObjectId,
1026
+ functionDeclaration: `function() { return this.parentElement; }`,
1027
+ returnByValue: false
1028
+ });
1029
+ const parentObj = parentResult.result;
1030
+ if (!parentObj.objectId || parentObj.subtype === "null")
1031
+ break;
1032
+ const reqNodeResult = await chrome.debugger.sendCommand({ tabId }, "DOM.requestNode", {
1033
+ objectId: parentObj.objectId
1034
+ });
1035
+ const parentNodeId = reqNodeResult.nodeId;
1036
+ if (!parentNodeId)
1037
+ break;
1038
+ const parentDesc = await chrome.debugger.sendCommand({ tabId }, "DOM.describeNode", { nodeId: parentNodeId });
1039
+ const pNode = parentDesc.node;
1040
+ if (!pNode.localName)
1041
+ break;
1042
+ const pAttrs = parseCdpAttributes(pNode.attributes ?? []);
1043
+ const pComputed = await chrome.debugger.sendCommand({ tabId }, "CSS.getComputedStyleForNode", { nodeId: parentNodeId });
1044
+ const pAllComputed = pComputed.computedStyle;
1045
+ const pFiltered = pAllComputed.filter((p) => ancestorProps.has(p.name));
1046
+ ancestorChain.push({
1047
+ level,
1048
+ tagName: pNode.localName,
1049
+ id: pAttrs.id ?? "",
1050
+ classList: (pAttrs.class ?? "").split(/\s+/).filter(Boolean),
1051
+ computed: pFiltered
1052
+ });
1053
+ currentObjectId = parentObj.objectId;
1054
+ } catch {
1055
+ break;
1056
+ }
1057
+ }
1058
+ const result = { target, boxModel, computedStyle, inlineStyle, matchedRules, ancestorChain };
1059
+ if (options.includeMeasured) {
1060
+ const targetObjectId = resolveResult.object?.objectId;
1061
+ if (targetObjectId) {
1062
+ const measureResult = await chrome.debugger.sendCommand({ tabId }, "Runtime.callFunctionOn", {
1063
+ objectId: targetObjectId,
1064
+ functionDeclaration: `function() {
1065
+ const measure = (el) => {
1066
+ const r = el.getBoundingClientRect();
1067
+ return {
1068
+ rect: { x: r.x, y: r.y, width: r.width, height: r.height },
1069
+ scrollWidth: el.scrollWidth,
1070
+ scrollHeight: el.scrollHeight,
1071
+ clientWidth: el.clientWidth,
1072
+ clientHeight: el.clientHeight,
1073
+ };
1074
+ };
1075
+ const target = measure(this);
1076
+ const ancestors = [];
1077
+ let el = this.parentElement;
1078
+ let level = 0;
1079
+ const maxLevel = ${options.ancestorDepth ?? 8};
1080
+ while (el && level < maxLevel) {
1081
+ ancestors.push({ level, ...measure(el) });
1082
+ el = el.parentElement;
1083
+ level++;
1084
+ }
1085
+ return JSON.stringify({ target, ancestors });
1086
+ }`,
1087
+ returnByValue: true
1088
+ });
1089
+ const measured = JSON.parse(measureResult.result.value);
1090
+ result.measuredLayout = measured;
1091
+ }
1092
+ }
1093
+ return result;
1094
+ }
1095
+ async function resolveBackendNode(tabId, backendNodeId) {
1096
+ const result = await chrome.debugger.sendCommand({ tabId }, "DOM.resolveNode", {
1097
+ backendNodeId
1098
+ });
1099
+ const objectId = result.object.objectId;
1100
+ if (!objectId)
1101
+ throw new Error("Could not resolve backendNodeId to objectId");
1102
+ return objectId;
1103
+ }
1104
+ async function getElementCenter(tabId, objectId) {
1105
+ const result = await chrome.debugger.sendCommand({ tabId }, "Runtime.callFunctionOn", {
1106
+ objectId,
1107
+ functionDeclaration: `function() {
1108
+ const rect = this.getBoundingClientRect();
1109
+ return { x: rect.x + rect.width / 2, y: rect.y + rect.height / 2 };
1110
+ }`,
1111
+ returnByValue: true
1112
+ });
1113
+ const pos = result.result.value;
1114
+ return pos;
1115
+ }
1116
+ var NAV_DETECT_INTERVAL = 50;
1117
+ var NAV_DETECT_TIMEOUT = 1500;
1118
+ var SPA_SETTLE_MS = 200;
1119
+ var SCROLL_SETTLE_MS = 50;
1120
+ async function waitForNavigationChange(tabId, beforeNavId, beforeUrl, timeoutMs) {
1121
+ const deadline = Date.now() + timeoutMs;
1122
+ while (Date.now() < deadline) {
1123
+ await new Promise((r) => setTimeout(r, NAV_DETECT_INTERVAL));
1124
+ const state = getState(tabId);
1125
+ if (!state)
1126
+ break;
1127
+ const cur = state.currentNavigation;
1128
+ if (!cur)
1129
+ continue;
1130
+ if (cur.navigationId !== beforeNavId || cur.url !== beforeUrl) {
1131
+ return { navigated: true, navigationType: cur.type, url: cur.url };
1132
+ }
1133
+ }
1134
+ return { navigated: false, navigationType: "none" };
1135
+ }
1136
+ async function waitForTabLoadSafe(tabId, timeoutMs) {
1137
+ try {
1138
+ const tab = await chrome.tabs.get(tabId);
1139
+ if (tab.status === "complete") {
1140
+ await new Promise((r) => setTimeout(r, 500));
1141
+ return;
1142
+ }
1143
+ } catch {
1144
+ return;
1145
+ }
1146
+ await waitForTabLoad(tabId, timeoutMs);
1147
+ }
1148
+ function captureNavigationState(tabId) {
1149
+ const state = getState(tabId);
1150
+ return {
1151
+ navigationId: state?.currentNavigation?.navigationId,
1152
+ url: state?.currentNavigation?.url
1153
+ };
1154
+ }
1155
+ async function waitForPostDispatchNavigation(tabId, before) {
1156
+ let navResult = await waitForNavigationChange(tabId, before.navigationId, before.url, NAV_DETECT_TIMEOUT);
1157
+ if (!navResult.navigated) {
1158
+ try {
1159
+ const tab = await chrome.tabs.get(tabId);
1160
+ if (before.url && tab.url !== before.url) {
1161
+ navResult = await waitForNavigationChange(tabId, before.navigationId, before.url, NAV_DETECT_TIMEOUT);
1162
+ if (!navResult.navigated) {
1163
+ navResult = { navigated: true, navigationType: "full", url: tab.url };
1164
+ }
1165
+ }
1166
+ } catch {}
1167
+ }
1168
+ if (navResult.navigated && navResult.navigationType === "full") {
1169
+ try {
1170
+ await waitForTabLoadSafe(tabId, 1e4);
1171
+ } catch {}
1172
+ } else if (navResult.navigated && navResult.navigationType === "same-document") {
1173
+ await new Promise((r) => setTimeout(r, SPA_SETTLE_MS));
1174
+ }
1175
+ let title;
1176
+ if (navResult.navigated) {
1177
+ try {
1178
+ const tab = await chrome.tabs.get(tabId);
1179
+ title = tab.title;
1180
+ } catch {}
1181
+ }
1182
+ return {
1183
+ navigated: navResult.navigated,
1184
+ navigationType: navResult.navigationType,
1185
+ url: navResult.url,
1186
+ title
1187
+ };
1188
+ }
1189
+ async function clickByBackendNodeId(tabId, backendNodeId, options) {
1190
+ const state = getState(tabId);
1191
+ if (!state)
1192
+ throw new Error("Debugger not attached to tab " + tabId);
1193
+ const objectId = await resolveBackendNode(tabId, backendNodeId);
1194
+ await chrome.debugger.sendCommand({ tabId }, "Runtime.callFunctionOn", {
1195
+ objectId,
1196
+ functionDeclaration: `function() { this.scrollIntoView({ block: "center", inline: "center", behavior: "instant" }); }`,
1197
+ returnByValue: true
1198
+ });
1199
+ await new Promise((r) => setTimeout(r, SCROLL_SETTLE_MS));
1200
+ const { x, y } = await getElementCenter(tabId, objectId);
1201
+ const beforeNavId = state.currentNavigation?.navigationId;
1202
+ const beforeUrl = state.currentNavigation?.url;
1203
+ await chrome.debugger.sendCommand({ tabId }, "Input.dispatchMouseEvent", {
1204
+ type: "mouseMoved",
1205
+ x,
1206
+ y
1207
+ });
1208
+ await chrome.debugger.sendCommand({ tabId }, "Input.dispatchMouseEvent", {
1209
+ type: "mousePressed",
1210
+ x,
1211
+ y,
1212
+ button: "left",
1213
+ clickCount: 1
1214
+ });
1215
+ await chrome.debugger.sendCommand({ tabId }, "Input.dispatchMouseEvent", {
1216
+ type: "mouseReleased",
1217
+ x,
1218
+ y,
1219
+ button: "left",
1220
+ clickCount: 1
1221
+ });
1222
+ if (!options?.waitForNavigation) {
1223
+ return { clicked: true, navigated: false, navigationType: "none" };
1224
+ }
1225
+ let navResult = await waitForNavigationChange(tabId, beforeNavId, beforeUrl, NAV_DETECT_TIMEOUT);
1226
+ if (!navResult.navigated) {
1227
+ let urlAlreadyChanged = false;
1228
+ try {
1229
+ const tab = await chrome.tabs.get(tabId);
1230
+ urlAlreadyChanged = !!beforeUrl && tab.url !== beforeUrl;
1231
+ } catch {}
1232
+ if (!urlAlreadyChanged) {
1233
+ try {
1234
+ await chrome.debugger.sendCommand({ tabId }, "Runtime.callFunctionOn", {
1235
+ objectId,
1236
+ functionDeclaration: `function() { this.click(); }`,
1237
+ returnByValue: true
1238
+ });
1239
+ navResult = await waitForNavigationChange(tabId, beforeNavId, beforeUrl, NAV_DETECT_TIMEOUT);
1240
+ } catch {}
1241
+ } else {
1242
+ navResult = await waitForNavigationChange(tabId, beforeNavId, beforeUrl, NAV_DETECT_TIMEOUT);
1243
+ }
1244
+ }
1245
+ if (navResult.navigated && navResult.navigationType === "full") {
1246
+ try {
1247
+ await waitForTabLoadSafe(tabId, 1e4);
1248
+ } catch {}
1249
+ } else if (navResult.navigated && navResult.navigationType === "same-document") {
1250
+ await new Promise((r) => setTimeout(r, SPA_SETTLE_MS));
1251
+ }
1252
+ const finalState = getState(tabId);
1253
+ const finalUrl = navResult.url ?? finalState?.currentNavigation?.url;
1254
+ let finalTitle;
1255
+ if (navResult.navigated) {
1256
+ try {
1257
+ const tab = await chrome.tabs.get(tabId);
1258
+ finalTitle = tab.title;
1259
+ } catch {}
1260
+ }
1261
+ return {
1262
+ clicked: true,
1263
+ navigated: navResult.navigated,
1264
+ navigationType: navResult.navigationType,
1265
+ url: finalUrl,
1266
+ title: finalTitle
1267
+ };
1268
+ }
1269
+ async function verifyValueByObjectId(tabId, objectId, expectedValue, info) {
1270
+ const readResult = await chrome.debugger.sendCommand({ tabId }, "Runtime.callFunctionOn", {
1271
+ objectId,
1272
+ functionDeclaration: `function() {
1273
+ if (this.isContentEditable) return this.innerText.replace(/\\n{2,}/g, "\\n").trim();
1274
+ return this.value ?? "";
1275
+ }`,
1276
+ returnByValue: true
1277
+ });
1278
+ const actualValue = String(readResult.result.value ?? "");
1279
+ const normalizedExpected = info.contentEditable ? expectedValue.trim() : expectedValue;
1280
+ const verified = actualValue === normalizedExpected;
1281
+ return {
1282
+ set: true,
1283
+ verified,
1284
+ verifyExpected: normalizedExpected.slice(0, 200),
1285
+ verifyActual: actualValue.slice(0, 200)
1286
+ };
1287
+ }
1288
+ async function setValueByBackendNodeId(tabId, backendNodeId, value, verify) {
1289
+ if (!tabStates.has(tabId))
1290
+ throw new Error("Debugger not attached to tab " + tabId);
1291
+ const objectId = await resolveBackendNode(tabId, backendNodeId);
1292
+ const typeResult = await chrome.debugger.sendCommand({ tabId }, "Runtime.callFunctionOn", {
1293
+ objectId,
1294
+ functionDeclaration: `function() { return { tagName: this.tagName?.toLowerCase(), type: this.type?.toLowerCase(), contentEditable: this.isContentEditable }; }`,
1295
+ returnByValue: true
1296
+ });
1297
+ const info = typeResult.result.value;
1298
+ if (info.tagName === "select") {
1299
+ await chrome.debugger.sendCommand({ tabId }, "Runtime.callFunctionOn", {
1300
+ objectId,
1301
+ functionDeclaration: `function(v) {
1302
+ this.value = v;
1303
+ this.dispatchEvent(new Event('input', { bubbles: true }));
1304
+ this.dispatchEvent(new Event('change', { bubbles: true }));
1305
+ }`,
1306
+ arguments: [{ value }]
1307
+ });
1308
+ if (verify) {
1309
+ return verifyValueByObjectId(tabId, objectId, value, info);
1310
+ }
1311
+ return { set: true };
1312
+ }
1313
+ if (info.tagName === "input" && info.type === "file") {
1314
+ await chrome.debugger.sendCommand({ tabId }, "DOM.setFileInputFiles", {
1315
+ backendNodeId,
1316
+ files: [value]
1317
+ });
1318
+ if (verify) {
1319
+ return { set: true, verified: false, verifyExpected: value.slice(0, 200), verifyActual: "(file input: verification not supported)" };
1320
+ }
1321
+ return { set: true };
1322
+ }
1323
+ const { x, y } = await getElementCenter(tabId, objectId);
1324
+ await chrome.debugger.sendCommand({ tabId }, "Input.dispatchMouseEvent", {
1325
+ type: "mousePressed",
1326
+ x,
1327
+ y,
1328
+ button: "left",
1329
+ clickCount: 1
1330
+ });
1331
+ await chrome.debugger.sendCommand({ tabId }, "Input.dispatchMouseEvent", {
1332
+ type: "mouseReleased",
1333
+ x,
1334
+ y,
1335
+ button: "left",
1336
+ clickCount: 1
1337
+ });
1338
+ await chrome.debugger.sendCommand({ tabId }, "Input.dispatchKeyEvent", {
1339
+ type: "keyDown",
1340
+ key: "a",
1341
+ code: "KeyA",
1342
+ commands: ["selectAll"]
1343
+ });
1344
+ await chrome.debugger.sendCommand({ tabId }, "Input.dispatchKeyEvent", {
1345
+ type: "keyUp",
1346
+ key: "a",
1347
+ code: "KeyA"
1348
+ });
1349
+ await chrome.debugger.sendCommand({ tabId }, "Input.insertText", { text: value });
1350
+ if (verify) {
1351
+ return verifyValueByObjectId(tabId, objectId, value, info);
1352
+ }
1353
+ return { set: true };
1354
+ }
1355
+ async function getAttributesByBackendNodeId(tabId, backendNodeId) {
1356
+ if (!tabStates.has(tabId))
1357
+ throw new Error("Debugger not attached to tab " + tabId);
1358
+ const result = await chrome.debugger.sendCommand({ tabId }, "DOM.describeNode", {
1359
+ backendNodeId
1360
+ });
1361
+ const node = result.node;
1362
+ const attrs = {};
1363
+ if (node.attributes) {
1364
+ for (let i = 0;i < node.attributes.length; i += 2) {
1365
+ attrs[node.attributes[i]] = node.attributes[i + 1] ?? "";
1366
+ }
1367
+ }
1368
+ return attrs;
1369
+ }
1370
+ async function getDetachedDomNodes(tabId) {
1371
+ if (!tabStates.has(tabId))
1372
+ throw new Error("Debugger not attached to tab " + tabId);
1373
+ try {
1374
+ const result = await chrome.debugger.sendCommand({ tabId }, "DOM.getDetachedDomNodes");
1375
+ const detachedNodes = result.detachedNodes ?? [];
1376
+ return detachedNodes.map((entry) => ({
1377
+ nodeName: entry.treeNode?.nodeName ?? "UNKNOWN",
1378
+ backendNodeId: entry.treeNode?.backendNodeId ?? 0,
1379
+ retainedNodeCount: entry.retainedNodeIds?.length ?? 0
1380
+ }));
1381
+ } catch (e) {
1382
+ const msg = e instanceof Error ? e.message : String(e);
1383
+ if (msg.includes("wasn't found") || msg.includes("not found") || msg.includes("not supported")) {
1384
+ return null;
1385
+ }
1386
+ throw new Error(`Failed to get detached DOM nodes: ${msg}`);
1387
+ }
1388
+ }
1389
+ async function getEventListenersForNode(tabId, backendNodeId) {
1390
+ if (!tabStates.has(tabId))
1391
+ throw new Error("Debugger not attached to tab " + tabId);
1392
+ let objectId;
1393
+ try {
1394
+ const resolveResult = await chrome.debugger.sendCommand({ tabId }, "DOM.resolveNode", { backendNodeId });
1395
+ objectId = resolveResult?.object?.objectId;
1396
+ if (!objectId)
1397
+ return { supported: true, listeners: null };
1398
+ const listenersResult = await chrome.debugger.sendCommand({ tabId }, "DOMDebugger.getEventListeners", { objectId });
1399
+ return {
1400
+ supported: true,
1401
+ listeners: (listenersResult?.listeners ?? []).map((l) => ({
1402
+ type: l.type ?? "unknown",
1403
+ scriptId: l.scriptId ?? "0",
1404
+ lineNumber: l.lineNumber ?? 0
1405
+ }))
1406
+ };
1407
+ } catch (e) {
1408
+ const msg = e instanceof Error ? e.message : String(e);
1409
+ if (msg.includes("wasn't found") || msg.includes("not found") || msg.includes("not supported")) {
1410
+ return { supported: false, listeners: null };
1411
+ }
1412
+ return { supported: true, listeners: null };
1413
+ } finally {
1414
+ if (objectId) {
1415
+ try {
1416
+ await chrome.debugger.sendCommand({ tabId }, "Runtime.releaseObject", { objectId });
1417
+ } catch {}
1418
+ }
1419
+ }
1420
+ }
1421
+ var MAX_EXPAND_DEPTH = 3;
1422
+ var MAX_PROPERTIES_PER_OBJECT = 20;
1423
+ async function startTrace(tabId, reload) {
1424
+ if (!tabStates.has(tabId))
1425
+ throw new Error("Debugger not attached to tab " + tabId);
1426
+ const state = getState(tabId);
1427
+ if (state.traceState?.recording)
1428
+ throw new Error("Trace already recording");
1429
+ state.traceState = { recording: true, events: [], resolve: null };
1430
+ await chrome.debugger.sendCommand({ tabId }, "Tracing.start", {
1431
+ categories: "-*,devtools.timeline,v8.execute,disabled-by-default-devtools.timeline,disabled-by-default-devtools.timeline.frame,toplevel,blink.console,blink.user_timing,latencyInfo,disabled-by-default-v8.cpu_profiler",
1432
+ options: "sampling-frequency=10000"
1433
+ });
1434
+ if (reload) {
1435
+ await chrome.debugger.sendCommand({ tabId }, "Page.reload");
1436
+ }
1437
+ return { started: true };
1438
+ }
1439
+ function stopTrace(tabId) {
1440
+ const state = getState(tabId);
1441
+ if (!state?.traceState?.recording)
1442
+ throw new Error("No trace recording");
1443
+ return new Promise((resolve) => {
1444
+ state.traceState.resolve = resolve;
1445
+ chrome.debugger.sendCommand({ tabId }, "Tracing.end");
1446
+ });
1447
+ }
1448
+ async function expandProperties(tabId, objectId, depth) {
1449
+ const result = await chrome.debugger.sendCommand({ tabId }, "Runtime.getProperties", {
1450
+ objectId,
1451
+ ownProperties: true,
1452
+ generatePreview: true
1453
+ });
1454
+ const allProps = result.result;
1455
+ const rawProps = allProps.length > MAX_PROPERTIES_PER_OBJECT ? allProps.slice(0, MAX_PROPERTIES_PER_OBJECT) : allProps;
1456
+ const properties = [];
1457
+ for (const p of rawProps) {
1458
+ const prop = {
1459
+ name: p.name,
1460
+ value: p.value?.description ?? String(p.value?.value ?? ""),
1461
+ type: p.value?.type ?? "unknown"
1462
+ };
1463
+ if (p.value?.objectId && p.value.type === "object" && depth < MAX_EXPAND_DEPTH) {
1464
+ try {
1465
+ prop.properties = await expandProperties(tabId, p.value.objectId, depth + 1);
1466
+ } catch {
1467
+ prop.value = "(object expired)";
1468
+ }
1469
+ }
1470
+ properties.push(prop);
1471
+ }
1472
+ if (allProps.length > MAX_PROPERTIES_PER_OBJECT) {
1473
+ properties.push({ name: "...", value: `(${allProps.length - MAX_PROPERTIES_PER_OBJECT} more)`, type: "truncated" });
1474
+ }
1475
+ return properties;
1476
+ }
1477
+ async function getConsoleMessageDetail(tabId, msgId) {
1478
+ const state = getState(tabId);
1479
+ if (!state)
1480
+ return null;
1481
+ const items = state.consoleBuffer.peek();
1482
+ const msg = items.find((m) => m.msgId === msgId);
1483
+ if (!msg)
1484
+ return null;
1485
+ const expandedArgs = [];
1486
+ for (const arg of msg.args) {
1487
+ if (arg.objectId) {
1488
+ try {
1489
+ const properties = await expandProperties(tabId, arg.objectId, 1);
1490
+ expandedArgs.push({
1491
+ type: arg.type,
1492
+ description: arg.description,
1493
+ properties
1494
+ });
1495
+ } catch {
1496
+ expandedArgs.push({ type: arg.type, description: arg.description, value: "(object expired)" });
1497
+ }
1498
+ } else {
1499
+ expandedArgs.push({ type: arg.type, value: arg.value, description: arg.description });
1500
+ }
1501
+ }
1502
+ return {
1503
+ msgId: msg.msgId,
1504
+ level: msg.level,
1505
+ text: msg.text,
1506
+ expandedArgs,
1507
+ stackTrace: msg.stackTrace
1508
+ };
1509
+ }
1510
+
1511
+ // apps/extension/src/background/recording.ts
1512
+ var activeScreencast = null;
1513
+ var sendFrameFn = null;
1514
+ function setSendFrameCallback(fn) {
1515
+ sendFrameFn = fn;
1516
+ }
1517
+ function getActiveScreencast() {
1518
+ return activeScreencast;
1519
+ }
1520
+ async function startRecording(tabId, sessionId, fps, quality, maxDurationSec) {
1521
+ if (activeScreencast) {
1522
+ return activeScreencast;
1523
+ }
1524
+ await attachDebugger(tabId);
1525
+ await chrome.debugger.sendCommand({ tabId }, "Page.enable");
1526
+ await chrome.debugger.sendCommand({ tabId }, "Page.startScreencast", {
1527
+ format: "jpeg",
1528
+ quality,
1529
+ maxWidth: 1280,
1530
+ maxHeight: 720,
1531
+ everyNthFrame: Math.max(1, Math.round(60 / fps))
1532
+ });
1533
+ activeScreencast = {
1534
+ tabId,
1535
+ sessionId,
1536
+ startedAt: Date.now(),
1537
+ frameCount: 0,
1538
+ fps,
1539
+ quality,
1540
+ maxDurationSec,
1541
+ autoStopTimer: setTimeout(() => {
1542
+ console.log("[Recording] Auto-stop: max duration reached");
1543
+ stopRecording().catch(console.error);
1544
+ }, maxDurationSec * 1000)
1545
+ };
1546
+ return activeScreencast;
1547
+ }
1548
+ async function stopRecording() {
1549
+ if (!activeScreencast)
1550
+ return null;
1551
+ const { tabId, sessionId, frameCount, startedAt, autoStopTimer } = activeScreencast;
1552
+ if (autoStopTimer)
1553
+ clearTimeout(autoStopTimer);
1554
+ activeScreencast = null;
1555
+ try {
1556
+ await chrome.debugger.sendCommand({ tabId }, "Page.stopScreencast");
1557
+ } catch (e) {
1558
+ console.warn("[Recording] Failed to stop screencast:", e);
1559
+ }
1560
+ return {
1561
+ sessionId,
1562
+ frameCount,
1563
+ durationSec: Math.round((Date.now() - startedAt) / 1000)
1564
+ };
1565
+ }
1566
+ function handleScreencastFrame(tabId, params) {
1567
+ if (!activeScreencast || activeScreencast.tabId !== tabId)
1568
+ return;
1569
+ const frameSessionId = params.sessionId;
1570
+ chrome.debugger.sendCommand({ tabId }, "Page.screencastFrameAck", { sessionId: frameSessionId }).catch(() => {});
1571
+ activeScreencast.frameCount++;
1572
+ if (sendFrameFn) {
1573
+ const metadata = params.metadata;
1574
+ sendFrameFn({
1575
+ type: "frame",
1576
+ sessionId: activeScreencast.sessionId,
1577
+ index: activeScreencast.frameCount,
1578
+ data: params.data,
1579
+ timestamp: metadata?.timestamp ?? Date.now() / 1000
1580
+ });
1581
+ }
1582
+ }
1583
+ function onTabRemoved(tabId) {
1584
+ if (activeScreencast?.tabId === tabId) {
1585
+ console.log("[Recording] Tab closed, stopping recording");
1586
+ if (activeScreencast.autoStopTimer)
1587
+ clearTimeout(activeScreencast.autoStopTimer);
1588
+ activeScreencast = null;
1589
+ }
1590
+ }
1591
+ function onWsDisconnect() {
1592
+ if (activeScreencast) {
1593
+ console.log("[Recording] WebSocket disconnected, stopping recording");
1594
+ stopRecording().catch(console.error);
1595
+ }
1596
+ }
1597
+
1598
+ // apps/extension/src/background/handle-request.ts
1599
+ var captureQueue = Promise.resolve();
1600
+ var lastCaptureTime = 0;
1601
+ function throttledCaptureVisibleTab(windowId, options) {
1602
+ const result = captureQueue.then(async () => {
1603
+ const now = Date.now();
1604
+ const elapsed = now - lastCaptureTime;
1605
+ const minInterval = 600;
1606
+ if (elapsed < minInterval) {
1607
+ await new Promise((r) => setTimeout(r, minInterval - elapsed));
1608
+ }
1609
+ lastCaptureTime = Date.now();
1610
+ return chrome.tabs.captureVisibleTab(windowId, options);
1611
+ });
1612
+ captureQueue = result.then(() => {}, () => {});
1613
+ return result;
1614
+ }
1615
+ async function handleRequest(req) {
1616
+ if (req.action === "list_tabs") {
1617
+ const tabs = await chrome.tabs.query({});
1618
+ return {
1619
+ data: tabs.filter((t) => t.url && (t.url.startsWith("http://") || t.url.startsWith("https://"))).map((t) => ({ id: t.id, url: t.url ?? "", title: t.title ?? "" }))
1620
+ };
1621
+ }
1622
+ if (req.action === "create_tab") {
1623
+ const url = req.params?.url;
1624
+ if (!url)
1625
+ throw new Error("url is required");
1626
+ if (!/^https?:\/\//i.test(url))
1627
+ throw new Error("Only http/https URLs allowed");
1628
+ const tab = await chrome.tabs.create({ url, active: false });
1629
+ if (!tab.id)
1630
+ throw new Error("Failed to get tab ID");
1631
+ await waitForTabLoad(tab.id);
1632
+ const updated = await chrome.tabs.get(tab.id);
1633
+ return {
1634
+ data: { id: tab.id, url: updated.url ?? url, title: updated.title ?? "" }
1635
+ };
1636
+ }
1637
+ if (req.action === "close_tab") {
1638
+ const tabId2 = req.params?.tabId;
1639
+ if (!tabId2)
1640
+ throw new Error("tabId is required");
1641
+ await chrome.tabs.remove(tabId2);
1642
+ return { data: { closed: true, tabId: tabId2 } };
1643
+ }
1644
+ if (req.action === "attach_debugger") {
1645
+ const tabId2 = req.params?.tabId;
1646
+ if (!tabId2)
1647
+ throw new Error("tabId is required");
1648
+ await attachDebugger(tabId2);
1649
+ return { data: { attached: true, tabId: tabId2 } };
1650
+ }
1651
+ if (req.action === "detach_debugger") {
1652
+ const tabId2 = req.params?.tabId;
1653
+ if (!tabId2)
1654
+ throw new Error("tabId is required");
1655
+ await detachDebugger(tabId2);
1656
+ return { data: { detached: true, tabId: tabId2 } };
1657
+ }
1658
+ if (req.action === "dispatch_mouse_event") {
1659
+ const tabId2 = req.params?.tabId;
1660
+ if (!tabId2)
1661
+ throw new Error("tabId is required");
1662
+ const events = req.params?.events;
1663
+ if (!events || events.length === 0)
1664
+ throw new Error("events array is required");
1665
+ const delayMs = req.params?.delayMs ?? 0;
1666
+ const waitForNavigation = req.params?.waitForNavigation === true;
1667
+ await attachDebugger(tabId2);
1668
+ const navBefore = waitForNavigation ? captureNavigationState(tabId2) : undefined;
1669
+ const tab = await chrome.tabs.get(tabId2);
1670
+ await chrome.tabs.update(tabId2, { active: true });
1671
+ await chrome.windows.update(tab.windowId, { focused: true });
1672
+ for (let i = 0;i < events.length; i++) {
1673
+ await chrome.debugger.sendCommand({ tabId: tabId2 }, "Input.dispatchMouseEvent", events[i]);
1674
+ if (delayMs > 0 && i < events.length - 1) {
1675
+ await new Promise((r) => setTimeout(r, delayMs));
1676
+ }
1677
+ }
1678
+ if (!waitForNavigation || !navBefore) {
1679
+ return { data: { dispatched: true, count: events.length } };
1680
+ }
1681
+ const navResult = await waitForPostDispatchNavigation(tabId2, navBefore);
1682
+ return {
1683
+ data: {
1684
+ dispatched: true,
1685
+ count: events.length,
1686
+ navigated: navResult.navigated,
1687
+ navigationType: navResult.navigationType,
1688
+ url: navResult.url,
1689
+ title: navResult.title
1690
+ }
1691
+ };
1692
+ }
1693
+ if (req.action === "get_console_messages") {
1694
+ const tabId2 = req.params?.tabId;
1695
+ if (!tabId2)
1696
+ throw new Error("tabId is required");
1697
+ const filter = {
1698
+ level: req.params?.level,
1699
+ textPattern: req.params?.textPattern
1700
+ };
1701
+ const result = getConsoleMessages(tabId2, filter);
1702
+ return { data: { messages: result.items, total: result.items.length, dropped: result.dropped } };
1703
+ }
1704
+ if (req.action === "get_runtime_exceptions") {
1705
+ const tabId2 = req.params?.tabId;
1706
+ if (!tabId2)
1707
+ throw new Error("tabId is required");
1708
+ const result = getRuntimeExceptions(tabId2);
1709
+ return { data: { exceptions: result.items, total: result.items.length, dropped: result.dropped } };
1710
+ }
1711
+ if (req.action === "get_console_message_detail") {
1712
+ const tabId2 = req.params?.tabId;
1713
+ if (!tabId2)
1714
+ throw new Error("tabId is required");
1715
+ const msgId = req.params?.msgId;
1716
+ if (msgId == null)
1717
+ throw new Error("msgId is required");
1718
+ const detail = await getConsoleMessageDetail(tabId2, msgId);
1719
+ return { data: detail };
1720
+ }
1721
+ if (req.action === "get_network_requests") {
1722
+ const tabId2 = req.params?.tabId;
1723
+ if (!tabId2)
1724
+ throw new Error("tabId is required");
1725
+ await attachDebugger(tabId2);
1726
+ const filter = {
1727
+ urlPattern: req.params?.urlPattern,
1728
+ method: req.params?.method,
1729
+ statusCode: req.params?.statusCode
1730
+ };
1731
+ const includePending = req.params?.includePending === true;
1732
+ const result = getNetworkRequests(tabId2, filter, { includePending });
1733
+ const captureStatus = getNetworkCaptureStatus(tabId2);
1734
+ return { data: { requests: result.items, total: result.items.length, dropped: result.dropped, captureStatus } };
1735
+ }
1736
+ if (req.action === "clear_network") {
1737
+ const tabId2 = req.params?.tabId;
1738
+ if (!tabId2)
1739
+ throw new Error("tabId is required");
1740
+ clearNetworkBuffer(tabId2);
1741
+ return { data: { cleared: true } };
1742
+ }
1743
+ if (req.action === "clear_exceptions") {
1744
+ const tabId2 = req.params?.tabId;
1745
+ if (!tabId2)
1746
+ throw new Error("tabId is required");
1747
+ clearExceptionBuffer(tabId2);
1748
+ return { data: { cleared: true } };
1749
+ }
1750
+ if (req.action === "get_network_request_detail") {
1751
+ const tabId2 = req.params?.tabId;
1752
+ if (!tabId2)
1753
+ throw new Error("tabId is required");
1754
+ await attachDebugger(tabId2);
1755
+ const requestId = req.params?.requestId;
1756
+ if (!requestId)
1757
+ throw new Error("requestId is required");
1758
+ const detail = await getNetworkRequestDetail(tabId2, requestId);
1759
+ return { data: detail };
1760
+ }
1761
+ if (req.action === "get_storage") {
1762
+ const tabId2 = req.params?.tabId;
1763
+ if (!tabId2)
1764
+ throw new Error("tabId is required");
1765
+ const type = req.params?.type;
1766
+ if (!type || !["cookies", "localStorage", "sessionStorage"].includes(type)) {
1767
+ throw new Error("type must be one of: cookies, localStorage, sessionStorage");
1768
+ }
1769
+ const namePattern = req.params?.namePattern;
1770
+ await attachDebugger(tabId2);
1771
+ if (type === "cookies") {
1772
+ let urls = req.params?.urls;
1773
+ if (!urls || urls.length === 0) {
1774
+ const tab = await chrome.tabs.get(tabId2);
1775
+ urls = tab.url ? [tab.url] : [];
1776
+ }
1777
+ const result2 = await chrome.debugger.sendCommand({ tabId: tabId2 }, "Network.getCookies", { urls });
1778
+ let cookies = result2.cookies ?? [];
1779
+ if (namePattern) {
1780
+ cookies = cookies.filter((c) => c.name.includes(namePattern));
1781
+ }
1782
+ return { data: { cookies } };
1783
+ }
1784
+ const storageType = type === "sessionStorage" ? "sessionStorage" : "localStorage";
1785
+ const expression = `(() => {
1786
+ const s = ${storageType};
1787
+ const entries = {};
1788
+ for (let i = 0; i < s.length; i++) {
1789
+ const key = s.key(i);
1790
+ if (key !== null) entries[key] = s.getItem(key) ?? "";
1791
+ }
1792
+ return { entries, count: s.length };
1793
+ })()`;
1794
+ const evalResult = await chrome.debugger.sendCommand({ tabId: tabId2 }, "Runtime.evaluate", { expression, returnByValue: true, awaitPromise: false });
1795
+ const result = evalResult.result?.value;
1796
+ if (!result)
1797
+ throw new Error(`Failed to read ${storageType}`);
1798
+ if (namePattern) {
1799
+ const filtered = {};
1800
+ let count = 0;
1801
+ for (const [k, v] of Object.entries(result.entries)) {
1802
+ if (k.includes(namePattern)) {
1803
+ filtered[k] = v;
1804
+ count++;
1805
+ }
1806
+ }
1807
+ return { data: { entries: filtered, count } };
1808
+ }
1809
+ return { data: result };
1810
+ }
1811
+ if (req.action === "get_sent_cookies") {
1812
+ const tabId2 = req.params?.tabId;
1813
+ if (!tabId2)
1814
+ throw new Error("tabId is required");
1815
+ const requestId = req.params?.requestId;
1816
+ return { data: { cookies: getSentCookies(tabId2, requestId) } };
1817
+ }
1818
+ if (req.action === "set_cookie") {
1819
+ const tabId2 = req.params?.tabId;
1820
+ if (!tabId2)
1821
+ throw new Error("tabId is required");
1822
+ await attachDebugger(tabId2);
1823
+ const cookieParams = {};
1824
+ for (const key of ["name", "value", "url", "domain", "path", "sameSite"]) {
1825
+ if (req.params?.[key] != null)
1826
+ cookieParams[key] = req.params[key];
1827
+ }
1828
+ for (const key of ["secure", "httpOnly"]) {
1829
+ if (req.params?.[key] != null)
1830
+ cookieParams[key] = req.params[key];
1831
+ }
1832
+ if (req.params?.expires != null)
1833
+ cookieParams.expires = req.params.expires;
1834
+ const result = await chrome.debugger.sendCommand({ tabId: tabId2 }, "Network.setCookie", cookieParams);
1835
+ const success = result.success ?? false;
1836
+ return { data: { success } };
1837
+ }
1838
+ if (req.action === "delete_cookie") {
1839
+ const tabId2 = req.params?.tabId;
1840
+ if (!tabId2)
1841
+ throw new Error("tabId is required");
1842
+ await attachDebugger(tabId2);
1843
+ const deleteParams = {
1844
+ name: req.params?.name
1845
+ };
1846
+ if (req.params?.url != null)
1847
+ deleteParams.url = req.params.url;
1848
+ if (req.params?.domain != null)
1849
+ deleteParams.domain = req.params.domain;
1850
+ if (req.params?.path != null)
1851
+ deleteParams.path = req.params.path;
1852
+ await chrome.debugger.sendCommand({ tabId: tabId2 }, "Network.deleteCookies", deleteParams);
1853
+ return { data: {} };
1854
+ }
1855
+ if (req.action === "get_performance_metrics") {
1856
+ const tabId2 = req.params?.tabId;
1857
+ if (!tabId2)
1858
+ throw new Error("tabId is required");
1859
+ const metrics = await getPerformanceMetrics(tabId2);
1860
+ return { data: { metrics } };
1861
+ }
1862
+ if (req.action === "start_trace") {
1863
+ const tabId2 = req.params?.tabId;
1864
+ if (!tabId2)
1865
+ throw new Error("tabId is required");
1866
+ const reload = req.params?.reload;
1867
+ const result = await startTrace(tabId2, reload);
1868
+ return { data: result };
1869
+ }
1870
+ if (req.action === "stop_trace") {
1871
+ const tabId2 = req.params?.tabId;
1872
+ if (!tabId2)
1873
+ throw new Error("tabId is required");
1874
+ const events = await stopTrace(tabId2);
1875
+ return { data: { events, total: events.length } };
1876
+ }
1877
+ if (req.action === "get_accessibility_tree") {
1878
+ const tabId2 = req.params?.tabId;
1879
+ if (!tabId2)
1880
+ throw new Error("tabId is required");
1881
+ const tree = await getAccessibilityTree(tabId2);
1882
+ return { data: tree };
1883
+ }
1884
+ if (req.action === "get_partial_accessibility_tree") {
1885
+ const tabId2 = req.params?.tabId;
1886
+ if (!tabId2)
1887
+ throw new Error("tabId is required");
1888
+ let backendNodeId = req.params?.backendNodeId;
1889
+ const selector = req.params?.selector;
1890
+ if (!backendNodeId && selector) {
1891
+ await attachDebugger(tabId2);
1892
+ backendNodeId = await resolveSelector(tabId2, selector);
1893
+ }
1894
+ if (backendNodeId == null)
1895
+ throw new Error("backendNodeId or selector is required");
1896
+ await attachDebugger(tabId2);
1897
+ const nodes = await getPartialAccessibilityTree(tabId2, backendNodeId);
1898
+ return { data: { nodes, total: nodes.length } };
1899
+ }
1900
+ if (req.action === "query_ax_tree") {
1901
+ const tabId2 = req.params?.tabId;
1902
+ if (!tabId2)
1903
+ throw new Error("tabId is required");
1904
+ let backendNodeId = req.params?.backendNodeId;
1905
+ const selector = req.params?.selector;
1906
+ if (!backendNodeId && selector) {
1907
+ await attachDebugger(tabId2);
1908
+ backendNodeId = await resolveSelector(tabId2, selector);
1909
+ }
1910
+ if (backendNodeId == null)
1911
+ throw new Error("backendNodeId or selector is required");
1912
+ await attachDebugger(tabId2);
1913
+ const nodes = await queryAccessibilitySubtree(tabId2, backendNodeId);
1914
+ return { data: { nodes, total: nodes.length } };
1915
+ }
1916
+ if (req.action === "find_elements_accessibility") {
1917
+ const tabId2 = req.params?.tabId;
1918
+ if (!tabId2)
1919
+ throw new Error("tabId is required");
1920
+ await attachDebugger(tabId2);
1921
+ const filters = {
1922
+ role: req.params?.role,
1923
+ name: req.params?.name,
1924
+ description: req.params?.description
1925
+ };
1926
+ const results = await findElementsByAccessibility(tabId2, filters);
1927
+ return { data: { elements: results, total: results.length } };
1928
+ }
1929
+ if (req.action === "click_by_backend_node_id") {
1930
+ const tabId2 = req.params?.tabId;
1931
+ if (!tabId2)
1932
+ throw new Error("tabId is required");
1933
+ const backendNodeId = req.params?.backendNodeId;
1934
+ if (backendNodeId == null)
1935
+ throw new Error("backendNodeId is required");
1936
+ const waitForNavigation = req.params?.waitForNavigation === true;
1937
+ await attachDebugger(tabId2);
1938
+ const result = await clickByBackendNodeId(tabId2, backendNodeId, { waitForNavigation });
1939
+ return { data: result };
1940
+ }
1941
+ if (req.action === "set_value_by_backend_node_id") {
1942
+ const tabId2 = req.params?.tabId;
1943
+ if (!tabId2)
1944
+ throw new Error("tabId is required");
1945
+ const backendNodeId = req.params?.backendNodeId;
1946
+ if (backendNodeId == null)
1947
+ throw new Error("backendNodeId is required");
1948
+ const value = req.params?.value;
1949
+ if (value == null)
1950
+ throw new Error("value is required");
1951
+ const verify = req.params?.verify === true;
1952
+ await attachDebugger(tabId2);
1953
+ const result = await setValueByBackendNodeId(tabId2, backendNodeId, value, verify || undefined);
1954
+ return { data: result };
1955
+ }
1956
+ if (req.action === "get_attributes_by_backend_node_id") {
1957
+ const tabId2 = req.params?.tabId;
1958
+ if (!tabId2)
1959
+ throw new Error("tabId is required");
1960
+ const backendNodeId = req.params?.backendNodeId;
1961
+ if (backendNodeId == null)
1962
+ throw new Error("backendNodeId is required");
1963
+ await attachDebugger(tabId2);
1964
+ const result = await getAttributesByBackendNodeId(tabId2, backendNodeId);
1965
+ return { data: { attributes: result } };
1966
+ }
1967
+ if (req.action === "get_script_meta_cache") {
1968
+ const tabId2 = req.params?.tabId;
1969
+ if (!tabId2)
1970
+ throw new Error("tabId is required");
1971
+ const result = getScriptMetaCache(tabId2);
1972
+ return { data: result };
1973
+ }
1974
+ if (req.action === "get_detached_dom_nodes") {
1975
+ const tabId2 = req.params?.tabId;
1976
+ if (!tabId2)
1977
+ throw new Error("tabId is required");
1978
+ const result = await getDetachedDomNodes(tabId2);
1979
+ return { data: { detachedNodes: result } };
1980
+ }
1981
+ if (req.action === "get_event_listeners_for_node") {
1982
+ const tabId2 = req.params?.tabId;
1983
+ const backendNodeId = req.params?.backendNodeId;
1984
+ if (!tabId2)
1985
+ throw new Error("tabId is required");
1986
+ if (backendNodeId == null)
1987
+ throw new Error("backendNodeId is required");
1988
+ const result = await getEventListenersForNode(tabId2, backendNodeId);
1989
+ return { data: result };
1990
+ }
1991
+ if (req.action === "css_inspect") {
1992
+ const tabId2 = req.params?.tabId;
1993
+ if (!tabId2)
1994
+ throw new Error("tabId is required");
1995
+ const selector = req.params?.selector;
1996
+ const backendNodeId = req.params?.backendNodeId;
1997
+ if (!selector && backendNodeId == null)
1998
+ throw new Error("selector or backendNodeId is required");
1999
+ const result = await cssInspect(tabId2, {
2000
+ selector,
2001
+ backendNodeId,
2002
+ properties: req.params?.properties,
2003
+ ancestorDepth: req.params?.ancestorDepth,
2004
+ includeComputed: req.params?.includeComputed,
2005
+ includeMeasured: req.params?.includeMeasured
2006
+ });
2007
+ return { data: result };
2008
+ }
2009
+ if (req.action === "evaluate") {
2010
+ const tabId2 = req.params?.tabId;
2011
+ if (!tabId2)
2012
+ throw new Error("tabId is required");
2013
+ const expression = req.params?.expression;
2014
+ if (!expression)
2015
+ throw new Error("expression is required");
2016
+ await attachDebugger(tabId2);
2017
+ try {
2018
+ const result = await chrome.debugger.sendCommand({ tabId: tabId2 }, "Runtime.evaluate", {
2019
+ expression,
2020
+ returnByValue: true,
2021
+ awaitPromise: true
2022
+ });
2023
+ const evalResult = result;
2024
+ if (evalResult.exceptionDetails) {
2025
+ const errMsg = evalResult.exceptionDetails.exception?.description ?? evalResult.exceptionDetails.text ?? "Evaluation failed";
2026
+ return { data: { result: `Error: ${errMsg}` } };
2027
+ }
2028
+ const value = evalResult.result?.value ?? null;
2029
+ return { data: { result: value } };
2030
+ } catch (e) {
2031
+ const msg2 = e instanceof Error ? e.message : String(e);
2032
+ return { data: { result: `Error: ${msg2}` } };
2033
+ }
2034
+ }
2035
+ if (req.action === "upload_file") {
2036
+ const tabId2 = req.params?.tabId;
2037
+ if (!tabId2)
2038
+ throw new Error("tabId is required");
2039
+ const filePath = req.params?.filePath;
2040
+ if (!filePath)
2041
+ throw new Error("filePath is required");
2042
+ const selector = req.params?.selector ?? 'input[type="file"]';
2043
+ await attachDebugger(tabId2);
2044
+ try {
2045
+ const doc = await chrome.debugger.sendCommand({ tabId: tabId2 }, "DOM.getDocument");
2046
+ const queryResult = await chrome.debugger.sendCommand({ tabId: tabId2 }, "DOM.querySelector", { nodeId: doc.root.nodeId, selector });
2047
+ if (!queryResult.nodeId) {
2048
+ throw new Error(`File input not found: "${selector}"`);
2049
+ }
2050
+ await chrome.debugger.sendCommand({ tabId: tabId2 }, "DOM.setFileInputFiles", { files: [filePath], nodeId: queryResult.nodeId });
2051
+ await chrome.debugger.sendCommand({ tabId: tabId2 }, "Runtime.evaluate", {
2052
+ expression: `(() => {
2053
+ const el = document.querySelector(${JSON.stringify(selector)});
2054
+ if (el) {
2055
+ el.dispatchEvent(new Event('input', { bubbles: true }));
2056
+ el.dispatchEvent(new Event('change', { bubbles: true }));
2057
+ }
2058
+ })()`,
2059
+ returnByValue: true
2060
+ });
2061
+ return { data: { uploaded: true, selector, filePath } };
2062
+ } catch (e) {
2063
+ const msg2 = e instanceof Error ? e.message : String(e);
2064
+ throw new Error(`upload_file failed: ${msg2}`);
2065
+ }
2066
+ }
2067
+ if (req.action === "start_recording") {
2068
+ const tabId2 = req.params?.tabId;
2069
+ if (!tabId2)
2070
+ throw new Error("tabId is required");
2071
+ const sessionId = req.params?.sessionId;
2072
+ if (!sessionId)
2073
+ throw new Error("sessionId is required");
2074
+ const fps = req.params?.fps ?? 15;
2075
+ const quality = req.params?.quality ?? 80;
2076
+ const maxDurationSec = req.params?.maxDurationSec ?? 300;
2077
+ await chrome.tabs.update(tabId2, { active: true });
2078
+ const existing = getActiveScreencast();
2079
+ const state = await startRecording(tabId2, sessionId, fps, quality, maxDurationSec);
2080
+ return {
2081
+ data: {
2082
+ sessionId: state.sessionId,
2083
+ tabId: state.tabId,
2084
+ startedAt: state.startedAt,
2085
+ alreadyRecording: existing !== null
2086
+ }
2087
+ };
2088
+ }
2089
+ if (req.action === "stop_recording") {
2090
+ const result = await stopRecording();
2091
+ if (!result)
2092
+ throw new Error("No active recording");
2093
+ return { data: result };
2094
+ }
2095
+ if (req.action === "recording_status") {
2096
+ const state = getActiveScreencast();
2097
+ if (!state) {
2098
+ return { data: { recording: false } };
2099
+ }
2100
+ return {
2101
+ data: {
2102
+ recording: true,
2103
+ sessionId: state.sessionId,
2104
+ tabId: state.tabId,
2105
+ frameCount: state.frameCount,
2106
+ durationSec: Math.round((Date.now() - state.startedAt) / 1000)
2107
+ }
2108
+ };
2109
+ }
2110
+ if (req.action === "screenshot") {
2111
+ const tabId2 = req.params?.tabId;
2112
+ if (!tabId2)
2113
+ throw new Error("tabId is required");
2114
+ const tab = await chrome.tabs.get(tabId2);
2115
+ await chrome.tabs.update(tabId2, { active: true });
2116
+ await chrome.windows.update(tab.windowId, { focused: true });
2117
+ const quality = req.params?.quality ?? 70;
2118
+ const format = req.params?.format === "png" ? "png" : "jpeg";
2119
+ let screenshot2;
2120
+ for (let attempt = 0;attempt < 3; attempt++) {
2121
+ try {
2122
+ screenshot2 = await throttledCaptureVisibleTab(tab.windowId, {
2123
+ format,
2124
+ quality: format === "jpeg" ? quality : undefined
2125
+ });
2126
+ break;
2127
+ } catch (e) {
2128
+ if (attempt === 2)
2129
+ throw new Error(`Failed to capture tab: ${e.message}`);
2130
+ }
2131
+ }
2132
+ return { data: { captured: true }, screenshot: screenshot2 };
2133
+ }
2134
+ if (req.action === "print_to_pdf") {
2135
+ const tabId2 = req.params?.tabId;
2136
+ if (!tabId2)
2137
+ throw new Error("tabId is required");
2138
+ await attachDebugger(tabId2);
2139
+ const cdpParams = {};
2140
+ for (const key of [
2141
+ "landscape",
2142
+ "printBackground",
2143
+ "scale",
2144
+ "paperWidth",
2145
+ "paperHeight",
2146
+ "pageRanges",
2147
+ "preferCSSPageSize"
2148
+ ]) {
2149
+ if (req.params?.[key] != null)
2150
+ cdpParams[key] = req.params[key];
2151
+ }
2152
+ const result = await chrome.debugger.sendCommand({ tabId: tabId2 }, "Page.printToPDF", cdpParams);
2153
+ return { data: { data: result?.data } };
2154
+ }
2155
+ if (req.action === "wait_for_download") {
2156
+ const timeoutMs = req.params?.timeout ?? 30000;
2157
+ return waitForDownload(timeoutMs);
2158
+ }
2159
+ const tabId = req.params?.tabId;
2160
+ if (!tabId) {
2161
+ throw new Error("tabId is required");
2162
+ }
2163
+ if (req.action === "go_back") {
2164
+ await chrome.scripting.executeScript({
2165
+ target: { tabId },
2166
+ func: () => window.history.back()
2167
+ });
2168
+ await waitForTabLoad(tabId);
2169
+ } else if (req.action === "go_forward") {
2170
+ await chrome.scripting.executeScript({
2171
+ target: { tabId },
2172
+ func: () => window.history.forward()
2173
+ });
2174
+ await waitForTabLoad(tabId);
2175
+ } else if (req.action === "reload") {
2176
+ await chrome.tabs.reload(tabId);
2177
+ await waitForTabLoad(tabId);
2178
+ } else if (req.action === "get_page" && req.params?.url) {
2179
+ const navUrl = req.params.url;
2180
+ if (!/^https?:\/\//i.test(navUrl))
2181
+ throw new Error("Only http/https URLs allowed");
2182
+ await chrome.tabs.update(tabId, { url: navUrl });
2183
+ await waitForTabLoad(tabId);
2184
+ }
2185
+ const NAV_ACTIONS = new Set(["go_back", "go_forward", "reload"]);
2186
+ const contentAction = NAV_ACTIONS.has(req.action) ? "get_page" : req.action;
2187
+ const msg = { action: contentAction, params: req.params };
2188
+ let data;
2189
+ try {
2190
+ data = await chrome.tabs.sendMessage(tabId, msg);
2191
+ } catch (e) {
2192
+ const errMsg = e instanceof Error ? e.message : String(e);
2193
+ await chrome.scripting.executeScript({
2194
+ target: { tabId },
2195
+ files: ["content.js"]
2196
+ });
2197
+ const delay = errMsg.includes("message channel closed") ? 500 : 300;
2198
+ await new Promise((r) => setTimeout(r, delay));
2199
+ data = await chrome.tabs.sendMessage(tabId, msg);
2200
+ }
2201
+ let screenshot = null;
2202
+ if (req.params?.include_screenshot === true) {
2203
+ try {
2204
+ const tab = await chrome.tabs.get(tabId);
2205
+ await chrome.tabs.update(tabId, { active: true });
2206
+ await chrome.windows.update(tab.windowId, { focused: true });
2207
+ screenshot = await throttledCaptureVisibleTab(tab.windowId, {
2208
+ format: "jpeg",
2209
+ quality: 70
2210
+ });
2211
+ } catch (e) {
2212
+ const errMsg = e instanceof Error ? e.message : String(e);
2213
+ console.warn("[MCP Bridge] Screenshot failed:", errMsg);
2214
+ }
2215
+ }
2216
+ return { data, screenshot };
2217
+ }
2218
+
2219
+ // apps/extension/src/background/connection.ts
2220
+ var ws = null;
2221
+ var reconnectTimer = null;
2222
+ async function connect() {
2223
+ if (ws) {
2224
+ ws.onclose = null;
2225
+ ws.close();
2226
+ }
2227
+ const stored = await chrome.storage.local.get(["mcpToken", "mcpPort"]);
2228
+ const token = stored.mcpToken;
2229
+ const port = stored.mcpPort ?? 18888;
2230
+ const protocols = token ? [token] : [];
2231
+ ws = new WebSocket(`ws://127.0.0.1:${port}`, protocols);
2232
+ ws.onopen = async () => {
2233
+ console.log("[MCP Bridge] Connected to MCP server");
2234
+ const stored2 = await chrome.storage.session.get("lastReloadId");
2235
+ const lastReloadId = stored2.lastReloadId ?? null;
2236
+ if (lastReloadId) {
2237
+ await chrome.storage.session.remove("lastReloadId");
2238
+ }
2239
+ if (ws?.readyState === WebSocket.OPEN) {
2240
+ ws.send(JSON.stringify({
2241
+ type: "hello",
2242
+ extensionId: chrome.runtime.id,
2243
+ lastReloadId,
2244
+ timestamp: Date.now()
2245
+ }));
2246
+ }
2247
+ if (reconnectTimer) {
2248
+ clearInterval(reconnectTimer);
2249
+ reconnectTimer = null;
2250
+ }
2251
+ setSendFrameCallback((data) => {
2252
+ if (ws?.readyState === WebSocket.OPEN) {
2253
+ ws.send(JSON.stringify(data));
2254
+ }
2255
+ });
2256
+ };
2257
+ ws.onmessage = async (event) => {
2258
+ let req;
2259
+ try {
2260
+ req = JSON.parse(event.data);
2261
+ } catch {
2262
+ console.error("[MCP Bridge] Invalid JSON received");
2263
+ return;
2264
+ }
2265
+ if (req.type === "reload") {
2266
+ const reloadId = req.reloadId ?? null;
2267
+ console.log(`[MCP Bridge] Reload requested (reloadId=${reloadId}), reloading extension...`);
2268
+ chrome.storage.session.set({ lastReloadId: reloadId }).then(() => {
2269
+ chrome.runtime.reload();
2270
+ });
2271
+ return;
2272
+ }
2273
+ if (!req.id || !req.action) {
2274
+ console.error("[MCP Bridge] Missing id or action");
2275
+ return;
2276
+ }
2277
+ try {
2278
+ console.log("[MCP Bridge] Handling request:", req.action, req.params);
2279
+ const result = await handleRequest(req);
2280
+ console.log("[MCP Bridge] Request succeeded:", req.action);
2281
+ if (ws?.readyState === WebSocket.OPEN) {
2282
+ ws.send(JSON.stringify({ id: req.id, status: "ok", ...result }));
2283
+ }
2284
+ } catch (e) {
2285
+ const msg = e instanceof Error ? e.message : String(e);
2286
+ console.error("[MCP Bridge] Request failed:", req.action, msg);
2287
+ if (ws?.readyState === WebSocket.OPEN) {
2288
+ ws.send(JSON.stringify({
2289
+ id: req.id,
2290
+ status: "error",
2291
+ error: msg,
2292
+ message: msg
2293
+ }));
2294
+ }
2295
+ }
2296
+ };
2297
+ ws.onclose = () => {
2298
+ console.log("[MCP Bridge] Disconnected. Reconnecting...");
2299
+ onWsDisconnect();
2300
+ scheduleReconnect();
2301
+ };
2302
+ ws.onerror = () => ws?.close();
2303
+ }
2304
+ function scheduleReconnect() {
2305
+ if (!reconnectTimer) {
2306
+ reconnectTimer = setInterval(connect, 5000);
2307
+ }
2308
+ }
2309
+ function isConnected() {
2310
+ return ws !== null && ws.readyState === WebSocket.OPEN;
2311
+ }
2312
+
2313
+ // apps/extension/src/background.ts
2314
+ initDebuggerListeners();
2315
+ setScreencastFrameHandler(handleScreencastFrame);
2316
+ chrome.alarms.create("keepalive", { periodInMinutes: 1 });
2317
+ chrome.alarms.onAlarm.addListener((alarm) => {
2318
+ if (alarm.name === "keepalive") {
2319
+ if (!isConnected()) {
2320
+ connect();
2321
+ }
2322
+ }
2323
+ });
2324
+ reinjectContentScripts();
2325
+ chrome.storage.onChanged.addListener((changes, area) => {
2326
+ if (area === "local" && (changes.mcpToken || changes.mcpPort)) {
2327
+ console.log("[BP] Token changed, reconnecting...");
2328
+ connect();
2329
+ }
2330
+ });
2331
+ chrome.tabs.onRemoved.addListener(onTabRemoved);
2332
+ connect();