@flrande/bak-extension 0.3.5 → 0.3.7
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 +99 -2
- package/package.json +2 -2
- package/src/background.ts +19 -1
- package/src/workspace.ts +111 -1
|
@@ -1 +1 @@
|
|
|
1
|
-
2026-03-
|
|
1
|
+
2026-03-09T13:25:43.147Z
|
|
@@ -96,6 +96,16 @@
|
|
|
96
96
|
repairActions.push("pruned-missing-tabs");
|
|
97
97
|
}
|
|
98
98
|
state.tabIds = tabs.map((tab) => tab.id);
|
|
99
|
+
if (state.windowId !== null) {
|
|
100
|
+
const ownership = await this.inspectWorkspaceWindowOwnership(state, state.windowId);
|
|
101
|
+
if (ownership.foreignTabs.length > 0) {
|
|
102
|
+
const migrated = await this.moveWorkspaceIntoDedicatedWindow(state, ownership, initialUrl);
|
|
103
|
+
window = migrated.window;
|
|
104
|
+
tabs = migrated.tabs;
|
|
105
|
+
state.tabIds = tabs.map((tab) => tab.id);
|
|
106
|
+
repairActions.push("migrated-dirty-window");
|
|
107
|
+
}
|
|
108
|
+
}
|
|
99
109
|
if (tabs.length === 0) {
|
|
100
110
|
const primary = await this.createWorkspaceTab({
|
|
101
111
|
windowId: state.windowId,
|
|
@@ -189,7 +199,7 @@
|
|
|
189
199
|
const desiredUrl = options.url ?? DEFAULT_WORKSPACE_URL;
|
|
190
200
|
let reusablePrimaryTab = await this.resolveReusablePrimaryTab(
|
|
191
201
|
state,
|
|
192
|
-
ensured.created || ensured.repairActions.includes("recreated-window") || ensured.repairActions.includes("created-primary-tab")
|
|
202
|
+
ensured.created || ensured.repairActions.includes("recreated-window") || ensured.repairActions.includes("created-primary-tab") || ensured.repairActions.includes("migrated-dirty-window")
|
|
193
203
|
);
|
|
194
204
|
let createdTab;
|
|
195
205
|
try {
|
|
@@ -527,6 +537,79 @@
|
|
|
527
537
|
}
|
|
528
538
|
return null;
|
|
529
539
|
}
|
|
540
|
+
async inspectWorkspaceWindowOwnership(state, windowId) {
|
|
541
|
+
const windowTabs = await this.waitForWindowTabs(windowId, 500);
|
|
542
|
+
const trackedIds = new Set(this.collectCandidateTabIds(state));
|
|
543
|
+
return {
|
|
544
|
+
workspaceTabs: windowTabs.filter((tab) => trackedIds.has(tab.id) || state.groupId !== null && tab.groupId === state.groupId),
|
|
545
|
+
foreignTabs: windowTabs.filter((tab) => !trackedIds.has(tab.id) && (state.groupId === null || tab.groupId !== state.groupId))
|
|
546
|
+
};
|
|
547
|
+
}
|
|
548
|
+
async moveWorkspaceIntoDedicatedWindow(state, ownership, initialUrl) {
|
|
549
|
+
const sourceTabs = this.orderWorkspaceTabsForMigration(state, ownership.workspaceTabs);
|
|
550
|
+
const seedUrl = sourceTabs[0]?.url ?? initialUrl;
|
|
551
|
+
const window = await this.browser.createWindow({
|
|
552
|
+
url: seedUrl || DEFAULT_WORKSPACE_URL,
|
|
553
|
+
focused: false
|
|
554
|
+
});
|
|
555
|
+
const recreatedTabs = await this.waitForWindowTabs(window.id);
|
|
556
|
+
const firstTab = recreatedTabs[0] ?? null;
|
|
557
|
+
const tabIdMap = /* @__PURE__ */ new Map();
|
|
558
|
+
if (sourceTabs[0] && firstTab) {
|
|
559
|
+
tabIdMap.set(sourceTabs[0].id, firstTab.id);
|
|
560
|
+
}
|
|
561
|
+
for (const sourceTab of sourceTabs.slice(1)) {
|
|
562
|
+
const recreated = await this.createWorkspaceTab({
|
|
563
|
+
windowId: window.id,
|
|
564
|
+
url: sourceTab.url,
|
|
565
|
+
active: false
|
|
566
|
+
});
|
|
567
|
+
recreatedTabs.push(recreated);
|
|
568
|
+
tabIdMap.set(sourceTab.id, recreated.id);
|
|
569
|
+
}
|
|
570
|
+
const nextPrimaryTabId = (state.primaryTabId !== null ? tabIdMap.get(state.primaryTabId) : void 0) ?? firstTab?.id ?? recreatedTabs[0]?.id ?? null;
|
|
571
|
+
const nextActiveTabId = (state.activeTabId !== null ? tabIdMap.get(state.activeTabId) : void 0) ?? nextPrimaryTabId ?? recreatedTabs[0]?.id ?? null;
|
|
572
|
+
if (nextActiveTabId !== null) {
|
|
573
|
+
await this.browser.updateTab(nextActiveTabId, { active: true });
|
|
574
|
+
}
|
|
575
|
+
state.windowId = window.id;
|
|
576
|
+
state.groupId = null;
|
|
577
|
+
state.tabIds = recreatedTabs.map((tab) => tab.id);
|
|
578
|
+
state.primaryTabId = nextPrimaryTabId;
|
|
579
|
+
state.activeTabId = nextActiveTabId;
|
|
580
|
+
for (const workspaceTab of ownership.workspaceTabs) {
|
|
581
|
+
await this.browser.closeTab(workspaceTab.id);
|
|
582
|
+
}
|
|
583
|
+
return {
|
|
584
|
+
window,
|
|
585
|
+
tabs: await this.readTrackedTabs(state.tabIds, state.windowId)
|
|
586
|
+
};
|
|
587
|
+
}
|
|
588
|
+
orderWorkspaceTabsForMigration(state, tabs) {
|
|
589
|
+
const ordered = [];
|
|
590
|
+
const seen = /* @__PURE__ */ new Set();
|
|
591
|
+
const pushById = (tabId) => {
|
|
592
|
+
if (typeof tabId !== "number") {
|
|
593
|
+
return;
|
|
594
|
+
}
|
|
595
|
+
const tab = tabs.find((candidate) => candidate.id === tabId);
|
|
596
|
+
if (!tab || seen.has(tab.id)) {
|
|
597
|
+
return;
|
|
598
|
+
}
|
|
599
|
+
ordered.push(tab);
|
|
600
|
+
seen.add(tab.id);
|
|
601
|
+
};
|
|
602
|
+
pushById(state.primaryTabId);
|
|
603
|
+
pushById(state.activeTabId);
|
|
604
|
+
for (const tab of tabs) {
|
|
605
|
+
if (seen.has(tab.id)) {
|
|
606
|
+
continue;
|
|
607
|
+
}
|
|
608
|
+
ordered.push(tab);
|
|
609
|
+
seen.add(tab.id);
|
|
610
|
+
}
|
|
611
|
+
return ordered;
|
|
612
|
+
}
|
|
530
613
|
async recoverWorkspaceTabs(state, existingTabs) {
|
|
531
614
|
if (state.windowId === null) {
|
|
532
615
|
return existingTabs;
|
|
@@ -965,13 +1048,14 @@
|
|
|
965
1048
|
});
|
|
966
1049
|
}
|
|
967
1050
|
async function waitForTabUrl(tabId, expectedUrl, timeoutMs = 1e4) {
|
|
1051
|
+
const normalizedExpectedUrl = normalizeComparableTabUrl(expectedUrl);
|
|
968
1052
|
const deadline = Date.now() + timeoutMs;
|
|
969
1053
|
while (Date.now() < deadline) {
|
|
970
1054
|
try {
|
|
971
1055
|
const tab = await chrome.tabs.get(tabId);
|
|
972
1056
|
const currentUrl = tab.url ?? "";
|
|
973
1057
|
const pendingUrl = "pendingUrl" in tab && typeof tab.pendingUrl === "string" ? tab.pendingUrl : "";
|
|
974
|
-
if (currentUrl ===
|
|
1058
|
+
if (normalizeComparableTabUrl(currentUrl) === normalizedExpectedUrl || normalizeComparableTabUrl(pendingUrl) === normalizedExpectedUrl) {
|
|
975
1059
|
return;
|
|
976
1060
|
}
|
|
977
1061
|
} catch {
|
|
@@ -980,6 +1064,19 @@
|
|
|
980
1064
|
}
|
|
981
1065
|
throw new Error(`tab url timeout: ${tabId} -> ${expectedUrl}`);
|
|
982
1066
|
}
|
|
1067
|
+
function normalizeComparableTabUrl(url) {
|
|
1068
|
+
const raw = url.trim();
|
|
1069
|
+
if (!raw) {
|
|
1070
|
+
return raw;
|
|
1071
|
+
}
|
|
1072
|
+
try {
|
|
1073
|
+
const parsed = new URL(raw);
|
|
1074
|
+
parsed.hash = "";
|
|
1075
|
+
return parsed.href;
|
|
1076
|
+
} catch {
|
|
1077
|
+
return raw;
|
|
1078
|
+
}
|
|
1079
|
+
}
|
|
983
1080
|
async function finalizeOpenedWorkspaceTab(opened, expectedUrl) {
|
|
984
1081
|
if (expectedUrl && expectedUrl !== "about:blank") {
|
|
985
1082
|
await waitForTabUrl(opened.tab.id, expectedUrl).catch(() => void 0);
|
package/package.json
CHANGED
package/src/background.ts
CHANGED
|
@@ -372,13 +372,17 @@ async function waitForTabComplete(tabId: number, timeoutMs = DEFAULT_TAB_LOAD_TI
|
|
|
372
372
|
}
|
|
373
373
|
|
|
374
374
|
async function waitForTabUrl(tabId: number, expectedUrl: string, timeoutMs = 10_000): Promise<void> {
|
|
375
|
+
const normalizedExpectedUrl = normalizeComparableTabUrl(expectedUrl);
|
|
375
376
|
const deadline = Date.now() + timeoutMs;
|
|
376
377
|
while (Date.now() < deadline) {
|
|
377
378
|
try {
|
|
378
379
|
const tab = await chrome.tabs.get(tabId);
|
|
379
380
|
const currentUrl = tab.url ?? '';
|
|
380
381
|
const pendingUrl = 'pendingUrl' in tab && typeof tab.pendingUrl === 'string' ? tab.pendingUrl : '';
|
|
381
|
-
if (
|
|
382
|
+
if (
|
|
383
|
+
normalizeComparableTabUrl(currentUrl) === normalizedExpectedUrl ||
|
|
384
|
+
normalizeComparableTabUrl(pendingUrl) === normalizedExpectedUrl
|
|
385
|
+
) {
|
|
382
386
|
return;
|
|
383
387
|
}
|
|
384
388
|
} catch {
|
|
@@ -390,6 +394,20 @@ async function waitForTabUrl(tabId: number, expectedUrl: string, timeoutMs = 10_
|
|
|
390
394
|
throw new Error(`tab url timeout: ${tabId} -> ${expectedUrl}`);
|
|
391
395
|
}
|
|
392
396
|
|
|
397
|
+
function normalizeComparableTabUrl(url: string): string {
|
|
398
|
+
const raw = url.trim();
|
|
399
|
+
if (!raw) {
|
|
400
|
+
return raw;
|
|
401
|
+
}
|
|
402
|
+
try {
|
|
403
|
+
const parsed = new URL(raw);
|
|
404
|
+
parsed.hash = '';
|
|
405
|
+
return parsed.href;
|
|
406
|
+
} catch {
|
|
407
|
+
return raw;
|
|
408
|
+
}
|
|
409
|
+
}
|
|
410
|
+
|
|
393
411
|
async function finalizeOpenedWorkspaceTab(
|
|
394
412
|
opened: Awaited<ReturnType<WorkspaceManager['openTab']>>,
|
|
395
413
|
expectedUrl?: string
|
package/src/workspace.ts
CHANGED
|
@@ -79,6 +79,11 @@ export interface WorkspaceBrowser {
|
|
|
79
79
|
updateGroup(groupId: number, options: { title?: string; color?: WorkspaceColor; collapsed?: boolean }): Promise<WorkspaceGroup>;
|
|
80
80
|
}
|
|
81
81
|
|
|
82
|
+
interface WorkspaceWindowOwnership {
|
|
83
|
+
workspaceTabs: WorkspaceTab[];
|
|
84
|
+
foreignTabs: WorkspaceTab[];
|
|
85
|
+
}
|
|
86
|
+
|
|
82
87
|
export interface WorkspaceEnsureOptions {
|
|
83
88
|
workspaceId?: string;
|
|
84
89
|
focus?: boolean;
|
|
@@ -165,6 +170,17 @@ export class WorkspaceManager {
|
|
|
165
170
|
}
|
|
166
171
|
state.tabIds = tabs.map((tab) => tab.id);
|
|
167
172
|
|
|
173
|
+
if (state.windowId !== null) {
|
|
174
|
+
const ownership = await this.inspectWorkspaceWindowOwnership(state, state.windowId);
|
|
175
|
+
if (ownership.foreignTabs.length > 0) {
|
|
176
|
+
const migrated = await this.moveWorkspaceIntoDedicatedWindow(state, ownership, initialUrl);
|
|
177
|
+
window = migrated.window;
|
|
178
|
+
tabs = migrated.tabs;
|
|
179
|
+
state.tabIds = tabs.map((tab) => tab.id);
|
|
180
|
+
repairActions.push('migrated-dirty-window');
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
|
|
168
184
|
if (tabs.length === 0) {
|
|
169
185
|
const primary = await this.createWorkspaceTab({
|
|
170
186
|
windowId: state.windowId,
|
|
@@ -267,7 +283,10 @@ export class WorkspaceManager {
|
|
|
267
283
|
const desiredUrl = options.url ?? DEFAULT_WORKSPACE_URL;
|
|
268
284
|
let reusablePrimaryTab = await this.resolveReusablePrimaryTab(
|
|
269
285
|
state,
|
|
270
|
-
ensured.created ||
|
|
286
|
+
ensured.created ||
|
|
287
|
+
ensured.repairActions.includes('recreated-window') ||
|
|
288
|
+
ensured.repairActions.includes('created-primary-tab') ||
|
|
289
|
+
ensured.repairActions.includes('migrated-dirty-window')
|
|
271
290
|
);
|
|
272
291
|
|
|
273
292
|
let createdTab: WorkspaceTab;
|
|
@@ -647,6 +666,97 @@ export class WorkspaceManager {
|
|
|
647
666
|
return null;
|
|
648
667
|
}
|
|
649
668
|
|
|
669
|
+
private async inspectWorkspaceWindowOwnership(state: WorkspaceRecord, windowId: number): Promise<WorkspaceWindowOwnership> {
|
|
670
|
+
const windowTabs = await this.waitForWindowTabs(windowId, 500);
|
|
671
|
+
const trackedIds = new Set(this.collectCandidateTabIds(state));
|
|
672
|
+
return {
|
|
673
|
+
workspaceTabs: windowTabs.filter((tab) => trackedIds.has(tab.id) || (state.groupId !== null && tab.groupId === state.groupId)),
|
|
674
|
+
foreignTabs: windowTabs.filter((tab) => !trackedIds.has(tab.id) && (state.groupId === null || tab.groupId !== state.groupId))
|
|
675
|
+
};
|
|
676
|
+
}
|
|
677
|
+
|
|
678
|
+
private async moveWorkspaceIntoDedicatedWindow(
|
|
679
|
+
state: WorkspaceRecord,
|
|
680
|
+
ownership: WorkspaceWindowOwnership,
|
|
681
|
+
initialUrl: string
|
|
682
|
+
): Promise<{ window: WorkspaceWindow; tabs: WorkspaceTab[] }> {
|
|
683
|
+
const sourceTabs = this.orderWorkspaceTabsForMigration(state, ownership.workspaceTabs);
|
|
684
|
+
const seedUrl = sourceTabs[0]?.url ?? initialUrl;
|
|
685
|
+
const window = await this.browser.createWindow({
|
|
686
|
+
url: seedUrl || DEFAULT_WORKSPACE_URL,
|
|
687
|
+
focused: false
|
|
688
|
+
});
|
|
689
|
+
const recreatedTabs = await this.waitForWindowTabs(window.id);
|
|
690
|
+
const firstTab = recreatedTabs[0] ?? null;
|
|
691
|
+
const tabIdMap = new Map<number, number>();
|
|
692
|
+
if (sourceTabs[0] && firstTab) {
|
|
693
|
+
tabIdMap.set(sourceTabs[0].id, firstTab.id);
|
|
694
|
+
}
|
|
695
|
+
|
|
696
|
+
for (const sourceTab of sourceTabs.slice(1)) {
|
|
697
|
+
const recreated = await this.createWorkspaceTab({
|
|
698
|
+
windowId: window.id,
|
|
699
|
+
url: sourceTab.url,
|
|
700
|
+
active: false
|
|
701
|
+
});
|
|
702
|
+
recreatedTabs.push(recreated);
|
|
703
|
+
tabIdMap.set(sourceTab.id, recreated.id);
|
|
704
|
+
}
|
|
705
|
+
|
|
706
|
+
const nextPrimaryTabId =
|
|
707
|
+
(state.primaryTabId !== null ? tabIdMap.get(state.primaryTabId) : undefined) ??
|
|
708
|
+
firstTab?.id ??
|
|
709
|
+
recreatedTabs[0]?.id ??
|
|
710
|
+
null;
|
|
711
|
+
const nextActiveTabId =
|
|
712
|
+
(state.activeTabId !== null ? tabIdMap.get(state.activeTabId) : undefined) ?? nextPrimaryTabId ?? recreatedTabs[0]?.id ?? null;
|
|
713
|
+
if (nextActiveTabId !== null) {
|
|
714
|
+
await this.browser.updateTab(nextActiveTabId, { active: true });
|
|
715
|
+
}
|
|
716
|
+
|
|
717
|
+
state.windowId = window.id;
|
|
718
|
+
state.groupId = null;
|
|
719
|
+
state.tabIds = recreatedTabs.map((tab) => tab.id);
|
|
720
|
+
state.primaryTabId = nextPrimaryTabId;
|
|
721
|
+
state.activeTabId = nextActiveTabId;
|
|
722
|
+
|
|
723
|
+
for (const workspaceTab of ownership.workspaceTabs) {
|
|
724
|
+
await this.browser.closeTab(workspaceTab.id);
|
|
725
|
+
}
|
|
726
|
+
|
|
727
|
+
return {
|
|
728
|
+
window,
|
|
729
|
+
tabs: await this.readTrackedTabs(state.tabIds, state.windowId)
|
|
730
|
+
};
|
|
731
|
+
}
|
|
732
|
+
|
|
733
|
+
private orderWorkspaceTabsForMigration(state: WorkspaceRecord, tabs: WorkspaceTab[]): WorkspaceTab[] {
|
|
734
|
+
const ordered: WorkspaceTab[] = [];
|
|
735
|
+
const seen = new Set<number>();
|
|
736
|
+
const pushById = (tabId: number | null): void => {
|
|
737
|
+
if (typeof tabId !== 'number') {
|
|
738
|
+
return;
|
|
739
|
+
}
|
|
740
|
+
const tab = tabs.find((candidate) => candidate.id === tabId);
|
|
741
|
+
if (!tab || seen.has(tab.id)) {
|
|
742
|
+
return;
|
|
743
|
+
}
|
|
744
|
+
ordered.push(tab);
|
|
745
|
+
seen.add(tab.id);
|
|
746
|
+
};
|
|
747
|
+
|
|
748
|
+
pushById(state.primaryTabId);
|
|
749
|
+
pushById(state.activeTabId);
|
|
750
|
+
for (const tab of tabs) {
|
|
751
|
+
if (seen.has(tab.id)) {
|
|
752
|
+
continue;
|
|
753
|
+
}
|
|
754
|
+
ordered.push(tab);
|
|
755
|
+
seen.add(tab.id);
|
|
756
|
+
}
|
|
757
|
+
return ordered;
|
|
758
|
+
}
|
|
759
|
+
|
|
650
760
|
private async recoverWorkspaceTabs(state: WorkspaceRecord, existingTabs: WorkspaceTab[]): Promise<WorkspaceTab[]> {
|
|
651
761
|
if (state.windowId === null) {
|
|
652
762
|
return existingTabs;
|