@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
- try {
84
- fs.mkdirSync(path.dirname(getSettingsPath()), { recursive: true });
85
- fs.writeFileSync(getSettingsPath(), JSON.stringify(settings, null, 2));
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
- try {
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
- try {
1206
- fs.mkdirSync(path.dirname(getHistoryPath()), { recursive: true });
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
- } catch (err) {
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
- result = `Error: Tool execution failed — ${toolErr.message || toolErr}. Try a different approach or call read_page to refresh context.`;
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
- result = `Error: Tool execution failed — ${toolErr.message || toolErr}. Try a different approach or call read_page to refresh context.`;
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: result
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
- try {
7418
- fs.mkdirSync(path.dirname(getBookmarksPath()), { recursive: true });
7419
- fs.writeFileSync(getBookmarksPath(), JSON.stringify(state, null, 2), "utf-8");
7420
- } catch (err) {
7421
- console.error("[Vessel] Failed to save bookmarks:", err);
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("load event");
8394
+ finish();
8351
8395
  }
8352
8396
  };
8353
- const timer = setTimeout(() => finish("timeout"), timeout);
8397
+ const timer = setTimeout(() => finish(), timeout);
8354
8398
  if (!wc.isLoading()) {
8355
- finish("already loaded");
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 interact directly with click/type_text.
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 captureScreenshotPayload(tab.view.webContents);
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
- res.setHeader("Access-Control-Allow-Origin", "null");
17214
- res.setHeader(
17215
- "Access-Control-Allow-Methods",
17216
- "POST, GET, DELETE, OPTIONS"
17217
- );
17218
- res.setHeader(
17219
- "Access-Control-Allow-Headers",
17220
- "Content-Type, mcp-session-id, Authorization"
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
- try {
18057
- fs$1.mkdirSync(path$1.dirname(getRuntimeStatePath()), { recursive: true });
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
- } catch (err) {
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
- if (this.persistDirty) this.persistNow();
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 === "error") toolBreakdown[name].errors++;
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.2";
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?.highlights?.getCount?.() ?? 0;
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?.highlights?.scrollTo?.(clamped);
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?.highlights?.remove?.(idx);
3245
- const newCount = await window.vessel?.highlights?.getCount?.() ?? 0;
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?.highlights?.scrollTo?.(newCount - 1);
3251
+ await window.vessel.highlights.scrollTo(newCount - 1);
3252
3252
  }
3253
3253
  };
3254
3254
  const clearAllHighlights = async () => {
3255
- await window.vessel?.highlights?.clearAll?.();
3255
+ await window.vessel.highlights.clearAll();
3256
3256
  setHighlightCount(0);
3257
3257
  setHighlightIndex(-1);
3258
3258
  };
@@ -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-DSaws_sH.js"></script>
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.15",
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.3.5",
56
- "electron": "^40.8.0",
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.10"
64
+ "vite-plugin-solid": "^2.11.11"
63
65
  },
64
66
  "dependencies": {
65
- "@anthropic-ai/sdk": "^0.78.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.2",
70
+ "dompurify": "^3.3.3",
69
71
  "linkedom": "^0.18.12",
70
- "openai": "^6.27.0",
72
+ "openai": "^6.32.0",
71
73
  "zod": "^4.3.6"
72
74
  }
73
75
  }