btcp-browser-agent 0.1.12 → 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 +35 -35
- 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
|
+
}
|
|
@@ -864,15 +864,15 @@ export class DOMActions {
|
|
|
864
864
|
// Create overlay container with absolute positioning covering entire document
|
|
865
865
|
this.overlayContainer = this.document.createElement('div');
|
|
866
866
|
this.overlayContainer.id = 'btcp-highlight-overlay';
|
|
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;
|
|
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;
|
|
876
876
|
`;
|
|
877
877
|
let highlightedCount = 0;
|
|
878
878
|
// Create border overlays and labels for each ref
|
|
@@ -893,17 +893,17 @@ export class DOMActions {
|
|
|
893
893
|
const border = this.document.createElement('div');
|
|
894
894
|
border.className = 'btcp-ref-border';
|
|
895
895
|
border.dataset.ref = ref;
|
|
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;
|
|
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;
|
|
907
907
|
`;
|
|
908
908
|
// Create label
|
|
909
909
|
const label = this.document.createElement('div');
|
|
@@ -911,21 +911,21 @@ export class DOMActions {
|
|
|
911
911
|
label.dataset.ref = ref;
|
|
912
912
|
// Extract number from ref (e.g., "@ref:5" -> "5")
|
|
913
913
|
label.textContent = ref.replace('@ref:', '');
|
|
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;
|
|
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;
|
|
929
929
|
`;
|
|
930
930
|
this.overlayContainer.appendChild(border);
|
|
931
931
|
this.overlayContainer.appendChild(label);
|
|
@@ -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');
|
|
@@ -190,65 +190,6 @@ export function createRemoteAgent(config) {
|
|
|
190
190
|
});
|
|
191
191
|
}
|
|
192
192
|
}
|
|
193
|
-
/**
|
|
194
|
-
* Ensure a session exists, creating one if needed
|
|
195
|
-
*
|
|
196
|
-
* This checks in order:
|
|
197
|
-
* 1. Current active session
|
|
198
|
-
* 2. Persistent session from storage (reconnects if found)
|
|
199
|
-
* 3. Existing BTCP tab groups (reconnects to first one found)
|
|
200
|
-
* 4. Creates a new session if none found (respects maxSession limit)
|
|
201
|
-
*/
|
|
202
|
-
async function ensureSession() {
|
|
203
|
-
// 1. Check if there's an active session
|
|
204
|
-
const sessionResult = await backgroundAgent.execute({ action: 'sessionGetCurrent' });
|
|
205
|
-
if (sessionResult.success && sessionResult.data) {
|
|
206
|
-
const session = sessionResult.data.session;
|
|
207
|
-
if (session?.groupId) {
|
|
208
|
-
log('Active session found:', session.groupId);
|
|
209
|
-
return; // Session already exists
|
|
210
|
-
}
|
|
211
|
-
}
|
|
212
|
-
// 2. Try to reconnect via popup initialize (handles persistent session check)
|
|
213
|
-
log('No active session, trying to reconnect to existing session...');
|
|
214
|
-
const initResult = await backgroundAgent.execute({ action: 'popupInitialize' });
|
|
215
|
-
if (initResult.success && initResult.data) {
|
|
216
|
-
const initData = initResult.data;
|
|
217
|
-
if (initData.reconnected) {
|
|
218
|
-
log('Reconnected to existing session');
|
|
219
|
-
return;
|
|
220
|
-
}
|
|
221
|
-
}
|
|
222
|
-
// 3. Check for existing BTCP tab groups and try to use one
|
|
223
|
-
const groupsResult = await backgroundAgent.execute({ action: 'groupList' });
|
|
224
|
-
if (groupsResult.success && groupsResult.data) {
|
|
225
|
-
const groups = groupsResult.data;
|
|
226
|
-
const btcpGroup = groups.find(g => g.title?.startsWith('BTCP'));
|
|
227
|
-
if (btcpGroup) {
|
|
228
|
-
log('Found existing BTCP tab group, setting it as active session:', btcpGroup.id);
|
|
229
|
-
const useResult = await backgroundAgent.execute({
|
|
230
|
-
action: 'sessionUseGroup',
|
|
231
|
-
groupId: btcpGroup.id,
|
|
232
|
-
});
|
|
233
|
-
if (useResult.success) {
|
|
234
|
-
log('Successfully using existing BTCP group as session');
|
|
235
|
-
return;
|
|
236
|
-
}
|
|
237
|
-
log('Failed to use existing BTCP group:', useResult.error);
|
|
238
|
-
}
|
|
239
|
-
}
|
|
240
|
-
// 4. Create a new session (will fail if maxSession limit reached)
|
|
241
|
-
log('No existing session found, creating one automatically...');
|
|
242
|
-
const groupResult = await backgroundAgent.execute({
|
|
243
|
-
action: 'groupCreate',
|
|
244
|
-
title: 'BTCP Session',
|
|
245
|
-
color: 'blue',
|
|
246
|
-
});
|
|
247
|
-
if (!groupResult.success) {
|
|
248
|
-
throw new Error(`Failed to create session: ${groupResult.error}`);
|
|
249
|
-
}
|
|
250
|
-
log('Session created:', groupResult.data);
|
|
251
|
-
}
|
|
252
193
|
/**
|
|
253
194
|
* Handle incoming tool call request
|
|
254
195
|
*/
|
|
@@ -257,9 +198,7 @@ export function createRemoteAgent(config) {
|
|
|
257
198
|
log('Tool call:', name, args);
|
|
258
199
|
emit('toolCall', name, args);
|
|
259
200
|
try {
|
|
260
|
-
//
|
|
261
|
-
await ensureSession();
|
|
262
|
-
// Map tool to command and execute
|
|
201
|
+
// Map tool to command and execute (session auto-ensured by BackgroundAgent)
|
|
263
202
|
const command = mapToolToCommand(name, args);
|
|
264
203
|
const response = await backgroundAgent.execute(command);
|
|
265
204
|
// Send response back to server
|
|
@@ -24,13 +24,29 @@ export declare class SessionManager {
|
|
|
24
24
|
private activeSessionGroupId;
|
|
25
25
|
private sessionCounter;
|
|
26
26
|
private initialized;
|
|
27
|
+
private initializationPromise;
|
|
27
28
|
private maxSession;
|
|
28
29
|
private maxOpenTab;
|
|
29
30
|
constructor(options?: SessionManagerOptions);
|
|
31
|
+
/**
|
|
32
|
+
* Wait for SessionManager to finish initialization
|
|
33
|
+
*/
|
|
34
|
+
waitForInitialization(): Promise<void>;
|
|
30
35
|
/**
|
|
31
36
|
* Restore session from storage
|
|
32
37
|
*/
|
|
33
38
|
private restoreSession;
|
|
39
|
+
/**
|
|
40
|
+
* Scan for and cleanup duplicate BTCP session groups
|
|
41
|
+
* Ensures only one BTCP session exists at any time
|
|
42
|
+
*
|
|
43
|
+
* @returns Stats about cleanup: { found, kept, removed }
|
|
44
|
+
*/
|
|
45
|
+
cleanupDuplicateSessions(): Promise<{
|
|
46
|
+
found: number;
|
|
47
|
+
kept: number;
|
|
48
|
+
removed: number;
|
|
49
|
+
}>;
|
|
34
50
|
/**
|
|
35
51
|
* Persist session to storage
|
|
36
52
|
*/
|
|
@@ -80,6 +96,10 @@ export declare class SessionManager {
|
|
|
80
96
|
* Get the active session group ID
|
|
81
97
|
*/
|
|
82
98
|
getActiveSessionGroupId(): number | null;
|
|
99
|
+
/**
|
|
100
|
+
* Get the active session group ID (async version that ensures initialization is complete)
|
|
101
|
+
*/
|
|
102
|
+
getActiveSessionGroupIdAsync(): Promise<number | null>;
|
|
83
103
|
/**
|
|
84
104
|
* Get the maximum number of sessions allowed
|
|
85
105
|
*/
|
|
@@ -113,6 +133,16 @@ export declare class SessionManager {
|
|
|
113
133
|
* This validates the group exists and sets it as active with persistence
|
|
114
134
|
*/
|
|
115
135
|
useExistingGroupAsSession(groupId: number): Promise<boolean>;
|
|
136
|
+
/**
|
|
137
|
+
* Ensure a session exists - restore from storage, use existing, or create new
|
|
138
|
+
* Returns the session group ID (creates if needed)
|
|
139
|
+
*/
|
|
140
|
+
ensureSession(): Promise<number>;
|
|
141
|
+
/**
|
|
142
|
+
* Get the primary tab in session (ensures session exists first)
|
|
143
|
+
* Returns the first tab in the session group
|
|
144
|
+
*/
|
|
145
|
+
getSessionTab(): Promise<number>;
|
|
116
146
|
/**
|
|
117
147
|
* Add a tab to the active session (if one exists)
|
|
118
148
|
* Automatically enforces the tab limit after adding
|