teamshare-bridge 0.26.12 → 0.26.14
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cli/commands/research.js +15 -0
- package/dist/cli/commands/research.js.map +1 -1
- package/dist/lib/browser/batch.d.ts +62 -2
- package/dist/lib/browser/batch.js +90 -5
- package/dist/lib/browser/batch.js.map +1 -1
- package/dist/lib/browser/browse-cdp.js +5 -1
- package/dist/lib/browser/browse-cdp.js.map +1 -1
- package/dist/lib/browser/browse-editor.js +8 -0
- package/dist/lib/browser/browse-editor.js.map +1 -1
- package/dist/lib/browser/cdp.d.ts +11 -0
- package/dist/lib/browser/cdp.js +36 -0
- package/dist/lib/browser/cdp.js.map +1 -1
- package/dist/lib/browser/index.d.ts +3 -1
- package/dist/lib/browser/index.js +12 -1
- package/dist/lib/browser/index.js.map +1 -1
- package/dist/lib/browser/login.d.ts +67 -25
- package/dist/lib/browser/login.js +681 -174
- package/dist/lib/browser/login.js.map +1 -1
- package/dist/lib/browser/network-log.d.ts +40 -0
- package/dist/lib/browser/network-log.js +115 -12
- package/dist/lib/browser/network-log.js.map +1 -1
- package/dist/lib/browser/page-context.d.ts +198 -0
- package/dist/lib/browser/page-context.js +804 -0
- package/dist/lib/browser/page-context.js.map +1 -0
- package/dist/lib/browser/page-events.d.ts +39 -0
- package/dist/lib/browser/page-events.js +128 -0
- package/dist/lib/browser/page-events.js.map +1 -0
- package/dist/lib/browser/screen-change.js +3 -0
- package/dist/lib/browser/screen-change.js.map +1 -1
- package/dist/lib/browser/session.d.ts +159 -3
- package/dist/lib/browser/session.js +596 -7
- package/dist/lib/browser/session.js.map +1 -1
- package/dist/lib/orchestrator/handlers/browser.js +300 -31
- package/dist/lib/orchestrator/handlers/browser.js.map +1 -1
- package/package.json +1 -1
|
@@ -55,6 +55,9 @@ const launch_1 = require("./launch");
|
|
|
55
55
|
const owned_registry_1 = require("./owned-registry");
|
|
56
56
|
const process_1 = require("./process");
|
|
57
57
|
const login_1 = require("./login");
|
|
58
|
+
const page_context_1 = require("./page-context");
|
|
59
|
+
const page_events_1 = require("./page-events");
|
|
60
|
+
const screen_change_1 = require("./screen-change");
|
|
58
61
|
/**
|
|
59
62
|
* Persistent Chrome profile for the AUTO-ANSWER browser (research reads,
|
|
60
63
|
* `browse --auto-answer`, the web-auto-answer tool).
|
|
@@ -78,6 +81,12 @@ exports.AUTO_ANSWER_CDP_PORT = 9222;
|
|
|
78
81
|
exports.DEBUG_CDP_PORT = 9333;
|
|
79
82
|
/** Separate user-data-dir for the debug browser (see DEBUG_CDP_PORT). */
|
|
80
83
|
exports.DEBUG_PROFILE_DIR = (0, node_path_1.join)((0, node_os_1.homedir)(), '.teamshare', 'chrome-profile-debug');
|
|
84
|
+
/**
|
|
85
|
+
* Pages that already carry the debug instrumentation (dialog listener,
|
|
86
|
+
* frame-navigation re-inject). A WeakSet, because a reconnect creates a new
|
|
87
|
+
* CdpPage and must instrument it again.
|
|
88
|
+
*/
|
|
89
|
+
const instrumentedPages = new WeakSet();
|
|
81
90
|
class BrowserSession {
|
|
82
91
|
id;
|
|
83
92
|
page = null;
|
|
@@ -102,6 +111,13 @@ class BrowserSession {
|
|
|
102
111
|
* { action: "networkLog" }` when it needs to prove an API mismatch.
|
|
103
112
|
*/
|
|
104
113
|
network = new network_log_1.NetworkLog();
|
|
114
|
+
/**
|
|
115
|
+
* The native alert/confirm/prompt/beforeunload currently blocking the page
|
|
116
|
+
* (2026-10). Captured from `Page.javascriptDialogOpening`; other actions are
|
|
117
|
+
* refused while it is set, and `browser_action({action:"dialog"})` resolves
|
|
118
|
+
* it. A native dialog blocks the renderer, so a page snapshot would hang.
|
|
119
|
+
*/
|
|
120
|
+
_nativeDialog = null;
|
|
105
121
|
constructor(id, opts) {
|
|
106
122
|
this.id = id;
|
|
107
123
|
this._port = opts?.port ?? exports.DEBUG_CDP_PORT;
|
|
@@ -155,6 +171,10 @@ class BrowserSession {
|
|
|
155
171
|
get allowCredentials() {
|
|
156
172
|
return this._allowCredentials;
|
|
157
173
|
}
|
|
174
|
+
/** The native JS dialog currently blocking the page, if any. */
|
|
175
|
+
get nativeDialog() {
|
|
176
|
+
return this._nativeDialog;
|
|
177
|
+
}
|
|
158
178
|
/** Last time a command actually used this session (drives idle close). */
|
|
159
179
|
_lastUsedAt = Date.now();
|
|
160
180
|
/** When this session was last used — the idle-close timer reads this. */
|
|
@@ -199,6 +219,7 @@ class BrowserSession {
|
|
|
199
219
|
async connect(port) {
|
|
200
220
|
if (port)
|
|
201
221
|
this._port = port;
|
|
222
|
+
this._nativeDialog = null;
|
|
202
223
|
this.touch();
|
|
203
224
|
// Try an existing Chrome first (the human may have started a debug Chrome
|
|
204
225
|
// and signed in on it — reusing it keeps their session and cookies).
|
|
@@ -242,6 +263,9 @@ class BrowserSession {
|
|
|
242
263
|
}
|
|
243
264
|
// Capture the network + console stream for the whole session (QA evidence).
|
|
244
265
|
await this.network.attach(this.page);
|
|
266
|
+
// Rich page context: persistent toast/dialog buffer, screen-change
|
|
267
|
+
// observer and native-dialog capture (2026-10).
|
|
268
|
+
await this.instrumentPage(this.page);
|
|
245
269
|
return (0, cdp_1.getPageInfo)(this.page);
|
|
246
270
|
}
|
|
247
271
|
/**
|
|
@@ -265,11 +289,389 @@ class BrowserSession {
|
|
|
265
289
|
/* best-effort */
|
|
266
290
|
}
|
|
267
291
|
await this.network.attach(this.page);
|
|
292
|
+
await this.instrumentPage(this.page);
|
|
268
293
|
return (0, cdp_1.getPageInfo)(this.page);
|
|
269
294
|
}
|
|
270
295
|
/**
|
|
271
|
-
*
|
|
296
|
+
* Install the debug browser's page instrumentation (2026-10):
|
|
297
|
+
*
|
|
298
|
+
* - the persistent toast/alert/dialog ring buffer (`page-events.ts`),
|
|
299
|
+
* registered for FUTURE documents via
|
|
300
|
+
* `Page.addScriptToEvaluateOnNewDocument` and injected into the current
|
|
301
|
+
* one immediately;
|
|
302
|
+
* - the screen-change observer (`__tsScreenObs.mutated`) so `waitFor
|
|
303
|
+
* {for:"change"}` knows a click had an effect without a blind sleep;
|
|
304
|
+
* - the native-dialog listener (`Page.javascriptDialogOpening`), because an
|
|
305
|
+
* alert/confirm/prompt blocks the renderer and must be surfaced and
|
|
306
|
+
* resolved explicitly.
|
|
307
|
+
*
|
|
308
|
+
* All pieces are idempotent in the page. The CDP listeners are bound once per
|
|
309
|
+
* page object (a reconnect creates a new one).
|
|
272
310
|
*/
|
|
311
|
+
async instrumentPage(page) {
|
|
312
|
+
try {
|
|
313
|
+
await (0, cdp_1.cdpSend)(page, 'Page.enable', {});
|
|
314
|
+
}
|
|
315
|
+
catch {
|
|
316
|
+
/* already enabled / restricted */
|
|
317
|
+
}
|
|
318
|
+
try {
|
|
319
|
+
await (0, cdp_1.cdpSend)(page, 'Page.addScriptToEvaluateOnNewDocument', {
|
|
320
|
+
source: (0, page_events_1.buildPageEventScript)(),
|
|
321
|
+
});
|
|
322
|
+
}
|
|
323
|
+
catch {
|
|
324
|
+
/* older Chrome may refuse; the immediate injection still runs */
|
|
325
|
+
}
|
|
326
|
+
const install = async () => {
|
|
327
|
+
try {
|
|
328
|
+
await (0, cdp_1.evaluateJs)(page, (0, page_events_1.buildPageEventScript)());
|
|
329
|
+
}
|
|
330
|
+
catch {
|
|
331
|
+
/* document not ready yet; the new-document script covers it */
|
|
332
|
+
}
|
|
333
|
+
try {
|
|
334
|
+
await (0, cdp_1.evaluateJs)(page, (0, screen_change_1.buildObserverScript)());
|
|
335
|
+
}
|
|
336
|
+
catch {
|
|
337
|
+
/* best-effort */
|
|
338
|
+
}
|
|
339
|
+
};
|
|
340
|
+
await install();
|
|
341
|
+
if (instrumentedPages.has(page))
|
|
342
|
+
return;
|
|
343
|
+
instrumentedPages.add(page);
|
|
344
|
+
(0, cdp_1.onCdpEvent)(page, 'Page.javascriptDialogOpening', (params) => {
|
|
345
|
+
this._nativeDialog = {
|
|
346
|
+
type: String(params?.type ?? 'dialog'),
|
|
347
|
+
message: String(params?.message ?? ''),
|
|
348
|
+
...(typeof params?.defaultPrompt === 'string'
|
|
349
|
+
? { defaultPrompt: params.defaultPrompt }
|
|
350
|
+
: {}),
|
|
351
|
+
at: Date.now(),
|
|
352
|
+
};
|
|
353
|
+
});
|
|
354
|
+
(0, cdp_1.onCdpEvent)(page, 'Page.javascriptDialogClosed', () => {
|
|
355
|
+
this._nativeDialog = null;
|
|
356
|
+
});
|
|
357
|
+
// `Page.addScript...` already covers a real navigation; this is the belt
|
|
358
|
+
// and braces path for a document that was already loading when we attached.
|
|
359
|
+
(0, cdp_1.onCdpEvent)(page, 'Page.frameNavigated', (params) => {
|
|
360
|
+
if (!(0, screen_change_1.isMainFrameNavigation)(params))
|
|
361
|
+
return;
|
|
362
|
+
setTimeout(() => {
|
|
363
|
+
void install();
|
|
364
|
+
}, 600);
|
|
365
|
+
});
|
|
366
|
+
}
|
|
367
|
+
/**
|
|
368
|
+
* Resolve a native JS dialog (alert/confirm/prompt/beforeunload).
|
|
369
|
+
* `accept:false` dismisses it; `promptText` is used by a prompt.
|
|
370
|
+
*/
|
|
371
|
+
async dialogAction(opts) {
|
|
372
|
+
this.ensureConnected();
|
|
373
|
+
const current = this._nativeDialog;
|
|
374
|
+
if (!current) {
|
|
375
|
+
return { ok: false, error: 'No native browser dialog is open.' };
|
|
376
|
+
}
|
|
377
|
+
try {
|
|
378
|
+
await (0, cdp_1.cdpSend)(this.page, 'Page.handleJavaScriptDialog', {
|
|
379
|
+
accept: opts.accept !== false,
|
|
380
|
+
...(typeof opts.promptText === 'string'
|
|
381
|
+
? { promptText: opts.promptText }
|
|
382
|
+
: {}),
|
|
383
|
+
});
|
|
384
|
+
this._nativeDialog = null;
|
|
385
|
+
return {
|
|
386
|
+
ok: true,
|
|
387
|
+
accepted: opts.accept !== false,
|
|
388
|
+
type: current.type,
|
|
389
|
+
message: current.message,
|
|
390
|
+
};
|
|
391
|
+
}
|
|
392
|
+
catch (err) {
|
|
393
|
+
return {
|
|
394
|
+
ok: false,
|
|
395
|
+
error: err instanceof Error ? err.message : String(err),
|
|
396
|
+
};
|
|
397
|
+
}
|
|
398
|
+
}
|
|
399
|
+
/**
|
|
400
|
+
* Current pagination state (controls, page, page size, total, rows).
|
|
401
|
+
* Scoped to the top dialog by the page-state script.
|
|
402
|
+
*/
|
|
403
|
+
async pagination() {
|
|
404
|
+
this.ensureConnected();
|
|
405
|
+
const state = await (0, page_context_1.readPageState)(this.page, { includeText: false });
|
|
406
|
+
return state.pagination;
|
|
407
|
+
}
|
|
408
|
+
/** Stable-ish signature used to prove a page actually changed. */
|
|
409
|
+
async paginationSignature() {
|
|
410
|
+
const state = await (0, page_context_1.readPageState)(this.page, { includeText: false });
|
|
411
|
+
let firstRow = '';
|
|
412
|
+
try {
|
|
413
|
+
firstRow = String(await (0, cdp_1.evaluateJs)(this.page, `(() => {
|
|
414
|
+
const vis = (el) => { if (!el) return false; const r = el.getBoundingClientRect(); return r.width > 0 && r.height > 0; };
|
|
415
|
+
const rows = [...document.querySelectorAll('tbody tr,[role="row"]')].filter(vis);
|
|
416
|
+
const row = rows.find((r) => r.querySelectorAll('td').length > 0);
|
|
417
|
+
return row ? String(row.innerText || row.textContent || '').replace(/\\s+/g, ' ').trim().slice(0, 80) : '';
|
|
418
|
+
})()`));
|
|
419
|
+
}
|
|
420
|
+
catch {
|
|
421
|
+
/* navigation / context loss */
|
|
422
|
+
}
|
|
423
|
+
const p = state.pagination;
|
|
424
|
+
return [
|
|
425
|
+
p.page ?? '',
|
|
426
|
+
p.rowCount,
|
|
427
|
+
p.total ?? '',
|
|
428
|
+
firstRow,
|
|
429
|
+
p.controls.map((c) => `${c.kind}:${c.disabled ? 1 : 0}`).join(','),
|
|
430
|
+
].join('|');
|
|
431
|
+
}
|
|
432
|
+
async waitPaginationChange(before, timeoutMs = 8000) {
|
|
433
|
+
const started = Date.now();
|
|
434
|
+
const deadline = started + Math.min(Math.max(timeoutMs, 500), 30_000);
|
|
435
|
+
while (Date.now() < deadline) {
|
|
436
|
+
await new Promise((resolve) => setTimeout(resolve, 220));
|
|
437
|
+
try {
|
|
438
|
+
if ((await this.paginationSignature()) !== before) {
|
|
439
|
+
return {
|
|
440
|
+
moved: true,
|
|
441
|
+
state: await this.pagination(),
|
|
442
|
+
elapsedMs: Date.now() - started,
|
|
443
|
+
};
|
|
444
|
+
}
|
|
445
|
+
}
|
|
446
|
+
catch {
|
|
447
|
+
/* navigation during the poll - retry */
|
|
448
|
+
}
|
|
449
|
+
}
|
|
450
|
+
return {
|
|
451
|
+
moved: false,
|
|
452
|
+
state: await this.pagination().catch(() => ({
|
|
453
|
+
kind: 'none',
|
|
454
|
+
hasNext: false,
|
|
455
|
+
hasPrev: false,
|
|
456
|
+
rowCount: 0,
|
|
457
|
+
pageSizeOptions: [],
|
|
458
|
+
controls: [],
|
|
459
|
+
})),
|
|
460
|
+
elapsedMs: Date.now() - started,
|
|
461
|
+
};
|
|
462
|
+
}
|
|
463
|
+
async clickPagination(kind, page) {
|
|
464
|
+
const state = await this.pagination();
|
|
465
|
+
const control = state.controls.find((c) => c.kind === kind && (page === undefined || c.page === page)) ??
|
|
466
|
+
(kind === 'next'
|
|
467
|
+
? state.controls.find((c) => c.kind === 'loadMore')
|
|
468
|
+
: undefined);
|
|
469
|
+
if (!control || control.disabled)
|
|
470
|
+
return { clicked: false };
|
|
471
|
+
await (0, cdp_1.clickAt)(this.page, control.x, control.y);
|
|
472
|
+
return { clicked: true, control };
|
|
473
|
+
}
|
|
474
|
+
/** Move to the next page (or Load more) and wait for the content to change. */
|
|
475
|
+
async nextPage(timeoutMs = 8000) {
|
|
476
|
+
this.ensureConnected();
|
|
477
|
+
const before = await this.paginationSignature();
|
|
478
|
+
const clicked = await this.clickPagination('next');
|
|
479
|
+
if (!clicked.clicked) {
|
|
480
|
+
return {
|
|
481
|
+
moved: false,
|
|
482
|
+
elapsedMs: 0,
|
|
483
|
+
state: await this.pagination(),
|
|
484
|
+
reason: 'no enabled next/load-more control',
|
|
485
|
+
};
|
|
486
|
+
}
|
|
487
|
+
const result = await this.waitPaginationChange(before, timeoutMs);
|
|
488
|
+
return {
|
|
489
|
+
...result,
|
|
490
|
+
...(result.moved ? {} : { reason: 'page did not change' }),
|
|
491
|
+
};
|
|
492
|
+
}
|
|
493
|
+
/** Move to the previous page. */
|
|
494
|
+
async prevPage(timeoutMs = 8000) {
|
|
495
|
+
this.ensureConnected();
|
|
496
|
+
const before = await this.paginationSignature();
|
|
497
|
+
const clicked = await this.clickPagination('prev');
|
|
498
|
+
if (!clicked.clicked) {
|
|
499
|
+
return {
|
|
500
|
+
moved: false,
|
|
501
|
+
elapsedMs: 0,
|
|
502
|
+
state: await this.pagination(),
|
|
503
|
+
reason: 'no enabled previous control',
|
|
504
|
+
};
|
|
505
|
+
}
|
|
506
|
+
const result = await this.waitPaginationChange(before, timeoutMs);
|
|
507
|
+
return {
|
|
508
|
+
...result,
|
|
509
|
+
...(result.moved ? {} : { reason: 'page did not change' }),
|
|
510
|
+
};
|
|
511
|
+
}
|
|
512
|
+
/** Go to a numbered page; walks next if the page control is not rendered. */
|
|
513
|
+
async gotoPage(page, timeoutMs = 8000) {
|
|
514
|
+
this.ensureConnected();
|
|
515
|
+
const target = Math.max(1, Math.floor(page));
|
|
516
|
+
const current = await this.pagination();
|
|
517
|
+
if (current.page === target) {
|
|
518
|
+
return {
|
|
519
|
+
moved: false,
|
|
520
|
+
elapsedMs: 0,
|
|
521
|
+
state: current,
|
|
522
|
+
reason: 'already on page',
|
|
523
|
+
};
|
|
524
|
+
}
|
|
525
|
+
const before = await this.paginationSignature();
|
|
526
|
+
const direct = await this.clickPagination('page', target);
|
|
527
|
+
if (direct.clicked) {
|
|
528
|
+
const result = await this.waitPaginationChange(before, timeoutMs);
|
|
529
|
+
return {
|
|
530
|
+
...result,
|
|
531
|
+
...(result.moved ? {} : { reason: 'page did not change' }),
|
|
532
|
+
};
|
|
533
|
+
}
|
|
534
|
+
// Walk forward (bounded) when only prev/next controls exist.
|
|
535
|
+
for (let i = 0; i < Math.min(target, 100); i += 1) {
|
|
536
|
+
const next = await this.nextPage(timeoutMs);
|
|
537
|
+
if (!next.moved)
|
|
538
|
+
return { ...next, reason: next.reason ?? 'page did not change' };
|
|
539
|
+
if (next.state.page === target)
|
|
540
|
+
return next;
|
|
541
|
+
}
|
|
542
|
+
return {
|
|
543
|
+
moved: false,
|
|
544
|
+
elapsedMs: 0,
|
|
545
|
+
state: await this.pagination(),
|
|
546
|
+
reason: 'page control not found and the target was not reached',
|
|
547
|
+
};
|
|
548
|
+
}
|
|
549
|
+
/** Select a native page-size option (custom comboboxes use click + waitFor). */
|
|
550
|
+
async setPageSize(size) {
|
|
551
|
+
this.ensureConnected();
|
|
552
|
+
const before = await this.paginationSignature();
|
|
553
|
+
const changed = await (0, cdp_1.evaluateJs)(this.page, `(() => {
|
|
554
|
+
const vis = (el) => { if (!el) return false; const r = el.getBoundingClientRect(); return r.width > 0 && r.height > 0; };
|
|
555
|
+
const target = ${Math.max(1, Math.floor(size))};
|
|
556
|
+
for (const sel of [...document.querySelectorAll('select')].filter(vis)) {
|
|
557
|
+
const options = [...sel.options];
|
|
558
|
+
const option = options.find((o) => Number(String(o.textContent || o.label || '').trim()) === target);
|
|
559
|
+
if (!option) continue;
|
|
560
|
+
const proto = HTMLSelectElement.prototype;
|
|
561
|
+
const desc = Object.getOwnPropertyDescriptor(proto, 'value');
|
|
562
|
+
if (desc && desc.set) desc.set.call(sel, option.value);
|
|
563
|
+
else sel.value = option.value;
|
|
564
|
+
sel.dispatchEvent(new Event('input', { bubbles: true }));
|
|
565
|
+
sel.dispatchEvent(new Event('change', { bubbles: true }));
|
|
566
|
+
return true;
|
|
567
|
+
}
|
|
568
|
+
return false;
|
|
569
|
+
})()`);
|
|
570
|
+
if (changed !== true) {
|
|
571
|
+
return {
|
|
572
|
+
ok: false,
|
|
573
|
+
state: await this.pagination(),
|
|
574
|
+
error: 'page size is not a native select; use getElements + click on the custom combobox',
|
|
575
|
+
};
|
|
576
|
+
}
|
|
577
|
+
const result = await this.waitPaginationChange(before);
|
|
578
|
+
return {
|
|
579
|
+
ok: true,
|
|
580
|
+
state: result.state,
|
|
581
|
+
...(result.moved ? {} : { error: 'page did not visibly change' }),
|
|
582
|
+
};
|
|
583
|
+
}
|
|
584
|
+
/**
|
|
585
|
+
* Read the current table (headers + rows), then walk every page and return
|
|
586
|
+
* the deduplicated rows. Bounded by maxPages/maxRows; stops on a repeated
|
|
587
|
+
* page signature, a disabled pager, or an unchanged table.
|
|
588
|
+
*/
|
|
589
|
+
async collectTable(opts) {
|
|
590
|
+
this.ensureConnected();
|
|
591
|
+
if (opts?.pageSize)
|
|
592
|
+
await this.setPageSize(opts.pageSize);
|
|
593
|
+
const maxPages = Math.min(Math.max(opts?.maxPages ?? 50, 1), 200);
|
|
594
|
+
const maxRows = Math.min(Math.max(opts?.maxRows ?? 2000, 1), 10_000);
|
|
595
|
+
const seen = new Set();
|
|
596
|
+
const rows = [];
|
|
597
|
+
let headers = [];
|
|
598
|
+
let duplicates = 0;
|
|
599
|
+
let pagesVisited = 0;
|
|
600
|
+
let stoppedBecause = 'unknown';
|
|
601
|
+
let truncated = false;
|
|
602
|
+
const readTable = async () => {
|
|
603
|
+
try {
|
|
604
|
+
const raw = await (0, cdp_1.evaluateJs)(this.page, `(() => {
|
|
605
|
+
const vis = (el) => { if (!el) return false; const r = el.getBoundingClientRect(); return r.width > 0 && r.height > 0; };
|
|
606
|
+
const selector = ${JSON.stringify(opts?.selector ?? '')};
|
|
607
|
+
let table = selector ? document.querySelector(selector) : null;
|
|
608
|
+
if (!table) {
|
|
609
|
+
const tables = [...document.querySelectorAll('table,[role="table"],[role="grid"]')].filter(vis);
|
|
610
|
+
table = tables.sort((a, b) => b.querySelectorAll('tr,[role="row"]').length - a.querySelectorAll('tr,[role="row"]').length)[0] || null;
|
|
611
|
+
}
|
|
612
|
+
if (!table) return null;
|
|
613
|
+
const headers = [...table.querySelectorAll('thead th,[role="columnheader"]')].map((h) => String(h.innerText || h.textContent || '').replace(/\\s+/g, ' ').trim()).filter(Boolean);
|
|
614
|
+
const bodyRows = [...table.querySelectorAll('tbody tr,[role="row"]')].filter(vis)
|
|
615
|
+
.filter((r) => r.querySelectorAll('td,[role="gridcell"],[role="cell"]').length > 0);
|
|
616
|
+
return {
|
|
617
|
+
headers,
|
|
618
|
+
rows: bodyRows.map((r) => [...r.querySelectorAll('td,[role="gridcell"],[role="cell"]')].map((c) => String(c.innerText || c.textContent || '').replace(/\\s+/g, ' ').trim())),
|
|
619
|
+
};
|
|
620
|
+
})()`);
|
|
621
|
+
if (!raw || typeof raw !== 'object')
|
|
622
|
+
return null;
|
|
623
|
+
return raw;
|
|
624
|
+
}
|
|
625
|
+
catch {
|
|
626
|
+
return null;
|
|
627
|
+
}
|
|
628
|
+
};
|
|
629
|
+
for (let page = 0; page < maxPages; page += 1) {
|
|
630
|
+
const table = await readTable();
|
|
631
|
+
if (!table) {
|
|
632
|
+
stoppedBecause = 'no table found';
|
|
633
|
+
break;
|
|
634
|
+
}
|
|
635
|
+
pagesVisited += 1;
|
|
636
|
+
if (table.headers.length)
|
|
637
|
+
headers = table.headers;
|
|
638
|
+
for (const row of table.rows) {
|
|
639
|
+
const key = row.join('\u0001');
|
|
640
|
+
if (seen.has(key)) {
|
|
641
|
+
duplicates += 1;
|
|
642
|
+
continue;
|
|
643
|
+
}
|
|
644
|
+
seen.add(key);
|
|
645
|
+
rows.push(row);
|
|
646
|
+
if (rows.length >= maxRows) {
|
|
647
|
+
truncated = true;
|
|
648
|
+
stoppedBecause = 'maxRows';
|
|
649
|
+
break;
|
|
650
|
+
}
|
|
651
|
+
}
|
|
652
|
+
if (truncated)
|
|
653
|
+
break;
|
|
654
|
+
const next = await this.nextPage();
|
|
655
|
+
if (!next.moved) {
|
|
656
|
+
stoppedBecause = next.reason ?? 'end of pages';
|
|
657
|
+
break;
|
|
658
|
+
}
|
|
659
|
+
}
|
|
660
|
+
const state = await this.pagination().catch(() => null);
|
|
661
|
+
return {
|
|
662
|
+
headers,
|
|
663
|
+
rows,
|
|
664
|
+
rowCount: rows.length,
|
|
665
|
+
pagesVisited,
|
|
666
|
+
...(state?.total ? { total: state.total } : {}),
|
|
667
|
+
complete: !truncated &&
|
|
668
|
+
(stoppedBecause === 'end of pages' ||
|
|
669
|
+
stoppedBecause === 'no enabled next/load-more control'),
|
|
670
|
+
duplicates,
|
|
671
|
+
truncated,
|
|
672
|
+
stoppedBecause,
|
|
673
|
+
};
|
|
674
|
+
}
|
|
273
675
|
async navigate(url) {
|
|
274
676
|
this.ensureConnected();
|
|
275
677
|
return (0, cdp_1.navigateTo)(this.page, url);
|
|
@@ -279,10 +681,7 @@ class BrowserSession {
|
|
|
279
681
|
*/
|
|
280
682
|
async screenshot(format = 'jpeg', quality = 70) {
|
|
281
683
|
this.ensureConnected();
|
|
282
|
-
|
|
283
|
-
if (!data)
|
|
284
|
-
throw new Error('Screenshot failed');
|
|
285
|
-
return data;
|
|
684
|
+
return (0, cdp_1.captureScreenshotChecked)(this.page, format, quality);
|
|
286
685
|
}
|
|
287
686
|
/**
|
|
288
687
|
* Read the network + console evidence collected for this session (QA).
|
|
@@ -308,13 +707,13 @@ class BrowserSession {
|
|
|
308
707
|
* credential for the run - the debug browser is the QA surface, the
|
|
309
708
|
* auto-answer loop is a separate path that still never touches credentials.
|
|
310
709
|
*/
|
|
311
|
-
async type(text) {
|
|
710
|
+
async type(text, opts) {
|
|
312
711
|
this.ensureConnected();
|
|
313
712
|
const credential = await (0, login_1.isCredentialFocused)(this.page);
|
|
314
713
|
if (credential && !this._allowCredentials) {
|
|
315
714
|
throw new Error('LOGIN_BLOCKED: refusing to type into a password/credential field. Pass allowCredentials:true on browser_open (debug browser only), use browser_action({action:"login"}) with supplied credentials, or ask the human to sign in.');
|
|
316
715
|
}
|
|
317
|
-
await (0, login_1.typeTextVerified)(this.page, text);
|
|
716
|
+
await (0, login_1.typeTextVerified)(this.page, text, opts);
|
|
318
717
|
}
|
|
319
718
|
/**
|
|
320
719
|
* Deterministically fill a login form (username + password) and submit.
|
|
@@ -619,6 +1018,195 @@ class BrowserSession {
|
|
|
619
1018
|
this.ensureConnected();
|
|
620
1019
|
return (0, cdp_1.getPageInfo)(this.page);
|
|
621
1020
|
}
|
|
1021
|
+
/**
|
|
1022
|
+
* Full page state (2026-10): dialogs (topmost with buttons/fields), toasts
|
|
1023
|
+
* (persistent buffer), visible errors, form fields (lengths only), focus,
|
|
1024
|
+
* busy, headings, optional page text, pending requests and console errors.
|
|
1025
|
+
*
|
|
1026
|
+
* This is the debug browser's equivalent of the auto-answer loop's rich
|
|
1027
|
+
* `extractPageContext` - and it is what lets an agent see a login modal
|
|
1028
|
+
* instead of guessing with raw `evaluate`.
|
|
1029
|
+
*/
|
|
1030
|
+
async snapshot(opts) {
|
|
1031
|
+
this.ensureConnected();
|
|
1032
|
+
// A native dialog blocks the renderer: an evaluate would hang. Report the
|
|
1033
|
+
// dialog plus the out-of-page evidence instead.
|
|
1034
|
+
if (this._nativeDialog) {
|
|
1035
|
+
return (0, page_context_1.emptyPageSnapshot)(this._nativeDialog, this.network.pendingCount(), this.network.recentErrors(5).map((c) => ({
|
|
1036
|
+
level: c.level,
|
|
1037
|
+
text: c.text,
|
|
1038
|
+
at: c.at,
|
|
1039
|
+
})));
|
|
1040
|
+
}
|
|
1041
|
+
const state = await (0, page_context_1.readPageState)(this.page, opts);
|
|
1042
|
+
return {
|
|
1043
|
+
...state,
|
|
1044
|
+
nativeDialog: this._nativeDialog,
|
|
1045
|
+
pendingRequests: this.network.pendingCount(),
|
|
1046
|
+
consoleErrors: this.network.recentErrors(5).map((c) => ({
|
|
1047
|
+
level: c.level,
|
|
1048
|
+
text: c.text,
|
|
1049
|
+
at: c.at,
|
|
1050
|
+
})),
|
|
1051
|
+
};
|
|
1052
|
+
}
|
|
1053
|
+
/**
|
|
1054
|
+
* Bounded state attached to every mutating action result. Never include the
|
|
1055
|
+
* page text (that is what `snapshot` is for); a failure returns null so an
|
|
1056
|
+
* action result is never failed by its own observation.
|
|
1057
|
+
*/
|
|
1058
|
+
async compactSnapshot() {
|
|
1059
|
+
// A native dialog blocks the renderer; return the dialog + out-of-page
|
|
1060
|
+
// evidence without an evaluate (which would hang).
|
|
1061
|
+
if (this._nativeDialog) {
|
|
1062
|
+
return {
|
|
1063
|
+
url: '',
|
|
1064
|
+
title: '',
|
|
1065
|
+
readyState: 'unknown',
|
|
1066
|
+
modal: false,
|
|
1067
|
+
dialogs: [],
|
|
1068
|
+
fields: [],
|
|
1069
|
+
focused: null,
|
|
1070
|
+
errors: [],
|
|
1071
|
+
toasts: [],
|
|
1072
|
+
busy: false,
|
|
1073
|
+
scroll: { x: 0, y: 0 },
|
|
1074
|
+
nativeDialog: this._nativeDialog,
|
|
1075
|
+
pendingRequests: this.network.pendingCount(),
|
|
1076
|
+
consoleErrors: this.network.recentErrors(3).map((c) => ({
|
|
1077
|
+
level: c.level,
|
|
1078
|
+
text: c.text,
|
|
1079
|
+
at: c.at,
|
|
1080
|
+
})),
|
|
1081
|
+
};
|
|
1082
|
+
}
|
|
1083
|
+
try {
|
|
1084
|
+
const state = await (0, page_context_1.readCompactState)(this.page);
|
|
1085
|
+
if (!state)
|
|
1086
|
+
return null;
|
|
1087
|
+
return {
|
|
1088
|
+
...state,
|
|
1089
|
+
nativeDialog: this._nativeDialog,
|
|
1090
|
+
pendingRequests: this.network.pendingCount(),
|
|
1091
|
+
consoleErrors: this.network.recentErrors(3).map((c) => ({
|
|
1092
|
+
level: c.level,
|
|
1093
|
+
text: c.text,
|
|
1094
|
+
at: c.at,
|
|
1095
|
+
})),
|
|
1096
|
+
};
|
|
1097
|
+
}
|
|
1098
|
+
catch {
|
|
1099
|
+
return null;
|
|
1100
|
+
}
|
|
1101
|
+
}
|
|
1102
|
+
/**
|
|
1103
|
+
* Recent toast / alert / dialog events from the persistent page buffer.
|
|
1104
|
+
* This is how a toast that disappeared before the next tool call is still
|
|
1105
|
+
* visible.
|
|
1106
|
+
*/
|
|
1107
|
+
async events(opts) {
|
|
1108
|
+
this.ensureConnected();
|
|
1109
|
+
if (this._nativeDialog)
|
|
1110
|
+
return { events: [], count: 0 };
|
|
1111
|
+
const limit = Math.min(Math.max(opts?.limit ?? 50, 1), 200);
|
|
1112
|
+
const since = Math.max(0, Number(opts?.since) || 0);
|
|
1113
|
+
let events = [];
|
|
1114
|
+
try {
|
|
1115
|
+
const raw = await (0, cdp_1.evaluateJs)(this.page, `JSON.stringify((window.__tsPageEvents || []).filter((e) => e && e.at >= ${since}).slice(-${limit}))`);
|
|
1116
|
+
const parsed = JSON.parse(String(raw ?? '[]'));
|
|
1117
|
+
if (Array.isArray(parsed))
|
|
1118
|
+
events = parsed;
|
|
1119
|
+
}
|
|
1120
|
+
catch {
|
|
1121
|
+
/* navigation/context loss - return what we have (nothing) */
|
|
1122
|
+
}
|
|
1123
|
+
if (opts?.clear) {
|
|
1124
|
+
try {
|
|
1125
|
+
await (0, cdp_1.evaluateJs)(this.page, '(() => { window.__tsPageEvents = []; return true; })()');
|
|
1126
|
+
}
|
|
1127
|
+
catch {
|
|
1128
|
+
/* best-effort */
|
|
1129
|
+
}
|
|
1130
|
+
}
|
|
1131
|
+
return { events, count: events.length };
|
|
1132
|
+
}
|
|
1133
|
+
/**
|
|
1134
|
+
* Wait until a page condition is true, then return the current screen.
|
|
1135
|
+
*
|
|
1136
|
+
* This replaces the manual `evaluate` polling loop the QA agent used to run
|
|
1137
|
+
* (and which broke on "Inspected target navigated"). On timeout it returns
|
|
1138
|
+
* `matched:false` WITH the current screen, so the caller can see why.
|
|
1139
|
+
*/
|
|
1140
|
+
async waitFor(opts) {
|
|
1141
|
+
this.ensureConnected();
|
|
1142
|
+
const started = Date.now();
|
|
1143
|
+
const timeout = Math.min(Math.max(Number(opts.timeoutMs) || 10_000, 250), 120_000);
|
|
1144
|
+
const pollMs = Math.min(Math.max(Number(opts.pollMs) || 250, 100), 2_000);
|
|
1145
|
+
const deadline = started + timeout;
|
|
1146
|
+
// Include a toast/mutation that landed just before the wait began.
|
|
1147
|
+
const since = started - 750;
|
|
1148
|
+
const probe = (0, page_context_1.buildWaitProbeScript)({ ...opts, since });
|
|
1149
|
+
let networkIdleTicks = 0;
|
|
1150
|
+
const matched = async () => ({
|
|
1151
|
+
matched: true,
|
|
1152
|
+
elapsedMs: Date.now() - started,
|
|
1153
|
+
for: opts.for,
|
|
1154
|
+
screen: await this.compactSnapshot(),
|
|
1155
|
+
nativeDialog: this._nativeDialog,
|
|
1156
|
+
});
|
|
1157
|
+
while (Date.now() < deadline) {
|
|
1158
|
+
if (this._nativeDialog) {
|
|
1159
|
+
if (opts.for === 'dialog' && opts.state !== 'closed') {
|
|
1160
|
+
return await matched();
|
|
1161
|
+
}
|
|
1162
|
+
// A native dialog blocks the page: any other condition cannot be
|
|
1163
|
+
// evaluated until the caller resolves it with `dialog`.
|
|
1164
|
+
}
|
|
1165
|
+
else if (opts.for === 'networkIdle') {
|
|
1166
|
+
if (this.network.pendingCount() === 0) {
|
|
1167
|
+
networkIdleTicks += 1;
|
|
1168
|
+
if (networkIdleTicks >= 2)
|
|
1169
|
+
return await matched();
|
|
1170
|
+
}
|
|
1171
|
+
else {
|
|
1172
|
+
networkIdleTicks = 0;
|
|
1173
|
+
}
|
|
1174
|
+
}
|
|
1175
|
+
else if (opts.for === 'loginStep') {
|
|
1176
|
+
try {
|
|
1177
|
+
const fields = await (0, login_1.findLoginFields)(this.page);
|
|
1178
|
+
const phase = (0, login_1.loginStepOf)(fields);
|
|
1179
|
+
if (!opts.loginPhase || phase === opts.loginPhase) {
|
|
1180
|
+
return await matched();
|
|
1181
|
+
}
|
|
1182
|
+
}
|
|
1183
|
+
catch {
|
|
1184
|
+
/* navigation - retry */
|
|
1185
|
+
}
|
|
1186
|
+
}
|
|
1187
|
+
else {
|
|
1188
|
+
try {
|
|
1189
|
+
const ok = await (0, cdp_1.evaluateJs)(this.page, probe);
|
|
1190
|
+
if (ok === true)
|
|
1191
|
+
return await matched();
|
|
1192
|
+
}
|
|
1193
|
+
catch {
|
|
1194
|
+
/* "Inspected target navigated" / execution context destroyed: retry */
|
|
1195
|
+
}
|
|
1196
|
+
}
|
|
1197
|
+
const remaining = deadline - Date.now();
|
|
1198
|
+
if (remaining <= 0)
|
|
1199
|
+
break;
|
|
1200
|
+
await new Promise((resolve) => setTimeout(resolve, Math.min(pollMs, remaining)));
|
|
1201
|
+
}
|
|
1202
|
+
return {
|
|
1203
|
+
matched: false,
|
|
1204
|
+
elapsedMs: Date.now() - started,
|
|
1205
|
+
for: opts.for,
|
|
1206
|
+
screen: await this.compactSnapshot(),
|
|
1207
|
+
nativeDialog: this._nativeDialog,
|
|
1208
|
+
};
|
|
1209
|
+
}
|
|
622
1210
|
/**
|
|
623
1211
|
* Is the page actually usable right now? (2026-10)
|
|
624
1212
|
*
|
|
@@ -674,6 +1262,7 @@ class BrowserSession {
|
|
|
674
1262
|
this.page = null;
|
|
675
1263
|
}
|
|
676
1264
|
this._connected = false;
|
|
1265
|
+
this._nativeDialog = null;
|
|
677
1266
|
}
|
|
678
1267
|
/**
|
|
679
1268
|
* Close the debug browser this session OWNS.
|