@quanta-intellect/vessel-browser 0.1.15 → 0.1.17
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/out/main/index.js
CHANGED
|
@@ -80,12 +80,9 @@ function loadSettings() {
|
|
|
80
80
|
return settings;
|
|
81
81
|
}
|
|
82
82
|
function saveSettings() {
|
|
83
|
-
|
|
84
|
-
fs.
|
|
85
|
-
|
|
86
|
-
} catch (err) {
|
|
87
|
-
console.error("[Vessel] Failed to save settings:", err);
|
|
88
|
-
}
|
|
83
|
+
fs.promises.mkdir(path.dirname(getSettingsPath()), { recursive: true }).then(
|
|
84
|
+
() => fs.promises.writeFile(getSettingsPath(), JSON.stringify(settings, null, 2))
|
|
85
|
+
).catch((err) => console.error("[Vessel] Failed to save settings:", err));
|
|
89
86
|
}
|
|
90
87
|
function setSetting(key, value) {
|
|
91
88
|
loadSettings();
|
|
@@ -528,11 +525,7 @@ function load$2() {
|
|
|
528
525
|
return state$3;
|
|
529
526
|
}
|
|
530
527
|
function save$2() {
|
|
531
|
-
|
|
532
|
-
fs.writeFileSync(getHighlightsPath(), JSON.stringify(state$3, null, 2), "utf-8");
|
|
533
|
-
} catch (err) {
|
|
534
|
-
console.error("[Vessel] Failed to save highlights:", err);
|
|
535
|
-
}
|
|
528
|
+
fs.promises.writeFile(getHighlightsPath(), JSON.stringify(state$3, null, 2), "utf-8").catch((err) => console.error("[Vessel] Failed to save highlights:", err));
|
|
536
529
|
}
|
|
537
530
|
function emit$2() {
|
|
538
531
|
if (!state$3) return;
|
|
@@ -1202,16 +1195,13 @@ function load$1() {
|
|
|
1202
1195
|
return state$2;
|
|
1203
1196
|
}
|
|
1204
1197
|
function save$1() {
|
|
1205
|
-
|
|
1206
|
-
fs.
|
|
1207
|
-
fs.writeFileSync(
|
|
1198
|
+
fs.promises.mkdir(path.dirname(getHistoryPath()), { recursive: true }).then(
|
|
1199
|
+
() => fs.promises.writeFile(
|
|
1208
1200
|
getHistoryPath(),
|
|
1209
1201
|
JSON.stringify(state$2, null, 2),
|
|
1210
1202
|
"utf-8"
|
|
1211
|
-
)
|
|
1212
|
-
|
|
1213
|
-
console.error("[Vessel] Failed to save history:", err);
|
|
1214
|
-
}
|
|
1203
|
+
)
|
|
1204
|
+
).catch((err) => console.error("[Vessel] Failed to save history:", err));
|
|
1215
1205
|
}
|
|
1216
1206
|
function emit$1() {
|
|
1217
1207
|
if (!state$2) return;
|
|
@@ -4406,6 +4396,19 @@ function setMcpHealth(update) {
|
|
|
4406
4396
|
}
|
|
4407
4397
|
}
|
|
4408
4398
|
}
|
|
4399
|
+
function isRichToolResult(value) {
|
|
4400
|
+
return typeof value === "object" && value !== null && value.__richResult === true;
|
|
4401
|
+
}
|
|
4402
|
+
function makeImageResult(base64, description, mediaType = "image/png") {
|
|
4403
|
+
const result = {
|
|
4404
|
+
__richResult: true,
|
|
4405
|
+
content: [
|
|
4406
|
+
{ type: "text", text: description },
|
|
4407
|
+
{ type: "image", mediaType, base64 }
|
|
4408
|
+
]
|
|
4409
|
+
};
|
|
4410
|
+
return JSON.stringify(result);
|
|
4411
|
+
}
|
|
4409
4412
|
const DEFAULT_MAX_ITERATIONS$1 = 200;
|
|
4410
4413
|
class AnthropicProvider {
|
|
4411
4414
|
client;
|
|
@@ -4437,7 +4440,7 @@ class AnthropicProvider {
|
|
|
4437
4440
|
}
|
|
4438
4441
|
}
|
|
4439
4442
|
} catch (err) {
|
|
4440
|
-
if (err.name !== "AbortError") {
|
|
4443
|
+
if (err instanceof Error && err.name !== "AbortError") {
|
|
4441
4444
|
onChunk(`
|
|
4442
4445
|
|
|
4443
4446
|
[Error: ${err.message}]`);
|
|
@@ -4460,7 +4463,6 @@ class AnthropicProvider {
|
|
|
4460
4463
|
iterationsUsed = i + 1;
|
|
4461
4464
|
const msgTokenEstimate = JSON.stringify(messages).length;
|
|
4462
4465
|
const sysTokenEstimate = systemPrompt.length;
|
|
4463
|
-
console.log(`[Vessel Agent] iteration=${i} messages=${messages.length} msgChars=${msgTokenEstimate} sysChars=${sysTokenEstimate} tools=${tools.length}`);
|
|
4464
4466
|
const streamStartTime = Date.now();
|
|
4465
4467
|
const stream = this.client.messages.stream(
|
|
4466
4468
|
{
|
|
@@ -4522,9 +4524,7 @@ class AnthropicProvider {
|
|
|
4522
4524
|
} finally {
|
|
4523
4525
|
if (idleTimer) clearTimeout(idleTimer);
|
|
4524
4526
|
}
|
|
4525
|
-
console.log(`[Vessel Agent] stream complete in ${Date.now() - streamStartTime}ms, toolCalls=${toolUseBlocks.length} textLen=${textContent.length}`);
|
|
4526
4527
|
const finalMessage = await stream.finalMessage();
|
|
4527
|
-
console.log(`[Vessel Agent] finalMessage received, stop_reason=${finalMessage.stop_reason}`);
|
|
4528
4528
|
const assistantContent = [];
|
|
4529
4529
|
if (textContent) {
|
|
4530
4530
|
assistantContent.push({ type: "text", text: textContent });
|
|
@@ -4548,19 +4548,43 @@ class AnthropicProvider {
|
|
|
4548
4548
|
<<tool:${tb.name}${argSummary ? ":" + argSummary : ""}>>
|
|
4549
4549
|
`);
|
|
4550
4550
|
let result;
|
|
4551
|
-
const toolStartTime = Date.now();
|
|
4552
|
-
console.log(`[Vessel Agent] executing tool: ${tb.name}`);
|
|
4553
4551
|
try {
|
|
4554
4552
|
result = await onToolCall(tb.name, tb.input);
|
|
4555
4553
|
} catch (toolErr) {
|
|
4556
|
-
|
|
4554
|
+
const msg = toolErr instanceof Error ? toolErr.message : String(toolErr);
|
|
4555
|
+
result = `Error: Tool execution failed — ${msg}. Try a different approach or call read_page to refresh context.`;
|
|
4556
|
+
}
|
|
4557
|
+
let parsedRich = null;
|
|
4558
|
+
try {
|
|
4559
|
+
const parsed = JSON.parse(result);
|
|
4560
|
+
if (isRichToolResult(parsed)) parsedRich = parsed;
|
|
4561
|
+
} catch {
|
|
4562
|
+
}
|
|
4563
|
+
if (parsedRich) {
|
|
4564
|
+
toolResults.push({
|
|
4565
|
+
type: "tool_result",
|
|
4566
|
+
tool_use_id: tb.id,
|
|
4567
|
+
content: parsedRich.content.map((block) => {
|
|
4568
|
+
if (block.type === "image") {
|
|
4569
|
+
return {
|
|
4570
|
+
type: "image",
|
|
4571
|
+
source: {
|
|
4572
|
+
type: "base64",
|
|
4573
|
+
media_type: block.mediaType,
|
|
4574
|
+
data: block.base64
|
|
4575
|
+
}
|
|
4576
|
+
};
|
|
4577
|
+
}
|
|
4578
|
+
return { type: "text", text: block.text };
|
|
4579
|
+
})
|
|
4580
|
+
});
|
|
4581
|
+
} else {
|
|
4582
|
+
toolResults.push({
|
|
4583
|
+
type: "tool_result",
|
|
4584
|
+
tool_use_id: tb.id,
|
|
4585
|
+
content: result
|
|
4586
|
+
});
|
|
4557
4587
|
}
|
|
4558
|
-
console.log(`[Vessel Agent] tool ${tb.name} completed in ${Date.now() - toolStartTime}ms, resultLen=${result.length}`);
|
|
4559
|
-
toolResults.push({
|
|
4560
|
-
type: "tool_result",
|
|
4561
|
-
tool_use_id: tb.id,
|
|
4562
|
-
content: result
|
|
4563
|
-
});
|
|
4564
4588
|
}
|
|
4565
4589
|
messages.push({ role: "user", content: toolResults });
|
|
4566
4590
|
}
|
|
@@ -4570,7 +4594,7 @@ class AnthropicProvider {
|
|
|
4570
4594
|
[Reached maximum tool call limit (${maxIterations} steps). You can adjust this in Settings → Max Tool Iterations, or continue by sending another message.]`);
|
|
4571
4595
|
}
|
|
4572
4596
|
} catch (err) {
|
|
4573
|
-
if (err.name !== "AbortError") {
|
|
4597
|
+
if (err instanceof Error && err.name !== "AbortError") {
|
|
4574
4598
|
onChunk(`
|
|
4575
4599
|
|
|
4576
4600
|
[Error: ${err.message}]`);
|
|
@@ -4721,7 +4745,7 @@ class OpenAICompatProvider {
|
|
|
4721
4745
|
}
|
|
4722
4746
|
}
|
|
4723
4747
|
} catch (err) {
|
|
4724
|
-
if (err.name !== "AbortError") {
|
|
4748
|
+
if (err instanceof Error && err.name !== "AbortError") {
|
|
4725
4749
|
onChunk(`
|
|
4726
4750
|
|
|
4727
4751
|
[Error: ${err.message}]`);
|
|
@@ -4745,7 +4769,6 @@ class OpenAICompatProvider {
|
|
|
4745
4769
|
for (let i = 0; i < maxIterations; i++) {
|
|
4746
4770
|
iterationsUsed = i + 1;
|
|
4747
4771
|
const msgTokenEstimate = JSON.stringify(messages).length;
|
|
4748
|
-
console.log(`[Vessel Agent OpenAI] iteration=${i} messages=${messages.length} msgChars=${msgTokenEstimate} tools=${openAITools.length}`);
|
|
4749
4772
|
const streamStartTime = Date.now();
|
|
4750
4773
|
let textAccum = "";
|
|
4751
4774
|
const toolCallAccums = {};
|
|
@@ -4781,7 +4804,6 @@ class OpenAICompatProvider {
|
|
|
4781
4804
|
}
|
|
4782
4805
|
}
|
|
4783
4806
|
}
|
|
4784
|
-
console.log(`[Vessel Agent OpenAI] stream complete in ${Date.now() - streamStartTime}ms, toolCalls=${Object.keys(toolCallAccums).length} textLen=${textAccum.length} finishReason=${finishReason}`);
|
|
4785
4807
|
const toolCalls = Object.values(toolCallAccums);
|
|
4786
4808
|
for (const tc of Object.values(toolCallAccums)) {
|
|
4787
4809
|
if (!tc.id) tc.id = `call_${Date.now()}_${Math.random().toString(36).slice(2, 8)}`;
|
|
@@ -4826,18 +4848,24 @@ class OpenAICompatProvider {
|
|
|
4826
4848
|
<<tool:${tc.name}${argSummary ? ":" + argSummary : ""}>>
|
|
4827
4849
|
`);
|
|
4828
4850
|
let result;
|
|
4829
|
-
const toolStartTime = Date.now();
|
|
4830
|
-
console.log(`[Vessel Agent OpenAI] executing tool: ${tc.name}`);
|
|
4831
4851
|
try {
|
|
4832
4852
|
result = await onToolCall(tc.name, args);
|
|
4833
4853
|
} catch (toolErr) {
|
|
4834
|
-
|
|
4854
|
+
const msg = toolErr instanceof Error ? toolErr.message : String(toolErr);
|
|
4855
|
+
result = `Error: Tool execution failed — ${msg}. Try a different approach or call read_page to refresh context.`;
|
|
4856
|
+
}
|
|
4857
|
+
let toolContent = result;
|
|
4858
|
+
try {
|
|
4859
|
+
const parsed = JSON.parse(result);
|
|
4860
|
+
if (isRichToolResult(parsed)) {
|
|
4861
|
+
toolContent = parsed.content.filter((b) => b.type === "text").map((b) => b.text).join("\n");
|
|
4862
|
+
}
|
|
4863
|
+
} catch {
|
|
4835
4864
|
}
|
|
4836
|
-
console.log(`[Vessel Agent OpenAI] tool ${tc.name} completed in ${Date.now() - toolStartTime}ms, resultLen=${result.length}`);
|
|
4837
4865
|
messages.push({
|
|
4838
4866
|
role: "tool",
|
|
4839
4867
|
tool_call_id: tc.id,
|
|
4840
|
-
content:
|
|
4868
|
+
content: toolContent
|
|
4841
4869
|
});
|
|
4842
4870
|
}
|
|
4843
4871
|
}
|
|
@@ -4847,7 +4875,7 @@ class OpenAICompatProvider {
|
|
|
4847
4875
|
[Reached maximum tool call limit (${maxIterations} steps). You can adjust this in Settings → Max Tool Iterations, or continue by sending another message.]`);
|
|
4848
4876
|
}
|
|
4849
4877
|
} catch (err) {
|
|
4850
|
-
if (err.name !== "AbortError") {
|
|
4878
|
+
if (err instanceof Error && err.name !== "AbortError") {
|
|
4851
4879
|
onChunk(`
|
|
4852
4880
|
|
|
4853
4881
|
[Error: ${err.message}]`);
|
|
@@ -6715,6 +6743,13 @@ const TOOL_DEFINITIONS = [
|
|
|
6715
6743
|
},
|
|
6716
6744
|
tier: 0
|
|
6717
6745
|
},
|
|
6746
|
+
{
|
|
6747
|
+
name: "screenshot",
|
|
6748
|
+
title: "Screenshot",
|
|
6749
|
+
description: "Take a screenshot of the current page — see exactly what the user sees. Returns the image for visual analysis. Use when you need to verify visual layout, check what's actually rendered on screen, or when text extraction fails on heavy pages.",
|
|
6750
|
+
inputSchema: {},
|
|
6751
|
+
tier: 1
|
|
6752
|
+
},
|
|
6718
6753
|
{
|
|
6719
6754
|
name: "wait_for",
|
|
6720
6755
|
title: "Wait For",
|
|
@@ -7150,6 +7185,7 @@ const ALWAYS_FAST_TOOL_NAMES = /* @__PURE__ */ new Set([
|
|
|
7150
7185
|
"accept_cookies",
|
|
7151
7186
|
"wait_for",
|
|
7152
7187
|
"read_page",
|
|
7188
|
+
"screenshot",
|
|
7153
7189
|
"inspect_element"
|
|
7154
7190
|
]);
|
|
7155
7191
|
function inferIntent(query) {
|
|
@@ -7414,12 +7450,13 @@ function load() {
|
|
|
7414
7450
|
return state;
|
|
7415
7451
|
}
|
|
7416
7452
|
function save() {
|
|
7417
|
-
|
|
7418
|
-
fs.
|
|
7419
|
-
|
|
7420
|
-
|
|
7421
|
-
|
|
7422
|
-
|
|
7453
|
+
fs.promises.mkdir(path.dirname(getBookmarksPath()), { recursive: true }).then(
|
|
7454
|
+
() => fs.promises.writeFile(
|
|
7455
|
+
getBookmarksPath(),
|
|
7456
|
+
JSON.stringify(state, null, 2),
|
|
7457
|
+
"utf-8"
|
|
7458
|
+
)
|
|
7459
|
+
).catch((err) => console.error("[Vessel] Failed to save bookmarks:", err));
|
|
7423
7460
|
}
|
|
7424
7461
|
function emit() {
|
|
7425
7462
|
if (!state) return;
|
|
@@ -7861,6 +7898,23 @@ function assertSafeURL(url) {
|
|
|
7861
7898
|
);
|
|
7862
7899
|
}
|
|
7863
7900
|
}
|
|
7901
|
+
async function captureScreenshot(wc) {
|
|
7902
|
+
for (let attempt = 0; attempt < 3; attempt += 1) {
|
|
7903
|
+
await new Promise((resolve) => setTimeout(resolve, 120 * (attempt + 1)));
|
|
7904
|
+
try {
|
|
7905
|
+
const image = await wc.capturePage();
|
|
7906
|
+
if (!image.isEmpty()) {
|
|
7907
|
+
const size = image.getSize();
|
|
7908
|
+
const base64 = image.toPNG().toString("base64");
|
|
7909
|
+
if (base64) {
|
|
7910
|
+
return { ok: true, base64, width: size.width, height: size.height };
|
|
7911
|
+
}
|
|
7912
|
+
}
|
|
7913
|
+
} catch {
|
|
7914
|
+
}
|
|
7915
|
+
}
|
|
7916
|
+
return { ok: false, error: "Page image was empty after 3 attempts" };
|
|
7917
|
+
}
|
|
7864
7918
|
const SESSION_VERSION = 1;
|
|
7865
7919
|
function getSessionsDir() {
|
|
7866
7920
|
return path$1.join(electron.app.getPath("userData"), "named-sessions");
|
|
@@ -8308,9 +8362,6 @@ async function executePageScript(wc, script, options) {
|
|
|
8308
8362
|
})
|
|
8309
8363
|
]);
|
|
8310
8364
|
if (result === PAGE_SCRIPT_TIMEOUT) {
|
|
8311
|
-
console.log(
|
|
8312
|
-
`[Vessel pageScript] timed out after ${timeoutMs}ms (${options?.label || "page-script"})`
|
|
8313
|
-
);
|
|
8314
8365
|
return PAGE_SCRIPT_TIMEOUT;
|
|
8315
8366
|
}
|
|
8316
8367
|
return result;
|
|
@@ -8325,9 +8376,6 @@ async function executePageScript(wc, script, options) {
|
|
|
8325
8376
|
function waitForLoad$1(wc, timeout = 5e3) {
|
|
8326
8377
|
return new Promise((resolve) => {
|
|
8327
8378
|
let finished = false;
|
|
8328
|
-
console.log(
|
|
8329
|
-
`[Vessel waitForLoad] started, isLoading=${wc.isLoading()}, timeout=${timeout}`
|
|
8330
|
-
);
|
|
8331
8379
|
const cleanup = () => {
|
|
8332
8380
|
wc.removeListener("did-finish-load", onLoadEvent);
|
|
8333
8381
|
wc.removeListener("did-stop-loading", onLoadEvent);
|
|
@@ -8336,23 +8384,19 @@ function waitForLoad$1(wc, timeout = 5e3) {
|
|
|
8336
8384
|
const finish = (reason) => {
|
|
8337
8385
|
if (finished) return;
|
|
8338
8386
|
finished = true;
|
|
8339
|
-
console.log(`[Vessel waitForLoad] finished: ${reason}`);
|
|
8340
8387
|
clearTimeout(timer);
|
|
8341
8388
|
cleanup();
|
|
8342
8389
|
resolve();
|
|
8343
8390
|
};
|
|
8344
8391
|
const onLoadEvent = () => {
|
|
8345
8392
|
const loading = wc.isLoading();
|
|
8346
|
-
console.log(
|
|
8347
|
-
`[Vessel waitForLoad] load event fired, isLoading=${loading}`
|
|
8348
|
-
);
|
|
8349
8393
|
if (!loading) {
|
|
8350
|
-
finish(
|
|
8394
|
+
finish();
|
|
8351
8395
|
}
|
|
8352
8396
|
};
|
|
8353
|
-
const timer = setTimeout(() => finish(
|
|
8397
|
+
const timer = setTimeout(() => finish(), timeout);
|
|
8354
8398
|
if (!wc.isLoading()) {
|
|
8355
|
-
finish(
|
|
8399
|
+
finish();
|
|
8356
8400
|
return;
|
|
8357
8401
|
}
|
|
8358
8402
|
wc.on("did-finish-load", onLoadEvent);
|
|
@@ -9041,19 +9085,12 @@ ${shadowOverlay}` : result;
|
|
|
9041
9085
|
const elInfo = await describeElementForClick$1(wc, selector);
|
|
9042
9086
|
if ("error" in elInfo) return `Error: ${elInfo.error}`;
|
|
9043
9087
|
const cartMatch = isAddToCartText(elInfo.text);
|
|
9044
|
-
console.log(
|
|
9045
|
-
`[Vessel cart-guard] text="${elInfo.text}" cartMatch=${cartMatch} url=${beforeUrl} hasPrior=${recentCartClicks.has(beforeUrl)}`
|
|
9046
|
-
);
|
|
9047
9088
|
if (cartMatch && isDuplicateCartClick(beforeUrl, elInfo.text)) {
|
|
9048
|
-
console.log(`[Vessel cart-guard] BLOCKED duplicate add-to-cart click`);
|
|
9049
9089
|
return `Blocked: "${elInfo.text}" was already clicked on this page. The item is in your cart. Call read_page to see available actions (e.g. View Cart, Continue Shopping).`;
|
|
9050
9090
|
}
|
|
9051
9091
|
if (!cartMatch && recentCartClicks.has(beforeUrl)) {
|
|
9052
9092
|
const dialogActions = await getCartDialogActions$1(wc);
|
|
9053
9093
|
if (dialogActions) {
|
|
9054
|
-
console.log(
|
|
9055
|
-
`[Vessel cart-guard] BLOCKED background click while cart dialog is open`
|
|
9056
|
-
);
|
|
9057
9094
|
return `Blocked: a cart confirmation dialog is open. Do not click background elements.
|
|
9058
9095
|
${dialogActions}
|
|
9059
9096
|
Click one of these dialog actions instead.`;
|
|
@@ -9066,7 +9103,6 @@ Click one of these dialog actions instead.`;
|
|
|
9066
9103
|
}
|
|
9067
9104
|
}
|
|
9068
9105
|
if (cartMatch) {
|
|
9069
|
-
console.log(`[Vessel cart-guard] RECORDED cart click for url=${beforeUrl}`);
|
|
9070
9106
|
recordCartClick(beforeUrl, elInfo.text);
|
|
9071
9107
|
}
|
|
9072
9108
|
const clickText = `Clicked: ${elInfo.text}`;
|
|
@@ -9306,9 +9342,6 @@ async function dismissPopup$1(wc) {
|
|
|
9306
9342
|
{ timeoutMs: 1500, label: "cart dialog continue shopping" }
|
|
9307
9343
|
);
|
|
9308
9344
|
if (continueResult && continueResult !== PAGE_SCRIPT_TIMEOUT && typeof continueResult === "string" && !continueResult.startsWith("Error")) {
|
|
9309
|
-
console.log(
|
|
9310
|
-
`[Vessel cart-guard] dismiss_popup auto-clicked dialog action: ${continueResult}`
|
|
9311
|
-
);
|
|
9312
9345
|
return `Cart confirmation handled: ${continueResult}. Item was already added to your cart.`;
|
|
9313
9346
|
}
|
|
9314
9347
|
const dialogActions = await getCartDialogActions$1(wc);
|
|
@@ -10669,6 +10702,7 @@ const KNOWN_TOOLS = /* @__PURE__ */ new Set([
|
|
|
10669
10702
|
"dismiss_popup",
|
|
10670
10703
|
"clear_overlays",
|
|
10671
10704
|
"read_page",
|
|
10705
|
+
"screenshot",
|
|
10672
10706
|
"wait_for",
|
|
10673
10707
|
"create_checkpoint",
|
|
10674
10708
|
"restore_checkpoint",
|
|
@@ -10748,6 +10782,19 @@ async function executeAction(name, args, ctx) {
|
|
|
10748
10782
|
dangerous: isDangerousAction$1(name),
|
|
10749
10783
|
executor: async () => {
|
|
10750
10784
|
switch (name) {
|
|
10785
|
+
case "screenshot": {
|
|
10786
|
+
if (!wc) return "Error: No active tab";
|
|
10787
|
+
const screenshotStart = Date.now();
|
|
10788
|
+
const shot = await captureScreenshot(wc);
|
|
10789
|
+
if (!shot.ok) return `Error: ${shot.error}`;
|
|
10790
|
+
const screenshotMs = Date.now() - screenshotStart;
|
|
10791
|
+
const title = wc.getTitle() || "(untitled)";
|
|
10792
|
+
const url = wc.getURL();
|
|
10793
|
+
return makeImageResult(
|
|
10794
|
+
shot.base64,
|
|
10795
|
+
`Screenshot of "${title}" (${url}) — ${shot.width}x${shot.height}, captured in ${screenshotMs}ms. Analyze the image to understand the current visual state of the page.`
|
|
10796
|
+
);
|
|
10797
|
+
}
|
|
10751
10798
|
case "current_tab": {
|
|
10752
10799
|
const active = ctx.tabManager.getActiveTab();
|
|
10753
10800
|
const activeId = ctx.tabManager.getActiveTabId();
|
|
@@ -10951,14 +10998,12 @@ async function executeAction(name, args, ctx) {
|
|
|
10951
10998
|
if (requestedGlance) {
|
|
10952
10999
|
return glanceExtract(wc);
|
|
10953
11000
|
}
|
|
10954
|
-
console.log("[Vessel read_page] starting extraction with 6s timeout");
|
|
10955
11001
|
let content = null;
|
|
10956
11002
|
try {
|
|
10957
11003
|
content = await Promise.race([
|
|
10958
11004
|
extractContent(wc),
|
|
10959
11005
|
new Promise(
|
|
10960
11006
|
(resolve) => setTimeout(() => {
|
|
10961
|
-
console.log("[Vessel read_page] timeout fired, falling back");
|
|
10962
11007
|
resolve(null);
|
|
10963
11008
|
}, 6e3)
|
|
10964
11009
|
)
|
|
@@ -10966,18 +11011,13 @@ async function executeAction(name, args, ctx) {
|
|
|
10966
11011
|
} catch {
|
|
10967
11012
|
content = null;
|
|
10968
11013
|
}
|
|
10969
|
-
console.log(
|
|
10970
|
-
`[Vessel read_page] extraction result: ${content ? `content=${content.content.length}` : "null (timeout)"}`
|
|
10971
|
-
);
|
|
10972
11014
|
if (!content || content.content.length === 0) {
|
|
10973
|
-
console.log("[Vessel read_page] content empty/null, trying quick iframe dismiss");
|
|
10974
11015
|
try {
|
|
10975
11016
|
const iframeResult = await Promise.race([
|
|
10976
11017
|
tryDismissConsentIframe(wc),
|
|
10977
11018
|
new Promise((resolve) => setTimeout(() => resolve(null), 2e3))
|
|
10978
11019
|
]);
|
|
10979
11020
|
if (iframeResult) {
|
|
10980
|
-
console.log(`[Vessel read_page] iframe dismiss: ${iframeResult}`);
|
|
10981
11021
|
await sleep$1(500);
|
|
10982
11022
|
try {
|
|
10983
11023
|
content = await Promise.race([
|
|
@@ -11023,7 +11063,6 @@ ${truncated}`;
|
|
|
11023
11063
|
`Need more detail? Escalate with read_page(mode="debug") only if the narrow modes are insufficient.`
|
|
11024
11064
|
].filter(Boolean).join("\n\n");
|
|
11025
11065
|
}
|
|
11026
|
-
console.log("[Vessel read_page] falling back to glance mode");
|
|
11027
11066
|
return glanceExtract(wc);
|
|
11028
11067
|
}
|
|
11029
11068
|
case "wait_for": {
|
|
@@ -11941,11 +11980,7 @@ async function handleAIQuery(query, provider, activeWebContents, onChunk, onEnd,
|
|
|
11941
11980
|
const isSummarize = lowerQuery.startsWith("summarize") || lowerQuery.startsWith("tldr") || lowerQuery === "summary";
|
|
11942
11981
|
if (provider.streamAgentQuery && tabManager && activeWebContents && runtime2) {
|
|
11943
11982
|
try {
|
|
11944
|
-
const extractStart = Date.now();
|
|
11945
11983
|
const pageContent = await extractContent(activeWebContents);
|
|
11946
|
-
console.log(
|
|
11947
|
-
`[Vessel Agent] initial extractContent completed in ${Date.now() - extractStart}ms, contentLen=${pageContent.content.length}`
|
|
11948
|
-
);
|
|
11949
11984
|
const pageType = detectPageType(pageContent);
|
|
11950
11985
|
const defaultReadMode = chooseAgentReadMode(pageContent);
|
|
11951
11986
|
const structuredContext = buildScopedContext(
|
|
@@ -11998,7 +12033,8 @@ Instructions:
|
|
|
11998
12033
|
- Escalate page reads progressively: read_page(mode="glance") for a fast viewport snapshot on heavy/slow pages, then read_page(mode="visible_only"), read_page(mode="results_only"), read_page(mode="forms_only"), read_page(mode="summary"), or read_page(mode="text_only") depending on what you need.
|
|
11999
12034
|
- Use read_page(mode="glance") when a page is slow to load or extraction times out — it shows what's on screen (headings, links, buttons, inputs) without waiting for heavy JS. It's what a human would see by just looking at the page.
|
|
12000
12035
|
- Use read_page(mode="debug") only as a last resort when the narrower modes are insufficient.
|
|
12001
|
-
- If read_page returns empty or times out, do NOT retry with the same mode. Switch to read_page(mode="glance") or
|
|
12036
|
+
- If read_page returns empty or times out, do NOT retry with the same mode. Switch to read_page(mode="glance") or use screenshot to see the page visually.
|
|
12037
|
+
- Use screenshot when you need to see exactly what the user sees — visual layout, rendered content, images, or when text extraction is failing. The screenshot returns the actual rendered page image for visual analysis. It works even when the JS thread is completely blocked.
|
|
12002
12038
|
- VIEWPORT SYNC: Treat scrolling as a real, user-visible browser action. If you say you are going to scroll, call scroll or scroll_to_element so the human sees the page move too.
|
|
12003
12039
|
- read_page inspects the page without moving the human-visible viewport. Do not describe read_page as scrolling. If you want more context without changing the user's view, say you're reading the page; if you want the user to follow along lower on the page, actually scroll first.
|
|
12004
12040
|
- After clicking or submitting a form, prefer wait_for on a specific result signal or a narrow read_page mode. Do not jump straight to read_page(mode="debug").
|
|
@@ -14215,25 +14251,6 @@ async function waitForCondition(wc, text, selector, timeoutMs) {
|
|
|
14215
14251
|
...diagnostic ? { diagnostic } : {}
|
|
14216
14252
|
});
|
|
14217
14253
|
}
|
|
14218
|
-
async function captureScreenshotPayload(wc) {
|
|
14219
|
-
for (let attempt = 0; attempt < 3; attempt += 1) {
|
|
14220
|
-
await new Promise((resolve) => setTimeout(resolve, 120 * (attempt + 1)));
|
|
14221
|
-
const image = await wc.capturePage();
|
|
14222
|
-
if (!image.isEmpty()) {
|
|
14223
|
-
const size = image.getSize();
|
|
14224
|
-
const base64 = image.toPNG().toString("base64");
|
|
14225
|
-
if (base64) {
|
|
14226
|
-
return {
|
|
14227
|
-
ok: true,
|
|
14228
|
-
base64,
|
|
14229
|
-
width: size.width,
|
|
14230
|
-
height: size.height
|
|
14231
|
-
};
|
|
14232
|
-
}
|
|
14233
|
-
}
|
|
14234
|
-
}
|
|
14235
|
-
return { ok: false, error: "page image was empty after 3 attempts" };
|
|
14236
|
-
}
|
|
14237
14254
|
function registerTools(server, tabManager, runtime2) {
|
|
14238
14255
|
server.registerPrompt(
|
|
14239
14256
|
"vessel-supervisor-brief",
|
|
@@ -15388,7 +15405,7 @@ ${buildScopedContext(pageContent, mode)}`;
|
|
|
15388
15405
|
"Error capturing screenshot: active tab has zero-sized bounds"
|
|
15389
15406
|
);
|
|
15390
15407
|
}
|
|
15391
|
-
const screenshot = await
|
|
15408
|
+
const screenshot = await captureScreenshot(tab.view.webContents);
|
|
15392
15409
|
if (!screenshot.ok) {
|
|
15393
15410
|
return asTextResponse(
|
|
15394
15411
|
`Error capturing screenshot: ${screenshot.error}`
|
|
@@ -17210,15 +17227,18 @@ function startMcpServer(tabManager, runtime2, port) {
|
|
|
17210
17227
|
res.end("Not found");
|
|
17211
17228
|
return;
|
|
17212
17229
|
}
|
|
17213
|
-
|
|
17214
|
-
|
|
17215
|
-
"Access-Control-Allow-
|
|
17216
|
-
|
|
17217
|
-
|
|
17218
|
-
|
|
17219
|
-
|
|
17220
|
-
|
|
17221
|
-
|
|
17230
|
+
const origin = req.headers.origin;
|
|
17231
|
+
if (origin && /^https?:\/\/(localhost|127\.0\.0\.1)(:\d+)?$/.test(origin)) {
|
|
17232
|
+
res.setHeader("Access-Control-Allow-Origin", origin);
|
|
17233
|
+
res.setHeader(
|
|
17234
|
+
"Access-Control-Allow-Methods",
|
|
17235
|
+
"POST, GET, DELETE, OPTIONS"
|
|
17236
|
+
);
|
|
17237
|
+
res.setHeader(
|
|
17238
|
+
"Access-Control-Allow-Headers",
|
|
17239
|
+
"Content-Type, mcp-session-id, Authorization"
|
|
17240
|
+
);
|
|
17241
|
+
}
|
|
17222
17242
|
if (req.method === "OPTIONS") {
|
|
17223
17243
|
res.writeHead(204);
|
|
17224
17244
|
res.end();
|
|
@@ -17932,7 +17952,6 @@ ${progress}
|
|
|
17932
17952
|
mode: "replace"
|
|
17933
17953
|
});
|
|
17934
17954
|
const approvalReason = this.getApprovalReason(dangerous);
|
|
17935
|
-
console.log(`[Vessel Runtime] action=${name} dangerous=${dangerous} approvalReason=${approvalReason} mode=${this.state.supervisor.approvalMode}`);
|
|
17936
17955
|
if (approvalReason) {
|
|
17937
17956
|
this.publishTranscript({
|
|
17938
17957
|
source,
|
|
@@ -17942,9 +17961,7 @@ ${progress}
|
|
|
17942
17961
|
streamId: transcriptStreamId,
|
|
17943
17962
|
mode: "replace"
|
|
17944
17963
|
});
|
|
17945
|
-
console.log(`[Vessel Runtime] awaiting approval for ${name}...`);
|
|
17946
17964
|
const approved = await this.awaitApproval(action, approvalReason);
|
|
17947
|
-
console.log(`[Vessel Runtime] approval result for ${name}: ${approved}`);
|
|
17948
17965
|
if (!approved) {
|
|
17949
17966
|
this.publishTranscript({
|
|
17950
17967
|
source,
|
|
@@ -18053,16 +18070,15 @@ ${progress}
|
|
|
18053
18070
|
actions: this.state.actions.slice(-120),
|
|
18054
18071
|
checkpoints: this.state.checkpoints.slice(-20)
|
|
18055
18072
|
};
|
|
18056
|
-
|
|
18057
|
-
fs$1.
|
|
18058
|
-
fs$1.writeFileSync(
|
|
18073
|
+
return fs$1.promises.mkdir(path$1.dirname(getRuntimeStatePath()), { recursive: true }).then(
|
|
18074
|
+
() => fs$1.promises.writeFile(
|
|
18059
18075
|
getRuntimeStatePath(),
|
|
18060
18076
|
JSON.stringify(persisted, null, 2),
|
|
18061
18077
|
"utf-8"
|
|
18062
|
-
)
|
|
18063
|
-
|
|
18064
|
-
console.error("[Vessel] Failed to persist runtime state:", err)
|
|
18065
|
-
|
|
18078
|
+
)
|
|
18079
|
+
).catch(
|
|
18080
|
+
(err) => console.error("[Vessel] Failed to persist runtime state:", err)
|
|
18081
|
+
);
|
|
18066
18082
|
}
|
|
18067
18083
|
schedulePersist() {
|
|
18068
18084
|
this.persistDirty = true;
|
|
@@ -18074,7 +18090,7 @@ ${progress}
|
|
|
18074
18090
|
}
|
|
18075
18091
|
/** Flush any pending debounced persist to disk immediately. Call on shutdown. */
|
|
18076
18092
|
flushPersist() {
|
|
18077
|
-
|
|
18093
|
+
return this.persistDirty ? this.persistNow() : Promise.resolve();
|
|
18078
18094
|
}
|
|
18079
18095
|
emit() {
|
|
18080
18096
|
this.schedulePersist();
|
|
@@ -18125,7 +18141,7 @@ ${progress}
|
|
|
18125
18141
|
if (!toolBreakdown[name]) toolBreakdown[name] = { count: 0, totalMs: 0, avgMs: 0, errors: 0 };
|
|
18126
18142
|
toolBreakdown[name].count++;
|
|
18127
18143
|
if (action.durationMs != null) toolBreakdown[name].totalMs += action.durationMs;
|
|
18128
|
-
if (action.status === "
|
|
18144
|
+
if (action.status === "failed") toolBreakdown[name].errors++;
|
|
18129
18145
|
}
|
|
18130
18146
|
for (const entry of Object.values(toolBreakdown)) {
|
|
18131
18147
|
entry.avgMs = entry.count > 0 ? Math.round(entry.totalMs / entry.count) : 0;
|
|
@@ -1972,7 +1972,7 @@ const _createHooksMap = function _createHooksMap2() {
|
|
|
1972
1972
|
function createDOMPurify() {
|
|
1973
1973
|
let window2 = arguments.length > 0 && arguments[0] !== void 0 ? arguments[0] : getGlobal();
|
|
1974
1974
|
const DOMPurify = (root2) => createDOMPurify(root2);
|
|
1975
|
-
DOMPurify.version = "3.3.
|
|
1975
|
+
DOMPurify.version = "3.3.3";
|
|
1976
1976
|
DOMPurify.removed = [];
|
|
1977
1977
|
if (!window2 || !window2.document || window2.document.nodeType !== NODE_TYPE.document || !window2.Element) {
|
|
1978
1978
|
DOMPurify.isSupported = false;
|
|
@@ -3221,7 +3221,7 @@ const Sidebar = (props) => {
|
|
|
3221
3221
|
if (sidebarTab() !== "chat") return;
|
|
3222
3222
|
const poll = async () => {
|
|
3223
3223
|
try {
|
|
3224
|
-
const count = await window.vessel
|
|
3224
|
+
const count = await window.vessel.highlights.getCount() ?? 0;
|
|
3225
3225
|
setHighlightCount(count);
|
|
3226
3226
|
if (count === 0 && highlightIndex() >= 0) setHighlightIndex(-1);
|
|
3227
3227
|
} catch {
|
|
@@ -3236,23 +3236,23 @@ const Sidebar = (props) => {
|
|
|
3236
3236
|
if (count === 0) return;
|
|
3237
3237
|
const clamped = Math.max(0, Math.min(idx, count - 1));
|
|
3238
3238
|
setHighlightIndex(clamped);
|
|
3239
|
-
await window.vessel
|
|
3239
|
+
await window.vessel.highlights.scrollTo(clamped);
|
|
3240
3240
|
};
|
|
3241
3241
|
const removeCurrentHighlight = async () => {
|
|
3242
3242
|
const idx = highlightIndex();
|
|
3243
3243
|
if (idx < 0) return;
|
|
3244
|
-
await window.vessel
|
|
3245
|
-
const newCount = await window.vessel
|
|
3244
|
+
await window.vessel.highlights.remove(idx);
|
|
3245
|
+
const newCount = await window.vessel.highlights.getCount() ?? 0;
|
|
3246
3246
|
setHighlightCount(newCount);
|
|
3247
3247
|
if (newCount === 0) {
|
|
3248
3248
|
setHighlightIndex(-1);
|
|
3249
3249
|
} else if (idx >= newCount) {
|
|
3250
3250
|
setHighlightIndex(newCount - 1);
|
|
3251
|
-
await window.vessel
|
|
3251
|
+
await window.vessel.highlights.scrollTo(newCount - 1);
|
|
3252
3252
|
}
|
|
3253
3253
|
};
|
|
3254
3254
|
const clearAllHighlights = async () => {
|
|
3255
|
-
await window.vessel
|
|
3255
|
+
await window.vessel.highlights.clearAll();
|
|
3256
3256
|
setHighlightCount(0);
|
|
3257
3257
|
setHighlightIndex(-1);
|
|
3258
3258
|
};
|
package/out/renderer/index.html
CHANGED
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
|
6
6
|
<meta http-equiv="Content-Security-Policy" content="default-src 'self'; script-src 'self'; style-src 'self' 'unsafe-inline'; img-src 'self' data:; connect-src 'self'; font-src 'self' data:;" />
|
|
7
7
|
<title>Vessel</title>
|
|
8
|
-
<script type="module" crossorigin src="./assets/index-
|
|
8
|
+
<script type="module" crossorigin src="./assets/index-iB-6qrfG.js"></script>
|
|
9
9
|
<link rel="stylesheet" crossorigin href="./assets/index-DMd-y6tm.css">
|
|
10
10
|
</head>
|
|
11
11
|
<body>
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@quanta-intellect/vessel-browser",
|
|
3
3
|
"mcpName": "io.github.unmodeled-tyler/vessel-browser",
|
|
4
|
-
"version": "0.1.
|
|
4
|
+
"version": "0.1.17",
|
|
5
5
|
"description": "AI-native web browser for Linux — persistent browser runtime for autonomous agents with human supervision",
|
|
6
6
|
"main": "./out/main/index.js",
|
|
7
7
|
"bin": {
|
|
@@ -24,7 +24,8 @@
|
|
|
24
24
|
"typecheck": "tsc --noEmit",
|
|
25
25
|
"prepublishOnly": "npm run build",
|
|
26
26
|
"test:navigation-regression": "electron --no-sandbox --disable-setuid-sandbox scripts/run-navigation-regression.mjs",
|
|
27
|
-
"smoke:test": "node scripts/smoke-test.mjs"
|
|
27
|
+
"smoke:test": "node scripts/smoke-test.mjs",
|
|
28
|
+
"test:coverage": "c8 --reporter=text --reporter=html node --test tests/**/*.test.ts"
|
|
28
29
|
},
|
|
29
30
|
"keywords": [
|
|
30
31
|
"agent-browser",
|
|
@@ -52,22 +53,23 @@
|
|
|
52
53
|
},
|
|
53
54
|
"devDependencies": {
|
|
54
55
|
"@types/dompurify": "^3.0.5",
|
|
55
|
-
"@types/node": "^25.
|
|
56
|
-
"electron": "^40.8.
|
|
56
|
+
"@types/node": "^25.5.0",
|
|
57
|
+
"electron": "^40.8.3",
|
|
57
58
|
"electron-builder": "^26.8.1",
|
|
58
59
|
"electron-vite": "^5.0.0",
|
|
59
60
|
"solid-js": "^1.9.11",
|
|
61
|
+
"c8": "^10.1.3",
|
|
60
62
|
"tsx": "^4.21.0",
|
|
61
63
|
"typescript": "^5.9.3",
|
|
62
|
-
"vite-plugin-solid": "^2.11.
|
|
64
|
+
"vite-plugin-solid": "^2.11.11"
|
|
63
65
|
},
|
|
64
66
|
"dependencies": {
|
|
65
|
-
"@anthropic-ai/sdk": "^0.
|
|
67
|
+
"@anthropic-ai/sdk": "^0.80.0",
|
|
66
68
|
"@modelcontextprotocol/sdk": "^1.27.1",
|
|
67
69
|
"@mozilla/readability": "^0.6.0",
|
|
68
|
-
"dompurify": "^3.3.
|
|
70
|
+
"dompurify": "^3.3.3",
|
|
69
71
|
"linkedom": "^0.18.12",
|
|
70
|
-
"openai": "^6.
|
|
72
|
+
"openai": "^6.32.0",
|
|
71
73
|
"zod": "^4.3.6"
|
|
72
74
|
}
|
|
73
75
|
}
|