@quanta-intellect/vessel-browser 0.1.53 → 0.1.56

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 = `
@@ -1433,61 +1512,45 @@ function persistHighlight(url, text) {
1433
1512
  return { success: true, text: capped, id: highlight.id };
1434
1513
  }
1435
1514
  const MAX_HISTORY_ENTRIES = 5e3;
1436
- const SAVE_DEBOUNCE_MS$1 = 250;
1437
- let state$2 = null;
1515
+ const SAVE_DEBOUNCE_MS$3 = 250;
1516
+ let state$3 = null;
1438
1517
  const listeners$1 = /* @__PURE__ */ new Set();
1439
- let saveTimer$1 = null;
1440
- let saveDirty$1 = false;
1441
1518
  function getHistoryPath() {
1442
1519
  return path.join(electron.app.getPath("userData"), "vessel-history.json");
1443
1520
  }
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));
1521
+ function load$3() {
1522
+ if (state$3) return state$3;
1523
+ state$3 = loadJsonFile({
1524
+ filePath: getHistoryPath(),
1525
+ fallback: { entries: [] },
1526
+ parse: (raw) => {
1527
+ const parsed = raw;
1528
+ return {
1529
+ entries: Array.isArray(parsed.entries) ? parsed.entries : []
1530
+ };
1531
+ }
1532
+ });
1533
+ return state$3;
1470
1534
  }
1535
+ const persistence$3 = createDebouncedJsonPersistence({
1536
+ debounceMs: SAVE_DEBOUNCE_MS$3,
1537
+ filePath: getHistoryPath(),
1538
+ getValue: () => state$3,
1539
+ logLabel: "history"
1540
+ });
1471
1541
  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);
1542
+ persistence$3.schedule();
1480
1543
  }
1481
1544
  function emit$1() {
1482
- if (!state$2) return;
1483
- const snapshot = { entries: [...state$2.entries] };
1545
+ if (!state$3) return;
1546
+ const snapshot = { entries: [...state$3.entries] };
1484
1547
  for (const listener of listeners$1) {
1485
1548
  listener(snapshot);
1486
1549
  }
1487
1550
  }
1488
1551
  function getState$1() {
1489
- load$1();
1490
- return { entries: [...state$2.entries] };
1552
+ load$3();
1553
+ return { entries: [...state$3.entries] };
1491
1554
  }
1492
1555
  function subscribe$1(listener) {
1493
1556
  listeners$1.add(listener);
@@ -1497,8 +1560,8 @@ function subscribe$1(listener) {
1497
1560
  }
1498
1561
  function addEntry$1(url, title) {
1499
1562
  if (!url || url === "about:blank") return;
1500
- load$1();
1501
- const last = state$2.entries[0];
1563
+ load$3();
1564
+ const last = state$3.entries[0];
1502
1565
  if (last && last.url === url) {
1503
1566
  if (title && title !== last.title) {
1504
1567
  last.title = title;
@@ -1512,28 +1575,28 @@ function addEntry$1(url, title) {
1512
1575
  title: title || url,
1513
1576
  visitedAt: (/* @__PURE__ */ new Date()).toISOString()
1514
1577
  };
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);
1578
+ state$3.entries.unshift(entry);
1579
+ if (state$3.entries.length > MAX_HISTORY_ENTRIES) {
1580
+ state$3.entries = state$3.entries.slice(0, MAX_HISTORY_ENTRIES);
1518
1581
  }
1519
1582
  save$1();
1520
1583
  emit$1();
1521
1584
  }
1522
1585
  function search(query, limit = 50) {
1523
- load$1();
1524
- if (!query.trim()) return state$2.entries.slice(0, limit);
1586
+ load$3();
1587
+ if (!query.trim()) return state$3.entries.slice(0, limit);
1525
1588
  const normalized = query.toLowerCase();
1526
- return state$2.entries.filter(
1589
+ return state$3.entries.filter(
1527
1590
  (e) => e.url.toLowerCase().includes(normalized) || e.title.toLowerCase().includes(normalized)
1528
1591
  ).slice(0, limit);
1529
1592
  }
1530
1593
  function clearAll$1() {
1531
- state$2 = { entries: [] };
1594
+ state$3 = { entries: [] };
1532
1595
  save$1();
1533
1596
  emit$1();
1534
1597
  }
1535
- function flushPersist$1() {
1536
- return saveDirty$1 ? persistNow$1() : Promise.resolve();
1598
+ function flushPersist$3() {
1599
+ return persistence$3.flush();
1537
1600
  }
1538
1601
  const MAX_CONSOLE_ENTRIES = 500;
1539
1602
  const MAX_NETWORK_ENTRIES = 200;
@@ -2246,10 +2309,14 @@ class TabManager {
2246
2309
  window;
2247
2310
  onStateChange;
2248
2311
  highlightCaptureCallback = null;
2312
+ pageLoadCallback = null;
2249
2313
  constructor(window2, onStateChange) {
2250
2314
  this.window = window2;
2251
2315
  this.onStateChange = onStateChange;
2252
2316
  }
2317
+ onPageLoad(cb) {
2318
+ this.pageLoadCallback = cb;
2319
+ }
2253
2320
  createTab(url = "about:blank", options) {
2254
2321
  const background = options?.background ?? false;
2255
2322
  const id = crypto$1.randomUUID();
@@ -2262,6 +2329,7 @@ class TabManager {
2262
2329
  onPageLoad: (pageUrl, wc) => {
2263
2330
  this.reapplyHighlights(pageUrl, wc);
2264
2331
  addEntry$1(pageUrl, wc.getTitle());
2332
+ this.pageLoadCallback?.(pageUrl, wc);
2265
2333
  },
2266
2334
  onHighlightSelection: (wc) => this.captureHighlightFromPage(wc),
2267
2335
  onHighlightRemove: (url2, text) => this.removeHighlightByText(url2, text),
@@ -2415,7 +2483,7 @@ class TabManager {
2415
2483
  const wcId = wc.id;
2416
2484
  const now = Date.now();
2417
2485
  const last = this.lastReapply.get(wcId);
2418
- const normalized = normalizeUrl(url);
2486
+ const normalized = normalizeUrl$1(url);
2419
2487
  if (last && last.url === normalized && now - last.at < 500) return;
2420
2488
  this.lastReapply.set(wcId, { url: normalized, at: now });
2421
2489
  const highlights = getHighlightsForUrl(url);
@@ -2464,14 +2532,14 @@ class TabManager {
2464
2532
  if (highlight) {
2465
2533
  removeHighlight(highlight.id);
2466
2534
  }
2467
- const normalized = normalizeUrl(url);
2535
+ const normalized = normalizeUrl$1(url);
2468
2536
  for (const id of this.order) {
2469
2537
  const tab = this.tabs.get(id);
2470
2538
  if (!tab) continue;
2471
2539
  const wc = tab.view.webContents;
2472
2540
  if (wc.isDestroyed()) continue;
2473
2541
  try {
2474
- const tabUrl = normalizeUrl(wc.getURL());
2542
+ const tabUrl = normalizeUrl$1(wc.getURL());
2475
2543
  if (tabUrl === normalized) {
2476
2544
  void this.removeHighlightMarksForText(wc, text);
2477
2545
  }
@@ -2488,14 +2556,14 @@ class TabManager {
2488
2556
  if (highlight) {
2489
2557
  updateHighlightColor(highlight.id, color);
2490
2558
  }
2491
- const normalized = normalizeUrl(url);
2559
+ const normalized = normalizeUrl$1(url);
2492
2560
  for (const id of this.order) {
2493
2561
  const tab = this.tabs.get(id);
2494
2562
  if (!tab) continue;
2495
2563
  const wc = tab.view.webContents;
2496
2564
  if (wc.isDestroyed()) continue;
2497
2565
  try {
2498
- const tabUrl = normalizeUrl(wc.getURL());
2566
+ const tabUrl = normalizeUrl$1(wc.getURL());
2499
2567
  if (tabUrl === normalized) {
2500
2568
  void this.removeHighlightMarksForText(wc, text).then(() => {
2501
2569
  void highlightOnPage(
@@ -2651,316 +2719,399 @@ const Channels = {
2651
2719
  // Window controls
2652
2720
  WINDOW_MINIMIZE: "window:minimize",
2653
2721
  WINDOW_MAXIMIZE: "window:maximize",
2654
- WINDOW_CLOSE: "window:close"
2722
+ WINDOW_CLOSE: "window:close",
2723
+ // Autofill
2724
+ AUTOFILL_LIST: "autofill:list",
2725
+ AUTOFILL_ADD: "autofill:add",
2726
+ AUTOFILL_UPDATE: "autofill:update",
2727
+ AUTOFILL_DELETE: "autofill:delete",
2728
+ AUTOFILL_FILL: "autofill:fill",
2729
+ // Page snapshots / What Changed
2730
+ PAGE_DIFF_ACTIVITY: "page:diff-activity",
2731
+ PAGE_CHANGED: "page:changed",
2732
+ PAGE_DIFF_GET: "page:diff-get",
2733
+ PAGE_DIFF_DIRTY: "page:diff-dirty"
2655
2734
  };
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
- }
2735
+ const MAX_DETAIL_ITEMS = 3;
2736
+ const MIN_BLOCK_SIMILARITY = 0.82;
2737
+ function normalizeText$2(value) {
2738
+ return value.replace(/\s+/g, " ").trim();
2739
+ }
2740
+ function truncateText(value, max = 180) {
2741
+ const normalized = normalizeText$2(value);
2742
+ if (normalized.length <= max) return normalized;
2743
+ return `${normalized.slice(0, max - 3)}...`;
2744
+ }
2745
+ function tokenize(text) {
2746
+ return normalizeText$2(text).toLowerCase().split(/\s+/).filter(Boolean);
2747
+ }
2748
+ function countOverlap(a, b) {
2749
+ if (a.length === 0 || b.length === 0) return 0;
2750
+ const counts = /* @__PURE__ */ new Map();
2751
+ for (const token of b) {
2752
+ counts.set(token, (counts.get(token) || 0) + 1);
2753
+ }
2754
+ let overlap = 0;
2755
+ for (const token of a) {
2756
+ const remaining = counts.get(token) || 0;
2757
+ if (remaining > 0) {
2758
+ overlap += 1;
2759
+ counts.set(token, remaining - 1);
2760
+ }
2761
+ }
2762
+ return overlap;
2763
+ }
2764
+ function similarityScore(a, b) {
2765
+ const aTokens = tokenize(a);
2766
+ const bTokens = tokenize(b);
2767
+ if (aTokens.length === 0 && bTokens.length === 0) return 1;
2768
+ if (aTokens.length === 0 || bTokens.length === 0) return 0;
2769
+ return countOverlap(aTokens, bTokens) / Math.max(aTokens.length, bTokens.length);
2770
+ }
2771
+ function extractTextBlocks(text) {
2772
+ const compact = text.replace(/\r\n/g, "\n").trim();
2773
+ if (!compact) return [];
2774
+ const paragraphs = compact.split(/\n\s*\n+/).map((block) => normalizeText$2(block)).filter(Boolean);
2775
+ if (paragraphs.length > 1) return paragraphs;
2776
+ return compact.split(/\n+/).map((line) => normalizeText$2(line)).filter(Boolean);
2777
+ }
2778
+ function buildLcsTable(a, b) {
2779
+ const table = Array.from(
2780
+ { length: a.length + 1 },
2781
+ () => Array.from({ length: b.length + 1 }).fill(0)
2782
+ );
2783
+ for (let i = a.length - 1; i >= 0; i -= 1) {
2784
+ for (let j = b.length - 1; j >= 0; j -= 1) {
2785
+ table[i][j] = a[i] === b[j] ? table[i + 1][j + 1] + 1 : Math.max(table[i + 1][j], table[i][j + 1]);
2786
+ }
2787
+ }
2788
+ return table;
2789
+ }
2790
+ function diffBlocks(oldBlocks, newBlocks) {
2791
+ const table = buildLcsTable(oldBlocks, newBlocks);
2792
+ const ops = [];
2793
+ let i = 0;
2794
+ let j = 0;
2795
+ while (i < oldBlocks.length && j < newBlocks.length) {
2796
+ if (oldBlocks[i] === newBlocks[j]) {
2797
+ ops.push({ type: "equal", value: oldBlocks[i] });
2798
+ i += 1;
2799
+ j += 1;
2800
+ continue;
2675
2801
  }
2676
- });
2802
+ if (table[i + 1][j] >= table[i][j + 1]) {
2803
+ ops.push({ type: "removed", value: oldBlocks[i] });
2804
+ i += 1;
2805
+ } else {
2806
+ ops.push({ type: "added", value: newBlocks[j] });
2807
+ j += 1;
2808
+ }
2809
+ }
2810
+ while (i < oldBlocks.length) {
2811
+ ops.push({ type: "removed", value: oldBlocks[i] });
2812
+ i += 1;
2813
+ }
2814
+ while (j < newBlocks.length) {
2815
+ ops.push({ type: "added", value: newBlocks[j] });
2816
+ j += 1;
2817
+ }
2818
+ return ops;
2677
2819
  }
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
2820
+ function summarizeContentChange(changedCount, addedCount, removedCount) {
2821
+ const parts = [];
2822
+ if (changedCount > 0) {
2823
+ parts.push(
2824
+ `${changedCount} updated ${changedCount === 1 ? "section" : "sections"}`
2701
2825
  );
2702
- } catch {
2703
- return { inHighlightNav: false, canRemoveCurrent: false };
2704
2826
  }
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
- })
2827
+ if (addedCount > 0) {
2828
+ parts.push(
2829
+ `${addedCount} added ${addedCount === 1 ? "section" : "sections"}`
2729
2830
  );
2730
2831
  }
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
- })
2832
+ if (removedCount > 0) {
2833
+ parts.push(
2834
+ `${removedCount} removed ${removedCount === 1 ? "section" : "sections"}`
2743
2835
  );
2744
2836
  }
2745
- if (params.isEditable) {
2746
- if (menu.items.length > 0) {
2747
- menu.append(new electron.MenuItem({ type: "separator" }));
2837
+ return parts.join(", ");
2838
+ }
2839
+ function diffSnapshots(oldSnap, currentContent, currentTitle, currentHeadings) {
2840
+ const changes = [];
2841
+ if (oldSnap.title !== currentTitle) {
2842
+ changes.push({
2843
+ kind: "changed",
2844
+ section: "title",
2845
+ summary: `"${oldSnap.title}" → "${currentTitle}"`,
2846
+ before: oldSnap.title,
2847
+ after: currentTitle
2848
+ });
2849
+ }
2850
+ const oldHeadings = oldSnap.headings.split("\n").filter(Boolean);
2851
+ const newHeadings = currentHeadings.split("\n").filter(Boolean);
2852
+ if (oldHeadings.join("\n") !== newHeadings.join("\n")) {
2853
+ const added = newHeadings.filter((h) => !oldHeadings.includes(h));
2854
+ const removed = oldHeadings.filter((h) => !newHeadings.includes(h));
2855
+ const parts = [];
2856
+ if (added.length > 0) parts.push(`New: ${added.join(", ")}`);
2857
+ if (removed.length > 0) parts.push(`Gone: ${removed.join(", ")}`);
2858
+ if (parts.length > 0) {
2859
+ changes.push({
2860
+ kind: added.length > 0 && removed.length > 0 ? "changed" : added.length > 0 ? "added" : "removed",
2861
+ section: "headings",
2862
+ summary: parts.join(". "),
2863
+ addedItems: added.slice(0, MAX_DETAIL_ITEMS),
2864
+ removedItems: removed.slice(0, MAX_DETAIL_ITEMS)
2865
+ });
2748
2866
  }
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" }));
2867
+ }
2868
+ const oldBlocks = extractTextBlocks(oldSnap.textContent);
2869
+ const newBlocks = extractTextBlocks(currentContent);
2870
+ const overallSimilarity = similarityScore(oldSnap.textContent, currentContent);
2871
+ if (overallSimilarity < 0.98) {
2872
+ const ops = diffBlocks(oldBlocks, newBlocks);
2873
+ const addedBlocks = [];
2874
+ const removedBlocks = [];
2875
+ const changedPairs = [];
2876
+ let idx = 0;
2877
+ while (idx < ops.length) {
2878
+ if (ops[idx]?.type === "equal") {
2879
+ idx += 1;
2880
+ continue;
2881
+ }
2882
+ const pendingRemoved = [];
2883
+ const pendingAdded = [];
2884
+ while (idx < ops.length && ops[idx]?.type !== "equal") {
2885
+ const op = ops[idx];
2886
+ if (op?.type === "removed") pendingRemoved.push(op.value);
2887
+ if (op?.type === "added") pendingAdded.push(op.value);
2888
+ idx += 1;
2889
+ }
2890
+ while (pendingRemoved.length > 0 && pendingAdded.length > 0) {
2891
+ const before = pendingRemoved[0];
2892
+ const after = pendingAdded[0];
2893
+ if (similarityScore(before, after) < MIN_BLOCK_SIMILARITY) break;
2894
+ changedPairs.push({ before, after });
2895
+ pendingRemoved.shift();
2896
+ pendingAdded.shift();
2897
+ }
2898
+ removedBlocks.push(...pendingRemoved);
2899
+ addedBlocks.push(...pendingAdded);
2900
+ }
2901
+ if (changedPairs.length > 0 || addedBlocks.length > 0 || removedBlocks.length > 0) {
2902
+ changes.push({
2903
+ kind: "changed",
2904
+ section: "content",
2905
+ summary: summarizeContentChange(
2906
+ changedPairs.length,
2907
+ addedBlocks.length,
2908
+ removedBlocks.length
2909
+ ),
2910
+ before: changedPairs[0] ? truncateText(changedPairs[0].before) : removedBlocks[0] ? truncateText(removedBlocks[0]) : void 0,
2911
+ after: changedPairs[0] ? truncateText(changedPairs[0].after) : addedBlocks[0] ? truncateText(addedBlocks[0]) : void 0,
2912
+ addedItems: addedBlocks.slice(0, MAX_DETAIL_ITEMS).map((item) => truncateText(item)),
2913
+ removedItems: removedBlocks.slice(0, MAX_DETAIL_ITEMS).map((item) => truncateText(item))
2914
+ });
2789
2915
  }
2790
- menu.append(new electron.MenuItem({ role: "copy" }));
2791
2916
  }
2792
- if (menu.items.length === 0) return;
2793
- sidebarView.webContents.focus();
2794
- menu.popup({ window: mainWindow });
2917
+ return {
2918
+ url: oldSnap.url,
2919
+ hasChanges: changes.length > 0,
2920
+ oldSnapshot: { capturedAt: oldSnap.capturedAt, title: oldSnap.title },
2921
+ changes
2922
+ };
2795
2923
  }
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));
2924
+ function normalizePageUrl(rawUrl) {
2925
+ try {
2926
+ const url = new URL(rawUrl);
2927
+ const pathname = url.pathname.replace(/\/+$/, "") || "/";
2928
+ return `${url.origin}${pathname}`.toLowerCase();
2929
+ } catch {
2930
+ return rawUrl.trim().replace(/\/+$/, "").toLowerCase();
2931
+ }
2803
2932
  }
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;
2933
+ const SNAPSHOT_QUERY_KEYS = /* @__PURE__ */ new Set([
2934
+ "q",
2935
+ "query",
2936
+ "search",
2937
+ "s",
2938
+ "term",
2939
+ "keyword",
2940
+ "keywords",
2941
+ "page",
2942
+ "p",
2943
+ "offset",
2944
+ "cursor",
2945
+ "sort",
2946
+ "order",
2947
+ "filter",
2948
+ "filters",
2949
+ "category",
2950
+ "categories",
2951
+ "tag",
2952
+ "tags",
2953
+ "tab",
2954
+ "view"
2955
+ ]);
2956
+ const TRACKING_QUERY_KEYS = /* @__PURE__ */ new Set([
2957
+ "fbclid",
2958
+ "gclid",
2959
+ "mc_cid",
2960
+ "mc_eid",
2961
+ "ref",
2962
+ "source",
2963
+ "si"
2964
+ ]);
2965
+ function normalizeQueryValue(value) {
2966
+ return value.replace(/\s+/g, " ").trim().toLowerCase();
2967
+ }
2968
+ function serializeSnapshotParams(params) {
2969
+ return params.map(
2970
+ ([key, value]) => `${encodeURIComponent(key)}=${encodeURIComponent(value)}`
2971
+ ).join("&");
2972
+ }
2973
+ function normalizeSnapshotParams(entries, pathname) {
2974
+ return Array.from(entries).filter(
2975
+ ([key, value]) => shouldKeepSnapshotQueryParam(pathname, key, value)
2976
+ ).map(([key, value]) => [
2977
+ key.trim().toLowerCase(),
2978
+ normalizeQueryValue(value)
2979
+ ]).sort(
2980
+ ([keyA, valueA], [keyB, valueB]) => keyA === keyB ? valueA.localeCompare(valueB) : keyA.localeCompare(keyB)
2981
+ );
2875
2982
  }
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 });
2983
+ function shouldKeepSnapshotQueryParam(pathname, rawKey, value) {
2984
+ const key = rawKey.trim().toLowerCase();
2985
+ if (!key || !value.trim()) return false;
2986
+ if (key.startsWith("utm_")) return false;
2987
+ if (TRACKING_QUERY_KEYS.has(key)) return false;
2988
+ if (SNAPSHOT_QUERY_KEYS.has(key)) return true;
2989
+ return /\/(search|results|browse|discover|find|category|tag|topics?|collections?|list)(\/|$)/i.test(
2990
+ pathname
2991
+ );
2992
+ }
2993
+ function buildSnapshotHashKey(hash, pathname) {
2994
+ let raw = hash.replace(/^#/, "").trim();
2995
+ if (!raw) return null;
2996
+ let bang = false;
2997
+ if (raw.startsWith("!")) {
2998
+ bang = true;
2999
+ raw = raw.slice(1).trim();
3000
+ }
3001
+ if (raw.startsWith("/")) {
3002
+ const [routePart, queryPart = ""] = raw.split("?");
3003
+ const route = routePart.replace(/\/+$/, "") || "/";
3004
+ const params = normalizeSnapshotParams(
3005
+ new URLSearchParams(queryPart).entries(),
3006
+ pathname
3007
+ );
3008
+ const query = serializeSnapshotParams(params);
3009
+ return `#${bang ? "!" : ""}${route.toLowerCase()}${query ? `?${query}` : ""}`;
3010
+ }
3011
+ const queryLike = raw.startsWith("?") ? raw.slice(1) : raw;
3012
+ if (queryLike.includes("=")) {
3013
+ const params = normalizeSnapshotParams(
3014
+ new URLSearchParams(queryLike).entries(),
3015
+ pathname
3016
+ );
3017
+ if (params.length === 0) return null;
3018
+ const query = serializeSnapshotParams(params);
3019
+ return `#${bang ? "!" : ""}?${query}`;
2894
3020
  }
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 });
3021
+ return null;
3022
+ }
3023
+ function buildPageSnapshotKey(rawUrl) {
3024
+ try {
3025
+ const url = new URL(rawUrl);
3026
+ const pathname = url.pathname.replace(/\/+$/, "") || "/";
3027
+ const params = normalizeSnapshotParams(url.searchParams.entries(), pathname);
3028
+ const query = serializeSnapshotParams(params);
3029
+ const hash = buildSnapshotHashKey(url.hash, pathname);
3030
+ return `${url.origin.toLowerCase()}${pathname.toLowerCase()}${query ? `?${query}` : ""}${hash || ""}`;
3031
+ } catch {
3032
+ return normalizePageUrl(rawUrl);
2905
3033
  }
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 });
3034
+ }
3035
+ function isTrackablePageUrl(rawUrl) {
3036
+ try {
3037
+ const url = new URL(rawUrl);
3038
+ return url.protocol === "http:" || url.protocol === "https:";
3039
+ } catch {
3040
+ return false;
2916
3041
  }
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
- });
3042
+ }
3043
+ const SAVE_DEBOUNCE_MS$2 = 500;
3044
+ const MAX_TEXT_LENGTH = 8e3;
3045
+ let snapshots = null;
3046
+ function getFilePath$1() {
3047
+ return path.join(electron.app.getPath("userData"), "vessel-page-snapshots.json");
3048
+ }
3049
+ function normalizeStoredSnapshot(value) {
3050
+ if (!value || typeof value !== "object") return null;
3051
+ const raw = value;
3052
+ if (typeof raw.url !== "string" || typeof raw.title !== "string" || typeof raw.textContent !== "string" || typeof raw.headings !== "string" || typeof raw.capturedAt !== "string") {
3053
+ return null;
2931
3054
  }
3055
+ return {
3056
+ url: raw.url,
3057
+ title: raw.title,
3058
+ textContent: raw.textContent,
3059
+ headings: raw.headings,
3060
+ capturedAt: raw.capturedAt
3061
+ };
2932
3062
  }
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
3063
+ function load$2() {
3064
+ if (snapshots) return snapshots;
3065
+ snapshots = loadJsonFile({
3066
+ filePath: getFilePath$1(),
3067
+ fallback: /* @__PURE__ */ new Map(),
3068
+ secure: true,
3069
+ parse: (raw) => {
3070
+ const next = /* @__PURE__ */ new Map();
3071
+ if (!Array.isArray(raw)) return next;
3072
+ for (const entry of raw) {
3073
+ const snapshot = normalizeStoredSnapshot(entry);
3074
+ if (snapshot) next.set(snapshot.url, snapshot);
3075
+ }
3076
+ return next;
3077
+ }
2946
3078
  });
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
- }
3079
+ return snapshots;
3080
+ }
3081
+ const persistence$2 = createDebouncedJsonPersistence({
3082
+ debounceMs: SAVE_DEBOUNCE_MS$2,
3083
+ filePath: getFilePath$1(),
3084
+ getValue: () => snapshots,
3085
+ logLabel: "page snapshots",
3086
+ secure: true,
3087
+ serialize: (value) => Array.from(value.values()).slice(-500)
3088
+ });
3089
+ function normalizeUrl(rawUrl) {
3090
+ return buildPageSnapshotKey(rawUrl);
3091
+ }
3092
+ function shouldTrackSnapshotUrl(rawUrl) {
3093
+ return isTrackablePageUrl(rawUrl);
3094
+ }
3095
+ function getSnapshot(normalizedUrl) {
3096
+ return load$2().get(normalizedUrl);
3097
+ }
3098
+ function saveSnapshot(rawUrl, title, textContent, headings) {
3099
+ const s = load$2();
3100
+ const key = normalizeUrl(rawUrl);
3101
+ const snapshot = {
3102
+ url: key,
3103
+ title,
3104
+ textContent: textContent.slice(0, MAX_TEXT_LENGTH),
3105
+ headings: headings.map((h) => `${"#".repeat(h.level)} ${h.text}`).join("\n"),
3106
+ capturedAt: (/* @__PURE__ */ new Date()).toISOString()
3107
+ };
3108
+ s.delete(key);
3109
+ s.set(key, snapshot);
3110
+ persistence$2.schedule();
3111
+ return snapshot;
3112
+ }
3113
+ function flushPersist$2() {
3114
+ return persistence$2.flush();
2964
3115
  }
2965
3116
  const SEARCH_ENGINE_HOSTS = [
2966
3117
  "google.",
@@ -5013,35 +5164,455 @@ function normalizePageContent(value) {
5013
5164
  pageIssues: Array.isArray(page.pageIssues) ? page.pageIssues : []
5014
5165
  };
5015
5166
  }
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;
5167
+ const latestPageDiffs = /* @__PURE__ */ new Map();
5168
+ const recentPageDiffBursts = /* @__PURE__ */ new Map();
5169
+ const pendingPageSnapshotTimers = /* @__PURE__ */ new Map();
5170
+ const pendingPageSnapshotDueAt = /* @__PURE__ */ new Map();
5171
+ const lastMutationSnapshotAt = /* @__PURE__ */ new Map();
5172
+ const lastMutationActivityAt = /* @__PURE__ */ new Map();
5173
+ const MIN_MUTATION_CAPTURE_INTERVAL_MS = 5e3;
5174
+ const SETTLE_AFTER_ACTIVITY_MS = 1500;
5175
+ function getLatestPageDiff(rawUrl) {
5176
+ if (!shouldTrackSnapshotUrl(rawUrl)) return null;
5177
+ return latestPageDiffs.get(normalizeUrl(rawUrl)) ?? null;
5178
+ }
5179
+ function summarizeDiffBurst(diff) {
5180
+ const items = diff.changes.slice(0, 2).map((change) => `${change.section}: ${change.summary}`);
5181
+ return items.join(" | ");
5182
+ }
5183
+ function enrichWithBurstHistory(key, diff) {
5184
+ const detectedAt = (/* @__PURE__ */ new Date()).toISOString();
5185
+ const nextBurst = {
5186
+ detectedAt,
5187
+ summary: summarizeDiffBurst(diff)
5188
+ };
5189
+ const bursts = [...recentPageDiffBursts.get(key) || [], nextBurst].slice(
5190
+ -5
5191
+ );
5192
+ recentPageDiffBursts.set(key, bursts);
5193
+ return {
5194
+ ...diff,
5195
+ burstCount: bursts.length,
5196
+ firstDetectedAt: bursts[0]?.detectedAt,
5197
+ lastDetectedAt: bursts[bursts.length - 1]?.detectedAt,
5198
+ recentBursts: bursts
5199
+ };
5200
+ }
5201
+ async function capturePageSnapshot(url, wc, sendToRendererViews) {
5202
+ try {
5203
+ if (!shouldTrackSnapshotUrl(url)) return;
5204
+ const key = normalizeUrl(url);
5205
+ const oldSnap = getSnapshot(key);
5206
+ const content = await extractContent(wc);
5207
+ const textContent = content.content || "";
5208
+ const title = content.title || "";
5209
+ const headings = content.headings || [];
5210
+ const currentHeadings = headings.map((h) => `${"#".repeat(h.level)} ${h.text}`).join("\n");
5211
+ if (oldSnap) {
5212
+ const diff = diffSnapshots(oldSnap, textContent, title, currentHeadings);
5213
+ if (diff.hasChanges) {
5214
+ const enrichedDiff = enrichWithBurstHistory(key, diff);
5215
+ latestPageDiffs.set(key, enrichedDiff);
5216
+ sendToRendererViews(Channels.PAGE_CHANGED, enrichedDiff);
5217
+ } else {
5218
+ latestPageDiffs.delete(key);
5219
+ }
5220
+ } else {
5221
+ latestPageDiffs.delete(key);
5222
+ recentPageDiffBursts.delete(key);
5036
5223
  }
5037
- .reader-container {
5038
- max-width: 680px;
5039
- margin: 0 auto;
5224
+ saveSnapshot(url, title, textContent, headings);
5225
+ } catch {
5226
+ }
5227
+ }
5228
+ function computeNextSnapshotDueAt(wcId, now, delayMs) {
5229
+ const lastCaptureAt = lastMutationSnapshotAt.get(wcId) || 0;
5230
+ const lastActivityAt = lastMutationActivityAt.get(wcId) || 0;
5231
+ const earliestAllowedAt = lastCaptureAt + MIN_MUTATION_CAPTURE_INTERVAL_MS;
5232
+ const stableAfterActivityAt = lastActivityAt ? lastActivityAt + SETTLE_AFTER_ACTIVITY_MS : 0;
5233
+ return Math.max(now + delayMs, earliestAllowedAt, stableAfterActivityAt);
5234
+ }
5235
+ function scheduleTimerAt(wc, sendToRendererViews, dueAt) {
5236
+ const wcId = wc.id;
5237
+ const existing = pendingPageSnapshotTimers.get(wcId);
5238
+ if (existing) clearTimeout(existing);
5239
+ const timer = setTimeout(() => {
5240
+ pendingPageSnapshotTimers.delete(wcId);
5241
+ pendingPageSnapshotDueAt.delete(wcId);
5242
+ if (wc.isDestroyed()) return;
5243
+ lastMutationSnapshotAt.set(wcId, Date.now());
5244
+ void capturePageSnapshot(wc.getURL(), wc, sendToRendererViews);
5245
+ }, Math.max(0, dueAt - Date.now()));
5246
+ pendingPageSnapshotTimers.set(wcId, timer);
5247
+ pendingPageSnapshotDueAt.set(wcId, dueAt);
5248
+ }
5249
+ function notePageMutationActivity(wc, sendToRendererViews) {
5250
+ if (wc.isDestroyed()) return;
5251
+ const wcId = wc.id;
5252
+ const now = Date.now();
5253
+ lastMutationActivityAt.set(wcId, now);
5254
+ const existingDueAt = pendingPageSnapshotDueAt.get(wcId);
5255
+ if (existingDueAt == null) return;
5256
+ const nextDueAt = computeNextSnapshotDueAt(wcId, now, 0);
5257
+ if (nextDueAt <= existingDueAt) return;
5258
+ scheduleTimerAt(wc, sendToRendererViews, nextDueAt);
5259
+ }
5260
+ function schedulePageSnapshotCapture(wc, sendToRendererViews, delayMs = 0) {
5261
+ if (wc.isDestroyed()) return;
5262
+ const wcId = wc.id;
5263
+ const now = Date.now();
5264
+ const nextDueAt = computeNextSnapshotDueAt(wcId, now, delayMs);
5265
+ const existingDueAt = pendingPageSnapshotDueAt.get(wcId);
5266
+ if (existingDueAt != null && existingDueAt >= nextDueAt) {
5267
+ return;
5268
+ }
5269
+ scheduleTimerAt(wc, sendToRendererViews, nextDueAt);
5270
+ }
5271
+ function enableClipboardShortcuts(view) {
5272
+ view.webContents.on("before-input-event", (event, input) => {
5273
+ if (!input.control && !input.meta) return;
5274
+ const key = input.key.toLowerCase();
5275
+ const wc = view.webContents;
5276
+ if (input.type === "keyDown") {
5277
+ if (key === "c") {
5278
+ wc.copy();
5279
+ event.preventDefault();
5280
+ } else if (key === "v") {
5281
+ wc.paste();
5282
+ event.preventDefault();
5283
+ } else if (key === "x") {
5284
+ wc.cut();
5285
+ event.preventDefault();
5286
+ } else if (key === "a") {
5287
+ wc.selectAll();
5288
+ event.preventDefault();
5289
+ }
5040
5290
  }
5041
- h1 {
5042
- font-size: 1.8em;
5043
- line-height: 1.3;
5044
- margin-bottom: 0.5rem;
5291
+ });
5292
+ }
5293
+ const CHROME_HEIGHT = 110;
5294
+ const DEFAULT_DEVTOOLS_PANEL_HEIGHT = 250;
5295
+ const MIN_DEVTOOLS_PANEL = 120;
5296
+ const MAX_DEVTOOLS_PANEL = 600;
5297
+ async function getSidebarContextTarget(sidebarView, x, y) {
5298
+ try {
5299
+ return await sidebarView.webContents.executeJavaScript(
5300
+ `(() => {
5301
+ const el = document.elementFromPoint(${x}, ${y});
5302
+ const nav = el && typeof el.closest === "function"
5303
+ ? el.closest(".highlight-nav")
5304
+ : null;
5305
+ const label = nav?.querySelector(".highlight-nav-label")?.textContent?.trim() || "";
5306
+ return {
5307
+ inHighlightNav: !!nav,
5308
+ canRemoveCurrent: /\\d+\\s*\\/\\s*\\d+/.test(label),
5309
+ bookmarkId:
5310
+ el && typeof el.closest === "function"
5311
+ ? el.closest("[data-bookmark-id]")?.getAttribute("data-bookmark-id") || undefined
5312
+ : undefined,
5313
+ };
5314
+ })()`,
5315
+ true
5316
+ );
5317
+ } catch {
5318
+ return { inHighlightNav: false, canRemoveCurrent: false };
5319
+ }
5320
+ }
5321
+ async function showSidebarContextMenu(mainWindow, sidebarView, params) {
5322
+ const target = await getSidebarContextTarget(sidebarView, params.x, params.y);
5323
+ const menu = new electron.Menu();
5324
+ if (target.inHighlightNav) {
5325
+ if (target.canRemoveCurrent) {
5326
+ menu.append(
5327
+ new electron.MenuItem({
5328
+ label: "Remove Current Highlight",
5329
+ click: () => sidebarView.webContents.send(
5330
+ Channels.SIDEBAR_HIGHLIGHT_ACTION,
5331
+ "remove-current"
5332
+ )
5333
+ })
5334
+ );
5335
+ }
5336
+ menu.append(
5337
+ new electron.MenuItem({
5338
+ label: "Clear All Highlights",
5339
+ click: () => sidebarView.webContents.send(
5340
+ Channels.SIDEBAR_HIGHLIGHT_ACTION,
5341
+ "clear-all"
5342
+ )
5343
+ })
5344
+ );
5345
+ }
5346
+ if (target.bookmarkId) {
5347
+ if (menu.items.length > 0) {
5348
+ menu.append(new electron.MenuItem({ type: "separator" }));
5349
+ }
5350
+ menu.append(
5351
+ new electron.MenuItem({
5352
+ label: "Add Context to Chat",
5353
+ click: () => sidebarView.webContents.send(
5354
+ Channels.BOOKMARK_ADD_CONTEXT_TO_CHAT,
5355
+ target.bookmarkId
5356
+ )
5357
+ })
5358
+ );
5359
+ }
5360
+ if (params.isEditable) {
5361
+ if (menu.items.length > 0) {
5362
+ menu.append(new electron.MenuItem({ type: "separator" }));
5363
+ }
5364
+ menu.append(
5365
+ new electron.MenuItem({
5366
+ role: "undo",
5367
+ enabled: params.editFlags.canUndo
5368
+ })
5369
+ );
5370
+ menu.append(
5371
+ new electron.MenuItem({
5372
+ role: "redo",
5373
+ enabled: params.editFlags.canRedo
5374
+ })
5375
+ );
5376
+ menu.append(new electron.MenuItem({ type: "separator" }));
5377
+ menu.append(
5378
+ new electron.MenuItem({
5379
+ role: "cut",
5380
+ enabled: params.editFlags.canCut
5381
+ })
5382
+ );
5383
+ menu.append(
5384
+ new electron.MenuItem({
5385
+ role: "copy",
5386
+ enabled: params.editFlags.canCopy
5387
+ })
5388
+ );
5389
+ menu.append(
5390
+ new electron.MenuItem({
5391
+ role: "paste",
5392
+ enabled: params.editFlags.canPaste
5393
+ })
5394
+ );
5395
+ menu.append(
5396
+ new electron.MenuItem({
5397
+ role: "selectAll",
5398
+ enabled: params.editFlags.canSelectAll
5399
+ })
5400
+ );
5401
+ } else if (params.selectionText?.trim()) {
5402
+ if (menu.items.length > 0) {
5403
+ menu.append(new electron.MenuItem({ type: "separator" }));
5404
+ }
5405
+ menu.append(new electron.MenuItem({ role: "copy" }));
5406
+ }
5407
+ if (menu.items.length === 0) return;
5408
+ sidebarView.webContents.focus();
5409
+ menu.popup({ window: mainWindow });
5410
+ }
5411
+ function getWindowIconPath() {
5412
+ const candidates = [
5413
+ path.join(electron.app.getAppPath(), "resources", "vessel-icon.png"),
5414
+ path.join(process.resourcesPath, "vessel-icon.png"),
5415
+ path.join(__dirname, "../../resources/vessel-icon.png")
5416
+ ];
5417
+ return candidates.find((candidate) => fs.existsSync(candidate));
5418
+ }
5419
+ function createMainWindow(onTabStateChange) {
5420
+ const mainWindow = new electron.BaseWindow({
5421
+ width: 1280,
5422
+ height: 800,
5423
+ minWidth: 800,
5424
+ minHeight: 600,
5425
+ frame: false,
5426
+ show: false,
5427
+ backgroundColor: "#1a1a1e",
5428
+ icon: getWindowIconPath()
5429
+ });
5430
+ const chromeView = new electron.WebContentsView({
5431
+ webPreferences: {
5432
+ preload: path.join(__dirname, "../preload/index.js"),
5433
+ sandbox: true,
5434
+ contextIsolation: true,
5435
+ nodeIntegration: false
5436
+ }
5437
+ });
5438
+ chromeView.setBackgroundColor("#00000000");
5439
+ mainWindow.contentView.addChildView(chromeView);
5440
+ const sidebarView = new electron.WebContentsView({
5441
+ webPreferences: {
5442
+ preload: path.join(__dirname, "../preload/index.js"),
5443
+ sandbox: true,
5444
+ contextIsolation: true,
5445
+ nodeIntegration: false
5446
+ }
5447
+ });
5448
+ sidebarView.setBackgroundColor("#00000000");
5449
+ sidebarView.webContents.on("context-menu", (event, params) => {
5450
+ event.preventDefault();
5451
+ void showSidebarContextMenu(mainWindow, sidebarView, params);
5452
+ });
5453
+ mainWindow.contentView.addChildView(sidebarView);
5454
+ const devtoolsPanelView = new electron.WebContentsView({
5455
+ webPreferences: {
5456
+ preload: path.join(__dirname, "../preload/index.js"),
5457
+ sandbox: true,
5458
+ contextIsolation: true,
5459
+ nodeIntegration: false
5460
+ }
5461
+ });
5462
+ devtoolsPanelView.setBackgroundColor("#00000000");
5463
+ mainWindow.contentView.addChildView(devtoolsPanelView);
5464
+ enableClipboardShortcuts(chromeView);
5465
+ enableClipboardShortcuts(sidebarView);
5466
+ enableClipboardShortcuts(devtoolsPanelView);
5467
+ const settings2 = loadSettings();
5468
+ const uiState = {
5469
+ sidebarOpen: true,
5470
+ sidebarWidth: settings2.sidebarWidth,
5471
+ focusMode: false,
5472
+ settingsOpen: false,
5473
+ devtoolsPanelOpen: false,
5474
+ devtoolsPanelHeight: DEFAULT_DEVTOOLS_PANEL_HEIGHT
5475
+ };
5476
+ const tabManager = new TabManager(mainWindow, onTabStateChange);
5477
+ const sendToRendererViews = (channel, ...args) => {
5478
+ chromeView.webContents.send(channel, ...args);
5479
+ sidebarView.webContents.send(channel, ...args);
5480
+ };
5481
+ tabManager.onPageLoad((url, wc) => {
5482
+ void capturePageSnapshot(url, wc, sendToRendererViews);
5483
+ });
5484
+ const state2 = {
5485
+ mainWindow,
5486
+ chromeView,
5487
+ sidebarView,
5488
+ devtoolsPanelView,
5489
+ tabManager,
5490
+ uiState
5491
+ };
5492
+ mainWindow.on("resize", () => layoutViews(state2));
5493
+ mainWindow.on("show", () => layoutViews(state2));
5494
+ mainWindow.on("focus", () => layoutViews(state2));
5495
+ layoutViews(state2);
5496
+ return state2;
5497
+ }
5498
+ function layoutViews(state2) {
5499
+ const {
5500
+ mainWindow,
5501
+ chromeView,
5502
+ sidebarView,
5503
+ devtoolsPanelView,
5504
+ tabManager,
5505
+ uiState
5506
+ } = state2;
5507
+ const [width, height] = mainWindow.getContentSize();
5508
+ const chromeHeight = uiState.focusMode ? 0 : CHROME_HEIGHT;
5509
+ const sidebarWidth = uiState.sidebarOpen ? uiState.sidebarWidth : 0;
5510
+ const devtoolsHeight = uiState.devtoolsPanelOpen ? uiState.devtoolsPanelHeight : 0;
5511
+ const chromeNeedsFullHeight = uiState.settingsOpen;
5512
+ if (chromeNeedsFullHeight) {
5513
+ chromeView.setBounds({ x: 0, y: 0, width, height });
5514
+ } else {
5515
+ chromeView.setBounds({ x: 0, y: 0, width, height: chromeHeight });
5516
+ }
5517
+ const resizeHandleOverlap = 6;
5518
+ if (uiState.sidebarOpen) {
5519
+ sidebarView.setBounds({
5520
+ x: width - sidebarWidth - resizeHandleOverlap,
5521
+ y: 0,
5522
+ width: sidebarWidth + resizeHandleOverlap,
5523
+ height
5524
+ });
5525
+ } else {
5526
+ sidebarView.setBounds({ x: width, y: 0, width: 0, height: 0 });
5527
+ }
5528
+ const contentWidth = width - sidebarWidth;
5529
+ if (uiState.devtoolsPanelOpen) {
5530
+ devtoolsPanelView.setBounds({
5531
+ x: 0,
5532
+ y: height - devtoolsHeight,
5533
+ width: contentWidth,
5534
+ height: devtoolsHeight
5535
+ });
5536
+ } else {
5537
+ devtoolsPanelView.setBounds({ x: 0, y: height, width: 0, height: 0 });
5538
+ }
5539
+ mainWindow.contentView.removeChildView(chromeView);
5540
+ mainWindow.contentView.addChildView(chromeView);
5541
+ mainWindow.contentView.removeChildView(sidebarView);
5542
+ mainWindow.contentView.addChildView(sidebarView);
5543
+ mainWindow.contentView.removeChildView(devtoolsPanelView);
5544
+ mainWindow.contentView.addChildView(devtoolsPanelView);
5545
+ const activeTab = tabManager.getActiveTab();
5546
+ if (activeTab) {
5547
+ activeTab.view.setBounds({
5548
+ x: 0,
5549
+ y: chromeHeight,
5550
+ width: contentWidth,
5551
+ height: height - chromeHeight - devtoolsHeight
5552
+ });
5553
+ }
5554
+ }
5555
+ function resizeSidebarViews(state2) {
5556
+ const { mainWindow, sidebarView, devtoolsPanelView, tabManager, uiState } = state2;
5557
+ const [width, height] = mainWindow.getContentSize();
5558
+ const chromeHeight = uiState.focusMode ? 0 : CHROME_HEIGHT;
5559
+ const sidebarWidth = uiState.sidebarOpen ? uiState.sidebarWidth : 0;
5560
+ const devtoolsHeight = uiState.devtoolsPanelOpen ? uiState.devtoolsPanelHeight : 0;
5561
+ const resizeHandleOverlap = 6;
5562
+ const contentWidth = width - sidebarWidth;
5563
+ sidebarView.setBounds({
5564
+ x: width - sidebarWidth - resizeHandleOverlap,
5565
+ y: 0,
5566
+ width: sidebarWidth + resizeHandleOverlap,
5567
+ height
5568
+ });
5569
+ if (uiState.devtoolsPanelOpen) {
5570
+ devtoolsPanelView.setBounds({
5571
+ x: 0,
5572
+ y: height - devtoolsHeight,
5573
+ width: contentWidth,
5574
+ height: devtoolsHeight
5575
+ });
5576
+ }
5577
+ const activeTab = tabManager.getActiveTab();
5578
+ if (activeTab) {
5579
+ activeTab.view.setBounds({
5580
+ x: 0,
5581
+ y: chromeHeight,
5582
+ width: contentWidth,
5583
+ height: height - chromeHeight - devtoolsHeight
5584
+ });
5585
+ }
5586
+ }
5587
+ function generateReaderHTML(page) {
5588
+ const escapedTitle = escapeHtml(page.title);
5589
+ const escapedByline = escapeHtml(page.byline);
5590
+ const renderedContent = renderReaderContent(page);
5591
+ return `<!DOCTYPE html>
5592
+ <html lang="en">
5593
+ <head>
5594
+ <meta charset="utf-8">
5595
+ <meta name="viewport" content="width=device-width, initial-scale=1">
5596
+ <meta http-equiv="Content-Security-Policy" content="default-src 'none'; style-src 'unsafe-inline'; img-src data:; base-uri 'none'; form-action 'none'">
5597
+ <title>${escapedTitle}</title>
5598
+ <style>
5599
+ * { margin: 0; padding: 0; box-sizing: border-box; }
5600
+ body {
5601
+ background: #1a1a1e;
5602
+ color: #d4d4d8;
5603
+ font-family: Charter, Georgia, serif;
5604
+ font-size: 19px;
5605
+ line-height: 1.7;
5606
+ padding: 3rem 1.5rem;
5607
+ }
5608
+ .reader-container {
5609
+ max-width: 680px;
5610
+ margin: 0 auto;
5611
+ }
5612
+ h1 {
5613
+ font-size: 1.8em;
5614
+ line-height: 1.3;
5615
+ margin-bottom: 0.5rem;
5045
5616
  color: #e4e4e8;
5046
5617
  }
5047
5618
  .byline {
@@ -5123,7 +5694,7 @@ function onRuntimeHealthChange(listener) {
5123
5694
  };
5124
5695
  }
5125
5696
  function getMcpStatus() {
5126
- return state$1.mcp.status;
5697
+ return state$2.mcp.status;
5127
5698
  }
5128
5699
  function emitRuntimeHealthChange() {
5129
5700
  const snapshot = getRuntimeHealth();
@@ -5131,7 +5702,7 @@ function emitRuntimeHealthChange() {
5131
5702
  listener(snapshot);
5132
5703
  }
5133
5704
  }
5134
- const state$1 = {
5705
+ const state$2 = {
5135
5706
  userDataPath: "",
5136
5707
  settingsPath: "",
5137
5708
  startupIssues: [],
@@ -5144,248 +5715,46 @@ const state$1 = {
5144
5715
  }
5145
5716
  };
5146
5717
  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);
5718
+ state$2.userDataPath = paths.userDataPath;
5719
+ state$2.settingsPath = paths.settingsPath;
5720
+ state$2.mcp.configuredPort = paths.configuredPort;
5721
+ state$2.mcp.activePort = null;
5722
+ state$2.mcp.endpoint = null;
5723
+ state$2.mcp.status = "stopped";
5724
+ state$2.mcp.message = "MCP server has not started yet.";
5725
+ emitRuntimeHealthChange();
5332
5726
  }
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 };
5727
+ function setStartupIssues(issues) {
5728
+ state$2.startupIssues = issues.map((issue) => ({ ...issue }));
5729
+ emitRuntimeHealthChange();
5337
5730
  }
5338
- function getTotpSecret(id) {
5339
- const entry = loadVault().find((e) => e.id === id);
5340
- return entry?.totpSecret ?? null;
5731
+ function getRuntimeHealth() {
5732
+ return {
5733
+ userDataPath: state$2.userDataPath,
5734
+ settingsPath: state$2.settingsPath,
5735
+ startupIssues: state$2.startupIssues.map((issue) => ({ ...issue })),
5736
+ mcp: { ...state$2.mcp }
5737
+ };
5341
5738
  }
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");
5739
+ function setMcpHealth(update) {
5740
+ if (typeof update.configuredPort === "number") {
5741
+ state$2.mcp.configuredPort = update.configuredPort;
5352
5742
  }
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);
5743
+ if ("activePort" in update) {
5744
+ state$2.mcp.activePort = update.activePort ?? null;
5356
5745
  }
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");
5364
- }
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);
5369
- }
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);
5746
+ if ("endpoint" in update) {
5747
+ state$2.mcp.endpoint = update.endpoint ?? null;
5377
5748
  }
5378
- }
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 [];
5749
+ const prevStatus = state$2.mcp.status;
5750
+ state$2.mcp.status = update.status;
5751
+ state$2.mcp.message = update.message;
5752
+ if (prevStatus !== state$2.mcp.status) {
5753
+ for (const listener of mcpStatusChangeListeners) {
5754
+ listener(state$2.mcp.status);
5755
+ }
5388
5756
  }
5757
+ emitRuntimeHealthChange();
5389
5758
  }
5390
5759
  function isRichToolResult(value) {
5391
5760
  return typeof value === "object" && value !== null && "__richResult" in value && value.__richResult === true;
@@ -9993,11 +10362,9 @@ function getBookmarkSearchMatch(args) {
9993
10362
  }
9994
10363
  const UNSORTED_ID = "unsorted";
9995
10364
  const ARCHIVE_FOLDER_NAME = "Archive";
9996
- const SAVE_DEBOUNCE_MS = 250;
9997
- let state = null;
10365
+ const SAVE_DEBOUNCE_MS$1 = 250;
10366
+ let state$1 = null;
9998
10367
  const listeners = /* @__PURE__ */ new Set();
9999
- let saveTimer = null;
10000
- let saveDirty = false;
10001
10368
  function cloneState(current) {
10002
10369
  return {
10003
10370
  folders: current.folders.map((folder) => ({ ...folder })),
@@ -10007,53 +10374,39 @@ function cloneState(current) {
10007
10374
  function getBookmarksPath() {
10008
10375
  return path.join(electron.app.getPath("userData"), "vessel-bookmarks.json");
10009
10376
  }
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));
10377
+ function load$1() {
10378
+ if (state$1) return state$1;
10379
+ state$1 = loadJsonFile({
10380
+ filePath: getBookmarksPath(),
10381
+ fallback: { folders: [], bookmarks: [] },
10382
+ parse: (raw) => {
10383
+ const parsed = raw;
10384
+ return {
10385
+ folders: Array.isArray(parsed.folders) ? parsed.folders : [],
10386
+ bookmarks: Array.isArray(parsed.bookmarks) ? parsed.bookmarks : []
10387
+ };
10388
+ }
10389
+ });
10390
+ return state$1;
10037
10391
  }
10392
+ const persistence$1 = createDebouncedJsonPersistence({
10393
+ debounceMs: SAVE_DEBOUNCE_MS$1,
10394
+ filePath: getBookmarksPath(),
10395
+ getValue: () => state$1,
10396
+ logLabel: "bookmarks"
10397
+ });
10038
10398
  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);
10399
+ persistence$1.schedule();
10047
10400
  }
10048
10401
  function emit() {
10049
- if (!state) return;
10050
- const snapshot = cloneState(state);
10402
+ if (!state$1) return;
10403
+ const snapshot = cloneState(state$1);
10051
10404
  for (const listener of listeners) {
10052
10405
  listener(snapshot);
10053
10406
  }
10054
10407
  }
10055
10408
  function getState() {
10056
- return cloneState(load());
10409
+ return cloneState(load$1());
10057
10410
  }
10058
10411
  function subscribe(listener) {
10059
10412
  listeners.add(listener);
@@ -10062,51 +10415,51 @@ function subscribe(listener) {
10062
10415
  };
10063
10416
  }
10064
10417
  function clearAll() {
10065
- state = { folders: [], bookmarks: [] };
10418
+ state$1 = { folders: [], bookmarks: [] };
10066
10419
  save();
10067
10420
  emit();
10068
10421
  }
10069
10422
  function getBookmark(id) {
10070
- load();
10071
- const bookmark = state.bookmarks.find((item) => item.id === id);
10423
+ load$1();
10424
+ const bookmark = state$1.bookmarks.find((item) => item.id === id);
10072
10425
  return bookmark ? { ...bookmark } : null;
10073
10426
  }
10074
10427
  function getBookmarkByUrl(url) {
10075
- load();
10428
+ load$1();
10076
10429
  const normalized = url.trim();
10077
10430
  if (!normalized) return null;
10078
- const bookmark = [...state.bookmarks].reverse().find((item) => item.url === normalized);
10431
+ const bookmark = [...state$1.bookmarks].reverse().find((item) => item.url === normalized);
10079
10432
  return bookmark ? { ...bookmark } : null;
10080
10433
  }
10081
10434
  function getBookmarkByUrlInFolder(url, folderId) {
10082
- load();
10435
+ load$1();
10083
10436
  const normalizedUrl = url.trim();
10084
10437
  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(
10438
+ const targetFolderId = folderId && folderId !== UNSORTED_ID ? state$1.folders.find((f) => f.id === folderId)?.id ?? UNSORTED_ID : UNSORTED_ID;
10439
+ const bookmark = [...state$1.bookmarks].reverse().find(
10087
10440
  (item) => item.url === normalizedUrl && item.folderId === targetFolderId
10088
10441
  );
10089
10442
  return bookmark ? { ...bookmark } : null;
10090
10443
  }
10091
10444
  function getFolder(id) {
10092
- load();
10445
+ load$1();
10093
10446
  if (!id || id === UNSORTED_ID) return null;
10094
- const folder = state.folders.find((item) => item.id === id);
10447
+ const folder = state$1.folders.find((item) => item.id === id);
10095
10448
  return folder ? { ...folder } : null;
10096
10449
  }
10097
10450
  function findFolderByName(name) {
10098
- load();
10451
+ load$1();
10099
10452
  const normalized = name.trim().toLowerCase();
10100
10453
  if (!normalized || normalized === "unsorted") return null;
10101
- const folder = state.folders.find(
10454
+ const folder = state$1.folders.find(
10102
10455
  (item) => item.name.trim().toLowerCase() === normalized
10103
10456
  );
10104
10457
  return folder ? { ...folder } : null;
10105
10458
  }
10106
10459
  function listFolderOverviews() {
10107
- load();
10460
+ load$1();
10108
10461
  const counts = /* @__PURE__ */ new Map();
10109
- for (const bookmark of state.bookmarks) {
10462
+ for (const bookmark of state$1.bookmarks) {
10110
10463
  counts.set(bookmark.folderId, (counts.get(bookmark.folderId) ?? 0) + 1);
10111
10464
  }
10112
10465
  return [
@@ -10115,7 +10468,7 @@ function listFolderOverviews() {
10115
10468
  name: "Unsorted",
10116
10469
  count: counts.get(UNSORTED_ID) ?? 0
10117
10470
  },
10118
- ...state.folders.map((folder) => ({
10471
+ ...state$1.folders.map((folder) => ({
10119
10472
  id: folder.id,
10120
10473
  name: folder.name,
10121
10474
  summary: folder.summary,
@@ -10124,10 +10477,10 @@ function listFolderOverviews() {
10124
10477
  ];
10125
10478
  }
10126
10479
  function searchBookmarks(query) {
10127
- load();
10480
+ load$1();
10128
10481
  if (!query.trim()) return [];
10129
- return state.bookmarks.map((bookmark) => {
10130
- const folder = state.folders.find(
10482
+ return state$1.bookmarks.map((bookmark) => {
10483
+ const folder = state$1.folders.find(
10131
10484
  (item) => item.id === bookmark.folderId
10132
10485
  );
10133
10486
  const { matchedFields, score } = getBookmarkSearchMatch({
@@ -10154,7 +10507,7 @@ function searchBookmarks(query) {
10154
10507
  );
10155
10508
  }
10156
10509
  function createFolderWithSummary(name, summary) {
10157
- load();
10510
+ load$1();
10158
10511
  const trimmed = name.trim();
10159
10512
  if (!trimmed) throw new Error("Folder name cannot be empty");
10160
10513
  const folder = {
@@ -10163,7 +10516,7 @@ function createFolderWithSummary(name, summary) {
10163
10516
  summary: summary?.trim() || void 0,
10164
10517
  createdAt: (/* @__PURE__ */ new Date()).toISOString()
10165
10518
  };
10166
- state.folders.push(folder);
10519
+ state$1.folders.push(folder);
10167
10520
  save();
10168
10521
  emit();
10169
10522
  return folder;
@@ -10188,13 +10541,13 @@ function saveBookmark(url, title, folderId, note) {
10188
10541
  return result.bookmark;
10189
10542
  }
10190
10543
  function saveBookmarkWithPolicy(url, title, folderId, note, options) {
10191
- load();
10544
+ load$1();
10192
10545
  const normalizedUrl = url.trim();
10193
10546
  if (!normalizedUrl) {
10194
10547
  throw new Error("Bookmark URL cannot be empty");
10195
10548
  }
10196
10549
  const normalizedTitle = title.trim() || normalizedUrl;
10197
- const targetId = folderId && folderId !== UNSORTED_ID ? state.folders.find((f) => f.id === folderId)?.id ?? UNSORTED_ID : UNSORTED_ID;
10550
+ const targetId = folderId && folderId !== UNSORTED_ID ? state$1.folders.find((f) => f.id === folderId)?.id ?? UNSORTED_ID : UNSORTED_ID;
10198
10551
  const duplicatePolicy = options?.onDuplicate ?? "ask";
10199
10552
  const existing = getBookmarkByUrlInFolder(normalizedUrl, targetId);
10200
10553
  if (existing) {
@@ -10205,7 +10558,7 @@ function saveBookmarkWithPolicy(url, title, folderId, note, options) {
10205
10558
  };
10206
10559
  }
10207
10560
  if (duplicatePolicy === "update") {
10208
- const bookmark2 = state.bookmarks.find((item) => item.id === existing.id);
10561
+ const bookmark2 = state$1.bookmarks.find((item) => item.id === existing.id);
10209
10562
  if (!bookmark2) {
10210
10563
  return {
10211
10564
  status: "conflict",
@@ -10233,7 +10586,7 @@ function saveBookmarkWithPolicy(url, title, folderId, note, options) {
10233
10586
  folderId: targetId,
10234
10587
  savedAt: (/* @__PURE__ */ new Date()).toISOString()
10235
10588
  };
10236
- state.bookmarks.push(bookmark);
10589
+ state$1.bookmarks.push(bookmark);
10237
10590
  save();
10238
10591
  emit();
10239
10592
  return {
@@ -10242,10 +10595,10 @@ function saveBookmarkWithPolicy(url, title, folderId, note, options) {
10242
10595
  };
10243
10596
  }
10244
10597
  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) {
10598
+ load$1();
10599
+ const before = state$1.bookmarks.length;
10600
+ state$1.bookmarks = state$1.bookmarks.filter((b) => b.id !== id);
10601
+ if (state$1.bookmarks.length !== before) {
10249
10602
  save();
10250
10603
  emit();
10251
10604
  return true;
@@ -10253,8 +10606,8 @@ function removeBookmark(id) {
10253
10606
  return false;
10254
10607
  }
10255
10608
  function updateBookmark(id, updates) {
10256
- load();
10257
- const bookmark = state.bookmarks.find((item) => item.id === id);
10609
+ load$1();
10610
+ const bookmark = state$1.bookmarks.find((item) => item.id === id);
10258
10611
  if (!bookmark) return null;
10259
10612
  if (typeof updates.title === "string") {
10260
10613
  const trimmed = updates.title.trim();
@@ -10265,31 +10618,31 @@ function updateBookmark(id, updates) {
10265
10618
  bookmark.note = trimmed || void 0;
10266
10619
  }
10267
10620
  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;
10621
+ bookmark.folderId = updates.folderId && updates.folderId !== UNSORTED_ID ? state$1.folders.find((item) => item.id === updates.folderId)?.id ?? UNSORTED_ID : UNSORTED_ID;
10269
10622
  }
10270
10623
  save();
10271
10624
  emit();
10272
10625
  return { ...bookmark };
10273
10626
  }
10274
10627
  function removeFolder(id, deleteContents = false) {
10275
- load();
10276
- const exists = state.folders.some((f) => f.id === id);
10628
+ load$1();
10629
+ const exists = state$1.folders.some((f) => f.id === id);
10277
10630
  if (!exists) return false;
10278
10631
  if (deleteContents) {
10279
- state.bookmarks = state.bookmarks.filter((b) => b.folderId !== id);
10632
+ state$1.bookmarks = state$1.bookmarks.filter((b) => b.folderId !== id);
10280
10633
  } else {
10281
- state.bookmarks = state.bookmarks.map(
10634
+ state$1.bookmarks = state$1.bookmarks.map(
10282
10635
  (b) => b.folderId === id ? { ...b, folderId: UNSORTED_ID } : b
10283
10636
  );
10284
10637
  }
10285
- state.folders = state.folders.filter((f) => f.id !== id);
10638
+ state$1.folders = state$1.folders.filter((f) => f.id !== id);
10286
10639
  save();
10287
10640
  emit();
10288
10641
  return true;
10289
10642
  }
10290
10643
  function renameFolder(id, newName, summary) {
10291
- load();
10292
- const folder = state.folders.find((f) => f.id === id);
10644
+ load$1();
10645
+ const folder = state$1.folders.find((f) => f.id === id);
10293
10646
  if (!folder) return null;
10294
10647
  const trimmed = newName.trim();
10295
10648
  if (!trimmed) return null;
@@ -10299,8 +10652,8 @@ function renameFolder(id, newName, summary) {
10299
10652
  emit();
10300
10653
  return { ...folder };
10301
10654
  }
10302
- function flushPersist() {
10303
- return saveDirty ? persistNow() : Promise.resolve();
10655
+ function flushPersist$1() {
10656
+ return persistence$1.flush();
10304
10657
  }
10305
10658
  function normalizeText(text) {
10306
10659
  return text?.trim() ?? "";
@@ -10911,7 +11264,7 @@ function isInvalidTextTargetQuery(rawQuery) {
10911
11264
  return false;
10912
11265
  }
10913
11266
  function resolveTextTargetInDocument(doc, rawQuery, mode) {
10914
- function normalize(value) {
11267
+ function normalize2(value) {
10915
11268
  return String(value || "").toLowerCase().replace(/\s+/g, " ").trim();
10916
11269
  }
10917
11270
  function text(value) {
@@ -11009,7 +11362,7 @@ function resolveTextTargetInDocument(doc, rawQuery, mode) {
11009
11362
  return [ariaLabel, title, ownText].filter(Boolean).join(" ");
11010
11363
  }
11011
11364
  function scoreText(query2, candidate) {
11012
- const normalizedCandidate = normalize(candidate);
11365
+ const normalizedCandidate = normalize2(candidate);
11013
11366
  if (!normalizedCandidate) return -1;
11014
11367
  if (normalizedCandidate === query2) return 180;
11015
11368
  if (normalizedCandidate.startsWith(query2)) return 150;
@@ -11025,7 +11378,7 @@ function resolveTextTargetInDocument(doc, rawQuery, mode) {
11025
11378
  function interactiveBonus(el) {
11026
11379
  const htmlEl = el;
11027
11380
  const tag = el.tagName.toLowerCase();
11028
- const label = normalize(labelFor(el));
11381
+ const label = normalize2(labelFor(el));
11029
11382
  let score = 0;
11030
11383
  if (tag === "button") score += 40;
11031
11384
  if (tag === "a") score += 35;
@@ -11063,7 +11416,7 @@ function resolveTextTargetInDocument(doc, rawQuery, mode) {
11063
11416
  return best;
11064
11417
  }
11065
11418
  if (isInvalidTextTargetQuery(rawQuery)) return null;
11066
- const query = normalize(rawQuery);
11419
+ const query = normalize2(rawQuery);
11067
11420
  if (!query) return null;
11068
11421
  let bestInteractive = null;
11069
11422
  const interactiveSelector = "a[href], button, [role='button'], input[type='submit'], input[type='button'], input[type='radio'], input[type='checkbox'], select, textarea";
@@ -13554,7 +13907,21 @@ async function setElementValue$1(wc, selector, value) {
13554
13907
  (function() {
13555
13908
  var el = window.__vessel?.resolveShadowSelector?.(${JSON.stringify(selector)});
13556
13909
  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";
13910
+ if (!(el instanceof HTMLInputElement || el instanceof HTMLTextAreaElement || el instanceof HTMLSelectElement)) return "Error[not-input]: Element is not a fillable input";
13911
+ if (el.disabled || el.getAttribute("aria-disabled") === "true") return "Error[disabled]: Input is disabled";
13912
+ if (el instanceof HTMLSelectElement) {
13913
+ var requested = ${JSON.stringify(value)}.trim().toLowerCase();
13914
+ var option = Array.from(el.options).find(function(item) {
13915
+ return item.value.trim().toLowerCase() === requested ||
13916
+ (item.textContent || "").trim().toLowerCase() === requested;
13917
+ });
13918
+ if (!option) return "Error[option-not-found]: Option not found";
13919
+ el.value = option.value;
13920
+ el.focus();
13921
+ el.dispatchEvent(new Event("input", { bubbles: true }));
13922
+ el.dispatchEvent(new Event("change", { bubbles: true }));
13923
+ return "Selected: " + ((option.textContent || option.value).trim().slice(0, 100));
13924
+ }
13558
13925
  var proto = el instanceof HTMLTextAreaElement ? HTMLTextAreaElement.prototype : HTMLInputElement.prototype;
13559
13926
  var desc = Object.getOwnPropertyDescriptor(proto, "value");
13560
13927
  if (desc && desc.set) { desc.set.call(el, ${JSON.stringify(value)}); } else { el.value = ${JSON.stringify(value)}; }
@@ -13576,13 +13943,29 @@ async function setElementValue$1(wc, selector, value) {
13576
13943
  (function() {
13577
13944
  const el = document.querySelector(${JSON.stringify(selector)});
13578
13945
  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';
13946
+ if (!(el instanceof HTMLInputElement || el instanceof HTMLTextAreaElement || el instanceof HTMLSelectElement)) {
13947
+ return 'Error[not-input]: Element is not a fillable input';
13581
13948
  }
13582
13949
  if (el.disabled || el.getAttribute('aria-disabled') === 'true') {
13583
13950
  return 'Error[disabled]: Input is disabled';
13584
13951
  }
13585
13952
 
13953
+ if (el instanceof HTMLSelectElement) {
13954
+ const requested = ${JSON.stringify(value)}.trim().toLowerCase();
13955
+ const option = Array.from(el.options).find((item) => {
13956
+ const label = (item.textContent || '').trim().toLowerCase();
13957
+ return label === requested || item.value.trim().toLowerCase() === requested;
13958
+ });
13959
+ if (!option) {
13960
+ return 'Error[option-not-found]: Option not found';
13961
+ }
13962
+ el.value = option.value;
13963
+ el.focus();
13964
+ el.dispatchEvent(new Event('input', { bubbles: true }));
13965
+ el.dispatchEvent(new Event('change', { bubbles: true }));
13966
+ return 'Selected: ' + ((option.textContent || option.value).trim().slice(0, 100));
13967
+ }
13968
+
13586
13969
  const prototype = el instanceof HTMLTextAreaElement
13587
13970
  ? HTMLTextAreaElement.prototype
13588
13971
  : HTMLInputElement.prototype;
@@ -16654,6 +17037,183 @@ Exception: ${result.exceptionDetails}`);
16654
17037
  }
16655
17038
  );
16656
17039
  }
17040
+ const VAULT_FILENAME = "vessel-vault.enc";
17041
+ const KEY_FILENAME = "vessel-vault.key";
17042
+ const ALGORITHM = "aes-256-gcm";
17043
+ const IV_LENGTH = 12;
17044
+ const AUTH_TAG_LENGTH = 16;
17045
+ let cachedEntries = null;
17046
+ function getVaultDir() {
17047
+ return electron.app.getPath("userData");
17048
+ }
17049
+ function getVaultPath() {
17050
+ return path$1.join(getVaultDir(), VAULT_FILENAME);
17051
+ }
17052
+ function getKeyPath() {
17053
+ return path$1.join(getVaultDir(), KEY_FILENAME);
17054
+ }
17055
+ function assertVaultSecretStorageAvailable() {
17056
+ if (!electron.safeStorage.isEncryptionAvailable()) {
17057
+ throw new Error(
17058
+ "Agent Credential Vault requires OS-backed secret storage. Enable Keychain, DPAPI, or libsecret support and restart Vessel."
17059
+ );
17060
+ }
17061
+ }
17062
+ function getOrCreateEncryptionKey() {
17063
+ assertVaultSecretStorageAvailable();
17064
+ const keyPath = getKeyPath();
17065
+ if (fs$1.existsSync(keyPath)) {
17066
+ const encryptedKey = fs$1.readFileSync(keyPath);
17067
+ return Buffer.from(electron.safeStorage.decryptString(encryptedKey), "utf-8");
17068
+ }
17069
+ const key = crypto$2.randomBytes(32);
17070
+ fs$1.mkdirSync(path$1.dirname(keyPath), { recursive: true });
17071
+ const encrypted = electron.safeStorage.encryptString(key.toString("utf-8"));
17072
+ fs$1.writeFileSync(keyPath, encrypted, { mode: 384 });
17073
+ return key;
17074
+ }
17075
+ function encrypt(plaintext) {
17076
+ const key = getOrCreateEncryptionKey();
17077
+ const iv = crypto$2.randomBytes(IV_LENGTH);
17078
+ const cipher = crypto$2.createCipheriv(ALGORITHM, key, iv, {
17079
+ authTagLength: AUTH_TAG_LENGTH
17080
+ });
17081
+ const encrypted = Buffer.concat([
17082
+ cipher.update(plaintext, "utf-8"),
17083
+ cipher.final()
17084
+ ]);
17085
+ const authTag = cipher.getAuthTag();
17086
+ return Buffer.concat([iv, authTag, encrypted]);
17087
+ }
17088
+ function decrypt(data) {
17089
+ const key = getOrCreateEncryptionKey();
17090
+ const iv = data.subarray(0, IV_LENGTH);
17091
+ const authTag = data.subarray(IV_LENGTH, IV_LENGTH + AUTH_TAG_LENGTH);
17092
+ const ciphertext = data.subarray(IV_LENGTH + AUTH_TAG_LENGTH);
17093
+ const decipher = crypto$2.createDecipheriv(ALGORITHM, key, iv, {
17094
+ authTagLength: AUTH_TAG_LENGTH
17095
+ });
17096
+ decipher.setAuthTag(authTag);
17097
+ return decipher.update(ciphertext, void 0, "utf-8") + decipher.final("utf-8");
17098
+ }
17099
+ function loadVault() {
17100
+ if (cachedEntries) return cachedEntries;
17101
+ const vaultPath = getVaultPath();
17102
+ if (!fs$1.existsSync(vaultPath)) {
17103
+ cachedEntries = [];
17104
+ return cachedEntries;
17105
+ }
17106
+ try {
17107
+ const raw = fs$1.readFileSync(vaultPath);
17108
+ const json = decrypt(raw);
17109
+ cachedEntries = JSON.parse(json);
17110
+ return cachedEntries;
17111
+ } catch (err) {
17112
+ console.error("[Vessel Vault] Failed to load vault:", err);
17113
+ throw new Error(
17114
+ "Could not unlock the Agent Credential Vault. Check that OS secret storage is available and that the stored vault key can be decrypted."
17115
+ );
17116
+ }
17117
+ }
17118
+ function saveVault(entries) {
17119
+ const json = JSON.stringify(entries, null, 2);
17120
+ const encrypted = encrypt(json);
17121
+ const vaultPath = getVaultPath();
17122
+ fs$1.mkdirSync(path$1.dirname(vaultPath), { recursive: true });
17123
+ fs$1.writeFileSync(vaultPath, encrypted);
17124
+ fs$1.chmodSync(vaultPath, 384);
17125
+ cachedEntries = entries;
17126
+ }
17127
+ function domainMatches(pattern, hostname) {
17128
+ const p = pattern.toLowerCase().trim();
17129
+ const h = hostname.toLowerCase().trim();
17130
+ if (p === h) return true;
17131
+ if (p.startsWith("*.")) {
17132
+ const suffix = p.slice(2);
17133
+ return h === suffix || h.endsWith("." + suffix);
17134
+ }
17135
+ return false;
17136
+ }
17137
+ function listEntries() {
17138
+ return loadVault().map(({ password, totpSecret, ...rest }) => rest);
17139
+ }
17140
+ function findEntriesForDomain(url) {
17141
+ let hostname;
17142
+ try {
17143
+ hostname = new URL(url).hostname;
17144
+ } catch {
17145
+ return [];
17146
+ }
17147
+ return loadVault().filter((e) => domainMatches(e.domainPattern, hostname)).map(({ password, totpSecret, ...rest }) => rest);
17148
+ }
17149
+ function addEntry(entry) {
17150
+ const entries = loadVault();
17151
+ const newEntry = {
17152
+ ...entry,
17153
+ id: crypto$2.randomUUID(),
17154
+ createdAt: (/* @__PURE__ */ new Date()).toISOString(),
17155
+ useCount: 0
17156
+ };
17157
+ entries.push(newEntry);
17158
+ saveVault(entries);
17159
+ return newEntry;
17160
+ }
17161
+ function updateEntry(id, updates) {
17162
+ const entries = loadVault();
17163
+ const idx = entries.findIndex((e) => e.id === id);
17164
+ if (idx === -1) return null;
17165
+ entries[idx] = { ...entries[idx], ...updates };
17166
+ saveVault(entries);
17167
+ return entries[idx];
17168
+ }
17169
+ function removeEntry(id) {
17170
+ const entries = loadVault();
17171
+ const idx = entries.findIndex((e) => e.id === id);
17172
+ if (idx === -1) return false;
17173
+ entries.splice(idx, 1);
17174
+ saveVault(entries);
17175
+ return true;
17176
+ }
17177
+ function recordUsage(id) {
17178
+ const entries = loadVault();
17179
+ const entry = entries.find((e) => e.id === id);
17180
+ if (!entry) return;
17181
+ entry.lastUsedAt = (/* @__PURE__ */ new Date()).toISOString();
17182
+ entry.useCount += 1;
17183
+ saveVault(entries);
17184
+ }
17185
+ function getCredential(id) {
17186
+ const entry = loadVault().find((e) => e.id === id);
17187
+ if (!entry) return null;
17188
+ return { username: entry.username, password: entry.password };
17189
+ }
17190
+ function getTotpSecret(id) {
17191
+ const entry = loadVault().find((e) => e.id === id);
17192
+ return entry?.totpSecret ?? null;
17193
+ }
17194
+ function generateTotpCode(secret) {
17195
+ const epoch = Math.floor(Date.now() / 1e3);
17196
+ const counter = Math.floor(epoch / 30);
17197
+ const base32Chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZ234567";
17198
+ const cleanSecret = secret.replace(/[\s=-]/g, "").toUpperCase();
17199
+ let bits = "";
17200
+ for (const ch of cleanSecret) {
17201
+ const val = base32Chars.indexOf(ch);
17202
+ if (val === -1) continue;
17203
+ bits += val.toString(2).padStart(5, "0");
17204
+ }
17205
+ const keyBytes = Buffer.alloc(Math.floor(bits.length / 8));
17206
+ for (let i = 0; i < keyBytes.length; i++) {
17207
+ keyBytes[i] = parseInt(bits.slice(i * 8, i * 8 + 8), 2);
17208
+ }
17209
+ const counterBuf = Buffer.alloc(8);
17210
+ counterBuf.writeUInt32BE(Math.floor(counter / 4294967296), 0);
17211
+ counterBuf.writeUInt32BE(counter & 4294967295, 4);
17212
+ const hmac = crypto$2.createHmac("sha1", keyBytes).update(counterBuf).digest();
17213
+ const offset = hmac[hmac.length - 1] & 15;
17214
+ const code = (hmac[offset] & 127) << 24 | (hmac[offset + 1] & 255) << 16 | (hmac[offset + 2] & 255) << 8 | hmac[offset + 3] & 255;
17215
+ return (code % 1e6).toString().padStart(6, "0");
17216
+ }
16657
17217
  const sessionTrustedDomains = /* @__PURE__ */ new Set();
16658
17218
  async function requestConsent(request) {
16659
17219
  const domain = request.domain.toLowerCase();
@@ -16689,6 +17249,31 @@ async function requestConsent(request) {
16689
17249
  }
16690
17250
  return { approved: true, trustForSession };
16691
17251
  }
17252
+ const AUDIT_FILENAME = "vessel-vault-audit.jsonl";
17253
+ const MAX_ENTRIES = 1e3;
17254
+ function getAuditPath() {
17255
+ return path$1.join(electron.app.getPath("userData"), AUDIT_FILENAME);
17256
+ }
17257
+ function appendAuditEntry(entry) {
17258
+ try {
17259
+ const auditPath = getAuditPath();
17260
+ fs$1.mkdirSync(path$1.dirname(auditPath), { recursive: true });
17261
+ fs$1.appendFileSync(auditPath, JSON.stringify(entry) + "\n");
17262
+ } catch (err) {
17263
+ console.error("[Vessel Vault] Failed to write audit log:", err);
17264
+ }
17265
+ }
17266
+ function readAuditLog(limit = 100) {
17267
+ try {
17268
+ const auditPath = getAuditPath();
17269
+ if (!fs$1.existsSync(auditPath)) return [];
17270
+ const lines = fs$1.readFileSync(auditPath, "utf-8").split("\n").filter((l) => l.trim());
17271
+ return lines.slice(-Math.min(limit, MAX_ENTRIES)).map((line) => JSON.parse(line)).reverse();
17272
+ } catch (err) {
17273
+ console.error("[Vessel Vault] Failed to read audit log:", err);
17274
+ return [];
17275
+ }
17276
+ }
16692
17277
  let httpServer = null;
16693
17278
  let mcpAuthToken = null;
16694
17279
  const MCP_AUTH_FILENAME = "mcp-auth.json";
@@ -19291,7 +19876,7 @@ To analyze visually, call vision_analyze with image_url="${screenshotPath}"`
19291
19876
  color
19292
19877
  );
19293
19878
  if (persist && !durationMs && !result.startsWith("Error") && !result.includes("not found")) {
19294
- const url = normalizeUrl(wc.getURL());
19879
+ const url = normalizeUrl$1(wc.getURL());
19295
19880
  addHighlight(
19296
19881
  url,
19297
19882
  resolvedSelector ?? void 0,
@@ -19322,7 +19907,7 @@ To analyze visually, call vision_analyze with image_url="${screenshotPath}"`
19322
19907
  {},
19323
19908
  async () => {
19324
19909
  const wc = tab.view.webContents;
19325
- const url = normalizeUrl(wc.getURL());
19910
+ const url = normalizeUrl$1(wc.getURL());
19326
19911
  clearHighlightsForUrl(url);
19327
19912
  return clearHighlights(wc);
19328
19913
  }
@@ -19343,7 +19928,7 @@ To analyze visually, call vision_analyze with image_url="${screenshotPath}"`
19343
19928
  async ({ url }) => {
19344
19929
  const state2 = getState$2();
19345
19930
  const activeTab = tabManager.getActiveTab();
19346
- const activeUrl = activeTab ? normalizeUrl(activeTab.view.webContents.getURL()) : null;
19931
+ const activeUrl = activeTab ? normalizeUrl$1(activeTab.view.webContents.getURL()) : null;
19347
19932
  const activeSavedHighlights = activeUrl ? state2.highlights.filter((highlight) => highlight.url === activeUrl) : [];
19348
19933
  const liveSnapshot = activeTab && activeUrl ? await captureLiveHighlightSnapshot(
19349
19934
  activeTab.view.webContents,
@@ -19354,9 +19939,9 @@ To analyze visually, call vision_analyze with image_url="${screenshotPath}"`
19354
19939
  );
19355
19940
  if (url) {
19356
19941
  const filtered = state2.highlights.filter(
19357
- (h) => h.url === normalizeUrl(url)
19942
+ (h) => h.url === normalizeUrl$1(url)
19358
19943
  );
19359
- const normalizedUrl = normalizeUrl(url);
19944
+ const normalizedUrl = normalizeUrl$1(url);
19360
19945
  const sections2 = [];
19361
19946
  if (activeUrl && activeUrl === normalizedUrl) {
19362
19947
  if (liveSnapshot.activeSelection) {
@@ -19437,7 +20022,7 @@ ${JSON.stringify(otherHighlights, null, 2)}`
19437
20022
  }
19438
20023
  const remaining = getHighlightsForUrl(removed.url);
19439
20024
  for (const tabState of tabManager.getAllStates()) {
19440
- if (normalizeUrl(tabState.url) !== removed.url) {
20025
+ if (normalizeUrl$1(tabState.url) !== removed.url) {
19441
20026
  continue;
19442
20027
  }
19443
20028
  const tab = tabManager.getTab(tabState.id);
@@ -21694,16 +22279,491 @@ function registerScheduleHandlers(windowState, runtime2, sendToAll) {
21694
22279
  return true;
21695
22280
  });
21696
22281
  }
21697
- let activeChatProvider = null;
21698
22282
  function assertString(value, name) {
21699
22283
  if (typeof value !== "string") throw new Error(`${name} must be a string`);
21700
22284
  }
21701
22285
  function assertOptionalString(value, name) {
21702
- if (value !== void 0 && typeof value !== "string") throw new Error(`${name} must be a string`);
22286
+ if (value !== void 0 && typeof value !== "string") {
22287
+ throw new Error(`${name} must be a string`);
22288
+ }
21703
22289
  }
21704
22290
  function assertNumber(value, name) {
21705
- if (typeof value !== "number" || Number.isNaN(value)) throw new Error(`${name} must be a number`);
22291
+ if (typeof value !== "number" || Number.isNaN(value)) {
22292
+ throw new Error(`${name} must be a number`);
22293
+ }
22294
+ }
22295
+ const SAVE_DEBOUNCE_MS = 250;
22296
+ const PROFILE_FIELDS = [
22297
+ "label",
22298
+ "firstName",
22299
+ "lastName",
22300
+ "email",
22301
+ "phone",
22302
+ "organization",
22303
+ "addressLine1",
22304
+ "addressLine2",
22305
+ "city",
22306
+ "state",
22307
+ "postalCode",
22308
+ "country"
22309
+ ];
22310
+ let state = null;
22311
+ function getFilePath() {
22312
+ return path.join(electron.app.getPath("userData"), "vessel-autofill.json");
22313
+ }
22314
+ function getDefaultState() {
22315
+ return { profiles: [] };
22316
+ }
22317
+ function normalizeStoredProfile(value) {
22318
+ if (!value || typeof value !== "object") return null;
22319
+ const raw = value;
22320
+ if (typeof raw.id !== "string" || typeof raw.createdAt !== "string" || typeof raw.updatedAt !== "string") {
22321
+ return null;
22322
+ }
22323
+ const profile = {
22324
+ id: raw.id,
22325
+ createdAt: raw.createdAt,
22326
+ updatedAt: raw.updatedAt
22327
+ };
22328
+ for (const field of PROFILE_FIELDS) {
22329
+ if (typeof raw[field] !== "string") return null;
22330
+ profile[field] = raw[field];
22331
+ }
22332
+ return profile;
22333
+ }
22334
+ function load() {
22335
+ if (state) return state;
22336
+ state = loadJsonFile({
22337
+ filePath: getFilePath(),
22338
+ fallback: getDefaultState(),
22339
+ secure: true,
22340
+ parse: (raw) => {
22341
+ const parsed = raw;
22342
+ return {
22343
+ profiles: Array.isArray(parsed.profiles) ? parsed.profiles.map(normalizeStoredProfile).filter((profile) => profile !== null) : []
22344
+ };
22345
+ }
22346
+ });
22347
+ return state;
22348
+ }
22349
+ const persistence = createDebouncedJsonPersistence({
22350
+ debounceMs: SAVE_DEBOUNCE_MS,
22351
+ filePath: getFilePath(),
22352
+ getValue: () => state,
22353
+ logLabel: "autofill",
22354
+ secure: true
22355
+ });
22356
+ function listProfiles() {
22357
+ return load().profiles;
22358
+ }
22359
+ function getProfile(id) {
22360
+ return load().profiles.find((p) => p.id === id);
22361
+ }
22362
+ function addProfile(input) {
22363
+ const s = load();
22364
+ const now = (/* @__PURE__ */ new Date()).toISOString();
22365
+ const profile = {
22366
+ ...input,
22367
+ id: crypto$1.randomUUID(),
22368
+ createdAt: now,
22369
+ updatedAt: now
22370
+ };
22371
+ s.profiles.push(profile);
22372
+ persistence.schedule();
22373
+ return profile;
22374
+ }
22375
+ function updateProfile(id, updates) {
22376
+ const s = load();
22377
+ const idx = s.profiles.findIndex((p) => p.id === id);
22378
+ if (idx === -1) return null;
22379
+ s.profiles[idx] = {
22380
+ ...s.profiles[idx],
22381
+ ...updates,
22382
+ updatedAt: (/* @__PURE__ */ new Date()).toISOString()
22383
+ };
22384
+ persistence.schedule();
22385
+ return s.profiles[idx];
22386
+ }
22387
+ function deleteProfile(id) {
22388
+ const s = load();
22389
+ const len = s.profiles.length;
22390
+ s.profiles = s.profiles.filter((p) => p.id !== id);
22391
+ if (s.profiles.length === len) return false;
22392
+ persistence.schedule();
22393
+ return true;
22394
+ }
22395
+ function flushPersist() {
22396
+ return persistence.flush();
22397
+ }
22398
+ const AUTOCOMPLETE_MAP = {
22399
+ "given-name": "firstName",
22400
+ "family-name": "lastName",
22401
+ "surname": "lastName",
22402
+ "email": "email",
22403
+ "tel": "phone",
22404
+ "tel-national": "phone",
22405
+ "phone": "phone",
22406
+ "organization": "organization",
22407
+ "company": "organization",
22408
+ "street-address": "addressLine1",
22409
+ "address-line1": "addressLine1",
22410
+ "address-line2": "addressLine2",
22411
+ "address-level1": "state",
22412
+ "address-level2": "city",
22413
+ "state": "state",
22414
+ "province": "state",
22415
+ "city": "city",
22416
+ "postal-code": "postalCode",
22417
+ "zip": "postalCode",
22418
+ "zip-code": "postalCode",
22419
+ "country": "country",
22420
+ "country-name": "country",
22421
+ "country-code": "country"
22422
+ };
22423
+ const INPUT_TYPE_MAP = {
22424
+ email: "email",
22425
+ tel: "phone"
22426
+ };
22427
+ const NAME_MAP = {
22428
+ firstname: "firstName",
22429
+ first_name: "firstName",
22430
+ "first-name": "firstName",
22431
+ fname: "firstName",
22432
+ givenname: "firstName",
22433
+ lastname: "lastName",
22434
+ last_name: "lastName",
22435
+ "last-name": "lastName",
22436
+ lname: "lastName",
22437
+ surname: "lastName",
22438
+ familyname: "lastName",
22439
+ email: "email",
22440
+ e_mail: "email",
22441
+ "e-mail": "email",
22442
+ emailaddress: "email",
22443
+ mail: "email",
22444
+ phone: "phone",
22445
+ telephone: "phone",
22446
+ tel: "phone",
22447
+ mobile: "phone",
22448
+ cell: "phone",
22449
+ company: "organization",
22450
+ organization: "organization",
22451
+ organisation: "organization",
22452
+ companyname: "organization",
22453
+ address: "addressLine1",
22454
+ street: "addressLine1",
22455
+ "street-address": "addressLine1",
22456
+ address1: "addressLine1",
22457
+ "address-line1": "addressLine1",
22458
+ "addr-line1": "addressLine1",
22459
+ address2: "addressLine2",
22460
+ "address-line2": "addressLine2",
22461
+ "addr-line2": "addressLine2",
22462
+ city: "city",
22463
+ town: "city",
22464
+ locality: "city",
22465
+ state: "state",
22466
+ province: "state",
22467
+ region: "state",
22468
+ zip: "postalCode",
22469
+ zipcode: "postalCode",
22470
+ "zip-code": "postalCode",
22471
+ "postal-code": "postalCode",
22472
+ postalcode: "postalCode",
22473
+ postcode: "postalCode",
22474
+ country: "country"
22475
+ };
22476
+ const LABEL_MAP = {
22477
+ "first name": "firstName",
22478
+ "given name": "firstName",
22479
+ "last name": "lastName",
22480
+ "surname": "lastName",
22481
+ "family name": "lastName",
22482
+ email: "email",
22483
+ "e-mail": "email",
22484
+ "email address": "email",
22485
+ phone: "phone",
22486
+ telephone: "phone",
22487
+ "phone number": "phone",
22488
+ mobile: "phone",
22489
+ cell: "phone",
22490
+ company: "organization",
22491
+ organization: "organization",
22492
+ organisation: "organization",
22493
+ "company name": "organization",
22494
+ address: "addressLine1",
22495
+ "street address": "addressLine1",
22496
+ "address line 1": "addressLine1",
22497
+ "address line 2": "addressLine2",
22498
+ city: "city",
22499
+ town: "city",
22500
+ state: "state",
22501
+ province: "state",
22502
+ region: "state",
22503
+ zip: "postalCode",
22504
+ "zip code": "postalCode",
22505
+ "postal code": "postalCode",
22506
+ "post code": "postalCode",
22507
+ country: "country"
22508
+ };
22509
+ function normalize(s) {
22510
+ return s.toLowerCase().trim().replace(/[\s_-]+/g, " ");
22511
+ }
22512
+ function getFullName(profile) {
22513
+ return [profile.firstName, profile.lastName].filter(Boolean).join(" ").trim();
22514
+ }
22515
+ function mk(val, confidence, matchedBy, profileKey) {
22516
+ return { value: val, confidence, matchedBy, profileKey };
22517
+ }
22518
+ function matchField(el, profile) {
22519
+ if (el.type !== "input" && el.type !== "select" && el.type !== "textarea") return null;
22520
+ if (el.disabled) return null;
22521
+ const inputType = (el.inputType || "text").toLowerCase();
22522
+ if (inputType === "hidden" || inputType === "submit" || inputType === "button" || inputType === "file" || inputType === "image") return null;
22523
+ if (inputType === "password" || inputType === "checkbox" || inputType === "radio") return null;
22524
+ if (el.autocomplete) {
22525
+ const key = el.autocomplete.replace(/section-\w+\s+/, "").replace(/^shipping\s+|^billing\s+/, "");
22526
+ if (key === "name" || key === "additional-name") {
22527
+ const fullName = getFullName(profile);
22528
+ if (fullName) return mk(fullName, 100, "autocomplete", "fullName");
22529
+ }
22530
+ const pk = AUTOCOMPLETE_MAP[key];
22531
+ if (pk && profile[pk]) return mk(profile[pk], 100, "autocomplete", pk);
22532
+ }
22533
+ if (INPUT_TYPE_MAP[inputType]) {
22534
+ const pk = INPUT_TYPE_MAP[inputType];
22535
+ if (profile[pk]) return mk(profile[pk], 90, "inputType", pk);
22536
+ }
22537
+ if (el.name) {
22538
+ const norm = normalize(el.name);
22539
+ const pk = NAME_MAP[norm];
22540
+ if (pk && profile[pk]) return mk(profile[pk], 80, "name", pk);
22541
+ for (const [pattern, pk2] of Object.entries(NAME_MAP)) {
22542
+ if (norm.includes(pattern) && profile[pk2]) return mk(profile[pk2], 70, "name", pk2);
22543
+ }
22544
+ }
22545
+ if (el.label) {
22546
+ const norm = normalize(el.label);
22547
+ if (norm === "full name" || norm.includes("full name")) {
22548
+ const fullName = getFullName(profile);
22549
+ if (fullName) return mk(fullName, 75, "label", "fullName");
22550
+ }
22551
+ const pk = LABEL_MAP[norm];
22552
+ if (pk && profile[pk]) return mk(profile[pk], 75, "label", pk);
22553
+ for (const [pattern, pk2] of Object.entries(LABEL_MAP)) {
22554
+ if (norm.includes(pattern) && profile[pk2]) return mk(profile[pk2], 65, "label", pk2);
22555
+ }
22556
+ }
22557
+ if (el.placeholder) {
22558
+ const norm = normalize(el.placeholder);
22559
+ if (norm === "full name" || norm.includes("full name")) {
22560
+ const fullName = getFullName(profile);
22561
+ if (fullName) return mk(fullName, 50, "placeholder", "fullName");
22562
+ }
22563
+ for (const [pattern, pk] of Object.entries(LABEL_MAP)) {
22564
+ if (norm.includes(pattern) && profile[pk]) return mk(profile[pk], 50, "placeholder", pk);
22565
+ }
22566
+ }
22567
+ return null;
22568
+ }
22569
+ function matchFields(elements, profile) {
22570
+ const assigned = /* @__PURE__ */ new Map();
22571
+ for (const el of elements) {
22572
+ const candidate = matchField(el, profile);
22573
+ if (!candidate) continue;
22574
+ const existing = assigned.get(candidate.profileKey);
22575
+ if (existing && existing.confidence >= candidate.confidence) continue;
22576
+ if (el.index == null || !el.selector) continue;
22577
+ assigned.set(candidate.profileKey, {
22578
+ fieldIndex: el.index,
22579
+ selector: el.selector,
22580
+ value: candidate.value,
22581
+ confidence: candidate.confidence,
22582
+ matchedBy: candidate.matchedBy
22583
+ });
22584
+ }
22585
+ const seen = /* @__PURE__ */ new Set();
22586
+ const results = [];
22587
+ for (const match of assigned.values()) {
22588
+ if (seen.has(match.fieldIndex)) continue;
22589
+ seen.add(match.fieldIndex);
22590
+ results.push(match);
22591
+ }
22592
+ return results;
22593
+ }
22594
+ const AUTOFILL_PROFILE_FIELDS = [
22595
+ "label",
22596
+ "firstName",
22597
+ "lastName",
22598
+ "email",
22599
+ "phone",
22600
+ "organization",
22601
+ "addressLine1",
22602
+ "addressLine2",
22603
+ "city",
22604
+ "state",
22605
+ "postalCode",
22606
+ "country"
22607
+ ];
22608
+ function sanitizeAutofillProfile(value) {
22609
+ if (!value || typeof value !== "object") throw new Error("Invalid profile");
22610
+ const raw = value;
22611
+ const profile = {};
22612
+ for (const field of AUTOFILL_PROFILE_FIELDS) {
22613
+ assertString(raw[field], field);
22614
+ profile[field] = raw[field];
22615
+ }
22616
+ if (!profile.label.trim()) throw new Error("Label is required");
22617
+ return profile;
22618
+ }
22619
+ function sanitizeAutofillUpdates(value) {
22620
+ if (!value || typeof value !== "object") throw new Error("Invalid updates");
22621
+ const raw = value;
22622
+ const updates = {};
22623
+ for (const field of AUTOFILL_PROFILE_FIELDS) {
22624
+ if (!(field in raw)) continue;
22625
+ assertString(raw[field], field);
22626
+ updates[field] = raw[field];
22627
+ }
22628
+ if ("label" in updates && !updates.label?.trim()) {
22629
+ throw new Error("Label is required");
22630
+ }
22631
+ return updates;
22632
+ }
22633
+ function registerAutofillHandlers(windowState) {
22634
+ electron.ipcMain.handle(Channels.AUTOFILL_LIST, () => {
22635
+ return listProfiles();
22636
+ });
22637
+ electron.ipcMain.handle(
22638
+ Channels.AUTOFILL_ADD,
22639
+ (_, profile) => {
22640
+ return addProfile(sanitizeAutofillProfile(profile));
22641
+ }
22642
+ );
22643
+ electron.ipcMain.handle(Channels.AUTOFILL_UPDATE, (_, id, updates) => {
22644
+ assertString(id, "id");
22645
+ return updateProfile(id, sanitizeAutofillUpdates(updates));
22646
+ });
22647
+ electron.ipcMain.handle(Channels.AUTOFILL_DELETE, (_, id) => {
22648
+ assertString(id, "id");
22649
+ return deleteProfile(id);
22650
+ });
22651
+ electron.ipcMain.handle(Channels.AUTOFILL_FILL, async (_, profileId) => {
22652
+ assertString(profileId, "profileId");
22653
+ const profile = getProfile(profileId);
22654
+ if (!profile) throw new Error("Profile not found");
22655
+ const activeTab = windowState.tabManager.getActiveTab();
22656
+ const wc = activeTab?.view.webContents;
22657
+ if (!wc) throw new Error("No active tab");
22658
+ const content = await extractContent(wc);
22659
+ const elements = content.interactiveElements || [];
22660
+ const matches = matchFields(elements, profile);
22661
+ if (matches.length === 0) {
22662
+ return { filled: 0, skipped: 0, details: [] };
22663
+ }
22664
+ const fields = matches.map((match) => ({
22665
+ index: match.fieldIndex,
22666
+ selector: match.selector,
22667
+ value: match.value
22668
+ }));
22669
+ const results = await fillFormFields(wc, fields);
22670
+ const filled = results.filter(
22671
+ (result) => result.result.startsWith("Typed into:") || result.result.startsWith("Selected:")
22672
+ ).length;
22673
+ return {
22674
+ filled,
22675
+ skipped: results.length - filled,
22676
+ details: results.map((result, index) => ({
22677
+ label: elements.find((element) => element.index === matches[index]?.fieldIndex)?.label || `Field ${index + 1}`,
22678
+ value: matches[index]?.value || "",
22679
+ matchedBy: matches[index]?.matchedBy || "unknown",
22680
+ result: result.result
22681
+ }))
22682
+ };
22683
+ });
22684
+ }
22685
+ function registerPageDiffHandlers(windowState, sendToRendererViews) {
22686
+ electron.ipcMain.handle(Channels.PAGE_DIFF_GET, () => {
22687
+ const activeTab = windowState.tabManager.getActiveTab();
22688
+ const wc = activeTab?.view.webContents;
22689
+ if (!wc) return null;
22690
+ return getLatestPageDiff(wc.getURL());
22691
+ });
22692
+ electron.ipcMain.on(Channels.PAGE_DIFF_ACTIVITY, (event) => {
22693
+ const wc = event.sender;
22694
+ if (!wc || wc.isDestroyed()) return;
22695
+ notePageMutationActivity(wc, sendToRendererViews);
22696
+ });
22697
+ electron.ipcMain.on(Channels.PAGE_DIFF_DIRTY, (event) => {
22698
+ const wc = event.sender;
22699
+ if (!wc || wc.isDestroyed()) return;
22700
+ schedulePageSnapshotCapture(wc, sendToRendererViews);
22701
+ });
22702
+ }
22703
+ function registerVaultHandlers() {
22704
+ electron.ipcMain.handle(Channels.VAULT_LIST, () => {
22705
+ return listEntries();
22706
+ });
22707
+ electron.ipcMain.handle(
22708
+ Channels.VAULT_ADD,
22709
+ (_, entry) => {
22710
+ if (!entry || typeof entry !== "object") {
22711
+ throw new Error("Invalid vault entry");
22712
+ }
22713
+ assertString(entry.label, "label");
22714
+ assertString(entry.domainPattern, "domainPattern");
22715
+ assertString(entry.username, "username");
22716
+ assertString(entry.password, "password");
22717
+ if (!entry.label.trim() || !entry.domainPattern.trim() || !entry.username.trim() || !entry.password.trim()) {
22718
+ throw new Error("Label, domain, username, and password are required");
22719
+ }
22720
+ assertOptionalString(entry.totpSecret, "totpSecret");
22721
+ assertOptionalString(entry.notes, "notes");
22722
+ trackVaultAction("credential_added");
22723
+ const created = addEntry(entry);
22724
+ return {
22725
+ id: created.id,
22726
+ label: created.label,
22727
+ domainPattern: created.domainPattern,
22728
+ username: created.username
22729
+ };
22730
+ }
22731
+ );
22732
+ electron.ipcMain.handle(
22733
+ Channels.VAULT_UPDATE,
22734
+ (_, id, updates) => {
22735
+ assertString(id, "id");
22736
+ if (!updates || typeof updates !== "object") {
22737
+ throw new Error("Invalid updates");
22738
+ }
22739
+ return updateEntry(id, updates) !== null;
22740
+ }
22741
+ );
22742
+ electron.ipcMain.handle(Channels.VAULT_REMOVE, (_, id) => {
22743
+ assertString(id, "id");
22744
+ trackVaultAction("credential_removed");
22745
+ return removeEntry(id);
22746
+ });
22747
+ electron.ipcMain.handle(Channels.VAULT_AUDIT_LOG, (_, limit) => {
22748
+ return readAuditLog(limit);
22749
+ });
21706
22750
  }
22751
+ function registerWindowControlHandlers(mainWindow) {
22752
+ electron.ipcMain.handle(Channels.WINDOW_MINIMIZE, () => {
22753
+ mainWindow.minimize();
22754
+ });
22755
+ electron.ipcMain.handle(Channels.WINDOW_MAXIMIZE, () => {
22756
+ if (mainWindow.isMaximized()) {
22757
+ mainWindow.unmaximize();
22758
+ } else {
22759
+ mainWindow.maximize();
22760
+ }
22761
+ });
22762
+ electron.ipcMain.handle(Channels.WINDOW_CLOSE, () => {
22763
+ mainWindow.close();
22764
+ });
22765
+ }
22766
+ let activeChatProvider = null;
21707
22767
  const VALID_APPROVAL_MODES = /* @__PURE__ */ new Set(["auto", "confirm-dangerous", "manual"]);
21708
22768
  function registerIpcHandlers(windowState, runtime2) {
21709
22769
  const { tabManager, chromeView, sidebarView, devtoolsPanelView, mainWindow } = windowState;
@@ -22302,56 +23362,8 @@ function registerIpcHandlers(windowState, runtime2) {
22302
23362
  }
22303
23363
  return result;
22304
23364
  });
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
- });
23365
+ registerVaultHandlers();
23366
+ registerWindowControlHandlers(mainWindow);
22355
23367
  electron.ipcMain.handle(Channels.AUTOMATION_GET_INSTALLED, () => {
22356
23368
  return getInstalledKits();
22357
23369
  });
@@ -22363,6 +23375,8 @@ function registerIpcHandlers(windowState, runtime2) {
22363
23375
  return uninstallKit(id, getScheduledKitIds());
22364
23376
  });
22365
23377
  registerScheduleHandlers(windowState, runtime2, sendToRendererViews);
23378
+ registerAutofillHandlers(windowState);
23379
+ registerPageDiffHandlers(windowState, sendToRendererViews);
22366
23380
  }
22367
23381
  function makeStep(label, status = "pending") {
22368
23382
  return { label, status };
@@ -23815,10 +24829,12 @@ electron.app.on("window-all-closed", () => {
23815
24829
  stopBackgroundRevalidation();
23816
24830
  void Promise.all([
23817
24831
  runtime?.flushPersist() ?? Promise.resolve(),
23818
- flushPersist(),
23819
24832
  flushPersist$1(),
24833
+ flushPersist$3(),
24834
+ flushPersist$4(),
24835
+ flushPersist(),
23820
24836
  flushPersist$2(),
23821
- flushPersist$3()
24837
+ flushPersist$5()
23822
24838
  ]).finally(() => {
23823
24839
  void stopMcpServer().finally(() => {
23824
24840
  electron.app.quit();