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.
@@ -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
- // If we have an active session, that counts as 1
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 result = await chrome.storage.session.get(SESSION_STORAGE_KEY);
346
- const data = result[SESSION_STORAGE_KEY];
347
- if (data?.groupId) {
348
- // Verify the stored group still exists
349
- try {
350
- await chrome.tabGroups.get(data.groupId);
351
- return 1;
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
- catch {
354
- // Group no longer exists, clear storage
355
- await this.clearStoredSession();
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
- catch (err) {
360
- console.error('[SessionManager] Failed to check persistent session:', err);
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
  }