btcp-browser-agent 0.1.11 → 0.1.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/LICENSE +21 -21
- package/README.md +338 -338
- package/package.json +69 -69
- package/packages/core/dist/actions.js +122 -54
- package/packages/core/dist/snapshot.js +20 -0
- package/packages/extension/dist/background.d.ts +13 -11
- package/packages/extension/dist/background.js +30 -72
- package/packages/extension/dist/remote.js +1 -62
- package/packages/extension/dist/session-manager.d.ts +30 -0
- package/packages/extension/dist/session-manager.js +192 -33
package/package.json
CHANGED
|
@@ -1,69 +1,69 @@
|
|
|
1
|
-
{
|
|
2
|
-
"name": "btcp-browser-agent",
|
|
3
|
-
"version": "0.1.
|
|
4
|
-
"description": "Give AI agents the power to control browsers. A foundation for building agentic systems with smart DOM snapshots and stable element references.",
|
|
5
|
-
"type": "module",
|
|
6
|
-
"main": "dist/index.js",
|
|
7
|
-
"types": "dist/index.d.ts",
|
|
8
|
-
"exports": {
|
|
9
|
-
".": {
|
|
10
|
-
"types": "./dist/index.d.ts",
|
|
11
|
-
"import": "./dist/index.js",
|
|
12
|
-
"default": "./dist/index.js"
|
|
13
|
-
},
|
|
14
|
-
"./core": {
|
|
15
|
-
"types": "./packages/core/dist/index.d.ts",
|
|
16
|
-
"import": "./packages/core/dist/index.js",
|
|
17
|
-
"default": "./packages/core/dist/index.js"
|
|
18
|
-
},
|
|
19
|
-
"./extension": {
|
|
20
|
-
"types": "./packages/extension/dist/index.d.ts",
|
|
21
|
-
"import": "./packages/extension/dist/index.js",
|
|
22
|
-
"default": "./packages/extension/dist/index.js"
|
|
23
|
-
},
|
|
24
|
-
"./extension/content": {
|
|
25
|
-
"types": "./packages/extension/dist/content.d.ts",
|
|
26
|
-
"import": "./packages/extension/dist/content.js",
|
|
27
|
-
"default": "./packages/extension/dist/content.js"
|
|
28
|
-
},
|
|
29
|
-
"./extension/background": {
|
|
30
|
-
"types": "./packages/extension/dist/background.d.ts",
|
|
31
|
-
"import": "./packages/extension/dist/background.js",
|
|
32
|
-
"default": "./packages/extension/dist/background.js"
|
|
33
|
-
}
|
|
34
|
-
},
|
|
35
|
-
"scripts": {
|
|
36
|
-
"build": "npm run build:packages && tsc -p tsconfig.build.json",
|
|
37
|
-
"build:packages": "tsc -p packages/core/tsconfig.json && tsc -p packages/extension/tsconfig.json && tsc -p packages/cli/tsconfig.json",
|
|
38
|
-
"clean": "rm -rf dist packages/*/dist",
|
|
39
|
-
"prepare": "npm run build",
|
|
40
|
-
"test": "vitest run",
|
|
41
|
-
"test:watch": "vitest",
|
|
42
|
-
"typecheck": "tsc --noEmit"
|
|
43
|
-
},
|
|
44
|
-
"workspaces": [
|
|
45
|
-
"packages/core",
|
|
46
|
-
"packages/extension",
|
|
47
|
-
"packages/cli"
|
|
48
|
-
],
|
|
49
|
-
"files": [
|
|
50
|
-
"dist",
|
|
51
|
-
"packages/core/dist",
|
|
52
|
-
"packages/extension/dist",
|
|
53
|
-
"!**/__tests__",
|
|
54
|
-
"!**/*.map"
|
|
55
|
-
],
|
|
56
|
-
"license": "Apache-2.0",
|
|
57
|
-
"repository": {
|
|
58
|
-
"type": "git",
|
|
59
|
-
"url": "git+https://github.com/browser-tool-calling-protocol/btcp-browser-agent.git"
|
|
60
|
-
},
|
|
61
|
-
"dependencies": {},
|
|
62
|
-
"devDependencies": {
|
|
63
|
-
"@types/chrome": "^0.0.268",
|
|
64
|
-
"@types/node": "^20.10.0",
|
|
65
|
-
"jsdom": "^24.0.0",
|
|
66
|
-
"typescript": "^5.3.0",
|
|
67
|
-
"vitest": "^2.0.0"
|
|
68
|
-
}
|
|
69
|
-
}
|
|
1
|
+
{
|
|
2
|
+
"name": "btcp-browser-agent",
|
|
3
|
+
"version": "0.1.14",
|
|
4
|
+
"description": "Give AI agents the power to control browsers. A foundation for building agentic systems with smart DOM snapshots and stable element references.",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"main": "dist/index.js",
|
|
7
|
+
"types": "dist/index.d.ts",
|
|
8
|
+
"exports": {
|
|
9
|
+
".": {
|
|
10
|
+
"types": "./dist/index.d.ts",
|
|
11
|
+
"import": "./dist/index.js",
|
|
12
|
+
"default": "./dist/index.js"
|
|
13
|
+
},
|
|
14
|
+
"./core": {
|
|
15
|
+
"types": "./packages/core/dist/index.d.ts",
|
|
16
|
+
"import": "./packages/core/dist/index.js",
|
|
17
|
+
"default": "./packages/core/dist/index.js"
|
|
18
|
+
},
|
|
19
|
+
"./extension": {
|
|
20
|
+
"types": "./packages/extension/dist/index.d.ts",
|
|
21
|
+
"import": "./packages/extension/dist/index.js",
|
|
22
|
+
"default": "./packages/extension/dist/index.js"
|
|
23
|
+
},
|
|
24
|
+
"./extension/content": {
|
|
25
|
+
"types": "./packages/extension/dist/content.d.ts",
|
|
26
|
+
"import": "./packages/extension/dist/content.js",
|
|
27
|
+
"default": "./packages/extension/dist/content.js"
|
|
28
|
+
},
|
|
29
|
+
"./extension/background": {
|
|
30
|
+
"types": "./packages/extension/dist/background.d.ts",
|
|
31
|
+
"import": "./packages/extension/dist/background.js",
|
|
32
|
+
"default": "./packages/extension/dist/background.js"
|
|
33
|
+
}
|
|
34
|
+
},
|
|
35
|
+
"scripts": {
|
|
36
|
+
"build": "npm run build:packages && tsc -p tsconfig.build.json",
|
|
37
|
+
"build:packages": "tsc -p packages/core/tsconfig.json && tsc -p packages/extension/tsconfig.json && tsc -p packages/cli/tsconfig.json",
|
|
38
|
+
"clean": "rm -rf dist packages/*/dist",
|
|
39
|
+
"prepare": "npm run build",
|
|
40
|
+
"test": "vitest run",
|
|
41
|
+
"test:watch": "vitest",
|
|
42
|
+
"typecheck": "tsc --noEmit"
|
|
43
|
+
},
|
|
44
|
+
"workspaces": [
|
|
45
|
+
"packages/core",
|
|
46
|
+
"packages/extension",
|
|
47
|
+
"packages/cli"
|
|
48
|
+
],
|
|
49
|
+
"files": [
|
|
50
|
+
"dist",
|
|
51
|
+
"packages/core/dist",
|
|
52
|
+
"packages/extension/dist",
|
|
53
|
+
"!**/__tests__",
|
|
54
|
+
"!**/*.map"
|
|
55
|
+
],
|
|
56
|
+
"license": "Apache-2.0",
|
|
57
|
+
"repository": {
|
|
58
|
+
"type": "git",
|
|
59
|
+
"url": "git+https://github.com/browser-tool-calling-protocol/btcp-browser-agent.git"
|
|
60
|
+
},
|
|
61
|
+
"dependencies": {},
|
|
62
|
+
"devDependencies": {
|
|
63
|
+
"@types/chrome": "^0.0.268",
|
|
64
|
+
"@types/node": "^20.10.0",
|
|
65
|
+
"jsdom": "^24.0.0",
|
|
66
|
+
"typescript": "^5.3.0",
|
|
67
|
+
"vitest": "^2.0.0"
|
|
68
|
+
}
|
|
69
|
+
}
|
|
@@ -349,31 +349,99 @@ export class DOMActions {
|
|
|
349
349
|
}
|
|
350
350
|
async type(selector, text, options = {}) {
|
|
351
351
|
const element = this.getElement(selector);
|
|
352
|
-
|
|
352
|
+
// Check if element is contenteditable
|
|
353
|
+
const isContentEditable = element.getAttribute('contenteditable') === 'true' ||
|
|
354
|
+
element.getAttribute('contenteditable') === '';
|
|
355
|
+
if (!(element instanceof HTMLInputElement || element instanceof HTMLTextAreaElement || isContentEditable)) {
|
|
353
356
|
const actualType = element.tagName.toLowerCase();
|
|
354
357
|
const availableActions = this.getAvailableActionsForElement(element);
|
|
355
|
-
throw createElementNotCompatibleError(selector, 'type', actualType, ['input', 'textarea'], availableActions);
|
|
358
|
+
throw createElementNotCompatibleError(selector, 'type', actualType, ['input', 'textarea', 'contenteditable'], availableActions);
|
|
356
359
|
}
|
|
357
|
-
element
|
|
358
|
-
if (
|
|
359
|
-
element.
|
|
360
|
-
element.dispatchEvent(new Event('input', { bubbles: true }));
|
|
360
|
+
// Focus the element (cast to HTMLElement for contenteditable)
|
|
361
|
+
if (element instanceof HTMLElement) {
|
|
362
|
+
element.focus();
|
|
361
363
|
}
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
364
|
+
// Handle contenteditable elements differently
|
|
365
|
+
if (isContentEditable) {
|
|
366
|
+
const htmlElement = element;
|
|
367
|
+
if (options.clear) {
|
|
368
|
+
htmlElement.textContent = '';
|
|
369
|
+
htmlElement.dispatchEvent(new Event('input', { bubbles: true }));
|
|
370
|
+
}
|
|
371
|
+
for (const char of text) {
|
|
372
|
+
htmlElement.dispatchEvent(new KeyboardEvent('keydown', { key: char, bubbles: true }));
|
|
373
|
+
htmlElement.dispatchEvent(new KeyboardEvent('keypress', { key: char, bubbles: true }));
|
|
374
|
+
// Insert text at cursor position or append
|
|
375
|
+
const selection = this.window.getSelection();
|
|
376
|
+
if (selection && selection.rangeCount > 0) {
|
|
377
|
+
const range = selection.getRangeAt(0);
|
|
378
|
+
range.deleteContents();
|
|
379
|
+
const textNode = this.document.createTextNode(char);
|
|
380
|
+
range.insertNode(textNode);
|
|
381
|
+
range.setStartAfter(textNode);
|
|
382
|
+
range.setEndAfter(textNode);
|
|
383
|
+
selection.removeAllRanges();
|
|
384
|
+
selection.addRange(range);
|
|
385
|
+
}
|
|
386
|
+
else {
|
|
387
|
+
htmlElement.textContent += char;
|
|
388
|
+
}
|
|
389
|
+
htmlElement.dispatchEvent(new Event('input', { bubbles: true }));
|
|
390
|
+
htmlElement.dispatchEvent(new KeyboardEvent('keyup', { key: char, bubbles: true }));
|
|
391
|
+
if (options.delay) {
|
|
392
|
+
await this.sleep(options.delay);
|
|
393
|
+
}
|
|
394
|
+
}
|
|
395
|
+
htmlElement.dispatchEvent(new Event('change', { bubbles: true }));
|
|
396
|
+
// Wait for verification that textContent contains typed text
|
|
397
|
+
const result = await waitForAssertion(() => {
|
|
398
|
+
const content = htmlElement.textContent || '';
|
|
399
|
+
const expected = text;
|
|
400
|
+
const actual = content;
|
|
401
|
+
if (!content.includes(text)) {
|
|
402
|
+
return {
|
|
403
|
+
success: false,
|
|
404
|
+
error: `Expected textContent to contain "${text}"`,
|
|
405
|
+
description: 'textContent check',
|
|
406
|
+
expected,
|
|
407
|
+
actual
|
|
408
|
+
};
|
|
409
|
+
}
|
|
410
|
+
return {
|
|
411
|
+
success: true,
|
|
412
|
+
error: null,
|
|
413
|
+
description: 'textContent check',
|
|
414
|
+
expected,
|
|
415
|
+
actual
|
|
416
|
+
};
|
|
417
|
+
}, { timeout: 1000, interval: 50 });
|
|
418
|
+
if (!result.success) {
|
|
419
|
+
throw createVerificationError('type', result, selector);
|
|
370
420
|
}
|
|
371
421
|
}
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
422
|
+
else {
|
|
423
|
+
// Handle regular input/textarea elements
|
|
424
|
+
const inputElement = element;
|
|
425
|
+
if (options.clear) {
|
|
426
|
+
inputElement.value = '';
|
|
427
|
+
inputElement.dispatchEvent(new Event('input', { bubbles: true }));
|
|
428
|
+
}
|
|
429
|
+
for (const char of text) {
|
|
430
|
+
inputElement.dispatchEvent(new KeyboardEvent('keydown', { key: char, bubbles: true }));
|
|
431
|
+
inputElement.dispatchEvent(new KeyboardEvent('keypress', { key: char, bubbles: true }));
|
|
432
|
+
inputElement.value += char;
|
|
433
|
+
inputElement.dispatchEvent(new Event('input', { bubbles: true }));
|
|
434
|
+
inputElement.dispatchEvent(new KeyboardEvent('keyup', { key: char, bubbles: true }));
|
|
435
|
+
if (options.delay) {
|
|
436
|
+
await this.sleep(options.delay);
|
|
437
|
+
}
|
|
438
|
+
}
|
|
439
|
+
inputElement.dispatchEvent(new Event('change', { bubbles: true }));
|
|
440
|
+
// Wait for verification that value contains typed text
|
|
441
|
+
const result = await waitForAssertion(() => assertValueContains(inputElement, text), { timeout: 1000, interval: 50 });
|
|
442
|
+
if (!result.success) {
|
|
443
|
+
throw createVerificationError('type', result, selector);
|
|
444
|
+
}
|
|
377
445
|
}
|
|
378
446
|
return { success: true, error: null };
|
|
379
447
|
}
|
|
@@ -796,15 +864,15 @@ export class DOMActions {
|
|
|
796
864
|
// Create overlay container with absolute positioning covering entire document
|
|
797
865
|
this.overlayContainer = this.document.createElement('div');
|
|
798
866
|
this.overlayContainer.id = 'btcp-highlight-overlay';
|
|
799
|
-
this.overlayContainer.style.cssText = `
|
|
800
|
-
position: absolute;
|
|
801
|
-
top: 0;
|
|
802
|
-
left: 0;
|
|
803
|
-
width: ${this.document.documentElement.scrollWidth}px;
|
|
804
|
-
height: ${this.document.documentElement.scrollHeight}px;
|
|
805
|
-
pointer-events: none;
|
|
806
|
-
z-index: 999999;
|
|
807
|
-
contain: layout style paint;
|
|
867
|
+
this.overlayContainer.style.cssText = `
|
|
868
|
+
position: absolute;
|
|
869
|
+
top: 0;
|
|
870
|
+
left: 0;
|
|
871
|
+
width: ${this.document.documentElement.scrollWidth}px;
|
|
872
|
+
height: ${this.document.documentElement.scrollHeight}px;
|
|
873
|
+
pointer-events: none;
|
|
874
|
+
z-index: 999999;
|
|
875
|
+
contain: layout style paint;
|
|
808
876
|
`;
|
|
809
877
|
let highlightedCount = 0;
|
|
810
878
|
// Create border overlays and labels for each ref
|
|
@@ -825,17 +893,17 @@ export class DOMActions {
|
|
|
825
893
|
const border = this.document.createElement('div');
|
|
826
894
|
border.className = 'btcp-ref-border';
|
|
827
895
|
border.dataset.ref = ref;
|
|
828
|
-
border.style.cssText = `
|
|
829
|
-
position: absolute;
|
|
830
|
-
width: ${bbox.width}px;
|
|
831
|
-
height: ${bbox.height}px;
|
|
832
|
-
transform: translate3d(${bbox.left + this.window.scrollX}px, ${bbox.top + this.window.scrollY}px, 0);
|
|
833
|
-
border: 2px solid rgba(59, 130, 246, 0.8);
|
|
834
|
-
border-radius: 2px;
|
|
835
|
-
box-shadow: 0 0 0 1px rgba(59, 130, 246, 0.2);
|
|
836
|
-
pointer-events: none;
|
|
837
|
-
will-change: transform;
|
|
838
|
-
contain: layout style paint;
|
|
896
|
+
border.style.cssText = `
|
|
897
|
+
position: absolute;
|
|
898
|
+
width: ${bbox.width}px;
|
|
899
|
+
height: ${bbox.height}px;
|
|
900
|
+
transform: translate3d(${bbox.left + this.window.scrollX}px, ${bbox.top + this.window.scrollY}px, 0);
|
|
901
|
+
border: 2px solid rgba(59, 130, 246, 0.8);
|
|
902
|
+
border-radius: 2px;
|
|
903
|
+
box-shadow: 0 0 0 1px rgba(59, 130, 246, 0.2);
|
|
904
|
+
pointer-events: none;
|
|
905
|
+
will-change: transform;
|
|
906
|
+
contain: layout style paint;
|
|
839
907
|
`;
|
|
840
908
|
// Create label
|
|
841
909
|
const label = this.document.createElement('div');
|
|
@@ -843,21 +911,21 @@ export class DOMActions {
|
|
|
843
911
|
label.dataset.ref = ref;
|
|
844
912
|
// Extract number from ref (e.g., "@ref:5" -> "5")
|
|
845
913
|
label.textContent = ref.replace('@ref:', '');
|
|
846
|
-
label.style.cssText = `
|
|
847
|
-
position: absolute;
|
|
848
|
-
transform: translate3d(${bbox.left + this.window.scrollX}px, ${bbox.top + this.window.scrollY}px, 0);
|
|
849
|
-
background: rgba(59, 130, 246, 0.9);
|
|
850
|
-
color: white;
|
|
851
|
-
padding: 2px 6px;
|
|
852
|
-
border-radius: 3px;
|
|
853
|
-
font-family: monospace;
|
|
854
|
-
font-size: 11px;
|
|
855
|
-
font-weight: bold;
|
|
856
|
-
box-shadow: 0 2px 4px rgba(0,0,0,0.3);
|
|
857
|
-
pointer-events: none;
|
|
858
|
-
white-space: nowrap;
|
|
859
|
-
will-change: transform;
|
|
860
|
-
contain: layout style paint;
|
|
914
|
+
label.style.cssText = `
|
|
915
|
+
position: absolute;
|
|
916
|
+
transform: translate3d(${bbox.left + this.window.scrollX}px, ${bbox.top + this.window.scrollY}px, 0);
|
|
917
|
+
background: rgba(59, 130, 246, 0.9);
|
|
918
|
+
color: white;
|
|
919
|
+
padding: 2px 6px;
|
|
920
|
+
border-radius: 3px;
|
|
921
|
+
font-family: monospace;
|
|
922
|
+
font-size: 11px;
|
|
923
|
+
font-weight: bold;
|
|
924
|
+
box-shadow: 0 2px 4px rgba(0,0,0,0.3);
|
|
925
|
+
pointer-events: none;
|
|
926
|
+
white-space: nowrap;
|
|
927
|
+
will-change: transform;
|
|
928
|
+
contain: layout style paint;
|
|
861
929
|
`;
|
|
862
930
|
this.overlayContainer.appendChild(border);
|
|
863
931
|
this.overlayContainer.appendChild(label);
|
|
@@ -110,6 +110,11 @@ function getRole(element) {
|
|
|
110
110
|
const type = element.type || 'text';
|
|
111
111
|
return INPUT_ROLES[type] || 'textbox';
|
|
112
112
|
}
|
|
113
|
+
// Detect contenteditable elements (ProseMirror, Quill, TinyMCE, etc.)
|
|
114
|
+
const contentEditable = element.getAttribute('contenteditable');
|
|
115
|
+
if (contentEditable === 'true' || contentEditable === '') {
|
|
116
|
+
return 'textbox';
|
|
117
|
+
}
|
|
113
118
|
return IMPLICIT_ROLES[tagName] || null;
|
|
114
119
|
}
|
|
115
120
|
/**
|
|
@@ -320,6 +325,21 @@ function getAccessibleName(element) {
|
|
|
320
325
|
if (isImage) {
|
|
321
326
|
return getImageLabel(element);
|
|
322
327
|
}
|
|
328
|
+
// Handle contenteditable elements (ProseMirror, Quill, TinyMCE, etc.)
|
|
329
|
+
const contentEditable = element.getAttribute('contenteditable');
|
|
330
|
+
if (contentEditable === 'true' || contentEditable === '') {
|
|
331
|
+
// Try data-placeholder attribute (common in rich text editors)
|
|
332
|
+
const placeholder = element.getAttribute('data-placeholder');
|
|
333
|
+
if (placeholder)
|
|
334
|
+
return placeholder.trim();
|
|
335
|
+
// Try finding placeholder in child paragraph element
|
|
336
|
+
const placeholderEl = element.querySelector('[data-placeholder]');
|
|
337
|
+
if (placeholderEl) {
|
|
338
|
+
const placeholderText = placeholderEl.getAttribute('data-placeholder');
|
|
339
|
+
if (placeholderText)
|
|
340
|
+
return placeholderText.trim();
|
|
341
|
+
}
|
|
342
|
+
}
|
|
323
343
|
const ariaLabel = element.getAttribute('aria-label');
|
|
324
344
|
if (ariaLabel)
|
|
325
345
|
return ariaLabel.trim();
|
|
@@ -91,22 +91,23 @@ export declare class BackgroundAgent {
|
|
|
91
91
|
*/
|
|
92
92
|
tab(tabId: number): TabHandle;
|
|
93
93
|
/**
|
|
94
|
-
* Get the currently active tab (
|
|
94
|
+
* Get the currently active tab (ensures session exists, creates if needed)
|
|
95
|
+
* This is the core "get or create" method that enables automatic session management.
|
|
95
96
|
*/
|
|
96
|
-
getActiveTab(): Promise<ChromeTab
|
|
97
|
+
getActiveTab(): Promise<ChromeTab>;
|
|
97
98
|
/**
|
|
98
|
-
* List all tabs
|
|
99
|
+
* List all tabs in session (ensures session exists, creates if needed)
|
|
99
100
|
*/
|
|
100
101
|
listTabs(): Promise<TabInfo[]>;
|
|
101
102
|
/**
|
|
102
|
-
* Create a new tab
|
|
103
|
+
* Create a new tab in session (ensures session exists, creates if needed)
|
|
103
104
|
*/
|
|
104
105
|
newTab(options?: {
|
|
105
106
|
url?: string;
|
|
106
107
|
active?: boolean;
|
|
107
108
|
}): Promise<TabInfo>;
|
|
108
109
|
/**
|
|
109
|
-
* Check if a tab is in the active session
|
|
110
|
+
* Check if a tab is in the active session (ensures session exists, creates if needed)
|
|
110
111
|
*/
|
|
111
112
|
private isTabInSession;
|
|
112
113
|
/**
|
|
@@ -118,7 +119,7 @@ export declare class BackgroundAgent {
|
|
|
118
119
|
*/
|
|
119
120
|
switchTab(tabId: number): Promise<void>;
|
|
120
121
|
/**
|
|
121
|
-
* Navigate to a URL (
|
|
122
|
+
* Navigate to a URL (session auto-created if needed)
|
|
122
123
|
* Always waits for page to be idle before returning.
|
|
123
124
|
* Verifies navigation completed to the expected origin.
|
|
124
125
|
*/
|
|
@@ -126,25 +127,25 @@ export declare class BackgroundAgent {
|
|
|
126
127
|
waitUntil?: 'load' | 'domcontentloaded';
|
|
127
128
|
}): Promise<void>;
|
|
128
129
|
/**
|
|
129
|
-
* Go back in history
|
|
130
|
+
* Go back in history (session auto-created if needed)
|
|
130
131
|
*/
|
|
131
132
|
back(): Promise<void>;
|
|
132
133
|
/**
|
|
133
|
-
* Go forward in history
|
|
134
|
+
* Go forward in history (session auto-created if needed)
|
|
134
135
|
*/
|
|
135
136
|
forward(): Promise<void>;
|
|
136
137
|
/**
|
|
137
|
-
* Reload the current page
|
|
138
|
+
* Reload the current page (session auto-created if needed)
|
|
138
139
|
*/
|
|
139
140
|
reload(options?: {
|
|
140
141
|
bypassCache?: boolean;
|
|
141
142
|
}): Promise<void>;
|
|
142
143
|
/**
|
|
143
|
-
* Get the current URL
|
|
144
|
+
* Get the current URL (session auto-created if needed)
|
|
144
145
|
*/
|
|
145
146
|
getUrl(): Promise<string>;
|
|
146
147
|
/**
|
|
147
|
-
* Get the page title
|
|
148
|
+
* Get the page title (session auto-created if needed)
|
|
148
149
|
*/
|
|
149
150
|
getTitle(): Promise<string>;
|
|
150
151
|
/**
|
|
@@ -180,6 +181,7 @@ export declare class BackgroundAgent {
|
|
|
180
181
|
}): Promise<Response>;
|
|
181
182
|
/**
|
|
182
183
|
* Send a command to the ContentAgent in a specific tab
|
|
184
|
+
* Session is automatically created if it doesn't exist.
|
|
183
185
|
*/
|
|
184
186
|
sendToContentAgent(command: Command, tabId?: number): Promise<Response>;
|
|
185
187
|
/**
|
|
@@ -151,38 +151,19 @@ export class BackgroundAgent {
|
|
|
151
151
|
// TAB MANAGEMENT
|
|
152
152
|
// ============================================================================
|
|
153
153
|
/**
|
|
154
|
-
* Get the currently active tab (
|
|
154
|
+
* Get the currently active tab (ensures session exists, creates if needed)
|
|
155
|
+
* This is the core "get or create" method that enables automatic session management.
|
|
155
156
|
*/
|
|
156
157
|
async getActiveTab() {
|
|
157
|
-
const
|
|
158
|
-
|
|
159
|
-
if (sessionGroupId === null) {
|
|
160
|
-
return null;
|
|
161
|
-
}
|
|
162
|
-
// Get active tab and verify it's in the session
|
|
163
|
-
return new Promise((resolve) => {
|
|
164
|
-
chrome.tabs.query({ active: true, currentWindow: true }, (tabs) => {
|
|
165
|
-
const activeTab = tabs[0];
|
|
166
|
-
// Only return if it's in the session group
|
|
167
|
-
if (activeTab && activeTab.groupId === sessionGroupId) {
|
|
168
|
-
resolve(activeTab);
|
|
169
|
-
}
|
|
170
|
-
else {
|
|
171
|
-
resolve(null);
|
|
172
|
-
}
|
|
173
|
-
});
|
|
174
|
-
});
|
|
158
|
+
const tabId = await this.sessionManager.getSessionTab();
|
|
159
|
+
return chrome.tabs.get(tabId);
|
|
175
160
|
}
|
|
176
161
|
/**
|
|
177
|
-
* List all tabs
|
|
162
|
+
* List all tabs in session (ensures session exists, creates if needed)
|
|
178
163
|
*/
|
|
179
164
|
async listTabs() {
|
|
180
|
-
//
|
|
181
|
-
const sessionGroupId = this.sessionManager.
|
|
182
|
-
// Session is required
|
|
183
|
-
if (sessionGroupId === null) {
|
|
184
|
-
throw new Error('No active session. Create a session first to manage tabs.');
|
|
185
|
-
}
|
|
165
|
+
// Ensure session exists (creates if needed)
|
|
166
|
+
const sessionGroupId = await this.sessionManager.ensureSession();
|
|
186
167
|
// Only return tabs in the session group
|
|
187
168
|
const tabs = await new Promise((resolve) => {
|
|
188
169
|
chrome.tabs.query({ groupId: sessionGroupId }, (t) => resolve(t));
|
|
@@ -196,14 +177,11 @@ export class BackgroundAgent {
|
|
|
196
177
|
}));
|
|
197
178
|
}
|
|
198
179
|
/**
|
|
199
|
-
* Create a new tab
|
|
180
|
+
* Create a new tab in session (ensures session exists, creates if needed)
|
|
200
181
|
*/
|
|
201
182
|
async newTab(options) {
|
|
202
|
-
//
|
|
203
|
-
|
|
204
|
-
if (sessionGroupId === null) {
|
|
205
|
-
throw new Error('No active session. Create a session first to manage tabs.');
|
|
206
|
-
}
|
|
183
|
+
// Ensure session exists (creates if needed)
|
|
184
|
+
await this.sessionManager.ensureSession();
|
|
207
185
|
const tab = await new Promise((resolve) => {
|
|
208
186
|
chrome.tabs.create({ url: options?.url, active: options?.active ?? true }, (t) => resolve(t));
|
|
209
187
|
});
|
|
@@ -231,14 +209,11 @@ export class BackgroundAgent {
|
|
|
231
209
|
};
|
|
232
210
|
}
|
|
233
211
|
/**
|
|
234
|
-
* Check if a tab is in the active session
|
|
212
|
+
* Check if a tab is in the active session (ensures session exists, creates if needed)
|
|
235
213
|
*/
|
|
236
214
|
async isTabInSession(tabId) {
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
if (sessionGroupId === null) {
|
|
240
|
-
throw new Error('No active session. Create a session first to manage tabs.');
|
|
241
|
-
}
|
|
215
|
+
// Ensure session exists (creates if needed)
|
|
216
|
+
const sessionGroupId = await this.sessionManager.ensureSession();
|
|
242
217
|
// Check if tab is in the session group
|
|
243
218
|
const tab = await chrome.tabs.get(tabId);
|
|
244
219
|
return tab.groupId === sessionGroupId;
|
|
@@ -282,19 +257,13 @@ export class BackgroundAgent {
|
|
|
282
257
|
// NAVIGATION
|
|
283
258
|
// ============================================================================
|
|
284
259
|
/**
|
|
285
|
-
* Navigate to a URL (
|
|
260
|
+
* Navigate to a URL (session auto-created if needed)
|
|
286
261
|
* Always waits for page to be idle before returning.
|
|
287
262
|
* Verifies navigation completed to the expected origin.
|
|
288
263
|
*/
|
|
289
264
|
async navigate(url, _options) {
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
throw new Error('No active tab');
|
|
293
|
-
// Validate tab is in session
|
|
294
|
-
const inSession = await this.isTabInSession(tabId);
|
|
295
|
-
if (!inSession) {
|
|
296
|
-
throw new Error('Cannot navigate: tab is not in the active session');
|
|
297
|
-
}
|
|
265
|
+
// getActiveTab() ensures session exists and always returns a valid tab
|
|
266
|
+
const tabId = this.activeTabId ?? (await this.getActiveTab()).id;
|
|
298
267
|
await new Promise((resolve) => {
|
|
299
268
|
chrome.tabs.update(tabId, { url }, () => resolve());
|
|
300
269
|
});
|
|
@@ -334,52 +303,46 @@ export class BackgroundAgent {
|
|
|
334
303
|
}
|
|
335
304
|
}
|
|
336
305
|
/**
|
|
337
|
-
* Go back in history
|
|
306
|
+
* Go back in history (session auto-created if needed)
|
|
338
307
|
*/
|
|
339
308
|
async back() {
|
|
340
|
-
const tabId = this.activeTabId ?? (await this.getActiveTab())
|
|
341
|
-
if (!tabId)
|
|
342
|
-
throw new Error('No active tab');
|
|
309
|
+
const tabId = this.activeTabId ?? (await this.getActiveTab()).id;
|
|
343
310
|
await new Promise((resolve) => {
|
|
344
311
|
chrome.tabs.goBack(tabId, () => resolve());
|
|
345
312
|
});
|
|
346
313
|
}
|
|
347
314
|
/**
|
|
348
|
-
* Go forward in history
|
|
315
|
+
* Go forward in history (session auto-created if needed)
|
|
349
316
|
*/
|
|
350
317
|
async forward() {
|
|
351
|
-
const tabId = this.activeTabId ?? (await this.getActiveTab())
|
|
352
|
-
if (!tabId)
|
|
353
|
-
throw new Error('No active tab');
|
|
318
|
+
const tabId = this.activeTabId ?? (await this.getActiveTab()).id;
|
|
354
319
|
await new Promise((resolve) => {
|
|
355
320
|
chrome.tabs.goForward(tabId, () => resolve());
|
|
356
321
|
});
|
|
357
322
|
}
|
|
358
323
|
/**
|
|
359
|
-
* Reload the current page
|
|
324
|
+
* Reload the current page (session auto-created if needed)
|
|
360
325
|
*/
|
|
361
326
|
async reload(options) {
|
|
362
|
-
const tabId = this.activeTabId ?? (await this.getActiveTab())
|
|
363
|
-
if (!tabId)
|
|
364
|
-
throw new Error('No active tab');
|
|
327
|
+
const tabId = this.activeTabId ?? (await this.getActiveTab()).id;
|
|
365
328
|
await new Promise((resolve) => {
|
|
366
329
|
chrome.tabs.reload(tabId, { bypassCache: options?.bypassCache }, () => resolve());
|
|
367
330
|
});
|
|
368
331
|
await this.waitForTabLoad(tabId);
|
|
369
332
|
}
|
|
370
333
|
/**
|
|
371
|
-
* Get the current URL
|
|
334
|
+
* Get the current URL (session auto-created if needed)
|
|
372
335
|
*/
|
|
373
336
|
async getUrl() {
|
|
374
337
|
const tab = await this.getActiveTab();
|
|
375
|
-
return tab
|
|
338
|
+
return tab.url || '';
|
|
376
339
|
}
|
|
377
340
|
/**
|
|
378
|
-
* Get the page title
|
|
341
|
+
* Get the page title (session auto-created if needed)
|
|
379
342
|
*/
|
|
380
343
|
async getTitle() {
|
|
381
344
|
const tab = await this.getActiveTab();
|
|
382
|
-
return tab
|
|
345
|
+
return tab.title || '';
|
|
383
346
|
}
|
|
384
347
|
// ============================================================================
|
|
385
348
|
// SCREENSHOTS
|
|
@@ -449,19 +412,14 @@ export class BackgroundAgent {
|
|
|
449
412
|
}
|
|
450
413
|
/**
|
|
451
414
|
* Send a command to the ContentAgent in a specific tab
|
|
415
|
+
* Session is automatically created if it doesn't exist.
|
|
452
416
|
*/
|
|
453
417
|
async sendToContentAgent(command, tabId) {
|
|
454
418
|
// Ensure command has an ID for internal use
|
|
455
419
|
const id = command.id || generateBgCommandId();
|
|
456
420
|
const internalCmd = { ...command, id };
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
return {
|
|
460
|
-
id,
|
|
461
|
-
success: false,
|
|
462
|
-
error: 'No active tab for DOM command',
|
|
463
|
-
};
|
|
464
|
-
}
|
|
421
|
+
// getActiveTab() ensures session exists and always returns a valid tab
|
|
422
|
+
const targetTabId = tabId ?? this.activeTabId ?? (await this.getActiveTab()).id;
|
|
465
423
|
// Try sending with automatic retry and recovery
|
|
466
424
|
return this.sendMessageWithRetry(targetTabId, internalCmd);
|
|
467
425
|
}
|
|
@@ -693,7 +651,7 @@ export class BackgroundAgent {
|
|
|
693
651
|
case 'popupInitialize': {
|
|
694
652
|
console.log('[BackgroundAgent] Popup initializing, checking for session reconnection...');
|
|
695
653
|
// Check if we have a stored session but no active connection
|
|
696
|
-
const sessionGroupId = this.sessionManager.
|
|
654
|
+
const sessionGroupId = await this.sessionManager.getActiveSessionGroupIdAsync();
|
|
697
655
|
if (sessionGroupId === null) {
|
|
698
656
|
// Try to reconnect from storage
|
|
699
657
|
const result = await chrome.storage.session.get('btcp_active_session');
|