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
|
@@ -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
|
}
|