@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.
@@ -1 +1 @@
1
- 2026-03-09T04:26:20.181Z
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: options.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: options.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 (!allowReuse || workspace.windowId === null) {
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
- return windowTabs.length === 1 ? windowTabs[0] : null;
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
@@ -1,9 +1,9 @@
1
1
  {
2
2
  "name": "@flrande/bak-extension",
3
- "version": "0.3.1",
3
+ "version": "0.3.3",
4
4
  "type": "module",
5
5
  "dependencies": {
6
- "@flrande/bak-protocol": "0.3.1"
6
+ "@flrande/bak-protocol": "0.3.3"
7
7
  },
8
8
  "devDependencies": {
9
9
  "@types/chrome": "^0.1.14",
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: options.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: options.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 (!allowReuse || workspace.windowId === null) {
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
- return windowTabs.length === 1 ? windowTabs[0]! : null;
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> {