btcp-browser-agent 0.1.12 → 0.1.16
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/index.d.ts +37 -10
- package/packages/extension/dist/index.js +23 -58
- 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/packages/extension/dist/transport/base-transport.d.ts +65 -0
- package/packages/extension/dist/transport/base-transport.js +115 -0
- package/packages/extension/dist/transport/chrome-extension.d.ts +71 -0
- package/packages/extension/dist/transport/chrome-extension.js +131 -0
- package/packages/extension/dist/transport/direct.d.ts +69 -0
- package/packages/extension/dist/transport/direct.js +90 -0
- package/packages/extension/dist/transport/index.d.ts +10 -0
- package/packages/extension/dist/transport/index.js +12 -0
- package/packages/extension/dist/transport/types.d.ts +73 -0
- package/packages/extension/dist/transport/types.js +8 -0
|
@@ -39,6 +39,7 @@
|
|
|
39
39
|
* await client.click('@ref:5');
|
|
40
40
|
* ```
|
|
41
41
|
*/
|
|
42
|
+
import { ChromeExtensionTransport } from './transport/chrome-extension.js';
|
|
42
43
|
// Import for local use (and re-export below)
|
|
43
44
|
import { BackgroundAgent as _BackgroundAgent, getBackgroundAgent as _getBackgroundAgent, setupMessageListener as _setupMessageListener, BrowserAgent as _BrowserAgent, getBrowserAgent as _getBrowserAgent, } from './background.js';
|
|
44
45
|
export * from './types.js';
|
|
@@ -52,6 +53,8 @@ export { _BackgroundAgent as BackgroundAgent, _getBackgroundAgent as getBackgrou
|
|
|
52
53
|
_BrowserAgent as BrowserAgent, _getBrowserAgent as getBrowserAgent, };
|
|
53
54
|
// Re-export ContentAgent for content script usage
|
|
54
55
|
export { createContentAgent } from '../../core/dist/index.js';
|
|
56
|
+
// Re-export transport module
|
|
57
|
+
export * from './transport/index.js';
|
|
55
58
|
let commandIdCounter = 0;
|
|
56
59
|
/**
|
|
57
60
|
* Generate a unique command ID for BTCP commands
|
|
@@ -59,79 +62,41 @@ let commandIdCounter = 0;
|
|
|
59
62
|
export function generateCommandId() {
|
|
60
63
|
return `cmd_${Date.now()}_${commandIdCounter++}`;
|
|
61
64
|
}
|
|
62
|
-
/**
|
|
63
|
-
* Check if we're running in a background/service worker context
|
|
64
|
-
*/
|
|
65
|
-
function isBackgroundContext() {
|
|
66
|
-
// In Manifest V3, background scripts run as service workers
|
|
67
|
-
return typeof ServiceWorkerGlobalScope !== 'undefined' && self instanceof ServiceWorkerGlobalScope;
|
|
68
|
-
}
|
|
69
65
|
/**
|
|
70
66
|
* Create a client for communicating with the extension
|
|
71
67
|
*
|
|
72
|
-
*
|
|
73
|
-
*
|
|
74
|
-
* - In background scripts: Uses BackgroundAgent directly for better performance
|
|
68
|
+
* By default uses ChromeExtensionTransport for popup/content script contexts.
|
|
69
|
+
* Pass a custom transport for different communication mechanisms.
|
|
75
70
|
*
|
|
76
|
-
* @example
|
|
71
|
+
* @example Default (Chrome Extension):
|
|
77
72
|
* ```typescript
|
|
78
73
|
* import { createClient } from '@btcp/browser-agent/extension';
|
|
79
74
|
* const client = createClient();
|
|
80
75
|
* await client.navigate('https://example.com');
|
|
81
76
|
* ```
|
|
82
77
|
*
|
|
83
|
-
* @example
|
|
78
|
+
* @example With explicit transport:
|
|
84
79
|
* ```typescript
|
|
85
|
-
* import { createClient } from '@btcp/browser-agent/extension';
|
|
86
|
-
*
|
|
87
|
-
*
|
|
88
|
-
*
|
|
80
|
+
* import { createClient, createChromeExtensionTransport } from '@btcp/browser-agent/extension';
|
|
81
|
+
*
|
|
82
|
+
* const transport = createChromeExtensionTransport({ debug: true });
|
|
83
|
+
* const client = createClient({ transport });
|
|
84
|
+
* ```
|
|
85
|
+
*
|
|
86
|
+
* @example Direct transport (background script):
|
|
87
|
+
* ```typescript
|
|
88
|
+
* import { createClient, createDirectTransport, getBackgroundAgent } from '@btcp/browser-agent/extension';
|
|
89
|
+
*
|
|
90
|
+
* const transport = createDirectTransport({ agent: getBackgroundAgent() });
|
|
91
|
+
* const client = createClient({ transport });
|
|
89
92
|
* ```
|
|
90
93
|
*/
|
|
91
|
-
export function createClient() {
|
|
92
|
-
//
|
|
93
|
-
const
|
|
94
|
-
// Lazily get the background agent to avoid circular dependency issues
|
|
95
|
-
let bgAgent = null;
|
|
96
|
-
function getAgent() {
|
|
97
|
-
if (!bgAgent) {
|
|
98
|
-
// Use the singleton getter from background.js
|
|
99
|
-
bgAgent = _getBackgroundAgent();
|
|
100
|
-
}
|
|
101
|
-
return bgAgent;
|
|
102
|
-
}
|
|
94
|
+
export function createClient(options = {}) {
|
|
95
|
+
// Default to Chrome extension transport
|
|
96
|
+
const transport = options.transport ?? new ChromeExtensionTransport();
|
|
103
97
|
async function sendCommand(command) {
|
|
104
|
-
// In background context, use BackgroundAgent directly
|
|
105
|
-
if (inBackground) {
|
|
106
|
-
return getAgent().execute(command);
|
|
107
|
-
}
|
|
108
|
-
// In popup/content context, use message passing
|
|
109
98
|
const id = command.id || generateCommandId();
|
|
110
|
-
return
|
|
111
|
-
chrome.runtime.sendMessage({ type: 'btcp:command', command: { ...command, id } }, (response) => {
|
|
112
|
-
if (chrome.runtime.lastError) {
|
|
113
|
-
resolve({
|
|
114
|
-
id,
|
|
115
|
-
success: false,
|
|
116
|
-
error: chrome.runtime.lastError.message || 'Unknown error',
|
|
117
|
-
});
|
|
118
|
-
}
|
|
119
|
-
else {
|
|
120
|
-
const resp = response;
|
|
121
|
-
if (resp.type === 'btcp:response') {
|
|
122
|
-
resolve(resp.response);
|
|
123
|
-
}
|
|
124
|
-
else {
|
|
125
|
-
// Unexpected pong response
|
|
126
|
-
resolve({
|
|
127
|
-
id,
|
|
128
|
-
success: false,
|
|
129
|
-
error: 'Unexpected response type',
|
|
130
|
-
});
|
|
131
|
-
}
|
|
132
|
-
}
|
|
133
|
-
});
|
|
134
|
-
});
|
|
99
|
+
return transport.send({ ...command, id });
|
|
135
100
|
}
|
|
136
101
|
function assertSuccess(response) {
|
|
137
102
|
if (!response.success) {
|
|
@@ -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
|
|
@@ -12,13 +12,20 @@ export class SessionManager {
|
|
|
12
12
|
activeSessionGroupId = null;
|
|
13
13
|
sessionCounter = 0;
|
|
14
14
|
initialized = false;
|
|
15
|
+
initializationPromise;
|
|
15
16
|
maxSession;
|
|
16
17
|
maxOpenTab;
|
|
17
18
|
constructor(options = {}) {
|
|
18
19
|
this.maxSession = options.maxSession ?? 1;
|
|
19
20
|
this.maxOpenTab = options.maxOpenTab ?? 1;
|
|
20
|
-
// Restore session on creation
|
|
21
|
-
this.restoreSession();
|
|
21
|
+
// Restore session on creation and store the promise
|
|
22
|
+
this.initializationPromise = this.restoreSession();
|
|
23
|
+
}
|
|
24
|
+
/**
|
|
25
|
+
* Wait for SessionManager to finish initialization
|
|
26
|
+
*/
|
|
27
|
+
async waitForInitialization() {
|
|
28
|
+
await this.initializationPromise;
|
|
22
29
|
}
|
|
23
30
|
/**
|
|
24
31
|
* Restore session from storage
|
|
@@ -49,6 +56,8 @@ export class SessionManager {
|
|
|
49
56
|
else {
|
|
50
57
|
console.log('[SessionManager] No stored session found');
|
|
51
58
|
}
|
|
59
|
+
// Clean up any duplicate sessions after restoring
|
|
60
|
+
await this.cleanupDuplicateSessions();
|
|
52
61
|
}
|
|
53
62
|
catch (err) {
|
|
54
63
|
console.error('[SessionManager] Failed to restore session:', err);
|
|
@@ -57,6 +66,98 @@ export class SessionManager {
|
|
|
57
66
|
this.initialized = true;
|
|
58
67
|
}
|
|
59
68
|
}
|
|
69
|
+
/**
|
|
70
|
+
* Scan for and cleanup duplicate BTCP session groups
|
|
71
|
+
* Ensures only one BTCP session exists at any time
|
|
72
|
+
*
|
|
73
|
+
* @returns Stats about cleanup: { found, kept, removed }
|
|
74
|
+
*/
|
|
75
|
+
async cleanupDuplicateSessions() {
|
|
76
|
+
try {
|
|
77
|
+
console.log('[SessionManager] Scanning for duplicate BTCP sessions...');
|
|
78
|
+
// Get all tab groups
|
|
79
|
+
const allGroups = await chrome.tabGroups.query({});
|
|
80
|
+
const btcpGroups = allGroups.filter(g => g.title?.startsWith('BTCP'));
|
|
81
|
+
console.log(`[SessionManager] Found ${btcpGroups.length} BTCP session group(s)`);
|
|
82
|
+
if (btcpGroups.length <= 1) {
|
|
83
|
+
// No duplicates, nothing to clean up
|
|
84
|
+
return { found: btcpGroups.length, kept: btcpGroups.length, removed: 0 };
|
|
85
|
+
}
|
|
86
|
+
// Multiple sessions found - need to consolidate
|
|
87
|
+
console.warn(`[SessionManager] Found ${btcpGroups.length} BTCP sessions - cleaning up duplicates!`);
|
|
88
|
+
// Determine which session to keep
|
|
89
|
+
let toKeep;
|
|
90
|
+
// Priority 1: Keep the one matching stored session ID
|
|
91
|
+
if (this.activeSessionGroupId !== null) {
|
|
92
|
+
const activeGroup = btcpGroups.find(g => g.id === this.activeSessionGroupId);
|
|
93
|
+
if (activeGroup) {
|
|
94
|
+
console.log('[SessionManager] Keeping active session:', activeGroup.id);
|
|
95
|
+
toKeep = activeGroup;
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
// Priority 2: Keep the one from storage if active session not set
|
|
99
|
+
if (!toKeep) {
|
|
100
|
+
const result = await chrome.storage.session.get(SESSION_STORAGE_KEY);
|
|
101
|
+
const data = result[SESSION_STORAGE_KEY];
|
|
102
|
+
if (data?.groupId) {
|
|
103
|
+
const storedGroup = btcpGroups.find(g => g.id === data.groupId);
|
|
104
|
+
if (storedGroup) {
|
|
105
|
+
console.log('[SessionManager] Keeping stored session:', storedGroup.id);
|
|
106
|
+
toKeep = storedGroup;
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
// Priority 3: Keep the one with the most tabs
|
|
111
|
+
if (!toKeep) {
|
|
112
|
+
console.log('[SessionManager] No active/stored session - keeping the one with most tabs');
|
|
113
|
+
const groupsWithTabCounts = await Promise.all(btcpGroups.map(async (g) => ({
|
|
114
|
+
group: g,
|
|
115
|
+
tabs: await chrome.tabs.query({ groupId: g.id })
|
|
116
|
+
})));
|
|
117
|
+
groupsWithTabCounts.sort((a, b) => b.tabs.length - a.tabs.length);
|
|
118
|
+
toKeep = groupsWithTabCounts[0]?.group;
|
|
119
|
+
if (toKeep) {
|
|
120
|
+
console.log(`[SessionManager] Keeping session ${toKeep.id} with ${groupsWithTabCounts[0].tabs.length} tabs`);
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
// If we still don't have a group to keep (shouldn't happen), just keep the first one
|
|
124
|
+
if (!toKeep) {
|
|
125
|
+
toKeep = btcpGroups[0];
|
|
126
|
+
console.log('[SessionManager] Fallback: keeping first session:', toKeep.id);
|
|
127
|
+
}
|
|
128
|
+
// Delete all other sessions
|
|
129
|
+
const toDelete = btcpGroups.filter(g => g.id !== toKeep.id);
|
|
130
|
+
console.log(`[SessionManager] Removing ${toDelete.length} duplicate session(s)`);
|
|
131
|
+
for (const group of toDelete) {
|
|
132
|
+
try {
|
|
133
|
+
console.log(`[SessionManager] Deleting duplicate session: ${group.id} (${group.title})`);
|
|
134
|
+
// Get tabs in this group and ungroup them (don't close them)
|
|
135
|
+
const tabs = await chrome.tabs.query({ groupId: group.id });
|
|
136
|
+
for (const tab of tabs) {
|
|
137
|
+
if (tab.id !== undefined) {
|
|
138
|
+
await chrome.tabs.ungroup(tab.id);
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
catch (err) {
|
|
143
|
+
console.error(`[SessionManager] Failed to delete duplicate session ${group.id}:`, err);
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
// Set the kept session as active
|
|
147
|
+
this.activeSessionGroupId = toKeep.id;
|
|
148
|
+
await this.persistSession();
|
|
149
|
+
console.log('[SessionManager] Cleanup complete - only one session remains');
|
|
150
|
+
return {
|
|
151
|
+
found: btcpGroups.length,
|
|
152
|
+
kept: 1,
|
|
153
|
+
removed: toDelete.length
|
|
154
|
+
};
|
|
155
|
+
}
|
|
156
|
+
catch (err) {
|
|
157
|
+
console.error('[SessionManager] Failed to cleanup duplicate sessions:', err);
|
|
158
|
+
return { found: 0, kept: 0, removed: 0 };
|
|
159
|
+
}
|
|
160
|
+
}
|
|
60
161
|
/**
|
|
61
162
|
* Persist session to storage
|
|
62
163
|
*/
|
|
@@ -95,6 +196,7 @@ export class SessionManager {
|
|
|
95
196
|
* Used when popup detects a stored session that isn't currently active
|
|
96
197
|
*/
|
|
97
198
|
async reconnectSession(groupId) {
|
|
199
|
+
await this.waitForInitialization();
|
|
98
200
|
try {
|
|
99
201
|
console.log('[SessionManager] Attempting to reconnect to session group:', groupId);
|
|
100
202
|
// Verify the group still exists
|
|
@@ -106,6 +208,8 @@ export class SessionManager {
|
|
|
106
208
|
// Restore session state
|
|
107
209
|
this.activeSessionGroupId = groupId;
|
|
108
210
|
this.sessionCounter = data?.sessionCounter ?? this.sessionCounter;
|
|
211
|
+
// Persist session state after reconnecting
|
|
212
|
+
await this.persistSession();
|
|
109
213
|
console.log('[SessionManager] Session reconnected successfully');
|
|
110
214
|
return true;
|
|
111
215
|
}
|
|
@@ -120,6 +224,8 @@ export class SessionManager {
|
|
|
120
224
|
* Create a new tab group
|
|
121
225
|
*/
|
|
122
226
|
async createGroup(options = {}) {
|
|
227
|
+
// Wait for initialization to complete first
|
|
228
|
+
await this.waitForInitialization();
|
|
123
229
|
console.log('[SessionManager] createGroup called with options:', options);
|
|
124
230
|
// Check if we can create a new session
|
|
125
231
|
const canCreate = await this.canCreateSession();
|
|
@@ -246,6 +352,7 @@ export class SessionManager {
|
|
|
246
352
|
* Get current active session info
|
|
247
353
|
*/
|
|
248
354
|
async getCurrentSession() {
|
|
355
|
+
await this.waitForInitialization();
|
|
249
356
|
if (this.activeSessionGroupId === null) {
|
|
250
357
|
return null;
|
|
251
358
|
}
|
|
@@ -275,6 +382,13 @@ export class SessionManager {
|
|
|
275
382
|
getActiveSessionGroupId() {
|
|
276
383
|
return this.activeSessionGroupId;
|
|
277
384
|
}
|
|
385
|
+
/**
|
|
386
|
+
* Get the active session group ID (async version that ensures initialization is complete)
|
|
387
|
+
*/
|
|
388
|
+
async getActiveSessionGroupIdAsync() {
|
|
389
|
+
await this.waitForInitialization();
|
|
390
|
+
return this.activeSessionGroupId;
|
|
391
|
+
}
|
|
278
392
|
/**
|
|
279
393
|
* Get the maximum number of sessions allowed
|
|
280
394
|
*/
|
|
@@ -292,6 +406,7 @@ export class SessionManager {
|
|
|
292
406
|
* Closes oldest tabs if the limit is exceeded
|
|
293
407
|
*/
|
|
294
408
|
async enforceTabLimit() {
|
|
409
|
+
await this.waitForInitialization();
|
|
295
410
|
if (this.activeSessionGroupId === null) {
|
|
296
411
|
return;
|
|
297
412
|
}
|
|
@@ -328,41 +443,31 @@ export class SessionManager {
|
|
|
328
443
|
* 3. Existing tab groups (BTCP prefixed)
|
|
329
444
|
*/
|
|
330
445
|
async getSessionCount() {
|
|
331
|
-
//
|
|
332
|
-
if (this.activeSessionGroupId !== null) {
|
|
333
|
-
try {
|
|
334
|
-
// Verify the group still exists
|
|
335
|
-
await chrome.tabGroups.get(this.activeSessionGroupId);
|
|
336
|
-
return 1;
|
|
337
|
-
}
|
|
338
|
-
catch {
|
|
339
|
-
// Group no longer exists, clear it
|
|
340
|
-
this.activeSessionGroupId = null;
|
|
341
|
-
}
|
|
342
|
-
}
|
|
343
|
-
// Check if there's a persistent session in storage
|
|
446
|
+
// Always count all BTCP tab groups to detect duplicates
|
|
344
447
|
try {
|
|
345
|
-
const
|
|
346
|
-
const
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
448
|
+
const groups = await chrome.tabGroups.query({});
|
|
449
|
+
const btcpGroups = groups.filter(g => g.title?.startsWith('BTCP'));
|
|
450
|
+
// Clean up stale references while counting
|
|
451
|
+
if (this.activeSessionGroupId !== null) {
|
|
452
|
+
const activeExists = btcpGroups.some(g => g.id === this.activeSessionGroupId);
|
|
453
|
+
if (!activeExists) {
|
|
454
|
+
this.activeSessionGroupId = null;
|
|
352
455
|
}
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
456
|
+
}
|
|
457
|
+
// Check stored session exists
|
|
458
|
+
try {
|
|
459
|
+
const result = await chrome.storage.session.get(SESSION_STORAGE_KEY);
|
|
460
|
+
const data = result[SESSION_STORAGE_KEY];
|
|
461
|
+
if (data?.groupId) {
|
|
462
|
+
const storedExists = btcpGroups.some(g => g.id === data.groupId);
|
|
463
|
+
if (!storedExists) {
|
|
464
|
+
await this.clearStoredSession();
|
|
465
|
+
}
|
|
356
466
|
}
|
|
357
467
|
}
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
}
|
|
362
|
-
// Count existing BTCP tab groups
|
|
363
|
-
try {
|
|
364
|
-
const groups = await chrome.tabGroups.query({});
|
|
365
|
-
const btcpGroups = groups.filter(g => g.title?.startsWith('BTCP'));
|
|
468
|
+
catch (err) {
|
|
469
|
+
console.error('[SessionManager] Failed to check persistent session:', err);
|
|
470
|
+
}
|
|
366
471
|
return btcpGroups.length;
|
|
367
472
|
}
|
|
368
473
|
catch (err) {
|
|
@@ -404,11 +509,65 @@ export class SessionManager {
|
|
|
404
509
|
return false;
|
|
405
510
|
}
|
|
406
511
|
}
|
|
512
|
+
/**
|
|
513
|
+
* Ensure a session exists - restore from storage, use existing, or create new
|
|
514
|
+
* Returns the session group ID (creates if needed)
|
|
515
|
+
*/
|
|
516
|
+
async ensureSession() {
|
|
517
|
+
await this.waitForInitialization();
|
|
518
|
+
// Step 1: Already have active session
|
|
519
|
+
if (this.activeSessionGroupId !== null) {
|
|
520
|
+
// Verify it still exists
|
|
521
|
+
try {
|
|
522
|
+
await chrome.tabGroups.get(this.activeSessionGroupId);
|
|
523
|
+
return this.activeSessionGroupId;
|
|
524
|
+
}
|
|
525
|
+
catch {
|
|
526
|
+
// Group no longer exists, continue to restore/create
|
|
527
|
+
this.activeSessionGroupId = null;
|
|
528
|
+
}
|
|
529
|
+
}
|
|
530
|
+
// Step 2: Try to restore from storage
|
|
531
|
+
const result = await chrome.storage.session.get(SESSION_STORAGE_KEY);
|
|
532
|
+
const stored = result[SESSION_STORAGE_KEY];
|
|
533
|
+
if (stored?.groupId) {
|
|
534
|
+
const reconnected = await this.reconnectSession(stored.groupId);
|
|
535
|
+
if (reconnected && this.activeSessionGroupId !== null) {
|
|
536
|
+
return this.activeSessionGroupId;
|
|
537
|
+
}
|
|
538
|
+
}
|
|
539
|
+
// Step 3: Find existing BTCP group
|
|
540
|
+
const groups = await chrome.tabGroups.query({});
|
|
541
|
+
const btcpGroup = groups.find(g => g.title?.startsWith('BTCP'));
|
|
542
|
+
if (btcpGroup) {
|
|
543
|
+
const used = await this.useExistingGroupAsSession(btcpGroup.id);
|
|
544
|
+
if (used && this.activeSessionGroupId !== null) {
|
|
545
|
+
return this.activeSessionGroupId;
|
|
546
|
+
}
|
|
547
|
+
}
|
|
548
|
+
// Step 4: Create new session
|
|
549
|
+
console.log('[SessionManager] No existing session found, creating new one...');
|
|
550
|
+
const newGroup = await this.createGroup({ color: 'blue' });
|
|
551
|
+
return newGroup.id;
|
|
552
|
+
}
|
|
553
|
+
/**
|
|
554
|
+
* Get the primary tab in session (ensures session exists first)
|
|
555
|
+
* Returns the first tab in the session group
|
|
556
|
+
*/
|
|
557
|
+
async getSessionTab() {
|
|
558
|
+
const groupId = await this.ensureSession();
|
|
559
|
+
const tabs = await chrome.tabs.query({ groupId });
|
|
560
|
+
if (tabs.length === 0 || tabs[0].id === undefined) {
|
|
561
|
+
throw new Error('Session exists but has no tabs');
|
|
562
|
+
}
|
|
563
|
+
return tabs[0].id;
|
|
564
|
+
}
|
|
407
565
|
/**
|
|
408
566
|
* Add a tab to the active session (if one exists)
|
|
409
567
|
* Automatically enforces the tab limit after adding
|
|
410
568
|
*/
|
|
411
569
|
async addTabToActiveSession(tabId) {
|
|
570
|
+
await this.waitForInitialization();
|
|
412
571
|
if (this.activeSessionGroupId === null) {
|
|
413
572
|
return false;
|
|
414
573
|
}
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Base transport class with shared functionality
|
|
3
|
+
*
|
|
4
|
+
* Provides event handling, state management, and utility methods for transports.
|
|
5
|
+
*/
|
|
6
|
+
import type { Command, Response } from '../types.js';
|
|
7
|
+
import type { Transport, TransportEvents, TransportOptions, TransportState } from './types.js';
|
|
8
|
+
/**
|
|
9
|
+
* Abstract base class for transports
|
|
10
|
+
*
|
|
11
|
+
* Provides common functionality for event handling and state management.
|
|
12
|
+
* Subclasses must implement `send()`, `connect()`, and `disconnect()`.
|
|
13
|
+
*/
|
|
14
|
+
export declare abstract class BaseTransport implements Transport {
|
|
15
|
+
abstract readonly name: string;
|
|
16
|
+
protected state: TransportState;
|
|
17
|
+
protected debug: boolean;
|
|
18
|
+
private eventHandlers;
|
|
19
|
+
constructor(options?: TransportOptions);
|
|
20
|
+
/**
|
|
21
|
+
* Send a command - must be implemented by subclasses
|
|
22
|
+
*/
|
|
23
|
+
abstract send(command: Command): Promise<Response>;
|
|
24
|
+
/**
|
|
25
|
+
* Connect the transport - must be implemented by subclasses
|
|
26
|
+
*/
|
|
27
|
+
abstract connect(): Promise<void>;
|
|
28
|
+
/**
|
|
29
|
+
* Disconnect the transport - must be implemented by subclasses
|
|
30
|
+
*/
|
|
31
|
+
abstract disconnect(): void;
|
|
32
|
+
/**
|
|
33
|
+
* Get the current connection state
|
|
34
|
+
*/
|
|
35
|
+
getState(): TransportState;
|
|
36
|
+
/**
|
|
37
|
+
* Check if the transport is connected
|
|
38
|
+
*/
|
|
39
|
+
isConnected(): boolean;
|
|
40
|
+
/**
|
|
41
|
+
* Register an event handler
|
|
42
|
+
*/
|
|
43
|
+
on<K extends keyof TransportEvents>(event: K, handler: TransportEvents[K]): void;
|
|
44
|
+
/**
|
|
45
|
+
* Unregister an event handler
|
|
46
|
+
*/
|
|
47
|
+
off<K extends keyof TransportEvents>(event: K, handler: TransportEvents[K]): void;
|
|
48
|
+
/**
|
|
49
|
+
* Emit an event to all registered handlers
|
|
50
|
+
*/
|
|
51
|
+
protected emit<K extends keyof TransportEvents>(event: K, ...args: Parameters<TransportEvents[K]>): void;
|
|
52
|
+
/**
|
|
53
|
+
* Update the transport state and emit stateChange event
|
|
54
|
+
*/
|
|
55
|
+
protected setState(newState: TransportState): void;
|
|
56
|
+
/**
|
|
57
|
+
* Log a message if debug is enabled
|
|
58
|
+
*/
|
|
59
|
+
protected log(level: 'debug' | 'info' | 'warn' | 'error', ...args: unknown[]): void;
|
|
60
|
+
/**
|
|
61
|
+
* Create an error response with the given message
|
|
62
|
+
*/
|
|
63
|
+
protected createErrorResponse(id: string, error: string): Response;
|
|
64
|
+
}
|
|
65
|
+
//# sourceMappingURL=base-transport.d.ts.map
|