@flrande/bak-extension 0.6.0 → 0.6.2

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.
@@ -389,7 +389,7 @@
389
389
  version: "1.2",
390
390
  creator: {
391
391
  name: "bak",
392
- version: "0.6.0"
392
+ version: "0.6.1"
393
393
  },
394
394
  entries: entries.map((entry) => ({
395
395
  startedDateTime: new Date(entry.startedAt ?? entry.ts).toISOString(),
@@ -465,22 +465,20 @@
465
465
 
466
466
  // src/session-binding-storage.ts
467
467
  var STORAGE_KEY_SESSION_BINDINGS = "sessionBindings";
468
- var LEGACY_STORAGE_KEY_WORKSPACES = "agentWorkspaces";
469
- var LEGACY_STORAGE_KEY_WORKSPACE = "agentWorkspace";
470
- function isWorkspaceRecord(value) {
468
+ function isSessionBindingRecord(value) {
471
469
  if (typeof value !== "object" || value === null) {
472
470
  return false;
473
471
  }
474
472
  const candidate = value;
475
473
  return typeof candidate.id === "string" && Array.isArray(candidate.tabIds) && (typeof candidate.windowId === "number" || candidate.windowId === null) && (typeof candidate.groupId === "number" || candidate.groupId === null) && (typeof candidate.activeTabId === "number" || candidate.activeTabId === null) && (typeof candidate.primaryTabId === "number" || candidate.primaryTabId === null);
476
474
  }
477
- function cloneWorkspaceRecord(state) {
475
+ function cloneSessionBindingRecord(state) {
478
476
  return {
479
477
  ...state,
480
478
  tabIds: [...state.tabIds]
481
479
  };
482
480
  }
483
- function normalizeWorkspaceRecordMap(value) {
481
+ function normalizeSessionBindingRecordMap(value) {
484
482
  if (typeof value !== "object" || value === null || Array.isArray(value)) {
485
483
  return {
486
484
  found: false,
@@ -488,11 +486,11 @@
488
486
  };
489
487
  }
490
488
  const normalizedEntries = [];
491
- for (const [workspaceId, entry] of Object.entries(value)) {
492
- if (!isWorkspaceRecord(entry)) {
489
+ for (const [bindingId, entry] of Object.entries(value)) {
490
+ if (!isSessionBindingRecord(entry)) {
493
491
  continue;
494
492
  }
495
- normalizedEntries.push([workspaceId, cloneWorkspaceRecord(entry)]);
493
+ normalizedEntries.push([bindingId, cloneSessionBindingRecord(entry)]);
496
494
  }
497
495
  return {
498
496
  found: true,
@@ -500,27 +498,17 @@
500
498
  };
501
499
  }
502
500
  function resolveSessionBindingStateMap(stored) {
503
- const current = normalizeWorkspaceRecordMap(stored[STORAGE_KEY_SESSION_BINDINGS]);
504
- if (current.found) {
505
- return current.map;
506
- }
507
- const legacyMap = normalizeWorkspaceRecordMap(stored[LEGACY_STORAGE_KEY_WORKSPACES]);
508
- if (legacyMap.found) {
509
- return legacyMap.map;
510
- }
511
- const legacySingle = stored[LEGACY_STORAGE_KEY_WORKSPACE];
512
- if (isWorkspaceRecord(legacySingle)) {
513
- return {
514
- [legacySingle.id]: cloneWorkspaceRecord(legacySingle)
515
- };
516
- }
517
- return {};
501
+ const current = normalizeSessionBindingRecordMap(stored[STORAGE_KEY_SESSION_BINDINGS]);
502
+ return current.found ? current.map : {};
518
503
  }
519
504
 
520
- // src/workspace.ts
521
- var DEFAULT_WORKSPACE_LABEL = "bak agent";
522
- var DEFAULT_WORKSPACE_COLOR = "blue";
523
- var DEFAULT_WORKSPACE_URL = "about:blank";
505
+ // src/session-binding.ts
506
+ var DEFAULT_SESSION_BINDING_LABEL = "bak agent";
507
+ var DEFAULT_SESSION_BINDING_COLOR = "blue";
508
+ var DEFAULT_SESSION_BINDING_URL = "about:blank";
509
+ var WINDOW_LOOKUP_TIMEOUT_MS = 1500;
510
+ var GROUP_LOOKUP_TIMEOUT_MS = 1e3;
511
+ var WINDOW_TABS_LOOKUP_TIMEOUT_MS = 1500;
524
512
  var SessionBindingManager = class {
525
513
  storage;
526
514
  browser;
@@ -528,21 +516,21 @@
528
516
  this.storage = storage;
529
517
  this.browser = browser;
530
518
  }
531
- async getWorkspaceInfo(workspaceId) {
532
- return this.inspectWorkspace(workspaceId);
519
+ async getBindingInfo(bindingId) {
520
+ return this.inspectBinding(bindingId);
533
521
  }
534
- async ensureWorkspace(options = {}) {
535
- const workspaceId = this.normalizeWorkspaceId(options.workspaceId);
522
+ async ensureBinding(options = {}) {
523
+ const bindingId = this.normalizeBindingId(options.bindingId);
536
524
  const repairActions = [];
537
- const initialUrl = options.initialUrl ?? DEFAULT_WORKSPACE_URL;
538
- const persisted = await this.storage.load(workspaceId);
525
+ const initialUrl = options.initialUrl ?? DEFAULT_SESSION_BINDING_URL;
526
+ const persisted = await this.storage.load(bindingId);
539
527
  const created = !persisted;
540
- let state = this.normalizeState(persisted, workspaceId);
528
+ let state = this.normalizeState(persisted, bindingId);
541
529
  const originalWindowId = state.windowId;
542
530
  let window2 = state.windowId !== null ? await this.waitForWindow(state.windowId) : null;
543
531
  let tabs = [];
544
532
  if (!window2) {
545
- const rebound = await this.rebindWorkspaceWindow(state);
533
+ const rebound = await this.rebindBindingWindow(state);
546
534
  if (rebound) {
547
535
  window2 = rebound.window;
548
536
  tabs = rebound.tabs;
@@ -573,7 +561,7 @@
573
561
  repairActions.push(created ? "created-window" : "recreated-window");
574
562
  }
575
563
  tabs = tabs.length > 0 ? tabs : await this.readTrackedTabs(state.tabIds, state.windowId);
576
- const recoveredTabs = await this.recoverWorkspaceTabs(state, tabs);
564
+ const recoveredTabs = await this.recoverBindingTabs(state, tabs);
577
565
  if (recoveredTabs.length > tabs.length) {
578
566
  tabs = recoveredTabs;
579
567
  repairActions.push("recovered-tracked-tabs");
@@ -583,9 +571,9 @@
583
571
  }
584
572
  state.tabIds = tabs.map((tab) => tab.id);
585
573
  if (state.windowId !== null) {
586
- const ownership = await this.inspectWorkspaceWindowOwnership(state, state.windowId);
574
+ const ownership = await this.inspectBindingWindowOwnership(state, state.windowId);
587
575
  if (ownership.foreignTabs.length > 0) {
588
- const migrated = await this.moveWorkspaceIntoDedicatedWindow(state, ownership, initialUrl);
576
+ const migrated = await this.moveBindingIntoDedicatedWindow(state, ownership, initialUrl);
589
577
  window2 = migrated.window;
590
578
  tabs = migrated.tabs;
591
579
  state.tabIds = tabs.map((tab) => tab.id);
@@ -593,7 +581,7 @@
593
581
  }
594
582
  }
595
583
  if (tabs.length === 0) {
596
- const primary = await this.createWorkspaceTab({
584
+ const primary = await this.createBindingTab({
597
585
  windowId: state.windowId,
598
586
  url: initialUrl,
599
587
  active: true
@@ -612,7 +600,7 @@
612
600
  state.activeTabId = state.primaryTabId ?? tabs[0]?.id ?? null;
613
601
  repairActions.push("reassigned-active-tab");
614
602
  }
615
- let group = state.groupId !== null ? await this.browser.getGroup(state.groupId) : null;
603
+ let group = state.groupId !== null ? await this.waitForGroup(state.groupId) : null;
616
604
  if (!group || group.windowId !== state.windowId) {
617
605
  const groupId = await this.browser.groupTabs(tabs.map((tab) => tab.id));
618
606
  group = await this.browser.updateGroup(groupId, {
@@ -635,7 +623,7 @@
635
623
  repairActions.push("regrouped-tabs");
636
624
  }
637
625
  tabs = await this.readTrackedTabs(state.tabIds, state.windowId);
638
- tabs = await this.recoverWorkspaceTabs(state, tabs);
626
+ tabs = await this.recoverBindingTabs(state, tabs);
639
627
  const activeTab = state.activeTabId !== null ? await this.waitForTrackedTab(state.activeTabId, state.windowId) : null;
640
628
  if (activeTab && !tabs.some((tab) => tab.id === activeTab.id)) {
641
629
  tabs = [...tabs, activeTab];
@@ -655,7 +643,7 @@
655
643
  }
656
644
  await this.storage.save(state);
657
645
  return {
658
- workspace: {
646
+ binding: {
659
647
  ...state,
660
648
  tabs
661
649
  },
@@ -665,16 +653,16 @@
665
653
  };
666
654
  }
667
655
  async openTab(options = {}) {
668
- const workspaceId = this.normalizeWorkspaceId(options.workspaceId);
669
- const hadWorkspace = await this.loadWorkspaceRecord(workspaceId) !== null;
670
- const ensured = await this.ensureWorkspace({
671
- workspaceId,
656
+ const bindingId = this.normalizeBindingId(options.bindingId);
657
+ const hadBinding = await this.loadBindingRecord(bindingId) !== null;
658
+ const ensured = await this.ensureBinding({
659
+ bindingId,
672
660
  focus: false,
673
- initialUrl: hadWorkspace ? options.url ?? DEFAULT_WORKSPACE_URL : DEFAULT_WORKSPACE_URL
661
+ initialUrl: hadBinding ? options.url ?? DEFAULT_SESSION_BINDING_URL : DEFAULT_SESSION_BINDING_URL
674
662
  });
675
- let state = { ...ensured.workspace, tabIds: [...ensured.workspace.tabIds], tabs: [...ensured.workspace.tabs] };
663
+ let state = { ...ensured.binding, tabIds: [...ensured.binding.tabIds], tabs: [...ensured.binding.tabs] };
676
664
  if (state.windowId !== null && state.tabs.length === 0) {
677
- const rebound = await this.rebindWorkspaceWindow(state);
665
+ const rebound = await this.rebindBindingWindow(state);
678
666
  if (rebound) {
679
667
  state.windowId = rebound.window.id;
680
668
  state.tabs = rebound.tabs;
@@ -682,7 +670,7 @@
682
670
  }
683
671
  }
684
672
  const active = options.active === true;
685
- const desiredUrl = options.url ?? DEFAULT_WORKSPACE_URL;
673
+ const desiredUrl = options.url ?? DEFAULT_SESSION_BINDING_URL;
686
674
  let reusablePrimaryTab = await this.resolveReusablePrimaryTab(
687
675
  state,
688
676
  ensured.created || ensured.repairActions.includes("recreated-window") || ensured.repairActions.includes("created-primary-tab") || ensured.repairActions.includes("migrated-dirty-window")
@@ -692,7 +680,7 @@
692
680
  createdTab = reusablePrimaryTab ? await this.browser.updateTab(reusablePrimaryTab.id, {
693
681
  url: desiredUrl,
694
682
  active
695
- }) : await this.createWorkspaceTab({
683
+ }) : await this.createBindingTab({
696
684
  windowId: state.windowId,
697
685
  url: desiredUrl,
698
686
  active
@@ -701,17 +689,17 @@
701
689
  if (!this.isMissingWindowError(error)) {
702
690
  throw error;
703
691
  }
704
- const repaired = await this.ensureWorkspace({
705
- workspaceId,
692
+ const repaired = await this.ensureBinding({
693
+ bindingId,
706
694
  focus: false,
707
695
  initialUrl: desiredUrl
708
696
  });
709
- state = { ...repaired.workspace };
697
+ state = { ...repaired.binding };
710
698
  reusablePrimaryTab = await this.resolveReusablePrimaryTab(state, true);
711
699
  createdTab = reusablePrimaryTab ? await this.browser.updateTab(reusablePrimaryTab.id, {
712
700
  url: desiredUrl,
713
701
  active
714
- }) : await this.createWorkspaceTab({
702
+ }) : await this.createBindingTab({
715
703
  windowId: state.windowId,
716
704
  url: desiredUrl,
717
705
  active
@@ -731,7 +719,7 @@
731
719
  windowId: state.windowId,
732
720
  groupId,
733
721
  tabIds: nextTabIds,
734
- activeTabId: createdTab.id,
722
+ activeTabId: active || options.focus === true ? createdTab.id : state.activeTabId ?? state.primaryTabId ?? createdTab.id,
735
723
  primaryTabId: state.primaryTabId ?? createdTab.id
736
724
  };
737
725
  if (options.focus === true) {
@@ -742,95 +730,95 @@
742
730
  const tabs = await this.readTrackedTabs(nextState.tabIds, nextState.windowId);
743
731
  const tab = tabs.find((item) => item.id === createdTab.id) ?? createdTab;
744
732
  return {
745
- workspace: {
733
+ binding: {
746
734
  ...nextState,
747
735
  tabs
748
736
  },
749
737
  tab
750
738
  };
751
739
  }
752
- async listTabs(workspaceId) {
753
- const ensured = await this.inspectWorkspace(workspaceId);
740
+ async listTabs(bindingId) {
741
+ const ensured = await this.inspectBinding(bindingId);
754
742
  if (!ensured) {
755
- throw new Error(`Workspace ${workspaceId} does not exist`);
743
+ throw new Error(`Binding ${bindingId} does not exist`);
756
744
  }
757
745
  return {
758
- workspace: ensured,
746
+ binding: ensured,
759
747
  tabs: ensured.tabs
760
748
  };
761
749
  }
762
- async getActiveTab(workspaceId) {
763
- const ensured = await this.inspectWorkspace(workspaceId);
750
+ async getActiveTab(bindingId) {
751
+ const ensured = await this.inspectBinding(bindingId);
764
752
  if (!ensured) {
765
- const normalizedWorkspaceId = this.normalizeWorkspaceId(workspaceId);
753
+ const normalizedBindingId = this.normalizeBindingId(bindingId);
766
754
  return {
767
- workspace: {
768
- ...this.normalizeState(null, normalizedWorkspaceId),
755
+ binding: {
756
+ ...this.normalizeState(null, normalizedBindingId),
769
757
  tabs: []
770
758
  },
771
759
  tab: null
772
760
  };
773
761
  }
774
762
  return {
775
- workspace: ensured,
763
+ binding: ensured,
776
764
  tab: ensured.tabs.find((tab) => tab.id === ensured.activeTabId) ?? null
777
765
  };
778
766
  }
779
- async setActiveTab(tabId, workspaceId) {
780
- const ensured = await this.ensureWorkspace({ workspaceId });
781
- if (!ensured.workspace.tabIds.includes(tabId)) {
782
- throw new Error(`Tab ${tabId} does not belong to workspace ${workspaceId}`);
767
+ async setActiveTab(tabId, bindingId) {
768
+ const ensured = await this.ensureBinding({ bindingId });
769
+ if (!ensured.binding.tabIds.includes(tabId)) {
770
+ throw new Error(`Tab ${tabId} does not belong to binding ${bindingId}`);
783
771
  }
784
772
  const nextState = {
785
- id: ensured.workspace.id,
786
- label: ensured.workspace.label,
787
- color: ensured.workspace.color,
788
- windowId: ensured.workspace.windowId,
789
- groupId: ensured.workspace.groupId,
790
- tabIds: [...ensured.workspace.tabIds],
773
+ id: ensured.binding.id,
774
+ label: ensured.binding.label,
775
+ color: ensured.binding.color,
776
+ windowId: ensured.binding.windowId,
777
+ groupId: ensured.binding.groupId,
778
+ tabIds: [...ensured.binding.tabIds],
791
779
  activeTabId: tabId,
792
- primaryTabId: ensured.workspace.primaryTabId ?? tabId
780
+ primaryTabId: ensured.binding.primaryTabId ?? tabId
793
781
  };
794
782
  await this.storage.save(nextState);
795
783
  const tabs = await this.readTrackedTabs(nextState.tabIds, nextState.windowId);
796
784
  const tab = tabs.find((item) => item.id === tabId);
797
785
  if (!tab) {
798
- throw new Error(`Tab ${tabId} is missing from workspace ${workspaceId}`);
786
+ throw new Error(`Tab ${tabId} is missing from binding ${bindingId}`);
799
787
  }
800
788
  return {
801
- workspace: {
789
+ binding: {
802
790
  ...nextState,
803
791
  tabs
804
792
  },
805
793
  tab
806
794
  };
807
795
  }
808
- async focus(workspaceId) {
809
- const ensured = await this.ensureWorkspace({ workspaceId, focus: false });
810
- if (ensured.workspace.activeTabId !== null) {
811
- await this.browser.updateTab(ensured.workspace.activeTabId, { active: true });
796
+ async focus(bindingId) {
797
+ const ensured = await this.ensureBinding({ bindingId, focus: false });
798
+ if (ensured.binding.activeTabId !== null) {
799
+ await this.browser.updateTab(ensured.binding.activeTabId, { active: true });
812
800
  }
813
- if (ensured.workspace.windowId !== null) {
814
- await this.browser.updateWindow(ensured.workspace.windowId, { focused: true });
801
+ if (ensured.binding.windowId !== null) {
802
+ await this.browser.updateWindow(ensured.binding.windowId, { focused: true });
815
803
  }
816
- const refreshed = await this.ensureWorkspace({ workspaceId, focus: false });
817
- return { ok: true, workspace: refreshed.workspace };
804
+ const refreshed = await this.ensureBinding({ bindingId, focus: false });
805
+ return { ok: true, binding: refreshed.binding };
818
806
  }
819
807
  async reset(options = {}) {
820
- const workspaceId = this.normalizeWorkspaceId(options.workspaceId);
821
- await this.close(workspaceId);
822
- return this.ensureWorkspace({
808
+ const bindingId = this.normalizeBindingId(options.bindingId);
809
+ await this.close(bindingId);
810
+ return this.ensureBinding({
823
811
  ...options,
824
- workspaceId
812
+ bindingId
825
813
  });
826
814
  }
827
- async close(workspaceId) {
828
- const state = await this.loadWorkspaceRecord(workspaceId);
815
+ async close(bindingId) {
816
+ const state = await this.loadBindingRecord(bindingId);
829
817
  if (!state) {
830
- await this.storage.delete(workspaceId);
818
+ await this.storage.delete(bindingId);
831
819
  return { ok: true };
832
820
  }
833
- await this.storage.delete(workspaceId);
821
+ await this.storage.delete(bindingId);
834
822
  if (state.windowId !== null) {
835
823
  const existingWindow = await this.browser.getWindow(state.windowId);
836
824
  if (existingWindow) {
@@ -847,20 +835,20 @@
847
835
  }
848
836
  return {
849
837
  tab: explicitTab,
850
- workspace: null,
838
+ binding: null,
851
839
  resolution: "explicit-tab",
852
- createdWorkspace: false,
840
+ createdBinding: false,
853
841
  repaired: false,
854
842
  repairActions: []
855
843
  };
856
844
  }
857
- const explicitWorkspaceId = typeof options.workspaceId === "string" ? this.normalizeWorkspaceId(options.workspaceId) : void 0;
858
- if (explicitWorkspaceId) {
859
- const ensured = await this.ensureWorkspace({
860
- workspaceId: explicitWorkspaceId,
845
+ const explicitBindingId = typeof options.bindingId === "string" ? this.normalizeBindingId(options.bindingId) : void 0;
846
+ if (explicitBindingId) {
847
+ const ensured = await this.ensureBinding({
848
+ bindingId: explicitBindingId,
861
849
  focus: false
862
850
  });
863
- return this.buildWorkspaceResolution(ensured, "explicit-workspace");
851
+ return this.buildBindingResolution(ensured, "explicit-binding");
864
852
  }
865
853
  if (options.createIfMissing !== true) {
866
854
  const activeTab = await this.browser.getActiveTab();
@@ -869,27 +857,27 @@
869
857
  }
870
858
  return {
871
859
  tab: activeTab,
872
- workspace: null,
860
+ binding: null,
873
861
  resolution: "browser-active",
874
- createdWorkspace: false,
862
+ createdBinding: false,
875
863
  repaired: false,
876
864
  repairActions: []
877
865
  };
878
866
  }
879
- throw new Error("workspaceId is required when createIfMissing is true");
867
+ throw new Error("bindingId is required when createIfMissing is true");
880
868
  }
881
- normalizeWorkspaceId(workspaceId) {
882
- const candidate = workspaceId?.trim();
869
+ normalizeBindingId(bindingId) {
870
+ const candidate = bindingId?.trim();
883
871
  if (!candidate) {
884
- throw new Error("workspaceId is required");
872
+ throw new Error("bindingId is required");
885
873
  }
886
874
  return candidate;
887
875
  }
888
- normalizeState(state, workspaceId) {
876
+ normalizeState(state, bindingId) {
889
877
  return {
890
- id: workspaceId,
891
- label: state?.label ?? DEFAULT_WORKSPACE_LABEL,
892
- color: state?.color ?? DEFAULT_WORKSPACE_COLOR,
878
+ id: bindingId,
879
+ label: state?.label ?? DEFAULT_SESSION_BINDING_LABEL,
880
+ color: state?.color ?? DEFAULT_SESSION_BINDING_COLOR,
893
881
  windowId: state?.windowId ?? null,
894
882
  groupId: state?.groupId ?? null,
895
883
  tabIds: state?.tabIds ?? [],
@@ -897,37 +885,37 @@
897
885
  primaryTabId: state?.primaryTabId ?? null
898
886
  };
899
887
  }
900
- async listWorkspaceRecords() {
888
+ async listBindingRecords() {
901
889
  return await this.storage.list();
902
890
  }
903
- async loadWorkspaceRecord(workspaceId) {
904
- const normalizedWorkspaceId = this.normalizeWorkspaceId(workspaceId);
905
- const state = await this.storage.load(normalizedWorkspaceId);
906
- if (!state || state.id !== normalizedWorkspaceId) {
891
+ async loadBindingRecord(bindingId) {
892
+ const normalizedBindingId = this.normalizeBindingId(bindingId);
893
+ const state = await this.storage.load(normalizedBindingId);
894
+ if (!state || state.id !== normalizedBindingId) {
907
895
  return null;
908
896
  }
909
- return this.normalizeState(state, normalizedWorkspaceId);
897
+ return this.normalizeState(state, normalizedBindingId);
910
898
  }
911
- async buildWorkspaceResolution(ensured, resolution) {
912
- const tab = ensured.workspace.tabs.find((item) => item.id === ensured.workspace.activeTabId) ?? ensured.workspace.tabs[0] ?? null;
899
+ async buildBindingResolution(ensured, resolution) {
900
+ const tab = ensured.binding.tabs.find((item) => item.id === ensured.binding.activeTabId) ?? ensured.binding.tabs[0] ?? null;
913
901
  if (tab) {
914
902
  return {
915
903
  tab,
916
- workspace: ensured.workspace,
904
+ binding: ensured.binding,
917
905
  resolution,
918
- createdWorkspace: ensured.created,
906
+ createdBinding: ensured.created,
919
907
  repaired: ensured.repaired,
920
908
  repairActions: ensured.repairActions
921
909
  };
922
910
  }
923
- if (ensured.workspace.activeTabId !== null) {
924
- const activeWorkspaceTab = await this.waitForTrackedTab(ensured.workspace.activeTabId, ensured.workspace.windowId);
925
- if (activeWorkspaceTab) {
911
+ if (ensured.binding.activeTabId !== null) {
912
+ const activeBindingTab = await this.waitForTrackedTab(ensured.binding.activeTabId, ensured.binding.windowId);
913
+ if (activeBindingTab) {
926
914
  return {
927
- tab: activeWorkspaceTab,
928
- workspace: ensured.workspace,
915
+ tab: activeBindingTab,
916
+ binding: ensured.binding,
929
917
  resolution,
930
- createdWorkspace: ensured.created,
918
+ createdBinding: ensured.created,
931
919
  repaired: ensured.repaired,
932
920
  repairActions: ensured.repairActions
933
921
  };
@@ -939,9 +927,9 @@
939
927
  }
940
928
  return {
941
929
  tab: activeTab,
942
- workspace: null,
930
+ binding: null,
943
931
  resolution: "browser-active",
944
- createdWorkspace: ensured.created,
932
+ createdBinding: ensured.created,
945
933
  repaired: ensured.repaired,
946
934
  repairActions: ensured.repairActions
947
935
  };
@@ -972,7 +960,7 @@
972
960
  collectCandidateTabIds(state) {
973
961
  return [...new Set(state.tabIds.concat([state.activeTabId, state.primaryTabId].filter((value) => typeof value === "number")))];
974
962
  }
975
- async rebindWorkspaceWindow(state) {
963
+ async rebindBindingWindow(state) {
976
964
  const candidateWindowIds = [];
977
965
  const pushWindowId = (windowId) => {
978
966
  if (typeof windowId !== "number") {
@@ -982,7 +970,7 @@
982
970
  candidateWindowIds.push(windowId);
983
971
  }
984
972
  };
985
- const group = state.groupId !== null ? await this.browser.getGroup(state.groupId) : null;
973
+ const group = state.groupId !== null ? await this.waitForGroup(state.groupId) : null;
986
974
  pushWindowId(group?.windowId);
987
975
  const trackedTabs = await this.readLooseTrackedTabs(this.collectCandidateTabIds(state));
988
976
  for (const tab of trackedTabs) {
@@ -995,7 +983,7 @@
995
983
  }
996
984
  let tabs = await this.readTrackedTabs(this.collectCandidateTabIds(state), candidateWindowId);
997
985
  if (tabs.length === 0 && group?.id !== null && group?.windowId === candidateWindowId) {
998
- const windowTabs = await this.waitForWindowTabs(candidateWindowId, 750);
986
+ const windowTabs = await this.waitForWindowTabs(candidateWindowId, WINDOW_TABS_LOOKUP_TIMEOUT_MS);
999
987
  tabs = windowTabs.filter((tab) => tab.groupId === group.id);
1000
988
  }
1001
989
  if (tabs.length === 0) {
@@ -1015,19 +1003,19 @@
1015
1003
  }
1016
1004
  return null;
1017
1005
  }
1018
- async inspectWorkspaceWindowOwnership(state, windowId) {
1006
+ async inspectBindingWindowOwnership(state, windowId) {
1019
1007
  const windowTabs = await this.waitForWindowTabs(windowId, 500);
1020
1008
  const trackedIds = new Set(this.collectCandidateTabIds(state));
1021
1009
  return {
1022
- workspaceTabs: windowTabs.filter((tab) => trackedIds.has(tab.id) || state.groupId !== null && tab.groupId === state.groupId),
1010
+ bindingTabs: windowTabs.filter((tab) => trackedIds.has(tab.id) || state.groupId !== null && tab.groupId === state.groupId),
1023
1011
  foreignTabs: windowTabs.filter((tab) => !trackedIds.has(tab.id) && (state.groupId === null || tab.groupId !== state.groupId))
1024
1012
  };
1025
1013
  }
1026
- async moveWorkspaceIntoDedicatedWindow(state, ownership, initialUrl) {
1027
- const sourceTabs = this.orderWorkspaceTabsForMigration(state, ownership.workspaceTabs);
1014
+ async moveBindingIntoDedicatedWindow(state, ownership, initialUrl) {
1015
+ const sourceTabs = this.orderSessionBindingTabsForMigration(state, ownership.bindingTabs);
1028
1016
  const seedUrl = sourceTabs[0]?.url ?? initialUrl;
1029
1017
  const window2 = await this.browser.createWindow({
1030
- url: seedUrl || DEFAULT_WORKSPACE_URL,
1018
+ url: seedUrl || DEFAULT_SESSION_BINDING_URL,
1031
1019
  focused: false
1032
1020
  });
1033
1021
  const recreatedTabs = await this.waitForWindowTabs(window2.id);
@@ -1037,7 +1025,7 @@
1037
1025
  tabIdMap.set(sourceTabs[0].id, firstTab.id);
1038
1026
  }
1039
1027
  for (const sourceTab of sourceTabs.slice(1)) {
1040
- const recreated = await this.createWorkspaceTab({
1028
+ const recreated = await this.createBindingTab({
1041
1029
  windowId: window2.id,
1042
1030
  url: sourceTab.url,
1043
1031
  active: false
@@ -1055,15 +1043,15 @@
1055
1043
  state.tabIds = recreatedTabs.map((tab) => tab.id);
1056
1044
  state.primaryTabId = nextPrimaryTabId;
1057
1045
  state.activeTabId = nextActiveTabId;
1058
- for (const workspaceTab of ownership.workspaceTabs) {
1059
- await this.browser.closeTab(workspaceTab.id);
1046
+ for (const bindingTab of ownership.bindingTabs) {
1047
+ await this.browser.closeTab(bindingTab.id);
1060
1048
  }
1061
1049
  return {
1062
1050
  window: window2,
1063
1051
  tabs: await this.readTrackedTabs(state.tabIds, state.windowId)
1064
1052
  };
1065
1053
  }
1066
- orderWorkspaceTabsForMigration(state, tabs) {
1054
+ orderSessionBindingTabsForMigration(state, tabs) {
1067
1055
  const ordered = [];
1068
1056
  const seen = /* @__PURE__ */ new Set();
1069
1057
  const pushById = (tabId) => {
@@ -1088,7 +1076,7 @@
1088
1076
  }
1089
1077
  return ordered;
1090
1078
  }
1091
- async recoverWorkspaceTabs(state, existingTabs) {
1079
+ async recoverBindingTabs(state, existingTabs) {
1092
1080
  if (state.windowId === null) {
1093
1081
  return existingTabs;
1094
1082
  }
@@ -1114,9 +1102,9 @@
1114
1102
  }
1115
1103
  return existingTabs;
1116
1104
  }
1117
- async createWorkspaceTab(options) {
1105
+ async createBindingTab(options) {
1118
1106
  if (options.windowId === null) {
1119
- throw new Error("Workspace window is unavailable");
1107
+ throw new Error("Binding window is unavailable");
1120
1108
  }
1121
1109
  const deadline = Date.now() + 1500;
1122
1110
  let lastError2 = null;
@@ -1137,8 +1125,8 @@
1137
1125
  }
1138
1126
  throw lastError2 ?? new Error(`No window with id: ${options.windowId}.`);
1139
1127
  }
1140
- async inspectWorkspace(workspaceId) {
1141
- const state = await this.loadWorkspaceRecord(workspaceId);
1128
+ async inspectBinding(bindingId) {
1129
+ const state = await this.loadBindingRecord(bindingId);
1142
1130
  if (!state) {
1143
1131
  return null;
1144
1132
  }
@@ -1159,34 +1147,34 @@
1159
1147
  tabs
1160
1148
  };
1161
1149
  }
1162
- async resolveReusablePrimaryTab(workspace, allowReuse) {
1163
- if (workspace.windowId === null) {
1150
+ async resolveReusablePrimaryTab(binding, allowReuse) {
1151
+ if (binding.windowId === null) {
1164
1152
  return null;
1165
1153
  }
1166
- if (workspace.primaryTabId !== null) {
1167
- const trackedPrimary = workspace.tabs.find((tab) => tab.id === workspace.primaryTabId) ?? await this.waitForTrackedTab(workspace.primaryTabId, workspace.windowId);
1168
- if (trackedPrimary && (allowReuse || this.isReusableBlankWorkspaceTab(trackedPrimary, workspace))) {
1154
+ if (binding.primaryTabId !== null) {
1155
+ const trackedPrimary = binding.tabs.find((tab) => tab.id === binding.primaryTabId) ?? await this.waitForTrackedTab(binding.primaryTabId, binding.windowId);
1156
+ if (trackedPrimary && (allowReuse || this.isReusableBlankSessionBindingTab(trackedPrimary, binding))) {
1169
1157
  return trackedPrimary;
1170
1158
  }
1171
1159
  }
1172
- const windowTabs = await this.waitForWindowTabs(workspace.windowId, 750);
1160
+ const windowTabs = await this.waitForWindowTabs(binding.windowId, WINDOW_TABS_LOOKUP_TIMEOUT_MS);
1173
1161
  if (windowTabs.length !== 1) {
1174
1162
  return null;
1175
1163
  }
1176
1164
  const candidate = windowTabs[0];
1177
- if (allowReuse || this.isReusableBlankWorkspaceTab(candidate, workspace)) {
1165
+ if (allowReuse || this.isReusableBlankSessionBindingTab(candidate, binding)) {
1178
1166
  return candidate;
1179
1167
  }
1180
1168
  return null;
1181
1169
  }
1182
- isReusableBlankWorkspaceTab(tab, workspace) {
1183
- if (workspace.tabIds.length > 1) {
1170
+ isReusableBlankSessionBindingTab(tab, binding) {
1171
+ if (binding.tabIds.length > 1) {
1184
1172
  return false;
1185
1173
  }
1186
1174
  const normalizedUrl = tab.url.trim().toLowerCase();
1187
- return normalizedUrl === "" || normalizedUrl === DEFAULT_WORKSPACE_URL;
1175
+ return normalizedUrl === "" || normalizedUrl === DEFAULT_SESSION_BINDING_URL;
1188
1176
  }
1189
- async waitForWindow(windowId, timeoutMs = 750) {
1177
+ async waitForWindow(windowId, timeoutMs = WINDOW_LOOKUP_TIMEOUT_MS) {
1190
1178
  const deadline = Date.now() + timeoutMs;
1191
1179
  while (Date.now() < deadline) {
1192
1180
  const window2 = await this.browser.getWindow(windowId);
@@ -1197,6 +1185,17 @@
1197
1185
  }
1198
1186
  return null;
1199
1187
  }
1188
+ async waitForGroup(groupId, timeoutMs = GROUP_LOOKUP_TIMEOUT_MS) {
1189
+ const deadline = Date.now() + timeoutMs;
1190
+ while (Date.now() < deadline) {
1191
+ const group = await this.browser.getGroup(groupId);
1192
+ if (group) {
1193
+ return group;
1194
+ }
1195
+ await this.delay(50);
1196
+ }
1197
+ return null;
1198
+ }
1200
1199
  async waitForTrackedTab(tabId, windowId, timeoutMs = 1e3) {
1201
1200
  const deadline = Date.now() + timeoutMs;
1202
1201
  while (Date.now() < deadline) {
@@ -1208,7 +1207,7 @@
1208
1207
  }
1209
1208
  return null;
1210
1209
  }
1211
- async waitForWindowTabs(windowId, timeoutMs = 1e3) {
1210
+ async waitForWindowTabs(windowId, timeoutMs = WINDOW_TABS_LOOKUP_TIMEOUT_MS) {
1212
1211
  const deadline = Date.now() + timeoutMs;
1213
1212
  while (Date.now() < deadline) {
1214
1213
  const tabs = await this.browser.listTabs({ windowId });
@@ -1236,6 +1235,9 @@
1236
1235
  var DEFAULT_TAB_LOAD_TIMEOUT_MS = 4e4;
1237
1236
  var textEncoder2 = new TextEncoder();
1238
1237
  var textDecoder2 = new TextDecoder();
1238
+ var DATA_TIMESTAMP_CONTEXT_PATTERN = /\b(updated|update|updatedat|asof|timestamp|generated|generatedat|refresh|freshness|latest|last|quote|trade|price|flow|market|time|snapshot|signal)\b/i;
1239
+ var CONTRACT_TIMESTAMP_CONTEXT_PATTERN = /\b(expiry|expiration|expires|option|contract|strike|maturity|dte|call|put|exercise)\b/i;
1240
+ var EVENT_TIMESTAMP_CONTEXT_PATTERN = /\b(earnings|event|report|dividend|split|meeting|fomc|release|filing)\b/i;
1239
1241
  var REPLAY_FORBIDDEN_HEADER_NAMES = /* @__PURE__ */ new Set([
1240
1242
  "accept-encoding",
1241
1243
  "authorization",
@@ -1254,6 +1256,7 @@
1254
1256
  var reconnectAttempt = 0;
1255
1257
  var lastError = null;
1256
1258
  var manualDisconnect = false;
1259
+ var sessionBindingStateMutationQueue = Promise.resolve();
1257
1260
  async function getConfig() {
1258
1261
  const stored = await chrome.storage.local.get([STORAGE_KEY_TOKEN, STORAGE_KEY_PORT, STORAGE_KEY_DEBUG_RICH_TEXT]);
1259
1262
  return {
@@ -1308,10 +1311,10 @@
1308
1311
  if (lower.includes("no tab with id") || lower.includes("no window with id")) {
1309
1312
  return toError("E_NOT_FOUND", message);
1310
1313
  }
1311
- if (lower.includes("workspace") && lower.includes("does not exist")) {
1314
+ if (lower.includes("binding") && lower.includes("does not exist")) {
1312
1315
  return toError("E_NOT_FOUND", message);
1313
1316
  }
1314
- if (lower.includes("does not belong to workspace") || lower.includes("is missing from workspace")) {
1317
+ if (lower.includes("does not belong to binding") || lower.includes("is missing from binding")) {
1315
1318
  return toError("E_NOT_FOUND", message);
1316
1319
  }
1317
1320
  if (lower.includes("invalid url") || lower.includes("url is invalid")) {
@@ -1335,38 +1338,55 @@
1335
1338
  groupId: typeof tab.groupId === "number" && tab.groupId >= 0 ? tab.groupId : null
1336
1339
  };
1337
1340
  }
1338
- async function loadWorkspaceStateMap() {
1339
- const stored = await chrome.storage.local.get([
1340
- STORAGE_KEY_SESSION_BINDINGS,
1341
- LEGACY_STORAGE_KEY_WORKSPACES,
1342
- LEGACY_STORAGE_KEY_WORKSPACE
1343
- ]);
1341
+ async function readSessionBindingStateMap() {
1342
+ const stored = await chrome.storage.local.get([STORAGE_KEY_SESSION_BINDINGS]);
1344
1343
  return resolveSessionBindingStateMap(stored);
1345
1344
  }
1346
- async function loadWorkspaceState(workspaceId) {
1347
- const stateMap = await loadWorkspaceStateMap();
1348
- return stateMap[workspaceId] ?? null;
1349
- }
1350
- async function listWorkspaceStates() {
1351
- return Object.values(await loadWorkspaceStateMap());
1352
- }
1353
- async function saveWorkspaceState(state) {
1354
- const stateMap = await loadWorkspaceStateMap();
1355
- stateMap[state.id] = state;
1356
- await chrome.storage.local.set({ [STORAGE_KEY_SESSION_BINDINGS]: stateMap });
1357
- await chrome.storage.local.remove([LEGACY_STORAGE_KEY_WORKSPACES, LEGACY_STORAGE_KEY_WORKSPACE]);
1358
- }
1359
- async function deleteWorkspaceState(workspaceId) {
1360
- const stateMap = await loadWorkspaceStateMap();
1361
- delete stateMap[workspaceId];
1345
+ async function flushSessionBindingStateMap(stateMap) {
1362
1346
  if (Object.keys(stateMap).length === 0) {
1363
- await chrome.storage.local.remove([STORAGE_KEY_SESSION_BINDINGS, LEGACY_STORAGE_KEY_WORKSPACES, LEGACY_STORAGE_KEY_WORKSPACE]);
1347
+ await chrome.storage.local.remove([STORAGE_KEY_SESSION_BINDINGS]);
1364
1348
  return;
1365
1349
  }
1366
1350
  await chrome.storage.local.set({ [STORAGE_KEY_SESSION_BINDINGS]: stateMap });
1367
- await chrome.storage.local.remove([LEGACY_STORAGE_KEY_WORKSPACES, LEGACY_STORAGE_KEY_WORKSPACE]);
1368
1351
  }
1369
- var workspaceBrowser = {
1352
+ async function runSessionBindingStateMutation(operation) {
1353
+ const run = sessionBindingStateMutationQueue.then(operation, operation);
1354
+ sessionBindingStateMutationQueue = run.then(
1355
+ () => void 0,
1356
+ () => void 0
1357
+ );
1358
+ return run;
1359
+ }
1360
+ async function mutateSessionBindingStateMap(mutator) {
1361
+ return await runSessionBindingStateMutation(async () => {
1362
+ const stateMap = await readSessionBindingStateMap();
1363
+ const result = await mutator(stateMap);
1364
+ await flushSessionBindingStateMap(stateMap);
1365
+ return result;
1366
+ });
1367
+ }
1368
+ async function loadSessionBindingStateMap() {
1369
+ await sessionBindingStateMutationQueue;
1370
+ return await readSessionBindingStateMap();
1371
+ }
1372
+ async function loadSessionBindingState(bindingId) {
1373
+ const stateMap = await loadSessionBindingStateMap();
1374
+ return stateMap[bindingId] ?? null;
1375
+ }
1376
+ async function listSessionBindingStates() {
1377
+ return Object.values(await loadSessionBindingStateMap());
1378
+ }
1379
+ async function saveSessionBindingState(state) {
1380
+ await mutateSessionBindingStateMap((stateMap) => {
1381
+ stateMap[state.id] = state;
1382
+ });
1383
+ }
1384
+ async function deleteSessionBindingState(bindingId) {
1385
+ await mutateSessionBindingStateMap((stateMap) => {
1386
+ delete stateMap[bindingId];
1387
+ });
1388
+ }
1389
+ var sessionBindingBrowser = {
1370
1390
  async getTab(tabId) {
1371
1391
  try {
1372
1392
  return toTabInfo(await chrome.tabs.get(tabId));
@@ -1498,12 +1518,12 @@
1498
1518
  };
1499
1519
  var bindingManager = new SessionBindingManager(
1500
1520
  {
1501
- load: loadWorkspaceState,
1502
- save: saveWorkspaceState,
1503
- delete: deleteWorkspaceState,
1504
- list: listWorkspaceStates
1521
+ load: loadSessionBindingState,
1522
+ save: saveSessionBindingState,
1523
+ delete: deleteSessionBindingState,
1524
+ list: listSessionBindingStates
1505
1525
  },
1506
- workspaceBrowser
1526
+ sessionBindingBrowser
1507
1527
  );
1508
1528
  async function waitForTabComplete(tabId, timeoutMs = DEFAULT_TAB_LOAD_TIMEOUT_MS) {
1509
1529
  try {
@@ -1592,7 +1612,7 @@
1592
1612
  return raw;
1593
1613
  }
1594
1614
  }
1595
- async function finalizeOpenedWorkspaceTab(opened, expectedUrl) {
1615
+ async function finalizeOpenedSessionBindingTab(opened, expectedUrl) {
1596
1616
  if (expectedUrl && expectedUrl !== "about:blank") {
1597
1617
  await waitForTabUrl(opened.tab.id, expectedUrl).catch(() => void 0);
1598
1618
  }
@@ -1607,14 +1627,14 @@
1607
1627
  url: effectiveUrl
1608
1628
  };
1609
1629
  } catch {
1610
- refreshedTab = await workspaceBrowser.getTab(opened.tab.id) ?? opened.tab;
1630
+ refreshedTab = await sessionBindingBrowser.getTab(opened.tab.id) ?? opened.tab;
1611
1631
  }
1612
- const refreshedWorkspace = await bindingManager.getWorkspaceInfo(opened.workspace.id) ?? {
1613
- ...opened.workspace,
1614
- tabs: opened.workspace.tabs.map((tab) => tab.id === refreshedTab.id ? refreshedTab : tab)
1632
+ const refreshedBinding = await bindingManager.getBindingInfo(opened.binding.id) ?? {
1633
+ ...opened.binding,
1634
+ tabs: opened.binding.tabs.map((tab) => tab.id === refreshedTab.id ? refreshedTab : tab)
1615
1635
  };
1616
1636
  return {
1617
- workspace: refreshedWorkspace,
1637
+ binding: refreshedBinding,
1618
1638
  tab: refreshedTab
1619
1639
  };
1620
1640
  }
@@ -1638,7 +1658,7 @@
1638
1658
  }
1639
1659
  const resolved = await bindingManager.resolveTarget({
1640
1660
  tabId: target.tabId,
1641
- workspaceId: typeof target.workspaceId === "string" ? target.workspaceId : void 0,
1661
+ bindingId: typeof target.bindingId === "string" ? target.bindingId : void 0,
1642
1662
  createIfMissing: false
1643
1663
  });
1644
1664
  const tab = await chrome.tabs.get(resolved.tab.id);
@@ -1774,6 +1794,7 @@
1774
1794
  framePath,
1775
1795
  expr: typeof params.expr === "string" ? params.expr : "",
1776
1796
  path: typeof params.path === "string" ? params.path : "",
1797
+ resolver: typeof params.resolver === "string" ? params.resolver : void 0,
1777
1798
  url: typeof params.url === "string" ? params.url : "",
1778
1799
  method: typeof params.method === "string" ? params.method : "GET",
1779
1800
  headers: typeof params.headers === "object" && params.headers !== null ? params.headers : void 0,
@@ -1856,6 +1877,54 @@
1856
1877
  }
1857
1878
  return currentWindow;
1858
1879
  };
1880
+ const buildPathExpression = (path) => parsePath(path).map((segment, index) => {
1881
+ if (typeof segment === "number") {
1882
+ return `[${segment}]`;
1883
+ }
1884
+ if (index === 0) {
1885
+ return segment;
1886
+ }
1887
+ return `.${segment}`;
1888
+ }).join("");
1889
+ const readPath = (targetWindow, path) => {
1890
+ const segments = parsePath(path);
1891
+ let current = targetWindow;
1892
+ for (const segment of segments) {
1893
+ if (current === null || current === void 0 || !(segment in current)) {
1894
+ throw { code: "E_NOT_FOUND", message: `path not found: ${path}` };
1895
+ }
1896
+ current = current[segment];
1897
+ }
1898
+ return current;
1899
+ };
1900
+ const resolveExtractValue = (targetWindow, path, resolver) => {
1901
+ const strategy = resolver === "globalThis" || resolver === "lexical" ? resolver : "auto";
1902
+ const lexicalExpression = buildPathExpression(path);
1903
+ const readLexical = () => {
1904
+ try {
1905
+ return targetWindow.eval(lexicalExpression);
1906
+ } catch (error) {
1907
+ if (error instanceof ReferenceError) {
1908
+ throw { code: "E_NOT_FOUND", message: `path not found: ${path}` };
1909
+ }
1910
+ throw error;
1911
+ }
1912
+ };
1913
+ if (strategy === "globalThis") {
1914
+ return { resolver: "globalThis", value: readPath(targetWindow, path) };
1915
+ }
1916
+ if (strategy === "lexical") {
1917
+ return { resolver: "lexical", value: readLexical() };
1918
+ }
1919
+ try {
1920
+ return { resolver: "globalThis", value: readPath(targetWindow, path) };
1921
+ } catch (error) {
1922
+ if (typeof error !== "object" || error === null || error.code !== "E_NOT_FOUND") {
1923
+ throw error;
1924
+ }
1925
+ }
1926
+ return { resolver: "lexical", value: readLexical() };
1927
+ };
1859
1928
  try {
1860
1929
  const targetWindow = payload.scope === "main" ? window : payload.scope === "current" ? resolveFrameWindow(payload.framePath ?? []) : window;
1861
1930
  if (payload.action === "eval") {
@@ -1864,16 +1933,15 @@
1864
1933
  return { url: targetWindow.location.href, framePath: payload.scope === "current" ? payload.framePath ?? [] : [], value: serialized.value, bytes: serialized.bytes };
1865
1934
  }
1866
1935
  if (payload.action === "extract") {
1867
- const segments = parsePath(payload.path);
1868
- let current = targetWindow;
1869
- for (const segment of segments) {
1870
- if (current === null || current === void 0 || !(segment in current)) {
1871
- throw { code: "E_NOT_FOUND", message: `path not found: ${payload.path}` };
1872
- }
1873
- current = current[segment];
1874
- }
1875
- const serialized = serializeValue(current, payload.maxBytes);
1876
- return { url: targetWindow.location.href, framePath: payload.scope === "current" ? payload.framePath ?? [] : [], value: serialized.value, bytes: serialized.bytes };
1936
+ const extracted = resolveExtractValue(targetWindow, payload.path, payload.resolver);
1937
+ const serialized = serializeValue(extracted.value, payload.maxBytes);
1938
+ return {
1939
+ url: targetWindow.location.href,
1940
+ framePath: payload.scope === "current" ? payload.framePath ?? [] : [],
1941
+ value: serialized.value,
1942
+ bytes: serialized.bytes,
1943
+ resolver: extracted.resolver
1944
+ };
1877
1945
  }
1878
1946
  if (payload.action === "fetch") {
1879
1947
  const headers = { ...payload.headers ?? {} };
@@ -2021,6 +2089,31 @@
2021
2089
  }
2022
2090
  return Object.keys(headers).length > 0 ? headers : void 0;
2023
2091
  }
2092
+ function collectTimestampMatchesFromText(text, source, patterns) {
2093
+ const regexes = (patterns ?? [
2094
+ String.raw`\b20\d{2}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(?:\.\d+)?(?:Z|[+-]\d{2}:?\d{2})?\b`,
2095
+ String.raw`\b20\d{2}-\d{2}-\d{2}\b`,
2096
+ String.raw`\b20\d{2}\/\d{2}\/\d{2}\b`
2097
+ ]).map((pattern) => new RegExp(pattern, "gi"));
2098
+ const collected = /* @__PURE__ */ new Map();
2099
+ for (const regex of regexes) {
2100
+ for (const match of text.matchAll(regex)) {
2101
+ const value = match[0];
2102
+ if (!value) {
2103
+ continue;
2104
+ }
2105
+ const index = match.index ?? text.indexOf(value);
2106
+ const start = Math.max(0, index - 28);
2107
+ const end = Math.min(text.length, index + value.length + 28);
2108
+ const context = text.slice(start, end).replace(/\s+/g, " ").trim();
2109
+ const key = `${value}::${context}`;
2110
+ if (!collected.has(key)) {
2111
+ collected.set(key, { value, source, context });
2112
+ }
2113
+ }
2114
+ }
2115
+ return [...collected.values()];
2116
+ }
2024
2117
  function parseTimestampCandidate(value, now = Date.now()) {
2025
2118
  const normalized = value.trim().toLowerCase();
2026
2119
  if (!normalized) {
@@ -2035,13 +2128,61 @@
2035
2128
  const parsed = Date.parse(value);
2036
2129
  return Number.isNaN(parsed) ? null : parsed;
2037
2130
  }
2038
- function extractLatestTimestamp(values, now = Date.now()) {
2039
- if (!Array.isArray(values) || values.length === 0) {
2131
+ function nearestPatternDistance(text, anchor, pattern) {
2132
+ const normalizedText = text.toLowerCase();
2133
+ const normalizedAnchor = anchor.toLowerCase();
2134
+ const anchorIndex = normalizedText.indexOf(normalizedAnchor);
2135
+ if (anchorIndex < 0) {
2040
2136
  return null;
2041
2137
  }
2138
+ const regex = new RegExp(pattern.source, "gi");
2139
+ let match;
2140
+ let best = null;
2141
+ while ((match = regex.exec(normalizedText)) !== null) {
2142
+ best = best === null ? Math.abs(anchorIndex - match.index) : Math.min(best, Math.abs(anchorIndex - match.index));
2143
+ }
2144
+ return best;
2145
+ }
2146
+ function classifyTimestampCandidate(candidate, now = Date.now()) {
2147
+ const normalizedPath = (candidate.path ?? "").toLowerCase();
2148
+ if (DATA_TIMESTAMP_CONTEXT_PATTERN.test(normalizedPath)) {
2149
+ return "data";
2150
+ }
2151
+ if (CONTRACT_TIMESTAMP_CONTEXT_PATTERN.test(normalizedPath)) {
2152
+ return "contract";
2153
+ }
2154
+ if (EVENT_TIMESTAMP_CONTEXT_PATTERN.test(normalizedPath)) {
2155
+ return "event";
2156
+ }
2157
+ const context = candidate.context ?? "";
2158
+ const distances = [
2159
+ { category: "data", distance: nearestPatternDistance(context, candidate.value, DATA_TIMESTAMP_CONTEXT_PATTERN) },
2160
+ { category: "contract", distance: nearestPatternDistance(context, candidate.value, CONTRACT_TIMESTAMP_CONTEXT_PATTERN) },
2161
+ { category: "event", distance: nearestPatternDistance(context, candidate.value, EVENT_TIMESTAMP_CONTEXT_PATTERN) }
2162
+ ].filter((entry) => typeof entry.distance === "number");
2163
+ if (distances.length > 0) {
2164
+ distances.sort((left, right) => left.distance - right.distance);
2165
+ return distances[0].category;
2166
+ }
2167
+ const parsed = parseTimestampCandidate(candidate.value, now);
2168
+ return typeof parsed === "number" && parsed > now + 36 * 60 * 60 * 1e3 ? "contract" : "unknown";
2169
+ }
2170
+ function normalizeTimestampCandidates(candidates, now = Date.now()) {
2171
+ return candidates.map((candidate) => ({
2172
+ value: candidate.value,
2173
+ source: candidate.source,
2174
+ category: candidate.category ?? classifyTimestampCandidate(candidate, now),
2175
+ context: candidate.context,
2176
+ path: candidate.path
2177
+ }));
2178
+ }
2179
+ function latestTimestampFromCandidates(candidates, now = Date.now()) {
2042
2180
  let latest = null;
2043
- for (const value of values) {
2044
- const parsed = parseTimestampCandidate(value, now);
2181
+ for (const candidate of candidates) {
2182
+ if (candidate.category === "contract" || candidate.category === "event") {
2183
+ continue;
2184
+ }
2185
+ const parsed = parseTimestampCandidate(candidate.value, now);
2045
2186
  if (parsed === null) {
2046
2187
  continue;
2047
2188
  }
@@ -2051,15 +2192,24 @@
2051
2192
  }
2052
2193
  function computeFreshnessAssessment(input) {
2053
2194
  const now = Date.now();
2054
- const latestDataTimestamp = [input.latestInlineDataTimestamp, input.domVisibleTimestamp].filter((value) => typeof value === "number").sort((left, right) => right - left)[0] ?? null;
2055
- if (latestDataTimestamp !== null && now - latestDataTimestamp <= input.freshWindowMs) {
2195
+ const latestPageVisibleTimestamp = [input.latestPageDataTimestamp, input.latestInlineDataTimestamp, input.domVisibleTimestamp].filter((value) => typeof value === "number").sort((left, right) => right - left)[0] ?? null;
2196
+ if (latestPageVisibleTimestamp !== null && now - latestPageVisibleTimestamp <= input.freshWindowMs) {
2056
2197
  return "fresh";
2057
2198
  }
2199
+ const networkHasFreshData = typeof input.latestNetworkDataTimestamp === "number" && now - input.latestNetworkDataTimestamp <= input.freshWindowMs;
2200
+ if (networkHasFreshData) {
2201
+ return "lagged";
2202
+ }
2058
2203
  const recentSignals = [input.latestNetworkTimestamp, input.lastMutationAt].filter((value) => typeof value === "number").some((value) => now - value <= input.freshWindowMs);
2059
- if (recentSignals && latestDataTimestamp !== null && now - latestDataTimestamp > input.freshWindowMs) {
2204
+ if (recentSignals && latestPageVisibleTimestamp !== null && now - latestPageVisibleTimestamp > input.freshWindowMs) {
2060
2205
  return "lagged";
2061
2206
  }
2062
- const staleSignals = [input.latestNetworkTimestamp, input.lastMutationAt, latestDataTimestamp].filter((value) => typeof value === "number");
2207
+ const staleSignals = [
2208
+ input.latestNetworkTimestamp,
2209
+ input.lastMutationAt,
2210
+ latestPageVisibleTimestamp,
2211
+ input.latestNetworkDataTimestamp
2212
+ ].filter((value) => typeof value === "number");
2063
2213
  if (staleSignals.length > 0 && staleSignals.every((value) => now - value > input.staleWindowMs)) {
2064
2214
  return "stale";
2065
2215
  }
@@ -2068,25 +2218,167 @@
2068
2218
  async function collectPageInspection(tabId, params = {}) {
2069
2219
  return await forwardContentRpc(tabId, "bak.internal.inspectState", params);
2070
2220
  }
2221
+ async function probePageDataCandidatesForTab(tabId, inspection) {
2222
+ const candidateNames = [
2223
+ ...Array.isArray(inspection.suspiciousGlobals) ? inspection.suspiciousGlobals.map(String) : [],
2224
+ ...Array.isArray(inspection.globalsPreview) ? inspection.globalsPreview.map(String) : []
2225
+ ].filter((name, index, array) => /^[A-Za-z_$][\w$]*$/.test(name) && array.indexOf(name) === index).slice(0, 16);
2226
+ if (candidateNames.length === 0) {
2227
+ return [];
2228
+ }
2229
+ const expr = `(() => {
2230
+ const candidates = ${JSON.stringify(candidateNames)};
2231
+ const dataPattern = /\\b(updated|update|updatedat|asof|timestamp|generated|generatedat|refresh|latest|last|quote|trade|price|flow|market|time|snapshot|signal)\\b/i;
2232
+ const contractPattern = /\\b(expiry|expiration|expires|option|contract|strike|maturity|dte|call|put|exercise)\\b/i;
2233
+ const eventPattern = /\\b(earnings|event|report|dividend|split|meeting|fomc|release|filing)\\b/i;
2234
+ const isTimestampString = (value) => typeof value === 'string' && value.trim().length > 0 && !Number.isNaN(Date.parse(value.trim()));
2235
+ const classify = (path, value) => {
2236
+ const normalized = String(path || '').toLowerCase();
2237
+ if (dataPattern.test(normalized)) return 'data';
2238
+ if (contractPattern.test(normalized)) return 'contract';
2239
+ if (eventPattern.test(normalized)) return 'event';
2240
+ const parsed = Date.parse(String(value || '').trim());
2241
+ return Number.isFinite(parsed) && parsed > Date.now() + 36 * 60 * 60 * 1000 ? 'contract' : 'unknown';
2242
+ };
2243
+ const sampleValue = (value, depth = 0) => {
2244
+ if (depth >= 2 || value == null || typeof value !== 'object') {
2245
+ if (typeof value === 'string') {
2246
+ return value.length > 160 ? value.slice(0, 160) : value;
2247
+ }
2248
+ if (typeof value === 'function') {
2249
+ return '[Function]';
2250
+ }
2251
+ return value;
2252
+ }
2253
+ if (Array.isArray(value)) {
2254
+ return value.slice(0, 3).map((item) => sampleValue(item, depth + 1));
2255
+ }
2256
+ const sampled = {};
2257
+ for (const key of Object.keys(value).slice(0, 8)) {
2258
+ try {
2259
+ sampled[key] = sampleValue(value[key], depth + 1);
2260
+ } catch {
2261
+ sampled[key] = '[Unreadable]';
2262
+ }
2263
+ }
2264
+ return sampled;
2265
+ };
2266
+ const collectTimestamps = (value, path, depth, collected) => {
2267
+ if (collected.length >= 16) return;
2268
+ if (isTimestampString(value)) {
2269
+ collected.push({ path, value: String(value), category: classify(path, value) });
2270
+ return;
2271
+ }
2272
+ if (depth >= 3) return;
2273
+ if (Array.isArray(value)) {
2274
+ value.slice(0, 3).forEach((item, index) => collectTimestamps(item, path + '[' + index + ']', depth + 1, collected));
2275
+ return;
2276
+ }
2277
+ if (value && typeof value === 'object') {
2278
+ Object.keys(value)
2279
+ .slice(0, 8)
2280
+ .forEach((key) => {
2281
+ try {
2282
+ collectTimestamps(value[key], path ? path + '.' + key : key, depth + 1, collected);
2283
+ } catch {
2284
+ // Ignore unreadable nested properties.
2285
+ }
2286
+ });
2287
+ }
2288
+ };
2289
+ const readCandidate = (name) => {
2290
+ if (name in globalThis) {
2291
+ return { resolver: 'globalThis', value: globalThis[name] };
2292
+ }
2293
+ return { resolver: 'lexical', value: globalThis.eval(name) };
2294
+ };
2295
+ const results = [];
2296
+ for (const name of candidates) {
2297
+ try {
2298
+ const resolved = readCandidate(name);
2299
+ const timestamps = [];
2300
+ collectTimestamps(resolved.value, name, 0, timestamps);
2301
+ results.push({
2302
+ name,
2303
+ resolver: resolved.resolver,
2304
+ sample: sampleValue(resolved.value),
2305
+ timestamps
2306
+ });
2307
+ } catch {
2308
+ // Ignore inaccessible candidates.
2309
+ }
2310
+ }
2311
+ return results;
2312
+ })()`;
2313
+ try {
2314
+ const evaluated = await executePageWorld(tabId, "eval", {
2315
+ expr,
2316
+ scope: "current",
2317
+ maxBytes: 64 * 1024
2318
+ });
2319
+ const frameResult = evaluated.result ?? evaluated.results?.find((candidate) => candidate.value || candidate.error);
2320
+ return Array.isArray(frameResult?.value) ? frameResult.value : [];
2321
+ } catch {
2322
+ return [];
2323
+ }
2324
+ }
2071
2325
  async function buildFreshnessForTab(tabId, params = {}) {
2072
2326
  const inspection = await collectPageInspection(tabId, params);
2073
- const visibleTimestamps = Array.isArray(inspection.visibleTimestamps) ? inspection.visibleTimestamps.map(String) : [];
2074
- const inlineTimestamps = Array.isArray(inspection.inlineTimestamps) ? inspection.inlineTimestamps.map(String) : [];
2327
+ const probedPageDataCandidates = await probePageDataCandidatesForTab(tabId, inspection);
2075
2328
  const now = Date.now();
2076
2329
  const freshWindowMs = typeof params.freshWindowMs === "number" ? Math.max(1, Math.floor(params.freshWindowMs)) : 15 * 60 * 1e3;
2077
2330
  const staleWindowMs = typeof params.staleWindowMs === "number" ? Math.max(freshWindowMs, Math.floor(params.staleWindowMs)) : 24 * 60 * 60 * 1e3;
2078
- const latestInlineDataTimestamp = extractLatestTimestamp(inlineTimestamps, now);
2079
- const domVisibleTimestamp = extractLatestTimestamp(visibleTimestamps, now);
2331
+ const visibleCandidates = normalizeTimestampCandidates(
2332
+ Array.isArray(inspection.visibleTimestampCandidates) ? inspection.visibleTimestampCandidates.filter((candidate) => typeof candidate === "object" && candidate !== null).map((candidate) => ({
2333
+ value: String(candidate.value ?? ""),
2334
+ context: typeof candidate.context === "string" ? candidate.context : void 0,
2335
+ source: "visible"
2336
+ })) : Array.isArray(inspection.visibleTimestamps) ? inspection.visibleTimestamps.map((value) => ({ value: String(value), source: "visible" })) : [],
2337
+ now
2338
+ );
2339
+ const inlineCandidates = normalizeTimestampCandidates(
2340
+ Array.isArray(inspection.inlineTimestampCandidates) ? inspection.inlineTimestampCandidates.filter((candidate) => typeof candidate === "object" && candidate !== null).map((candidate) => ({
2341
+ value: String(candidate.value ?? ""),
2342
+ context: typeof candidate.context === "string" ? candidate.context : void 0,
2343
+ source: "inline"
2344
+ })) : Array.isArray(inspection.inlineTimestamps) ? inspection.inlineTimestamps.map((value) => ({ value: String(value), source: "inline" })) : [],
2345
+ now
2346
+ );
2347
+ const pageDataCandidates = probedPageDataCandidates.flatMap(
2348
+ (candidate) => Array.isArray(candidate.timestamps) ? candidate.timestamps.map((timestamp) => ({
2349
+ value: String(timestamp.value ?? ""),
2350
+ source: "page-data",
2351
+ path: typeof timestamp.path === "string" ? timestamp.path : candidate.name,
2352
+ category: timestamp.category === "data" || timestamp.category === "contract" || timestamp.category === "event" || timestamp.category === "unknown" ? timestamp.category : "unknown"
2353
+ })) : []
2354
+ );
2355
+ const networkEntries = listNetworkEntries(tabId, { limit: 25 });
2356
+ const networkCandidates = normalizeTimestampCandidates(
2357
+ networkEntries.flatMap((entry) => {
2358
+ const previews = [entry.responseBodyPreview, entry.requestBodyPreview].filter((value) => typeof value === "string");
2359
+ return previews.flatMap((preview) => collectTimestampMatchesFromText(preview, "network", Array.isArray(params.patterns) ? params.patterns.map(String) : void 0));
2360
+ }),
2361
+ now
2362
+ );
2363
+ const latestInlineDataTimestamp = latestTimestampFromCandidates(inlineCandidates, now);
2364
+ const latestPageDataTimestamp = latestTimestampFromCandidates(pageDataCandidates, now);
2365
+ const latestNetworkDataTimestamp = latestTimestampFromCandidates(networkCandidates, now);
2366
+ const domVisibleTimestamp = latestTimestampFromCandidates(visibleCandidates, now);
2080
2367
  const latestNetworkTs = latestNetworkTimestamp(tabId);
2081
2368
  const lastMutationAt = typeof inspection.lastMutationAt === "number" ? inspection.lastMutationAt : null;
2369
+ const allCandidates = [...visibleCandidates, ...inlineCandidates, ...pageDataCandidates, ...networkCandidates];
2082
2370
  return {
2083
2371
  pageLoadedAt: typeof inspection.pageLoadedAt === "number" ? inspection.pageLoadedAt : null,
2084
2372
  lastMutationAt,
2085
2373
  latestNetworkTimestamp: latestNetworkTs,
2086
2374
  latestInlineDataTimestamp,
2375
+ latestPageDataTimestamp,
2376
+ latestNetworkDataTimestamp,
2087
2377
  domVisibleTimestamp,
2088
2378
  assessment: computeFreshnessAssessment({
2089
2379
  latestInlineDataTimestamp,
2380
+ latestPageDataTimestamp,
2381
+ latestNetworkDataTimestamp,
2090
2382
  latestNetworkTimestamp: latestNetworkTs,
2091
2383
  domVisibleTimestamp,
2092
2384
  lastMutationAt,
@@ -2094,17 +2386,116 @@
2094
2386
  staleWindowMs
2095
2387
  }),
2096
2388
  evidence: {
2097
- visibleTimestamps,
2098
- inlineTimestamps,
2389
+ visibleTimestamps: visibleCandidates.map((candidate) => candidate.value),
2390
+ inlineTimestamps: inlineCandidates.map((candidate) => candidate.value),
2391
+ pageDataTimestamps: pageDataCandidates.map((candidate) => candidate.value),
2392
+ networkDataTimestamps: networkCandidates.map((candidate) => candidate.value),
2393
+ classifiedTimestamps: allCandidates,
2099
2394
  networkSampleIds: recentNetworkSampleIds(tabId)
2100
2395
  }
2101
2396
  };
2102
2397
  }
2398
+ function summarizeNetworkCadence(entries) {
2399
+ const relevant = entries.filter((entry) => entry.kind === "fetch" || entry.kind === "xhr").slice().sort((left, right) => left.ts - right.ts);
2400
+ if (relevant.length === 0) {
2401
+ return {
2402
+ sampleCount: 0,
2403
+ classification: "none",
2404
+ averageIntervalMs: null,
2405
+ medianIntervalMs: null,
2406
+ latestGapMs: null,
2407
+ endpoints: []
2408
+ };
2409
+ }
2410
+ const intervals = [];
2411
+ for (let index = 1; index < relevant.length; index += 1) {
2412
+ intervals.push(Math.max(0, relevant[index].ts - relevant[index - 1].ts));
2413
+ }
2414
+ const sortedIntervals = intervals.slice().sort((left, right) => left - right);
2415
+ const averageIntervalMs = intervals.length > 0 ? Math.round(intervals.reduce((sum, value) => sum + value, 0) / intervals.length) : null;
2416
+ const medianIntervalMs = sortedIntervals.length > 0 ? sortedIntervals[Math.floor(sortedIntervals.length / 2)] ?? null : null;
2417
+ const latestGapMs = Math.max(0, Date.now() - relevant[relevant.length - 1].ts);
2418
+ const classification = relevant.length >= 3 && medianIntervalMs !== null && medianIntervalMs <= 3e4 ? "polling" : relevant.length >= 2 ? "bursty" : "single-request";
2419
+ return {
2420
+ sampleCount: relevant.length,
2421
+ classification,
2422
+ averageIntervalMs,
2423
+ medianIntervalMs,
2424
+ latestGapMs,
2425
+ endpoints: [...new Set(relevant.slice(-5).map((entry) => entry.url))].slice(0, 5)
2426
+ };
2427
+ }
2428
+ function extractReplayRowsCandidate(json) {
2429
+ if (Array.isArray(json)) {
2430
+ return { rows: json, source: "$" };
2431
+ }
2432
+ if (typeof json !== "object" || json === null) {
2433
+ return null;
2434
+ }
2435
+ const record = json;
2436
+ const preferredKeys = ["data", "rows", "results", "items"];
2437
+ for (const key of preferredKeys) {
2438
+ if (Array.isArray(record[key])) {
2439
+ return { rows: record[key], source: `$.${key}` };
2440
+ }
2441
+ }
2442
+ return null;
2443
+ }
2444
+ async function enrichReplayWithSchema(tabId, response) {
2445
+ const candidate = extractReplayRowsCandidate(response.json);
2446
+ if (!candidate || candidate.rows.length === 0) {
2447
+ return response;
2448
+ }
2449
+ const firstRow = candidate.rows[0];
2450
+ const tablesResult = await forwardContentRpc(tabId, "table.list", {});
2451
+ const tables = Array.isArray(tablesResult.tables) ? tablesResult.tables : [];
2452
+ if (tables.length === 0) {
2453
+ return response;
2454
+ }
2455
+ const schemas = [];
2456
+ for (const table of tables) {
2457
+ const schemaResult = await forwardContentRpc(tabId, "table.schema", { table: table.id });
2458
+ if (schemaResult.schema && Array.isArray(schemaResult.schema.columns)) {
2459
+ schemas.push({ table: schemaResult.table ?? table, schema: schemaResult.schema });
2460
+ }
2461
+ }
2462
+ if (schemas.length === 0) {
2463
+ return response;
2464
+ }
2465
+ if (Array.isArray(firstRow)) {
2466
+ const matchingSchema = schemas.find(({ schema }) => schema.columns.length === firstRow.length) ?? schemas[0];
2467
+ if (!matchingSchema) {
2468
+ return response;
2469
+ }
2470
+ const mappedRows = candidate.rows.filter((row) => Array.isArray(row)).map((row) => {
2471
+ const mapped = {};
2472
+ matchingSchema.schema.columns.forEach((column, index) => {
2473
+ mapped[column.label] = row[index];
2474
+ });
2475
+ return mapped;
2476
+ });
2477
+ return {
2478
+ ...response,
2479
+ table: matchingSchema.table,
2480
+ schema: matchingSchema.schema,
2481
+ mappedRows,
2482
+ mappingSource: candidate.source
2483
+ };
2484
+ }
2485
+ if (typeof firstRow === "object" && firstRow !== null) {
2486
+ return {
2487
+ ...response,
2488
+ mappedRows: candidate.rows.filter((row) => typeof row === "object" && row !== null),
2489
+ mappingSource: candidate.source
2490
+ };
2491
+ }
2492
+ return response;
2493
+ }
2103
2494
  async function handleRequest(request) {
2104
2495
  const params = request.params ?? {};
2105
2496
  const target = {
2106
2497
  tabId: typeof params.tabId === "number" ? params.tabId : void 0,
2107
- workspaceId: typeof params.workspaceId === "string" ? params.workspaceId : void 0
2498
+ bindingId: typeof params.bindingId === "string" ? params.bindingId : void 0
2108
2499
  };
2109
2500
  const rpcForwardMethods = /* @__PURE__ */ new Set([
2110
2501
  "page.title",
@@ -2203,63 +2594,92 @@
2203
2594
  await chrome.tabs.remove(tabId);
2204
2595
  return { ok: true };
2205
2596
  }
2206
- case "workspace.ensure": {
2597
+ case "sessionBinding.ensure": {
2207
2598
  return preserveHumanFocus(params.focus !== true, async () => {
2208
- const result = await bindingManager.ensureWorkspace({
2209
- workspaceId: String(params.workspaceId ?? ""),
2599
+ const result = await bindingManager.ensureBinding({
2600
+ bindingId: String(params.bindingId ?? ""),
2210
2601
  focus: params.focus === true,
2211
2602
  initialUrl: typeof params.url === "string" ? params.url : void 0
2212
2603
  });
2213
- for (const tab of result.workspace.tabs) {
2604
+ for (const tab of result.binding.tabs) {
2214
2605
  void ensureNetworkDebugger(tab.id).catch(() => void 0);
2215
2606
  }
2216
- return result;
2607
+ return {
2608
+ browser: result.binding,
2609
+ created: result.created,
2610
+ repaired: result.repaired,
2611
+ repairActions: result.repairActions
2612
+ };
2217
2613
  });
2218
2614
  }
2219
- case "workspace.info": {
2615
+ case "sessionBinding.info": {
2220
2616
  return {
2221
- workspace: await bindingManager.getWorkspaceInfo(String(params.workspaceId ?? ""))
2617
+ browser: await bindingManager.getBindingInfo(String(params.bindingId ?? ""))
2222
2618
  };
2223
2619
  }
2224
- case "workspace.openTab": {
2620
+ case "sessionBinding.openTab": {
2225
2621
  const expectedUrl = typeof params.url === "string" ? params.url : void 0;
2226
2622
  const opened = await preserveHumanFocus(params.focus !== true, async () => {
2227
2623
  return await bindingManager.openTab({
2228
- workspaceId: String(params.workspaceId ?? ""),
2624
+ bindingId: String(params.bindingId ?? ""),
2229
2625
  url: expectedUrl,
2230
2626
  active: params.active === true,
2231
2627
  focus: params.focus === true
2232
2628
  });
2233
2629
  });
2234
- const finalized = await finalizeOpenedWorkspaceTab(opened, expectedUrl);
2630
+ const finalized = await finalizeOpenedSessionBindingTab(opened, expectedUrl);
2235
2631
  void ensureNetworkDebugger(finalized.tab.id).catch(() => void 0);
2236
- return finalized;
2632
+ return {
2633
+ browser: finalized.binding,
2634
+ tab: finalized.tab
2635
+ };
2237
2636
  }
2238
- case "workspace.listTabs": {
2239
- return await bindingManager.listTabs(String(params.workspaceId ?? ""));
2637
+ case "sessionBinding.listTabs": {
2638
+ const listed = await bindingManager.listTabs(String(params.bindingId ?? ""));
2639
+ return {
2640
+ browser: listed.binding,
2641
+ tabs: listed.tabs
2642
+ };
2240
2643
  }
2241
- case "workspace.getActiveTab": {
2242
- return await bindingManager.getActiveTab(String(params.workspaceId ?? ""));
2644
+ case "sessionBinding.getActiveTab": {
2645
+ const active = await bindingManager.getActiveTab(String(params.bindingId ?? ""));
2646
+ return {
2647
+ browser: active.binding,
2648
+ tab: active.tab
2649
+ };
2243
2650
  }
2244
- case "workspace.setActiveTab": {
2245
- const result = await bindingManager.setActiveTab(Number(params.tabId), String(params.workspaceId ?? ""));
2651
+ case "sessionBinding.setActiveTab": {
2652
+ const result = await bindingManager.setActiveTab(Number(params.tabId), String(params.bindingId ?? ""));
2246
2653
  void ensureNetworkDebugger(result.tab.id).catch(() => void 0);
2247
- return result;
2654
+ return {
2655
+ browser: result.binding,
2656
+ tab: result.tab
2657
+ };
2248
2658
  }
2249
- case "workspace.focus": {
2250
- return await bindingManager.focus(String(params.workspaceId ?? ""));
2659
+ case "sessionBinding.focus": {
2660
+ const result = await bindingManager.focus(String(params.bindingId ?? ""));
2661
+ return {
2662
+ ok: true,
2663
+ browser: result.binding
2664
+ };
2251
2665
  }
2252
- case "workspace.reset": {
2666
+ case "sessionBinding.reset": {
2253
2667
  return await preserveHumanFocus(params.focus !== true, async () => {
2254
- return await bindingManager.reset({
2255
- workspaceId: String(params.workspaceId ?? ""),
2668
+ const result = await bindingManager.reset({
2669
+ bindingId: String(params.bindingId ?? ""),
2256
2670
  focus: params.focus === true,
2257
2671
  initialUrl: typeof params.url === "string" ? params.url : void 0
2258
2672
  });
2673
+ return {
2674
+ browser: result.binding,
2675
+ created: result.created,
2676
+ repaired: result.repaired,
2677
+ repairActions: result.repairActions
2678
+ };
2259
2679
  });
2260
2680
  }
2261
- case "workspace.close": {
2262
- return await bindingManager.close(String(params.workspaceId ?? ""));
2681
+ case "sessionBinding.close": {
2682
+ return await bindingManager.close(String(params.bindingId ?? ""));
2263
2683
  }
2264
2684
  case "page.goto": {
2265
2685
  return await preserveHumanFocus(typeof target.tabId !== "number", async () => {
@@ -2562,7 +2982,7 @@
2562
2982
  if (!first) {
2563
2983
  throw toError("E_EXECUTION", "network replay returned no response payload");
2564
2984
  }
2565
- return first;
2985
+ return params.withSchema === "auto" && params.mode === "json" ? await enrichReplayWithSchema(tab.id, first) : first;
2566
2986
  });
2567
2987
  }
2568
2988
  case "page.freshness": {
@@ -2638,15 +3058,17 @@
2638
3058
  const tab = await withTab(target);
2639
3059
  await ensureNetworkDebugger(tab.id).catch(() => void 0);
2640
3060
  const inspection = await collectPageInspection(tab.id, params);
3061
+ const pageDataCandidates = await probePageDataCandidatesForTab(tab.id, inspection);
2641
3062
  const network = listNetworkEntries(tab.id, { limit: 10 });
2642
3063
  return {
2643
3064
  suspiciousGlobals: inspection.suspiciousGlobals ?? [],
2644
3065
  tables: inspection.tables ?? [],
2645
3066
  visibleTimestamps: inspection.visibleTimestamps ?? [],
2646
3067
  inlineTimestamps: inspection.inlineTimestamps ?? [],
3068
+ pageDataCandidates,
2647
3069
  recentNetwork: network,
2648
3070
  recommendedNextSteps: [
2649
- "bak page extract --path table_data",
3071
+ "bak page extract --path table_data --resolver auto",
2650
3072
  "bak network search --pattern table_data",
2651
3073
  "bak page freshness"
2652
3074
  ]
@@ -2663,6 +3085,7 @@
2663
3085
  lastMutationAt: inspection.lastMutationAt ?? null,
2664
3086
  timers: inspection.timers ?? { timeouts: 0, intervals: 0 },
2665
3087
  networkCount: network.length,
3088
+ networkCadence: summarizeNetworkCadence(network),
2666
3089
  recentNetwork: network.slice(0, 10)
2667
3090
  };
2668
3091
  });
@@ -2673,7 +3096,10 @@
2673
3096
  const freshness = await buildFreshnessForTab(tab.id, params);
2674
3097
  return {
2675
3098
  ...freshness,
2676
- lagMs: typeof freshness.latestNetworkTimestamp === "number" && typeof freshness.latestInlineDataTimestamp === "number" ? Math.max(0, freshness.latestNetworkTimestamp - freshness.latestInlineDataTimestamp) : null
3099
+ lagMs: typeof freshness.latestNetworkTimestamp === "number" && typeof (freshness.latestPageDataTimestamp ?? freshness.latestInlineDataTimestamp) === "number" ? Math.max(
3100
+ 0,
3101
+ freshness.latestNetworkTimestamp - (freshness.latestPageDataTimestamp ?? freshness.latestInlineDataTimestamp ?? freshness.latestNetworkTimestamp)
3102
+ ) : null
2677
3103
  };
2678
3104
  });
2679
3105
  }
@@ -2777,7 +3203,7 @@
2777
3203
  ws?.send(JSON.stringify({
2778
3204
  type: "hello",
2779
3205
  role: "extension",
2780
- version: "0.6.0",
3206
+ version: "0.6.1",
2781
3207
  ts: Date.now()
2782
3208
  }));
2783
3209
  });
@@ -2813,48 +3239,48 @@
2813
3239
  }
2814
3240
  chrome.tabs.onRemoved.addListener((tabId) => {
2815
3241
  dropNetworkCapture(tabId);
2816
- void listWorkspaceStates().then(async (states) => {
2817
- for (const state of states) {
3242
+ void mutateSessionBindingStateMap((stateMap) => {
3243
+ for (const [bindingId, state] of Object.entries(stateMap)) {
2818
3244
  if (!state.tabIds.includes(tabId)) {
2819
3245
  continue;
2820
3246
  }
2821
3247
  const nextTabIds = state.tabIds.filter((id) => id !== tabId);
2822
- await saveWorkspaceState({
3248
+ stateMap[bindingId] = {
2823
3249
  ...state,
2824
3250
  tabIds: nextTabIds,
2825
3251
  activeTabId: state.activeTabId === tabId ? null : state.activeTabId,
2826
3252
  primaryTabId: state.primaryTabId === tabId ? null : state.primaryTabId
2827
- });
3253
+ };
2828
3254
  }
2829
3255
  });
2830
3256
  });
2831
3257
  chrome.tabs.onActivated.addListener((activeInfo) => {
2832
- void listWorkspaceStates().then(async (states) => {
2833
- for (const state of states) {
3258
+ void mutateSessionBindingStateMap((stateMap) => {
3259
+ for (const [bindingId, state] of Object.entries(stateMap)) {
2834
3260
  if (state.windowId !== activeInfo.windowId || !state.tabIds.includes(activeInfo.tabId)) {
2835
3261
  continue;
2836
3262
  }
2837
- await saveWorkspaceState({
3263
+ stateMap[bindingId] = {
2838
3264
  ...state,
2839
3265
  activeTabId: activeInfo.tabId
2840
- });
3266
+ };
2841
3267
  }
2842
3268
  });
2843
3269
  });
2844
3270
  chrome.windows.onRemoved.addListener((windowId) => {
2845
- void listWorkspaceStates().then(async (states) => {
2846
- for (const state of states) {
3271
+ void mutateSessionBindingStateMap((stateMap) => {
3272
+ for (const [bindingId, state] of Object.entries(stateMap)) {
2847
3273
  if (state.windowId !== windowId) {
2848
3274
  continue;
2849
3275
  }
2850
- await saveWorkspaceState({
3276
+ stateMap[bindingId] = {
2851
3277
  ...state,
2852
3278
  windowId: null,
2853
3279
  groupId: null,
2854
3280
  tabIds: [],
2855
3281
  activeTabId: null,
2856
3282
  primaryTabId: null
2857
- });
3283
+ };
2858
3284
  }
2859
3285
  });
2860
3286
  });