tabctl 0.6.0-rc.2 → 0.6.0-rc.4
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 +41 -15
- package/dist/extension/background.js +317 -16
- package/dist/extension/manifest.json +1 -1
- package/package.json +2 -2
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,22 +232,26 @@ 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
|
+
- Windows pipe endpoint file: `<dataDir>/pipe-endpoint`
|
|
232
238
|
|
|
233
239
|
## Windows + WSL transport
|
|
234
240
|
|
|
235
|
-
On Windows, the host exposes a
|
|
241
|
+
On Windows, the host exposes a named-pipe endpoint model:
|
|
236
242
|
- Windows native clients use a named pipe endpoint (`\\.\pipe\tabctl-<hash>`).
|
|
237
|
-
- WSL/Linux clients use
|
|
243
|
+
- WSL/Linux clients use a Windows named-pipe bridge; the Windows host publishes `<dataDir>/pipe-endpoint`, and the WSL CLI relays through `powershell.exe`.
|
|
238
244
|
|
|
239
245
|
WSL endpoint discovery (CLI):
|
|
240
|
-
1. `TABCTL_SOCKET` (explicit endpoint)
|
|
241
|
-
2. `
|
|
242
|
-
|
|
243
|
-
|
|
246
|
+
1. `TABCTL_SOCKET` (explicit endpoint).
|
|
247
|
+
2. `pipe-endpoint` file discovery from resolved data dir (and equivalent `/mnt/c/Users/*/.../tabctl/.../pipe-endpoint` locations).
|
|
248
|
+
|
|
249
|
+
WSL named-pipe mode:
|
|
250
|
+
- This is the default WSL transport now.
|
|
251
|
+
- The CLI discovers the pipe endpoint from `<dataDir>/pipe-endpoint` (including the mirrored `/mnt/c/Users/*/...` candidate paths used for other WSL bridge files).
|
|
252
|
+
- TCP is disabled for the WSL transport path.
|
|
244
253
|
|
|
245
|
-
Relevant knobs: `TABCTL_SOCKET`, `
|
|
254
|
+
Relevant knobs: `TABCTL_SOCKET`, `TABCTL_PROFILE`, `TABCTL_DATA_DIR`, `TABCTL_STATE_DIR`, `TABCTL_CONFIG_DIR`.
|
|
246
255
|
|
|
247
256
|
## Troubleshooting (setup/ping on Windows + WSL)
|
|
248
257
|
|
|
@@ -252,9 +261,9 @@ Relevant knobs: `TABCTL_SOCKET`, `TABCTL_TCP_PORT`, `TABCTL_PROFILE`, `TABCTL_DA
|
|
|
252
261
|
- Runtime command runs can auto-sync extension files when host/extension versions drift; rerun `tabctl query 'mutation { reloadExtension { reloading } }'` if the browser does not pick up changes immediately.
|
|
253
262
|
- For local release-like testing while developing, force runtime sync behavior with `TABCTL_AUTO_SYNC_MODE=release-like`.
|
|
254
263
|
- Disable runtime sync entirely with `TABCTL_AUTO_SYNC_MODE=off`.
|
|
255
|
-
- `tabctl ping --json` is the
|
|
256
|
-
- Version metadata is intentionally health-only: regular GraphQL payloads do not include version fields unless you explicitly query health surfaces
|
|
257
|
-
- `tabctl ping` returns connect errors (`ENOENT`, `ECONNREFUSED`, timeout): ensure extension is loaded and active, rerun `tabctl setup`, and in WSL verify
|
|
264
|
+
- `tabctl ping --json` is a host connectivity/health check; use it to confirm the native host is reachable and healthy.
|
|
265
|
+
- Version metadata is intentionally health-only: regular GraphQL payloads do not include version fields unless you explicitly query health surfaces, which may expose fields such as `versionsInSync`, `hostBaseVersion`, and `baseVersion`.
|
|
266
|
+
- `tabctl ping` returns connect errors (`ENOENT`, `ECONNREFUSED`, timeout): ensure extension is loaded and active, rerun `tabctl setup`, and in WSL verify the profile data dir contains a current `pipe-endpoint` file.
|
|
258
267
|
- `tabctl doctor --fix --json` includes per-profile connectivity diagnostics in `data.profiles[].connectivity`; if ping remains unhealthy after local repairs, follow `manualSteps`.
|
|
259
268
|
|
|
260
269
|
Local release-like sync test recipe:
|
|
@@ -265,7 +274,7 @@ tabctl setup --browser edge --extension-id <extension-id> --release-tag v0.5.2
|
|
|
265
274
|
# 2) Run the current binary with forced release-like auto-sync
|
|
266
275
|
TABCTL_AUTO_SYNC_MODE=release-like cargo run --manifest-path rust/Cargo.toml -p tabctl -- query '{ tabs { total } }'
|
|
267
276
|
|
|
268
|
-
# 3) Verify host/
|
|
277
|
+
# 3) Verify host connectivity/health after auto-sync
|
|
269
278
|
tabctl ping --json
|
|
270
279
|
```
|
|
271
280
|
|
|
@@ -317,8 +326,7 @@ Policy is shared across all profiles.
|
|
|
317
326
|
## Security
|
|
318
327
|
- The native host is locked to your extension ID.
|
|
319
328
|
- All data stays local; no external API keys are used.
|
|
320
|
-
-
|
|
321
|
-
- TCP transport is available on all platforms via `TABCTL_HOST_TCP=1` (host) and `TABCTL_TRANSPORT=tcp` (CLI). All TCP connections are authenticated. See [CLI.md](CLI.md) for details.
|
|
329
|
+
- WSL ↔ Windows communication uses a local named-pipe bridge via `powershell.exe`; no TCP fallback is used on that path.
|
|
322
330
|
|
|
323
331
|
## Development
|
|
324
332
|
|
|
@@ -336,8 +344,20 @@ npm test # unit tests
|
|
|
336
344
|
Rust-only validation:
|
|
337
345
|
```bash
|
|
338
346
|
npm run rust:verify
|
|
347
|
+
npm run check:targets # local cross-target cfg/type check
|
|
348
|
+
```
|
|
349
|
+
|
|
350
|
+
On macOS, `npm run check:targets` can use Zig for the C cross-compiler needed
|
|
351
|
+
by `libsqlite3-sys`:
|
|
352
|
+
|
|
353
|
+
```bash
|
|
354
|
+
brew install zig
|
|
339
355
|
```
|
|
340
356
|
|
|
357
|
+
The script auto-detects Zig outside CI and wires the Linux/Windows C compiler
|
|
358
|
+
environment for the check. The pre-push hook does not run this optional check;
|
|
359
|
+
run it manually when you want local cross-target coverage.
|
|
360
|
+
|
|
341
361
|
Browser-backed integration harness (requires built dist artifacts and Chrome):
|
|
342
362
|
```bash
|
|
343
363
|
npm run test:integration
|
|
@@ -363,8 +383,14 @@ Pre-release staging flow:
|
|
|
363
383
|
- `bump:rc` promotes alpha to `x.y.z-rc.1` (or increments RC)
|
|
364
384
|
- `bump:stable` drops the prerelease suffix for final stable publish
|
|
365
385
|
|
|
366
|
-
Release
|
|
386
|
+
Release automation:
|
|
387
|
+
- Run the **Prepare Release** workflow to choose or auto-detect the next version and open a release PR.
|
|
388
|
+
- When that PR merges, **Tag Release** creates `v<version>` and dispatches **Release**.
|
|
389
|
+
- The root `package.json` version and `optionalDependencies.tabctl-win32-x64` are kept in sync by `scripts/bump-version.js`.
|
|
390
|
+
|
|
391
|
+
Release publishing (`.github/workflows/release.yml`) supports both tag pushes and explicit workflow dispatch, and enforces:
|
|
367
392
|
- Git tag must match `package.json` version (`v<version>`)
|
|
393
|
+
- `package.json.optionalDependencies["tabctl-win32-x64"]` must match `package.json` version
|
|
368
394
|
- prerelease tags publish to `alpha`/`rc`; stable publishes to `latest`
|
|
369
395
|
- `npm run build` and `npm test` must pass before publish
|
|
370
396
|
- release assets include `tabctl-extension.zip` plus `tabctl-extension.zip.sha256`
|
|
@@ -474,7 +474,13 @@
|
|
|
474
474
|
dirty: parsed.dirty
|
|
475
475
|
};
|
|
476
476
|
var KEEPALIVE_ALARM = "tabctl-keepalive";
|
|
477
|
+
var RECONNECT_ALARM = "tabctl-reconnect";
|
|
477
478
|
var KEEPALIVE_INTERVAL_MINUTES = 1;
|
|
479
|
+
var BROWSER_STATE_SYNC_DEBOUNCE_MS = 750;
|
|
480
|
+
var RECONNECT_INITIAL_DELAY_MS = 250;
|
|
481
|
+
var RECONNECT_MAX_DELAY_MS = 3e4;
|
|
482
|
+
var RECONNECT_ALARM_MIN_DELAY_MS = 3e4;
|
|
483
|
+
var RECONNECT_STABLE_RESET_MS = 5e3;
|
|
478
484
|
var screenshot = require_screenshot();
|
|
479
485
|
var content = require_content();
|
|
480
486
|
var { delay, executeWithTimeout } = content;
|
|
@@ -486,22 +492,87 @@
|
|
|
486
492
|
return n;
|
|
487
493
|
}
|
|
488
494
|
var state = {
|
|
489
|
-
port: null
|
|
495
|
+
port: null,
|
|
496
|
+
reconnectTimer: null,
|
|
497
|
+
reconnectStableTimer: null,
|
|
498
|
+
reconnectAttempt: 0
|
|
499
|
+
};
|
|
500
|
+
var browserState = {
|
|
501
|
+
nextId: 1,
|
|
502
|
+
syncTimer: null,
|
|
503
|
+
pendingEvents: [],
|
|
504
|
+
incognitoWindowIds: /* @__PURE__ */ new Set(),
|
|
505
|
+
incognitoTabIds: /* @__PURE__ */ new Set(),
|
|
506
|
+
incognitoGroupIds: /* @__PURE__ */ new Set()
|
|
490
507
|
};
|
|
491
508
|
function log(...args) {
|
|
492
509
|
console.log("[tabctl]", ...args);
|
|
493
510
|
}
|
|
494
|
-
function
|
|
495
|
-
|
|
511
|
+
function reconnectDelayMs(attempt) {
|
|
512
|
+
return Math.min(RECONNECT_INITIAL_DELAY_MS * 2 ** attempt, RECONNECT_MAX_DELAY_MS);
|
|
513
|
+
}
|
|
514
|
+
function clearReconnectTimer() {
|
|
515
|
+
if (!state.reconnectTimer) {
|
|
516
|
+
return;
|
|
517
|
+
}
|
|
518
|
+
clearTimeout(state.reconnectTimer);
|
|
519
|
+
state.reconnectTimer = null;
|
|
520
|
+
}
|
|
521
|
+
function clearReconnectAlarm() {
|
|
522
|
+
chrome.alarms.clear(RECONNECT_ALARM);
|
|
523
|
+
}
|
|
524
|
+
function clearReconnectStableTimer() {
|
|
525
|
+
if (!state.reconnectStableTimer) {
|
|
496
526
|
return;
|
|
497
527
|
}
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
528
|
+
clearTimeout(state.reconnectStableTimer);
|
|
529
|
+
state.reconnectStableTimer = null;
|
|
530
|
+
}
|
|
531
|
+
function scheduleReconnect(reason) {
|
|
532
|
+
if (state.port || state.reconnectTimer) {
|
|
533
|
+
return;
|
|
534
|
+
}
|
|
535
|
+
const attempt = state.reconnectAttempt;
|
|
536
|
+
const delayMs = reconnectDelayMs(attempt);
|
|
537
|
+
state.reconnectAttempt += 1;
|
|
538
|
+
log("Scheduling native host reconnect", { reason, delayMs, attempt });
|
|
539
|
+
chrome.alarms.create(RECONNECT_ALARM, {
|
|
540
|
+
delayInMinutes: Math.max(delayMs, RECONNECT_ALARM_MIN_DELAY_MS) / 6e4
|
|
541
|
+
});
|
|
542
|
+
state.reconnectTimer = setTimeout(() => {
|
|
543
|
+
state.reconnectTimer = null;
|
|
544
|
+
clearReconnectAlarm();
|
|
545
|
+
connectNative();
|
|
546
|
+
}, delayMs);
|
|
547
|
+
}
|
|
548
|
+
function resetReconnectBackoffAfterStablePort(port) {
|
|
549
|
+
clearReconnectStableTimer();
|
|
550
|
+
state.reconnectStableTimer = setTimeout(() => {
|
|
551
|
+
state.reconnectStableTimer = null;
|
|
552
|
+
if (state.port === port) {
|
|
553
|
+
state.reconnectAttempt = 0;
|
|
554
|
+
}
|
|
555
|
+
}, RECONNECT_STABLE_RESET_MS);
|
|
556
|
+
}
|
|
557
|
+
function ensureKeepaliveAlarm() {
|
|
558
|
+
chrome.alarms.create(KEEPALIVE_ALARM, { periodInMinutes: KEEPALIVE_INTERVAL_MINUTES });
|
|
559
|
+
}
|
|
560
|
+
function sendResponse(port, id, ok, payload) {
|
|
561
|
+
if (!port) {
|
|
562
|
+
log("dropping response because native port is unavailable", { id, ok });
|
|
501
563
|
return;
|
|
502
564
|
}
|
|
503
|
-
|
|
504
|
-
|
|
565
|
+
try {
|
|
566
|
+
if (ok) {
|
|
567
|
+
const data = typeof payload === "object" && payload !== null ? payload : { payload };
|
|
568
|
+
port.postMessage({ id, ok: true, data });
|
|
569
|
+
return;
|
|
570
|
+
}
|
|
571
|
+
const error = payload instanceof Error ? { message: payload.message, stack: payload.stack } : payload;
|
|
572
|
+
port.postMessage({ id, ok: false, error });
|
|
573
|
+
} catch (error) {
|
|
574
|
+
log("failed to send native response", { id, ok, error });
|
|
575
|
+
}
|
|
505
576
|
}
|
|
506
577
|
function connectNative() {
|
|
507
578
|
if (state.port) {
|
|
@@ -509,8 +580,13 @@
|
|
|
509
580
|
}
|
|
510
581
|
try {
|
|
511
582
|
const port = chrome.runtime.connectNative(HOST_NAME);
|
|
583
|
+
clearReconnectTimer();
|
|
584
|
+
clearReconnectAlarm();
|
|
512
585
|
state.port = port;
|
|
513
|
-
port
|
|
586
|
+
resetReconnectBackoffAfterStablePort(port);
|
|
587
|
+
port.onMessage.addListener((message) => {
|
|
588
|
+
void handleNativeMessage(port, message);
|
|
589
|
+
});
|
|
514
590
|
port.onDisconnect.addListener(() => {
|
|
515
591
|
const lastError = chrome.runtime.lastError;
|
|
516
592
|
if (lastError) {
|
|
@@ -518,27 +594,247 @@
|
|
|
518
594
|
} else {
|
|
519
595
|
log("Native host disconnected");
|
|
520
596
|
}
|
|
521
|
-
state.port
|
|
597
|
+
if (state.port === port) {
|
|
598
|
+
state.port = null;
|
|
599
|
+
}
|
|
600
|
+
clearReconnectStableTimer();
|
|
601
|
+
scheduleReconnect("disconnect");
|
|
522
602
|
});
|
|
523
603
|
log("Native host connected");
|
|
604
|
+
queueBrowserStateSync("startup");
|
|
524
605
|
} catch (error) {
|
|
525
606
|
log("Native host connection failed", error);
|
|
607
|
+
scheduleReconnect("connect-failed");
|
|
608
|
+
}
|
|
609
|
+
}
|
|
610
|
+
function nextBrowserStateId() {
|
|
611
|
+
const id = browserState.nextId;
|
|
612
|
+
browserState.nextId += 1;
|
|
613
|
+
return `browser-state-${Date.now()}-${id}`;
|
|
614
|
+
}
|
|
615
|
+
function normalizeEventPayload(kind, payload) {
|
|
616
|
+
const event = {
|
|
617
|
+
kind,
|
|
618
|
+
occurredAt: Date.now()
|
|
619
|
+
};
|
|
620
|
+
for (const [key, value] of Object.entries(payload)) {
|
|
621
|
+
if (value === void 0) {
|
|
622
|
+
continue;
|
|
623
|
+
}
|
|
624
|
+
event[key] = value;
|
|
625
|
+
}
|
|
626
|
+
if (event.incognito !== true && inferIncognitoEvent(payload)) {
|
|
627
|
+
event.incognito = true;
|
|
628
|
+
}
|
|
629
|
+
return event;
|
|
630
|
+
}
|
|
631
|
+
function inferIncognitoEvent(payload) {
|
|
632
|
+
const tabId = typeof payload.tabId === "number" ? payload.tabId : null;
|
|
633
|
+
if (tabId !== null && browserState.incognitoTabIds.has(tabId)) {
|
|
634
|
+
return true;
|
|
635
|
+
}
|
|
636
|
+
const groupId = typeof payload.groupId === "number" ? payload.groupId : null;
|
|
637
|
+
if (groupId !== null && browserState.incognitoGroupIds.has(groupId)) {
|
|
638
|
+
return true;
|
|
639
|
+
}
|
|
640
|
+
const windowId = typeof payload.windowId === "number" ? payload.windowId : null;
|
|
641
|
+
return windowId !== null && browserState.incognitoWindowIds.has(windowId);
|
|
642
|
+
}
|
|
643
|
+
function updateIncognitoState(snapshot) {
|
|
644
|
+
browserState.incognitoWindowIds.clear();
|
|
645
|
+
browserState.incognitoTabIds.clear();
|
|
646
|
+
browserState.incognitoGroupIds.clear();
|
|
647
|
+
for (const window2 of snapshot.windows || []) {
|
|
648
|
+
if (window2.incognito !== true || typeof window2.windowId !== "number") {
|
|
649
|
+
continue;
|
|
650
|
+
}
|
|
651
|
+
browserState.incognitoWindowIds.add(window2.windowId);
|
|
652
|
+
for (const tab of window2.tabs || []) {
|
|
653
|
+
if (typeof tab.tabId === "number") {
|
|
654
|
+
browserState.incognitoTabIds.add(tab.tabId);
|
|
655
|
+
}
|
|
656
|
+
}
|
|
657
|
+
for (const group of window2.groups || []) {
|
|
658
|
+
if (typeof group.groupId === "number") {
|
|
659
|
+
browserState.incognitoGroupIds.add(group.groupId);
|
|
660
|
+
}
|
|
661
|
+
}
|
|
526
662
|
}
|
|
527
663
|
}
|
|
664
|
+
async function postBrowserStateSync(reason) {
|
|
665
|
+
if (!state.port) {
|
|
666
|
+
return;
|
|
667
|
+
}
|
|
668
|
+
const eventCount = browserState.pendingEvents.length;
|
|
669
|
+
const events = browserState.pendingEvents.slice(0, eventCount);
|
|
670
|
+
try {
|
|
671
|
+
const snapshot = await getTabSnapshot();
|
|
672
|
+
updateIncognitoState(snapshot);
|
|
673
|
+
state.port.postMessage({
|
|
674
|
+
id: nextBrowserStateId(),
|
|
675
|
+
action: "browser-state-sync",
|
|
676
|
+
ok: true,
|
|
677
|
+
data: {
|
|
678
|
+
reason,
|
|
679
|
+
recordedAt: Date.now(),
|
|
680
|
+
events,
|
|
681
|
+
snapshot
|
|
682
|
+
}
|
|
683
|
+
});
|
|
684
|
+
browserState.pendingEvents.splice(0, eventCount);
|
|
685
|
+
} catch (error) {
|
|
686
|
+
log("Browser state sync failed", error);
|
|
687
|
+
}
|
|
688
|
+
}
|
|
689
|
+
function queueBrowserStateSync(reason) {
|
|
690
|
+
if (!state.port) {
|
|
691
|
+
connectNative();
|
|
692
|
+
return;
|
|
693
|
+
}
|
|
694
|
+
if (browserState.syncTimer) {
|
|
695
|
+
clearTimeout(browserState.syncTimer);
|
|
696
|
+
}
|
|
697
|
+
const delayMs = reason === "startup" ? 0 : BROWSER_STATE_SYNC_DEBOUNCE_MS;
|
|
698
|
+
browserState.syncTimer = setTimeout(() => {
|
|
699
|
+
browserState.syncTimer = null;
|
|
700
|
+
void postBrowserStateSync(reason);
|
|
701
|
+
}, delayMs);
|
|
702
|
+
}
|
|
703
|
+
function enqueueBrowserStateEvent(kind, payload, reason = "event") {
|
|
704
|
+
browserState.pendingEvents.push(normalizeEventPayload(kind, payload));
|
|
705
|
+
queueBrowserStateSync(reason);
|
|
706
|
+
}
|
|
707
|
+
function registerBrowserStateListeners() {
|
|
708
|
+
chrome.tabs?.onCreated?.addListener((tab) => {
|
|
709
|
+
enqueueBrowserStateEvent("tabs.onCreated", {
|
|
710
|
+
tabId: tab.id,
|
|
711
|
+
windowId: tab.windowId,
|
|
712
|
+
groupId: tab.groupId,
|
|
713
|
+
incognito: tab.incognito,
|
|
714
|
+
url: tab.url || tab.pendingUrl,
|
|
715
|
+
title: tab.title,
|
|
716
|
+
index: tab.index
|
|
717
|
+
});
|
|
718
|
+
});
|
|
719
|
+
chrome.tabs?.onUpdated?.addListener((tabId, changeInfo, tab) => {
|
|
720
|
+
const interesting = ["url", "title", "status", "pinned", "audible", "discarded", "favIconUrl"];
|
|
721
|
+
if (!interesting.some((key) => key in changeInfo)) {
|
|
722
|
+
return;
|
|
723
|
+
}
|
|
724
|
+
enqueueBrowserStateEvent("tabs.onUpdated", {
|
|
725
|
+
tabId,
|
|
726
|
+
windowId: tab.windowId,
|
|
727
|
+
groupId: tab.groupId,
|
|
728
|
+
incognito: tab.incognito,
|
|
729
|
+
changeInfo
|
|
730
|
+
});
|
|
731
|
+
});
|
|
732
|
+
chrome.tabs?.onMoved?.addListener((tabId, moveInfo) => {
|
|
733
|
+
enqueueBrowserStateEvent("tabs.onMoved", {
|
|
734
|
+
tabId,
|
|
735
|
+
windowId: moveInfo.windowId,
|
|
736
|
+
fromIndex: moveInfo.fromIndex,
|
|
737
|
+
toIndex: moveInfo.toIndex
|
|
738
|
+
});
|
|
739
|
+
});
|
|
740
|
+
chrome.tabs?.onAttached?.addListener((tabId, attachInfo) => {
|
|
741
|
+
enqueueBrowserStateEvent("tabs.onAttached", {
|
|
742
|
+
tabId,
|
|
743
|
+
windowId: attachInfo.newWindowId,
|
|
744
|
+
newPosition: attachInfo.newPosition
|
|
745
|
+
});
|
|
746
|
+
});
|
|
747
|
+
chrome.tabs?.onDetached?.addListener((tabId, detachInfo) => {
|
|
748
|
+
enqueueBrowserStateEvent("tabs.onDetached", {
|
|
749
|
+
tabId,
|
|
750
|
+
windowId: detachInfo.oldWindowId,
|
|
751
|
+
oldPosition: detachInfo.oldPosition
|
|
752
|
+
});
|
|
753
|
+
});
|
|
754
|
+
chrome.tabs?.onRemoved?.addListener((tabId, removeInfo) => {
|
|
755
|
+
enqueueBrowserStateEvent("tabs.onRemoved", {
|
|
756
|
+
tabId,
|
|
757
|
+
windowId: removeInfo.windowId,
|
|
758
|
+
isWindowClosing: removeInfo.isWindowClosing
|
|
759
|
+
});
|
|
760
|
+
});
|
|
761
|
+
chrome.tabs?.onActivated?.addListener((activeInfo) => {
|
|
762
|
+
enqueueBrowserStateEvent("tabs.onActivated", {
|
|
763
|
+
tabId: activeInfo.tabId,
|
|
764
|
+
windowId: activeInfo.windowId
|
|
765
|
+
});
|
|
766
|
+
});
|
|
767
|
+
chrome.tabGroups?.onCreated?.addListener((group) => {
|
|
768
|
+
enqueueBrowserStateEvent("tabGroups.onCreated", {
|
|
769
|
+
groupId: group.id,
|
|
770
|
+
windowId: group.windowId,
|
|
771
|
+
title: group.title,
|
|
772
|
+
color: group.color,
|
|
773
|
+
collapsed: group.collapsed
|
|
774
|
+
});
|
|
775
|
+
});
|
|
776
|
+
chrome.tabGroups?.onUpdated?.addListener((group) => {
|
|
777
|
+
enqueueBrowserStateEvent("tabGroups.onUpdated", {
|
|
778
|
+
groupId: group.id,
|
|
779
|
+
windowId: group.windowId,
|
|
780
|
+
title: group.title,
|
|
781
|
+
color: group.color,
|
|
782
|
+
collapsed: group.collapsed
|
|
783
|
+
});
|
|
784
|
+
});
|
|
785
|
+
chrome.tabGroups?.onMoved?.addListener((group) => {
|
|
786
|
+
enqueueBrowserStateEvent("tabGroups.onMoved", {
|
|
787
|
+
groupId: group.id,
|
|
788
|
+
windowId: group.windowId,
|
|
789
|
+
title: group.title,
|
|
790
|
+
color: group.color,
|
|
791
|
+
collapsed: group.collapsed
|
|
792
|
+
});
|
|
793
|
+
});
|
|
794
|
+
chrome.tabGroups?.onRemoved?.addListener((group) => {
|
|
795
|
+
enqueueBrowserStateEvent("tabGroups.onRemoved", {
|
|
796
|
+
groupId: group.id,
|
|
797
|
+
windowId: group.windowId,
|
|
798
|
+
title: group.title,
|
|
799
|
+
color: group.color,
|
|
800
|
+
collapsed: group.collapsed
|
|
801
|
+
});
|
|
802
|
+
});
|
|
803
|
+
chrome.windows?.onCreated?.addListener((window2) => {
|
|
804
|
+
enqueueBrowserStateEvent("windows.onCreated", {
|
|
805
|
+
windowId: window2.id,
|
|
806
|
+
incognito: window2.incognito,
|
|
807
|
+
focused: window2.focused,
|
|
808
|
+
state: window2.state
|
|
809
|
+
});
|
|
810
|
+
});
|
|
811
|
+
chrome.windows?.onRemoved?.addListener((windowId) => {
|
|
812
|
+
enqueueBrowserStateEvent("windows.onRemoved", { windowId });
|
|
813
|
+
});
|
|
814
|
+
chrome.windows?.onFocusChanged?.addListener((windowId) => {
|
|
815
|
+
enqueueBrowserStateEvent("windows.onFocusChanged", { windowId });
|
|
816
|
+
});
|
|
817
|
+
}
|
|
818
|
+
connectNative();
|
|
819
|
+
registerBrowserStateListeners();
|
|
820
|
+
ensureKeepaliveAlarm();
|
|
528
821
|
chrome.runtime.onInstalled.addListener(() => {
|
|
529
822
|
connectNative();
|
|
530
|
-
|
|
823
|
+
ensureKeepaliveAlarm();
|
|
531
824
|
});
|
|
532
825
|
chrome.runtime.onStartup.addListener(() => {
|
|
533
826
|
connectNative();
|
|
534
|
-
|
|
827
|
+
ensureKeepaliveAlarm();
|
|
535
828
|
});
|
|
536
829
|
chrome.alarms.onAlarm.addListener((alarm) => {
|
|
537
830
|
if (alarm.name === KEEPALIVE_ALARM) {
|
|
538
831
|
connectNative();
|
|
832
|
+
} else if (alarm.name === RECONNECT_ALARM) {
|
|
833
|
+
clearReconnectTimer();
|
|
834
|
+
connectNative();
|
|
539
835
|
}
|
|
540
836
|
});
|
|
541
|
-
async function handleNativeMessage(message) {
|
|
837
|
+
async function handleNativeMessage(requestPort, message) {
|
|
542
838
|
if (!message || typeof message !== "object") {
|
|
543
839
|
return;
|
|
544
840
|
}
|
|
@@ -548,9 +844,9 @@
|
|
|
548
844
|
}
|
|
549
845
|
try {
|
|
550
846
|
const data = await handleAction(action, params || {}, id);
|
|
551
|
-
sendResponse(id, true, data);
|
|
847
|
+
sendResponse(requestPort, id, true, data);
|
|
552
848
|
} catch (error) {
|
|
553
|
-
sendResponse(id, false, error);
|
|
849
|
+
sendResponse(requestPort, id, false, error);
|
|
554
850
|
}
|
|
555
851
|
}
|
|
556
852
|
async function handleAction(action, params, requestId) {
|
|
@@ -570,6 +866,9 @@
|
|
|
570
866
|
component: "extension"
|
|
571
867
|
};
|
|
572
868
|
case "reload":
|
|
869
|
+
chrome.alarms.create(RECONNECT_ALARM, {
|
|
870
|
+
delayInMinutes: RECONNECT_ALARM_MIN_DELAY_MS / 6e4
|
|
871
|
+
});
|
|
573
872
|
setTimeout(() => chrome.runtime.reload(), 100);
|
|
574
873
|
return { reloading: true };
|
|
575
874
|
// --- Primitives: thin Chrome API wrappers (p: prefix) ---
|
|
@@ -656,7 +955,8 @@
|
|
|
656
955
|
tabId: tab.id,
|
|
657
956
|
windowId: win.id,
|
|
658
957
|
index: tab.index,
|
|
659
|
-
|
|
958
|
+
incognito: win.incognito || false,
|
|
959
|
+
url: tab.url || tab.pendingUrl,
|
|
660
960
|
title: tab.title,
|
|
661
961
|
active: tab.active,
|
|
662
962
|
pinned: tab.pinned,
|
|
@@ -680,6 +980,7 @@
|
|
|
680
980
|
return {
|
|
681
981
|
windowId: win.id,
|
|
682
982
|
focused: win.focused,
|
|
983
|
+
incognito: win.incognito || false,
|
|
683
984
|
state: win.state,
|
|
684
985
|
tabs,
|
|
685
986
|
groups: windowGroups
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "tabctl",
|
|
3
|
-
"version": "0.6.0-rc.
|
|
3
|
+
"version": "0.6.0-rc.4",
|
|
4
4
|
"description": "CLI tool to manage and analyze browser tabs",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"repository": {
|
|
@@ -52,7 +52,7 @@
|
|
|
52
52
|
"typescript": "^5.4.5"
|
|
53
53
|
},
|
|
54
54
|
"optionalDependencies": {
|
|
55
|
-
"tabctl-win32-x64": "0.
|
|
55
|
+
"tabctl-win32-x64": "0.6.0-rc.4"
|
|
56
56
|
},
|
|
57
57
|
"dependencies": {
|
|
58
58
|
"normalize-url": "^8.1.1"
|