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 +6 -0
- package/dist/extension/background.js +222 -0
- package/dist/extension/manifest.json +1 -1
- package/package.json +1 -1
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
|