@quanta-intellect/vessel-browser 0.1.19 → 0.1.21
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 +53 -10
- package/out/main/index.js +1064 -172
- package/out/preload/index.js +63 -1
- package/out/renderer/assets/{index-CKOT_IZt.js → index-32axMD1q.js} +2444 -397
- package/out/renderer/assets/{index-DwRZftNk.css → index-BynCvURs.css} +831 -0
- package/out/renderer/index.html +2 -2
- package/package.json +5 -4
package/out/main/index.js
CHANGED
|
@@ -3,11 +3,12 @@ const electron = require("electron");
|
|
|
3
3
|
const fs$1 = require("node:fs");
|
|
4
4
|
const path = require("path");
|
|
5
5
|
const fs = require("fs");
|
|
6
|
-
const crypto = require("crypto");
|
|
7
|
-
const crypto$
|
|
6
|
+
const crypto$1 = require("crypto");
|
|
7
|
+
const crypto$2 = require("node:crypto");
|
|
8
8
|
const path$1 = require("node:path");
|
|
9
9
|
const Anthropic = require("@anthropic-ai/sdk");
|
|
10
10
|
const OpenAI = require("openai");
|
|
11
|
+
const node_module = require("node:module");
|
|
11
12
|
const zod = require("zod");
|
|
12
13
|
const http = require("node:http");
|
|
13
14
|
const os = require("node:os");
|
|
@@ -16,7 +17,7 @@ const streamableHttp_js = require("@modelcontextprotocol/sdk/server/streamableHt
|
|
|
16
17
|
const defaults = {
|
|
17
18
|
defaultUrl: "https://start.duckduckgo.com",
|
|
18
19
|
theme: "dark",
|
|
19
|
-
sidebarWidth:
|
|
20
|
+
sidebarWidth: 400,
|
|
20
21
|
mcpPort: 3100,
|
|
21
22
|
autoRestoreSession: true,
|
|
22
23
|
clearBookmarksOnLaunch: false,
|
|
@@ -36,11 +37,20 @@ const defaults = {
|
|
|
36
37
|
expiresAt: ""
|
|
37
38
|
}
|
|
38
39
|
};
|
|
40
|
+
const SAVE_DEBOUNCE_MS$3 = 150;
|
|
39
41
|
const SETTABLE_KEYS = new Set(Object.keys(defaults));
|
|
40
42
|
let settings = null;
|
|
41
43
|
let settingsIssues = [];
|
|
44
|
+
let saveTimer$3 = null;
|
|
45
|
+
let saveDirty$3 = false;
|
|
46
|
+
function getUserDataPath() {
|
|
47
|
+
if (typeof electron.app?.getPath === "function") {
|
|
48
|
+
return electron.app.getPath("userData");
|
|
49
|
+
}
|
|
50
|
+
return path.join(process.cwd(), ".vessel-test-data");
|
|
51
|
+
}
|
|
42
52
|
function getSettingsPath() {
|
|
43
|
-
return path.join(
|
|
53
|
+
return path.join(getUserDataPath(), "vessel-settings.json");
|
|
44
54
|
}
|
|
45
55
|
function getSettingsLoadIssues() {
|
|
46
56
|
return settingsIssues.map((issue) => ({ ...issue }));
|
|
@@ -87,11 +97,26 @@ function loadSettings() {
|
|
|
87
97
|
}
|
|
88
98
|
return settings;
|
|
89
99
|
}
|
|
90
|
-
function
|
|
91
|
-
|
|
100
|
+
function persistNow$3() {
|
|
101
|
+
saveDirty$3 = false;
|
|
102
|
+
if (saveTimer$3) {
|
|
103
|
+
clearTimeout(saveTimer$3);
|
|
104
|
+
saveTimer$3 = null;
|
|
105
|
+
}
|
|
106
|
+
return fs.promises.mkdir(path.dirname(getSettingsPath()), { recursive: true }).then(
|
|
92
107
|
() => fs.promises.writeFile(getSettingsPath(), JSON.stringify(settings, null, 2))
|
|
93
108
|
).catch((err) => console.error("[Vessel] Failed to save settings:", err));
|
|
94
109
|
}
|
|
110
|
+
function saveSettings() {
|
|
111
|
+
saveDirty$3 = true;
|
|
112
|
+
if (saveTimer$3) return;
|
|
113
|
+
saveTimer$3 = setTimeout(() => {
|
|
114
|
+
saveTimer$3 = null;
|
|
115
|
+
if (saveDirty$3) {
|
|
116
|
+
void persistNow$3();
|
|
117
|
+
}
|
|
118
|
+
}, SAVE_DEBOUNCE_MS$3);
|
|
119
|
+
}
|
|
95
120
|
function setSetting(key, value) {
|
|
96
121
|
loadSettings();
|
|
97
122
|
if (key === "mcpPort") {
|
|
@@ -102,6 +127,9 @@ function setSetting(key, value) {
|
|
|
102
127
|
saveSettings();
|
|
103
128
|
return { ...settings };
|
|
104
129
|
}
|
|
130
|
+
function flushPersist$3() {
|
|
131
|
+
return saveDirty$3 ? persistNow$3() : Promise.resolve();
|
|
132
|
+
}
|
|
105
133
|
function checkDomainPolicy(url) {
|
|
106
134
|
if (!url || url.startsWith("about:")) return null;
|
|
107
135
|
const settings2 = loadSettings();
|
|
@@ -215,7 +243,7 @@ class Tab {
|
|
|
215
243
|
this._state.canGoForward = this.urlForwardStack.length > 0;
|
|
216
244
|
this.onChange();
|
|
217
245
|
};
|
|
218
|
-
|
|
246
|
+
const recordNavigation = (url) => {
|
|
219
247
|
if (this.navigatingViaHistory) {
|
|
220
248
|
this.navigatingViaHistory = false;
|
|
221
249
|
this.lastCommittedUrl = url;
|
|
@@ -231,6 +259,9 @@ class Tab {
|
|
|
231
259
|
}
|
|
232
260
|
this.lastCommittedUrl = url;
|
|
233
261
|
syncNavigationState();
|
|
262
|
+
};
|
|
263
|
+
wc.on("did-navigate", (_event, url) => {
|
|
264
|
+
recordNavigation(url);
|
|
234
265
|
});
|
|
235
266
|
wc.on("page-title-updated", (_, title) => {
|
|
236
267
|
this._state.title = title;
|
|
@@ -244,8 +275,9 @@ class Tab {
|
|
|
244
275
|
this._state.isLoading = false;
|
|
245
276
|
syncNavigationState();
|
|
246
277
|
});
|
|
247
|
-
wc.on("did-navigate-in-page", () => {
|
|
248
|
-
|
|
278
|
+
wc.on("did-navigate-in-page", (_event, url, isMainFrame) => {
|
|
279
|
+
if (!isMainFrame) return;
|
|
280
|
+
recordNavigation(url);
|
|
249
281
|
this.onPageLoad?.(wc.getURL(), wc);
|
|
250
282
|
});
|
|
251
283
|
wc.on("did-finish-load", () => {
|
|
@@ -362,7 +394,7 @@ class Tab {
|
|
|
362
394
|
get state() {
|
|
363
395
|
return { ...this._state };
|
|
364
396
|
}
|
|
365
|
-
navigate(url) {
|
|
397
|
+
navigate(url, postBody) {
|
|
366
398
|
if (!/^https?:\/\//i.test(url) && !url.startsWith("about:")) {
|
|
367
399
|
if (url.includes(".") && !url.includes(" ")) {
|
|
368
400
|
url = "https://" + url;
|
|
@@ -375,7 +407,24 @@ class Tab {
|
|
|
375
407
|
}
|
|
376
408
|
const policyError = checkDomainPolicy(url);
|
|
377
409
|
if (policyError) return policyError;
|
|
378
|
-
|
|
410
|
+
if (postBody) {
|
|
411
|
+
const params = new URLSearchParams();
|
|
412
|
+
for (const [key, value] of Object.entries(postBody)) {
|
|
413
|
+
params.set(key, value);
|
|
414
|
+
}
|
|
415
|
+
this.view.webContents.loadURL(url, {
|
|
416
|
+
method: "POST",
|
|
417
|
+
extraHeaders: "Content-Type: application/x-www-form-urlencoded\r\n",
|
|
418
|
+
postData: [
|
|
419
|
+
{
|
|
420
|
+
type: "rawData",
|
|
421
|
+
bytes: Buffer.from(params.toString())
|
|
422
|
+
}
|
|
423
|
+
]
|
|
424
|
+
});
|
|
425
|
+
} else {
|
|
426
|
+
this.view.webContents.loadURL(url);
|
|
427
|
+
}
|
|
379
428
|
return null;
|
|
380
429
|
}
|
|
381
430
|
goBack() {
|
|
@@ -516,6 +565,9 @@ class Tab {
|
|
|
516
565
|
}
|
|
517
566
|
let state$3 = null;
|
|
518
567
|
const listeners$2 = /* @__PURE__ */ new Set();
|
|
568
|
+
const SAVE_DEBOUNCE_MS$2 = 250;
|
|
569
|
+
let saveTimer$2 = null;
|
|
570
|
+
let saveDirty$2 = false;
|
|
519
571
|
function getHighlightsPath() {
|
|
520
572
|
return path.join(electron.app.getPath("userData"), "vessel-highlights.json");
|
|
521
573
|
}
|
|
@@ -532,8 +584,18 @@ function load$2() {
|
|
|
532
584
|
}
|
|
533
585
|
return state$3;
|
|
534
586
|
}
|
|
587
|
+
function persistNow$2() {
|
|
588
|
+
if (!state$3) return Promise.resolve();
|
|
589
|
+
saveDirty$2 = false;
|
|
590
|
+
return fs.promises.writeFile(getHighlightsPath(), JSON.stringify(state$3, null, 2), "utf-8").catch((err) => console.error("[Vessel] Failed to save highlights:", err));
|
|
591
|
+
}
|
|
535
592
|
function save$2() {
|
|
536
|
-
|
|
593
|
+
saveDirty$2 = true;
|
|
594
|
+
if (saveTimer$2) clearTimeout(saveTimer$2);
|
|
595
|
+
saveTimer$2 = setTimeout(() => {
|
|
596
|
+
saveTimer$2 = null;
|
|
597
|
+
void persistNow$2();
|
|
598
|
+
}, SAVE_DEBOUNCE_MS$2);
|
|
537
599
|
}
|
|
538
600
|
function emit$2() {
|
|
539
601
|
if (!state$3) return;
|
|
@@ -563,7 +625,7 @@ function getHighlightsForUrl(url) {
|
|
|
563
625
|
function addHighlight(url, selector, text, label, color, source) {
|
|
564
626
|
load$2();
|
|
565
627
|
const highlight = {
|
|
566
|
-
id: crypto.randomUUID(),
|
|
628
|
+
id: crypto$1.randomUUID(),
|
|
567
629
|
url: normalizeUrl(url),
|
|
568
630
|
selector: selector || void 0,
|
|
569
631
|
text: text || void 0,
|
|
@@ -614,6 +676,14 @@ function clearHighlightsForUrl(url) {
|
|
|
614
676
|
}
|
|
615
677
|
return removed;
|
|
616
678
|
}
|
|
679
|
+
function flushPersist$2() {
|
|
680
|
+
if (saveTimer$2) {
|
|
681
|
+
clearTimeout(saveTimer$2);
|
|
682
|
+
saveTimer$2 = null;
|
|
683
|
+
}
|
|
684
|
+
if (!saveDirty$2) return Promise.resolve();
|
|
685
|
+
return persistNow$2();
|
|
686
|
+
}
|
|
617
687
|
const HIGHLIGHT_COLORS = {
|
|
618
688
|
yellow: {
|
|
619
689
|
solid: "#f0c636",
|
|
@@ -1184,8 +1254,11 @@ function persistHighlight(url, text) {
|
|
|
1184
1254
|
return { success: true, text: capped, id: highlight.id };
|
|
1185
1255
|
}
|
|
1186
1256
|
const MAX_HISTORY_ENTRIES = 5e3;
|
|
1257
|
+
const SAVE_DEBOUNCE_MS$1 = 250;
|
|
1187
1258
|
let state$2 = null;
|
|
1188
1259
|
const listeners$1 = /* @__PURE__ */ new Set();
|
|
1260
|
+
let saveTimer$1 = null;
|
|
1261
|
+
let saveDirty$1 = false;
|
|
1189
1262
|
function getHistoryPath() {
|
|
1190
1263
|
return path.join(electron.app.getPath("userData"), "vessel-history.json");
|
|
1191
1264
|
}
|
|
@@ -1202,8 +1275,13 @@ function load$1() {
|
|
|
1202
1275
|
}
|
|
1203
1276
|
return state$2;
|
|
1204
1277
|
}
|
|
1205
|
-
function
|
|
1206
|
-
|
|
1278
|
+
function persistNow$1() {
|
|
1279
|
+
saveDirty$1 = false;
|
|
1280
|
+
if (saveTimer$1) {
|
|
1281
|
+
clearTimeout(saveTimer$1);
|
|
1282
|
+
saveTimer$1 = null;
|
|
1283
|
+
}
|
|
1284
|
+
return fs.promises.mkdir(path.dirname(getHistoryPath()), { recursive: true }).then(
|
|
1207
1285
|
() => fs.promises.writeFile(
|
|
1208
1286
|
getHistoryPath(),
|
|
1209
1287
|
JSON.stringify(state$2, null, 2),
|
|
@@ -1211,6 +1289,16 @@ function save$1() {
|
|
|
1211
1289
|
)
|
|
1212
1290
|
).catch((err) => console.error("[Vessel] Failed to save history:", err));
|
|
1213
1291
|
}
|
|
1292
|
+
function save$1() {
|
|
1293
|
+
saveDirty$1 = true;
|
|
1294
|
+
if (saveTimer$1) return;
|
|
1295
|
+
saveTimer$1 = setTimeout(() => {
|
|
1296
|
+
saveTimer$1 = null;
|
|
1297
|
+
if (saveDirty$1) {
|
|
1298
|
+
void persistNow$1();
|
|
1299
|
+
}
|
|
1300
|
+
}, SAVE_DEBOUNCE_MS$1);
|
|
1301
|
+
}
|
|
1214
1302
|
function emit$1() {
|
|
1215
1303
|
if (!state$2) return;
|
|
1216
1304
|
const snapshot = { entries: [...state$2.entries] };
|
|
@@ -1265,6 +1353,9 @@ function clearAll$1() {
|
|
|
1265
1353
|
save$1();
|
|
1266
1354
|
emit$1();
|
|
1267
1355
|
}
|
|
1356
|
+
function flushPersist$1() {
|
|
1357
|
+
return saveDirty$1 ? persistNow$1() : Promise.resolve();
|
|
1358
|
+
}
|
|
1268
1359
|
const MAX_CONSOLE_ENTRIES = 500;
|
|
1269
1360
|
const MAX_NETWORK_ENTRIES = 200;
|
|
1270
1361
|
const MAX_ERROR_ENTRIES = 200;
|
|
@@ -1982,7 +2073,7 @@ class TabManager {
|
|
|
1982
2073
|
}
|
|
1983
2074
|
createTab(url = "about:blank", options) {
|
|
1984
2075
|
const background = options?.background ?? false;
|
|
1985
|
-
const id = crypto.randomUUID();
|
|
2076
|
+
const id = crypto$1.randomUUID();
|
|
1986
2077
|
const tab = new Tab(id, url, () => this.broadcastState(), {
|
|
1987
2078
|
adBlockingEnabled: options?.adBlockingEnabled,
|
|
1988
2079
|
parentWindow: this.window,
|
|
@@ -2022,6 +2113,10 @@ class TabManager {
|
|
|
2022
2113
|
closeTab(id) {
|
|
2023
2114
|
const tab = this.tabs.get(id);
|
|
2024
2115
|
if (!tab) return;
|
|
2116
|
+
const wcId = tab.webContentsId;
|
|
2117
|
+
if (wcId !== void 0) {
|
|
2118
|
+
this.lastReapply.delete(wcId);
|
|
2119
|
+
}
|
|
2025
2120
|
destroySession(id);
|
|
2026
2121
|
this.window.contentView.removeChildView(tab.view);
|
|
2027
2122
|
tab.destroy();
|
|
@@ -2037,9 +2132,10 @@ class TabManager {
|
|
|
2037
2132
|
this.broadcastState();
|
|
2038
2133
|
}
|
|
2039
2134
|
}
|
|
2040
|
-
navigateTab(id, url) {
|
|
2135
|
+
navigateTab(id, url, postBody) {
|
|
2041
2136
|
const tab = this.tabs.get(id);
|
|
2042
|
-
if (tab) tab
|
|
2137
|
+
if (!tab) return `No tab with id ${id}`;
|
|
2138
|
+
return tab.navigate(url, postBody);
|
|
2043
2139
|
}
|
|
2044
2140
|
goBack(id) {
|
|
2045
2141
|
return this.tabs.get(id)?.goBack() ?? false;
|
|
@@ -2278,6 +2374,10 @@ const Channels = {
|
|
|
2278
2374
|
AI_STREAM_START: "ai:stream-start",
|
|
2279
2375
|
AI_STREAM_CHUNK: "ai:stream-chunk",
|
|
2280
2376
|
AI_STREAM_END: "ai:stream-end",
|
|
2377
|
+
AI_STREAM_IDLE: "ai:stream-idle",
|
|
2378
|
+
AUTOMATION_ACTIVITY_START: "automation:activity-start",
|
|
2379
|
+
AUTOMATION_ACTIVITY_CHUNK: "automation:activity-chunk",
|
|
2380
|
+
AUTOMATION_ACTIVITY_END: "automation:activity-end",
|
|
2281
2381
|
AI_CANCEL: "ai:cancel",
|
|
2282
2382
|
AI_FETCH_MODELS: "ai:fetch-models",
|
|
2283
2383
|
AGENT_RUNTIME_GET: "agent-runtime:get",
|
|
@@ -2306,6 +2406,7 @@ const Channels = {
|
|
|
2306
2406
|
SETTINGS_SET: "settings:set",
|
|
2307
2407
|
SETTINGS_UPDATE: "settings:update",
|
|
2308
2408
|
SETTINGS_HEALTH_GET: "settings:health:get",
|
|
2409
|
+
SETTINGS_HEALTH_UPDATE: "settings:health:update",
|
|
2309
2410
|
// Bookmarks
|
|
2310
2411
|
BOOKMARKS_GET: "bookmarks:get",
|
|
2311
2412
|
BOOKMARKS_UPDATE: "bookmarks:update",
|
|
@@ -2320,6 +2421,7 @@ const Channels = {
|
|
|
2320
2421
|
HIGHLIGHT_CAPTURE_RESULT: "highlights:capture-result",
|
|
2321
2422
|
HIGHLIGHT_SELECTION: "vessel:highlight-selection",
|
|
2322
2423
|
HIGHLIGHT_NAV_COUNT: "highlights:nav-count",
|
|
2424
|
+
HIGHLIGHT_COUNT_UPDATE: "highlights:count-update",
|
|
2323
2425
|
HIGHLIGHT_NAV_SCROLL: "highlights:nav-scroll",
|
|
2324
2426
|
HIGHLIGHT_NAV_REMOVE: "highlights:nav-remove",
|
|
2325
2427
|
HIGHLIGHT_NAV_CLEAR: "highlights:nav-clear",
|
|
@@ -2355,6 +2457,16 @@ const Channels = {
|
|
|
2355
2457
|
VAULT_UPDATE: "vault:update",
|
|
2356
2458
|
VAULT_REMOVE: "vault:remove",
|
|
2357
2459
|
VAULT_AUDIT_LOG: "vault:audit-log",
|
|
2460
|
+
// Automation kits
|
|
2461
|
+
AUTOMATION_GET_INSTALLED: "automation:get-installed",
|
|
2462
|
+
AUTOMATION_INSTALL_FROM_FILE: "automation:install-from-file",
|
|
2463
|
+
AUTOMATION_UNINSTALL: "automation:uninstall",
|
|
2464
|
+
// Scheduled jobs
|
|
2465
|
+
SCHEDULE_GET_ALL: "schedule:get-all",
|
|
2466
|
+
SCHEDULE_CREATE: "schedule:create",
|
|
2467
|
+
SCHEDULE_UPDATE: "schedule:update",
|
|
2468
|
+
SCHEDULE_DELETE: "schedule:delete",
|
|
2469
|
+
SCHEDULE_JOBS_UPDATE: "schedule:jobs-update",
|
|
2358
2470
|
// Window controls
|
|
2359
2471
|
WINDOW_MINIMIZE: "window:minimize",
|
|
2360
2472
|
WINDOW_MAXIMIZE: "window:maximize",
|
|
@@ -2515,6 +2627,7 @@ function createMainWindow(onTabStateChange) {
|
|
|
2515
2627
|
minWidth: 800,
|
|
2516
2628
|
minHeight: 600,
|
|
2517
2629
|
frame: false,
|
|
2630
|
+
show: false,
|
|
2518
2631
|
backgroundColor: "#1a1a1e",
|
|
2519
2632
|
icon: getWindowIconPath()
|
|
2520
2633
|
});
|
|
@@ -2557,7 +2670,7 @@ function createMainWindow(onTabStateChange) {
|
|
|
2557
2670
|
enableClipboardShortcuts(devtoolsPanelView);
|
|
2558
2671
|
const settings2 = loadSettings();
|
|
2559
2672
|
const uiState = {
|
|
2560
|
-
sidebarOpen:
|
|
2673
|
+
sidebarOpen: true,
|
|
2561
2674
|
sidebarWidth: settings2.sidebarWidth,
|
|
2562
2675
|
focusMode: false,
|
|
2563
2676
|
settingsOpen: false,
|
|
@@ -3374,7 +3487,7 @@ function getDeviceId() {
|
|
|
3374
3487
|
if (deviceId) return deviceId;
|
|
3375
3488
|
} catch {
|
|
3376
3489
|
}
|
|
3377
|
-
deviceId = crypto.randomUUID();
|
|
3490
|
+
deviceId = crypto$1.randomUUID();
|
|
3378
3491
|
try {
|
|
3379
3492
|
fs.mkdirSync(path.dirname(idPath), { recursive: true });
|
|
3380
3493
|
fs.writeFileSync(idPath, deviceId, "utf-8");
|
|
@@ -4687,15 +4800,28 @@ function escapeHtml(str) {
|
|
|
4687
4800
|
return str.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """);
|
|
4688
4801
|
}
|
|
4689
4802
|
const mcpStatusChangeListeners = /* @__PURE__ */ new Set();
|
|
4803
|
+
const runtimeHealthChangeListeners = /* @__PURE__ */ new Set();
|
|
4690
4804
|
function onMcpStatusChange(listener) {
|
|
4691
4805
|
mcpStatusChangeListeners.add(listener);
|
|
4692
4806
|
return () => {
|
|
4693
4807
|
mcpStatusChangeListeners.delete(listener);
|
|
4694
4808
|
};
|
|
4695
4809
|
}
|
|
4810
|
+
function onRuntimeHealthChange(listener) {
|
|
4811
|
+
runtimeHealthChangeListeners.add(listener);
|
|
4812
|
+
return () => {
|
|
4813
|
+
runtimeHealthChangeListeners.delete(listener);
|
|
4814
|
+
};
|
|
4815
|
+
}
|
|
4696
4816
|
function getMcpStatus() {
|
|
4697
4817
|
return state$1.mcp.status;
|
|
4698
4818
|
}
|
|
4819
|
+
function emitRuntimeHealthChange() {
|
|
4820
|
+
const snapshot = getRuntimeHealth();
|
|
4821
|
+
for (const listener of runtimeHealthChangeListeners) {
|
|
4822
|
+
listener(snapshot);
|
|
4823
|
+
}
|
|
4824
|
+
}
|
|
4699
4825
|
const state$1 = {
|
|
4700
4826
|
userDataPath: "",
|
|
4701
4827
|
settingsPath: "",
|
|
@@ -4716,9 +4842,11 @@ function initializeRuntimeHealth(paths) {
|
|
|
4716
4842
|
state$1.mcp.endpoint = null;
|
|
4717
4843
|
state$1.mcp.status = "stopped";
|
|
4718
4844
|
state$1.mcp.message = "MCP server has not started yet.";
|
|
4845
|
+
emitRuntimeHealthChange();
|
|
4719
4846
|
}
|
|
4720
4847
|
function setStartupIssues(issues) {
|
|
4721
4848
|
state$1.startupIssues = issues.map((issue) => ({ ...issue }));
|
|
4849
|
+
emitRuntimeHealthChange();
|
|
4722
4850
|
}
|
|
4723
4851
|
function getRuntimeHealth() {
|
|
4724
4852
|
return {
|
|
@@ -4746,6 +4874,7 @@ function setMcpHealth(update) {
|
|
|
4746
4874
|
listener(state$1.mcp.status);
|
|
4747
4875
|
}
|
|
4748
4876
|
}
|
|
4877
|
+
emitRuntimeHealthChange();
|
|
4749
4878
|
}
|
|
4750
4879
|
const VAULT_FILENAME = "vessel-vault.enc";
|
|
4751
4880
|
const KEY_FILENAME = "vessel-vault.key";
|
|
@@ -4771,7 +4900,7 @@ function getOrCreateEncryptionKey() {
|
|
|
4771
4900
|
}
|
|
4772
4901
|
return encryptedKey;
|
|
4773
4902
|
}
|
|
4774
|
-
const key = crypto$
|
|
4903
|
+
const key = crypto$2.randomBytes(32);
|
|
4775
4904
|
fs$1.mkdirSync(path$1.dirname(keyPath), { recursive: true });
|
|
4776
4905
|
if (electron.safeStorage.isEncryptionAvailable()) {
|
|
4777
4906
|
const encrypted = electron.safeStorage.encryptString(key.toString("utf-8"));
|
|
@@ -4784,8 +4913,8 @@ function getOrCreateEncryptionKey() {
|
|
|
4784
4913
|
}
|
|
4785
4914
|
function encrypt(plaintext) {
|
|
4786
4915
|
const key = getOrCreateEncryptionKey();
|
|
4787
|
-
const iv = crypto$
|
|
4788
|
-
const cipher = crypto$
|
|
4916
|
+
const iv = crypto$2.randomBytes(IV_LENGTH);
|
|
4917
|
+
const cipher = crypto$2.createCipheriv(ALGORITHM, key, iv, {
|
|
4789
4918
|
authTagLength: AUTH_TAG_LENGTH
|
|
4790
4919
|
});
|
|
4791
4920
|
const encrypted = Buffer.concat([
|
|
@@ -4800,7 +4929,7 @@ function decrypt(data) {
|
|
|
4800
4929
|
const iv = data.subarray(0, IV_LENGTH);
|
|
4801
4930
|
const authTag = data.subarray(IV_LENGTH, IV_LENGTH + AUTH_TAG_LENGTH);
|
|
4802
4931
|
const ciphertext = data.subarray(IV_LENGTH + AUTH_TAG_LENGTH);
|
|
4803
|
-
const decipher = crypto$
|
|
4932
|
+
const decipher = crypto$2.createDecipheriv(ALGORITHM, key, iv, {
|
|
4804
4933
|
authTagLength: AUTH_TAG_LENGTH
|
|
4805
4934
|
});
|
|
4806
4935
|
decipher.setAuthTag(authTag);
|
|
@@ -4859,7 +4988,7 @@ function addEntry(entry) {
|
|
|
4859
4988
|
const entries = loadVault();
|
|
4860
4989
|
const newEntry = {
|
|
4861
4990
|
...entry,
|
|
4862
|
-
id: crypto$
|
|
4991
|
+
id: crypto$2.randomUUID(),
|
|
4863
4992
|
createdAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
4864
4993
|
useCount: 0
|
|
4865
4994
|
};
|
|
@@ -4918,7 +5047,7 @@ function generateTotpCode(secret) {
|
|
|
4918
5047
|
const counterBuf = Buffer.alloc(8);
|
|
4919
5048
|
counterBuf.writeUInt32BE(Math.floor(counter / 4294967296), 0);
|
|
4920
5049
|
counterBuf.writeUInt32BE(counter & 4294967295, 4);
|
|
4921
|
-
const hmac = crypto$
|
|
5050
|
+
const hmac = crypto$2.createHmac("sha1", keyBytes).update(counterBuf).digest();
|
|
4922
5051
|
const offset = hmac[hmac.length - 1] & 15;
|
|
4923
5052
|
const code = (hmac[offset] & 127) << 24 | (hmac[offset + 1] & 255) << 16 | (hmac[offset + 2] & 255) << 8 | hmac[offset + 3] & 255;
|
|
4924
5053
|
return (code % 1e6).toString().padStart(6, "0");
|
|
@@ -5524,6 +5653,48 @@ function createProvider(config) {
|
|
|
5524
5653
|
}
|
|
5525
5654
|
return new OpenAICompatProvider(normalized);
|
|
5526
5655
|
}
|
|
5656
|
+
const require$1 = node_module.createRequire(require("url").pathToFileURL(__filename).href);
|
|
5657
|
+
let cachedFactory;
|
|
5658
|
+
function createNoopTraceSession() {
|
|
5659
|
+
return {
|
|
5660
|
+
logToolCall() {
|
|
5661
|
+
},
|
|
5662
|
+
end() {
|
|
5663
|
+
}
|
|
5664
|
+
};
|
|
5665
|
+
}
|
|
5666
|
+
function getCandidatePaths() {
|
|
5667
|
+
const roots = /* @__PURE__ */ new Set([
|
|
5668
|
+
process.cwd(),
|
|
5669
|
+
electron.app.getAppPath(),
|
|
5670
|
+
path$1.join(electron.app.getAppPath(), "..")
|
|
5671
|
+
]);
|
|
5672
|
+
return Array.from(roots).map(
|
|
5673
|
+
(root) => path$1.join(root, "src", "main", "telemetry", "trace-logger.local.cjs")
|
|
5674
|
+
);
|
|
5675
|
+
}
|
|
5676
|
+
function loadLocalFactory() {
|
|
5677
|
+
if (cachedFactory !== void 0) return cachedFactory;
|
|
5678
|
+
cachedFactory = null;
|
|
5679
|
+
if (electron.app.isPackaged) return cachedFactory;
|
|
5680
|
+
for (const candidate of getCandidatePaths()) {
|
|
5681
|
+
if (!fs$1.existsSync(candidate)) continue;
|
|
5682
|
+
try {
|
|
5683
|
+
const loaded = require$1(candidate);
|
|
5684
|
+
if (typeof loaded.createTraceSession === "function") {
|
|
5685
|
+
cachedFactory = loaded.createTraceSession;
|
|
5686
|
+
return cachedFactory;
|
|
5687
|
+
}
|
|
5688
|
+
} catch (err) {
|
|
5689
|
+
console.warn("[dev-trace] Failed to load local trace logger:", err);
|
|
5690
|
+
}
|
|
5691
|
+
}
|
|
5692
|
+
return cachedFactory;
|
|
5693
|
+
}
|
|
5694
|
+
function createTraceSession(query, url, title) {
|
|
5695
|
+
const factory = loadLocalFactory();
|
|
5696
|
+
return factory ? factory(query, url, title) : createNoopTraceSession();
|
|
5697
|
+
}
|
|
5527
5698
|
const CORRECT_HINT_RE = /\b(correct|right choice|this is correct|correct answer|pick this|select this|choose this|right answer)\b/i;
|
|
5528
5699
|
const WRONG_HINT_RE = /\b(wrong|incorrect|not this|don't pick|do not pick|bad option|decoy)\b/i;
|
|
5529
5700
|
function elementLabel(el) {
|
|
@@ -7137,9 +7308,12 @@ const TOOL_DEFINITIONS = [
|
|
|
7137
7308
|
{
|
|
7138
7309
|
name: "navigate",
|
|
7139
7310
|
title: "Navigate",
|
|
7140
|
-
description: "Navigate the browser to a URL.",
|
|
7311
|
+
description: "Navigate the browser to a URL. Use postBody to submit data via POST request (e.g. form submissions).",
|
|
7141
7312
|
inputSchema: {
|
|
7142
|
-
url: zod.z.string().describe("The URL to navigate to")
|
|
7313
|
+
url: zod.z.string().describe("The URL to navigate to"),
|
|
7314
|
+
postBody: zod.z.record(zod.z.string(), zod.z.string()).optional().describe(
|
|
7315
|
+
"Optional form fields to submit via POST (application/x-www-form-urlencoded). Only supported on http/https URLs."
|
|
7316
|
+
)
|
|
7143
7317
|
},
|
|
7144
7318
|
tier: 0
|
|
7145
7319
|
},
|
|
@@ -7165,7 +7339,7 @@ const TOOL_DEFINITIONS = [
|
|
|
7165
7339
|
{
|
|
7166
7340
|
name: "click",
|
|
7167
7341
|
title: "Click Element",
|
|
7168
|
-
description: "Click an element on the page by its index number or CSS selector.",
|
|
7342
|
+
description: "Click an element on the page by its index number or CSS selector. Use this to check or uncheck checkboxes and to select radio buttons — do NOT use select_option for those.",
|
|
7169
7343
|
inputSchema: {
|
|
7170
7344
|
index: zod.z.number().optional().describe("Element index from the page content listing"),
|
|
7171
7345
|
selector: zod.z.string().optional().describe("CSS selector as fallback")
|
|
@@ -7190,7 +7364,7 @@ const TOOL_DEFINITIONS = [
|
|
|
7190
7364
|
{
|
|
7191
7365
|
name: "select_option",
|
|
7192
7366
|
title: "Select Option",
|
|
7193
|
-
description: "Select an option in a dropdown by visible label or option value.",
|
|
7367
|
+
description: "Select an option in a <select> dropdown by visible label or option value. Only works on <select> elements — for checkboxes or radio buttons use click instead.",
|
|
7194
7368
|
inputSchema: {
|
|
7195
7369
|
index: zod.z.number().optional().describe("The select element index number"),
|
|
7196
7370
|
selector: zod.z.string().optional().describe("CSS selector as fallback"),
|
|
@@ -8000,8 +8174,11 @@ function getBookmarkSearchMatch(args) {
|
|
|
8000
8174
|
}
|
|
8001
8175
|
const UNSORTED_ID = "unsorted";
|
|
8002
8176
|
const ARCHIVE_FOLDER_NAME = "Archive";
|
|
8177
|
+
const SAVE_DEBOUNCE_MS = 250;
|
|
8003
8178
|
let state = null;
|
|
8004
8179
|
const listeners = /* @__PURE__ */ new Set();
|
|
8180
|
+
let saveTimer = null;
|
|
8181
|
+
let saveDirty = false;
|
|
8005
8182
|
function cloneState(current) {
|
|
8006
8183
|
return {
|
|
8007
8184
|
folders: current.folders.map((folder) => ({ ...folder })),
|
|
@@ -8025,8 +8202,13 @@ function load() {
|
|
|
8025
8202
|
}
|
|
8026
8203
|
return state;
|
|
8027
8204
|
}
|
|
8028
|
-
function
|
|
8029
|
-
|
|
8205
|
+
function persistNow() {
|
|
8206
|
+
saveDirty = false;
|
|
8207
|
+
if (saveTimer) {
|
|
8208
|
+
clearTimeout(saveTimer);
|
|
8209
|
+
saveTimer = null;
|
|
8210
|
+
}
|
|
8211
|
+
return fs.promises.mkdir(path.dirname(getBookmarksPath()), { recursive: true }).then(
|
|
8030
8212
|
() => fs.promises.writeFile(
|
|
8031
8213
|
getBookmarksPath(),
|
|
8032
8214
|
JSON.stringify(state, null, 2),
|
|
@@ -8034,6 +8216,16 @@ function save() {
|
|
|
8034
8216
|
)
|
|
8035
8217
|
).catch((err) => console.error("[Vessel] Failed to save bookmarks:", err));
|
|
8036
8218
|
}
|
|
8219
|
+
function save() {
|
|
8220
|
+
saveDirty = true;
|
|
8221
|
+
if (saveTimer) return;
|
|
8222
|
+
saveTimer = setTimeout(() => {
|
|
8223
|
+
saveTimer = null;
|
|
8224
|
+
if (saveDirty) {
|
|
8225
|
+
void persistNow();
|
|
8226
|
+
}
|
|
8227
|
+
}, SAVE_DEBOUNCE_MS);
|
|
8228
|
+
}
|
|
8037
8229
|
function emit() {
|
|
8038
8230
|
if (!state) return;
|
|
8039
8231
|
const snapshot = cloneState(state);
|
|
@@ -8147,7 +8339,7 @@ function createFolderWithSummary(name, summary) {
|
|
|
8147
8339
|
const trimmed = name.trim();
|
|
8148
8340
|
if (!trimmed) throw new Error("Folder name cannot be empty");
|
|
8149
8341
|
const folder = {
|
|
8150
|
-
id: crypto.randomUUID(),
|
|
8342
|
+
id: crypto$1.randomUUID(),
|
|
8151
8343
|
name: trimmed,
|
|
8152
8344
|
summary: summary?.trim() || void 0,
|
|
8153
8345
|
createdAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
@@ -8215,7 +8407,7 @@ function saveBookmarkWithPolicy(url, title, folderId, note, options) {
|
|
|
8215
8407
|
}
|
|
8216
8408
|
}
|
|
8217
8409
|
const bookmark = {
|
|
8218
|
-
id: crypto.randomUUID(),
|
|
8410
|
+
id: crypto$1.randomUUID(),
|
|
8219
8411
|
url: normalizedUrl,
|
|
8220
8412
|
title: normalizedTitle,
|
|
8221
8413
|
note: note?.trim() || void 0,
|
|
@@ -8284,6 +8476,9 @@ function renameFolder(id, newName, summary) {
|
|
|
8284
8476
|
emit();
|
|
8285
8477
|
return { ...folder };
|
|
8286
8478
|
}
|
|
8479
|
+
function flushPersist() {
|
|
8480
|
+
return saveDirty ? persistNow() : Promise.resolve();
|
|
8481
|
+
}
|
|
8287
8482
|
function normalizeText(text) {
|
|
8288
8483
|
return text?.trim() ?? "";
|
|
8289
8484
|
}
|
|
@@ -8510,7 +8705,7 @@ function normalizeSessionName(name) {
|
|
|
8510
8705
|
function sessionFileName(name) {
|
|
8511
8706
|
const normalized = normalizeSessionName(name).toLowerCase();
|
|
8512
8707
|
const slug = normalized.replace(/[^a-z0-9._-]+/g, "-").replace(/^-+|-+$/g, "").slice(0, 48) || "session";
|
|
8513
|
-
const hash = crypto$
|
|
8708
|
+
const hash = crypto$2.createHash("sha256").update(normalized).digest("hex").slice(0, 8);
|
|
8514
8709
|
return `${slug}-${hash}.json`;
|
|
8515
8710
|
}
|
|
8516
8711
|
function getSessionPath(name) {
|
|
@@ -11418,7 +11613,7 @@ async function executeAction(name, args, ctx) {
|
|
|
11418
11613
|
const createdId = ctx.tabManager.createTab(
|
|
11419
11614
|
typeof args.url === "string" && args.url.trim() ? args.url.trim() : "about:blank"
|
|
11420
11615
|
);
|
|
11421
|
-
const created = ctx.tabManager.
|
|
11616
|
+
const created = ctx.tabManager.getTab(createdId);
|
|
11422
11617
|
if (created) {
|
|
11423
11618
|
await waitForLoad$1(created.view.webContents);
|
|
11424
11619
|
return `Created tab ${createdId}${await getPostNavSummary(created.view.webContents)}`;
|
|
@@ -11431,7 +11626,8 @@ async function executeAction(name, args, ctx) {
|
|
|
11431
11626
|
if (navValidation.status === "dead") {
|
|
11432
11627
|
return `Navigation blocked: ${args.url} returned ${navValidation.detail || "dead link"}. Try a different URL or go back and choose another link.`;
|
|
11433
11628
|
}
|
|
11434
|
-
ctx.tabManager.navigateTab(tabId, args.url);
|
|
11629
|
+
const navError = ctx.tabManager.navigateTab(tabId, args.url, args.postBody);
|
|
11630
|
+
if (navError) return navError;
|
|
11435
11631
|
await waitForLoad$1(wc);
|
|
11436
11632
|
return `Navigated to ${wc.getURL()}${await getPostNavSummary(wc)}`;
|
|
11437
11633
|
}
|
|
@@ -12638,13 +12834,38 @@ Instructions:
|
|
|
12638
12834
|
pageType,
|
|
12639
12835
|
query
|
|
12640
12836
|
);
|
|
12837
|
+
const trace = createTraceSession(query, activeTabUrl, activeTabTitle);
|
|
12838
|
+
let accumulatedResponse = "";
|
|
12839
|
+
const tracedOnChunk = (text) => {
|
|
12840
|
+
accumulatedResponse += text;
|
|
12841
|
+
onChunk(text);
|
|
12842
|
+
};
|
|
12843
|
+
const tracedOnEnd = () => {
|
|
12844
|
+
trace.end(accumulatedResponse);
|
|
12845
|
+
onEnd();
|
|
12846
|
+
};
|
|
12847
|
+
const tracedExecuteAction = async (name, args) => {
|
|
12848
|
+
const t0 = Date.now();
|
|
12849
|
+
let output = "";
|
|
12850
|
+
let isError = false;
|
|
12851
|
+
try {
|
|
12852
|
+
output = await executeAction(name, args, actionCtx);
|
|
12853
|
+
} catch (err) {
|
|
12854
|
+
isError = true;
|
|
12855
|
+
output = err instanceof Error ? err.message : String(err);
|
|
12856
|
+
throw err;
|
|
12857
|
+
} finally {
|
|
12858
|
+
trace.logToolCall(name, args, output, Date.now() - t0, isError);
|
|
12859
|
+
}
|
|
12860
|
+
return output;
|
|
12861
|
+
};
|
|
12641
12862
|
await provider.streamAgentQuery(
|
|
12642
12863
|
systemPrompt,
|
|
12643
12864
|
query,
|
|
12644
12865
|
contextualTools,
|
|
12645
|
-
|
|
12646
|
-
|
|
12647
|
-
|
|
12866
|
+
tracedOnChunk,
|
|
12867
|
+
tracedExecuteAction,
|
|
12868
|
+
tracedOnEnd,
|
|
12648
12869
|
history
|
|
12649
12870
|
);
|
|
12650
12871
|
return;
|
|
@@ -12674,6 +12895,27 @@ Instructions:
|
|
|
12674
12895
|
history
|
|
12675
12896
|
);
|
|
12676
12897
|
}
|
|
12898
|
+
let activeSource = null;
|
|
12899
|
+
const idleListeners = /* @__PURE__ */ new Set();
|
|
12900
|
+
function tryBeginAIStream(source) {
|
|
12901
|
+
if (activeSource !== null) return false;
|
|
12902
|
+
activeSource = source;
|
|
12903
|
+
return true;
|
|
12904
|
+
}
|
|
12905
|
+
function endAIStream(source) {
|
|
12906
|
+
if (activeSource !== source) return;
|
|
12907
|
+
activeSource = null;
|
|
12908
|
+
for (const listener of idleListeners) {
|
|
12909
|
+
listener();
|
|
12910
|
+
}
|
|
12911
|
+
}
|
|
12912
|
+
function isAIStreamActive() {
|
|
12913
|
+
return activeSource !== null;
|
|
12914
|
+
}
|
|
12915
|
+
function onAIStreamIdle(listener) {
|
|
12916
|
+
idleListeners.add(listener);
|
|
12917
|
+
return () => idleListeners.delete(listener);
|
|
12918
|
+
}
|
|
12677
12919
|
const DEFAULT_PAGE_FOLDER = "Vessel/Pages";
|
|
12678
12920
|
const DEFAULT_NOTE_FOLDER = "Vessel/Research";
|
|
12679
12921
|
const DEFAULT_BOOKMARK_FOLDER = "Vessel/Bookmarks";
|
|
@@ -15175,10 +15417,15 @@ ${buildScopedContext(pageContent, mode)}`;
|
|
|
15175
15417
|
"vessel_navigate",
|
|
15176
15418
|
{
|
|
15177
15419
|
title: "Navigate",
|
|
15178
|
-
description: "Navigate the active browser tab to a URL.",
|
|
15179
|
-
inputSchema: {
|
|
15420
|
+
description: "Navigate the active browser tab to a URL. Use postBody to submit data via POST request (e.g. form submissions).",
|
|
15421
|
+
inputSchema: {
|
|
15422
|
+
url: zod.z.string().describe("The URL to navigate to"),
|
|
15423
|
+
postBody: zod.z.record(zod.z.string(), zod.z.string()).optional().describe(
|
|
15424
|
+
"Optional form fields to submit via POST (application/x-www-form-urlencoded). Only supported on http/https URLs."
|
|
15425
|
+
)
|
|
15426
|
+
}
|
|
15180
15427
|
},
|
|
15181
|
-
async ({ url }) => {
|
|
15428
|
+
async ({ url, postBody }) => {
|
|
15182
15429
|
const tab = tabManager.getActiveTab();
|
|
15183
15430
|
if (!tab) return asTextResponse("Error: No active tab");
|
|
15184
15431
|
const preCheck = await validateLinkDestination(url);
|
|
@@ -15187,9 +15434,17 @@ ${buildScopedContext(pageContent, mode)}`;
|
|
|
15187
15434
|
`Navigation blocked: ${url} returned ${preCheck.detail || "dead link"}. Try a different URL or go back and choose another link.`
|
|
15188
15435
|
);
|
|
15189
15436
|
}
|
|
15437
|
+
try {
|
|
15438
|
+
assertSafeURL(url);
|
|
15439
|
+
} catch (err) {
|
|
15440
|
+
return asTextResponse(
|
|
15441
|
+
`Navigation blocked: ${err instanceof Error ? err.message : "Unsafe URL scheme"}`
|
|
15442
|
+
);
|
|
15443
|
+
}
|
|
15190
15444
|
return withAction(runtime2, tabManager, "navigate", { url }, async () => {
|
|
15191
15445
|
const id = tabManager.getActiveTabId();
|
|
15192
|
-
tabManager.navigateTab(id, url);
|
|
15446
|
+
const navError = tabManager.navigateTab(id, url, postBody);
|
|
15447
|
+
if (navError) return navError;
|
|
15193
15448
|
const { httpStatus } = await waitForLoadWithStatus(
|
|
15194
15449
|
tab.view.webContents
|
|
15195
15450
|
);
|
|
@@ -15866,27 +16121,6 @@ ${buildScopedContext(pageContent, mode)}`;
|
|
|
15866
16121
|
}
|
|
15867
16122
|
)
|
|
15868
16123
|
);
|
|
15869
|
-
server.registerTool(
|
|
15870
|
-
"vessel_create_checkpoint",
|
|
15871
|
-
{
|
|
15872
|
-
title: "Create Checkpoint",
|
|
15873
|
-
description: "Alias for vessel_checkpoint_create. Capture the current session as a checkpoint.",
|
|
15874
|
-
inputSchema: {
|
|
15875
|
-
name: zod.z.string().optional().describe("Optional checkpoint name"),
|
|
15876
|
-
note: zod.z.string().optional().describe("Optional note")
|
|
15877
|
-
}
|
|
15878
|
-
},
|
|
15879
|
-
async ({ name, note }) => withAction(
|
|
15880
|
-
runtime2,
|
|
15881
|
-
tabManager,
|
|
15882
|
-
"create_checkpoint",
|
|
15883
|
-
{ name, note },
|
|
15884
|
-
async () => {
|
|
15885
|
-
const checkpoint = runtime2.createCheckpoint(name, note);
|
|
15886
|
-
return `Created checkpoint ${checkpoint.name} (${checkpoint.id})`;
|
|
15887
|
-
}
|
|
15888
|
-
)
|
|
15889
|
-
);
|
|
15890
16124
|
server.registerTool(
|
|
15891
16125
|
"vessel_checkpoint_restore",
|
|
15892
16126
|
{
|
|
@@ -15913,32 +16147,6 @@ ${buildScopedContext(pageContent, mode)}`;
|
|
|
15913
16147
|
}
|
|
15914
16148
|
)
|
|
15915
16149
|
);
|
|
15916
|
-
server.registerTool(
|
|
15917
|
-
"vessel_restore_checkpoint",
|
|
15918
|
-
{
|
|
15919
|
-
title: "Restore Checkpoint",
|
|
15920
|
-
description: "Alias for vessel_checkpoint_restore. Restore a saved checkpoint by ID or exact name.",
|
|
15921
|
-
inputSchema: {
|
|
15922
|
-
checkpointId: zod.z.string().optional().describe("Checkpoint ID"),
|
|
15923
|
-
name: zod.z.string().optional().describe("Exact checkpoint name")
|
|
15924
|
-
}
|
|
15925
|
-
},
|
|
15926
|
-
async ({ checkpointId, name }) => withAction(
|
|
15927
|
-
runtime2,
|
|
15928
|
-
tabManager,
|
|
15929
|
-
"restore_checkpoint",
|
|
15930
|
-
{ checkpointId, name },
|
|
15931
|
-
async () => {
|
|
15932
|
-
const state2 = runtime2.getState();
|
|
15933
|
-
const checkpoint = state2.checkpoints.find((item) => item.id === checkpointId) || state2.checkpoints.find((item) => item.name === name);
|
|
15934
|
-
if (!checkpoint) {
|
|
15935
|
-
return "Error: No matching checkpoint found";
|
|
15936
|
-
}
|
|
15937
|
-
runtime2.restoreCheckpoint(checkpoint.id);
|
|
15938
|
-
return `Restored checkpoint ${checkpoint.name}`;
|
|
15939
|
-
}
|
|
15940
|
-
)
|
|
15941
|
-
);
|
|
15942
16150
|
server.registerTool(
|
|
15943
16151
|
"vessel_save_session",
|
|
15944
16152
|
{
|
|
@@ -18071,7 +18279,7 @@ function startMcpServer(tabManager, runtime2, port) {
|
|
|
18071
18279
|
status: "starting",
|
|
18072
18280
|
message: `Starting MCP server on port ${port}.`
|
|
18073
18281
|
});
|
|
18074
|
-
mcpAuthToken = crypto$
|
|
18282
|
+
mcpAuthToken = crypto$2.randomBytes(32).toString("hex");
|
|
18075
18283
|
return new Promise((resolve) => {
|
|
18076
18284
|
const server = http.createServer(async (req, res) => {
|
|
18077
18285
|
const url = new URL(req.url || "/", `http://localhost:${port}`);
|
|
@@ -18162,8 +18370,7 @@ function startMcpServer(tabManager, runtime2, port) {
|
|
|
18162
18370
|
status: "ready",
|
|
18163
18371
|
message: `MCP server listening on ${endpoint}.`
|
|
18164
18372
|
});
|
|
18165
|
-
console.log(`[Vessel MCP] Server listening on ${endpoint}`);
|
|
18166
|
-
console.log(`[Vessel MCP] Auth token: ${mcpAuthToken}`);
|
|
18373
|
+
console.log(`[Vessel MCP] Server listening on ${endpoint} (auth enabled)`);
|
|
18167
18374
|
finish({
|
|
18168
18375
|
ok: true,
|
|
18169
18376
|
configuredPort: port,
|
|
@@ -18201,6 +18408,372 @@ function stopMcpServer() {
|
|
|
18201
18408
|
});
|
|
18202
18409
|
});
|
|
18203
18410
|
}
|
|
18411
|
+
const BUNDLED_KIT_IDS = /* @__PURE__ */ new Set([
|
|
18412
|
+
"research-collect",
|
|
18413
|
+
"price-scout",
|
|
18414
|
+
"form-filler"
|
|
18415
|
+
]);
|
|
18416
|
+
function getUserKitsDir() {
|
|
18417
|
+
return path$1.join(electron.app.getPath("userData"), "kits");
|
|
18418
|
+
}
|
|
18419
|
+
function ensureKitsDir() {
|
|
18420
|
+
const dir = getUserKitsDir();
|
|
18421
|
+
if (!fs$1.existsSync(dir)) {
|
|
18422
|
+
fs$1.mkdirSync(dir, { recursive: true });
|
|
18423
|
+
}
|
|
18424
|
+
}
|
|
18425
|
+
function isValidKit(value) {
|
|
18426
|
+
if (!value || typeof value !== "object") return false;
|
|
18427
|
+
const k = value;
|
|
18428
|
+
return typeof k.id === "string" && k.id.length > 0 && typeof k.name === "string" && k.name.length > 0 && typeof k.description === "string" && typeof k.icon === "string" && typeof k.promptTemplate === "string" && k.promptTemplate.length > 0 && Array.isArray(k.inputs);
|
|
18429
|
+
}
|
|
18430
|
+
function getInstalledKits() {
|
|
18431
|
+
ensureKitsDir();
|
|
18432
|
+
const dir = getUserKitsDir();
|
|
18433
|
+
let files;
|
|
18434
|
+
try {
|
|
18435
|
+
files = fs$1.readdirSync(dir).filter((f) => f.endsWith(".kit.json"));
|
|
18436
|
+
} catch {
|
|
18437
|
+
return [];
|
|
18438
|
+
}
|
|
18439
|
+
const kits = [];
|
|
18440
|
+
for (const file of files) {
|
|
18441
|
+
try {
|
|
18442
|
+
const raw = fs$1.readFileSync(path$1.join(dir, file), "utf-8");
|
|
18443
|
+
const parsed = JSON.parse(raw);
|
|
18444
|
+
if (isValidKit(parsed)) {
|
|
18445
|
+
kits.push(parsed);
|
|
18446
|
+
} else {
|
|
18447
|
+
console.warn(`[kit-registry] Skipping invalid kit file: ${file}`);
|
|
18448
|
+
}
|
|
18449
|
+
} catch {
|
|
18450
|
+
console.warn(`[kit-registry] Failed to read kit file: ${file}`);
|
|
18451
|
+
}
|
|
18452
|
+
}
|
|
18453
|
+
return kits;
|
|
18454
|
+
}
|
|
18455
|
+
async function installKitFromFile() {
|
|
18456
|
+
const { canceled, filePaths } = await electron.dialog.showOpenDialog({
|
|
18457
|
+
title: "Install Automation Kit",
|
|
18458
|
+
filters: [{ name: "Automation Kit", extensions: ["kit.json", "json"] }],
|
|
18459
|
+
properties: ["openFile"]
|
|
18460
|
+
});
|
|
18461
|
+
if (canceled || filePaths.length === 0) {
|
|
18462
|
+
return { ok: false, error: "canceled" };
|
|
18463
|
+
}
|
|
18464
|
+
let raw;
|
|
18465
|
+
try {
|
|
18466
|
+
raw = fs$1.readFileSync(filePaths[0], "utf-8");
|
|
18467
|
+
} catch {
|
|
18468
|
+
return { ok: false, error: "Could not read the selected file." };
|
|
18469
|
+
}
|
|
18470
|
+
let parsed;
|
|
18471
|
+
try {
|
|
18472
|
+
parsed = JSON.parse(raw);
|
|
18473
|
+
} catch {
|
|
18474
|
+
return { ok: false, error: "File is not valid JSON." };
|
|
18475
|
+
}
|
|
18476
|
+
if (!isValidKit(parsed)) {
|
|
18477
|
+
return {
|
|
18478
|
+
ok: false,
|
|
18479
|
+
error: "File is not a valid automation kit. Required fields: id, name, description, icon, inputs, promptTemplate."
|
|
18480
|
+
};
|
|
18481
|
+
}
|
|
18482
|
+
if (BUNDLED_KIT_IDS.has(parsed.id)) {
|
|
18483
|
+
return {
|
|
18484
|
+
ok: false,
|
|
18485
|
+
error: `Kit id "${parsed.id}" conflicts with a built-in kit and cannot be overwritten.`
|
|
18486
|
+
};
|
|
18487
|
+
}
|
|
18488
|
+
ensureKitsDir();
|
|
18489
|
+
const dest = path$1.join(getUserKitsDir(), `${parsed.id}.kit.json`);
|
|
18490
|
+
try {
|
|
18491
|
+
fs$1.writeFileSync(dest, JSON.stringify(parsed, null, 2), "utf-8");
|
|
18492
|
+
} catch {
|
|
18493
|
+
return { ok: false, error: "Failed to save the kit file." };
|
|
18494
|
+
}
|
|
18495
|
+
return { ok: true, kit: parsed };
|
|
18496
|
+
}
|
|
18497
|
+
function uninstallKit(id) {
|
|
18498
|
+
if (BUNDLED_KIT_IDS.has(id)) {
|
|
18499
|
+
return { ok: false, error: "Built-in kits cannot be removed." };
|
|
18500
|
+
}
|
|
18501
|
+
ensureKitsDir();
|
|
18502
|
+
const target = path$1.join(getUserKitsDir(), `${id}.kit.json`);
|
|
18503
|
+
if (!fs$1.existsSync(target)) {
|
|
18504
|
+
return { ok: false, error: "Kit not found." };
|
|
18505
|
+
}
|
|
18506
|
+
try {
|
|
18507
|
+
fs$1.unlinkSync(target);
|
|
18508
|
+
return { ok: true };
|
|
18509
|
+
} catch {
|
|
18510
|
+
return { ok: false, error: "Failed to remove the kit file." };
|
|
18511
|
+
}
|
|
18512
|
+
}
|
|
18513
|
+
let jobs = [];
|
|
18514
|
+
let removeIdleListener = null;
|
|
18515
|
+
let broadcastFn = null;
|
|
18516
|
+
function getJobsPath() {
|
|
18517
|
+
return path$1.join(electron.app.getPath("userData"), "scheduled-jobs.json");
|
|
18518
|
+
}
|
|
18519
|
+
function loadJobs() {
|
|
18520
|
+
try {
|
|
18521
|
+
const raw = fs$1.readFileSync(getJobsPath(), "utf-8");
|
|
18522
|
+
const parsed = JSON.parse(raw);
|
|
18523
|
+
if (Array.isArray(parsed)) {
|
|
18524
|
+
jobs = parsed;
|
|
18525
|
+
}
|
|
18526
|
+
} catch {
|
|
18527
|
+
jobs = [];
|
|
18528
|
+
}
|
|
18529
|
+
}
|
|
18530
|
+
function saveJobs() {
|
|
18531
|
+
try {
|
|
18532
|
+
fs$1.writeFileSync(getJobsPath(), JSON.stringify(jobs, null, 2), "utf-8");
|
|
18533
|
+
} catch (err) {
|
|
18534
|
+
console.warn("[scheduler] Failed to save jobs:", err);
|
|
18535
|
+
}
|
|
18536
|
+
}
|
|
18537
|
+
function normalizeJob(job, now = /* @__PURE__ */ new Date()) {
|
|
18538
|
+
if (!job.enabled) return false;
|
|
18539
|
+
if (job.schedule.type === "once") {
|
|
18540
|
+
const runAt = job.schedule.runAt ? new Date(job.schedule.runAt) : null;
|
|
18541
|
+
if (!runAt || Number.isNaN(runAt.getTime()) || runAt <= now) {
|
|
18542
|
+
job.enabled = false;
|
|
18543
|
+
return true;
|
|
18544
|
+
}
|
|
18545
|
+
const nextRunAt2 = runAt.toISOString();
|
|
18546
|
+
if (job.nextRunAt !== nextRunAt2) {
|
|
18547
|
+
job.nextRunAt = nextRunAt2;
|
|
18548
|
+
return true;
|
|
18549
|
+
}
|
|
18550
|
+
return false;
|
|
18551
|
+
}
|
|
18552
|
+
const nextRunAt = computeNextRun(job.schedule, now).toISOString();
|
|
18553
|
+
if (job.nextRunAt !== nextRunAt) {
|
|
18554
|
+
job.nextRunAt = nextRunAt;
|
|
18555
|
+
return true;
|
|
18556
|
+
}
|
|
18557
|
+
return false;
|
|
18558
|
+
}
|
|
18559
|
+
function normalizeJobs(now = /* @__PURE__ */ new Date()) {
|
|
18560
|
+
let changed = false;
|
|
18561
|
+
for (const job of jobs) {
|
|
18562
|
+
changed = normalizeJob(job, now) || changed;
|
|
18563
|
+
}
|
|
18564
|
+
return changed;
|
|
18565
|
+
}
|
|
18566
|
+
function isIntegerInRange(value, min, max) {
|
|
18567
|
+
return Number.isInteger(value) && Number(value) >= min && Number(value) <= max;
|
|
18568
|
+
}
|
|
18569
|
+
function isStringRecord(value) {
|
|
18570
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) return false;
|
|
18571
|
+
return Object.values(value).every((entry) => typeof entry === "string");
|
|
18572
|
+
}
|
|
18573
|
+
function computeNextRun(schedule, from = /* @__PURE__ */ new Date()) {
|
|
18574
|
+
switch (schedule.type) {
|
|
18575
|
+
case "once":
|
|
18576
|
+
return new Date(schedule.runAt);
|
|
18577
|
+
case "hourly": {
|
|
18578
|
+
const next = new Date(from);
|
|
18579
|
+
next.setMinutes(0, 0, 0);
|
|
18580
|
+
next.setHours(next.getHours() + 1);
|
|
18581
|
+
return next;
|
|
18582
|
+
}
|
|
18583
|
+
case "daily": {
|
|
18584
|
+
const next = new Date(from);
|
|
18585
|
+
next.setHours(schedule.hour, schedule.minute, 0, 0);
|
|
18586
|
+
if (next <= from) next.setDate(next.getDate() + 1);
|
|
18587
|
+
return next;
|
|
18588
|
+
}
|
|
18589
|
+
case "weekly": {
|
|
18590
|
+
const next = new Date(from);
|
|
18591
|
+
next.setHours(schedule.hour, schedule.minute, 0, 0);
|
|
18592
|
+
const daysUntil = (schedule.dayOfWeek - next.getDay() + 7) % 7;
|
|
18593
|
+
if (daysUntil === 0 && next <= from) {
|
|
18594
|
+
next.setDate(next.getDate() + 7);
|
|
18595
|
+
} else {
|
|
18596
|
+
next.setDate(next.getDate() + (daysUntil || 7));
|
|
18597
|
+
}
|
|
18598
|
+
return next;
|
|
18599
|
+
}
|
|
18600
|
+
}
|
|
18601
|
+
}
|
|
18602
|
+
function isValidScheduleConfig(s) {
|
|
18603
|
+
if (!s || typeof s !== "object") return false;
|
|
18604
|
+
const sc = s;
|
|
18605
|
+
if (!["once", "hourly", "daily", "weekly"].includes(sc.type)) return false;
|
|
18606
|
+
if (sc.type === "once") {
|
|
18607
|
+
if (typeof sc.runAt !== "string") return false;
|
|
18608
|
+
if (Number.isNaN(new Date(sc.runAt).getTime())) return false;
|
|
18609
|
+
}
|
|
18610
|
+
if ((sc.type === "daily" || sc.type === "weekly") && (!isIntegerInRange(sc.hour, 0, 23) || !isIntegerInRange(sc.minute, 0, 59)))
|
|
18611
|
+
return false;
|
|
18612
|
+
if (sc.type === "weekly" && !isIntegerInRange(sc.dayOfWeek, 0, 6)) return false;
|
|
18613
|
+
return true;
|
|
18614
|
+
}
|
|
18615
|
+
function isValidJobData(v) {
|
|
18616
|
+
if (!v || typeof v !== "object") return false;
|
|
18617
|
+
const j = v;
|
|
18618
|
+
return typeof j.kitId === "string" && j.kitId.length > 0 && typeof j.kitName === "string" && j.kitName.length > 0 && typeof j.kitIcon === "string" && typeof j.renderedPrompt === "string" && j.renderedPrompt.length > 0 && (j.fieldValues === void 0 || isStringRecord(j.fieldValues)) && isValidScheduleConfig(j.schedule) && typeof j.enabled === "boolean";
|
|
18619
|
+
}
|
|
18620
|
+
async function fireJob(job, windowState, runtime2) {
|
|
18621
|
+
const { chromeView, sidebarView, devtoolsPanelView, tabManager } = windowState;
|
|
18622
|
+
const send = (channel, ...args) => {
|
|
18623
|
+
if (!chromeView.webContents.isDestroyed())
|
|
18624
|
+
chromeView.webContents.send(channel, ...args);
|
|
18625
|
+
if (!sidebarView.webContents.isDestroyed())
|
|
18626
|
+
sidebarView.webContents.send(channel, ...args);
|
|
18627
|
+
if (!devtoolsPanelView.webContents.isDestroyed())
|
|
18628
|
+
devtoolsPanelView.webContents.send(channel, ...args);
|
|
18629
|
+
};
|
|
18630
|
+
const settings2 = loadSettings();
|
|
18631
|
+
const activityId = `scheduled:${job.id}:${Date.now()}`;
|
|
18632
|
+
const startActivity = () => {
|
|
18633
|
+
const entry = {
|
|
18634
|
+
id: activityId,
|
|
18635
|
+
source: "scheduled",
|
|
18636
|
+
title: job.kitName,
|
|
18637
|
+
icon: job.kitIcon,
|
|
18638
|
+
status: "running",
|
|
18639
|
+
startedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
18640
|
+
output: ""
|
|
18641
|
+
};
|
|
18642
|
+
send(Channels.AUTOMATION_ACTIVITY_START, entry);
|
|
18643
|
+
};
|
|
18644
|
+
const appendActivity = (chunk) => {
|
|
18645
|
+
send(Channels.AUTOMATION_ACTIVITY_CHUNK, { id: activityId, chunk });
|
|
18646
|
+
};
|
|
18647
|
+
const finishActivity = (status) => {
|
|
18648
|
+
send(Channels.AUTOMATION_ACTIVITY_END, {
|
|
18649
|
+
id: activityId,
|
|
18650
|
+
status,
|
|
18651
|
+
finishedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
18652
|
+
});
|
|
18653
|
+
};
|
|
18654
|
+
startActivity();
|
|
18655
|
+
if (!settings2.chatProvider) {
|
|
18656
|
+
console.warn(`[scheduler] Job "${job.kitName}" skipped — no chat provider configured`);
|
|
18657
|
+
appendActivity(
|
|
18658
|
+
"Chat provider not configured. Open Settings (Ctrl+,) to choose a provider."
|
|
18659
|
+
);
|
|
18660
|
+
finishActivity("failed");
|
|
18661
|
+
return;
|
|
18662
|
+
}
|
|
18663
|
+
console.log(`[scheduler] Firing scheduled job: ${job.kitName} (${job.id})`);
|
|
18664
|
+
try {
|
|
18665
|
+
const provider = createProvider(settings2.chatProvider);
|
|
18666
|
+
const activeTab = tabManager.getActiveTab();
|
|
18667
|
+
await handleAIQuery(
|
|
18668
|
+
job.renderedPrompt,
|
|
18669
|
+
provider,
|
|
18670
|
+
activeTab?.view.webContents,
|
|
18671
|
+
(chunk) => appendActivity(chunk),
|
|
18672
|
+
() => finishActivity("completed"),
|
|
18673
|
+
tabManager,
|
|
18674
|
+
runtime2
|
|
18675
|
+
);
|
|
18676
|
+
} catch (err) {
|
|
18677
|
+
const msg = err instanceof Error ? err.message : "Unknown error";
|
|
18678
|
+
appendActivity(`
|
|
18679
|
+
[Scheduled Kit Error: ${msg}]`);
|
|
18680
|
+
finishActivity("failed");
|
|
18681
|
+
}
|
|
18682
|
+
}
|
|
18683
|
+
function tick(windowState, runtime2) {
|
|
18684
|
+
if (isAIStreamActive()) return;
|
|
18685
|
+
const now = /* @__PURE__ */ new Date();
|
|
18686
|
+
let changed = false;
|
|
18687
|
+
for (const job of jobs) {
|
|
18688
|
+
if (!job.enabled) continue;
|
|
18689
|
+
if (now < new Date(job.nextRunAt)) continue;
|
|
18690
|
+
if (!tryBeginAIStream("scheduled")) break;
|
|
18691
|
+
void fireJob(job, windowState, runtime2).finally(() => {
|
|
18692
|
+
endAIStream("scheduled");
|
|
18693
|
+
queueMicrotask(() => tick(windowState, runtime2));
|
|
18694
|
+
});
|
|
18695
|
+
job.lastRunAt = now.toISOString();
|
|
18696
|
+
if (job.schedule.type === "once") {
|
|
18697
|
+
job.enabled = false;
|
|
18698
|
+
} else {
|
|
18699
|
+
job.nextRunAt = computeNextRun(job.schedule, now).toISOString();
|
|
18700
|
+
}
|
|
18701
|
+
changed = true;
|
|
18702
|
+
break;
|
|
18703
|
+
}
|
|
18704
|
+
if (changed) {
|
|
18705
|
+
saveJobs();
|
|
18706
|
+
broadcastFn?.(Channels.SCHEDULE_JOBS_UPDATE, jobs);
|
|
18707
|
+
}
|
|
18708
|
+
}
|
|
18709
|
+
function registerScheduleHandlers(windowState, runtime2, sendToAll) {
|
|
18710
|
+
broadcastFn = sendToAll;
|
|
18711
|
+
loadJobs();
|
|
18712
|
+
if (normalizeJobs()) {
|
|
18713
|
+
saveJobs();
|
|
18714
|
+
}
|
|
18715
|
+
removeIdleListener?.();
|
|
18716
|
+
removeIdleListener = onAIStreamIdle(() => tick(windowState, runtime2));
|
|
18717
|
+
const now = /* @__PURE__ */ new Date();
|
|
18718
|
+
const msToNextMinute = (60 - now.getSeconds()) * 1e3 - now.getMilliseconds();
|
|
18719
|
+
setTimeout(() => {
|
|
18720
|
+
tick(windowState, runtime2);
|
|
18721
|
+
setInterval(() => tick(windowState, runtime2), 6e4);
|
|
18722
|
+
}, msToNextMinute);
|
|
18723
|
+
electron.ipcMain.handle(Channels.SCHEDULE_GET_ALL, () => jobs);
|
|
18724
|
+
electron.ipcMain.handle(Channels.SCHEDULE_CREATE, (_, rawJob) => {
|
|
18725
|
+
if (!isValidJobData(rawJob)) {
|
|
18726
|
+
throw new Error(
|
|
18727
|
+
"Invalid job data. Required: kitId, kitName, kitIcon, renderedPrompt, schedule, enabled."
|
|
18728
|
+
);
|
|
18729
|
+
}
|
|
18730
|
+
const newJob = {
|
|
18731
|
+
...rawJob,
|
|
18732
|
+
id: crypto.randomUUID(),
|
|
18733
|
+
createdAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
18734
|
+
nextRunAt: computeNextRun(rawJob.schedule).toISOString()
|
|
18735
|
+
};
|
|
18736
|
+
jobs.push(newJob);
|
|
18737
|
+
saveJobs();
|
|
18738
|
+
sendToAll(Channels.SCHEDULE_JOBS_UPDATE, jobs);
|
|
18739
|
+
return newJob;
|
|
18740
|
+
});
|
|
18741
|
+
electron.ipcMain.handle(Channels.SCHEDULE_UPDATE, (_, id, updates) => {
|
|
18742
|
+
if (typeof id !== "string") throw new Error("id must be a string");
|
|
18743
|
+
const job = jobs.find((j) => j.id === id);
|
|
18744
|
+
if (!job) return null;
|
|
18745
|
+
if (updates && typeof updates === "object") {
|
|
18746
|
+
const u = updates;
|
|
18747
|
+
const wasEnabled = job.enabled;
|
|
18748
|
+
if (u.enabled !== void 0) job.enabled = u.enabled;
|
|
18749
|
+
if (u.schedule !== void 0 && isValidScheduleConfig(u.schedule)) {
|
|
18750
|
+
job.schedule = u.schedule;
|
|
18751
|
+
job.nextRunAt = computeNextRun(u.schedule).toISOString();
|
|
18752
|
+
}
|
|
18753
|
+
if (typeof u.renderedPrompt === "string" && u.renderedPrompt.length > 0) {
|
|
18754
|
+
job.renderedPrompt = u.renderedPrompt;
|
|
18755
|
+
}
|
|
18756
|
+
if (u.fieldValues !== void 0 && isStringRecord(u.fieldValues)) {
|
|
18757
|
+
job.fieldValues = u.fieldValues;
|
|
18758
|
+
}
|
|
18759
|
+
if ((u.schedule !== void 0 || u.enabled === true && !wasEnabled) && job.enabled) {
|
|
18760
|
+
normalizeJob(job);
|
|
18761
|
+
}
|
|
18762
|
+
}
|
|
18763
|
+
saveJobs();
|
|
18764
|
+
sendToAll(Channels.SCHEDULE_JOBS_UPDATE, jobs);
|
|
18765
|
+
return job;
|
|
18766
|
+
});
|
|
18767
|
+
electron.ipcMain.handle(Channels.SCHEDULE_DELETE, (_, id) => {
|
|
18768
|
+
if (typeof id !== "string") throw new Error("id must be a string");
|
|
18769
|
+
const before = jobs.length;
|
|
18770
|
+
jobs = jobs.filter((j) => j.id !== id);
|
|
18771
|
+
if (jobs.length === before) return false;
|
|
18772
|
+
saveJobs();
|
|
18773
|
+
sendToAll(Channels.SCHEDULE_JOBS_UPDATE, jobs);
|
|
18774
|
+
return true;
|
|
18775
|
+
});
|
|
18776
|
+
}
|
|
18204
18777
|
let activeChatProvider = null;
|
|
18205
18778
|
function assertString(value, name) {
|
|
18206
18779
|
if (typeof value !== "string") throw new Error(`${name} must be a string`);
|
|
@@ -18219,9 +18792,30 @@ function registerIpcHandlers(windowState, runtime2) {
|
|
|
18219
18792
|
sidebarView.webContents.send(channel, ...args);
|
|
18220
18793
|
devtoolsPanelView.webContents.send(channel, ...args);
|
|
18221
18794
|
};
|
|
18795
|
+
const getActiveHighlightCountSafe = async () => {
|
|
18796
|
+
const tab = tabManager.getActiveTab();
|
|
18797
|
+
if (!tab) return 0;
|
|
18798
|
+
const wc = tab.view.webContents;
|
|
18799
|
+
if (wc.isDestroyed()) return 0;
|
|
18800
|
+
try {
|
|
18801
|
+
return await getHighlightCount(wc) ?? 0;
|
|
18802
|
+
} catch {
|
|
18803
|
+
return 0;
|
|
18804
|
+
}
|
|
18805
|
+
};
|
|
18806
|
+
const emitHighlightCount = async () => {
|
|
18807
|
+
const count = await getActiveHighlightCountSafe();
|
|
18808
|
+
sendToRendererViews(Channels.HIGHLIGHT_COUNT_UPDATE, count);
|
|
18809
|
+
};
|
|
18222
18810
|
runtime2.setUpdateListener((state2) => {
|
|
18223
18811
|
sendToRendererViews(Channels.AGENT_RUNTIME_UPDATE, state2);
|
|
18224
18812
|
});
|
|
18813
|
+
onRuntimeHealthChange((health) => {
|
|
18814
|
+
sendToRendererViews(Channels.SETTINGS_HEALTH_UPDATE, health);
|
|
18815
|
+
});
|
|
18816
|
+
onAIStreamIdle(() => {
|
|
18817
|
+
sendToRendererViews(Channels.AI_STREAM_IDLE);
|
|
18818
|
+
});
|
|
18225
18819
|
electron.ipcMain.handle(Channels.TAB_CREATE, (_, url) => {
|
|
18226
18820
|
const id = tabManager.createTab(url || loadSettings().defaultUrl);
|
|
18227
18821
|
layoutViews(windowState);
|
|
@@ -18235,11 +18829,14 @@ function registerIpcHandlers(windowState, runtime2) {
|
|
|
18235
18829
|
tabManager.switchTab(id);
|
|
18236
18830
|
layoutViews(windowState);
|
|
18237
18831
|
});
|
|
18238
|
-
electron.ipcMain.handle(
|
|
18239
|
-
|
|
18240
|
-
|
|
18241
|
-
|
|
18242
|
-
|
|
18832
|
+
electron.ipcMain.handle(
|
|
18833
|
+
Channels.TAB_NAVIGATE,
|
|
18834
|
+
(_, id, url, postBody) => {
|
|
18835
|
+
assertString(id, "tabId");
|
|
18836
|
+
assertString(url, "url");
|
|
18837
|
+
return tabManager.navigateTab(id, url, postBody);
|
|
18838
|
+
}
|
|
18839
|
+
);
|
|
18243
18840
|
electron.ipcMain.handle(Channels.TAB_BACK, (_, id) => {
|
|
18244
18841
|
tabManager.goBack(id);
|
|
18245
18842
|
});
|
|
@@ -18252,15 +18849,19 @@ function registerIpcHandlers(windowState, runtime2) {
|
|
|
18252
18849
|
electron.ipcMain.handle(Channels.AI_QUERY, async (_, query, history) => {
|
|
18253
18850
|
const settings2 = loadSettings();
|
|
18254
18851
|
const chatConfig = settings2.chatProvider;
|
|
18255
|
-
sendToRendererViews(Channels.AI_STREAM_START, query);
|
|
18256
18852
|
if (!chatConfig) {
|
|
18853
|
+
sendToRendererViews(Channels.AI_STREAM_START, query);
|
|
18257
18854
|
sendToRendererViews(
|
|
18258
18855
|
Channels.AI_STREAM_CHUNK,
|
|
18259
18856
|
"Chat provider not configured. Open Settings (Ctrl+,) to choose a provider."
|
|
18260
18857
|
);
|
|
18261
18858
|
sendToRendererViews(Channels.AI_STREAM_END);
|
|
18262
|
-
return;
|
|
18859
|
+
return { accepted: true };
|
|
18860
|
+
}
|
|
18861
|
+
if (!tryBeginAIStream("manual")) {
|
|
18862
|
+
return { accepted: false, reason: "busy" };
|
|
18263
18863
|
}
|
|
18864
|
+
sendToRendererViews(Channels.AI_STREAM_START, query);
|
|
18264
18865
|
try {
|
|
18265
18866
|
activeChatProvider = createProvider(chatConfig);
|
|
18266
18867
|
trackProviderConfigured(chatConfig.id);
|
|
@@ -18282,7 +18883,9 @@ function registerIpcHandlers(windowState, runtime2) {
|
|
|
18282
18883
|
sendToRendererViews(Channels.AI_STREAM_END);
|
|
18283
18884
|
} finally {
|
|
18284
18885
|
activeChatProvider = null;
|
|
18886
|
+
endAIStream("manual");
|
|
18285
18887
|
}
|
|
18888
|
+
return { accepted: true };
|
|
18286
18889
|
});
|
|
18287
18890
|
electron.ipcMain.handle(Channels.AI_CANCEL, () => {
|
|
18288
18891
|
activeChatProvider?.cancel();
|
|
@@ -18453,6 +19056,7 @@ function registerIpcHandlers(windowState, runtime2) {
|
|
|
18453
19056
|
if (result.success && result.text) {
|
|
18454
19057
|
await highlightOnPage(wc, null, result.text, void 0, void 0, "yellow").catch(() => {
|
|
18455
19058
|
});
|
|
19059
|
+
await emitHighlightCount();
|
|
18456
19060
|
}
|
|
18457
19061
|
return result;
|
|
18458
19062
|
} catch {
|
|
@@ -18460,6 +19064,9 @@ function registerIpcHandlers(windowState, runtime2) {
|
|
|
18460
19064
|
}
|
|
18461
19065
|
});
|
|
18462
19066
|
tabManager.onHighlightCapture((result) => {
|
|
19067
|
+
if (result.success) {
|
|
19068
|
+
void emitHighlightCount();
|
|
19069
|
+
}
|
|
18463
19070
|
if (!chromeView.webContents.isDestroyed()) {
|
|
18464
19071
|
chromeView.webContents.send(Channels.HIGHLIGHT_CAPTURE_RESULT, result);
|
|
18465
19072
|
}
|
|
@@ -18472,6 +19079,7 @@ function registerIpcHandlers(windowState, runtime2) {
|
|
|
18472
19079
|
if (!tab || !tab.highlightModeActive) return;
|
|
18473
19080
|
void persistAndMarkHighlight(wc, text).then((result) => {
|
|
18474
19081
|
if (result.success && !chromeView.webContents.isDestroyed()) {
|
|
19082
|
+
void emitHighlightCount();
|
|
18475
19083
|
chromeView.webContents.send(Channels.HIGHLIGHT_CAPTURE_RESULT, result);
|
|
18476
19084
|
}
|
|
18477
19085
|
});
|
|
@@ -18479,15 +19087,7 @@ function registerIpcHandlers(windowState, runtime2) {
|
|
|
18479
19087
|
}
|
|
18480
19088
|
});
|
|
18481
19089
|
electron.ipcMain.handle(Channels.HIGHLIGHT_NAV_COUNT, () => {
|
|
18482
|
-
|
|
18483
|
-
if (!tab) return 0;
|
|
18484
|
-
const wc = tab.view.webContents;
|
|
18485
|
-
if (wc.isDestroyed()) return 0;
|
|
18486
|
-
try {
|
|
18487
|
-
return getHighlightCount(wc);
|
|
18488
|
-
} catch {
|
|
18489
|
-
return 0;
|
|
18490
|
-
}
|
|
19090
|
+
return getActiveHighlightCountSafe();
|
|
18491
19091
|
});
|
|
18492
19092
|
electron.ipcMain.handle(Channels.HIGHLIGHT_NAV_SCROLL, (_, index) => {
|
|
18493
19093
|
const tab = tabManager.getActiveTab();
|
|
@@ -18500,24 +19100,32 @@ function registerIpcHandlers(windowState, runtime2) {
|
|
|
18500
19100
|
return false;
|
|
18501
19101
|
}
|
|
18502
19102
|
});
|
|
18503
|
-
electron.ipcMain.handle(Channels.HIGHLIGHT_NAV_REMOVE, (_, index) => {
|
|
19103
|
+
electron.ipcMain.handle(Channels.HIGHLIGHT_NAV_REMOVE, async (_, index) => {
|
|
18504
19104
|
const tab = tabManager.getActiveTab();
|
|
18505
19105
|
if (!tab) return false;
|
|
18506
19106
|
const wc = tab.view.webContents;
|
|
18507
19107
|
if (wc.isDestroyed()) return false;
|
|
18508
19108
|
try {
|
|
18509
|
-
|
|
19109
|
+
const removed = await removeHighlightAtIndex(wc, index);
|
|
19110
|
+
if (removed) {
|
|
19111
|
+
await emitHighlightCount();
|
|
19112
|
+
}
|
|
19113
|
+
return removed;
|
|
18510
19114
|
} catch {
|
|
18511
19115
|
return false;
|
|
18512
19116
|
}
|
|
18513
19117
|
});
|
|
18514
|
-
electron.ipcMain.handle(Channels.HIGHLIGHT_NAV_CLEAR, () => {
|
|
19118
|
+
electron.ipcMain.handle(Channels.HIGHLIGHT_NAV_CLEAR, async () => {
|
|
18515
19119
|
const tab = tabManager.getActiveTab();
|
|
18516
19120
|
if (!tab) return false;
|
|
18517
19121
|
const wc = tab.view.webContents;
|
|
18518
19122
|
if (wc.isDestroyed()) return false;
|
|
18519
19123
|
try {
|
|
18520
|
-
|
|
19124
|
+
const cleared = await clearAllHighlightElements(wc);
|
|
19125
|
+
if (cleared) {
|
|
19126
|
+
await emitHighlightCount();
|
|
19127
|
+
}
|
|
19128
|
+
return cleared;
|
|
18521
19129
|
} catch {
|
|
18522
19130
|
return false;
|
|
18523
19131
|
}
|
|
@@ -18671,6 +19279,17 @@ function registerIpcHandlers(windowState, runtime2) {
|
|
|
18671
19279
|
electron.ipcMain.handle(Channels.WINDOW_CLOSE, () => {
|
|
18672
19280
|
mainWindow.close();
|
|
18673
19281
|
});
|
|
19282
|
+
electron.ipcMain.handle(Channels.AUTOMATION_GET_INSTALLED, () => {
|
|
19283
|
+
return getInstalledKits();
|
|
19284
|
+
});
|
|
19285
|
+
electron.ipcMain.handle(Channels.AUTOMATION_INSTALL_FROM_FILE, async () => {
|
|
19286
|
+
return await installKitFromFile();
|
|
19287
|
+
});
|
|
19288
|
+
electron.ipcMain.handle(Channels.AUTOMATION_UNINSTALL, (_event, id) => {
|
|
19289
|
+
assertString(id, "id");
|
|
19290
|
+
return uninstallKit(id);
|
|
19291
|
+
});
|
|
19292
|
+
registerScheduleHandlers(windowState, runtime2, sendToRendererViews);
|
|
18674
19293
|
}
|
|
18675
19294
|
const MAX_TRANSCRIPT_TEXT_LENGTH = 8e3;
|
|
18676
19295
|
const PERSIST_DEBOUNCE_MS = 500;
|
|
@@ -18751,7 +19370,7 @@ class AgentRuntime {
|
|
|
18751
19370
|
createCheckpoint(name, note) {
|
|
18752
19371
|
const snapshot = this.captureSession(note);
|
|
18753
19372
|
const checkpoint = {
|
|
18754
|
-
id: crypto$
|
|
19373
|
+
id: crypto$2.randomUUID(),
|
|
18755
19374
|
name: name?.trim() || `Checkpoint ${this.state.checkpoints.length + 1}`,
|
|
18756
19375
|
createdAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
18757
19376
|
note: note?.trim() || void 0,
|
|
@@ -18805,7 +19424,7 @@ class AgentRuntime {
|
|
|
18805
19424
|
}
|
|
18806
19425
|
}
|
|
18807
19426
|
const entry = {
|
|
18808
|
-
id: crypto$
|
|
19427
|
+
id: crypto$2.randomUUID(),
|
|
18809
19428
|
source: input.source,
|
|
18810
19429
|
kind,
|
|
18811
19430
|
title: input.title?.trim() || void 0,
|
|
@@ -18829,7 +19448,7 @@ class AgentRuntime {
|
|
|
18829
19448
|
// --- Speedee Flow State ---
|
|
18830
19449
|
startFlow(goal, steps, startUrl) {
|
|
18831
19450
|
const flow = {
|
|
18832
|
-
id: crypto$
|
|
19451
|
+
id: crypto$2.randomUUID(),
|
|
18833
19452
|
goal,
|
|
18834
19453
|
steps: steps.map((label) => ({ label, status: "pending" })),
|
|
18835
19454
|
currentStepIndex: 0,
|
|
@@ -19058,7 +19677,7 @@ ${progress}
|
|
|
19058
19677
|
}
|
|
19059
19678
|
startAction(input) {
|
|
19060
19679
|
const action = {
|
|
19061
|
-
id: crypto$
|
|
19680
|
+
id: crypto$2.randomUUID(),
|
|
19062
19681
|
source: input.source,
|
|
19063
19682
|
name: input.name,
|
|
19064
19683
|
args: clone(input.args),
|
|
@@ -19130,7 +19749,7 @@ ${progress}
|
|
|
19130
19749
|
}
|
|
19131
19750
|
awaitApproval(action, reason) {
|
|
19132
19751
|
const approval = {
|
|
19133
|
-
id: crypto$
|
|
19752
|
+
id: crypto$2.randomUUID(),
|
|
19134
19753
|
actionId: action.id,
|
|
19135
19754
|
source: action.source,
|
|
19136
19755
|
name: action.name,
|
|
@@ -19256,12 +19875,25 @@ function installAdBlocking(tabManager) {
|
|
|
19256
19875
|
callback({ cancel: shouldBlockRequest(details) });
|
|
19257
19876
|
});
|
|
19258
19877
|
}
|
|
19878
|
+
function resolveDownloadPath(downloadDir, filename) {
|
|
19879
|
+
fs$1.mkdirSync(downloadDir, { recursive: true });
|
|
19880
|
+
const parsed = path.parse(filename);
|
|
19881
|
+
let attempt = 0;
|
|
19882
|
+
while (true) {
|
|
19883
|
+
const candidateName = attempt === 0 ? filename : `${parsed.name} (${attempt})${parsed.ext}`;
|
|
19884
|
+
const candidatePath = path.join(downloadDir, candidateName);
|
|
19885
|
+
if (!fs$1.existsSync(candidatePath)) {
|
|
19886
|
+
return candidatePath;
|
|
19887
|
+
}
|
|
19888
|
+
attempt += 1;
|
|
19889
|
+
}
|
|
19890
|
+
}
|
|
19259
19891
|
function installDownloadHandler(chromeView) {
|
|
19260
19892
|
electron.session.defaultSession.on("will-download", (_event, item) => {
|
|
19261
19893
|
const settings2 = loadSettings();
|
|
19262
19894
|
const downloadDir = settings2.downloadPath.trim() || electron.app.getPath("downloads");
|
|
19263
19895
|
const filename = item.getFilename();
|
|
19264
|
-
const savePath =
|
|
19896
|
+
const savePath = resolveDownloadPath(downloadDir, filename);
|
|
19265
19897
|
item.setSavePath(savePath);
|
|
19266
19898
|
const info = {
|
|
19267
19899
|
filename,
|
|
@@ -19290,13 +19922,245 @@ function installDownloadHandler(chromeView) {
|
|
|
19290
19922
|
});
|
|
19291
19923
|
});
|
|
19292
19924
|
}
|
|
19293
|
-
|
|
19925
|
+
function registerHighlightShortcut(mainWindow, tabManager) {
|
|
19926
|
+
const register = () => {
|
|
19927
|
+
electron.globalShortcut.unregister("CommandOrControl+H");
|
|
19928
|
+
const success = electron.globalShortcut.register("CommandOrControl+H", () => {
|
|
19929
|
+
const activeTab = tabManager.getActiveTab();
|
|
19930
|
+
if (!activeTab) return;
|
|
19931
|
+
tabManager.captureHighlightFromActiveTab();
|
|
19932
|
+
});
|
|
19933
|
+
if (!success) {
|
|
19934
|
+
console.warn("[Vessel] Failed to register Ctrl+H shortcut");
|
|
19935
|
+
}
|
|
19936
|
+
};
|
|
19937
|
+
register();
|
|
19938
|
+
mainWindow.on("focus", register);
|
|
19939
|
+
return () => {
|
|
19940
|
+
electron.globalShortcut.unregister("CommandOrControl+H");
|
|
19941
|
+
mainWindow.removeListener("focus", register);
|
|
19942
|
+
};
|
|
19943
|
+
}
|
|
19944
|
+
function setupAppMenu() {
|
|
19945
|
+
const appMenu = electron.Menu.buildFromTemplate([
|
|
19946
|
+
{
|
|
19947
|
+
label: "Edit",
|
|
19948
|
+
submenu: [
|
|
19949
|
+
{ role: "undo" },
|
|
19950
|
+
{ role: "redo" },
|
|
19951
|
+
{ type: "separator" },
|
|
19952
|
+
{ role: "cut" },
|
|
19953
|
+
{ role: "copy" },
|
|
19954
|
+
{ role: "paste" },
|
|
19955
|
+
{ role: "selectAll" }
|
|
19956
|
+
]
|
|
19957
|
+
}
|
|
19958
|
+
]);
|
|
19959
|
+
electron.Menu.setApplicationMenu(appMenu);
|
|
19960
|
+
}
|
|
19294
19961
|
function rendererUrlFor(view) {
|
|
19295
19962
|
if (!process.env.ELECTRON_RENDERER_URL) return null;
|
|
19296
19963
|
const url = new URL(process.env.ELECTRON_RENDERER_URL);
|
|
19297
19964
|
url.searchParams.set("view", view);
|
|
19298
19965
|
return url.toString();
|
|
19299
19966
|
}
|
|
19967
|
+
function loadRenderers(chromeView, sidebarView, devtoolsPanelView) {
|
|
19968
|
+
const chromeUrl = rendererUrlFor("chrome");
|
|
19969
|
+
const sidebarUrl = rendererUrlFor("sidebar");
|
|
19970
|
+
const devtoolsUrl = rendererUrlFor("devtools");
|
|
19971
|
+
if (chromeUrl && sidebarUrl && devtoolsUrl) {
|
|
19972
|
+
chromeView.webContents.loadURL(chromeUrl);
|
|
19973
|
+
sidebarView.webContents.loadURL(sidebarUrl);
|
|
19974
|
+
devtoolsPanelView.webContents.loadURL(devtoolsUrl);
|
|
19975
|
+
} else {
|
|
19976
|
+
const rendererFile = path$1.join(__dirname, "../../renderer/index.html");
|
|
19977
|
+
chromeView.webContents.loadFile(rendererFile, {
|
|
19978
|
+
query: { view: "chrome" }
|
|
19979
|
+
});
|
|
19980
|
+
sidebarView.webContents.loadFile(rendererFile, {
|
|
19981
|
+
query: { view: "sidebar" }
|
|
19982
|
+
});
|
|
19983
|
+
devtoolsPanelView.webContents.loadFile(rendererFile, {
|
|
19984
|
+
query: { view: "devtools" }
|
|
19985
|
+
});
|
|
19986
|
+
}
|
|
19987
|
+
}
|
|
19988
|
+
function findIconBase64() {
|
|
19989
|
+
const candidates = [
|
|
19990
|
+
path$1.join(electron.app.getAppPath(), "resources", "vessel-icon-400x400.png"),
|
|
19991
|
+
path$1.join(process.resourcesPath, "vessel-icon-400x400.png"),
|
|
19992
|
+
path$1.join(__dirname, "../../resources/vessel-icon-400x400.png"),
|
|
19993
|
+
path$1.join(electron.app.getAppPath(), "resources", "vessel-icon.png"),
|
|
19994
|
+
path$1.join(process.resourcesPath, "vessel-icon.png"),
|
|
19995
|
+
path$1.join(__dirname, "../../resources/vessel-icon.png")
|
|
19996
|
+
];
|
|
19997
|
+
for (const p of candidates) {
|
|
19998
|
+
try {
|
|
19999
|
+
const data = fs$1.readFileSync(p);
|
|
20000
|
+
return `data:image/png;base64,${data.toString("base64")}`;
|
|
20001
|
+
} catch {
|
|
20002
|
+
}
|
|
20003
|
+
}
|
|
20004
|
+
return "";
|
|
20005
|
+
}
|
|
20006
|
+
function buildSplashHTML(iconSrc) {
|
|
20007
|
+
const imgTag = iconSrc ? `<img class="logo" src="${iconSrc}" alt="" />` : `<div class="logo-fallback">V</div>`;
|
|
20008
|
+
return `<!DOCTYPE html>
|
|
20009
|
+
<html>
|
|
20010
|
+
<head>
|
|
20011
|
+
<meta charset="UTF-8">
|
|
20012
|
+
<style>
|
|
20013
|
+
* { margin: 0; padding: 0; box-sizing: border-box; }
|
|
20014
|
+
html, body {
|
|
20015
|
+
width: 100%; height: 100%;
|
|
20016
|
+
background: #1a1a1e;
|
|
20017
|
+
display: flex;
|
|
20018
|
+
flex-direction: column;
|
|
20019
|
+
align-items: center;
|
|
20020
|
+
justify-content: center;
|
|
20021
|
+
gap: 20px;
|
|
20022
|
+
font-family: system-ui, -apple-system, BlinkMacSystemFont, sans-serif;
|
|
20023
|
+
overflow: hidden;
|
|
20024
|
+
-webkit-app-region: drag;
|
|
20025
|
+
user-select: none;
|
|
20026
|
+
}
|
|
20027
|
+
.logo-wrap {
|
|
20028
|
+
position: relative;
|
|
20029
|
+
display: flex;
|
|
20030
|
+
align-items: center;
|
|
20031
|
+
justify-content: center;
|
|
20032
|
+
animation: pop-in 0.6s cubic-bezier(0.34, 1.56, 0.64, 1) both;
|
|
20033
|
+
}
|
|
20034
|
+
.glow {
|
|
20035
|
+
position: absolute;
|
|
20036
|
+
inset: -16px;
|
|
20037
|
+
border-radius: 36px;
|
|
20038
|
+
background: radial-gradient(ellipse at center,
|
|
20039
|
+
rgba(196, 160, 90, 0.22) 0%,
|
|
20040
|
+
transparent 68%
|
|
20041
|
+
);
|
|
20042
|
+
animation: glow-pulse 2.8s ease-in-out infinite;
|
|
20043
|
+
}
|
|
20044
|
+
.logo {
|
|
20045
|
+
width: 84px;
|
|
20046
|
+
height: 84px;
|
|
20047
|
+
border-radius: 20px;
|
|
20048
|
+
display: block;
|
|
20049
|
+
position: relative;
|
|
20050
|
+
}
|
|
20051
|
+
.logo-fallback {
|
|
20052
|
+
width: 84px;
|
|
20053
|
+
height: 84px;
|
|
20054
|
+
border-radius: 20px;
|
|
20055
|
+
background: linear-gradient(135deg, #2a2a30, #1e1e24);
|
|
20056
|
+
border: 1px solid rgba(196, 160, 90, 0.25);
|
|
20057
|
+
display: flex;
|
|
20058
|
+
align-items: center;
|
|
20059
|
+
justify-content: center;
|
|
20060
|
+
font-size: 36px;
|
|
20061
|
+
font-weight: 700;
|
|
20062
|
+
color: #c4a05a;
|
|
20063
|
+
position: relative;
|
|
20064
|
+
}
|
|
20065
|
+
.name {
|
|
20066
|
+
font-size: 13px;
|
|
20067
|
+
font-weight: 600;
|
|
20068
|
+
letter-spacing: 0.22em;
|
|
20069
|
+
color: #7a7a8a;
|
|
20070
|
+
text-transform: uppercase;
|
|
20071
|
+
animation: fade-up 0.5s 0.2s cubic-bezier(0.16, 1, 0.3, 1) both;
|
|
20072
|
+
}
|
|
20073
|
+
.dots {
|
|
20074
|
+
display: flex;
|
|
20075
|
+
gap: 6px;
|
|
20076
|
+
animation: fade-up 0.4s 0.35s cubic-bezier(0.16, 1, 0.3, 1) both;
|
|
20077
|
+
}
|
|
20078
|
+
.dot {
|
|
20079
|
+
width: 5px;
|
|
20080
|
+
height: 5px;
|
|
20081
|
+
border-radius: 50%;
|
|
20082
|
+
background: #3e3e50;
|
|
20083
|
+
animation: dot-bounce 1.5s ease-in-out infinite;
|
|
20084
|
+
}
|
|
20085
|
+
.dot:nth-child(2) { animation-delay: 0.2s; }
|
|
20086
|
+
.dot:nth-child(3) { animation-delay: 0.4s; }
|
|
20087
|
+
|
|
20088
|
+
@keyframes pop-in {
|
|
20089
|
+
from { opacity: 0; transform: scale(0.78); }
|
|
20090
|
+
to { opacity: 1; transform: scale(1); }
|
|
20091
|
+
}
|
|
20092
|
+
@keyframes fade-up {
|
|
20093
|
+
from { opacity: 0; transform: translateY(7px); }
|
|
20094
|
+
to { opacity: 1; transform: translateY(0); }
|
|
20095
|
+
}
|
|
20096
|
+
@keyframes glow-pulse {
|
|
20097
|
+
0%, 100% { opacity: 0.55; transform: scale(1); }
|
|
20098
|
+
50% { opacity: 1; transform: scale(1.1); }
|
|
20099
|
+
}
|
|
20100
|
+
@keyframes dot-bounce {
|
|
20101
|
+
0%, 75%, 100% { transform: translateY(0); opacity: 0.3; }
|
|
20102
|
+
40% { transform: translateY(-6px); opacity: 1; }
|
|
20103
|
+
}
|
|
20104
|
+
</style>
|
|
20105
|
+
</head>
|
|
20106
|
+
<body>
|
|
20107
|
+
<div class="logo-wrap">
|
|
20108
|
+
<div class="glow"></div>
|
|
20109
|
+
${imgTag}
|
|
20110
|
+
</div>
|
|
20111
|
+
<div class="name">Vessel</div>
|
|
20112
|
+
<div class="dots">
|
|
20113
|
+
<div class="dot"></div>
|
|
20114
|
+
<div class="dot"></div>
|
|
20115
|
+
<div class="dot"></div>
|
|
20116
|
+
</div>
|
|
20117
|
+
</body>
|
|
20118
|
+
</html>`;
|
|
20119
|
+
}
|
|
20120
|
+
function createSplashWindow() {
|
|
20121
|
+
const splash = new electron.BrowserWindow({
|
|
20122
|
+
width: 1280,
|
|
20123
|
+
height: 800,
|
|
20124
|
+
center: true,
|
|
20125
|
+
frame: false,
|
|
20126
|
+
show: false,
|
|
20127
|
+
// only show once content has painted — prevents black-window flash
|
|
20128
|
+
resizable: false,
|
|
20129
|
+
movable: true,
|
|
20130
|
+
alwaysOnTop: true,
|
|
20131
|
+
skipTaskbar: true,
|
|
20132
|
+
backgroundColor: "#1a1a1e",
|
|
20133
|
+
webPreferences: {
|
|
20134
|
+
nodeIntegration: false,
|
|
20135
|
+
contextIsolation: true
|
|
20136
|
+
}
|
|
20137
|
+
});
|
|
20138
|
+
splash.once("ready-to-show", () => splash.show());
|
|
20139
|
+
const iconSrc = findIconBase64();
|
|
20140
|
+
const html = buildSplashHTML(iconSrc);
|
|
20141
|
+
try {
|
|
20142
|
+
const tmpDir = fs$1.mkdtempSync(path$1.join(os.tmpdir(), "vessel-splash-"));
|
|
20143
|
+
const tmpPath = path$1.join(tmpDir, "index.html");
|
|
20144
|
+
splash.once("closed", () => {
|
|
20145
|
+
try {
|
|
20146
|
+
fs$1.rmSync(tmpDir, { recursive: true, force: true });
|
|
20147
|
+
} catch {
|
|
20148
|
+
}
|
|
20149
|
+
});
|
|
20150
|
+
fs$1.writeFileSync(tmpPath, html, "utf-8");
|
|
20151
|
+
void splash.loadFile(tmpPath);
|
|
20152
|
+
} catch (err) {
|
|
20153
|
+
console.warn("[splash] Failed to write temp HTML, using fallback:", err);
|
|
20154
|
+
void splash.loadFile(path$1.join(__dirname, "../../resources/vessel-icon.png"));
|
|
20155
|
+
}
|
|
20156
|
+
return splash;
|
|
20157
|
+
}
|
|
20158
|
+
function closeSplash(splash, delayMs = 0) {
|
|
20159
|
+
setTimeout(() => {
|
|
20160
|
+
if (!splash.isDestroyed()) splash.close();
|
|
20161
|
+
}, delayMs);
|
|
20162
|
+
}
|
|
20163
|
+
let runtime = null;
|
|
19300
20164
|
function checkWritableUserData(userDataPath) {
|
|
19301
20165
|
const issues = [];
|
|
19302
20166
|
try {
|
|
@@ -19360,6 +20224,7 @@ Action: Open Settings (Ctrl+,) to choose a different port, then save to restart
|
|
|
19360
20224
|
});
|
|
19361
20225
|
}
|
|
19362
20226
|
async function bootstrap() {
|
|
20227
|
+
const splash = createSplashWindow();
|
|
19363
20228
|
const settings2 = loadSettings();
|
|
19364
20229
|
const userDataPath = electron.app.getPath("userData");
|
|
19365
20230
|
initializeRuntimeHealth({
|
|
@@ -19371,15 +20236,51 @@ async function bootstrap() {
|
|
|
19371
20236
|
if (settings2.clearBookmarksOnLaunch) {
|
|
19372
20237
|
clearAll();
|
|
19373
20238
|
}
|
|
20239
|
+
const syncActiveHighlightCount = async (state2) => {
|
|
20240
|
+
const activeTab = state2.tabManager.getActiveTab();
|
|
20241
|
+
const wc = activeTab?.view.webContents;
|
|
20242
|
+
let count = 0;
|
|
20243
|
+
if (wc && !wc.isDestroyed()) {
|
|
20244
|
+
try {
|
|
20245
|
+
count = await getHighlightCount(wc) ?? 0;
|
|
20246
|
+
} catch {
|
|
20247
|
+
count = 0;
|
|
20248
|
+
}
|
|
20249
|
+
}
|
|
20250
|
+
if (!state2.chromeView.webContents.isDestroyed()) {
|
|
20251
|
+
state2.chromeView.webContents.send(Channels.HIGHLIGHT_COUNT_UPDATE, count);
|
|
20252
|
+
}
|
|
20253
|
+
if (!state2.sidebarView.webContents.isDestroyed()) {
|
|
20254
|
+
state2.sidebarView.webContents.send(Channels.HIGHLIGHT_COUNT_UPDATE, count);
|
|
20255
|
+
}
|
|
20256
|
+
if (!state2.devtoolsPanelView.webContents.isDestroyed()) {
|
|
20257
|
+
state2.devtoolsPanelView.webContents.send(
|
|
20258
|
+
Channels.HIGHLIGHT_COUNT_UPDATE,
|
|
20259
|
+
count
|
|
20260
|
+
);
|
|
20261
|
+
}
|
|
20262
|
+
};
|
|
19374
20263
|
const windowState = createMainWindow((tabs, activeId) => {
|
|
19375
20264
|
windowState.chromeView.webContents.send(
|
|
19376
20265
|
Channels.TAB_STATE_UPDATE,
|
|
19377
20266
|
tabs,
|
|
19378
20267
|
activeId
|
|
19379
20268
|
);
|
|
20269
|
+
void syncActiveHighlightCount(windowState);
|
|
19380
20270
|
layoutViews(windowState);
|
|
19381
20271
|
runtime?.onTabStateChanged();
|
|
19382
20272
|
});
|
|
20273
|
+
let didRevealMainWindow = false;
|
|
20274
|
+
const revealMainWindow = () => {
|
|
20275
|
+
if (didRevealMainWindow) return;
|
|
20276
|
+
didRevealMainWindow = true;
|
|
20277
|
+
windowState.mainWindow.show();
|
|
20278
|
+
closeSplash(splash, 0);
|
|
20279
|
+
};
|
|
20280
|
+
const splashTimeout = setTimeout(() => {
|
|
20281
|
+
console.warn("[bootstrap] Renderer did not finish loading before splash timeout");
|
|
20282
|
+
revealMainWindow();
|
|
20283
|
+
}, 8e3);
|
|
19383
20284
|
const { chromeView, sidebarView, devtoolsPanelView, tabManager } = windowState;
|
|
19384
20285
|
runtime = new AgentRuntime(tabManager);
|
|
19385
20286
|
installAdBlocking(tabManager);
|
|
@@ -19389,34 +20290,8 @@ async function bootstrap() {
|
|
|
19389
20290
|
}
|
|
19390
20291
|
});
|
|
19391
20292
|
registerIpcHandlers(windowState, runtime);
|
|
19392
|
-
|
|
19393
|
-
|
|
19394
|
-
const success = electron.globalShortcut.register("CommandOrControl+H", () => {
|
|
19395
|
-
const activeTab = tabManager.getActiveTab();
|
|
19396
|
-
if (!activeTab) return;
|
|
19397
|
-
tabManager.captureHighlightFromActiveTab();
|
|
19398
|
-
});
|
|
19399
|
-
if (!success) {
|
|
19400
|
-
console.warn("[Vessel] Failed to register Ctrl+H shortcut");
|
|
19401
|
-
}
|
|
19402
|
-
};
|
|
19403
|
-
registerHighlightShortcut();
|
|
19404
|
-
windowState.mainWindow.on("focus", registerHighlightShortcut);
|
|
19405
|
-
const appMenu = electron.Menu.buildFromTemplate([
|
|
19406
|
-
{
|
|
19407
|
-
label: "Edit",
|
|
19408
|
-
submenu: [
|
|
19409
|
-
{ role: "undo" },
|
|
19410
|
-
{ role: "redo" },
|
|
19411
|
-
{ type: "separator" },
|
|
19412
|
-
{ role: "cut" },
|
|
19413
|
-
{ role: "copy" },
|
|
19414
|
-
{ role: "paste" },
|
|
19415
|
-
{ role: "selectAll" }
|
|
19416
|
-
]
|
|
19417
|
-
}
|
|
19418
|
-
]);
|
|
19419
|
-
electron.Menu.setApplicationMenu(appMenu);
|
|
20293
|
+
registerHighlightShortcut(windowState.mainWindow, tabManager);
|
|
20294
|
+
setupAppMenu();
|
|
19420
20295
|
subscribe((state2) => {
|
|
19421
20296
|
chromeView.webContents.send(Channels.BOOKMARKS_UPDATE, state2);
|
|
19422
20297
|
sidebarView.webContents.send(Channels.BOOKMARKS_UPDATE, state2);
|
|
@@ -19428,26 +20303,7 @@ async function bootstrap() {
|
|
|
19428
20303
|
installDownloadHandler(chromeView);
|
|
19429
20304
|
startBackgroundRevalidation();
|
|
19430
20305
|
startTelemetry();
|
|
19431
|
-
|
|
19432
|
-
const sidebarUrl = rendererUrlFor("sidebar");
|
|
19433
|
-
const devtoolsUrl = rendererUrlFor("devtools");
|
|
19434
|
-
if (chromeUrl && sidebarUrl && devtoolsUrl) {
|
|
19435
|
-
chromeView.webContents.loadURL(chromeUrl);
|
|
19436
|
-
sidebarView.webContents.loadURL(sidebarUrl);
|
|
19437
|
-
devtoolsPanelView.webContents.loadURL(devtoolsUrl);
|
|
19438
|
-
} else {
|
|
19439
|
-
const rendererFile = path.join(__dirname, "../renderer/index.html");
|
|
19440
|
-
chromeView.webContents.loadFile(rendererFile, {
|
|
19441
|
-
query: { view: "chrome" }
|
|
19442
|
-
});
|
|
19443
|
-
sidebarView.webContents.loadFile(rendererFile, {
|
|
19444
|
-
query: { view: "sidebar" }
|
|
19445
|
-
});
|
|
19446
|
-
devtoolsPanelView.webContents.loadFile(rendererFile, {
|
|
19447
|
-
query: { view: "devtools" }
|
|
19448
|
-
});
|
|
19449
|
-
}
|
|
19450
|
-
await startMcpServer(tabManager, runtime, settings2.mcpPort);
|
|
20306
|
+
loadRenderers(chromeView, sidebarView, devtoolsPanelView);
|
|
19451
20307
|
chromeView.webContents.once("did-finish-load", () => {
|
|
19452
20308
|
const savedSession = runtime.getState().session;
|
|
19453
20309
|
if (settings2.autoRestoreSession && savedSession?.tabs.length) {
|
|
@@ -19458,9 +20314,38 @@ async function bootstrap() {
|
|
|
19458
20314
|
}
|
|
19459
20315
|
layoutViews(windowState);
|
|
19460
20316
|
setImmediate(() => layoutViews(windowState));
|
|
20317
|
+
clearTimeout(splashTimeout);
|
|
20318
|
+
revealMainWindow();
|
|
19461
20319
|
void maybeShowStartupHealthDialog(windowState);
|
|
19462
20320
|
});
|
|
20321
|
+
chromeView.webContents.once(
|
|
20322
|
+
"did-fail-load",
|
|
20323
|
+
(_event, errorCode, errorDescription, validatedURL, isMainFrame) => {
|
|
20324
|
+
if (!isMainFrame) return;
|
|
20325
|
+
console.error(
|
|
20326
|
+
"[bootstrap] Chrome renderer failed to load:",
|
|
20327
|
+
errorCode,
|
|
20328
|
+
errorDescription,
|
|
20329
|
+
validatedURL
|
|
20330
|
+
);
|
|
20331
|
+
clearTimeout(splashTimeout);
|
|
20332
|
+
revealMainWindow();
|
|
20333
|
+
}
|
|
20334
|
+
);
|
|
20335
|
+
startMcpServer(tabManager, runtime, settings2.mcpPort).catch((err) => {
|
|
20336
|
+
console.error("[bootstrap] MCP server failed to start:", err);
|
|
20337
|
+
});
|
|
19463
20338
|
}
|
|
20339
|
+
process.on("uncaughtException", (error) => {
|
|
20340
|
+
console.error("[Vessel] Uncaught exception:", error.message, error.stack);
|
|
20341
|
+
electron.app.quit();
|
|
20342
|
+
});
|
|
20343
|
+
process.on("unhandledRejection", (reason) => {
|
|
20344
|
+
console.error(
|
|
20345
|
+
"[Vessel] Unhandled rejection:",
|
|
20346
|
+
reason instanceof Error ? reason.message : reason
|
|
20347
|
+
);
|
|
20348
|
+
});
|
|
19464
20349
|
electron.app.whenReady().then(bootstrap).catch((error) => {
|
|
19465
20350
|
console.error("[Vessel] Failed to bootstrap application:", error);
|
|
19466
20351
|
electron.app.quit();
|
|
@@ -19469,8 +20354,15 @@ electron.app.on("window-all-closed", () => {
|
|
|
19469
20354
|
electron.globalShortcut.unregisterAll();
|
|
19470
20355
|
stopTelemetry();
|
|
19471
20356
|
stopBackgroundRevalidation();
|
|
19472
|
-
|
|
19473
|
-
|
|
19474
|
-
|
|
20357
|
+
void Promise.all([
|
|
20358
|
+
runtime?.flushPersist() ?? Promise.resolve(),
|
|
20359
|
+
flushPersist(),
|
|
20360
|
+
flushPersist$1(),
|
|
20361
|
+
flushPersist$2(),
|
|
20362
|
+
flushPersist$3()
|
|
20363
|
+
]).finally(() => {
|
|
20364
|
+
void stopMcpServer().finally(() => {
|
|
20365
|
+
electron.app.quit();
|
|
20366
|
+
});
|
|
19475
20367
|
});
|
|
19476
20368
|
});
|