tabctl 0.6.0-rc.2 → 0.6.0-rc.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -167,6 +167,11 @@ tabctl query '{ reportTabs(windowId: 123) { entries { tabId title url descriptio
167
167
  # Capture screenshots
168
168
  tabctl query 'query { captureScreenshots(tabIds: [456], mode: "viewport") { entries { tabId tiles { index width height } } } }'
169
169
 
170
+ # Inspect persisted browser-state history for future restore tooling
171
+ tabctl query 'query { latestBrowserState { snapshotId reason groups { logicalGroupId title browserGroupId tabUrls } } }'
172
+ tabctl query 'query { browserStateHistory(limit: 10) { snapshotId recordedAt reason eventCount eventKinds } }'
173
+ tabctl query 'query { browserStateGroupHistory(title: "Research", limit: 10) { snapshotId logicalGroupId title browserGroupId tabUrls } }'
174
+
170
175
  # Open tabs in a new grouped window
171
176
  tabctl query 'mutation { openTabs(urls: ["https://example.com"], group: "Research", newWindow: true) { windowId groupId tabs { tabId url } } }'
172
177
 
@@ -227,6 +232,7 @@ See [CLI.md](CLI.md#configuration) for full details.
227
232
  ## Runtime state
228
233
  - Socket: `<dataDir>/tabctl.sock` (default: `~/.local/state/tabctl/tabctl.sock`)
229
234
  - Undo log: `<dataDir>/undo.jsonl` (default: `~/.local/state/tabctl/undo.jsonl`)
235
+ - Browser-state history DB: `<dataDir>/state.db` (default: `~/.local/state/tabctl/state.db`)
230
236
  - Profile registry: `<configDir>/profiles.json`
231
237
  - WSL TCP port file: `<dataDir>/tcp-port` (written by the Windows host)
232
238
 
@@ -475,6 +475,7 @@
475
475
  };
476
476
  var KEEPALIVE_ALARM = "tabctl-keepalive";
477
477
  var KEEPALIVE_INTERVAL_MINUTES = 1;
478
+ var BROWSER_STATE_SYNC_DEBOUNCE_MS = 750;
478
479
  var screenshot = require_screenshot();
479
480
  var content = require_content();
480
481
  var { delay, executeWithTimeout } = content;
@@ -488,6 +489,14 @@
488
489
  var state = {
489
490
  port: null
490
491
  };
492
+ var browserState = {
493
+ nextId: 1,
494
+ syncTimer: null,
495
+ pendingEvents: [],
496
+ incognitoWindowIds: /* @__PURE__ */ new Set(),
497
+ incognitoTabIds: /* @__PURE__ */ new Set(),
498
+ incognitoGroupIds: /* @__PURE__ */ new Set()
499
+ };
491
500
  function log(...args) {
492
501
  console.log("[tabctl]", ...args);
493
502
  }
@@ -521,10 +530,221 @@
521
530
  state.port = null;
522
531
  });
523
532
  log("Native host connected");
533
+ queueBrowserStateSync("startup");
524
534
  } catch (error) {
525
535
  log("Native host connection failed", error);
526
536
  }
527
537
  }
538
+ function nextBrowserStateId() {
539
+ const id = browserState.nextId;
540
+ browserState.nextId += 1;
541
+ return `browser-state-${Date.now()}-${id}`;
542
+ }
543
+ function normalizeEventPayload(kind, payload) {
544
+ const event = {
545
+ kind,
546
+ occurredAt: Date.now()
547
+ };
548
+ for (const [key, value] of Object.entries(payload)) {
549
+ if (value === void 0) {
550
+ continue;
551
+ }
552
+ event[key] = value;
553
+ }
554
+ if (event.incognito !== true && inferIncognitoEvent(payload)) {
555
+ event.incognito = true;
556
+ }
557
+ return event;
558
+ }
559
+ function inferIncognitoEvent(payload) {
560
+ const tabId = typeof payload.tabId === "number" ? payload.tabId : null;
561
+ if (tabId !== null && browserState.incognitoTabIds.has(tabId)) {
562
+ return true;
563
+ }
564
+ const groupId = typeof payload.groupId === "number" ? payload.groupId : null;
565
+ if (groupId !== null && browserState.incognitoGroupIds.has(groupId)) {
566
+ return true;
567
+ }
568
+ const windowId = typeof payload.windowId === "number" ? payload.windowId : null;
569
+ return windowId !== null && browserState.incognitoWindowIds.has(windowId);
570
+ }
571
+ function updateIncognitoState(snapshot) {
572
+ browserState.incognitoWindowIds.clear();
573
+ browserState.incognitoTabIds.clear();
574
+ browserState.incognitoGroupIds.clear();
575
+ for (const window2 of snapshot.windows || []) {
576
+ if (window2.incognito !== true || typeof window2.windowId !== "number") {
577
+ continue;
578
+ }
579
+ browserState.incognitoWindowIds.add(window2.windowId);
580
+ for (const tab of window2.tabs || []) {
581
+ if (typeof tab.tabId === "number") {
582
+ browserState.incognitoTabIds.add(tab.tabId);
583
+ }
584
+ }
585
+ for (const group of window2.groups || []) {
586
+ if (typeof group.groupId === "number") {
587
+ browserState.incognitoGroupIds.add(group.groupId);
588
+ }
589
+ }
590
+ }
591
+ }
592
+ async function postBrowserStateSync(reason) {
593
+ if (!state.port) {
594
+ return;
595
+ }
596
+ const eventCount = browserState.pendingEvents.length;
597
+ const events = browserState.pendingEvents.slice(0, eventCount);
598
+ try {
599
+ const snapshot = await getTabSnapshot();
600
+ updateIncognitoState(snapshot);
601
+ state.port.postMessage({
602
+ id: nextBrowserStateId(),
603
+ action: "browser-state-sync",
604
+ ok: true,
605
+ data: {
606
+ reason,
607
+ recordedAt: Date.now(),
608
+ events,
609
+ snapshot
610
+ }
611
+ });
612
+ browserState.pendingEvents.splice(0, eventCount);
613
+ } catch (error) {
614
+ log("Browser state sync failed", error);
615
+ }
616
+ }
617
+ function queueBrowserStateSync(reason) {
618
+ if (!state.port) {
619
+ connectNative();
620
+ return;
621
+ }
622
+ if (browserState.syncTimer) {
623
+ clearTimeout(browserState.syncTimer);
624
+ }
625
+ const delayMs = reason === "startup" ? 0 : BROWSER_STATE_SYNC_DEBOUNCE_MS;
626
+ browserState.syncTimer = setTimeout(() => {
627
+ browserState.syncTimer = null;
628
+ void postBrowserStateSync(reason);
629
+ }, delayMs);
630
+ }
631
+ function enqueueBrowserStateEvent(kind, payload, reason = "event") {
632
+ browserState.pendingEvents.push(normalizeEventPayload(kind, payload));
633
+ queueBrowserStateSync(reason);
634
+ }
635
+ function registerBrowserStateListeners() {
636
+ chrome.tabs?.onCreated?.addListener((tab) => {
637
+ enqueueBrowserStateEvent("tabs.onCreated", {
638
+ tabId: tab.id,
639
+ windowId: tab.windowId,
640
+ groupId: tab.groupId,
641
+ incognito: tab.incognito,
642
+ url: tab.url,
643
+ title: tab.title,
644
+ index: tab.index
645
+ });
646
+ });
647
+ chrome.tabs?.onUpdated?.addListener((tabId, changeInfo, tab) => {
648
+ const interesting = ["url", "title", "status", "pinned", "audible", "discarded", "favIconUrl"];
649
+ if (!interesting.some((key) => key in changeInfo)) {
650
+ return;
651
+ }
652
+ enqueueBrowserStateEvent("tabs.onUpdated", {
653
+ tabId,
654
+ windowId: tab.windowId,
655
+ groupId: tab.groupId,
656
+ incognito: tab.incognito,
657
+ changeInfo
658
+ });
659
+ });
660
+ chrome.tabs?.onMoved?.addListener((tabId, moveInfo) => {
661
+ enqueueBrowserStateEvent("tabs.onMoved", {
662
+ tabId,
663
+ windowId: moveInfo.windowId,
664
+ fromIndex: moveInfo.fromIndex,
665
+ toIndex: moveInfo.toIndex
666
+ });
667
+ });
668
+ chrome.tabs?.onAttached?.addListener((tabId, attachInfo) => {
669
+ enqueueBrowserStateEvent("tabs.onAttached", {
670
+ tabId,
671
+ windowId: attachInfo.newWindowId,
672
+ newPosition: attachInfo.newPosition
673
+ });
674
+ });
675
+ chrome.tabs?.onDetached?.addListener((tabId, detachInfo) => {
676
+ enqueueBrowserStateEvent("tabs.onDetached", {
677
+ tabId,
678
+ windowId: detachInfo.oldWindowId,
679
+ oldPosition: detachInfo.oldPosition
680
+ });
681
+ });
682
+ chrome.tabs?.onRemoved?.addListener((tabId, removeInfo) => {
683
+ enqueueBrowserStateEvent("tabs.onRemoved", {
684
+ tabId,
685
+ windowId: removeInfo.windowId,
686
+ isWindowClosing: removeInfo.isWindowClosing
687
+ });
688
+ });
689
+ chrome.tabs?.onActivated?.addListener((activeInfo) => {
690
+ enqueueBrowserStateEvent("tabs.onActivated", {
691
+ tabId: activeInfo.tabId,
692
+ windowId: activeInfo.windowId
693
+ });
694
+ });
695
+ chrome.tabGroups?.onCreated?.addListener((group) => {
696
+ enqueueBrowserStateEvent("tabGroups.onCreated", {
697
+ groupId: group.id,
698
+ windowId: group.windowId,
699
+ title: group.title,
700
+ color: group.color,
701
+ collapsed: group.collapsed
702
+ });
703
+ });
704
+ chrome.tabGroups?.onUpdated?.addListener((group) => {
705
+ enqueueBrowserStateEvent("tabGroups.onUpdated", {
706
+ groupId: group.id,
707
+ windowId: group.windowId,
708
+ title: group.title,
709
+ color: group.color,
710
+ collapsed: group.collapsed
711
+ });
712
+ });
713
+ chrome.tabGroups?.onMoved?.addListener((group) => {
714
+ enqueueBrowserStateEvent("tabGroups.onMoved", {
715
+ groupId: group.id,
716
+ windowId: group.windowId,
717
+ title: group.title,
718
+ color: group.color,
719
+ collapsed: group.collapsed
720
+ });
721
+ });
722
+ chrome.tabGroups?.onRemoved?.addListener((group) => {
723
+ enqueueBrowserStateEvent("tabGroups.onRemoved", {
724
+ groupId: group.id,
725
+ windowId: group.windowId,
726
+ title: group.title,
727
+ color: group.color,
728
+ collapsed: group.collapsed
729
+ });
730
+ });
731
+ chrome.windows?.onCreated?.addListener((window2) => {
732
+ enqueueBrowserStateEvent("windows.onCreated", {
733
+ windowId: window2.id,
734
+ incognito: window2.incognito,
735
+ focused: window2.focused,
736
+ state: window2.state
737
+ });
738
+ });
739
+ chrome.windows?.onRemoved?.addListener((windowId) => {
740
+ enqueueBrowserStateEvent("windows.onRemoved", { windowId });
741
+ });
742
+ chrome.windows?.onFocusChanged?.addListener((windowId) => {
743
+ enqueueBrowserStateEvent("windows.onFocusChanged", { windowId });
744
+ });
745
+ }
746
+ connectNative();
747
+ registerBrowserStateListeners();
528
748
  chrome.runtime.onInstalled.addListener(() => {
529
749
  connectNative();
530
750
  chrome.alarms.create(KEEPALIVE_ALARM, { periodInMinutes: KEEPALIVE_INTERVAL_MINUTES });
@@ -656,6 +876,7 @@
656
876
  tabId: tab.id,
657
877
  windowId: win.id,
658
878
  index: tab.index,
879
+ incognito: win.incognito || false,
659
880
  url: tab.url,
660
881
  title: tab.title,
661
882
  active: tab.active,
@@ -680,6 +901,7 @@
680
901
  return {
681
902
  windowId: win.id,
682
903
  focused: win.focused,
904
+ incognito: win.incognito || false,
683
905
  state: win.state,
684
906
  tabs,
685
907
  groups: windowGroups
@@ -19,5 +19,5 @@
19
19
  "background": {
20
20
  "service_worker": "background.js"
21
21
  },
22
- "version_name": "0.6.0-rc.2"
22
+ "version_name": "0.6.0-rc.3"
23
23
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "tabctl",
3
- "version": "0.6.0-rc.2",
3
+ "version": "0.6.0-rc.3",
4
4
  "description": "CLI tool to manage and analyze browser tabs",
5
5
  "license": "MIT",
6
6
  "repository": {