@flrande/bak-extension 0.6.14 → 0.6.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/dist/.bak-e2e-build-stamp +1 -1
- package/dist/background.global.js +406 -92
- package/dist/content.global.js +9 -3
- package/dist/manifest.json +1 -1
- package/dist/popup.global.js +11 -8
- package/dist/popup.html +13 -3
- package/package.json +2 -2
- package/public/popup.html +13 -3
- package/src/background.ts +537 -258
- package/src/content.ts +9 -3
- package/src/dynamic-data-tools.ts +2 -2
- package/src/network-debugger.ts +90 -12
- package/src/popup.ts +27 -9
- package/src/session-binding-storage.ts +7 -1
- package/src/session-binding.ts +41 -11
package/src/content.ts
CHANGED
|
@@ -1936,7 +1936,7 @@ function describeTables(): TableHandle[] {
|
|
|
1936
1936
|
for (const [index, table] of htmlTables.entries()) {
|
|
1937
1937
|
const handle: TableHandle = {
|
|
1938
1938
|
id: buildTableId(table.closest('.dataTables_wrapper') ? 'dataTables' : 'html', index),
|
|
1939
|
-
|
|
1939
|
+
label: (table.getAttribute('aria-label') || table.getAttribute('data-testid') || table.id || `table-${index + 1}`).trim(),
|
|
1940
1940
|
kind: table.closest('.dataTables_wrapper') ? 'dataTables' : 'html',
|
|
1941
1941
|
selector: table.id ? `#${table.id}` : undefined,
|
|
1942
1942
|
rowCount: table.querySelectorAll('tbody tr').length || table.querySelectorAll('tr').length,
|
|
@@ -1949,7 +1949,7 @@ function describeTables(): TableHandle[] {
|
|
|
1949
1949
|
const kind: TableHandle['kind'] = grid.className.includes('ag-') ? 'ag-grid' : 'aria-grid';
|
|
1950
1950
|
const handle: TableHandle = {
|
|
1951
1951
|
id: buildTableId(kind, index),
|
|
1952
|
-
|
|
1952
|
+
label: (grid.getAttribute('aria-label') || grid.getAttribute('data-testid') || grid.id || `grid-${index + 1}`).trim(),
|
|
1953
1953
|
kind,
|
|
1954
1954
|
selector: grid.id ? `#${grid.id}` : undefined,
|
|
1955
1955
|
rowCount: gridRowNodes(grid).length,
|
|
@@ -1962,7 +1962,13 @@ function describeTables(): TableHandle[] {
|
|
|
1962
1962
|
|
|
1963
1963
|
function resolveTable(handleId: string): { table: TableHandle; element: Element | null } | null {
|
|
1964
1964
|
const tables = describeTables();
|
|
1965
|
-
const
|
|
1965
|
+
const normalizedHandleId = handleId.trim().toLowerCase();
|
|
1966
|
+
const handle = tables.find((candidate) => {
|
|
1967
|
+
if (candidate.id === handleId) {
|
|
1968
|
+
return true;
|
|
1969
|
+
}
|
|
1970
|
+
return typeof candidate.label === 'string' && candidate.label.trim().toLowerCase() === normalizedHandleId;
|
|
1971
|
+
});
|
|
1966
1972
|
if (!handle) {
|
|
1967
1973
|
return null;
|
|
1968
1974
|
}
|
|
@@ -530,9 +530,9 @@ function scoreSourceMapping(table: TableAnalysis, source: DynamicSourceAnalysis,
|
|
|
530
530
|
}
|
|
531
531
|
|
|
532
532
|
const explicitReferenceHit =
|
|
533
|
-
table.table.
|
|
533
|
+
table.table.label.toLowerCase().includes(source.source.label.toLowerCase()) ||
|
|
534
534
|
(table.table.selector ?? '').toLowerCase().includes(source.source.label.toLowerCase()) ||
|
|
535
|
-
source.source.label.toLowerCase().includes(table.table.
|
|
535
|
+
source.source.label.toLowerCase().includes(table.table.label.toLowerCase());
|
|
536
536
|
if (explicitReferenceHit) {
|
|
537
537
|
basis.push({
|
|
538
538
|
type: 'explicitReference',
|
package/src/network-debugger.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { NetworkEntry } from '@flrande/bak-protocol';
|
|
1
|
+
import type { NetworkEntry, NetworkSearchResult } from '@flrande/bak-protocol';
|
|
2
2
|
import { redactHeaderMap, redactTransportText } from './privacy.js';
|
|
3
3
|
import { EXTENSION_VERSION } from './version.js';
|
|
4
4
|
|
|
@@ -16,12 +16,18 @@ interface DebuggerTarget {
|
|
|
16
16
|
interface TabCaptureState {
|
|
17
17
|
attached: boolean;
|
|
18
18
|
attachError: string | null;
|
|
19
|
-
entries:
|
|
20
|
-
entriesById: Map<string,
|
|
19
|
+
entries: CapturedNetworkEntry[];
|
|
20
|
+
entriesById: Map<string, CapturedNetworkEntry>;
|
|
21
21
|
requestIdToEntryId: Map<string, string>;
|
|
22
22
|
lastTouchedAt: number;
|
|
23
23
|
}
|
|
24
24
|
|
|
25
|
+
interface CapturedNetworkEntry extends NetworkEntry {
|
|
26
|
+
rawRequestHeaders?: Record<string, string>;
|
|
27
|
+
rawRequestBody?: string;
|
|
28
|
+
rawRequestBodyTruncated?: boolean;
|
|
29
|
+
}
|
|
30
|
+
|
|
25
31
|
const captures = new Map<number, TabCaptureState>();
|
|
26
32
|
|
|
27
33
|
function getState(tabId: number): TabCaptureState {
|
|
@@ -121,7 +127,25 @@ function isTextualContentType(contentType: string | undefined): boolean {
|
|
|
121
127
|
);
|
|
122
128
|
}
|
|
123
129
|
|
|
124
|
-
function
|
|
130
|
+
function sanitizeEntry(entry: CapturedNetworkEntry): NetworkEntry {
|
|
131
|
+
const { rawRequestHeaders, rawRequestBody, rawRequestBodyTruncated, ...publicEntry } = entry;
|
|
132
|
+
void rawRequestHeaders;
|
|
133
|
+
void rawRequestBody;
|
|
134
|
+
void rawRequestBodyTruncated;
|
|
135
|
+
return {
|
|
136
|
+
...publicEntry,
|
|
137
|
+
requestHeaders:
|
|
138
|
+
typeof entry.requestHeaders === 'object' && entry.requestHeaders !== null
|
|
139
|
+
? { ...entry.requestHeaders }
|
|
140
|
+
: undefined,
|
|
141
|
+
responseHeaders:
|
|
142
|
+
typeof entry.responseHeaders === 'object' && entry.responseHeaders !== null
|
|
143
|
+
? { ...entry.responseHeaders }
|
|
144
|
+
: undefined
|
|
145
|
+
};
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
function pushEntry(state: TabCaptureState, entry: CapturedNetworkEntry, requestId: string): void {
|
|
125
149
|
state.entries.push(entry);
|
|
126
150
|
state.entriesById.set(entry.id, entry);
|
|
127
151
|
state.requestIdToEntryId.set(requestId, entry.id);
|
|
@@ -135,7 +159,7 @@ function pushEntry(state: TabCaptureState, entry: NetworkEntry, requestId: strin
|
|
|
135
159
|
}
|
|
136
160
|
}
|
|
137
161
|
|
|
138
|
-
function entryForRequest(tabId: number, requestId: string):
|
|
162
|
+
function entryForRequest(tabId: number, requestId: string): CapturedNetworkEntry | null {
|
|
139
163
|
const state = captures.get(tabId);
|
|
140
164
|
if (!state) {
|
|
141
165
|
return null;
|
|
@@ -206,9 +230,10 @@ function upsertRequest(tabId: number, params: Record<string, unknown>): void {
|
|
|
206
230
|
return;
|
|
207
231
|
}
|
|
208
232
|
const request = typeof params.request === 'object' && params.request !== null ? (params.request as Record<string, unknown>) : {};
|
|
209
|
-
const
|
|
233
|
+
const rawHeaders = normalizeHeaders(request.headers);
|
|
234
|
+
const headers = redactHeaderMap(rawHeaders);
|
|
210
235
|
const truncatedRequest = truncateText(typeof request.postData === 'string' ? request.postData : undefined, DEFAULT_BODY_BYTES);
|
|
211
|
-
const entry:
|
|
236
|
+
const entry: CapturedNetworkEntry = {
|
|
212
237
|
id: `net_${tabId}_${requestId}`,
|
|
213
238
|
url: typeof request.url === 'string' ? request.url : '',
|
|
214
239
|
method: typeof request.method === 'string' ? request.method : 'GET',
|
|
@@ -230,6 +255,9 @@ function upsertRequest(tabId: number, params: Record<string, unknown>): void {
|
|
|
230
255
|
requestHeaders: headers,
|
|
231
256
|
requestBodyPreview: truncatedRequest.text ? redactTransportText(truncatedRequest.text) : undefined,
|
|
232
257
|
requestBodyTruncated: truncatedRequest.truncated,
|
|
258
|
+
rawRequestHeaders: rawHeaders,
|
|
259
|
+
rawRequestBody: typeof request.postData === 'string' ? request.postData : undefined,
|
|
260
|
+
rawRequestBodyTruncated: false,
|
|
233
261
|
initiatorUrl:
|
|
234
262
|
typeof params.initiator === 'object' &&
|
|
235
263
|
params.initiator !== null &&
|
|
@@ -378,13 +406,43 @@ export function listNetworkEntries(
|
|
|
378
406
|
.filter((entry) => entryMatchesFilters(entry, filters))
|
|
379
407
|
.slice(-limit)
|
|
380
408
|
.reverse()
|
|
381
|
-
.map((entry) => (
|
|
409
|
+
.map((entry) => sanitizeEntry(entry));
|
|
382
410
|
}
|
|
383
411
|
|
|
384
412
|
export function getNetworkEntry(tabId: number, id: string): NetworkEntry | null {
|
|
385
413
|
const state = getState(tabId);
|
|
386
414
|
const entry = state.entriesById.get(id);
|
|
387
|
-
return entry ?
|
|
415
|
+
return entry ? sanitizeEntry(entry) : null;
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
export function getReplayableNetworkRequest(
|
|
419
|
+
tabId: number,
|
|
420
|
+
id: string
|
|
421
|
+
): {
|
|
422
|
+
entry: NetworkEntry;
|
|
423
|
+
headers?: Record<string, string>;
|
|
424
|
+
body?: string;
|
|
425
|
+
contentType?: string;
|
|
426
|
+
degradedReason?: string;
|
|
427
|
+
} | null {
|
|
428
|
+
const state = getState(tabId);
|
|
429
|
+
const entry = state.entriesById.get(id);
|
|
430
|
+
if (!entry) {
|
|
431
|
+
return null;
|
|
432
|
+
}
|
|
433
|
+
const publicEntry = sanitizeEntry(entry);
|
|
434
|
+
if (entry.rawRequestBodyTruncated === true) {
|
|
435
|
+
return {
|
|
436
|
+
entry: publicEntry,
|
|
437
|
+
degradedReason: 'live replay unavailable because the captured request body was truncated in memory'
|
|
438
|
+
};
|
|
439
|
+
}
|
|
440
|
+
return {
|
|
441
|
+
entry: publicEntry,
|
|
442
|
+
headers: entry.rawRequestHeaders ? { ...entry.rawRequestHeaders } : undefined,
|
|
443
|
+
body: entry.rawRequestBody,
|
|
444
|
+
contentType: headerValue(entry.rawRequestHeaders, 'content-type')
|
|
445
|
+
};
|
|
388
446
|
}
|
|
389
447
|
|
|
390
448
|
export async function waitForNetworkEntry(
|
|
@@ -405,7 +463,7 @@ export async function waitForNetworkEntry(
|
|
|
405
463
|
const nextState = getState(tabId);
|
|
406
464
|
const matched = nextState.entries.find((entry) => !seenIds.has(entry.id) && entryMatchesFilters(entry, filters));
|
|
407
465
|
if (matched) {
|
|
408
|
-
return
|
|
466
|
+
return sanitizeEntry(matched);
|
|
409
467
|
}
|
|
410
468
|
await new Promise((resolve) => setTimeout(resolve, 75));
|
|
411
469
|
}
|
|
@@ -415,9 +473,10 @@ export async function waitForNetworkEntry(
|
|
|
415
473
|
};
|
|
416
474
|
}
|
|
417
475
|
|
|
418
|
-
export function searchNetworkEntries(tabId: number, pattern: string, limit = 50):
|
|
476
|
+
export function searchNetworkEntries(tabId: number, pattern: string, limit = 50): NetworkSearchResult {
|
|
477
|
+
const state = getState(tabId);
|
|
419
478
|
const normalized = pattern.toLowerCase();
|
|
420
|
-
|
|
479
|
+
const matchedEntries = state.entries.filter((entry) => {
|
|
421
480
|
const headerText = JSON.stringify({
|
|
422
481
|
requestHeaders: entry.requestHeaders,
|
|
423
482
|
responseHeaders: entry.responseHeaders
|
|
@@ -429,6 +488,25 @@ export function searchNetworkEntries(tabId: number, pattern: string, limit = 50)
|
|
|
429
488
|
headerText.includes(normalized)
|
|
430
489
|
);
|
|
431
490
|
});
|
|
491
|
+
const scannedEntries = state.entries.filter((entry) => entryMatchesFilters(entry, {}));
|
|
492
|
+
const toCoverage = (
|
|
493
|
+
entries: CapturedNetworkEntry[],
|
|
494
|
+
key: 'requestBodyPreview' | 'responseBodyPreview',
|
|
495
|
+
truncatedKey: 'requestBodyTruncated' | 'responseBodyTruncated'
|
|
496
|
+
) => ({
|
|
497
|
+
full: entries.filter((entry) => typeof entry[key] === 'string' && entry[truncatedKey] !== true).length,
|
|
498
|
+
partial: entries.filter((entry) => typeof entry[key] === 'string' && entry[truncatedKey] === true).length,
|
|
499
|
+
none: entries.filter((entry) => typeof entry[key] !== 'string').length
|
|
500
|
+
});
|
|
501
|
+
return {
|
|
502
|
+
entries: matchedEntries.slice(-Math.max(limit, 1)).reverse().map((entry) => sanitizeEntry(entry)),
|
|
503
|
+
scanned: scannedEntries.length,
|
|
504
|
+
matched: matchedEntries.length,
|
|
505
|
+
bodyCoverage: {
|
|
506
|
+
request: toCoverage(scannedEntries, 'requestBodyPreview', 'requestBodyTruncated'),
|
|
507
|
+
response: toCoverage(scannedEntries, 'responseBodyPreview', 'responseBodyTruncated')
|
|
508
|
+
}
|
|
509
|
+
};
|
|
432
510
|
}
|
|
433
511
|
|
|
434
512
|
export function latestNetworkTimestamp(tabId: number): number | null {
|
package/src/popup.ts
CHANGED
|
@@ -21,6 +21,7 @@ const sessionSummaryEl = document.getElementById('sessionSummary') as HTMLDivEle
|
|
|
21
21
|
const sessionCardsEl = document.getElementById('sessionCards') as HTMLDivElement;
|
|
22
22
|
|
|
23
23
|
type PopupTone = 'neutral' | 'success' | 'warning' | 'error';
|
|
24
|
+
type PopupSessionBindingStatus = 'attached' | 'window-only' | 'detached';
|
|
24
25
|
|
|
25
26
|
interface PopupState {
|
|
26
27
|
ok: boolean;
|
|
@@ -42,6 +43,7 @@ interface PopupState {
|
|
|
42
43
|
sessionBindings: {
|
|
43
44
|
count: number;
|
|
44
45
|
attachedCount: number;
|
|
46
|
+
windowOnlyCount: number;
|
|
45
47
|
detachedCount: number;
|
|
46
48
|
tabCount: number;
|
|
47
49
|
items: Array<{
|
|
@@ -53,7 +55,7 @@ interface PopupState {
|
|
|
53
55
|
activeTabUrl: string | null;
|
|
54
56
|
windowId: number | null;
|
|
55
57
|
groupId: number | null;
|
|
56
|
-
|
|
58
|
+
status: PopupSessionBindingStatus;
|
|
57
59
|
lastBindingUpdateAt: number | null;
|
|
58
60
|
lastBindingUpdateReason: string | null;
|
|
59
61
|
}>;
|
|
@@ -158,6 +160,8 @@ function describeStatus(state: PopupState): StatusDescriptor {
|
|
|
158
160
|
const body =
|
|
159
161
|
state.sessionBindings.detachedCount > 0
|
|
160
162
|
? `${pluralize(state.sessionBindings.detachedCount, 'remembered session')} are detached. Check the cards below before you continue browser work.`
|
|
163
|
+
: state.sessionBindings.windowOnlyCount > 0
|
|
164
|
+
? `${pluralize(state.sessionBindings.windowOnlyCount, 'remembered session')} still point at a live window, but do not own tabs yet.`
|
|
161
165
|
: 'The extension bridge is healthy and ready for CLI-driven browser work.';
|
|
162
166
|
return {
|
|
163
167
|
badge: 'Ready',
|
|
@@ -169,6 +173,11 @@ function describeStatus(state: PopupState): StatusDescriptor {
|
|
|
169
173
|
'Resume or recreate detached work from the bak CLI before sending new page commands.',
|
|
170
174
|
'Use the Sessions panel below to confirm which remembered session lost its owned tabs.'
|
|
171
175
|
]
|
|
176
|
+
: state.sessionBindings.windowOnlyCount > 0
|
|
177
|
+
? [
|
|
178
|
+
'New bak page commands can reuse those remembered windows without disturbing your human tabs.',
|
|
179
|
+
'Use the Sessions panel below to confirm which binding is only waiting for a new owned tab.'
|
|
180
|
+
]
|
|
172
181
|
: [
|
|
173
182
|
'Start browser work from the bak CLI.',
|
|
174
183
|
'Use Reconnect bridge only when you intentionally changed token or port settings.'
|
|
@@ -311,6 +320,7 @@ function renderSessionBindings(state: PopupState['sessionBindings']): void {
|
|
|
311
320
|
sessionSummaryEl.textContent =
|
|
312
321
|
`${pluralize(state.count, 'session')}, ` +
|
|
313
322
|
`${pluralize(state.attachedCount, 'attached binding')}, ` +
|
|
323
|
+
`${pluralize(state.windowOnlyCount, 'window-only binding')}, ` +
|
|
314
324
|
`${pluralize(state.detachedCount, 'detached binding')}, ` +
|
|
315
325
|
`${pluralize(state.tabCount, 'tracked tab')}`;
|
|
316
326
|
}
|
|
@@ -328,7 +338,7 @@ function renderSessionBindings(state: PopupState['sessionBindings']): void {
|
|
|
328
338
|
for (const item of state.items) {
|
|
329
339
|
const card = document.createElement('section');
|
|
330
340
|
card.className = 'session-card';
|
|
331
|
-
card.dataset.
|
|
341
|
+
card.dataset.status = item.status;
|
|
332
342
|
|
|
333
343
|
const header = document.createElement('div');
|
|
334
344
|
header.className = 'session-card-header';
|
|
@@ -348,19 +358,24 @@ function renderSessionBindings(state: PopupState['sessionBindings']): void {
|
|
|
348
358
|
|
|
349
359
|
const badge = document.createElement('span');
|
|
350
360
|
badge.className = 'session-badge';
|
|
351
|
-
badge.dataset.
|
|
352
|
-
badge.textContent = item.
|
|
361
|
+
badge.dataset.status = item.status;
|
|
362
|
+
badge.textContent = item.status === 'attached' ? 'Attached' : item.status === 'window-only' ? 'Window only' : 'Detached';
|
|
353
363
|
|
|
354
364
|
header.append(titleWrap, badge);
|
|
355
365
|
|
|
356
366
|
const activeTitle = document.createElement('div');
|
|
357
367
|
activeTitle.className = 'session-active-title';
|
|
358
|
-
activeTitle.textContent =
|
|
368
|
+
activeTitle.textContent =
|
|
369
|
+
item.activeTabTitle
|
|
370
|
+
? truncate(item.activeTabTitle, 72)
|
|
371
|
+
: item.status === 'window-only'
|
|
372
|
+
? 'No owned tab yet'
|
|
373
|
+
: 'No active tab title';
|
|
359
374
|
activeTitle.title = item.activeTabTitle ?? '';
|
|
360
375
|
|
|
361
376
|
const activeUrl = document.createElement('div');
|
|
362
377
|
activeUrl.className = 'session-active-url';
|
|
363
|
-
activeUrl.textContent = formatUrl(item.activeTabUrl);
|
|
378
|
+
activeUrl.textContent = item.status === 'window-only' && !item.activeTabUrl ? 'Next bak page command can reuse this window' : formatUrl(item.activeTabUrl);
|
|
364
379
|
activeUrl.title = item.activeTabUrl ?? '';
|
|
365
380
|
|
|
366
381
|
const meta = document.createElement('div');
|
|
@@ -380,9 +395,12 @@ function renderSessionBindings(state: PopupState['sessionBindings']): void {
|
|
|
380
395
|
|
|
381
396
|
const footer = document.createElement('div');
|
|
382
397
|
footer.className = 'session-card-footer';
|
|
383
|
-
footer.textContent =
|
|
384
|
-
|
|
385
|
-
|
|
398
|
+
footer.textContent =
|
|
399
|
+
item.status === 'attached'
|
|
400
|
+
? 'The saved binding still points at live browser tabs.'
|
|
401
|
+
: item.status === 'window-only'
|
|
402
|
+
? 'bak still remembers the target window for this session, but it does not own any live tabs right now.'
|
|
403
|
+
: 'bak still remembers this session, but its owned tabs or window are missing.';
|
|
386
404
|
|
|
387
405
|
card.append(header, activeTitle, activeUrl, meta, footer);
|
|
388
406
|
sessionCardsEl.appendChild(card);
|
|
@@ -18,10 +18,16 @@ function isSessionBindingRecord(value: unknown): value is SessionBindingRecord {
|
|
|
18
18
|
}
|
|
19
19
|
|
|
20
20
|
function cloneSessionBindingRecord(state: SessionBindingRecord): SessionBindingRecord {
|
|
21
|
-
|
|
21
|
+
const cloned: SessionBindingRecord = {
|
|
22
22
|
...state,
|
|
23
23
|
tabIds: [...state.tabIds]
|
|
24
24
|
};
|
|
25
|
+
if (cloned.tabIds.length === 0) {
|
|
26
|
+
cloned.groupId = null;
|
|
27
|
+
cloned.activeTabId = null;
|
|
28
|
+
cloned.primaryTabId = null;
|
|
29
|
+
}
|
|
30
|
+
return cloned;
|
|
25
31
|
}
|
|
26
32
|
|
|
27
33
|
function normalizeSessionBindingRecordMap(value: unknown): { found: boolean; map: Record<string, SessionBindingRecord> } {
|
package/src/session-binding.ts
CHANGED
|
@@ -482,10 +482,10 @@ class SessionBindingManager {
|
|
|
482
482
|
};
|
|
483
483
|
}
|
|
484
484
|
|
|
485
|
-
async closeTab(bindingId: string, tabId?: number): Promise<{ binding: SessionBindingInfo | null; closedTabId: number }> {
|
|
486
|
-
const binding = await this.inspectBinding(bindingId);
|
|
487
|
-
if (!binding) {
|
|
488
|
-
throw new Error(`Binding ${bindingId} does not exist`);
|
|
485
|
+
async closeTab(bindingId: string, tabId?: number): Promise<{ binding: SessionBindingInfo | null; closedTabId: number }> {
|
|
486
|
+
const binding = await this.inspectBinding(bindingId);
|
|
487
|
+
if (!binding) {
|
|
488
|
+
throw new Error(`Binding ${bindingId} does not exist`);
|
|
489
489
|
}
|
|
490
490
|
const resolvedTabId =
|
|
491
491
|
typeof tabId === 'number'
|
|
@@ -505,13 +505,43 @@ class SessionBindingManager {
|
|
|
505
505
|
closedTabId: resolvedTabId
|
|
506
506
|
};
|
|
507
507
|
}
|
|
508
|
-
|
|
509
|
-
const tabs = await this.readLooseTrackedTabs(remainingTabIds);
|
|
510
|
-
|
|
511
|
-
binding.
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
508
|
+
|
|
509
|
+
const tabs = await this.readLooseTrackedTabs(remainingTabIds);
|
|
510
|
+
if (tabs.length === 0) {
|
|
511
|
+
const liveWindow = binding.windowId !== null ? await this.waitForWindow(binding.windowId, 300) : null;
|
|
512
|
+
if (!liveWindow) {
|
|
513
|
+
await this.storage.delete(binding.id);
|
|
514
|
+
return {
|
|
515
|
+
binding: null,
|
|
516
|
+
closedTabId: resolvedTabId
|
|
517
|
+
};
|
|
518
|
+
}
|
|
519
|
+
|
|
520
|
+
const nextState: SessionBindingRecord = {
|
|
521
|
+
id: binding.id,
|
|
522
|
+
label: binding.label,
|
|
523
|
+
color: binding.color,
|
|
524
|
+
windowId: liveWindow.id,
|
|
525
|
+
groupId: null,
|
|
526
|
+
tabIds: [],
|
|
527
|
+
activeTabId: null,
|
|
528
|
+
primaryTabId: null
|
|
529
|
+
};
|
|
530
|
+
await this.storage.save(nextState);
|
|
531
|
+
return {
|
|
532
|
+
binding: {
|
|
533
|
+
...nextState,
|
|
534
|
+
tabs: []
|
|
535
|
+
},
|
|
536
|
+
closedTabId: resolvedTabId
|
|
537
|
+
};
|
|
538
|
+
}
|
|
539
|
+
|
|
540
|
+
const nextPrimaryTabId =
|
|
541
|
+
binding.primaryTabId === resolvedTabId ? tabs[0]?.id ?? null : binding.primaryTabId;
|
|
542
|
+
const nextActiveTabId =
|
|
543
|
+
binding.activeTabId === resolvedTabId
|
|
544
|
+
? tabs.find((candidate) => candidate.active)?.id ?? nextPrimaryTabId ?? tabs[0]?.id ?? null
|
|
515
545
|
: binding.activeTabId;
|
|
516
546
|
const nextState: SessionBindingRecord = {
|
|
517
547
|
id: binding.id,
|