@quanta-intellect/vessel-browser 0.1.53 → 0.1.58

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/out/main/index.js CHANGED
@@ -4,12 +4,12 @@ const fs$1 = require("node:fs");
4
4
  const path = require("path");
5
5
  const fs = require("fs");
6
6
  const crypto$1 = require("crypto");
7
- const crypto$2 = require("node:crypto");
8
- const path$1 = require("node:path");
9
7
  const Anthropic = require("@anthropic-ai/sdk");
10
8
  const OpenAI = require("openai");
9
+ const path$1 = require("node:path");
11
10
  const node_module = require("node:module");
12
11
  const zod = require("zod");
12
+ const crypto$2 = require("node:crypto");
13
13
  const http = require("node:http");
14
14
  const os = require("node:os");
15
15
  const mcp_js = require("@modelcontextprotocol/sdk/server/mcp.js");
@@ -38,13 +38,13 @@ const defaults = {
38
38
  expiresAt: ""
39
39
  }
40
40
  };
41
- const SAVE_DEBOUNCE_MS$3 = 150;
41
+ const SAVE_DEBOUNCE_MS$5 = 150;
42
42
  const CHAT_PROVIDER_SECRET_FILENAME = "vessel-chat-provider-secret";
43
43
  const SETTABLE_KEYS = new Set(Object.keys(defaults));
44
44
  let settings = null;
45
45
  let settingsIssues = [];
46
- let saveTimer$3 = null;
47
- let saveDirty$3 = false;
46
+ let saveTimer = null;
47
+ let saveDirty = false;
48
48
  function getUserDataPath() {
49
49
  if (typeof electron.app?.getPath === "function") {
50
50
  return electron.app.getPath("userData");
@@ -57,7 +57,7 @@ function getSettingsPath() {
57
57
  function getChatProviderSecretPath() {
58
58
  return path.join(getUserDataPath(), CHAT_PROVIDER_SECRET_FILENAME);
59
59
  }
60
- function canUseSafeStorage() {
60
+ function canUseSafeStorage$1() {
61
61
  try {
62
62
  return electron.safeStorage.isEncryptionAvailable();
63
63
  } catch {
@@ -67,7 +67,7 @@ function canUseSafeStorage() {
67
67
  function readStoredProviderSecret() {
68
68
  try {
69
69
  const raw = fs.readFileSync(getChatProviderSecretPath());
70
- const decoded = canUseSafeStorage() && electron.safeStorage.decryptString ? electron.safeStorage.decryptString(raw) : raw.toString("utf-8");
70
+ const decoded = canUseSafeStorage$1() && electron.safeStorage.decryptString ? electron.safeStorage.decryptString(raw) : raw.toString("utf-8");
71
71
  const parsed = JSON.parse(decoded);
72
72
  if (parsed && typeof parsed === "object" && typeof parsed.providerId === "string" && typeof parsed.apiKey === "string") {
73
73
  return parsed;
@@ -80,7 +80,7 @@ function writeStoredProviderSecret(secret) {
80
80
  const filePath = getChatProviderSecretPath();
81
81
  fs.mkdirSync(path.dirname(filePath), { recursive: true });
82
82
  const payload = JSON.stringify(secret);
83
- if (canUseSafeStorage()) {
83
+ if (canUseSafeStorage$1()) {
84
84
  const encrypted = electron.safeStorage.encryptString(payload);
85
85
  fs.writeFileSync(filePath, encrypted, { mode: 384 });
86
86
  return;
@@ -174,11 +174,11 @@ function loadSettings() {
174
174
  }
175
175
  return settings;
176
176
  }
177
- function persistNow$3() {
178
- saveDirty$3 = false;
179
- if (saveTimer$3) {
180
- clearTimeout(saveTimer$3);
181
- saveTimer$3 = null;
177
+ function persistNow() {
178
+ saveDirty = false;
179
+ if (saveTimer) {
180
+ clearTimeout(saveTimer);
181
+ saveTimer = null;
182
182
  }
183
183
  return fs.promises.mkdir(path.dirname(getSettingsPath()), { recursive: true }).then(
184
184
  () => fs.promises.writeFile(
@@ -188,14 +188,14 @@ function persistNow$3() {
188
188
  ).catch((err) => console.error("[Vessel] Failed to save settings:", err));
189
189
  }
190
190
  function saveSettings() {
191
- saveDirty$3 = true;
192
- if (saveTimer$3) return;
193
- saveTimer$3 = setTimeout(() => {
194
- saveTimer$3 = null;
195
- if (saveDirty$3) {
196
- void persistNow$3();
191
+ saveDirty = true;
192
+ if (saveTimer) return;
193
+ saveTimer = setTimeout(() => {
194
+ saveTimer = null;
195
+ if (saveDirty) {
196
+ void persistNow();
197
197
  }
198
- }, SAVE_DEBOUNCE_MS$3);
198
+ }, SAVE_DEBOUNCE_MS$5);
199
199
  }
200
200
  function setSetting(key, value) {
201
201
  loadSettings();
@@ -231,8 +231,8 @@ function setSetting(key, value) {
231
231
  saveSettings();
232
232
  return { ...settings };
233
233
  }
234
- function flushPersist$3() {
235
- return saveDirty$3 ? persistNow$3() : Promise.resolve();
234
+ function flushPersist$5() {
235
+ return saveDirty ? persistNow() : Promise.resolve();
236
236
  }
237
237
  function checkDomainPolicy(url) {
238
238
  if (!url || url.startsWith("about:")) return null;
@@ -746,48 +746,132 @@ class Tab {
746
746
  this.view.webContents.close();
747
747
  }
748
748
  }
749
- let state$3 = null;
750
- const listeners$2 = /* @__PURE__ */ new Set();
751
- const SAVE_DEBOUNCE_MS$2 = 250;
752
- let saveTimer$2 = null;
753
- let saveDirty$2 = false;
754
- function getHighlightsPath() {
755
- return path.join(electron.app.getPath("userData"), "vessel-highlights.json");
749
+ function canUseSafeStorage() {
750
+ try {
751
+ return electron.safeStorage.isEncryptionAvailable();
752
+ } catch {
753
+ return false;
754
+ }
756
755
  }
757
- function load$2() {
758
- if (state$3) return state$3;
756
+ function decodeStoredData(data, secure) {
757
+ if (secure && canUseSafeStorage() && electron.safeStorage.decryptString) {
758
+ return electron.safeStorage.decryptString(data);
759
+ }
760
+ return data.toString("utf-8");
761
+ }
762
+ function encodeStoredData(payload, secure) {
763
+ if (secure && canUseSafeStorage() && electron.safeStorage.encryptString) {
764
+ return electron.safeStorage.encryptString(payload);
765
+ }
766
+ return payload;
767
+ }
768
+ function loadJsonFile({
769
+ filePath,
770
+ fallback,
771
+ parse,
772
+ secure = false
773
+ }) {
759
774
  try {
760
- const raw = fs.readFileSync(getHighlightsPath(), "utf-8");
761
- const parsed = JSON.parse(raw);
762
- state$3 = {
763
- highlights: Array.isArray(parsed.highlights) ? parsed.highlights : []
764
- };
775
+ const raw = fs.readFileSync(filePath);
776
+ const decoded = decodeStoredData(raw, secure);
777
+ return parse(JSON.parse(decoded));
765
778
  } catch {
766
- state$3 = { highlights: [] };
779
+ return fallback;
767
780
  }
768
- return state$3;
769
781
  }
770
- function persistNow$2() {
771
- if (!state$3) return Promise.resolve();
772
- saveDirty$2 = false;
773
- return fs.promises.writeFile(getHighlightsPath(), JSON.stringify(state$3, null, 2), "utf-8").catch((err) => console.error("[Vessel] Failed to save highlights:", err));
782
+ function createDebouncedJsonPersistence({
783
+ debounceMs,
784
+ filePath,
785
+ getValue,
786
+ logLabel,
787
+ resetOnSchedule = false,
788
+ secure = false,
789
+ serialize
790
+ }) {
791
+ let saveTimer2 = null;
792
+ let saveDirty2 = false;
793
+ const persistNow2 = async () => {
794
+ saveDirty2 = false;
795
+ if (saveTimer2) {
796
+ clearTimeout(saveTimer2);
797
+ saveTimer2 = null;
798
+ }
799
+ const value = getValue();
800
+ if (value == null) return;
801
+ const payload = JSON.stringify(
802
+ serialize ? serialize(value) : value,
803
+ null,
804
+ 2
805
+ );
806
+ const data = encodeStoredData(payload, secure);
807
+ await fs.promises.mkdir(path.dirname(filePath), { recursive: true }).then(
808
+ () => fs.promises.writeFile(
809
+ filePath,
810
+ data,
811
+ typeof data === "string" ? { encoding: "utf-8", mode: 384 } : { mode: 384 }
812
+ )
813
+ ).catch(
814
+ (err) => console.error(`[Vessel] Failed to save ${logLabel}:`, err)
815
+ );
816
+ };
817
+ const schedule = () => {
818
+ saveDirty2 = true;
819
+ if (saveTimer2) {
820
+ if (!resetOnSchedule) return;
821
+ clearTimeout(saveTimer2);
822
+ }
823
+ saveTimer2 = setTimeout(() => {
824
+ saveTimer2 = null;
825
+ if (saveDirty2) void persistNow2();
826
+ }, debounceMs);
827
+ };
828
+ const flush2 = () => {
829
+ return saveDirty2 ? persistNow2() : Promise.resolve();
830
+ };
831
+ return {
832
+ persistNow: persistNow2,
833
+ schedule,
834
+ flush: flush2
835
+ };
836
+ }
837
+ let state$4 = null;
838
+ const listeners$2 = /* @__PURE__ */ new Set();
839
+ const SAVE_DEBOUNCE_MS$4 = 250;
840
+ function getHighlightsPath() {
841
+ return path.join(electron.app.getPath("userData"), "vessel-highlights.json");
774
842
  }
843
+ function load$4() {
844
+ if (state$4) return state$4;
845
+ state$4 = loadJsonFile({
846
+ filePath: getHighlightsPath(),
847
+ fallback: { highlights: [] },
848
+ parse: (raw) => {
849
+ const parsed = raw;
850
+ return {
851
+ highlights: Array.isArray(parsed.highlights) ? parsed.highlights : []
852
+ };
853
+ }
854
+ });
855
+ return state$4;
856
+ }
857
+ const persistence$4 = createDebouncedJsonPersistence({
858
+ debounceMs: SAVE_DEBOUNCE_MS$4,
859
+ filePath: getHighlightsPath(),
860
+ getValue: () => state$4,
861
+ logLabel: "highlights",
862
+ resetOnSchedule: true
863
+ });
775
864
  function save$2() {
776
- saveDirty$2 = true;
777
- if (saveTimer$2) clearTimeout(saveTimer$2);
778
- saveTimer$2 = setTimeout(() => {
779
- saveTimer$2 = null;
780
- void persistNow$2();
781
- }, SAVE_DEBOUNCE_MS$2);
865
+ persistence$4.schedule();
782
866
  }
783
867
  function emit$2() {
784
- if (!state$3) return;
785
- const snapshot = { highlights: [...state$3.highlights] };
868
+ if (!state$4) return;
869
+ const snapshot = { highlights: [...state$4.highlights] };
786
870
  for (const listener of listeners$2) {
787
871
  listener(snapshot);
788
872
  }
789
873
  }
790
- function normalizeUrl(rawUrl) {
874
+ function normalizeUrl$1(rawUrl) {
791
875
  try {
792
876
  const parsed = new URL(rawUrl);
793
877
  parsed.hash = "";
@@ -797,19 +881,19 @@ function normalizeUrl(rawUrl) {
797
881
  }
798
882
  }
799
883
  function getState$2() {
800
- load$2();
801
- return { highlights: [...state$3.highlights] };
884
+ load$4();
885
+ return { highlights: [...state$4.highlights] };
802
886
  }
803
887
  function getHighlightsForUrl(url) {
804
- load$2();
805
- const normalized = normalizeUrl(url);
806
- return state$3.highlights.filter((h) => h.url === normalized);
888
+ load$4();
889
+ const normalized = normalizeUrl$1(url);
890
+ return state$4.highlights.filter((h) => h.url === normalized);
807
891
  }
808
892
  function addHighlight(url, selector, text, label, color, source) {
809
- load$2();
893
+ load$4();
810
894
  const highlight = {
811
895
  id: crypto$1.randomUUID(),
812
- url: normalizeUrl(url),
896
+ url: normalizeUrl$1(url),
813
897
  selector: selector || void 0,
814
898
  text: text || void 0,
815
899
  label: label || void 0,
@@ -817,30 +901,30 @@ function addHighlight(url, selector, text, label, color, source) {
817
901
  source: source || void 0,
818
902
  createdAt: (/* @__PURE__ */ new Date()).toISOString()
819
903
  };
820
- state$3.highlights.push(highlight);
904
+ state$4.highlights.push(highlight);
821
905
  save$2();
822
906
  emit$2();
823
907
  return highlight;
824
908
  }
825
909
  function removeHighlight(id) {
826
- load$2();
827
- const index = state$3.highlights.findIndex((h) => h.id === id);
910
+ load$4();
911
+ const index = state$4.highlights.findIndex((h) => h.id === id);
828
912
  if (index === -1) return null;
829
- const [removed] = state$3.highlights.splice(index, 1);
913
+ const [removed] = state$4.highlights.splice(index, 1);
830
914
  save$2();
831
915
  emit$2();
832
916
  return removed;
833
917
  }
834
918
  function findHighlightByText(url, text) {
835
- load$2();
836
- const normalized = normalizeUrl(url);
837
- return state$3.highlights.find(
919
+ load$4();
920
+ const normalized = normalizeUrl$1(url);
921
+ return state$4.highlights.find(
838
922
  (h) => h.url === normalized && h.text && h.text === text
839
923
  ) ?? null;
840
924
  }
841
925
  function updateHighlightColor(id, color) {
842
- load$2();
843
- const highlight = state$3.highlights.find((h) => h.id === id);
926
+ load$4();
927
+ const highlight = state$4.highlights.find((h) => h.id === id);
844
928
  if (!highlight) return null;
845
929
  highlight.color = color;
846
930
  save$2();
@@ -848,24 +932,19 @@ function updateHighlightColor(id, color) {
848
932
  return highlight;
849
933
  }
850
934
  function clearHighlightsForUrl(url) {
851
- load$2();
852
- const normalized = normalizeUrl(url);
853
- const before = state$3.highlights.length;
854
- state$3.highlights = state$3.highlights.filter((h) => h.url !== normalized);
855
- const removed = before - state$3.highlights.length;
935
+ load$4();
936
+ const normalized = normalizeUrl$1(url);
937
+ const before = state$4.highlights.length;
938
+ state$4.highlights = state$4.highlights.filter((h) => h.url !== normalized);
939
+ const removed = before - state$4.highlights.length;
856
940
  if (removed > 0) {
857
941
  save$2();
858
942
  emit$2();
859
943
  }
860
944
  return removed;
861
945
  }
862
- function flushPersist$2() {
863
- if (saveTimer$2) {
864
- clearTimeout(saveTimer$2);
865
- saveTimer$2 = null;
866
- }
867
- if (!saveDirty$2) return Promise.resolve();
868
- return persistNow$2();
946
+ function flushPersist$4() {
947
+ return persistence$4.flush();
869
948
  }
870
949
  const SKIP_TAGS_JS = "var SKIP_TAGS = {SCRIPT:1,STYLE:1,NOSCRIPT:1,TEMPLATE:1,IFRAME:1,SVG:1};";
871
950
  const CONTENT_ROOTS_JS = `
@@ -1297,6 +1376,10 @@ async function highlightBatchOnPage(wc, entries) {
1297
1376
  }
1298
1377
  const HIGHLIGHT_SELECTOR = "'.__vessel-highlight, .__vessel-highlight-text'";
1299
1378
  async function getHighlightCount(wc) {
1379
+ if (wc.isDestroyed()) return 0;
1380
+ if (wc.isLoading()) return 0;
1381
+ const currentUrl = wc.getURL();
1382
+ if (!currentUrl || currentUrl === "about:blank") return 0;
1300
1383
  return wc.executeJavaScript(
1301
1384
  `document.querySelectorAll(${HIGHLIGHT_SELECTOR}).length`
1302
1385
  );
@@ -1433,61 +1516,45 @@ function persistHighlight(url, text) {
1433
1516
  return { success: true, text: capped, id: highlight.id };
1434
1517
  }
1435
1518
  const MAX_HISTORY_ENTRIES = 5e3;
1436
- const SAVE_DEBOUNCE_MS$1 = 250;
1437
- let state$2 = null;
1519
+ const SAVE_DEBOUNCE_MS$3 = 250;
1520
+ let state$3 = null;
1438
1521
  const listeners$1 = /* @__PURE__ */ new Set();
1439
- let saveTimer$1 = null;
1440
- let saveDirty$1 = false;
1441
1522
  function getHistoryPath() {
1442
1523
  return path.join(electron.app.getPath("userData"), "vessel-history.json");
1443
1524
  }
1444
- function load$1() {
1445
- if (state$2) return state$2;
1446
- try {
1447
- const raw = fs.readFileSync(getHistoryPath(), "utf-8");
1448
- const parsed = JSON.parse(raw);
1449
- state$2 = {
1450
- entries: Array.isArray(parsed.entries) ? parsed.entries : []
1451
- };
1452
- } catch {
1453
- state$2 = { entries: [] };
1454
- }
1455
- return state$2;
1456
- }
1457
- function persistNow$1() {
1458
- saveDirty$1 = false;
1459
- if (saveTimer$1) {
1460
- clearTimeout(saveTimer$1);
1461
- saveTimer$1 = null;
1462
- }
1463
- return fs.promises.mkdir(path.dirname(getHistoryPath()), { recursive: true }).then(
1464
- () => fs.promises.writeFile(
1465
- getHistoryPath(),
1466
- JSON.stringify(state$2, null, 2),
1467
- "utf-8"
1468
- )
1469
- ).catch((err) => console.error("[Vessel] Failed to save history:", err));
1525
+ function load$3() {
1526
+ if (state$3) return state$3;
1527
+ state$3 = loadJsonFile({
1528
+ filePath: getHistoryPath(),
1529
+ fallback: { entries: [] },
1530
+ parse: (raw) => {
1531
+ const parsed = raw;
1532
+ return {
1533
+ entries: Array.isArray(parsed.entries) ? parsed.entries : []
1534
+ };
1535
+ }
1536
+ });
1537
+ return state$3;
1470
1538
  }
1539
+ const persistence$3 = createDebouncedJsonPersistence({
1540
+ debounceMs: SAVE_DEBOUNCE_MS$3,
1541
+ filePath: getHistoryPath(),
1542
+ getValue: () => state$3,
1543
+ logLabel: "history"
1544
+ });
1471
1545
  function save$1() {
1472
- saveDirty$1 = true;
1473
- if (saveTimer$1) return;
1474
- saveTimer$1 = setTimeout(() => {
1475
- saveTimer$1 = null;
1476
- if (saveDirty$1) {
1477
- void persistNow$1();
1478
- }
1479
- }, SAVE_DEBOUNCE_MS$1);
1546
+ persistence$3.schedule();
1480
1547
  }
1481
1548
  function emit$1() {
1482
- if (!state$2) return;
1483
- const snapshot = { entries: [...state$2.entries] };
1549
+ if (!state$3) return;
1550
+ const snapshot = { entries: [...state$3.entries] };
1484
1551
  for (const listener of listeners$1) {
1485
1552
  listener(snapshot);
1486
1553
  }
1487
1554
  }
1488
1555
  function getState$1() {
1489
- load$1();
1490
- return { entries: [...state$2.entries] };
1556
+ load$3();
1557
+ return { entries: [...state$3.entries] };
1491
1558
  }
1492
1559
  function subscribe$1(listener) {
1493
1560
  listeners$1.add(listener);
@@ -1497,8 +1564,8 @@ function subscribe$1(listener) {
1497
1564
  }
1498
1565
  function addEntry$1(url, title) {
1499
1566
  if (!url || url === "about:blank") return;
1500
- load$1();
1501
- const last = state$2.entries[0];
1567
+ load$3();
1568
+ const last = state$3.entries[0];
1502
1569
  if (last && last.url === url) {
1503
1570
  if (title && title !== last.title) {
1504
1571
  last.title = title;
@@ -1512,28 +1579,28 @@ function addEntry$1(url, title) {
1512
1579
  title: title || url,
1513
1580
  visitedAt: (/* @__PURE__ */ new Date()).toISOString()
1514
1581
  };
1515
- state$2.entries.unshift(entry);
1516
- if (state$2.entries.length > MAX_HISTORY_ENTRIES) {
1517
- state$2.entries = state$2.entries.slice(0, MAX_HISTORY_ENTRIES);
1582
+ state$3.entries.unshift(entry);
1583
+ if (state$3.entries.length > MAX_HISTORY_ENTRIES) {
1584
+ state$3.entries = state$3.entries.slice(0, MAX_HISTORY_ENTRIES);
1518
1585
  }
1519
1586
  save$1();
1520
1587
  emit$1();
1521
1588
  }
1522
1589
  function search(query, limit = 50) {
1523
- load$1();
1524
- if (!query.trim()) return state$2.entries.slice(0, limit);
1590
+ load$3();
1591
+ if (!query.trim()) return state$3.entries.slice(0, limit);
1525
1592
  const normalized = query.toLowerCase();
1526
- return state$2.entries.filter(
1593
+ return state$3.entries.filter(
1527
1594
  (e) => e.url.toLowerCase().includes(normalized) || e.title.toLowerCase().includes(normalized)
1528
1595
  ).slice(0, limit);
1529
1596
  }
1530
1597
  function clearAll$1() {
1531
- state$2 = { entries: [] };
1598
+ state$3 = { entries: [] };
1532
1599
  save$1();
1533
1600
  emit$1();
1534
1601
  }
1535
- function flushPersist$1() {
1536
- return saveDirty$1 ? persistNow$1() : Promise.resolve();
1602
+ function flushPersist$3() {
1603
+ return persistence$3.flush();
1537
1604
  }
1538
1605
  const MAX_CONSOLE_ENTRIES = 500;
1539
1606
  const MAX_NETWORK_ENTRIES = 200;
@@ -2246,10 +2313,14 @@ class TabManager {
2246
2313
  window;
2247
2314
  onStateChange;
2248
2315
  highlightCaptureCallback = null;
2316
+ pageLoadCallback = null;
2249
2317
  constructor(window2, onStateChange) {
2250
2318
  this.window = window2;
2251
2319
  this.onStateChange = onStateChange;
2252
2320
  }
2321
+ onPageLoad(cb) {
2322
+ this.pageLoadCallback = cb;
2323
+ }
2253
2324
  createTab(url = "about:blank", options) {
2254
2325
  const background = options?.background ?? false;
2255
2326
  const id = crypto$1.randomUUID();
@@ -2262,6 +2333,7 @@ class TabManager {
2262
2333
  onPageLoad: (pageUrl, wc) => {
2263
2334
  this.reapplyHighlights(pageUrl, wc);
2264
2335
  addEntry$1(pageUrl, wc.getTitle());
2336
+ this.pageLoadCallback?.(pageUrl, wc);
2265
2337
  },
2266
2338
  onHighlightSelection: (wc) => this.captureHighlightFromPage(wc),
2267
2339
  onHighlightRemove: (url2, text) => this.removeHighlightByText(url2, text),
@@ -2415,7 +2487,7 @@ class TabManager {
2415
2487
  const wcId = wc.id;
2416
2488
  const now = Date.now();
2417
2489
  const last = this.lastReapply.get(wcId);
2418
- const normalized = normalizeUrl(url);
2490
+ const normalized = normalizeUrl$1(url);
2419
2491
  if (last && last.url === normalized && now - last.at < 500) return;
2420
2492
  this.lastReapply.set(wcId, { url: normalized, at: now });
2421
2493
  const highlights = getHighlightsForUrl(url);
@@ -2464,14 +2536,14 @@ class TabManager {
2464
2536
  if (highlight) {
2465
2537
  removeHighlight(highlight.id);
2466
2538
  }
2467
- const normalized = normalizeUrl(url);
2539
+ const normalized = normalizeUrl$1(url);
2468
2540
  for (const id of this.order) {
2469
2541
  const tab = this.tabs.get(id);
2470
2542
  if (!tab) continue;
2471
2543
  const wc = tab.view.webContents;
2472
2544
  if (wc.isDestroyed()) continue;
2473
2545
  try {
2474
- const tabUrl = normalizeUrl(wc.getURL());
2546
+ const tabUrl = normalizeUrl$1(wc.getURL());
2475
2547
  if (tabUrl === normalized) {
2476
2548
  void this.removeHighlightMarksForText(wc, text);
2477
2549
  }
@@ -2488,14 +2560,14 @@ class TabManager {
2488
2560
  if (highlight) {
2489
2561
  updateHighlightColor(highlight.id, color);
2490
2562
  }
2491
- const normalized = normalizeUrl(url);
2563
+ const normalized = normalizeUrl$1(url);
2492
2564
  for (const id of this.order) {
2493
2565
  const tab = this.tabs.get(id);
2494
2566
  if (!tab) continue;
2495
2567
  const wc = tab.view.webContents;
2496
2568
  if (wc.isDestroyed()) continue;
2497
2569
  try {
2498
- const tabUrl = normalizeUrl(wc.getURL());
2570
+ const tabUrl = normalizeUrl$1(wc.getURL());
2499
2571
  if (tabUrl === normalized) {
2500
2572
  void this.removeHighlightMarksForText(wc, text).then(() => {
2501
2573
  void highlightOnPage(
@@ -2547,7 +2619,9 @@ const Channels = {
2547
2619
  TAB_BACK: "tab:back",
2548
2620
  TAB_FORWARD: "tab:forward",
2549
2621
  TAB_RELOAD: "tab:reload",
2622
+ TAB_STATE_GET: "tab:state-get",
2550
2623
  TAB_STATE_UPDATE: "tab:state-update",
2624
+ RENDERER_VIEW_READY: "renderer:view-ready",
2551
2625
  // AI
2552
2626
  AI_QUERY: "ai:query",
2553
2627
  AI_STREAM_START: "ai:stream-start",
@@ -2651,316 +2725,399 @@ const Channels = {
2651
2725
  // Window controls
2652
2726
  WINDOW_MINIMIZE: "window:minimize",
2653
2727
  WINDOW_MAXIMIZE: "window:maximize",
2654
- WINDOW_CLOSE: "window:close"
2728
+ WINDOW_CLOSE: "window:close",
2729
+ // Autofill
2730
+ AUTOFILL_LIST: "autofill:list",
2731
+ AUTOFILL_ADD: "autofill:add",
2732
+ AUTOFILL_UPDATE: "autofill:update",
2733
+ AUTOFILL_DELETE: "autofill:delete",
2734
+ AUTOFILL_FILL: "autofill:fill",
2735
+ // Page snapshots / What Changed
2736
+ PAGE_DIFF_ACTIVITY: "page:diff-activity",
2737
+ PAGE_CHANGED: "page:changed",
2738
+ PAGE_DIFF_GET: "page:diff-get",
2739
+ PAGE_DIFF_DIRTY: "page:diff-dirty"
2655
2740
  };
2656
- function enableClipboardShortcuts(view) {
2657
- view.webContents.on("before-input-event", (event, input) => {
2658
- if (!input.control && !input.meta) return;
2659
- const key = input.key.toLowerCase();
2660
- const wc = view.webContents;
2661
- if (input.type === "keyDown") {
2662
- if (key === "c") {
2663
- wc.copy();
2664
- event.preventDefault();
2665
- } else if (key === "v") {
2666
- wc.paste();
2667
- event.preventDefault();
2668
- } else if (key === "x") {
2669
- wc.cut();
2670
- event.preventDefault();
2671
- } else if (key === "a") {
2672
- wc.selectAll();
2673
- event.preventDefault();
2674
- }
2741
+ const MAX_DETAIL_ITEMS = 3;
2742
+ const MIN_BLOCK_SIMILARITY = 0.82;
2743
+ function normalizeText$2(value) {
2744
+ return value.replace(/\s+/g, " ").trim();
2745
+ }
2746
+ function truncateText(value, max = 180) {
2747
+ const normalized = normalizeText$2(value);
2748
+ if (normalized.length <= max) return normalized;
2749
+ return `${normalized.slice(0, max - 3)}...`;
2750
+ }
2751
+ function tokenize(text) {
2752
+ return normalizeText$2(text).toLowerCase().split(/\s+/).filter(Boolean);
2753
+ }
2754
+ function countOverlap(a, b) {
2755
+ if (a.length === 0 || b.length === 0) return 0;
2756
+ const counts = /* @__PURE__ */ new Map();
2757
+ for (const token of b) {
2758
+ counts.set(token, (counts.get(token) || 0) + 1);
2759
+ }
2760
+ let overlap = 0;
2761
+ for (const token of a) {
2762
+ const remaining = counts.get(token) || 0;
2763
+ if (remaining > 0) {
2764
+ overlap += 1;
2765
+ counts.set(token, remaining - 1);
2766
+ }
2767
+ }
2768
+ return overlap;
2769
+ }
2770
+ function similarityScore(a, b) {
2771
+ const aTokens = tokenize(a);
2772
+ const bTokens = tokenize(b);
2773
+ if (aTokens.length === 0 && bTokens.length === 0) return 1;
2774
+ if (aTokens.length === 0 || bTokens.length === 0) return 0;
2775
+ return countOverlap(aTokens, bTokens) / Math.max(aTokens.length, bTokens.length);
2776
+ }
2777
+ function extractTextBlocks(text) {
2778
+ const compact = text.replace(/\r\n/g, "\n").trim();
2779
+ if (!compact) return [];
2780
+ const paragraphs = compact.split(/\n\s*\n+/).map((block) => normalizeText$2(block)).filter(Boolean);
2781
+ if (paragraphs.length > 1) return paragraphs;
2782
+ return compact.split(/\n+/).map((line) => normalizeText$2(line)).filter(Boolean);
2783
+ }
2784
+ function buildLcsTable(a, b) {
2785
+ const table = Array.from(
2786
+ { length: a.length + 1 },
2787
+ () => Array.from({ length: b.length + 1 }).fill(0)
2788
+ );
2789
+ for (let i = a.length - 1; i >= 0; i -= 1) {
2790
+ for (let j = b.length - 1; j >= 0; j -= 1) {
2791
+ table[i][j] = a[i] === b[j] ? table[i + 1][j + 1] + 1 : Math.max(table[i + 1][j], table[i][j + 1]);
2792
+ }
2793
+ }
2794
+ return table;
2795
+ }
2796
+ function diffBlocks(oldBlocks, newBlocks) {
2797
+ const table = buildLcsTable(oldBlocks, newBlocks);
2798
+ const ops = [];
2799
+ let i = 0;
2800
+ let j = 0;
2801
+ while (i < oldBlocks.length && j < newBlocks.length) {
2802
+ if (oldBlocks[i] === newBlocks[j]) {
2803
+ ops.push({ type: "equal", value: oldBlocks[i] });
2804
+ i += 1;
2805
+ j += 1;
2806
+ continue;
2675
2807
  }
2676
- });
2808
+ if (table[i + 1][j] >= table[i][j + 1]) {
2809
+ ops.push({ type: "removed", value: oldBlocks[i] });
2810
+ i += 1;
2811
+ } else {
2812
+ ops.push({ type: "added", value: newBlocks[j] });
2813
+ j += 1;
2814
+ }
2815
+ }
2816
+ while (i < oldBlocks.length) {
2817
+ ops.push({ type: "removed", value: oldBlocks[i] });
2818
+ i += 1;
2819
+ }
2820
+ while (j < newBlocks.length) {
2821
+ ops.push({ type: "added", value: newBlocks[j] });
2822
+ j += 1;
2823
+ }
2824
+ return ops;
2677
2825
  }
2678
- const CHROME_HEIGHT = 110;
2679
- const DEFAULT_DEVTOOLS_PANEL_HEIGHT = 250;
2680
- const MIN_DEVTOOLS_PANEL = 120;
2681
- const MAX_DEVTOOLS_PANEL = 600;
2682
- async function getSidebarContextTarget(sidebarView, x, y) {
2683
- try {
2684
- return await sidebarView.webContents.executeJavaScript(
2685
- `(() => {
2686
- const el = document.elementFromPoint(${x}, ${y});
2687
- const nav = el && typeof el.closest === "function"
2688
- ? el.closest(".highlight-nav")
2689
- : null;
2690
- const label = nav?.querySelector(".highlight-nav-label")?.textContent?.trim() || "";
2691
- return {
2692
- inHighlightNav: !!nav,
2693
- canRemoveCurrent: /\\d+\\s*\\/\\s*\\d+/.test(label),
2694
- bookmarkId:
2695
- el && typeof el.closest === "function"
2696
- ? el.closest("[data-bookmark-id]")?.getAttribute("data-bookmark-id") || undefined
2697
- : undefined,
2698
- };
2699
- })()`,
2700
- true
2826
+ function summarizeContentChange(changedCount, addedCount, removedCount) {
2827
+ const parts = [];
2828
+ if (changedCount > 0) {
2829
+ parts.push(
2830
+ `${changedCount} updated ${changedCount === 1 ? "section" : "sections"}`
2701
2831
  );
2702
- } catch {
2703
- return { inHighlightNav: false, canRemoveCurrent: false };
2704
2832
  }
2705
- }
2706
- async function showSidebarContextMenu(mainWindow, sidebarView, params) {
2707
- const target = await getSidebarContextTarget(sidebarView, params.x, params.y);
2708
- const menu = new electron.Menu();
2709
- if (target.inHighlightNav) {
2710
- if (target.canRemoveCurrent) {
2711
- menu.append(
2712
- new electron.MenuItem({
2713
- label: "Remove Current Highlight",
2714
- click: () => sidebarView.webContents.send(
2715
- Channels.SIDEBAR_HIGHLIGHT_ACTION,
2716
- "remove-current"
2717
- )
2718
- })
2719
- );
2720
- }
2721
- menu.append(
2722
- new electron.MenuItem({
2723
- label: "Clear All Highlights",
2724
- click: () => sidebarView.webContents.send(
2725
- Channels.SIDEBAR_HIGHLIGHT_ACTION,
2726
- "clear-all"
2727
- )
2728
- })
2833
+ if (addedCount > 0) {
2834
+ parts.push(
2835
+ `${addedCount} added ${addedCount === 1 ? "section" : "sections"}`
2729
2836
  );
2730
2837
  }
2731
- if (target.bookmarkId) {
2732
- if (menu.items.length > 0) {
2733
- menu.append(new electron.MenuItem({ type: "separator" }));
2734
- }
2735
- menu.append(
2736
- new electron.MenuItem({
2737
- label: "Add Context to Chat",
2738
- click: () => sidebarView.webContents.send(
2739
- Channels.BOOKMARK_ADD_CONTEXT_TO_CHAT,
2740
- target.bookmarkId
2741
- )
2742
- })
2838
+ if (removedCount > 0) {
2839
+ parts.push(
2840
+ `${removedCount} removed ${removedCount === 1 ? "section" : "sections"}`
2743
2841
  );
2744
2842
  }
2745
- if (params.isEditable) {
2746
- if (menu.items.length > 0) {
2747
- menu.append(new electron.MenuItem({ type: "separator" }));
2843
+ return parts.join(", ");
2844
+ }
2845
+ function diffSnapshots(oldSnap, currentContent, currentTitle, currentHeadings) {
2846
+ const changes = [];
2847
+ if (oldSnap.title !== currentTitle) {
2848
+ changes.push({
2849
+ kind: "changed",
2850
+ section: "title",
2851
+ summary: `"${oldSnap.title}" → "${currentTitle}"`,
2852
+ before: oldSnap.title,
2853
+ after: currentTitle
2854
+ });
2855
+ }
2856
+ const oldHeadings = oldSnap.headings.split("\n").filter(Boolean);
2857
+ const newHeadings = currentHeadings.split("\n").filter(Boolean);
2858
+ if (oldHeadings.join("\n") !== newHeadings.join("\n")) {
2859
+ const added = newHeadings.filter((h) => !oldHeadings.includes(h));
2860
+ const removed = oldHeadings.filter((h) => !newHeadings.includes(h));
2861
+ const parts = [];
2862
+ if (added.length > 0) parts.push(`New: ${added.join(", ")}`);
2863
+ if (removed.length > 0) parts.push(`Gone: ${removed.join(", ")}`);
2864
+ if (parts.length > 0) {
2865
+ changes.push({
2866
+ kind: added.length > 0 && removed.length > 0 ? "changed" : added.length > 0 ? "added" : "removed",
2867
+ section: "headings",
2868
+ summary: parts.join(". "),
2869
+ addedItems: added.slice(0, MAX_DETAIL_ITEMS),
2870
+ removedItems: removed.slice(0, MAX_DETAIL_ITEMS)
2871
+ });
2748
2872
  }
2749
- menu.append(
2750
- new electron.MenuItem({
2751
- role: "undo",
2752
- enabled: params.editFlags.canUndo
2753
- })
2754
- );
2755
- menu.append(
2756
- new electron.MenuItem({
2757
- role: "redo",
2758
- enabled: params.editFlags.canRedo
2759
- })
2760
- );
2761
- menu.append(new electron.MenuItem({ type: "separator" }));
2762
- menu.append(
2763
- new electron.MenuItem({
2764
- role: "cut",
2765
- enabled: params.editFlags.canCut
2766
- })
2767
- );
2768
- menu.append(
2769
- new electron.MenuItem({
2770
- role: "copy",
2771
- enabled: params.editFlags.canCopy
2772
- })
2773
- );
2774
- menu.append(
2775
- new electron.MenuItem({
2776
- role: "paste",
2777
- enabled: params.editFlags.canPaste
2778
- })
2779
- );
2780
- menu.append(
2781
- new electron.MenuItem({
2782
- role: "selectAll",
2783
- enabled: params.editFlags.canSelectAll
2784
- })
2785
- );
2786
- } else if (params.selectionText?.trim()) {
2787
- if (menu.items.length > 0) {
2788
- menu.append(new electron.MenuItem({ type: "separator" }));
2873
+ }
2874
+ const oldBlocks = extractTextBlocks(oldSnap.textContent);
2875
+ const newBlocks = extractTextBlocks(currentContent);
2876
+ const overallSimilarity = similarityScore(oldSnap.textContent, currentContent);
2877
+ if (overallSimilarity < 0.98) {
2878
+ const ops = diffBlocks(oldBlocks, newBlocks);
2879
+ const addedBlocks = [];
2880
+ const removedBlocks = [];
2881
+ const changedPairs = [];
2882
+ let idx = 0;
2883
+ while (idx < ops.length) {
2884
+ if (ops[idx]?.type === "equal") {
2885
+ idx += 1;
2886
+ continue;
2887
+ }
2888
+ const pendingRemoved = [];
2889
+ const pendingAdded = [];
2890
+ while (idx < ops.length && ops[idx]?.type !== "equal") {
2891
+ const op = ops[idx];
2892
+ if (op?.type === "removed") pendingRemoved.push(op.value);
2893
+ if (op?.type === "added") pendingAdded.push(op.value);
2894
+ idx += 1;
2895
+ }
2896
+ while (pendingRemoved.length > 0 && pendingAdded.length > 0) {
2897
+ const before = pendingRemoved[0];
2898
+ const after = pendingAdded[0];
2899
+ if (similarityScore(before, after) < MIN_BLOCK_SIMILARITY) break;
2900
+ changedPairs.push({ before, after });
2901
+ pendingRemoved.shift();
2902
+ pendingAdded.shift();
2903
+ }
2904
+ removedBlocks.push(...pendingRemoved);
2905
+ addedBlocks.push(...pendingAdded);
2906
+ }
2907
+ if (changedPairs.length > 0 || addedBlocks.length > 0 || removedBlocks.length > 0) {
2908
+ changes.push({
2909
+ kind: "changed",
2910
+ section: "content",
2911
+ summary: summarizeContentChange(
2912
+ changedPairs.length,
2913
+ addedBlocks.length,
2914
+ removedBlocks.length
2915
+ ),
2916
+ before: changedPairs[0] ? truncateText(changedPairs[0].before) : removedBlocks[0] ? truncateText(removedBlocks[0]) : void 0,
2917
+ after: changedPairs[0] ? truncateText(changedPairs[0].after) : addedBlocks[0] ? truncateText(addedBlocks[0]) : void 0,
2918
+ addedItems: addedBlocks.slice(0, MAX_DETAIL_ITEMS).map((item) => truncateText(item)),
2919
+ removedItems: removedBlocks.slice(0, MAX_DETAIL_ITEMS).map((item) => truncateText(item))
2920
+ });
2789
2921
  }
2790
- menu.append(new electron.MenuItem({ role: "copy" }));
2791
2922
  }
2792
- if (menu.items.length === 0) return;
2793
- sidebarView.webContents.focus();
2794
- menu.popup({ window: mainWindow });
2923
+ return {
2924
+ url: oldSnap.url,
2925
+ hasChanges: changes.length > 0,
2926
+ oldSnapshot: { capturedAt: oldSnap.capturedAt, title: oldSnap.title },
2927
+ changes
2928
+ };
2795
2929
  }
2796
- function getWindowIconPath() {
2797
- const candidates = [
2798
- path.join(electron.app.getAppPath(), "resources", "vessel-icon.png"),
2799
- path.join(process.resourcesPath, "vessel-icon.png"),
2800
- path.join(__dirname, "../../resources/vessel-icon.png")
2801
- ];
2802
- return candidates.find((candidate) => fs.existsSync(candidate));
2930
+ function normalizePageUrl(rawUrl) {
2931
+ try {
2932
+ const url = new URL(rawUrl);
2933
+ const pathname = url.pathname.replace(/\/+$/, "") || "/";
2934
+ return `${url.origin}${pathname}`.toLowerCase();
2935
+ } catch {
2936
+ return rawUrl.trim().replace(/\/+$/, "").toLowerCase();
2937
+ }
2803
2938
  }
2804
- function createMainWindow(onTabStateChange) {
2805
- const mainWindow = new electron.BaseWindow({
2806
- width: 1280,
2807
- height: 800,
2808
- minWidth: 800,
2809
- minHeight: 600,
2810
- frame: false,
2811
- show: false,
2812
- backgroundColor: "#1a1a1e",
2813
- icon: getWindowIconPath()
2814
- });
2815
- const chromeView = new electron.WebContentsView({
2816
- webPreferences: {
2817
- preload: path.join(__dirname, "../preload/index.js"),
2818
- sandbox: true,
2819
- contextIsolation: true,
2820
- nodeIntegration: false
2821
- }
2822
- });
2823
- chromeView.setBackgroundColor("#00000000");
2824
- mainWindow.contentView.addChildView(chromeView);
2825
- const sidebarView = new electron.WebContentsView({
2826
- webPreferences: {
2827
- preload: path.join(__dirname, "../preload/index.js"),
2828
- sandbox: true,
2829
- contextIsolation: true,
2830
- nodeIntegration: false
2831
- }
2832
- });
2833
- sidebarView.setBackgroundColor("#00000000");
2834
- sidebarView.webContents.on("context-menu", (event, params) => {
2835
- event.preventDefault();
2836
- void showSidebarContextMenu(mainWindow, sidebarView, params);
2837
- });
2838
- mainWindow.contentView.addChildView(sidebarView);
2839
- const devtoolsPanelView = new electron.WebContentsView({
2840
- webPreferences: {
2841
- preload: path.join(__dirname, "../preload/index.js"),
2842
- sandbox: true,
2843
- contextIsolation: true,
2844
- nodeIntegration: false
2845
- }
2846
- });
2847
- devtoolsPanelView.setBackgroundColor("#00000000");
2848
- mainWindow.contentView.addChildView(devtoolsPanelView);
2849
- enableClipboardShortcuts(chromeView);
2850
- enableClipboardShortcuts(sidebarView);
2851
- enableClipboardShortcuts(devtoolsPanelView);
2852
- const settings2 = loadSettings();
2853
- const uiState = {
2854
- sidebarOpen: true,
2855
- sidebarWidth: settings2.sidebarWidth,
2856
- focusMode: false,
2857
- settingsOpen: false,
2858
- devtoolsPanelOpen: false,
2859
- devtoolsPanelHeight: DEFAULT_DEVTOOLS_PANEL_HEIGHT
2860
- };
2861
- const tabManager = new TabManager(mainWindow, onTabStateChange);
2862
- const state2 = {
2863
- mainWindow,
2864
- chromeView,
2865
- sidebarView,
2866
- devtoolsPanelView,
2867
- tabManager,
2868
- uiState
2869
- };
2870
- mainWindow.on("resize", () => layoutViews(state2));
2871
- mainWindow.on("show", () => layoutViews(state2));
2872
- mainWindow.on("focus", () => layoutViews(state2));
2873
- layoutViews(state2);
2874
- return state2;
2939
+ const SNAPSHOT_QUERY_KEYS = /* @__PURE__ */ new Set([
2940
+ "q",
2941
+ "query",
2942
+ "search",
2943
+ "s",
2944
+ "term",
2945
+ "keyword",
2946
+ "keywords",
2947
+ "page",
2948
+ "p",
2949
+ "offset",
2950
+ "cursor",
2951
+ "sort",
2952
+ "order",
2953
+ "filter",
2954
+ "filters",
2955
+ "category",
2956
+ "categories",
2957
+ "tag",
2958
+ "tags",
2959
+ "tab",
2960
+ "view"
2961
+ ]);
2962
+ const TRACKING_QUERY_KEYS = /* @__PURE__ */ new Set([
2963
+ "fbclid",
2964
+ "gclid",
2965
+ "mc_cid",
2966
+ "mc_eid",
2967
+ "ref",
2968
+ "source",
2969
+ "si"
2970
+ ]);
2971
+ function normalizeQueryValue(value) {
2972
+ return value.replace(/\s+/g, " ").trim().toLowerCase();
2973
+ }
2974
+ function serializeSnapshotParams(params) {
2975
+ return params.map(
2976
+ ([key, value]) => `${encodeURIComponent(key)}=${encodeURIComponent(value)}`
2977
+ ).join("&");
2978
+ }
2979
+ function normalizeSnapshotParams(entries, pathname) {
2980
+ return Array.from(entries).filter(
2981
+ ([key, value]) => shouldKeepSnapshotQueryParam(pathname, key, value)
2982
+ ).map(([key, value]) => [
2983
+ key.trim().toLowerCase(),
2984
+ normalizeQueryValue(value)
2985
+ ]).sort(
2986
+ ([keyA, valueA], [keyB, valueB]) => keyA === keyB ? valueA.localeCompare(valueB) : keyA.localeCompare(keyB)
2987
+ );
2875
2988
  }
2876
- function layoutViews(state2) {
2877
- const {
2878
- mainWindow,
2879
- chromeView,
2880
- sidebarView,
2881
- devtoolsPanelView,
2882
- tabManager,
2883
- uiState
2884
- } = state2;
2885
- const [width, height] = mainWindow.getContentSize();
2886
- const chromeHeight = uiState.focusMode ? 0 : CHROME_HEIGHT;
2887
- const sidebarWidth = uiState.sidebarOpen ? uiState.sidebarWidth : 0;
2888
- const devtoolsHeight = uiState.devtoolsPanelOpen ? uiState.devtoolsPanelHeight : 0;
2889
- const chromeNeedsFullHeight = uiState.settingsOpen;
2890
- if (chromeNeedsFullHeight) {
2891
- chromeView.setBounds({ x: 0, y: 0, width, height });
2892
- } else {
2893
- chromeView.setBounds({ x: 0, y: 0, width, height: chromeHeight });
2989
+ function shouldKeepSnapshotQueryParam(pathname, rawKey, value) {
2990
+ const key = rawKey.trim().toLowerCase();
2991
+ if (!key || !value.trim()) return false;
2992
+ if (key.startsWith("utm_")) return false;
2993
+ if (TRACKING_QUERY_KEYS.has(key)) return false;
2994
+ if (SNAPSHOT_QUERY_KEYS.has(key)) return true;
2995
+ return /\/(search|results|browse|discover|find|category|tag|topics?|collections?|list)(\/|$)/i.test(
2996
+ pathname
2997
+ );
2998
+ }
2999
+ function buildSnapshotHashKey(hash, pathname) {
3000
+ let raw = hash.replace(/^#/, "").trim();
3001
+ if (!raw) return null;
3002
+ let bang = false;
3003
+ if (raw.startsWith("!")) {
3004
+ bang = true;
3005
+ raw = raw.slice(1).trim();
3006
+ }
3007
+ if (raw.startsWith("/")) {
3008
+ const [routePart, queryPart = ""] = raw.split("?");
3009
+ const route = routePart.replace(/\/+$/, "") || "/";
3010
+ const params = normalizeSnapshotParams(
3011
+ new URLSearchParams(queryPart).entries(),
3012
+ pathname
3013
+ );
3014
+ const query = serializeSnapshotParams(params);
3015
+ return `#${bang ? "!" : ""}${route.toLowerCase()}${query ? `?${query}` : ""}`;
3016
+ }
3017
+ const queryLike = raw.startsWith("?") ? raw.slice(1) : raw;
3018
+ if (queryLike.includes("=")) {
3019
+ const params = normalizeSnapshotParams(
3020
+ new URLSearchParams(queryLike).entries(),
3021
+ pathname
3022
+ );
3023
+ if (params.length === 0) return null;
3024
+ const query = serializeSnapshotParams(params);
3025
+ return `#${bang ? "!" : ""}?${query}`;
2894
3026
  }
2895
- const resizeHandleOverlap = 6;
2896
- if (uiState.sidebarOpen) {
2897
- sidebarView.setBounds({
2898
- x: width - sidebarWidth - resizeHandleOverlap,
2899
- y: 0,
2900
- width: sidebarWidth + resizeHandleOverlap,
2901
- height
2902
- });
2903
- } else {
2904
- sidebarView.setBounds({ x: width, y: 0, width: 0, height: 0 });
3027
+ return null;
3028
+ }
3029
+ function buildPageSnapshotKey(rawUrl) {
3030
+ try {
3031
+ const url = new URL(rawUrl);
3032
+ const pathname = url.pathname.replace(/\/+$/, "") || "/";
3033
+ const params = normalizeSnapshotParams(url.searchParams.entries(), pathname);
3034
+ const query = serializeSnapshotParams(params);
3035
+ const hash = buildSnapshotHashKey(url.hash, pathname);
3036
+ return `${url.origin.toLowerCase()}${pathname.toLowerCase()}${query ? `?${query}` : ""}${hash || ""}`;
3037
+ } catch {
3038
+ return normalizePageUrl(rawUrl);
2905
3039
  }
2906
- const contentWidth = width - sidebarWidth;
2907
- if (uiState.devtoolsPanelOpen) {
2908
- devtoolsPanelView.setBounds({
2909
- x: 0,
2910
- y: height - devtoolsHeight,
2911
- width: contentWidth,
2912
- height: devtoolsHeight
2913
- });
2914
- } else {
2915
- devtoolsPanelView.setBounds({ x: 0, y: height, width: 0, height: 0 });
3040
+ }
3041
+ function isTrackablePageUrl(rawUrl) {
3042
+ try {
3043
+ const url = new URL(rawUrl);
3044
+ return url.protocol === "http:" || url.protocol === "https:";
3045
+ } catch {
3046
+ return false;
2916
3047
  }
2917
- mainWindow.contentView.removeChildView(chromeView);
2918
- mainWindow.contentView.addChildView(chromeView);
2919
- mainWindow.contentView.removeChildView(sidebarView);
2920
- mainWindow.contentView.addChildView(sidebarView);
2921
- mainWindow.contentView.removeChildView(devtoolsPanelView);
2922
- mainWindow.contentView.addChildView(devtoolsPanelView);
2923
- const activeTab = tabManager.getActiveTab();
2924
- if (activeTab) {
2925
- activeTab.view.setBounds({
2926
- x: 0,
2927
- y: chromeHeight,
2928
- width: contentWidth,
2929
- height: height - chromeHeight - devtoolsHeight
2930
- });
3048
+ }
3049
+ const SAVE_DEBOUNCE_MS$2 = 500;
3050
+ const MAX_TEXT_LENGTH = 8e3;
3051
+ let snapshots = null;
3052
+ function getFilePath$1() {
3053
+ return path.join(electron.app.getPath("userData"), "vessel-page-snapshots.json");
3054
+ }
3055
+ function normalizeStoredSnapshot(value) {
3056
+ if (!value || typeof value !== "object") return null;
3057
+ const raw = value;
3058
+ if (typeof raw.url !== "string" || typeof raw.title !== "string" || typeof raw.textContent !== "string" || typeof raw.headings !== "string" || typeof raw.capturedAt !== "string") {
3059
+ return null;
2931
3060
  }
3061
+ return {
3062
+ url: raw.url,
3063
+ title: raw.title,
3064
+ textContent: raw.textContent,
3065
+ headings: raw.headings,
3066
+ capturedAt: raw.capturedAt
3067
+ };
2932
3068
  }
2933
- function resizeSidebarViews(state2) {
2934
- const { mainWindow, sidebarView, devtoolsPanelView, tabManager, uiState } = state2;
2935
- const [width, height] = mainWindow.getContentSize();
2936
- const chromeHeight = uiState.focusMode ? 0 : CHROME_HEIGHT;
2937
- const sidebarWidth = uiState.sidebarOpen ? uiState.sidebarWidth : 0;
2938
- const devtoolsHeight = uiState.devtoolsPanelOpen ? uiState.devtoolsPanelHeight : 0;
2939
- const resizeHandleOverlap = 6;
2940
- const contentWidth = width - sidebarWidth;
2941
- sidebarView.setBounds({
2942
- x: width - sidebarWidth - resizeHandleOverlap,
2943
- y: 0,
2944
- width: sidebarWidth + resizeHandleOverlap,
2945
- height
3069
+ function load$2() {
3070
+ if (snapshots) return snapshots;
3071
+ snapshots = loadJsonFile({
3072
+ filePath: getFilePath$1(),
3073
+ fallback: /* @__PURE__ */ new Map(),
3074
+ secure: true,
3075
+ parse: (raw) => {
3076
+ const next = /* @__PURE__ */ new Map();
3077
+ if (!Array.isArray(raw)) return next;
3078
+ for (const entry of raw) {
3079
+ const snapshot = normalizeStoredSnapshot(entry);
3080
+ if (snapshot) next.set(snapshot.url, snapshot);
3081
+ }
3082
+ return next;
3083
+ }
2946
3084
  });
2947
- if (uiState.devtoolsPanelOpen) {
2948
- devtoolsPanelView.setBounds({
2949
- x: 0,
2950
- y: height - devtoolsHeight,
2951
- width: contentWidth,
2952
- height: devtoolsHeight
2953
- });
2954
- }
2955
- const activeTab = tabManager.getActiveTab();
2956
- if (activeTab) {
2957
- activeTab.view.setBounds({
2958
- x: 0,
2959
- y: chromeHeight,
2960
- width: contentWidth,
2961
- height: height - chromeHeight - devtoolsHeight
2962
- });
2963
- }
3085
+ return snapshots;
3086
+ }
3087
+ const persistence$2 = createDebouncedJsonPersistence({
3088
+ debounceMs: SAVE_DEBOUNCE_MS$2,
3089
+ filePath: getFilePath$1(),
3090
+ getValue: () => snapshots,
3091
+ logLabel: "page snapshots",
3092
+ secure: true,
3093
+ serialize: (value) => Array.from(value.values()).slice(-500)
3094
+ });
3095
+ function normalizeUrl(rawUrl) {
3096
+ return buildPageSnapshotKey(rawUrl);
3097
+ }
3098
+ function shouldTrackSnapshotUrl(rawUrl) {
3099
+ return isTrackablePageUrl(rawUrl);
3100
+ }
3101
+ function getSnapshot(normalizedUrl) {
3102
+ return load$2().get(normalizedUrl);
3103
+ }
3104
+ function saveSnapshot(rawUrl, title, textContent, headings) {
3105
+ const s = load$2();
3106
+ const key = normalizeUrl(rawUrl);
3107
+ const snapshot = {
3108
+ url: key,
3109
+ title,
3110
+ textContent: textContent.slice(0, MAX_TEXT_LENGTH),
3111
+ headings: headings.map((h) => `${"#".repeat(h.level)} ${h.text}`).join("\n"),
3112
+ capturedAt: (/* @__PURE__ */ new Date()).toISOString()
3113
+ };
3114
+ s.delete(key);
3115
+ s.set(key, snapshot);
3116
+ persistence$2.schedule();
3117
+ return snapshot;
3118
+ }
3119
+ function flushPersist$2() {
3120
+ return persistence$2.flush();
2964
3121
  }
2965
3122
  const SEARCH_ENGINE_HOSTS = [
2966
3123
  "google.",
@@ -5013,36 +5170,456 @@ function normalizePageContent(value) {
5013
5170
  pageIssues: Array.isArray(page.pageIssues) ? page.pageIssues : []
5014
5171
  };
5015
5172
  }
5016
- function generateReaderHTML(page) {
5017
- const escapedTitle = escapeHtml(page.title);
5018
- const escapedByline = escapeHtml(page.byline);
5019
- const renderedContent = renderReaderContent(page);
5020
- return `<!DOCTYPE html>
5021
- <html lang="en">
5022
- <head>
5023
- <meta charset="utf-8">
5024
- <meta name="viewport" content="width=device-width, initial-scale=1">
5025
- <meta http-equiv="Content-Security-Policy" content="default-src 'none'; style-src 'unsafe-inline'; img-src data:; base-uri 'none'; form-action 'none'">
5026
- <title>${escapedTitle}</title>
5027
- <style>
5028
- * { margin: 0; padding: 0; box-sizing: border-box; }
5029
- body {
5030
- background: #1a1a1e;
5031
- color: #d4d4d8;
5032
- font-family: Charter, Georgia, serif;
5033
- font-size: 19px;
5034
- line-height: 1.7;
5035
- padding: 3rem 1.5rem;
5036
- }
5037
- .reader-container {
5038
- max-width: 680px;
5039
- margin: 0 auto;
5173
+ const latestPageDiffs = /* @__PURE__ */ new Map();
5174
+ const recentPageDiffBursts = /* @__PURE__ */ new Map();
5175
+ const pendingPageSnapshotTimers = /* @__PURE__ */ new Map();
5176
+ const pendingPageSnapshotDueAt = /* @__PURE__ */ new Map();
5177
+ const lastMutationSnapshotAt = /* @__PURE__ */ new Map();
5178
+ const lastMutationActivityAt = /* @__PURE__ */ new Map();
5179
+ const MIN_MUTATION_CAPTURE_INTERVAL_MS = 5e3;
5180
+ const SETTLE_AFTER_ACTIVITY_MS = 1500;
5181
+ function getLatestPageDiff(rawUrl) {
5182
+ if (!shouldTrackSnapshotUrl(rawUrl)) return null;
5183
+ return latestPageDiffs.get(normalizeUrl(rawUrl)) ?? null;
5184
+ }
5185
+ function summarizeDiffBurst(diff) {
5186
+ const items = diff.changes.slice(0, 2).map((change) => `${change.section}: ${change.summary}`);
5187
+ return items.join(" | ");
5188
+ }
5189
+ function enrichWithBurstHistory(key, diff) {
5190
+ const detectedAt = (/* @__PURE__ */ new Date()).toISOString();
5191
+ const nextBurst = {
5192
+ detectedAt,
5193
+ summary: summarizeDiffBurst(diff)
5194
+ };
5195
+ const bursts = [...recentPageDiffBursts.get(key) || [], nextBurst].slice(
5196
+ -5
5197
+ );
5198
+ recentPageDiffBursts.set(key, bursts);
5199
+ return {
5200
+ ...diff,
5201
+ burstCount: bursts.length,
5202
+ firstDetectedAt: bursts[0]?.detectedAt,
5203
+ lastDetectedAt: bursts[bursts.length - 1]?.detectedAt,
5204
+ recentBursts: bursts
5205
+ };
5206
+ }
5207
+ async function capturePageSnapshot(url, wc, sendToRendererViews) {
5208
+ try {
5209
+ if (!shouldTrackSnapshotUrl(url)) return;
5210
+ const key = normalizeUrl(url);
5211
+ const oldSnap = getSnapshot(key);
5212
+ const content = await extractContent(wc);
5213
+ const textContent = content.content || "";
5214
+ const title = content.title || "";
5215
+ const headings = content.headings || [];
5216
+ const currentHeadings = headings.map((h) => `${"#".repeat(h.level)} ${h.text}`).join("\n");
5217
+ if (oldSnap) {
5218
+ const diff = diffSnapshots(oldSnap, textContent, title, currentHeadings);
5219
+ if (diff.hasChanges) {
5220
+ const enrichedDiff = enrichWithBurstHistory(key, diff);
5221
+ latestPageDiffs.set(key, enrichedDiff);
5222
+ sendToRendererViews(Channels.PAGE_CHANGED, enrichedDiff);
5223
+ } else {
5224
+ latestPageDiffs.delete(key);
5225
+ }
5226
+ } else {
5227
+ latestPageDiffs.delete(key);
5228
+ recentPageDiffBursts.delete(key);
5040
5229
  }
5041
- h1 {
5042
- font-size: 1.8em;
5043
- line-height: 1.3;
5044
- margin-bottom: 0.5rem;
5045
- color: #e4e4e8;
5230
+ saveSnapshot(url, title, textContent, headings);
5231
+ } catch {
5232
+ }
5233
+ }
5234
+ function computeNextSnapshotDueAt(wcId, now, delayMs) {
5235
+ const lastCaptureAt = lastMutationSnapshotAt.get(wcId) || 0;
5236
+ const lastActivityAt = lastMutationActivityAt.get(wcId) || 0;
5237
+ const earliestAllowedAt = lastCaptureAt + MIN_MUTATION_CAPTURE_INTERVAL_MS;
5238
+ const stableAfterActivityAt = lastActivityAt ? lastActivityAt + SETTLE_AFTER_ACTIVITY_MS : 0;
5239
+ return Math.max(now + delayMs, earliestAllowedAt, stableAfterActivityAt);
5240
+ }
5241
+ function scheduleTimerAt(wc, sendToRendererViews, dueAt) {
5242
+ const wcId = wc.id;
5243
+ const existing = pendingPageSnapshotTimers.get(wcId);
5244
+ if (existing) clearTimeout(existing);
5245
+ const timer = setTimeout(() => {
5246
+ pendingPageSnapshotTimers.delete(wcId);
5247
+ pendingPageSnapshotDueAt.delete(wcId);
5248
+ if (wc.isDestroyed()) return;
5249
+ lastMutationSnapshotAt.set(wcId, Date.now());
5250
+ void capturePageSnapshot(wc.getURL(), wc, sendToRendererViews);
5251
+ }, Math.max(0, dueAt - Date.now()));
5252
+ pendingPageSnapshotTimers.set(wcId, timer);
5253
+ pendingPageSnapshotDueAt.set(wcId, dueAt);
5254
+ }
5255
+ function notePageMutationActivity(wc, sendToRendererViews) {
5256
+ if (wc.isDestroyed()) return;
5257
+ const wcId = wc.id;
5258
+ const now = Date.now();
5259
+ lastMutationActivityAt.set(wcId, now);
5260
+ const existingDueAt = pendingPageSnapshotDueAt.get(wcId);
5261
+ if (existingDueAt == null) return;
5262
+ const nextDueAt = computeNextSnapshotDueAt(wcId, now, 0);
5263
+ if (nextDueAt <= existingDueAt) return;
5264
+ scheduleTimerAt(wc, sendToRendererViews, nextDueAt);
5265
+ }
5266
+ function schedulePageSnapshotCapture(wc, sendToRendererViews, delayMs = 0) {
5267
+ if (wc.isDestroyed()) return;
5268
+ const wcId = wc.id;
5269
+ const now = Date.now();
5270
+ const nextDueAt = computeNextSnapshotDueAt(wcId, now, delayMs);
5271
+ const existingDueAt = pendingPageSnapshotDueAt.get(wcId);
5272
+ if (existingDueAt != null && existingDueAt >= nextDueAt) {
5273
+ return;
5274
+ }
5275
+ scheduleTimerAt(wc, sendToRendererViews, nextDueAt);
5276
+ }
5277
+ function enableClipboardShortcuts(view) {
5278
+ view.webContents.on("before-input-event", (event, input) => {
5279
+ if (!input.control && !input.meta) return;
5280
+ const key = input.key.toLowerCase();
5281
+ const wc = view.webContents;
5282
+ if (input.type === "keyDown") {
5283
+ if (key === "c") {
5284
+ wc.copy();
5285
+ event.preventDefault();
5286
+ } else if (key === "v") {
5287
+ wc.paste();
5288
+ event.preventDefault();
5289
+ } else if (key === "x") {
5290
+ wc.cut();
5291
+ event.preventDefault();
5292
+ } else if (key === "a") {
5293
+ wc.selectAll();
5294
+ event.preventDefault();
5295
+ }
5296
+ }
5297
+ });
5298
+ }
5299
+ const CHROME_HEIGHT = 110;
5300
+ const DEFAULT_DEVTOOLS_PANEL_HEIGHT = 250;
5301
+ const MIN_DEVTOOLS_PANEL = 120;
5302
+ const MAX_DEVTOOLS_PANEL = 600;
5303
+ async function getSidebarContextTarget(sidebarView, x, y) {
5304
+ try {
5305
+ return await sidebarView.webContents.executeJavaScript(
5306
+ `(() => {
5307
+ const el = document.elementFromPoint(${x}, ${y});
5308
+ const nav = el && typeof el.closest === "function"
5309
+ ? el.closest(".highlight-nav")
5310
+ : null;
5311
+ const label = nav?.querySelector(".highlight-nav-label")?.textContent?.trim() || "";
5312
+ return {
5313
+ inHighlightNav: !!nav,
5314
+ canRemoveCurrent: /\\d+\\s*\\/\\s*\\d+/.test(label),
5315
+ bookmarkId:
5316
+ el && typeof el.closest === "function"
5317
+ ? el.closest("[data-bookmark-id]")?.getAttribute("data-bookmark-id") || undefined
5318
+ : undefined,
5319
+ };
5320
+ })()`,
5321
+ true
5322
+ );
5323
+ } catch {
5324
+ return { inHighlightNav: false, canRemoveCurrent: false };
5325
+ }
5326
+ }
5327
+ async function showSidebarContextMenu(mainWindow, sidebarView, params) {
5328
+ const target = await getSidebarContextTarget(sidebarView, params.x, params.y);
5329
+ const menu = new electron.Menu();
5330
+ if (target.inHighlightNav) {
5331
+ if (target.canRemoveCurrent) {
5332
+ menu.append(
5333
+ new electron.MenuItem({
5334
+ label: "Remove Current Highlight",
5335
+ click: () => sidebarView.webContents.send(
5336
+ Channels.SIDEBAR_HIGHLIGHT_ACTION,
5337
+ "remove-current"
5338
+ )
5339
+ })
5340
+ );
5341
+ }
5342
+ menu.append(
5343
+ new electron.MenuItem({
5344
+ label: "Clear All Highlights",
5345
+ click: () => sidebarView.webContents.send(
5346
+ Channels.SIDEBAR_HIGHLIGHT_ACTION,
5347
+ "clear-all"
5348
+ )
5349
+ })
5350
+ );
5351
+ }
5352
+ if (target.bookmarkId) {
5353
+ if (menu.items.length > 0) {
5354
+ menu.append(new electron.MenuItem({ type: "separator" }));
5355
+ }
5356
+ menu.append(
5357
+ new electron.MenuItem({
5358
+ label: "Add Context to Chat",
5359
+ click: () => sidebarView.webContents.send(
5360
+ Channels.BOOKMARK_ADD_CONTEXT_TO_CHAT,
5361
+ target.bookmarkId
5362
+ )
5363
+ })
5364
+ );
5365
+ }
5366
+ if (params.isEditable) {
5367
+ if (menu.items.length > 0) {
5368
+ menu.append(new electron.MenuItem({ type: "separator" }));
5369
+ }
5370
+ menu.append(
5371
+ new electron.MenuItem({
5372
+ role: "undo",
5373
+ enabled: params.editFlags.canUndo
5374
+ })
5375
+ );
5376
+ menu.append(
5377
+ new electron.MenuItem({
5378
+ role: "redo",
5379
+ enabled: params.editFlags.canRedo
5380
+ })
5381
+ );
5382
+ menu.append(new electron.MenuItem({ type: "separator" }));
5383
+ menu.append(
5384
+ new electron.MenuItem({
5385
+ role: "cut",
5386
+ enabled: params.editFlags.canCut
5387
+ })
5388
+ );
5389
+ menu.append(
5390
+ new electron.MenuItem({
5391
+ role: "copy",
5392
+ enabled: params.editFlags.canCopy
5393
+ })
5394
+ );
5395
+ menu.append(
5396
+ new electron.MenuItem({
5397
+ role: "paste",
5398
+ enabled: params.editFlags.canPaste
5399
+ })
5400
+ );
5401
+ menu.append(
5402
+ new electron.MenuItem({
5403
+ role: "selectAll",
5404
+ enabled: params.editFlags.canSelectAll
5405
+ })
5406
+ );
5407
+ } else if (params.selectionText?.trim()) {
5408
+ if (menu.items.length > 0) {
5409
+ menu.append(new electron.MenuItem({ type: "separator" }));
5410
+ }
5411
+ menu.append(new electron.MenuItem({ role: "copy" }));
5412
+ }
5413
+ if (menu.items.length === 0) return;
5414
+ sidebarView.webContents.focus();
5415
+ menu.popup({ window: mainWindow });
5416
+ }
5417
+ function getWindowIconPath() {
5418
+ const candidates = [
5419
+ path.join(electron.app.getAppPath(), "resources", "vessel-icon.png"),
5420
+ path.join(process.resourcesPath, "vessel-icon.png"),
5421
+ path.join(__dirname, "../../resources/vessel-icon.png")
5422
+ ];
5423
+ return candidates.find((candidate) => fs.existsSync(candidate));
5424
+ }
5425
+ function createMainWindow(onTabStateChange) {
5426
+ const mainWindow = new electron.BaseWindow({
5427
+ width: 1280,
5428
+ height: 800,
5429
+ minWidth: 800,
5430
+ minHeight: 600,
5431
+ frame: false,
5432
+ show: false,
5433
+ backgroundColor: "#1a1a1e",
5434
+ icon: getWindowIconPath()
5435
+ });
5436
+ const chromeView = new electron.WebContentsView({
5437
+ webPreferences: {
5438
+ preload: path.join(__dirname, "../preload/index.js"),
5439
+ sandbox: true,
5440
+ contextIsolation: true,
5441
+ nodeIntegration: false
5442
+ }
5443
+ });
5444
+ chromeView.setBackgroundColor("#00000000");
5445
+ mainWindow.contentView.addChildView(chromeView);
5446
+ const sidebarView = new electron.WebContentsView({
5447
+ webPreferences: {
5448
+ preload: path.join(__dirname, "../preload/index.js"),
5449
+ sandbox: true,
5450
+ contextIsolation: true,
5451
+ nodeIntegration: false
5452
+ }
5453
+ });
5454
+ sidebarView.setBackgroundColor("#00000000");
5455
+ sidebarView.webContents.on("context-menu", (event, params) => {
5456
+ event.preventDefault();
5457
+ void showSidebarContextMenu(mainWindow, sidebarView, params);
5458
+ });
5459
+ mainWindow.contentView.addChildView(sidebarView);
5460
+ const devtoolsPanelView = new electron.WebContentsView({
5461
+ webPreferences: {
5462
+ preload: path.join(__dirname, "../preload/index.js"),
5463
+ sandbox: true,
5464
+ contextIsolation: true,
5465
+ nodeIntegration: false
5466
+ }
5467
+ });
5468
+ devtoolsPanelView.setBackgroundColor("#00000000");
5469
+ mainWindow.contentView.addChildView(devtoolsPanelView);
5470
+ enableClipboardShortcuts(chromeView);
5471
+ enableClipboardShortcuts(sidebarView);
5472
+ enableClipboardShortcuts(devtoolsPanelView);
5473
+ const settings2 = loadSettings();
5474
+ const uiState = {
5475
+ sidebarOpen: true,
5476
+ sidebarWidth: settings2.sidebarWidth,
5477
+ focusMode: false,
5478
+ settingsOpen: false,
5479
+ devtoolsPanelOpen: false,
5480
+ devtoolsPanelHeight: DEFAULT_DEVTOOLS_PANEL_HEIGHT
5481
+ };
5482
+ const tabManager = new TabManager(mainWindow, onTabStateChange);
5483
+ const sendToRendererViews = (channel, ...args) => {
5484
+ chromeView.webContents.send(channel, ...args);
5485
+ sidebarView.webContents.send(channel, ...args);
5486
+ };
5487
+ tabManager.onPageLoad((url, wc) => {
5488
+ void capturePageSnapshot(url, wc, sendToRendererViews);
5489
+ });
5490
+ const state2 = {
5491
+ mainWindow,
5492
+ chromeView,
5493
+ sidebarView,
5494
+ devtoolsPanelView,
5495
+ tabManager,
5496
+ uiState
5497
+ };
5498
+ mainWindow.on("resize", () => layoutViews(state2));
5499
+ mainWindow.on("show", () => layoutViews(state2));
5500
+ mainWindow.on("focus", () => layoutViews(state2));
5501
+ layoutViews(state2);
5502
+ return state2;
5503
+ }
5504
+ function layoutViews(state2) {
5505
+ const {
5506
+ mainWindow,
5507
+ chromeView,
5508
+ sidebarView,
5509
+ devtoolsPanelView,
5510
+ tabManager,
5511
+ uiState
5512
+ } = state2;
5513
+ const [width, height] = mainWindow.getContentSize();
5514
+ const chromeHeight = uiState.focusMode ? 0 : CHROME_HEIGHT;
5515
+ const sidebarWidth = uiState.sidebarOpen ? uiState.sidebarWidth : 0;
5516
+ const devtoolsHeight = uiState.devtoolsPanelOpen ? uiState.devtoolsPanelHeight : 0;
5517
+ const chromeNeedsFullHeight = uiState.settingsOpen;
5518
+ if (chromeNeedsFullHeight) {
5519
+ chromeView.setBounds({ x: 0, y: 0, width, height });
5520
+ } else {
5521
+ chromeView.setBounds({ x: 0, y: 0, width, height: chromeHeight });
5522
+ }
5523
+ const resizeHandleOverlap = 6;
5524
+ if (uiState.sidebarOpen) {
5525
+ sidebarView.setBounds({
5526
+ x: width - sidebarWidth - resizeHandleOverlap,
5527
+ y: chromeHeight,
5528
+ width: sidebarWidth + resizeHandleOverlap,
5529
+ height: height - chromeHeight
5530
+ });
5531
+ } else {
5532
+ sidebarView.setBounds({ x: width, y: 0, width: 0, height: 0 });
5533
+ }
5534
+ const contentWidth = width - sidebarWidth;
5535
+ if (uiState.devtoolsPanelOpen) {
5536
+ devtoolsPanelView.setBounds({
5537
+ x: 0,
5538
+ y: height - devtoolsHeight,
5539
+ width: contentWidth,
5540
+ height: devtoolsHeight
5541
+ });
5542
+ } else {
5543
+ devtoolsPanelView.setBounds({ x: 0, y: height, width: 0, height: 0 });
5544
+ }
5545
+ mainWindow.contentView.removeChildView(chromeView);
5546
+ mainWindow.contentView.addChildView(chromeView);
5547
+ mainWindow.contentView.removeChildView(sidebarView);
5548
+ mainWindow.contentView.addChildView(sidebarView);
5549
+ mainWindow.contentView.removeChildView(devtoolsPanelView);
5550
+ mainWindow.contentView.addChildView(devtoolsPanelView);
5551
+ const activeTab = tabManager.getActiveTab();
5552
+ if (activeTab) {
5553
+ activeTab.view.setBounds({
5554
+ x: 0,
5555
+ y: chromeHeight,
5556
+ width: contentWidth,
5557
+ height: height - chromeHeight - devtoolsHeight
5558
+ });
5559
+ }
5560
+ }
5561
+ function resizeSidebarViews(state2) {
5562
+ const { mainWindow, sidebarView, devtoolsPanelView, tabManager, uiState } = state2;
5563
+ const [width, height] = mainWindow.getContentSize();
5564
+ const chromeHeight = uiState.focusMode ? 0 : CHROME_HEIGHT;
5565
+ const sidebarWidth = uiState.sidebarOpen ? uiState.sidebarWidth : 0;
5566
+ const devtoolsHeight = uiState.devtoolsPanelOpen ? uiState.devtoolsPanelHeight : 0;
5567
+ const resizeHandleOverlap = 6;
5568
+ const contentWidth = width - sidebarWidth;
5569
+ sidebarView.setBounds({
5570
+ x: width - sidebarWidth - resizeHandleOverlap,
5571
+ y: 0,
5572
+ width: sidebarWidth + resizeHandleOverlap,
5573
+ height
5574
+ });
5575
+ if (uiState.devtoolsPanelOpen) {
5576
+ devtoolsPanelView.setBounds({
5577
+ x: 0,
5578
+ y: height - devtoolsHeight,
5579
+ width: contentWidth,
5580
+ height: devtoolsHeight
5581
+ });
5582
+ }
5583
+ const activeTab = tabManager.getActiveTab();
5584
+ if (activeTab) {
5585
+ activeTab.view.setBounds({
5586
+ x: 0,
5587
+ y: chromeHeight,
5588
+ width: contentWidth,
5589
+ height: height - chromeHeight - devtoolsHeight
5590
+ });
5591
+ }
5592
+ }
5593
+ function generateReaderHTML(page) {
5594
+ const escapedTitle = escapeHtml(page.title);
5595
+ const escapedByline = escapeHtml(page.byline);
5596
+ const renderedContent = renderReaderContent(page);
5597
+ return `<!DOCTYPE html>
5598
+ <html lang="en">
5599
+ <head>
5600
+ <meta charset="utf-8">
5601
+ <meta name="viewport" content="width=device-width, initial-scale=1">
5602
+ <meta http-equiv="Content-Security-Policy" content="default-src 'none'; style-src 'unsafe-inline'; img-src data:; base-uri 'none'; form-action 'none'">
5603
+ <title>${escapedTitle}</title>
5604
+ <style>
5605
+ * { margin: 0; padding: 0; box-sizing: border-box; }
5606
+ body {
5607
+ background: #1a1a1e;
5608
+ color: #d4d4d8;
5609
+ font-family: Charter, Georgia, serif;
5610
+ font-size: 19px;
5611
+ line-height: 1.7;
5612
+ padding: 3rem 1.5rem;
5613
+ }
5614
+ .reader-container {
5615
+ max-width: 680px;
5616
+ margin: 0 auto;
5617
+ }
5618
+ h1 {
5619
+ font-size: 1.8em;
5620
+ line-height: 1.3;
5621
+ margin-bottom: 0.5rem;
5622
+ color: #e4e4e8;
5046
5623
  }
5047
5624
  .byline {
5048
5625
  color: #71717a;
@@ -5123,7 +5700,7 @@ function onRuntimeHealthChange(listener) {
5123
5700
  };
5124
5701
  }
5125
5702
  function getMcpStatus() {
5126
- return state$1.mcp.status;
5703
+ return state$2.mcp.status;
5127
5704
  }
5128
5705
  function emitRuntimeHealthChange() {
5129
5706
  const snapshot = getRuntimeHealth();
@@ -5131,261 +5708,59 @@ function emitRuntimeHealthChange() {
5131
5708
  listener(snapshot);
5132
5709
  }
5133
5710
  }
5134
- const state$1 = {
5711
+ const state$2 = {
5135
5712
  userDataPath: "",
5136
5713
  settingsPath: "",
5137
- startupIssues: [],
5138
- mcp: {
5139
- configuredPort: 3100,
5140
- activePort: null,
5141
- endpoint: null,
5142
- status: "stopped",
5143
- message: "MCP server has not started yet."
5144
- }
5145
- };
5146
- function initializeRuntimeHealth(paths) {
5147
- state$1.userDataPath = paths.userDataPath;
5148
- state$1.settingsPath = paths.settingsPath;
5149
- state$1.mcp.configuredPort = paths.configuredPort;
5150
- state$1.mcp.activePort = null;
5151
- state$1.mcp.endpoint = null;
5152
- state$1.mcp.status = "stopped";
5153
- state$1.mcp.message = "MCP server has not started yet.";
5154
- emitRuntimeHealthChange();
5155
- }
5156
- function setStartupIssues(issues) {
5157
- state$1.startupIssues = issues.map((issue) => ({ ...issue }));
5158
- emitRuntimeHealthChange();
5159
- }
5160
- function getRuntimeHealth() {
5161
- return {
5162
- userDataPath: state$1.userDataPath,
5163
- settingsPath: state$1.settingsPath,
5164
- startupIssues: state$1.startupIssues.map((issue) => ({ ...issue })),
5165
- mcp: { ...state$1.mcp }
5166
- };
5167
- }
5168
- function setMcpHealth(update) {
5169
- if (typeof update.configuredPort === "number") {
5170
- state$1.mcp.configuredPort = update.configuredPort;
5171
- }
5172
- if ("activePort" in update) {
5173
- state$1.mcp.activePort = update.activePort ?? null;
5174
- }
5175
- if ("endpoint" in update) {
5176
- state$1.mcp.endpoint = update.endpoint ?? null;
5177
- }
5178
- const prevStatus = state$1.mcp.status;
5179
- state$1.mcp.status = update.status;
5180
- state$1.mcp.message = update.message;
5181
- if (prevStatus !== state$1.mcp.status) {
5182
- for (const listener of mcpStatusChangeListeners) {
5183
- listener(state$1.mcp.status);
5184
- }
5185
- }
5186
- emitRuntimeHealthChange();
5187
- }
5188
- const VAULT_FILENAME = "vessel-vault.enc";
5189
- const KEY_FILENAME = "vessel-vault.key";
5190
- const ALGORITHM = "aes-256-gcm";
5191
- const IV_LENGTH = 12;
5192
- const AUTH_TAG_LENGTH = 16;
5193
- let cachedEntries = null;
5194
- function getVaultDir() {
5195
- return electron.app.getPath("userData");
5196
- }
5197
- function getVaultPath() {
5198
- return path$1.join(getVaultDir(), VAULT_FILENAME);
5199
- }
5200
- function getKeyPath() {
5201
- return path$1.join(getVaultDir(), KEY_FILENAME);
5202
- }
5203
- function assertVaultSecretStorageAvailable() {
5204
- if (!electron.safeStorage.isEncryptionAvailable()) {
5205
- throw new Error(
5206
- "Agent Credential Vault requires OS-backed secret storage. Enable Keychain, DPAPI, or libsecret support and restart Vessel."
5207
- );
5208
- }
5209
- }
5210
- function getOrCreateEncryptionKey() {
5211
- assertVaultSecretStorageAvailable();
5212
- const keyPath = getKeyPath();
5213
- if (fs$1.existsSync(keyPath)) {
5214
- const encryptedKey = fs$1.readFileSync(keyPath);
5215
- return Buffer.from(electron.safeStorage.decryptString(encryptedKey), "utf-8");
5216
- }
5217
- const key = crypto$2.randomBytes(32);
5218
- fs$1.mkdirSync(path$1.dirname(keyPath), { recursive: true });
5219
- const encrypted = electron.safeStorage.encryptString(key.toString("utf-8"));
5220
- fs$1.writeFileSync(keyPath, encrypted, { mode: 384 });
5221
- return key;
5222
- }
5223
- function encrypt(plaintext) {
5224
- const key = getOrCreateEncryptionKey();
5225
- const iv = crypto$2.randomBytes(IV_LENGTH);
5226
- const cipher = crypto$2.createCipheriv(ALGORITHM, key, iv, {
5227
- authTagLength: AUTH_TAG_LENGTH
5228
- });
5229
- const encrypted = Buffer.concat([
5230
- cipher.update(plaintext, "utf-8"),
5231
- cipher.final()
5232
- ]);
5233
- const authTag = cipher.getAuthTag();
5234
- return Buffer.concat([iv, authTag, encrypted]);
5235
- }
5236
- function decrypt(data) {
5237
- const key = getOrCreateEncryptionKey();
5238
- const iv = data.subarray(0, IV_LENGTH);
5239
- const authTag = data.subarray(IV_LENGTH, IV_LENGTH + AUTH_TAG_LENGTH);
5240
- const ciphertext = data.subarray(IV_LENGTH + AUTH_TAG_LENGTH);
5241
- const decipher = crypto$2.createDecipheriv(ALGORITHM, key, iv, {
5242
- authTagLength: AUTH_TAG_LENGTH
5243
- });
5244
- decipher.setAuthTag(authTag);
5245
- return decipher.update(ciphertext, void 0, "utf-8") + decipher.final("utf-8");
5246
- }
5247
- function loadVault() {
5248
- if (cachedEntries) return cachedEntries;
5249
- const vaultPath = getVaultPath();
5250
- if (!fs$1.existsSync(vaultPath)) {
5251
- cachedEntries = [];
5252
- return cachedEntries;
5253
- }
5254
- try {
5255
- const raw = fs$1.readFileSync(vaultPath);
5256
- const json = decrypt(raw);
5257
- cachedEntries = JSON.parse(json);
5258
- return cachedEntries;
5259
- } catch (err) {
5260
- console.error("[Vessel Vault] Failed to load vault:", err);
5261
- throw new Error(
5262
- "Could not unlock the Agent Credential Vault. Check that OS secret storage is available and that the stored vault key can be decrypted."
5263
- );
5264
- }
5265
- }
5266
- function saveVault(entries) {
5267
- const json = JSON.stringify(entries, null, 2);
5268
- const encrypted = encrypt(json);
5269
- const vaultPath = getVaultPath();
5270
- fs$1.mkdirSync(path$1.dirname(vaultPath), { recursive: true });
5271
- fs$1.writeFileSync(vaultPath, encrypted);
5272
- fs$1.chmodSync(vaultPath, 384);
5273
- cachedEntries = entries;
5274
- }
5275
- function domainMatches(pattern, hostname) {
5276
- const p = pattern.toLowerCase().trim();
5277
- const h = hostname.toLowerCase().trim();
5278
- if (p === h) return true;
5279
- if (p.startsWith("*.")) {
5280
- const suffix = p.slice(2);
5281
- return h === suffix || h.endsWith("." + suffix);
5282
- }
5283
- return false;
5284
- }
5285
- function listEntries() {
5286
- return loadVault().map(({ password, totpSecret, ...rest }) => rest);
5287
- }
5288
- function findEntriesForDomain(url) {
5289
- let hostname;
5290
- try {
5291
- hostname = new URL(url).hostname;
5292
- } catch {
5293
- return [];
5294
- }
5295
- return loadVault().filter((e) => domainMatches(e.domainPattern, hostname)).map(({ password, totpSecret, ...rest }) => rest);
5296
- }
5297
- function addEntry(entry) {
5298
- const entries = loadVault();
5299
- const newEntry = {
5300
- ...entry,
5301
- id: crypto$2.randomUUID(),
5302
- createdAt: (/* @__PURE__ */ new Date()).toISOString(),
5303
- useCount: 0
5304
- };
5305
- entries.push(newEntry);
5306
- saveVault(entries);
5307
- return newEntry;
5308
- }
5309
- function updateEntry(id, updates) {
5310
- const entries = loadVault();
5311
- const idx = entries.findIndex((e) => e.id === id);
5312
- if (idx === -1) return null;
5313
- entries[idx] = { ...entries[idx], ...updates };
5314
- saveVault(entries);
5315
- return entries[idx];
5316
- }
5317
- function removeEntry(id) {
5318
- const entries = loadVault();
5319
- const idx = entries.findIndex((e) => e.id === id);
5320
- if (idx === -1) return false;
5321
- entries.splice(idx, 1);
5322
- saveVault(entries);
5323
- return true;
5324
- }
5325
- function recordUsage(id) {
5326
- const entries = loadVault();
5327
- const entry = entries.find((e) => e.id === id);
5328
- if (!entry) return;
5329
- entry.lastUsedAt = (/* @__PURE__ */ new Date()).toISOString();
5330
- entry.useCount += 1;
5331
- saveVault(entries);
5332
- }
5333
- function getCredential(id) {
5334
- const entry = loadVault().find((e) => e.id === id);
5335
- if (!entry) return null;
5336
- return { username: entry.username, password: entry.password };
5337
- }
5338
- function getTotpSecret(id) {
5339
- const entry = loadVault().find((e) => e.id === id);
5340
- return entry?.totpSecret ?? null;
5341
- }
5342
- function generateTotpCode(secret) {
5343
- const epoch = Math.floor(Date.now() / 1e3);
5344
- const counter = Math.floor(epoch / 30);
5345
- const base32Chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZ234567";
5346
- const cleanSecret = secret.replace(/[\s=-]/g, "").toUpperCase();
5347
- let bits = "";
5348
- for (const ch of cleanSecret) {
5349
- const val = base32Chars.indexOf(ch);
5350
- if (val === -1) continue;
5351
- bits += val.toString(2).padStart(5, "0");
5352
- }
5353
- const keyBytes = Buffer.alloc(Math.floor(bits.length / 8));
5354
- for (let i = 0; i < keyBytes.length; i++) {
5355
- keyBytes[i] = parseInt(bits.slice(i * 8, i * 8 + 8), 2);
5714
+ startupIssues: [],
5715
+ mcp: {
5716
+ configuredPort: 3100,
5717
+ activePort: null,
5718
+ endpoint: null,
5719
+ status: "stopped",
5720
+ message: "MCP server has not started yet."
5356
5721
  }
5357
- const counterBuf = Buffer.alloc(8);
5358
- counterBuf.writeUInt32BE(Math.floor(counter / 4294967296), 0);
5359
- counterBuf.writeUInt32BE(counter & 4294967295, 4);
5360
- const hmac = crypto$2.createHmac("sha1", keyBytes).update(counterBuf).digest();
5361
- const offset = hmac[hmac.length - 1] & 15;
5362
- const code = (hmac[offset] & 127) << 24 | (hmac[offset + 1] & 255) << 16 | (hmac[offset + 2] & 255) << 8 | hmac[offset + 3] & 255;
5363
- return (code % 1e6).toString().padStart(6, "0");
5722
+ };
5723
+ function initializeRuntimeHealth(paths) {
5724
+ state$2.userDataPath = paths.userDataPath;
5725
+ state$2.settingsPath = paths.settingsPath;
5726
+ state$2.mcp.configuredPort = paths.configuredPort;
5727
+ state$2.mcp.activePort = null;
5728
+ state$2.mcp.endpoint = null;
5729
+ state$2.mcp.status = "stopped";
5730
+ state$2.mcp.message = "MCP server has not started yet.";
5731
+ emitRuntimeHealthChange();
5364
5732
  }
5365
- const AUDIT_FILENAME = "vessel-vault-audit.jsonl";
5366
- const MAX_ENTRIES = 1e3;
5367
- function getAuditPath() {
5368
- return path$1.join(electron.app.getPath("userData"), AUDIT_FILENAME);
5733
+ function setStartupIssues(issues) {
5734
+ state$2.startupIssues = issues.map((issue) => ({ ...issue }));
5735
+ emitRuntimeHealthChange();
5369
5736
  }
5370
- function appendAuditEntry(entry) {
5371
- try {
5372
- const auditPath = getAuditPath();
5373
- fs$1.mkdirSync(path$1.dirname(auditPath), { recursive: true });
5374
- fs$1.appendFileSync(auditPath, JSON.stringify(entry) + "\n");
5375
- } catch (err) {
5376
- console.error("[Vessel Vault] Failed to write audit log:", err);
5377
- }
5737
+ function getRuntimeHealth() {
5738
+ return {
5739
+ userDataPath: state$2.userDataPath,
5740
+ settingsPath: state$2.settingsPath,
5741
+ startupIssues: state$2.startupIssues.map((issue) => ({ ...issue })),
5742
+ mcp: { ...state$2.mcp }
5743
+ };
5378
5744
  }
5379
- function readAuditLog(limit = 100) {
5380
- try {
5381
- const auditPath = getAuditPath();
5382
- if (!fs$1.existsSync(auditPath)) return [];
5383
- const lines = fs$1.readFileSync(auditPath, "utf-8").split("\n").filter((l) => l.trim());
5384
- return lines.slice(-Math.min(limit, MAX_ENTRIES)).map((line) => JSON.parse(line)).reverse();
5385
- } catch (err) {
5386
- console.error("[Vessel Vault] Failed to read audit log:", err);
5387
- return [];
5745
+ function setMcpHealth(update) {
5746
+ if (typeof update.configuredPort === "number") {
5747
+ state$2.mcp.configuredPort = update.configuredPort;
5748
+ }
5749
+ if ("activePort" in update) {
5750
+ state$2.mcp.activePort = update.activePort ?? null;
5751
+ }
5752
+ if ("endpoint" in update) {
5753
+ state$2.mcp.endpoint = update.endpoint ?? null;
5754
+ }
5755
+ const prevStatus = state$2.mcp.status;
5756
+ state$2.mcp.status = update.status;
5757
+ state$2.mcp.message = update.message;
5758
+ if (prevStatus !== state$2.mcp.status) {
5759
+ for (const listener of mcpStatusChangeListeners) {
5760
+ listener(state$2.mcp.status);
5761
+ }
5388
5762
  }
5763
+ emitRuntimeHealthChange();
5389
5764
  }
5390
5765
  function isRichToolResult(value) {
5391
5766
  return typeof value === "object" && value !== null && "__richResult" in value && value.__richResult === true;
@@ -9993,11 +10368,9 @@ function getBookmarkSearchMatch(args) {
9993
10368
  }
9994
10369
  const UNSORTED_ID = "unsorted";
9995
10370
  const ARCHIVE_FOLDER_NAME = "Archive";
9996
- const SAVE_DEBOUNCE_MS = 250;
9997
- let state = null;
10371
+ const SAVE_DEBOUNCE_MS$1 = 250;
10372
+ let state$1 = null;
9998
10373
  const listeners = /* @__PURE__ */ new Set();
9999
- let saveTimer = null;
10000
- let saveDirty = false;
10001
10374
  function cloneState(current) {
10002
10375
  return {
10003
10376
  folders: current.folders.map((folder) => ({ ...folder })),
@@ -10007,53 +10380,39 @@ function cloneState(current) {
10007
10380
  function getBookmarksPath() {
10008
10381
  return path.join(electron.app.getPath("userData"), "vessel-bookmarks.json");
10009
10382
  }
10010
- function load() {
10011
- if (state) return state;
10012
- try {
10013
- const raw = fs.readFileSync(getBookmarksPath(), "utf-8");
10014
- const parsed = JSON.parse(raw);
10015
- state = {
10016
- folders: Array.isArray(parsed.folders) ? parsed.folders : [],
10017
- bookmarks: Array.isArray(parsed.bookmarks) ? parsed.bookmarks : []
10018
- };
10019
- } catch {
10020
- state = { folders: [], bookmarks: [] };
10021
- }
10022
- return state;
10023
- }
10024
- function persistNow() {
10025
- saveDirty = false;
10026
- if (saveTimer) {
10027
- clearTimeout(saveTimer);
10028
- saveTimer = null;
10029
- }
10030
- return fs.promises.mkdir(path.dirname(getBookmarksPath()), { recursive: true }).then(
10031
- () => fs.promises.writeFile(
10032
- getBookmarksPath(),
10033
- JSON.stringify(state, null, 2),
10034
- "utf-8"
10035
- )
10036
- ).catch((err) => console.error("[Vessel] Failed to save bookmarks:", err));
10383
+ function load$1() {
10384
+ if (state$1) return state$1;
10385
+ state$1 = loadJsonFile({
10386
+ filePath: getBookmarksPath(),
10387
+ fallback: { folders: [], bookmarks: [] },
10388
+ parse: (raw) => {
10389
+ const parsed = raw;
10390
+ return {
10391
+ folders: Array.isArray(parsed.folders) ? parsed.folders : [],
10392
+ bookmarks: Array.isArray(parsed.bookmarks) ? parsed.bookmarks : []
10393
+ };
10394
+ }
10395
+ });
10396
+ return state$1;
10037
10397
  }
10398
+ const persistence$1 = createDebouncedJsonPersistence({
10399
+ debounceMs: SAVE_DEBOUNCE_MS$1,
10400
+ filePath: getBookmarksPath(),
10401
+ getValue: () => state$1,
10402
+ logLabel: "bookmarks"
10403
+ });
10038
10404
  function save() {
10039
- saveDirty = true;
10040
- if (saveTimer) return;
10041
- saveTimer = setTimeout(() => {
10042
- saveTimer = null;
10043
- if (saveDirty) {
10044
- void persistNow();
10045
- }
10046
- }, SAVE_DEBOUNCE_MS);
10405
+ persistence$1.schedule();
10047
10406
  }
10048
10407
  function emit() {
10049
- if (!state) return;
10050
- const snapshot = cloneState(state);
10408
+ if (!state$1) return;
10409
+ const snapshot = cloneState(state$1);
10051
10410
  for (const listener of listeners) {
10052
10411
  listener(snapshot);
10053
10412
  }
10054
10413
  }
10055
10414
  function getState() {
10056
- return cloneState(load());
10415
+ return cloneState(load$1());
10057
10416
  }
10058
10417
  function subscribe(listener) {
10059
10418
  listeners.add(listener);
@@ -10062,51 +10421,51 @@ function subscribe(listener) {
10062
10421
  };
10063
10422
  }
10064
10423
  function clearAll() {
10065
- state = { folders: [], bookmarks: [] };
10424
+ state$1 = { folders: [], bookmarks: [] };
10066
10425
  save();
10067
10426
  emit();
10068
10427
  }
10069
10428
  function getBookmark(id) {
10070
- load();
10071
- const bookmark = state.bookmarks.find((item) => item.id === id);
10429
+ load$1();
10430
+ const bookmark = state$1.bookmarks.find((item) => item.id === id);
10072
10431
  return bookmark ? { ...bookmark } : null;
10073
10432
  }
10074
10433
  function getBookmarkByUrl(url) {
10075
- load();
10434
+ load$1();
10076
10435
  const normalized = url.trim();
10077
10436
  if (!normalized) return null;
10078
- const bookmark = [...state.bookmarks].reverse().find((item) => item.url === normalized);
10437
+ const bookmark = [...state$1.bookmarks].reverse().find((item) => item.url === normalized);
10079
10438
  return bookmark ? { ...bookmark } : null;
10080
10439
  }
10081
10440
  function getBookmarkByUrlInFolder(url, folderId) {
10082
- load();
10441
+ load$1();
10083
10442
  const normalizedUrl = url.trim();
10084
10443
  if (!normalizedUrl) return null;
10085
- const targetFolderId = folderId && folderId !== UNSORTED_ID ? state.folders.find((f) => f.id === folderId)?.id ?? UNSORTED_ID : UNSORTED_ID;
10086
- const bookmark = [...state.bookmarks].reverse().find(
10444
+ const targetFolderId = folderId && folderId !== UNSORTED_ID ? state$1.folders.find((f) => f.id === folderId)?.id ?? UNSORTED_ID : UNSORTED_ID;
10445
+ const bookmark = [...state$1.bookmarks].reverse().find(
10087
10446
  (item) => item.url === normalizedUrl && item.folderId === targetFolderId
10088
10447
  );
10089
10448
  return bookmark ? { ...bookmark } : null;
10090
10449
  }
10091
10450
  function getFolder(id) {
10092
- load();
10451
+ load$1();
10093
10452
  if (!id || id === UNSORTED_ID) return null;
10094
- const folder = state.folders.find((item) => item.id === id);
10453
+ const folder = state$1.folders.find((item) => item.id === id);
10095
10454
  return folder ? { ...folder } : null;
10096
10455
  }
10097
10456
  function findFolderByName(name) {
10098
- load();
10457
+ load$1();
10099
10458
  const normalized = name.trim().toLowerCase();
10100
10459
  if (!normalized || normalized === "unsorted") return null;
10101
- const folder = state.folders.find(
10460
+ const folder = state$1.folders.find(
10102
10461
  (item) => item.name.trim().toLowerCase() === normalized
10103
10462
  );
10104
10463
  return folder ? { ...folder } : null;
10105
10464
  }
10106
10465
  function listFolderOverviews() {
10107
- load();
10466
+ load$1();
10108
10467
  const counts = /* @__PURE__ */ new Map();
10109
- for (const bookmark of state.bookmarks) {
10468
+ for (const bookmark of state$1.bookmarks) {
10110
10469
  counts.set(bookmark.folderId, (counts.get(bookmark.folderId) ?? 0) + 1);
10111
10470
  }
10112
10471
  return [
@@ -10115,7 +10474,7 @@ function listFolderOverviews() {
10115
10474
  name: "Unsorted",
10116
10475
  count: counts.get(UNSORTED_ID) ?? 0
10117
10476
  },
10118
- ...state.folders.map((folder) => ({
10477
+ ...state$1.folders.map((folder) => ({
10119
10478
  id: folder.id,
10120
10479
  name: folder.name,
10121
10480
  summary: folder.summary,
@@ -10124,10 +10483,10 @@ function listFolderOverviews() {
10124
10483
  ];
10125
10484
  }
10126
10485
  function searchBookmarks(query) {
10127
- load();
10486
+ load$1();
10128
10487
  if (!query.trim()) return [];
10129
- return state.bookmarks.map((bookmark) => {
10130
- const folder = state.folders.find(
10488
+ return state$1.bookmarks.map((bookmark) => {
10489
+ const folder = state$1.folders.find(
10131
10490
  (item) => item.id === bookmark.folderId
10132
10491
  );
10133
10492
  const { matchedFields, score } = getBookmarkSearchMatch({
@@ -10154,7 +10513,7 @@ function searchBookmarks(query) {
10154
10513
  );
10155
10514
  }
10156
10515
  function createFolderWithSummary(name, summary) {
10157
- load();
10516
+ load$1();
10158
10517
  const trimmed = name.trim();
10159
10518
  if (!trimmed) throw new Error("Folder name cannot be empty");
10160
10519
  const folder = {
@@ -10163,7 +10522,7 @@ function createFolderWithSummary(name, summary) {
10163
10522
  summary: summary?.trim() || void 0,
10164
10523
  createdAt: (/* @__PURE__ */ new Date()).toISOString()
10165
10524
  };
10166
- state.folders.push(folder);
10525
+ state$1.folders.push(folder);
10167
10526
  save();
10168
10527
  emit();
10169
10528
  return folder;
@@ -10188,13 +10547,13 @@ function saveBookmark(url, title, folderId, note) {
10188
10547
  return result.bookmark;
10189
10548
  }
10190
10549
  function saveBookmarkWithPolicy(url, title, folderId, note, options) {
10191
- load();
10550
+ load$1();
10192
10551
  const normalizedUrl = url.trim();
10193
10552
  if (!normalizedUrl) {
10194
10553
  throw new Error("Bookmark URL cannot be empty");
10195
10554
  }
10196
10555
  const normalizedTitle = title.trim() || normalizedUrl;
10197
- const targetId = folderId && folderId !== UNSORTED_ID ? state.folders.find((f) => f.id === folderId)?.id ?? UNSORTED_ID : UNSORTED_ID;
10556
+ const targetId = folderId && folderId !== UNSORTED_ID ? state$1.folders.find((f) => f.id === folderId)?.id ?? UNSORTED_ID : UNSORTED_ID;
10198
10557
  const duplicatePolicy = options?.onDuplicate ?? "ask";
10199
10558
  const existing = getBookmarkByUrlInFolder(normalizedUrl, targetId);
10200
10559
  if (existing) {
@@ -10205,7 +10564,7 @@ function saveBookmarkWithPolicy(url, title, folderId, note, options) {
10205
10564
  };
10206
10565
  }
10207
10566
  if (duplicatePolicy === "update") {
10208
- const bookmark2 = state.bookmarks.find((item) => item.id === existing.id);
10567
+ const bookmark2 = state$1.bookmarks.find((item) => item.id === existing.id);
10209
10568
  if (!bookmark2) {
10210
10569
  return {
10211
10570
  status: "conflict",
@@ -10233,7 +10592,7 @@ function saveBookmarkWithPolicy(url, title, folderId, note, options) {
10233
10592
  folderId: targetId,
10234
10593
  savedAt: (/* @__PURE__ */ new Date()).toISOString()
10235
10594
  };
10236
- state.bookmarks.push(bookmark);
10595
+ state$1.bookmarks.push(bookmark);
10237
10596
  save();
10238
10597
  emit();
10239
10598
  return {
@@ -10242,10 +10601,10 @@ function saveBookmarkWithPolicy(url, title, folderId, note, options) {
10242
10601
  };
10243
10602
  }
10244
10603
  function removeBookmark(id) {
10245
- load();
10246
- const before = state.bookmarks.length;
10247
- state.bookmarks = state.bookmarks.filter((b) => b.id !== id);
10248
- if (state.bookmarks.length !== before) {
10604
+ load$1();
10605
+ const before = state$1.bookmarks.length;
10606
+ state$1.bookmarks = state$1.bookmarks.filter((b) => b.id !== id);
10607
+ if (state$1.bookmarks.length !== before) {
10249
10608
  save();
10250
10609
  emit();
10251
10610
  return true;
@@ -10253,8 +10612,8 @@ function removeBookmark(id) {
10253
10612
  return false;
10254
10613
  }
10255
10614
  function updateBookmark(id, updates) {
10256
- load();
10257
- const bookmark = state.bookmarks.find((item) => item.id === id);
10615
+ load$1();
10616
+ const bookmark = state$1.bookmarks.find((item) => item.id === id);
10258
10617
  if (!bookmark) return null;
10259
10618
  if (typeof updates.title === "string") {
10260
10619
  const trimmed = updates.title.trim();
@@ -10265,31 +10624,31 @@ function updateBookmark(id, updates) {
10265
10624
  bookmark.note = trimmed || void 0;
10266
10625
  }
10267
10626
  if (typeof updates.folderId === "string") {
10268
- bookmark.folderId = updates.folderId && updates.folderId !== UNSORTED_ID ? state.folders.find((item) => item.id === updates.folderId)?.id ?? UNSORTED_ID : UNSORTED_ID;
10627
+ bookmark.folderId = updates.folderId && updates.folderId !== UNSORTED_ID ? state$1.folders.find((item) => item.id === updates.folderId)?.id ?? UNSORTED_ID : UNSORTED_ID;
10269
10628
  }
10270
10629
  save();
10271
10630
  emit();
10272
10631
  return { ...bookmark };
10273
10632
  }
10274
10633
  function removeFolder(id, deleteContents = false) {
10275
- load();
10276
- const exists = state.folders.some((f) => f.id === id);
10634
+ load$1();
10635
+ const exists = state$1.folders.some((f) => f.id === id);
10277
10636
  if (!exists) return false;
10278
10637
  if (deleteContents) {
10279
- state.bookmarks = state.bookmarks.filter((b) => b.folderId !== id);
10638
+ state$1.bookmarks = state$1.bookmarks.filter((b) => b.folderId !== id);
10280
10639
  } else {
10281
- state.bookmarks = state.bookmarks.map(
10640
+ state$1.bookmarks = state$1.bookmarks.map(
10282
10641
  (b) => b.folderId === id ? { ...b, folderId: UNSORTED_ID } : b
10283
10642
  );
10284
10643
  }
10285
- state.folders = state.folders.filter((f) => f.id !== id);
10644
+ state$1.folders = state$1.folders.filter((f) => f.id !== id);
10286
10645
  save();
10287
10646
  emit();
10288
10647
  return true;
10289
10648
  }
10290
10649
  function renameFolder(id, newName, summary) {
10291
- load();
10292
- const folder = state.folders.find((f) => f.id === id);
10650
+ load$1();
10651
+ const folder = state$1.folders.find((f) => f.id === id);
10293
10652
  if (!folder) return null;
10294
10653
  const trimmed = newName.trim();
10295
10654
  if (!trimmed) return null;
@@ -10299,8 +10658,8 @@ function renameFolder(id, newName, summary) {
10299
10658
  emit();
10300
10659
  return { ...folder };
10301
10660
  }
10302
- function flushPersist() {
10303
- return saveDirty ? persistNow() : Promise.resolve();
10661
+ function flushPersist$1() {
10662
+ return persistence$1.flush();
10304
10663
  }
10305
10664
  function normalizeText(text) {
10306
10665
  return text?.trim() ?? "";
@@ -10911,7 +11270,7 @@ function isInvalidTextTargetQuery(rawQuery) {
10911
11270
  return false;
10912
11271
  }
10913
11272
  function resolveTextTargetInDocument(doc, rawQuery, mode) {
10914
- function normalize(value) {
11273
+ function normalize2(value) {
10915
11274
  return String(value || "").toLowerCase().replace(/\s+/g, " ").trim();
10916
11275
  }
10917
11276
  function text(value) {
@@ -11009,7 +11368,7 @@ function resolveTextTargetInDocument(doc, rawQuery, mode) {
11009
11368
  return [ariaLabel, title, ownText].filter(Boolean).join(" ");
11010
11369
  }
11011
11370
  function scoreText(query2, candidate) {
11012
- const normalizedCandidate = normalize(candidate);
11371
+ const normalizedCandidate = normalize2(candidate);
11013
11372
  if (!normalizedCandidate) return -1;
11014
11373
  if (normalizedCandidate === query2) return 180;
11015
11374
  if (normalizedCandidate.startsWith(query2)) return 150;
@@ -11025,7 +11384,7 @@ function resolveTextTargetInDocument(doc, rawQuery, mode) {
11025
11384
  function interactiveBonus(el) {
11026
11385
  const htmlEl = el;
11027
11386
  const tag = el.tagName.toLowerCase();
11028
- const label = normalize(labelFor(el));
11387
+ const label = normalize2(labelFor(el));
11029
11388
  let score = 0;
11030
11389
  if (tag === "button") score += 40;
11031
11390
  if (tag === "a") score += 35;
@@ -11063,7 +11422,7 @@ function resolveTextTargetInDocument(doc, rawQuery, mode) {
11063
11422
  return best;
11064
11423
  }
11065
11424
  if (isInvalidTextTargetQuery(rawQuery)) return null;
11066
- const query = normalize(rawQuery);
11425
+ const query = normalize2(rawQuery);
11067
11426
  if (!query) return null;
11068
11427
  let bestInteractive = null;
11069
11428
  const interactiveSelector = "a[href], button, [role='button'], input[type='submit'], input[type='button'], input[type='radio'], input[type='checkbox'], select, textarea";
@@ -13554,7 +13913,21 @@ async function setElementValue$1(wc, selector, value) {
13554
13913
  (function() {
13555
13914
  var el = window.__vessel?.resolveShadowSelector?.(${JSON.stringify(selector)});
13556
13915
  if (!el) return "Error[stale-index]: Shadow DOM element not found — call read_page to refresh.";
13557
- if (!(el instanceof HTMLInputElement || el instanceof HTMLTextAreaElement)) return "Error[not-input]: Element is not a text input";
13916
+ if (!(el instanceof HTMLInputElement || el instanceof HTMLTextAreaElement || el instanceof HTMLSelectElement)) return "Error[not-input]: Element is not a fillable input";
13917
+ if (el.disabled || el.getAttribute("aria-disabled") === "true") return "Error[disabled]: Input is disabled";
13918
+ if (el instanceof HTMLSelectElement) {
13919
+ var requested = ${JSON.stringify(value)}.trim().toLowerCase();
13920
+ var option = Array.from(el.options).find(function(item) {
13921
+ return item.value.trim().toLowerCase() === requested ||
13922
+ (item.textContent || "").trim().toLowerCase() === requested;
13923
+ });
13924
+ if (!option) return "Error[option-not-found]: Option not found";
13925
+ el.value = option.value;
13926
+ el.focus();
13927
+ el.dispatchEvent(new Event("input", { bubbles: true }));
13928
+ el.dispatchEvent(new Event("change", { bubbles: true }));
13929
+ return "Selected: " + ((option.textContent || option.value).trim().slice(0, 100));
13930
+ }
13558
13931
  var proto = el instanceof HTMLTextAreaElement ? HTMLTextAreaElement.prototype : HTMLInputElement.prototype;
13559
13932
  var desc = Object.getOwnPropertyDescriptor(proto, "value");
13560
13933
  if (desc && desc.set) { desc.set.call(el, ${JSON.stringify(value)}); } else { el.value = ${JSON.stringify(value)}; }
@@ -13576,13 +13949,29 @@ async function setElementValue$1(wc, selector, value) {
13576
13949
  (function() {
13577
13950
  const el = document.querySelector(${JSON.stringify(selector)});
13578
13951
  if (!el) return 'Error[stale-index]: Element not found — the page may have changed. Call read_page to refresh.';
13579
- if (!(el instanceof HTMLInputElement || el instanceof HTMLTextAreaElement)) {
13580
- return 'Error[not-input]: Element is not a text input';
13952
+ if (!(el instanceof HTMLInputElement || el instanceof HTMLTextAreaElement || el instanceof HTMLSelectElement)) {
13953
+ return 'Error[not-input]: Element is not a fillable input';
13581
13954
  }
13582
13955
  if (el.disabled || el.getAttribute('aria-disabled') === 'true') {
13583
13956
  return 'Error[disabled]: Input is disabled';
13584
13957
  }
13585
13958
 
13959
+ if (el instanceof HTMLSelectElement) {
13960
+ const requested = ${JSON.stringify(value)}.trim().toLowerCase();
13961
+ const option = Array.from(el.options).find((item) => {
13962
+ const label = (item.textContent || '').trim().toLowerCase();
13963
+ return label === requested || item.value.trim().toLowerCase() === requested;
13964
+ });
13965
+ if (!option) {
13966
+ return 'Error[option-not-found]: Option not found';
13967
+ }
13968
+ el.value = option.value;
13969
+ el.focus();
13970
+ el.dispatchEvent(new Event('input', { bubbles: true }));
13971
+ el.dispatchEvent(new Event('change', { bubbles: true }));
13972
+ return 'Selected: ' + ((option.textContent || option.value).trim().slice(0, 100));
13973
+ }
13974
+
13586
13975
  const prototype = el instanceof HTMLTextAreaElement
13587
13976
  ? HTMLTextAreaElement.prototype
13588
13977
  : HTMLInputElement.prototype;
@@ -16654,6 +17043,183 @@ Exception: ${result.exceptionDetails}`);
16654
17043
  }
16655
17044
  );
16656
17045
  }
17046
+ const VAULT_FILENAME = "vessel-vault.enc";
17047
+ const KEY_FILENAME = "vessel-vault.key";
17048
+ const ALGORITHM = "aes-256-gcm";
17049
+ const IV_LENGTH = 12;
17050
+ const AUTH_TAG_LENGTH = 16;
17051
+ let cachedEntries = null;
17052
+ function getVaultDir() {
17053
+ return electron.app.getPath("userData");
17054
+ }
17055
+ function getVaultPath() {
17056
+ return path$1.join(getVaultDir(), VAULT_FILENAME);
17057
+ }
17058
+ function getKeyPath() {
17059
+ return path$1.join(getVaultDir(), KEY_FILENAME);
17060
+ }
17061
+ function assertVaultSecretStorageAvailable() {
17062
+ if (!electron.safeStorage.isEncryptionAvailable()) {
17063
+ throw new Error(
17064
+ "Agent Credential Vault requires OS-backed secret storage. Enable Keychain, DPAPI, or libsecret support and restart Vessel."
17065
+ );
17066
+ }
17067
+ }
17068
+ function getOrCreateEncryptionKey() {
17069
+ assertVaultSecretStorageAvailable();
17070
+ const keyPath = getKeyPath();
17071
+ if (fs$1.existsSync(keyPath)) {
17072
+ const encryptedKey = fs$1.readFileSync(keyPath);
17073
+ return Buffer.from(electron.safeStorage.decryptString(encryptedKey), "utf-8");
17074
+ }
17075
+ const key = crypto$2.randomBytes(32);
17076
+ fs$1.mkdirSync(path$1.dirname(keyPath), { recursive: true });
17077
+ const encrypted = electron.safeStorage.encryptString(key.toString("utf-8"));
17078
+ fs$1.writeFileSync(keyPath, encrypted, { mode: 384 });
17079
+ return key;
17080
+ }
17081
+ function encrypt(plaintext) {
17082
+ const key = getOrCreateEncryptionKey();
17083
+ const iv = crypto$2.randomBytes(IV_LENGTH);
17084
+ const cipher = crypto$2.createCipheriv(ALGORITHM, key, iv, {
17085
+ authTagLength: AUTH_TAG_LENGTH
17086
+ });
17087
+ const encrypted = Buffer.concat([
17088
+ cipher.update(plaintext, "utf-8"),
17089
+ cipher.final()
17090
+ ]);
17091
+ const authTag = cipher.getAuthTag();
17092
+ return Buffer.concat([iv, authTag, encrypted]);
17093
+ }
17094
+ function decrypt(data) {
17095
+ const key = getOrCreateEncryptionKey();
17096
+ const iv = data.subarray(0, IV_LENGTH);
17097
+ const authTag = data.subarray(IV_LENGTH, IV_LENGTH + AUTH_TAG_LENGTH);
17098
+ const ciphertext = data.subarray(IV_LENGTH + AUTH_TAG_LENGTH);
17099
+ const decipher = crypto$2.createDecipheriv(ALGORITHM, key, iv, {
17100
+ authTagLength: AUTH_TAG_LENGTH
17101
+ });
17102
+ decipher.setAuthTag(authTag);
17103
+ return decipher.update(ciphertext, void 0, "utf-8") + decipher.final("utf-8");
17104
+ }
17105
+ function loadVault() {
17106
+ if (cachedEntries) return cachedEntries;
17107
+ const vaultPath = getVaultPath();
17108
+ if (!fs$1.existsSync(vaultPath)) {
17109
+ cachedEntries = [];
17110
+ return cachedEntries;
17111
+ }
17112
+ try {
17113
+ const raw = fs$1.readFileSync(vaultPath);
17114
+ const json = decrypt(raw);
17115
+ cachedEntries = JSON.parse(json);
17116
+ return cachedEntries;
17117
+ } catch (err) {
17118
+ console.error("[Vessel Vault] Failed to load vault:", err);
17119
+ throw new Error(
17120
+ "Could not unlock the Agent Credential Vault. Check that OS secret storage is available and that the stored vault key can be decrypted."
17121
+ );
17122
+ }
17123
+ }
17124
+ function saveVault(entries) {
17125
+ const json = JSON.stringify(entries, null, 2);
17126
+ const encrypted = encrypt(json);
17127
+ const vaultPath = getVaultPath();
17128
+ fs$1.mkdirSync(path$1.dirname(vaultPath), { recursive: true });
17129
+ fs$1.writeFileSync(vaultPath, encrypted);
17130
+ fs$1.chmodSync(vaultPath, 384);
17131
+ cachedEntries = entries;
17132
+ }
17133
+ function domainMatches(pattern, hostname) {
17134
+ const p = pattern.toLowerCase().trim();
17135
+ const h = hostname.toLowerCase().trim();
17136
+ if (p === h) return true;
17137
+ if (p.startsWith("*.")) {
17138
+ const suffix = p.slice(2);
17139
+ return h === suffix || h.endsWith("." + suffix);
17140
+ }
17141
+ return false;
17142
+ }
17143
+ function listEntries() {
17144
+ return loadVault().map(({ password, totpSecret, ...rest }) => rest);
17145
+ }
17146
+ function findEntriesForDomain(url) {
17147
+ let hostname;
17148
+ try {
17149
+ hostname = new URL(url).hostname;
17150
+ } catch {
17151
+ return [];
17152
+ }
17153
+ return loadVault().filter((e) => domainMatches(e.domainPattern, hostname)).map(({ password, totpSecret, ...rest }) => rest);
17154
+ }
17155
+ function addEntry(entry) {
17156
+ const entries = loadVault();
17157
+ const newEntry = {
17158
+ ...entry,
17159
+ id: crypto$2.randomUUID(),
17160
+ createdAt: (/* @__PURE__ */ new Date()).toISOString(),
17161
+ useCount: 0
17162
+ };
17163
+ entries.push(newEntry);
17164
+ saveVault(entries);
17165
+ return newEntry;
17166
+ }
17167
+ function updateEntry(id, updates) {
17168
+ const entries = loadVault();
17169
+ const idx = entries.findIndex((e) => e.id === id);
17170
+ if (idx === -1) return null;
17171
+ entries[idx] = { ...entries[idx], ...updates };
17172
+ saveVault(entries);
17173
+ return entries[idx];
17174
+ }
17175
+ function removeEntry(id) {
17176
+ const entries = loadVault();
17177
+ const idx = entries.findIndex((e) => e.id === id);
17178
+ if (idx === -1) return false;
17179
+ entries.splice(idx, 1);
17180
+ saveVault(entries);
17181
+ return true;
17182
+ }
17183
+ function recordUsage(id) {
17184
+ const entries = loadVault();
17185
+ const entry = entries.find((e) => e.id === id);
17186
+ if (!entry) return;
17187
+ entry.lastUsedAt = (/* @__PURE__ */ new Date()).toISOString();
17188
+ entry.useCount += 1;
17189
+ saveVault(entries);
17190
+ }
17191
+ function getCredential(id) {
17192
+ const entry = loadVault().find((e) => e.id === id);
17193
+ if (!entry) return null;
17194
+ return { username: entry.username, password: entry.password };
17195
+ }
17196
+ function getTotpSecret(id) {
17197
+ const entry = loadVault().find((e) => e.id === id);
17198
+ return entry?.totpSecret ?? null;
17199
+ }
17200
+ function generateTotpCode(secret) {
17201
+ const epoch = Math.floor(Date.now() / 1e3);
17202
+ const counter = Math.floor(epoch / 30);
17203
+ const base32Chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZ234567";
17204
+ const cleanSecret = secret.replace(/[\s=-]/g, "").toUpperCase();
17205
+ let bits = "";
17206
+ for (const ch of cleanSecret) {
17207
+ const val = base32Chars.indexOf(ch);
17208
+ if (val === -1) continue;
17209
+ bits += val.toString(2).padStart(5, "0");
17210
+ }
17211
+ const keyBytes = Buffer.alloc(Math.floor(bits.length / 8));
17212
+ for (let i = 0; i < keyBytes.length; i++) {
17213
+ keyBytes[i] = parseInt(bits.slice(i * 8, i * 8 + 8), 2);
17214
+ }
17215
+ const counterBuf = Buffer.alloc(8);
17216
+ counterBuf.writeUInt32BE(Math.floor(counter / 4294967296), 0);
17217
+ counterBuf.writeUInt32BE(counter & 4294967295, 4);
17218
+ const hmac = crypto$2.createHmac("sha1", keyBytes).update(counterBuf).digest();
17219
+ const offset = hmac[hmac.length - 1] & 15;
17220
+ const code = (hmac[offset] & 127) << 24 | (hmac[offset + 1] & 255) << 16 | (hmac[offset + 2] & 255) << 8 | hmac[offset + 3] & 255;
17221
+ return (code % 1e6).toString().padStart(6, "0");
17222
+ }
16657
17223
  const sessionTrustedDomains = /* @__PURE__ */ new Set();
16658
17224
  async function requestConsent(request) {
16659
17225
  const domain = request.domain.toLowerCase();
@@ -16689,6 +17255,31 @@ async function requestConsent(request) {
16689
17255
  }
16690
17256
  return { approved: true, trustForSession };
16691
17257
  }
17258
+ const AUDIT_FILENAME = "vessel-vault-audit.jsonl";
17259
+ const MAX_ENTRIES = 1e3;
17260
+ function getAuditPath() {
17261
+ return path$1.join(electron.app.getPath("userData"), AUDIT_FILENAME);
17262
+ }
17263
+ function appendAuditEntry(entry) {
17264
+ try {
17265
+ const auditPath = getAuditPath();
17266
+ fs$1.mkdirSync(path$1.dirname(auditPath), { recursive: true });
17267
+ fs$1.appendFileSync(auditPath, JSON.stringify(entry) + "\n");
17268
+ } catch (err) {
17269
+ console.error("[Vessel Vault] Failed to write audit log:", err);
17270
+ }
17271
+ }
17272
+ function readAuditLog(limit = 100) {
17273
+ try {
17274
+ const auditPath = getAuditPath();
17275
+ if (!fs$1.existsSync(auditPath)) return [];
17276
+ const lines = fs$1.readFileSync(auditPath, "utf-8").split("\n").filter((l) => l.trim());
17277
+ return lines.slice(-Math.min(limit, MAX_ENTRIES)).map((line) => JSON.parse(line)).reverse();
17278
+ } catch (err) {
17279
+ console.error("[Vessel Vault] Failed to read audit log:", err);
17280
+ return [];
17281
+ }
17282
+ }
16692
17283
  let httpServer = null;
16693
17284
  let mcpAuthToken = null;
16694
17285
  const MCP_AUTH_FILENAME = "mcp-auth.json";
@@ -19291,7 +19882,7 @@ To analyze visually, call vision_analyze with image_url="${screenshotPath}"`
19291
19882
  color
19292
19883
  );
19293
19884
  if (persist && !durationMs && !result.startsWith("Error") && !result.includes("not found")) {
19294
- const url = normalizeUrl(wc.getURL());
19885
+ const url = normalizeUrl$1(wc.getURL());
19295
19886
  addHighlight(
19296
19887
  url,
19297
19888
  resolvedSelector ?? void 0,
@@ -19322,7 +19913,7 @@ To analyze visually, call vision_analyze with image_url="${screenshotPath}"`
19322
19913
  {},
19323
19914
  async () => {
19324
19915
  const wc = tab.view.webContents;
19325
- const url = normalizeUrl(wc.getURL());
19916
+ const url = normalizeUrl$1(wc.getURL());
19326
19917
  clearHighlightsForUrl(url);
19327
19918
  return clearHighlights(wc);
19328
19919
  }
@@ -19343,7 +19934,7 @@ To analyze visually, call vision_analyze with image_url="${screenshotPath}"`
19343
19934
  async ({ url }) => {
19344
19935
  const state2 = getState$2();
19345
19936
  const activeTab = tabManager.getActiveTab();
19346
- const activeUrl = activeTab ? normalizeUrl(activeTab.view.webContents.getURL()) : null;
19937
+ const activeUrl = activeTab ? normalizeUrl$1(activeTab.view.webContents.getURL()) : null;
19347
19938
  const activeSavedHighlights = activeUrl ? state2.highlights.filter((highlight) => highlight.url === activeUrl) : [];
19348
19939
  const liveSnapshot = activeTab && activeUrl ? await captureLiveHighlightSnapshot(
19349
19940
  activeTab.view.webContents,
@@ -19354,9 +19945,9 @@ To analyze visually, call vision_analyze with image_url="${screenshotPath}"`
19354
19945
  );
19355
19946
  if (url) {
19356
19947
  const filtered = state2.highlights.filter(
19357
- (h) => h.url === normalizeUrl(url)
19948
+ (h) => h.url === normalizeUrl$1(url)
19358
19949
  );
19359
- const normalizedUrl = normalizeUrl(url);
19950
+ const normalizedUrl = normalizeUrl$1(url);
19360
19951
  const sections2 = [];
19361
19952
  if (activeUrl && activeUrl === normalizedUrl) {
19362
19953
  if (liveSnapshot.activeSelection) {
@@ -19437,7 +20028,7 @@ ${JSON.stringify(otherHighlights, null, 2)}`
19437
20028
  }
19438
20029
  const remaining = getHighlightsForUrl(removed.url);
19439
20030
  for (const tabState of tabManager.getAllStates()) {
19440
- if (normalizeUrl(tabState.url) !== removed.url) {
20031
+ if (normalizeUrl$1(tabState.url) !== removed.url) {
19441
20032
  continue;
19442
20033
  }
19443
20034
  const tab = tabManager.getTab(tabState.id);
@@ -21694,16 +22285,491 @@ function registerScheduleHandlers(windowState, runtime2, sendToAll) {
21694
22285
  return true;
21695
22286
  });
21696
22287
  }
21697
- let activeChatProvider = null;
21698
22288
  function assertString(value, name) {
21699
22289
  if (typeof value !== "string") throw new Error(`${name} must be a string`);
21700
22290
  }
21701
22291
  function assertOptionalString(value, name) {
21702
- if (value !== void 0 && typeof value !== "string") throw new Error(`${name} must be a string`);
22292
+ if (value !== void 0 && typeof value !== "string") {
22293
+ throw new Error(`${name} must be a string`);
22294
+ }
21703
22295
  }
21704
22296
  function assertNumber(value, name) {
21705
- if (typeof value !== "number" || Number.isNaN(value)) throw new Error(`${name} must be a number`);
22297
+ if (typeof value !== "number" || Number.isNaN(value)) {
22298
+ throw new Error(`${name} must be a number`);
22299
+ }
22300
+ }
22301
+ const SAVE_DEBOUNCE_MS = 250;
22302
+ const PROFILE_FIELDS = [
22303
+ "label",
22304
+ "firstName",
22305
+ "lastName",
22306
+ "email",
22307
+ "phone",
22308
+ "organization",
22309
+ "addressLine1",
22310
+ "addressLine2",
22311
+ "city",
22312
+ "state",
22313
+ "postalCode",
22314
+ "country"
22315
+ ];
22316
+ let state = null;
22317
+ function getFilePath() {
22318
+ return path.join(electron.app.getPath("userData"), "vessel-autofill.json");
22319
+ }
22320
+ function getDefaultState() {
22321
+ return { profiles: [] };
22322
+ }
22323
+ function normalizeStoredProfile(value) {
22324
+ if (!value || typeof value !== "object") return null;
22325
+ const raw = value;
22326
+ if (typeof raw.id !== "string" || typeof raw.createdAt !== "string" || typeof raw.updatedAt !== "string") {
22327
+ return null;
22328
+ }
22329
+ const profile = {
22330
+ id: raw.id,
22331
+ createdAt: raw.createdAt,
22332
+ updatedAt: raw.updatedAt
22333
+ };
22334
+ for (const field of PROFILE_FIELDS) {
22335
+ if (typeof raw[field] !== "string") return null;
22336
+ profile[field] = raw[field];
22337
+ }
22338
+ return profile;
22339
+ }
22340
+ function load() {
22341
+ if (state) return state;
22342
+ state = loadJsonFile({
22343
+ filePath: getFilePath(),
22344
+ fallback: getDefaultState(),
22345
+ secure: true,
22346
+ parse: (raw) => {
22347
+ const parsed = raw;
22348
+ return {
22349
+ profiles: Array.isArray(parsed.profiles) ? parsed.profiles.map(normalizeStoredProfile).filter((profile) => profile !== null) : []
22350
+ };
22351
+ }
22352
+ });
22353
+ return state;
22354
+ }
22355
+ const persistence = createDebouncedJsonPersistence({
22356
+ debounceMs: SAVE_DEBOUNCE_MS,
22357
+ filePath: getFilePath(),
22358
+ getValue: () => state,
22359
+ logLabel: "autofill",
22360
+ secure: true
22361
+ });
22362
+ function listProfiles() {
22363
+ return load().profiles;
22364
+ }
22365
+ function getProfile(id) {
22366
+ return load().profiles.find((p) => p.id === id);
22367
+ }
22368
+ function addProfile(input) {
22369
+ const s = load();
22370
+ const now = (/* @__PURE__ */ new Date()).toISOString();
22371
+ const profile = {
22372
+ ...input,
22373
+ id: crypto$1.randomUUID(),
22374
+ createdAt: now,
22375
+ updatedAt: now
22376
+ };
22377
+ s.profiles.push(profile);
22378
+ persistence.schedule();
22379
+ return profile;
22380
+ }
22381
+ function updateProfile(id, updates) {
22382
+ const s = load();
22383
+ const idx = s.profiles.findIndex((p) => p.id === id);
22384
+ if (idx === -1) return null;
22385
+ s.profiles[idx] = {
22386
+ ...s.profiles[idx],
22387
+ ...updates,
22388
+ updatedAt: (/* @__PURE__ */ new Date()).toISOString()
22389
+ };
22390
+ persistence.schedule();
22391
+ return s.profiles[idx];
22392
+ }
22393
+ function deleteProfile(id) {
22394
+ const s = load();
22395
+ const len = s.profiles.length;
22396
+ s.profiles = s.profiles.filter((p) => p.id !== id);
22397
+ if (s.profiles.length === len) return false;
22398
+ persistence.schedule();
22399
+ return true;
22400
+ }
22401
+ function flushPersist() {
22402
+ return persistence.flush();
22403
+ }
22404
+ const AUTOCOMPLETE_MAP = {
22405
+ "given-name": "firstName",
22406
+ "family-name": "lastName",
22407
+ "surname": "lastName",
22408
+ "email": "email",
22409
+ "tel": "phone",
22410
+ "tel-national": "phone",
22411
+ "phone": "phone",
22412
+ "organization": "organization",
22413
+ "company": "organization",
22414
+ "street-address": "addressLine1",
22415
+ "address-line1": "addressLine1",
22416
+ "address-line2": "addressLine2",
22417
+ "address-level1": "state",
22418
+ "address-level2": "city",
22419
+ "state": "state",
22420
+ "province": "state",
22421
+ "city": "city",
22422
+ "postal-code": "postalCode",
22423
+ "zip": "postalCode",
22424
+ "zip-code": "postalCode",
22425
+ "country": "country",
22426
+ "country-name": "country",
22427
+ "country-code": "country"
22428
+ };
22429
+ const INPUT_TYPE_MAP = {
22430
+ email: "email",
22431
+ tel: "phone"
22432
+ };
22433
+ const NAME_MAP = {
22434
+ firstname: "firstName",
22435
+ first_name: "firstName",
22436
+ "first-name": "firstName",
22437
+ fname: "firstName",
22438
+ givenname: "firstName",
22439
+ lastname: "lastName",
22440
+ last_name: "lastName",
22441
+ "last-name": "lastName",
22442
+ lname: "lastName",
22443
+ surname: "lastName",
22444
+ familyname: "lastName",
22445
+ email: "email",
22446
+ e_mail: "email",
22447
+ "e-mail": "email",
22448
+ emailaddress: "email",
22449
+ mail: "email",
22450
+ phone: "phone",
22451
+ telephone: "phone",
22452
+ tel: "phone",
22453
+ mobile: "phone",
22454
+ cell: "phone",
22455
+ company: "organization",
22456
+ organization: "organization",
22457
+ organisation: "organization",
22458
+ companyname: "organization",
22459
+ address: "addressLine1",
22460
+ street: "addressLine1",
22461
+ "street-address": "addressLine1",
22462
+ address1: "addressLine1",
22463
+ "address-line1": "addressLine1",
22464
+ "addr-line1": "addressLine1",
22465
+ address2: "addressLine2",
22466
+ "address-line2": "addressLine2",
22467
+ "addr-line2": "addressLine2",
22468
+ city: "city",
22469
+ town: "city",
22470
+ locality: "city",
22471
+ state: "state",
22472
+ province: "state",
22473
+ region: "state",
22474
+ zip: "postalCode",
22475
+ zipcode: "postalCode",
22476
+ "zip-code": "postalCode",
22477
+ "postal-code": "postalCode",
22478
+ postalcode: "postalCode",
22479
+ postcode: "postalCode",
22480
+ country: "country"
22481
+ };
22482
+ const LABEL_MAP = {
22483
+ "first name": "firstName",
22484
+ "given name": "firstName",
22485
+ "last name": "lastName",
22486
+ "surname": "lastName",
22487
+ "family name": "lastName",
22488
+ email: "email",
22489
+ "e-mail": "email",
22490
+ "email address": "email",
22491
+ phone: "phone",
22492
+ telephone: "phone",
22493
+ "phone number": "phone",
22494
+ mobile: "phone",
22495
+ cell: "phone",
22496
+ company: "organization",
22497
+ organization: "organization",
22498
+ organisation: "organization",
22499
+ "company name": "organization",
22500
+ address: "addressLine1",
22501
+ "street address": "addressLine1",
22502
+ "address line 1": "addressLine1",
22503
+ "address line 2": "addressLine2",
22504
+ city: "city",
22505
+ town: "city",
22506
+ state: "state",
22507
+ province: "state",
22508
+ region: "state",
22509
+ zip: "postalCode",
22510
+ "zip code": "postalCode",
22511
+ "postal code": "postalCode",
22512
+ "post code": "postalCode",
22513
+ country: "country"
22514
+ };
22515
+ function normalize(s) {
22516
+ return s.toLowerCase().trim().replace(/[\s_-]+/g, " ");
22517
+ }
22518
+ function getFullName(profile) {
22519
+ return [profile.firstName, profile.lastName].filter(Boolean).join(" ").trim();
22520
+ }
22521
+ function mk(val, confidence, matchedBy, profileKey) {
22522
+ return { value: val, confidence, matchedBy, profileKey };
22523
+ }
22524
+ function matchField(el, profile) {
22525
+ if (el.type !== "input" && el.type !== "select" && el.type !== "textarea") return null;
22526
+ if (el.disabled) return null;
22527
+ const inputType = (el.inputType || "text").toLowerCase();
22528
+ if (inputType === "hidden" || inputType === "submit" || inputType === "button" || inputType === "file" || inputType === "image") return null;
22529
+ if (inputType === "password" || inputType === "checkbox" || inputType === "radio") return null;
22530
+ if (el.autocomplete) {
22531
+ const key = el.autocomplete.replace(/section-\w+\s+/, "").replace(/^shipping\s+|^billing\s+/, "");
22532
+ if (key === "name" || key === "additional-name") {
22533
+ const fullName = getFullName(profile);
22534
+ if (fullName) return mk(fullName, 100, "autocomplete", "fullName");
22535
+ }
22536
+ const pk = AUTOCOMPLETE_MAP[key];
22537
+ if (pk && profile[pk]) return mk(profile[pk], 100, "autocomplete", pk);
22538
+ }
22539
+ if (INPUT_TYPE_MAP[inputType]) {
22540
+ const pk = INPUT_TYPE_MAP[inputType];
22541
+ if (profile[pk]) return mk(profile[pk], 90, "inputType", pk);
22542
+ }
22543
+ if (el.name) {
22544
+ const norm = normalize(el.name);
22545
+ const pk = NAME_MAP[norm];
22546
+ if (pk && profile[pk]) return mk(profile[pk], 80, "name", pk);
22547
+ for (const [pattern, pk2] of Object.entries(NAME_MAP)) {
22548
+ if (norm.includes(pattern) && profile[pk2]) return mk(profile[pk2], 70, "name", pk2);
22549
+ }
22550
+ }
22551
+ if (el.label) {
22552
+ const norm = normalize(el.label);
22553
+ if (norm === "full name" || norm.includes("full name")) {
22554
+ const fullName = getFullName(profile);
22555
+ if (fullName) return mk(fullName, 75, "label", "fullName");
22556
+ }
22557
+ const pk = LABEL_MAP[norm];
22558
+ if (pk && profile[pk]) return mk(profile[pk], 75, "label", pk);
22559
+ for (const [pattern, pk2] of Object.entries(LABEL_MAP)) {
22560
+ if (norm.includes(pattern) && profile[pk2]) return mk(profile[pk2], 65, "label", pk2);
22561
+ }
22562
+ }
22563
+ if (el.placeholder) {
22564
+ const norm = normalize(el.placeholder);
22565
+ if (norm === "full name" || norm.includes("full name")) {
22566
+ const fullName = getFullName(profile);
22567
+ if (fullName) return mk(fullName, 50, "placeholder", "fullName");
22568
+ }
22569
+ for (const [pattern, pk] of Object.entries(LABEL_MAP)) {
22570
+ if (norm.includes(pattern) && profile[pk]) return mk(profile[pk], 50, "placeholder", pk);
22571
+ }
22572
+ }
22573
+ return null;
22574
+ }
22575
+ function matchFields(elements, profile) {
22576
+ const assigned = /* @__PURE__ */ new Map();
22577
+ for (const el of elements) {
22578
+ const candidate = matchField(el, profile);
22579
+ if (!candidate) continue;
22580
+ const existing = assigned.get(candidate.profileKey);
22581
+ if (existing && existing.confidence >= candidate.confidence) continue;
22582
+ if (el.index == null || !el.selector) continue;
22583
+ assigned.set(candidate.profileKey, {
22584
+ fieldIndex: el.index,
22585
+ selector: el.selector,
22586
+ value: candidate.value,
22587
+ confidence: candidate.confidence,
22588
+ matchedBy: candidate.matchedBy
22589
+ });
22590
+ }
22591
+ const seen = /* @__PURE__ */ new Set();
22592
+ const results = [];
22593
+ for (const match of assigned.values()) {
22594
+ if (seen.has(match.fieldIndex)) continue;
22595
+ seen.add(match.fieldIndex);
22596
+ results.push(match);
22597
+ }
22598
+ return results;
22599
+ }
22600
+ const AUTOFILL_PROFILE_FIELDS = [
22601
+ "label",
22602
+ "firstName",
22603
+ "lastName",
22604
+ "email",
22605
+ "phone",
22606
+ "organization",
22607
+ "addressLine1",
22608
+ "addressLine2",
22609
+ "city",
22610
+ "state",
22611
+ "postalCode",
22612
+ "country"
22613
+ ];
22614
+ function sanitizeAutofillProfile(value) {
22615
+ if (!value || typeof value !== "object") throw new Error("Invalid profile");
22616
+ const raw = value;
22617
+ const profile = {};
22618
+ for (const field of AUTOFILL_PROFILE_FIELDS) {
22619
+ assertString(raw[field], field);
22620
+ profile[field] = raw[field];
22621
+ }
22622
+ if (!profile.label.trim()) throw new Error("Label is required");
22623
+ return profile;
22624
+ }
22625
+ function sanitizeAutofillUpdates(value) {
22626
+ if (!value || typeof value !== "object") throw new Error("Invalid updates");
22627
+ const raw = value;
22628
+ const updates = {};
22629
+ for (const field of AUTOFILL_PROFILE_FIELDS) {
22630
+ if (!(field in raw)) continue;
22631
+ assertString(raw[field], field);
22632
+ updates[field] = raw[field];
22633
+ }
22634
+ if ("label" in updates && !updates.label?.trim()) {
22635
+ throw new Error("Label is required");
22636
+ }
22637
+ return updates;
22638
+ }
22639
+ function registerAutofillHandlers(windowState) {
22640
+ electron.ipcMain.handle(Channels.AUTOFILL_LIST, () => {
22641
+ return listProfiles();
22642
+ });
22643
+ electron.ipcMain.handle(
22644
+ Channels.AUTOFILL_ADD,
22645
+ (_, profile) => {
22646
+ return addProfile(sanitizeAutofillProfile(profile));
22647
+ }
22648
+ );
22649
+ electron.ipcMain.handle(Channels.AUTOFILL_UPDATE, (_, id, updates) => {
22650
+ assertString(id, "id");
22651
+ return updateProfile(id, sanitizeAutofillUpdates(updates));
22652
+ });
22653
+ electron.ipcMain.handle(Channels.AUTOFILL_DELETE, (_, id) => {
22654
+ assertString(id, "id");
22655
+ return deleteProfile(id);
22656
+ });
22657
+ electron.ipcMain.handle(Channels.AUTOFILL_FILL, async (_, profileId) => {
22658
+ assertString(profileId, "profileId");
22659
+ const profile = getProfile(profileId);
22660
+ if (!profile) throw new Error("Profile not found");
22661
+ const activeTab = windowState.tabManager.getActiveTab();
22662
+ const wc = activeTab?.view.webContents;
22663
+ if (!wc) throw new Error("No active tab");
22664
+ const content = await extractContent(wc);
22665
+ const elements = content.interactiveElements || [];
22666
+ const matches = matchFields(elements, profile);
22667
+ if (matches.length === 0) {
22668
+ return { filled: 0, skipped: 0, details: [] };
22669
+ }
22670
+ const fields = matches.map((match) => ({
22671
+ index: match.fieldIndex,
22672
+ selector: match.selector,
22673
+ value: match.value
22674
+ }));
22675
+ const results = await fillFormFields(wc, fields);
22676
+ const filled = results.filter(
22677
+ (result) => result.result.startsWith("Typed into:") || result.result.startsWith("Selected:")
22678
+ ).length;
22679
+ return {
22680
+ filled,
22681
+ skipped: results.length - filled,
22682
+ details: results.map((result, index) => ({
22683
+ label: elements.find((element) => element.index === matches[index]?.fieldIndex)?.label || `Field ${index + 1}`,
22684
+ value: matches[index]?.value || "",
22685
+ matchedBy: matches[index]?.matchedBy || "unknown",
22686
+ result: result.result
22687
+ }))
22688
+ };
22689
+ });
22690
+ }
22691
+ function registerPageDiffHandlers(windowState, sendToRendererViews) {
22692
+ electron.ipcMain.handle(Channels.PAGE_DIFF_GET, () => {
22693
+ const activeTab = windowState.tabManager.getActiveTab();
22694
+ const wc = activeTab?.view.webContents;
22695
+ if (!wc) return null;
22696
+ return getLatestPageDiff(wc.getURL());
22697
+ });
22698
+ electron.ipcMain.on(Channels.PAGE_DIFF_ACTIVITY, (event) => {
22699
+ const wc = event.sender;
22700
+ if (!wc || wc.isDestroyed()) return;
22701
+ notePageMutationActivity(wc, sendToRendererViews);
22702
+ });
22703
+ electron.ipcMain.on(Channels.PAGE_DIFF_DIRTY, (event) => {
22704
+ const wc = event.sender;
22705
+ if (!wc || wc.isDestroyed()) return;
22706
+ schedulePageSnapshotCapture(wc, sendToRendererViews);
22707
+ });
22708
+ }
22709
+ function registerVaultHandlers() {
22710
+ electron.ipcMain.handle(Channels.VAULT_LIST, () => {
22711
+ return listEntries();
22712
+ });
22713
+ electron.ipcMain.handle(
22714
+ Channels.VAULT_ADD,
22715
+ (_, entry) => {
22716
+ if (!entry || typeof entry !== "object") {
22717
+ throw new Error("Invalid vault entry");
22718
+ }
22719
+ assertString(entry.label, "label");
22720
+ assertString(entry.domainPattern, "domainPattern");
22721
+ assertString(entry.username, "username");
22722
+ assertString(entry.password, "password");
22723
+ if (!entry.label.trim() || !entry.domainPattern.trim() || !entry.username.trim() || !entry.password.trim()) {
22724
+ throw new Error("Label, domain, username, and password are required");
22725
+ }
22726
+ assertOptionalString(entry.totpSecret, "totpSecret");
22727
+ assertOptionalString(entry.notes, "notes");
22728
+ trackVaultAction("credential_added");
22729
+ const created = addEntry(entry);
22730
+ return {
22731
+ id: created.id,
22732
+ label: created.label,
22733
+ domainPattern: created.domainPattern,
22734
+ username: created.username
22735
+ };
22736
+ }
22737
+ );
22738
+ electron.ipcMain.handle(
22739
+ Channels.VAULT_UPDATE,
22740
+ (_, id, updates) => {
22741
+ assertString(id, "id");
22742
+ if (!updates || typeof updates !== "object") {
22743
+ throw new Error("Invalid updates");
22744
+ }
22745
+ return updateEntry(id, updates) !== null;
22746
+ }
22747
+ );
22748
+ electron.ipcMain.handle(Channels.VAULT_REMOVE, (_, id) => {
22749
+ assertString(id, "id");
22750
+ trackVaultAction("credential_removed");
22751
+ return removeEntry(id);
22752
+ });
22753
+ electron.ipcMain.handle(Channels.VAULT_AUDIT_LOG, (_, limit) => {
22754
+ return readAuditLog(limit);
22755
+ });
22756
+ }
22757
+ function registerWindowControlHandlers(mainWindow) {
22758
+ electron.ipcMain.handle(Channels.WINDOW_MINIMIZE, () => {
22759
+ mainWindow.minimize();
22760
+ });
22761
+ electron.ipcMain.handle(Channels.WINDOW_MAXIMIZE, () => {
22762
+ if (mainWindow.isMaximized()) {
22763
+ mainWindow.unmaximize();
22764
+ } else {
22765
+ mainWindow.maximize();
22766
+ }
22767
+ });
22768
+ electron.ipcMain.handle(Channels.WINDOW_CLOSE, () => {
22769
+ mainWindow.close();
22770
+ });
21706
22771
  }
22772
+ let activeChatProvider = null;
21707
22773
  const VALID_APPROVAL_MODES = /* @__PURE__ */ new Set(["auto", "confirm-dangerous", "manual"]);
21708
22774
  function registerIpcHandlers(windowState, runtime2) {
21709
22775
  const { tabManager, chromeView, sidebarView, devtoolsPanelView, mainWindow } = windowState;
@@ -21878,6 +22944,10 @@ function registerIpcHandlers(windowState, runtime2) {
21878
22944
  electron.ipcMain.handle(Channels.TAB_RELOAD, (_, id) => {
21879
22945
  tabManager.reloadTab(id);
21880
22946
  });
22947
+ electron.ipcMain.handle(Channels.TAB_STATE_GET, () => ({
22948
+ tabs: tabManager.getAllStates(),
22949
+ activeId: tabManager.getActiveTabId() || ""
22950
+ }));
21881
22951
  electron.ipcMain.handle(Channels.AI_QUERY, async (_, query, history) => {
21882
22952
  const settings2 = loadSettings();
21883
22953
  const chatConfig = settings2.chatProvider;
@@ -21989,6 +23059,16 @@ function registerIpcHandlers(windowState, runtime2) {
21989
23059
  setSetting("sidebarWidth", windowState.uiState.sidebarWidth);
21990
23060
  layoutViews(windowState);
21991
23061
  });
23062
+ electron.ipcMain.on(
23063
+ Channels.RENDERER_VIEW_READY,
23064
+ (_event, view) => {
23065
+ if (view !== "sidebar") return;
23066
+ if (!windowState.uiState.sidebarOpen) {
23067
+ windowState.uiState.sidebarOpen = true;
23068
+ layoutViews(windowState);
23069
+ }
23070
+ }
23071
+ );
21992
23072
  electron.ipcMain.handle(Channels.FOCUS_MODE_TOGGLE, () => {
21993
23073
  windowState.uiState.focusMode = !windowState.uiState.focusMode;
21994
23074
  layoutViews(windowState);
@@ -22302,56 +23382,8 @@ function registerIpcHandlers(windowState, runtime2) {
22302
23382
  }
22303
23383
  return result;
22304
23384
  });
22305
- electron.ipcMain.handle(Channels.VAULT_LIST, () => {
22306
- return listEntries();
22307
- });
22308
- electron.ipcMain.handle(
22309
- Channels.VAULT_ADD,
22310
- (_, entry) => {
22311
- if (!entry || typeof entry !== "object") throw new Error("Invalid vault entry");
22312
- assertString(entry.label, "label");
22313
- assertString(entry.domainPattern, "domainPattern");
22314
- assertString(entry.username, "username");
22315
- assertString(entry.password, "password");
22316
- if (!entry.label.trim() || !entry.domainPattern.trim() || !entry.username.trim() || !entry.password.trim()) {
22317
- throw new Error("Label, domain, username, and password are required");
22318
- }
22319
- assertOptionalString(entry.totpSecret, "totpSecret");
22320
- assertOptionalString(entry.notes, "notes");
22321
- trackVaultAction("credential_added");
22322
- const created = addEntry(entry);
22323
- return { id: created.id, label: created.label, domainPattern: created.domainPattern, username: created.username };
22324
- }
22325
- );
22326
- electron.ipcMain.handle(
22327
- Channels.VAULT_UPDATE,
22328
- (_, id, updates) => {
22329
- assertString(id, "id");
22330
- if (!updates || typeof updates !== "object") throw new Error("Invalid updates");
22331
- return updateEntry(id, updates) !== null;
22332
- }
22333
- );
22334
- electron.ipcMain.handle(Channels.VAULT_REMOVE, (_, id) => {
22335
- assertString(id, "id");
22336
- trackVaultAction("credential_removed");
22337
- return removeEntry(id);
22338
- });
22339
- electron.ipcMain.handle(Channels.VAULT_AUDIT_LOG, (_, limit) => {
22340
- return readAuditLog(limit);
22341
- });
22342
- electron.ipcMain.handle(Channels.WINDOW_MINIMIZE, () => {
22343
- mainWindow.minimize();
22344
- });
22345
- electron.ipcMain.handle(Channels.WINDOW_MAXIMIZE, () => {
22346
- if (mainWindow.isMaximized()) {
22347
- mainWindow.unmaximize();
22348
- } else {
22349
- mainWindow.maximize();
22350
- }
22351
- });
22352
- electron.ipcMain.handle(Channels.WINDOW_CLOSE, () => {
22353
- mainWindow.close();
22354
- });
23385
+ registerVaultHandlers();
23386
+ registerWindowControlHandlers(mainWindow);
22355
23387
  electron.ipcMain.handle(Channels.AUTOMATION_GET_INSTALLED, () => {
22356
23388
  return getInstalledKits();
22357
23389
  });
@@ -22363,6 +23395,8 @@ function registerIpcHandlers(windowState, runtime2) {
22363
23395
  return uninstallKit(id, getScheduledKitIds());
22364
23396
  });
22365
23397
  registerScheduleHandlers(windowState, runtime2, sendToRendererViews);
23398
+ registerAutofillHandlers(windowState);
23399
+ registerPageDiffHandlers(windowState, sendToRendererViews);
22366
23400
  }
22367
23401
  function makeStep(label, status = "pending") {
22368
23402
  return { label, status };
@@ -23736,6 +24770,7 @@ async function bootstrap() {
23736
24770
  windowState.mainWindow.show();
23737
24771
  closeSplash(splash, 0);
23738
24772
  };
24773
+ let didInitializeChromeRenderer = false;
23739
24774
  const splashTimeout = setTimeout(() => {
23740
24775
  console.warn("[bootstrap] Renderer did not finish loading before splash timeout");
23741
24776
  revealMainWindow();
@@ -23762,8 +24797,9 @@ async function bootstrap() {
23762
24797
  installDownloadHandler(chromeView);
23763
24798
  startBackgroundRevalidation();
23764
24799
  startTelemetry();
23765
- loadRenderers(chromeView, sidebarView, devtoolsPanelView);
23766
- chromeView.webContents.once("did-finish-load", () => {
24800
+ const initializeChromeRenderer = () => {
24801
+ if (didInitializeChromeRenderer) return;
24802
+ didInitializeChromeRenderer = true;
23767
24803
  const savedSession = runtime.getState().session;
23768
24804
  if (settings2.autoRestoreSession && savedSession?.tabs.length) {
23769
24805
  runtime.restoreSession(savedSession);
@@ -23776,6 +24812,12 @@ async function bootstrap() {
23776
24812
  clearTimeout(splashTimeout);
23777
24813
  revealMainWindow();
23778
24814
  void maybeShowStartupHealthDialog(windowState);
24815
+ };
24816
+ chromeView.webContents.once("dom-ready", () => {
24817
+ initializeChromeRenderer();
24818
+ });
24819
+ chromeView.webContents.once("did-finish-load", () => {
24820
+ initializeChromeRenderer();
23779
24821
  });
23780
24822
  chromeView.webContents.once(
23781
24823
  "did-fail-load",
@@ -23791,6 +24833,7 @@ async function bootstrap() {
23791
24833
  revealMainWindow();
23792
24834
  }
23793
24835
  );
24836
+ loadRenderers(chromeView, sidebarView, devtoolsPanelView);
23794
24837
  startMcpServer(tabManager, runtime, settings2.mcpPort).catch((err) => {
23795
24838
  console.error("[bootstrap] MCP server failed to start:", err);
23796
24839
  });
@@ -23815,10 +24858,12 @@ electron.app.on("window-all-closed", () => {
23815
24858
  stopBackgroundRevalidation();
23816
24859
  void Promise.all([
23817
24860
  runtime?.flushPersist() ?? Promise.resolve(),
23818
- flushPersist(),
23819
24861
  flushPersist$1(),
24862
+ flushPersist$3(),
24863
+ flushPersist$4(),
24864
+ flushPersist(),
23820
24865
  flushPersist$2(),
23821
- flushPersist$3()
24866
+ flushPersist$5()
23822
24867
  ]).finally(() => {
23823
24868
  void stopMcpServer().finally(() => {
23824
24869
  electron.app.quit();