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