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 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
- - WSL TCP port file: `<dataDir>/tcp-port` (written by the Windows host)
237
+ - Windows pipe endpoint file: `<dataDir>/pipe-endpoint`
232
238
 
233
239
  ## Windows + WSL transport
234
240
 
235
- On Windows, the host exposes a dual endpoint model:
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 `tcp://127.0.0.1:<port>`, with the host writing `<dataDir>/tcp-port`.
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); if this is a pipe endpoint in WSL, CLI still prefers discovered TCP.
241
- 2. `TABCTL_TCP_PORT` (forces `127.0.0.1:<port>`).
242
- 3. `tcp-port` file discovery from resolved data dir (and equivalent `/mnt/c/Users/*/.../tabctl/.../tcp-port` locations).
243
- 4. Fallback: `tcp://127.0.0.1:38000`.
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`, `TABCTL_TCP_PORT`, `TABCTL_PROFILE`, `TABCTL_DATA_DIR`, `TABCTL_STATE_DIR`, `TABCTL_CONFIG_DIR`.
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 canonical runtime version check (`versionsInSync`, `hostBaseVersion`, `baseVersion`).
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 `TABCTL_TCP_PORT` or `<dataDir>/tcp-port` matches a listening localhost port.
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/extension base versions are back in sync
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
- - TCP connections (used for WSL ↔ Windows communication) are secured with a per-session auth token. The host generates a random token on startup; the CLI reads it automatically. See [CLI.md](CLI.md) for details.
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 publishing (`.github/workflows/publish.yml`) enforces:
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 sendResponse(id, ok, payload) {
495
- if (!state.port) {
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
- if (ok) {
499
- const data = typeof payload === "object" && payload !== null ? payload : { payload };
500
- state.port.postMessage({ id, ok: true, data });
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
- const error = payload instanceof Error ? { message: payload.message, stack: payload.stack } : payload;
504
- state.port.postMessage({ id, ok: false, error });
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.onMessage.addListener(handleNativeMessage);
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 = null;
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
- chrome.alarms.create(KEEPALIVE_ALARM, { periodInMinutes: KEEPALIVE_INTERVAL_MINUTES });
823
+ ensureKeepaliveAlarm();
531
824
  });
532
825
  chrome.runtime.onStartup.addListener(() => {
533
826
  connectNative();
534
- chrome.alarms.create(KEEPALIVE_ALARM, { periodInMinutes: KEEPALIVE_INTERVAL_MINUTES });
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
- url: tab.url,
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
@@ -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.4"
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.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.3.0"
55
+ "tabctl-win32-x64": "0.6.0-rc.4"
56
56
  },
57
57
  "dependencies": {
58
58
  "normalize-url": "^8.1.1"