@flrande/bak-extension 0.6.3 → 0.6.5
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 +267 -55
- package/dist/manifest.json +1 -1
- package/dist/popup.global.js +92 -2
- package/dist/popup.html +72 -1
- package/package.json +2 -2
- package/public/popup.html +72 -1
- package/src/background.ts +355 -134
- package/src/popup.ts +135 -11
- package/src/session-binding.ts +132 -74
package/src/popup.ts
CHANGED
|
@@ -3,28 +3,140 @@ const tokenInput = document.getElementById('token') as HTMLInputElement;
|
|
|
3
3
|
const portInput = document.getElementById('port') as HTMLInputElement;
|
|
4
4
|
const debugRichTextInput = document.getElementById('debugRichText') as HTMLInputElement;
|
|
5
5
|
const saveBtn = document.getElementById('save') as HTMLButtonElement;
|
|
6
|
+
const reconnectBtn = document.getElementById('reconnect') as HTMLButtonElement;
|
|
6
7
|
const disconnectBtn = document.getElementById('disconnect') as HTMLButtonElement;
|
|
8
|
+
const connectionStateEl = document.getElementById('connectionState') as HTMLDivElement;
|
|
9
|
+
const tokenStateEl = document.getElementById('tokenState') as HTMLDivElement;
|
|
10
|
+
const reconnectStateEl = document.getElementById('reconnectState') as HTMLDivElement;
|
|
11
|
+
const connectionUrlEl = document.getElementById('connectionUrl') as HTMLDivElement;
|
|
12
|
+
const lastErrorEl = document.getElementById('lastError') as HTMLDivElement;
|
|
13
|
+
const lastBindingUpdateEl = document.getElementById('lastBindingUpdate') as HTMLDivElement;
|
|
14
|
+
const extensionVersionEl = document.getElementById('extensionVersion') as HTMLDivElement;
|
|
15
|
+
const sessionSummaryEl = document.getElementById('sessionSummary') as HTMLDivElement;
|
|
16
|
+
const sessionListEl = document.getElementById('sessionList') as HTMLUListElement;
|
|
17
|
+
|
|
18
|
+
interface PopupState {
|
|
19
|
+
ok: boolean;
|
|
20
|
+
connected: boolean;
|
|
21
|
+
connectionState: 'connected' | 'connecting' | 'reconnecting' | 'disconnected' | 'manual' | 'missing-token';
|
|
22
|
+
hasToken: boolean;
|
|
23
|
+
port: number;
|
|
24
|
+
wsUrl: string;
|
|
25
|
+
debugRichText: boolean;
|
|
26
|
+
lastError: string | null;
|
|
27
|
+
lastErrorAt: number | null;
|
|
28
|
+
lastErrorContext: string | null;
|
|
29
|
+
reconnectAttempt: number;
|
|
30
|
+
nextReconnectInMs: number | null;
|
|
31
|
+
manualDisconnect: boolean;
|
|
32
|
+
extensionVersion: string;
|
|
33
|
+
lastBindingUpdateAt: number | null;
|
|
34
|
+
lastBindingUpdateReason: string | null;
|
|
35
|
+
sessionBindings: {
|
|
36
|
+
count: number;
|
|
37
|
+
attachedCount: number;
|
|
38
|
+
detachedCount: number;
|
|
39
|
+
tabCount: number;
|
|
40
|
+
items: Array<{
|
|
41
|
+
id: string;
|
|
42
|
+
label: string;
|
|
43
|
+
tabCount: number;
|
|
44
|
+
activeTabId: number | null;
|
|
45
|
+
windowId: number | null;
|
|
46
|
+
groupId: number | null;
|
|
47
|
+
detached: boolean;
|
|
48
|
+
}>;
|
|
49
|
+
};
|
|
50
|
+
}
|
|
51
|
+
let latestState: PopupState | null = null;
|
|
7
52
|
|
|
8
53
|
function setStatus(text: string, bad = false): void {
|
|
9
54
|
statusEl.textContent = text;
|
|
10
55
|
statusEl.style.color = bad ? '#dc2626' : '#0f172a';
|
|
11
56
|
}
|
|
12
57
|
|
|
58
|
+
function formatTimeAgo(at: number | null): string {
|
|
59
|
+
if (typeof at !== 'number') {
|
|
60
|
+
return 'never';
|
|
61
|
+
}
|
|
62
|
+
const deltaSeconds = Math.max(0, Math.round((Date.now() - at) / 1000));
|
|
63
|
+
if (deltaSeconds < 5) {
|
|
64
|
+
return 'just now';
|
|
65
|
+
}
|
|
66
|
+
if (deltaSeconds < 60) {
|
|
67
|
+
return `${deltaSeconds}s ago`;
|
|
68
|
+
}
|
|
69
|
+
const deltaMinutes = Math.round(deltaSeconds / 60);
|
|
70
|
+
if (deltaMinutes < 60) {
|
|
71
|
+
return `${deltaMinutes}m ago`;
|
|
72
|
+
}
|
|
73
|
+
const deltaHours = Math.round(deltaMinutes / 60);
|
|
74
|
+
return `${deltaHours}h ago`;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
function renderSessionBindings(state: PopupState['sessionBindings']): void {
|
|
78
|
+
sessionSummaryEl.textContent = `${state.count} sessions, ${state.attachedCount} attached, ${state.tabCount} tabs, ${state.detachedCount} detached`;
|
|
79
|
+
sessionListEl.replaceChildren();
|
|
80
|
+
for (const item of state.items) {
|
|
81
|
+
const li = document.createElement('li');
|
|
82
|
+
const location = item.windowId === null ? 'no window' : `window ${item.windowId}`;
|
|
83
|
+
const active = item.activeTabId === null ? 'no active tab' : `active ${item.activeTabId}`;
|
|
84
|
+
li.textContent = `${item.label}: ${item.tabCount} tabs, ${location}, ${active}`;
|
|
85
|
+
if (item.detached) {
|
|
86
|
+
li.style.color = '#b45309';
|
|
87
|
+
}
|
|
88
|
+
sessionListEl.appendChild(li);
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
function renderConnectionDetails(state: PopupState): void {
|
|
93
|
+
connectionStateEl.textContent = state.connectionState;
|
|
94
|
+
tokenStateEl.textContent = state.hasToken ? 'configured' : 'missing';
|
|
95
|
+
connectionUrlEl.textContent = state.wsUrl;
|
|
96
|
+
extensionVersionEl.textContent = state.extensionVersion;
|
|
97
|
+
|
|
98
|
+
if (state.manualDisconnect) {
|
|
99
|
+
reconnectStateEl.textContent = 'manual disconnect';
|
|
100
|
+
} else if (typeof state.nextReconnectInMs === 'number') {
|
|
101
|
+
const seconds = Math.max(0, Math.ceil(state.nextReconnectInMs / 100) / 10);
|
|
102
|
+
reconnectStateEl.textContent = `attempt ${state.reconnectAttempt}, retry in ${seconds}s`;
|
|
103
|
+
} else if (state.connected) {
|
|
104
|
+
reconnectStateEl.textContent = 'connected';
|
|
105
|
+
} else {
|
|
106
|
+
reconnectStateEl.textContent = 'idle';
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
if (state.lastError) {
|
|
110
|
+
const context = state.lastErrorContext ? `${state.lastErrorContext}: ` : '';
|
|
111
|
+
lastErrorEl.textContent = `${context}${state.lastError} (${formatTimeAgo(state.lastErrorAt)})`;
|
|
112
|
+
} else {
|
|
113
|
+
lastErrorEl.textContent = 'none';
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
if (state.lastBindingUpdateReason) {
|
|
117
|
+
lastBindingUpdateEl.textContent = `${state.lastBindingUpdateReason} (${formatTimeAgo(state.lastBindingUpdateAt)})`;
|
|
118
|
+
} else {
|
|
119
|
+
lastBindingUpdateEl.textContent = 'none';
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
|
|
13
123
|
async function refreshState(): Promise<void> {
|
|
14
|
-
const state = (await chrome.runtime.sendMessage({ type: 'bak.getState' })) as
|
|
15
|
-
ok: boolean;
|
|
16
|
-
connected: boolean;
|
|
17
|
-
hasToken: boolean;
|
|
18
|
-
port: number;
|
|
19
|
-
debugRichText: boolean;
|
|
20
|
-
lastError: string | null;
|
|
21
|
-
};
|
|
124
|
+
const state = (await chrome.runtime.sendMessage({ type: 'bak.getState' })) as PopupState;
|
|
22
125
|
|
|
23
126
|
if (state.ok) {
|
|
127
|
+
latestState = state;
|
|
24
128
|
portInput.value = String(state.port);
|
|
25
129
|
debugRichTextInput.checked = Boolean(state.debugRichText);
|
|
130
|
+
renderConnectionDetails(state);
|
|
131
|
+
renderSessionBindings(state.sessionBindings);
|
|
26
132
|
if (state.connected) {
|
|
27
133
|
setStatus('Connected to bak CLI');
|
|
134
|
+
} else if (state.connectionState === 'missing-token') {
|
|
135
|
+
setStatus('Pair token is required', true);
|
|
136
|
+
} else if (state.connectionState === 'manual') {
|
|
137
|
+
setStatus('Disconnected manually');
|
|
138
|
+
} else if (state.connectionState === 'reconnecting') {
|
|
139
|
+
setStatus('Reconnecting to bak CLI', true);
|
|
28
140
|
} else if (state.lastError) {
|
|
29
141
|
setStatus(`Disconnected: ${state.lastError}`, true);
|
|
30
142
|
} else {
|
|
@@ -37,7 +149,7 @@ saveBtn.addEventListener('click', async () => {
|
|
|
37
149
|
const token = tokenInput.value.trim();
|
|
38
150
|
const port = Number.parseInt(portInput.value.trim(), 10);
|
|
39
151
|
|
|
40
|
-
if (!token) {
|
|
152
|
+
if (!token && latestState?.hasToken !== true) {
|
|
41
153
|
setStatus('Pair token is required', true);
|
|
42
154
|
return;
|
|
43
155
|
}
|
|
@@ -49,11 +161,17 @@ saveBtn.addEventListener('click', async () => {
|
|
|
49
161
|
|
|
50
162
|
await chrome.runtime.sendMessage({
|
|
51
163
|
type: 'bak.updateConfig',
|
|
52
|
-
token,
|
|
164
|
+
...(token ? { token } : {}),
|
|
53
165
|
port,
|
|
54
166
|
debugRichText: debugRichTextInput.checked
|
|
55
167
|
});
|
|
56
168
|
|
|
169
|
+
tokenInput.value = '';
|
|
170
|
+
await refreshState();
|
|
171
|
+
});
|
|
172
|
+
|
|
173
|
+
reconnectBtn.addEventListener('click', async () => {
|
|
174
|
+
await chrome.runtime.sendMessage({ type: 'bak.reconnectNow' });
|
|
57
175
|
await refreshState();
|
|
58
176
|
});
|
|
59
177
|
|
|
@@ -62,4 +180,10 @@ disconnectBtn.addEventListener('click', async () => {
|
|
|
62
180
|
await refreshState();
|
|
63
181
|
});
|
|
64
182
|
|
|
65
|
-
void refreshState();
|
|
183
|
+
void refreshState();
|
|
184
|
+
const refreshInterval = window.setInterval(() => {
|
|
185
|
+
void refreshState();
|
|
186
|
+
}, 1000);
|
|
187
|
+
window.addEventListener('unload', () => {
|
|
188
|
+
window.clearInterval(refreshInterval);
|
|
189
|
+
});
|
package/src/session-binding.ts
CHANGED
|
@@ -88,18 +88,20 @@ interface SessionBindingWindowOwnership {
|
|
|
88
88
|
foreignTabs: SessionBindingTab[];
|
|
89
89
|
}
|
|
90
90
|
|
|
91
|
-
export interface SessionBindingEnsureOptions {
|
|
92
|
-
bindingId?: string;
|
|
93
|
-
focus?: boolean;
|
|
94
|
-
initialUrl?: string;
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
91
|
+
export interface SessionBindingEnsureOptions {
|
|
92
|
+
bindingId?: string;
|
|
93
|
+
focus?: boolean;
|
|
94
|
+
initialUrl?: string;
|
|
95
|
+
label?: string;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
export interface SessionBindingOpenTabOptions {
|
|
99
|
+
bindingId?: string;
|
|
100
|
+
url?: string;
|
|
101
|
+
active?: boolean;
|
|
102
|
+
focus?: boolean;
|
|
103
|
+
label?: string;
|
|
104
|
+
}
|
|
103
105
|
|
|
104
106
|
export interface SessionBindingResolveTargetOptions {
|
|
105
107
|
tabId?: number;
|
|
@@ -120,13 +122,13 @@ class SessionBindingManager {
|
|
|
120
122
|
return this.inspectBinding(bindingId);
|
|
121
123
|
}
|
|
122
124
|
|
|
123
|
-
async ensureBinding(options: SessionBindingEnsureOptions = {}): Promise<SessionBindingEnsureResult> {
|
|
124
|
-
const bindingId = this.normalizeBindingId(options.bindingId);
|
|
125
|
-
const repairActions: string[] = [];
|
|
126
|
-
const initialUrl = options.initialUrl ?? DEFAULT_SESSION_BINDING_URL;
|
|
127
|
-
const persisted = await this.storage.load(bindingId);
|
|
128
|
-
const created = !persisted;
|
|
129
|
-
let state = this.normalizeState(persisted, bindingId);
|
|
125
|
+
async ensureBinding(options: SessionBindingEnsureOptions = {}): Promise<SessionBindingEnsureResult> {
|
|
126
|
+
const bindingId = this.normalizeBindingId(options.bindingId);
|
|
127
|
+
const repairActions: string[] = [];
|
|
128
|
+
const initialUrl = options.initialUrl ?? DEFAULT_SESSION_BINDING_URL;
|
|
129
|
+
const persisted = await this.storage.load(bindingId);
|
|
130
|
+
const created = !persisted;
|
|
131
|
+
let state = this.normalizeState(persisted, bindingId, options.label);
|
|
130
132
|
|
|
131
133
|
const originalWindowId = state.windowId;
|
|
132
134
|
let window = state.windowId !== null ? await this.waitForWindow(state.windowId) : null;
|
|
@@ -266,14 +268,15 @@ class SessionBindingManager {
|
|
|
266
268
|
};
|
|
267
269
|
}
|
|
268
270
|
|
|
269
|
-
async openTab(options: SessionBindingOpenTabOptions = {}): Promise<{ binding: SessionBindingInfo; tab: SessionBindingTab }> {
|
|
270
|
-
const bindingId = this.normalizeBindingId(options.bindingId);
|
|
271
|
-
const hadBinding = (await this.loadBindingRecord(bindingId)) !== null;
|
|
272
|
-
const ensured = await this.ensureBinding({
|
|
273
|
-
bindingId,
|
|
274
|
-
focus: false,
|
|
275
|
-
initialUrl: hadBinding ? options.url ?? DEFAULT_SESSION_BINDING_URL : DEFAULT_SESSION_BINDING_URL
|
|
276
|
-
|
|
271
|
+
async openTab(options: SessionBindingOpenTabOptions = {}): Promise<{ binding: SessionBindingInfo; tab: SessionBindingTab }> {
|
|
272
|
+
const bindingId = this.normalizeBindingId(options.bindingId);
|
|
273
|
+
const hadBinding = (await this.loadBindingRecord(bindingId)) !== null;
|
|
274
|
+
const ensured = await this.ensureBinding({
|
|
275
|
+
bindingId,
|
|
276
|
+
focus: false,
|
|
277
|
+
initialUrl: hadBinding ? options.url ?? DEFAULT_SESSION_BINDING_URL : DEFAULT_SESSION_BINDING_URL,
|
|
278
|
+
label: options.label
|
|
279
|
+
});
|
|
277
280
|
let state = { ...ensured.binding, tabIds: [...ensured.binding.tabIds], tabs: [...ensured.binding.tabs] };
|
|
278
281
|
if (state.windowId !== null && state.tabs.length === 0) {
|
|
279
282
|
const rebound = await this.rebindBindingWindow(state);
|
|
@@ -309,11 +312,12 @@ class SessionBindingManager {
|
|
|
309
312
|
if (!this.isMissingWindowError(error)) {
|
|
310
313
|
throw error;
|
|
311
314
|
}
|
|
312
|
-
const repaired = await this.ensureBinding({
|
|
313
|
-
bindingId,
|
|
314
|
-
focus: false,
|
|
315
|
-
initialUrl: desiredUrl
|
|
316
|
-
|
|
315
|
+
const repaired = await this.ensureBinding({
|
|
316
|
+
bindingId,
|
|
317
|
+
focus: false,
|
|
318
|
+
initialUrl: desiredUrl,
|
|
319
|
+
label: options.label
|
|
320
|
+
});
|
|
317
321
|
state = { ...repaired.binding };
|
|
318
322
|
reusablePrimaryTab = await this.resolveReusablePrimaryTab(state, true);
|
|
319
323
|
createdTab = reusablePrimaryTab
|
|
@@ -421,44 +425,94 @@ class SessionBindingManager {
|
|
|
421
425
|
};
|
|
422
426
|
}
|
|
423
427
|
|
|
424
|
-
async focus(bindingId: string): Promise<{ ok: true; binding: SessionBindingInfo }> {
|
|
425
|
-
const ensured = await this.ensureBinding({ bindingId, focus: false });
|
|
426
|
-
if (ensured.binding.activeTabId !== null) {
|
|
427
|
-
await this.browser.updateTab(ensured.binding.activeTabId, { active: true });
|
|
428
|
-
}
|
|
428
|
+
async focus(bindingId: string): Promise<{ ok: true; binding: SessionBindingInfo }> {
|
|
429
|
+
const ensured = await this.ensureBinding({ bindingId, focus: false });
|
|
430
|
+
if (ensured.binding.activeTabId !== null) {
|
|
431
|
+
await this.browser.updateTab(ensured.binding.activeTabId, { active: true });
|
|
432
|
+
}
|
|
429
433
|
if (ensured.binding.windowId !== null) {
|
|
430
434
|
await this.browser.updateWindow(ensured.binding.windowId, { focused: true });
|
|
431
435
|
}
|
|
432
|
-
const refreshed = await this.ensureBinding({ bindingId, focus: false });
|
|
433
|
-
return { ok: true, binding: refreshed.binding };
|
|
434
|
-
}
|
|
435
|
-
|
|
436
|
-
async
|
|
437
|
-
const
|
|
438
|
-
|
|
436
|
+
const refreshed = await this.ensureBinding({ bindingId, focus: false });
|
|
437
|
+
return { ok: true, binding: refreshed.binding };
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
async closeTab(bindingId: string, tabId?: number): Promise<{ binding: SessionBindingInfo | null; closedTabId: number }> {
|
|
441
|
+
const ensured = await this.ensureBinding({ bindingId, focus: false });
|
|
442
|
+
const resolvedTabId =
|
|
443
|
+
typeof tabId === 'number'
|
|
444
|
+
? tabId
|
|
445
|
+
: ensured.binding.activeTabId ?? ensured.binding.primaryTabId ?? ensured.binding.tabs[0]?.id;
|
|
446
|
+
if (typeof resolvedTabId !== 'number' || !ensured.binding.tabIds.includes(resolvedTabId)) {
|
|
447
|
+
throw new Error(`Tab ${tabId ?? 'active'} does not belong to binding ${bindingId}`);
|
|
448
|
+
}
|
|
449
|
+
|
|
450
|
+
await this.browser.closeTab(resolvedTabId);
|
|
451
|
+
const remainingTabIds = ensured.binding.tabIds.filter((candidate) => candidate !== resolvedTabId);
|
|
452
|
+
|
|
453
|
+
if (remainingTabIds.length === 0) {
|
|
454
|
+
await this.storage.delete(ensured.binding.id);
|
|
455
|
+
return {
|
|
456
|
+
binding: null,
|
|
457
|
+
closedTabId: resolvedTabId
|
|
458
|
+
};
|
|
459
|
+
}
|
|
460
|
+
|
|
461
|
+
const tabs = await this.readLooseTrackedTabs(remainingTabIds);
|
|
462
|
+
const nextPrimaryTabId =
|
|
463
|
+
ensured.binding.primaryTabId === resolvedTabId ? tabs[0]?.id ?? null : ensured.binding.primaryTabId;
|
|
464
|
+
const nextActiveTabId =
|
|
465
|
+
ensured.binding.activeTabId === resolvedTabId
|
|
466
|
+
? tabs.find((candidate) => candidate.active)?.id ?? nextPrimaryTabId ?? tabs[0]?.id ?? null
|
|
467
|
+
: ensured.binding.activeTabId;
|
|
468
|
+
const nextState: SessionBindingRecord = {
|
|
469
|
+
id: ensured.binding.id,
|
|
470
|
+
label: ensured.binding.label,
|
|
471
|
+
color: ensured.binding.color,
|
|
472
|
+
windowId: tabs[0]?.windowId ?? ensured.binding.windowId,
|
|
473
|
+
groupId: tabs[0]?.groupId ?? ensured.binding.groupId,
|
|
474
|
+
tabIds: tabs.map((candidate) => candidate.id),
|
|
475
|
+
activeTabId: nextActiveTabId,
|
|
476
|
+
primaryTabId: nextPrimaryTabId
|
|
477
|
+
};
|
|
478
|
+
await this.storage.save(nextState);
|
|
479
|
+
return {
|
|
480
|
+
binding: {
|
|
481
|
+
...nextState,
|
|
482
|
+
tabs
|
|
483
|
+
},
|
|
484
|
+
closedTabId: resolvedTabId
|
|
485
|
+
};
|
|
486
|
+
}
|
|
487
|
+
|
|
488
|
+
async reset(options: SessionBindingEnsureOptions = {}): Promise<SessionBindingEnsureResult> {
|
|
489
|
+
const bindingId = this.normalizeBindingId(options.bindingId);
|
|
490
|
+
await this.close(bindingId);
|
|
439
491
|
return this.ensureBinding({
|
|
440
492
|
...options,
|
|
441
493
|
bindingId
|
|
442
494
|
});
|
|
443
495
|
}
|
|
444
496
|
|
|
445
|
-
async close(bindingId: string): Promise<{ ok: true }> {
|
|
446
|
-
const state = await this.loadBindingRecord(bindingId);
|
|
447
|
-
if (!state) {
|
|
448
|
-
await this.storage.delete(bindingId);
|
|
449
|
-
return { ok: true };
|
|
450
|
-
}
|
|
451
|
-
// Clear persisted state before closing the window so tab/window removal
|
|
452
|
-
// listeners cannot race and resurrect an empty binding record.
|
|
453
|
-
await this.storage.delete(bindingId);
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
await this.browser.
|
|
458
|
-
}
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
497
|
+
async close(bindingId: string): Promise<{ ok: true }> {
|
|
498
|
+
const state = await this.loadBindingRecord(bindingId);
|
|
499
|
+
if (!state) {
|
|
500
|
+
await this.storage.delete(bindingId);
|
|
501
|
+
return { ok: true };
|
|
502
|
+
}
|
|
503
|
+
// Clear persisted state before closing the window so tab/window removal
|
|
504
|
+
// listeners cannot race and resurrect an empty binding record.
|
|
505
|
+
await this.storage.delete(bindingId);
|
|
506
|
+
const trackedTabs = await this.readLooseTrackedTabs(this.collectCandidateTabIds(state));
|
|
507
|
+
for (const trackedTab of trackedTabs) {
|
|
508
|
+
try {
|
|
509
|
+
await this.browser.closeTab(trackedTab.id);
|
|
510
|
+
} catch {
|
|
511
|
+
// Ignore tabs that were already removed before explicit close.
|
|
512
|
+
}
|
|
513
|
+
}
|
|
514
|
+
return { ok: true };
|
|
515
|
+
}
|
|
462
516
|
|
|
463
517
|
async resolveTarget(options: SessionBindingResolveTargetOptions = {}): Promise<SessionBindingTargetResolution> {
|
|
464
518
|
if (typeof options.tabId === 'number') {
|
|
@@ -511,13 +565,13 @@ class SessionBindingManager {
|
|
|
511
565
|
return candidate;
|
|
512
566
|
}
|
|
513
567
|
|
|
514
|
-
private normalizeState(state: SessionBindingRecord | null, bindingId: string): SessionBindingRecord {
|
|
515
|
-
return {
|
|
516
|
-
id: bindingId,
|
|
517
|
-
label: state?.label ?? DEFAULT_SESSION_BINDING_LABEL,
|
|
518
|
-
color: state?.color ?? DEFAULT_SESSION_BINDING_COLOR,
|
|
519
|
-
windowId: state?.windowId ?? null,
|
|
520
|
-
groupId: state?.groupId ?? null,
|
|
568
|
+
private normalizeState(state: SessionBindingRecord | null, bindingId: string, label?: string): SessionBindingRecord {
|
|
569
|
+
return {
|
|
570
|
+
id: bindingId,
|
|
571
|
+
label: label?.trim() ? label.trim() : state?.label ?? DEFAULT_SESSION_BINDING_LABEL,
|
|
572
|
+
color: state?.color ?? DEFAULT_SESSION_BINDING_COLOR,
|
|
573
|
+
windowId: state?.windowId ?? null,
|
|
574
|
+
groupId: state?.groupId ?? null,
|
|
521
575
|
tabIds: state?.tabIds ?? [],
|
|
522
576
|
activeTabId: state?.activeTabId ?? null,
|
|
523
577
|
primaryTabId: state?.primaryTabId ?? null
|
|
@@ -710,12 +764,16 @@ class SessionBindingManager {
|
|
|
710
764
|
await this.browser.updateTab(nextActiveTabId, { active: true });
|
|
711
765
|
}
|
|
712
766
|
|
|
713
|
-
state.windowId = window.id;
|
|
714
|
-
state.groupId = null;
|
|
715
|
-
state.tabIds = recreatedTabs.map((tab) => tab.id);
|
|
716
|
-
state.primaryTabId = nextPrimaryTabId;
|
|
717
|
-
state.activeTabId = nextActiveTabId;
|
|
718
|
-
|
|
767
|
+
state.windowId = window.id;
|
|
768
|
+
state.groupId = null;
|
|
769
|
+
state.tabIds = recreatedTabs.map((tab) => tab.id);
|
|
770
|
+
state.primaryTabId = nextPrimaryTabId;
|
|
771
|
+
state.activeTabId = nextActiveTabId;
|
|
772
|
+
await this.storage.save({
|
|
773
|
+
...state,
|
|
774
|
+
tabIds: [...state.tabIds]
|
|
775
|
+
});
|
|
776
|
+
|
|
719
777
|
for (const bindingTab of ownership.bindingTabs) {
|
|
720
778
|
await this.browser.closeTab(bindingTab.id);
|
|
721
779
|
}
|