@flrande/bak-extension 0.3.1 → 0.3.3
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 +96 -7
- package/package.json +2 -2
- package/src/workspace.ts +106 -7
|
@@ -1 +1 @@
|
|
|
1
|
-
2026-03-
|
|
1
|
+
2026-03-09T05:59:06.644Z
|
|
@@ -52,8 +52,19 @@
|
|
|
52
52
|
const persisted = await this.storage.load();
|
|
53
53
|
const created = !persisted;
|
|
54
54
|
let state = this.normalizeState(persisted, workspaceId);
|
|
55
|
+
const originalWindowId = state.windowId;
|
|
55
56
|
let window = state.windowId !== null ? await this.waitForWindow(state.windowId) : null;
|
|
56
57
|
let tabs = [];
|
|
58
|
+
if (!window) {
|
|
59
|
+
const rebound = await this.rebindWorkspaceWindow(state);
|
|
60
|
+
if (rebound) {
|
|
61
|
+
window = rebound.window;
|
|
62
|
+
tabs = rebound.tabs;
|
|
63
|
+
if (originalWindowId !== rebound.window.id) {
|
|
64
|
+
repairActions.push("rebound-window");
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
}
|
|
57
68
|
if (!window) {
|
|
58
69
|
const createdWindow = await this.browser.createWindow({
|
|
59
70
|
url: initialUrl,
|
|
@@ -158,12 +169,22 @@
|
|
|
158
169
|
};
|
|
159
170
|
}
|
|
160
171
|
async openTab(options = {}) {
|
|
172
|
+
const workspaceId = this.normalizeWorkspaceId(options.workspaceId);
|
|
173
|
+
const hadWorkspace = await this.loadWorkspaceRecord(workspaceId) !== null;
|
|
161
174
|
const ensured = await this.ensureWorkspace({
|
|
162
|
-
workspaceId
|
|
175
|
+
workspaceId,
|
|
163
176
|
focus: false,
|
|
164
|
-
initialUrl: options.url ?? DEFAULT_WORKSPACE_URL
|
|
177
|
+
initialUrl: hadWorkspace ? options.url ?? DEFAULT_WORKSPACE_URL : DEFAULT_WORKSPACE_URL
|
|
165
178
|
});
|
|
166
|
-
let state = { ...ensured.workspace };
|
|
179
|
+
let state = { ...ensured.workspace, tabIds: [...ensured.workspace.tabIds], tabs: [...ensured.workspace.tabs] };
|
|
180
|
+
if (state.windowId !== null && state.tabs.length === 0) {
|
|
181
|
+
const rebound = await this.rebindWorkspaceWindow(state);
|
|
182
|
+
if (rebound) {
|
|
183
|
+
state.windowId = rebound.window.id;
|
|
184
|
+
state.tabs = rebound.tabs;
|
|
185
|
+
state.tabIds = [...new Set(rebound.tabs.map((tab2) => tab2.id))];
|
|
186
|
+
}
|
|
187
|
+
}
|
|
167
188
|
const active = options.active === true;
|
|
168
189
|
const desiredUrl = options.url ?? DEFAULT_WORKSPACE_URL;
|
|
169
190
|
let reusablePrimaryTab = await this.resolveReusablePrimaryTab(
|
|
@@ -185,7 +206,7 @@
|
|
|
185
206
|
throw error;
|
|
186
207
|
}
|
|
187
208
|
const repaired = await this.ensureWorkspace({
|
|
188
|
-
workspaceId
|
|
209
|
+
workspaceId,
|
|
189
210
|
focus: false,
|
|
190
211
|
initialUrl: desiredUrl
|
|
191
212
|
});
|
|
@@ -452,6 +473,60 @@
|
|
|
452
473
|
)).filter((tab) => tab !== null);
|
|
453
474
|
return tabs;
|
|
454
475
|
}
|
|
476
|
+
async readLooseTrackedTabs(tabIds) {
|
|
477
|
+
const tabs = (await Promise.all(
|
|
478
|
+
tabIds.map(async (tabId) => {
|
|
479
|
+
return await this.browser.getTab(tabId);
|
|
480
|
+
})
|
|
481
|
+
)).filter((tab) => tab !== null);
|
|
482
|
+
return tabs;
|
|
483
|
+
}
|
|
484
|
+
collectCandidateTabIds(state) {
|
|
485
|
+
return [...new Set(state.tabIds.concat([state.activeTabId, state.primaryTabId].filter((value) => typeof value === "number")))];
|
|
486
|
+
}
|
|
487
|
+
async rebindWorkspaceWindow(state) {
|
|
488
|
+
const candidateWindowIds = [];
|
|
489
|
+
const pushWindowId = (windowId) => {
|
|
490
|
+
if (typeof windowId !== "number") {
|
|
491
|
+
return;
|
|
492
|
+
}
|
|
493
|
+
if (!candidateWindowIds.includes(windowId)) {
|
|
494
|
+
candidateWindowIds.push(windowId);
|
|
495
|
+
}
|
|
496
|
+
};
|
|
497
|
+
const group = state.groupId !== null ? await this.browser.getGroup(state.groupId) : null;
|
|
498
|
+
pushWindowId(group?.windowId);
|
|
499
|
+
const trackedTabs = await this.readLooseTrackedTabs(this.collectCandidateTabIds(state));
|
|
500
|
+
for (const tab of trackedTabs) {
|
|
501
|
+
pushWindowId(tab.windowId);
|
|
502
|
+
}
|
|
503
|
+
for (const candidateWindowId of candidateWindowIds) {
|
|
504
|
+
const window = await this.waitForWindow(candidateWindowId);
|
|
505
|
+
if (!window) {
|
|
506
|
+
continue;
|
|
507
|
+
}
|
|
508
|
+
let tabs = await this.readTrackedTabs(this.collectCandidateTabIds(state), candidateWindowId);
|
|
509
|
+
if (tabs.length === 0 && group?.id !== null && group?.windowId === candidateWindowId) {
|
|
510
|
+
const windowTabs = await this.waitForWindowTabs(candidateWindowId, 750);
|
|
511
|
+
tabs = windowTabs.filter((tab) => tab.groupId === group.id);
|
|
512
|
+
}
|
|
513
|
+
if (tabs.length === 0) {
|
|
514
|
+
tabs = trackedTabs.filter((tab) => tab.windowId === candidateWindowId);
|
|
515
|
+
}
|
|
516
|
+
state.windowId = candidateWindowId;
|
|
517
|
+
if (tabs.length > 0) {
|
|
518
|
+
state.tabIds = [...new Set(tabs.map((tab) => tab.id))];
|
|
519
|
+
if (state.primaryTabId === null || !state.tabIds.includes(state.primaryTabId)) {
|
|
520
|
+
state.primaryTabId = tabs[0]?.id ?? null;
|
|
521
|
+
}
|
|
522
|
+
if (state.activeTabId === null || !state.tabIds.includes(state.activeTabId)) {
|
|
523
|
+
state.activeTabId = tabs.find((tab) => tab.active)?.id ?? state.primaryTabId;
|
|
524
|
+
}
|
|
525
|
+
}
|
|
526
|
+
return { window, tabs };
|
|
527
|
+
}
|
|
528
|
+
return null;
|
|
529
|
+
}
|
|
455
530
|
async recoverWorkspaceTabs(state, existingTabs) {
|
|
456
531
|
if (state.windowId === null) {
|
|
457
532
|
return existingTabs;
|
|
@@ -524,17 +599,31 @@
|
|
|
524
599
|
};
|
|
525
600
|
}
|
|
526
601
|
async resolveReusablePrimaryTab(workspace, allowReuse) {
|
|
527
|
-
if (
|
|
602
|
+
if (workspace.windowId === null) {
|
|
528
603
|
return null;
|
|
529
604
|
}
|
|
530
605
|
if (workspace.primaryTabId !== null) {
|
|
531
606
|
const trackedPrimary = workspace.tabs.find((tab) => tab.id === workspace.primaryTabId) ?? await this.waitForTrackedTab(workspace.primaryTabId, workspace.windowId);
|
|
532
|
-
if (trackedPrimary) {
|
|
607
|
+
if (trackedPrimary && (allowReuse || this.isReusableBlankWorkspaceTab(trackedPrimary, workspace))) {
|
|
533
608
|
return trackedPrimary;
|
|
534
609
|
}
|
|
535
610
|
}
|
|
536
611
|
const windowTabs = await this.waitForWindowTabs(workspace.windowId, 750);
|
|
537
|
-
|
|
612
|
+
if (windowTabs.length !== 1) {
|
|
613
|
+
return null;
|
|
614
|
+
}
|
|
615
|
+
const candidate = windowTabs[0];
|
|
616
|
+
if (allowReuse || this.isReusableBlankWorkspaceTab(candidate, workspace)) {
|
|
617
|
+
return candidate;
|
|
618
|
+
}
|
|
619
|
+
return null;
|
|
620
|
+
}
|
|
621
|
+
isReusableBlankWorkspaceTab(tab, workspace) {
|
|
622
|
+
if (workspace.tabIds.length > 1) {
|
|
623
|
+
return false;
|
|
624
|
+
}
|
|
625
|
+
const normalizedUrl = tab.url.trim().toLowerCase();
|
|
626
|
+
return normalizedUrl === "" || normalizedUrl === DEFAULT_WORKSPACE_URL;
|
|
538
627
|
}
|
|
539
628
|
async waitForWindow(windowId, timeoutMs = 750) {
|
|
540
629
|
const deadline = Date.now() + timeoutMs;
|
package/package.json
CHANGED
package/src/workspace.ts
CHANGED
|
@@ -119,8 +119,19 @@ export class WorkspaceManager {
|
|
|
119
119
|
const created = !persisted;
|
|
120
120
|
let state = this.normalizeState(persisted, workspaceId);
|
|
121
121
|
|
|
122
|
+
const originalWindowId = state.windowId;
|
|
122
123
|
let window = state.windowId !== null ? await this.waitForWindow(state.windowId) : null;
|
|
123
124
|
let tabs: WorkspaceTab[] = [];
|
|
125
|
+
if (!window) {
|
|
126
|
+
const rebound = await this.rebindWorkspaceWindow(state);
|
|
127
|
+
if (rebound) {
|
|
128
|
+
window = rebound.window;
|
|
129
|
+
tabs = rebound.tabs;
|
|
130
|
+
if (originalWindowId !== rebound.window.id) {
|
|
131
|
+
repairActions.push('rebound-window');
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
}
|
|
124
135
|
if (!window) {
|
|
125
136
|
const createdWindow = await this.browser.createWindow({
|
|
126
137
|
url: initialUrl,
|
|
@@ -236,12 +247,22 @@ export class WorkspaceManager {
|
|
|
236
247
|
}
|
|
237
248
|
|
|
238
249
|
async openTab(options: WorkspaceOpenTabOptions = {}): Promise<{ workspace: WorkspaceInfo; tab: WorkspaceTab }> {
|
|
250
|
+
const workspaceId = this.normalizeWorkspaceId(options.workspaceId);
|
|
251
|
+
const hadWorkspace = (await this.loadWorkspaceRecord(workspaceId)) !== null;
|
|
239
252
|
const ensured = await this.ensureWorkspace({
|
|
240
|
-
workspaceId
|
|
253
|
+
workspaceId,
|
|
241
254
|
focus: false,
|
|
242
|
-
initialUrl: options.url ?? DEFAULT_WORKSPACE_URL
|
|
255
|
+
initialUrl: hadWorkspace ? options.url ?? DEFAULT_WORKSPACE_URL : DEFAULT_WORKSPACE_URL
|
|
243
256
|
});
|
|
244
|
-
let state = { ...ensured.workspace };
|
|
257
|
+
let state = { ...ensured.workspace, tabIds: [...ensured.workspace.tabIds], tabs: [...ensured.workspace.tabs] };
|
|
258
|
+
if (state.windowId !== null && state.tabs.length === 0) {
|
|
259
|
+
const rebound = await this.rebindWorkspaceWindow(state);
|
|
260
|
+
if (rebound) {
|
|
261
|
+
state.windowId = rebound.window.id;
|
|
262
|
+
state.tabs = rebound.tabs;
|
|
263
|
+
state.tabIds = [...new Set(rebound.tabs.map((tab) => tab.id))];
|
|
264
|
+
}
|
|
265
|
+
}
|
|
245
266
|
const active = options.active === true;
|
|
246
267
|
const desiredUrl = options.url ?? DEFAULT_WORKSPACE_URL;
|
|
247
268
|
let reusablePrimaryTab = await this.resolveReusablePrimaryTab(
|
|
@@ -266,7 +287,7 @@ export class WorkspaceManager {
|
|
|
266
287
|
throw error;
|
|
267
288
|
}
|
|
268
289
|
const repaired = await this.ensureWorkspace({
|
|
269
|
-
workspaceId
|
|
290
|
+
workspaceId,
|
|
270
291
|
focus: false,
|
|
271
292
|
initialUrl: desiredUrl
|
|
272
293
|
});
|
|
@@ -561,6 +582,69 @@ export class WorkspaceManager {
|
|
|
561
582
|
return tabs;
|
|
562
583
|
}
|
|
563
584
|
|
|
585
|
+
private async readLooseTrackedTabs(tabIds: number[]): Promise<WorkspaceTab[]> {
|
|
586
|
+
const tabs = (
|
|
587
|
+
await Promise.all(
|
|
588
|
+
tabIds.map(async (tabId) => {
|
|
589
|
+
return await this.browser.getTab(tabId);
|
|
590
|
+
})
|
|
591
|
+
)
|
|
592
|
+
).filter((tab): tab is WorkspaceTab => tab !== null);
|
|
593
|
+
return tabs;
|
|
594
|
+
}
|
|
595
|
+
|
|
596
|
+
private collectCandidateTabIds(state: WorkspaceRecord): number[] {
|
|
597
|
+
return [...new Set(state.tabIds.concat([state.activeTabId, state.primaryTabId].filter((value): value is number => typeof value === 'number')))];
|
|
598
|
+
}
|
|
599
|
+
|
|
600
|
+
private async rebindWorkspaceWindow(state: WorkspaceRecord): Promise<{ window: WorkspaceWindow; tabs: WorkspaceTab[] } | null> {
|
|
601
|
+
const candidateWindowIds: number[] = [];
|
|
602
|
+
const pushWindowId = (windowId: number | null | undefined): void => {
|
|
603
|
+
if (typeof windowId !== 'number') {
|
|
604
|
+
return;
|
|
605
|
+
}
|
|
606
|
+
if (!candidateWindowIds.includes(windowId)) {
|
|
607
|
+
candidateWindowIds.push(windowId);
|
|
608
|
+
}
|
|
609
|
+
};
|
|
610
|
+
|
|
611
|
+
const group = state.groupId !== null ? await this.browser.getGroup(state.groupId) : null;
|
|
612
|
+
pushWindowId(group?.windowId);
|
|
613
|
+
|
|
614
|
+
const trackedTabs = await this.readLooseTrackedTabs(this.collectCandidateTabIds(state));
|
|
615
|
+
for (const tab of trackedTabs) {
|
|
616
|
+
pushWindowId(tab.windowId);
|
|
617
|
+
}
|
|
618
|
+
|
|
619
|
+
for (const candidateWindowId of candidateWindowIds) {
|
|
620
|
+
const window = await this.waitForWindow(candidateWindowId);
|
|
621
|
+
if (!window) {
|
|
622
|
+
continue;
|
|
623
|
+
}
|
|
624
|
+
let tabs = await this.readTrackedTabs(this.collectCandidateTabIds(state), candidateWindowId);
|
|
625
|
+
if (tabs.length === 0 && group?.id !== null && group?.windowId === candidateWindowId) {
|
|
626
|
+
const windowTabs = await this.waitForWindowTabs(candidateWindowId, 750);
|
|
627
|
+
tabs = windowTabs.filter((tab) => tab.groupId === group.id);
|
|
628
|
+
}
|
|
629
|
+
if (tabs.length === 0) {
|
|
630
|
+
tabs = trackedTabs.filter((tab) => tab.windowId === candidateWindowId);
|
|
631
|
+
}
|
|
632
|
+
state.windowId = candidateWindowId;
|
|
633
|
+
if (tabs.length > 0) {
|
|
634
|
+
state.tabIds = [...new Set(tabs.map((tab) => tab.id))];
|
|
635
|
+
if (state.primaryTabId === null || !state.tabIds.includes(state.primaryTabId)) {
|
|
636
|
+
state.primaryTabId = tabs[0]?.id ?? null;
|
|
637
|
+
}
|
|
638
|
+
if (state.activeTabId === null || !state.tabIds.includes(state.activeTabId)) {
|
|
639
|
+
state.activeTabId = tabs.find((tab) => tab.active)?.id ?? state.primaryTabId;
|
|
640
|
+
}
|
|
641
|
+
}
|
|
642
|
+
return { window, tabs };
|
|
643
|
+
}
|
|
644
|
+
|
|
645
|
+
return null;
|
|
646
|
+
}
|
|
647
|
+
|
|
564
648
|
private async recoverWorkspaceTabs(state: WorkspaceRecord, existingTabs: WorkspaceTab[]): Promise<WorkspaceTab[]> {
|
|
565
649
|
if (state.windowId === null) {
|
|
566
650
|
return existingTabs;
|
|
@@ -646,17 +730,32 @@ export class WorkspaceManager {
|
|
|
646
730
|
}
|
|
647
731
|
|
|
648
732
|
private async resolveReusablePrimaryTab(workspace: WorkspaceInfo, allowReuse: boolean): Promise<WorkspaceTab | null> {
|
|
649
|
-
if (
|
|
733
|
+
if (workspace.windowId === null) {
|
|
650
734
|
return null;
|
|
651
735
|
}
|
|
652
736
|
if (workspace.primaryTabId !== null) {
|
|
653
737
|
const trackedPrimary = workspace.tabs.find((tab) => tab.id === workspace.primaryTabId) ?? (await this.waitForTrackedTab(workspace.primaryTabId, workspace.windowId));
|
|
654
|
-
if (trackedPrimary) {
|
|
738
|
+
if (trackedPrimary && (allowReuse || this.isReusableBlankWorkspaceTab(trackedPrimary, workspace))) {
|
|
655
739
|
return trackedPrimary;
|
|
656
740
|
}
|
|
657
741
|
}
|
|
658
742
|
const windowTabs = await this.waitForWindowTabs(workspace.windowId, 750);
|
|
659
|
-
|
|
743
|
+
if (windowTabs.length !== 1) {
|
|
744
|
+
return null;
|
|
745
|
+
}
|
|
746
|
+
const candidate = windowTabs[0]!;
|
|
747
|
+
if (allowReuse || this.isReusableBlankWorkspaceTab(candidate, workspace)) {
|
|
748
|
+
return candidate;
|
|
749
|
+
}
|
|
750
|
+
return null;
|
|
751
|
+
}
|
|
752
|
+
|
|
753
|
+
private isReusableBlankWorkspaceTab(tab: WorkspaceTab, workspace: WorkspaceInfo): boolean {
|
|
754
|
+
if (workspace.tabIds.length > 1) {
|
|
755
|
+
return false;
|
|
756
|
+
}
|
|
757
|
+
const normalizedUrl = tab.url.trim().toLowerCase();
|
|
758
|
+
return normalizedUrl === '' || normalizedUrl === DEFAULT_WORKSPACE_URL;
|
|
660
759
|
}
|
|
661
760
|
|
|
662
761
|
private async waitForWindow(windowId: number, timeoutMs = 750): Promise<WorkspaceWindow | null> {
|