@flrande/bak-extension 0.3.4 → 0.3.6

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-09T06:27:35.243Z
1
+ 2026-03-09T12:16:27.259Z
@@ -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;
@@ -980,6 +1063,32 @@
980
1063
  }
981
1064
  throw new Error(`tab url timeout: ${tabId} -> ${expectedUrl}`);
982
1065
  }
1066
+ async function finalizeOpenedWorkspaceTab(opened, expectedUrl) {
1067
+ if (expectedUrl && expectedUrl !== "about:blank") {
1068
+ await waitForTabUrl(opened.tab.id, expectedUrl).catch(() => void 0);
1069
+ }
1070
+ let refreshedTab = opened.tab;
1071
+ try {
1072
+ const rawTab = await chrome.tabs.get(opened.tab.id);
1073
+ const pendingUrl = "pendingUrl" in rawTab && typeof rawTab.pendingUrl === "string" ? rawTab.pendingUrl : "";
1074
+ const currentUrl = rawTab.url ?? "";
1075
+ const effectiveUrl = currentUrl && currentUrl !== "about:blank" ? currentUrl : pendingUrl && pendingUrl !== "about:blank" ? pendingUrl : currentUrl || pendingUrl || opened.tab.url;
1076
+ refreshedTab = {
1077
+ ...toTabInfo(rawTab),
1078
+ url: effectiveUrl
1079
+ };
1080
+ } catch {
1081
+ refreshedTab = await workspaceBrowser.getTab(opened.tab.id) ?? opened.tab;
1082
+ }
1083
+ const refreshedWorkspace = await workspaceManager.getWorkspaceInfo(opened.workspace.id) ?? {
1084
+ ...opened.workspace,
1085
+ tabs: opened.workspace.tabs.map((tab) => tab.id === refreshedTab.id ? refreshedTab : tab)
1086
+ };
1087
+ return {
1088
+ workspace: refreshedWorkspace,
1089
+ tab: refreshedTab
1090
+ };
1091
+ }
983
1092
  async function withTab(target = {}, options = {}) {
984
1093
  const requireSupportedAutomationUrl = options.requireSupportedAutomationUrl !== false;
985
1094
  const validate = (tab2) => {
@@ -1179,19 +1288,21 @@
1179
1288
  }
1180
1289
  case "tabs.new": {
1181
1290
  if (typeof params.workspaceId === "string" || params.windowId === void 0) {
1291
+ const expectedUrl = params.url ?? "about:blank";
1182
1292
  const opened = await preserveHumanFocus(true, async () => {
1183
1293
  return await workspaceManager.openTab({
1184
1294
  workspaceId: typeof params.workspaceId === "string" ? params.workspaceId : DEFAULT_WORKSPACE_ID,
1185
- url: params.url ?? "about:blank",
1295
+ url: expectedUrl,
1186
1296
  active: params.active === true,
1187
1297
  focus: false
1188
1298
  });
1189
1299
  });
1300
+ const stabilized = await finalizeOpenedWorkspaceTab(opened, expectedUrl);
1190
1301
  return {
1191
- tabId: opened.tab.id,
1192
- windowId: opened.tab.windowId,
1193
- groupId: opened.workspace.groupId,
1194
- workspaceId: opened.workspace.id
1302
+ tabId: stabilized.tab.id,
1303
+ windowId: stabilized.tab.windowId,
1304
+ groupId: stabilized.workspace.groupId,
1305
+ workspaceId: stabilized.workspace.id
1195
1306
  };
1196
1307
  }
1197
1308
  const tab = await chrome.tabs.create({
@@ -1232,14 +1343,16 @@
1232
1343
  };
1233
1344
  }
1234
1345
  case "workspace.openTab": {
1235
- return await preserveHumanFocus(params.focus !== true, async () => {
1346
+ const expectedUrl = typeof params.url === "string" ? params.url : void 0;
1347
+ const opened = await preserveHumanFocus(params.focus !== true, async () => {
1236
1348
  return await workspaceManager.openTab({
1237
1349
  workspaceId: typeof params.workspaceId === "string" ? params.workspaceId : void 0,
1238
- url: typeof params.url === "string" ? params.url : void 0,
1350
+ url: expectedUrl,
1239
1351
  active: params.active === true,
1240
1352
  focus: params.focus === true
1241
1353
  });
1242
1354
  });
1355
+ return await finalizeOpenedWorkspaceTab(opened, expectedUrl);
1243
1356
  }
1244
1357
  case "workspace.listTabs": {
1245
1358
  return await workspaceManager.listTabs(typeof params.workspaceId === "string" ? params.workspaceId : void 0);
package/package.json CHANGED
@@ -1,9 +1,9 @@
1
1
  {
2
2
  "name": "@flrande/bak-extension",
3
- "version": "0.3.4",
3
+ "version": "0.3.6",
4
4
  "type": "module",
5
5
  "dependencies": {
6
- "@flrande/bak-protocol": "0.3.4"
6
+ "@flrande/bak-protocol": "0.3.6"
7
7
  },
8
8
  "devDependencies": {
9
9
  "@types/chrome": "^0.1.14",
package/src/background.ts CHANGED
@@ -390,6 +390,42 @@ async function waitForTabUrl(tabId: number, expectedUrl: string, timeoutMs = 10_
390
390
  throw new Error(`tab url timeout: ${tabId} -> ${expectedUrl}`);
391
391
  }
392
392
 
393
+ async function finalizeOpenedWorkspaceTab(
394
+ opened: Awaited<ReturnType<WorkspaceManager['openTab']>>,
395
+ expectedUrl?: string
396
+ ): Promise<Awaited<ReturnType<WorkspaceManager['openTab']>>> {
397
+ if (expectedUrl && expectedUrl !== 'about:blank') {
398
+ await waitForTabUrl(opened.tab.id, expectedUrl).catch(() => undefined);
399
+ }
400
+ let refreshedTab = opened.tab;
401
+ try {
402
+ const rawTab = await chrome.tabs.get(opened.tab.id);
403
+ const pendingUrl = 'pendingUrl' in rawTab && typeof rawTab.pendingUrl === 'string' ? rawTab.pendingUrl : '';
404
+ const currentUrl = rawTab.url ?? '';
405
+ const effectiveUrl =
406
+ currentUrl && currentUrl !== 'about:blank'
407
+ ? currentUrl
408
+ : pendingUrl && pendingUrl !== 'about:blank'
409
+ ? pendingUrl
410
+ : currentUrl || pendingUrl || opened.tab.url;
411
+ refreshedTab = {
412
+ ...toTabInfo(rawTab),
413
+ url: effectiveUrl
414
+ };
415
+ } catch {
416
+ refreshedTab = (await workspaceBrowser.getTab(opened.tab.id)) ?? opened.tab;
417
+ }
418
+ const refreshedWorkspace = (await workspaceManager.getWorkspaceInfo(opened.workspace.id)) ?? {
419
+ ...opened.workspace,
420
+ tabs: opened.workspace.tabs.map((tab) => (tab.id === refreshedTab.id ? refreshedTab : tab))
421
+ };
422
+
423
+ return {
424
+ workspace: refreshedWorkspace,
425
+ tab: refreshedTab
426
+ };
427
+ }
428
+
393
429
  interface WithTabOptions {
394
430
  requireSupportedAutomationUrl?: boolean;
395
431
  }
@@ -634,19 +670,21 @@ async function handleRequest(request: CliRequest): Promise<unknown> {
634
670
  }
635
671
  case 'tabs.new': {
636
672
  if (typeof params.workspaceId === 'string' || params.windowId === undefined) {
673
+ const expectedUrl = (params.url as string | undefined) ?? 'about:blank';
637
674
  const opened = await preserveHumanFocus(true, async () => {
638
675
  return await workspaceManager.openTab({
639
676
  workspaceId: typeof params.workspaceId === 'string' ? params.workspaceId : DEFAULT_WORKSPACE_ID,
640
- url: (params.url as string | undefined) ?? 'about:blank',
677
+ url: expectedUrl,
641
678
  active: params.active === true,
642
679
  focus: false
643
680
  });
644
681
  });
682
+ const stabilized = await finalizeOpenedWorkspaceTab(opened, expectedUrl);
645
683
  return {
646
- tabId: opened.tab.id,
647
- windowId: opened.tab.windowId,
648
- groupId: opened.workspace.groupId,
649
- workspaceId: opened.workspace.id
684
+ tabId: stabilized.tab.id,
685
+ windowId: stabilized.tab.windowId,
686
+ groupId: stabilized.workspace.groupId,
687
+ workspaceId: stabilized.workspace.id
650
688
  };
651
689
  }
652
690
  const tab = await chrome.tabs.create({
@@ -687,14 +725,16 @@ async function handleRequest(request: CliRequest): Promise<unknown> {
687
725
  };
688
726
  }
689
727
  case 'workspace.openTab': {
690
- return await preserveHumanFocus(params.focus !== true, async () => {
728
+ const expectedUrl = typeof params.url === 'string' ? params.url : undefined;
729
+ const opened = await preserveHumanFocus(params.focus !== true, async () => {
691
730
  return await workspaceManager.openTab({
692
731
  workspaceId: typeof params.workspaceId === 'string' ? params.workspaceId : undefined,
693
- url: typeof params.url === 'string' ? params.url : undefined,
732
+ url: expectedUrl,
694
733
  active: params.active === true,
695
734
  focus: params.focus === true
696
735
  });
697
736
  });
737
+ return await finalizeOpenedWorkspaceTab(opened, expectedUrl);
698
738
  }
699
739
  case 'workspace.listTabs': {
700
740
  return await workspaceManager.listTabs(typeof params.workspaceId === 'string' ? params.workspaceId : undefined);
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 || ensured.repairActions.includes('recreated-window') || ensured.repairActions.includes('created-primary-tab')
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;