@quanta-intellect/vessel-browser 0.1.58 → 0.1.61

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
@@ -2683,6 +2683,8 @@ const Channels = {
2683
2683
  DEVTOOLS_PANEL_TOGGLE: "devtools-panel:toggle",
2684
2684
  DEVTOOLS_PANEL_STATE: "devtools-panel:state",
2685
2685
  DEVTOOLS_PANEL_RESIZE: "devtools-panel:resize",
2686
+ // Ad blocking
2687
+ TAB_TOGGLE_AD_BLOCK: "tab:toggle-ad-block",
2686
2688
  // Find in page
2687
2689
  FIND_IN_PAGE_START: "find:start",
2688
2690
  FIND_IN_PAGE_NEXT: "find:next",
@@ -2706,6 +2708,11 @@ const Channels = {
2706
2708
  PREMIUM_RESET: "premium:reset",
2707
2709
  PREMIUM_TRACK_CONTEXT: "premium:track-context",
2708
2710
  PREMIUM_UPDATE: "premium:update",
2711
+ // Named sessions
2712
+ SESSION_LIST: "session:list",
2713
+ SESSION_SAVE: "session:save",
2714
+ SESSION_LOAD: "session:load",
2715
+ SESSION_DELETE: "session:delete",
2709
2716
  // Agent Credential Vault
2710
2717
  VAULT_LIST: "vault:list",
2711
2718
  VAULT_ADD: "vault:add",
@@ -2740,6 +2747,7 @@ const Channels = {
2740
2747
  };
2741
2748
  const MAX_DETAIL_ITEMS = 3;
2742
2749
  const MIN_BLOCK_SIMILARITY = 0.82;
2750
+ const MAX_DIFF_BLOCKS = 500;
2743
2751
  function normalizeText$2(value) {
2744
2752
  return value.replace(/\s+/g, " ").trim();
2745
2753
  }
@@ -2856,8 +2864,10 @@ function diffSnapshots(oldSnap, currentContent, currentTitle, currentHeadings) {
2856
2864
  const oldHeadings = oldSnap.headings.split("\n").filter(Boolean);
2857
2865
  const newHeadings = currentHeadings.split("\n").filter(Boolean);
2858
2866
  if (oldHeadings.join("\n") !== newHeadings.join("\n")) {
2859
- const added = newHeadings.filter((h) => !oldHeadings.includes(h));
2860
- const removed = oldHeadings.filter((h) => !newHeadings.includes(h));
2867
+ const oldSet = new Set(oldHeadings);
2868
+ const newSet = new Set(newHeadings);
2869
+ const added = newHeadings.filter((h) => !oldSet.has(h));
2870
+ const removed = oldHeadings.filter((h) => !newSet.has(h));
2861
2871
  const parts = [];
2862
2872
  if (added.length > 0) parts.push(`New: ${added.join(", ")}`);
2863
2873
  if (removed.length > 0) parts.push(`Gone: ${removed.join(", ")}`);
@@ -2871,8 +2881,10 @@ function diffSnapshots(oldSnap, currentContent, currentTitle, currentHeadings) {
2871
2881
  });
2872
2882
  }
2873
2883
  }
2874
- const oldBlocks = extractTextBlocks(oldSnap.textContent);
2875
- const newBlocks = extractTextBlocks(currentContent);
2884
+ const rawOldBlocks = extractTextBlocks(oldSnap.textContent);
2885
+ const rawNewBlocks = extractTextBlocks(currentContent);
2886
+ const oldBlocks = rawOldBlocks.slice(0, MAX_DIFF_BLOCKS);
2887
+ const newBlocks = rawNewBlocks.slice(0, MAX_DIFF_BLOCKS);
2876
2888
  const overallSimilarity = similarityScore(oldSnap.textContent, currentContent);
2877
2889
  if (overallSimilarity < 0.98) {
2878
2890
  const ops = diffBlocks(oldBlocks, newBlocks);
@@ -3754,7 +3766,7 @@ async function getCheckoutUrl(email) {
3754
3766
  async function getPortalUrl() {
3755
3767
  return {
3756
3768
  ok: false,
3757
- error: "Billing portal access is temporarily disabled while we harden the premium API. Use the Stripe billing link from your subscription email for now."
3769
+ error: "Billing portal access is temporarily disabled until authenticated customer access is implemented."
3758
3770
  };
3759
3771
  }
3760
3772
  async function verifySubscription(identifier) {
@@ -3891,10 +3903,8 @@ function startBackgroundRevalidation() {
3891
3903
  }, REVALIDATION_INTERVAL_MS);
3892
3904
  }
3893
3905
  function stopBackgroundRevalidation() {
3894
- if (revalidationTimer) {
3895
- clearInterval(revalidationTimer);
3896
- revalidationTimer = null;
3897
- }
3906
+ clearInterval(revalidationTimer);
3907
+ revalidationTimer = null;
3898
3908
  }
3899
3909
  function isPremiumActiveState(state2) {
3900
3910
  return state2.status === "active" || state2.status === "trialing";
@@ -3999,8 +4009,8 @@ function stopTelemetry() {
3999
4009
  }
4000
4010
  if (flushTimer) {
4001
4011
  clearInterval(flushTimer);
4002
- flushTimer = null;
4003
4012
  }
4013
+ flushTimer = null;
4004
4014
  void flush();
4005
4015
  }
4006
4016
  async function flush() {
@@ -4101,6 +4111,226 @@ function selectorHelpersJS(attributes = DEFAULT_SELECTOR_ATTRIBUTES) {
4101
4111
  "}"
4102
4112
  ].join("\n");
4103
4113
  }
4114
+ function normalizeString(value) {
4115
+ return (value ?? "").trim().toLowerCase();
4116
+ }
4117
+ function mapInputType(el) {
4118
+ const inputType = el.inputType ?? el.type ?? "text";
4119
+ switch (inputType.toLowerCase()) {
4120
+ case "email":
4121
+ return "email";
4122
+ case "password":
4123
+ return "password";
4124
+ case "number":
4125
+ case "range":
4126
+ return "number";
4127
+ case "select-one":
4128
+ case "select":
4129
+ return "select";
4130
+ case "checkbox":
4131
+ case "radio":
4132
+ return "checkbox";
4133
+ case "date":
4134
+ case "datetime-local":
4135
+ case "time":
4136
+ case "month":
4137
+ case "week":
4138
+ return "date";
4139
+ case "file":
4140
+ return "file";
4141
+ default:
4142
+ return "text";
4143
+ }
4144
+ }
4145
+ function mapFormFields(forms, interactiveElements) {
4146
+ const fields = [];
4147
+ const formFieldSelectors = /* @__PURE__ */ new Set();
4148
+ for (const form of forms) {
4149
+ for (const el of form.fields ?? []) {
4150
+ formFieldSelectors.add(el.selector || el.name || el.label || String(el.index));
4151
+ }
4152
+ }
4153
+ for (const el of interactiveElements) {
4154
+ const key = el.selector || el.name || el.label || String(el.index);
4155
+ if (formFieldSelectors.has(key)) {
4156
+ fields.push({
4157
+ name: el.name || el.label || key,
4158
+ type: mapInputType(el),
4159
+ label: el.label,
4160
+ required: el.required,
4161
+ selector: el.selector || ""
4162
+ });
4163
+ }
4164
+ }
4165
+ return fields;
4166
+ }
4167
+ function mapActionButtons(interactiveElements) {
4168
+ const buttons = [];
4169
+ const seen = /* @__PURE__ */ new Set();
4170
+ for (const el of interactiveElements) {
4171
+ if (el.type !== "button" && el.type !== "submit" && el.type !== "reset") continue;
4172
+ const label = (el.label || el.textContent || "").trim();
4173
+ if (!label || seen.has(label)) continue;
4174
+ seen.add(label);
4175
+ let intent;
4176
+ const normalized = normalizeString(label);
4177
+ if (/\b(add to cart|buy now|add to bag|add to basket|shop now)\b/.test(normalized)) {
4178
+ intent = "addToCart";
4179
+ } else if (/\b(login|sign in|log in|signin|log-in)\b/.test(normalized)) {
4180
+ intent = "login";
4181
+ } else if (/\b(submit|send|continue|next|proceed|register|create account|sign up)\b/.test(normalized)) {
4182
+ intent = "submit";
4183
+ } else if (/\b(cancel|back|return|go back|close)\b/.test(normalized)) {
4184
+ intent = "cancel";
4185
+ } else if (/\b(download|export|save as)\b/.test(normalized)) {
4186
+ intent = "download";
4187
+ } else if (/\b(search|find|go|submit search)\b/.test(normalized)) {
4188
+ intent = "search";
4189
+ } else if (el.href || el.url) {
4190
+ intent = "navigate";
4191
+ }
4192
+ if (el.selector || el.name || el.label) {
4193
+ buttons.push({
4194
+ label,
4195
+ selector: el.selector || el.name || el.label,
4196
+ intent
4197
+ });
4198
+ }
4199
+ }
4200
+ return buttons;
4201
+ }
4202
+ function extractPrimaryEntity(pageType, structuredData, metaTags) {
4203
+ if (pageType === "product") {
4204
+ const product = structuredData?.find(
4205
+ (e) => e.types.some((t) => /^product$/i.test(t))
4206
+ );
4207
+ if (product) {
4208
+ const attrs = product.attributes ?? {};
4209
+ return {
4210
+ type: "Product",
4211
+ nameField: typeof attrs.name === "string" ? attrs.name : void 0,
4212
+ priceField: typeof attrs.price === "string" ? attrs.price : typeof attrs.offers === "object" && attrs.offers !== null ? String(attrs.offers["price"] ?? "") : void 0,
4213
+ imageField: typeof attrs.image === "string" ? attrs.image : Array.isArray(attrs.image) ? String(attrs.image[0]) : void 0,
4214
+ descriptionField: typeof attrs.description === "string" ? attrs.description : void 0,
4215
+ reviewsField: typeof attrs.reviews === "string" ? attrs.reviews : void 0,
4216
+ ratingField: typeof attrs.rating === "string" ? attrs.rating : void 0,
4217
+ addToCartField: void 0
4218
+ };
4219
+ }
4220
+ }
4221
+ if (pageType === "article") {
4222
+ const article = structuredData?.find(
4223
+ (e) => e.types.some(
4224
+ (t) => /^(article|newsarticle|blogposting|webpage)$/i.test(t)
4225
+ )
4226
+ );
4227
+ if (article) {
4228
+ const attrs = article.attributes ?? {};
4229
+ return {
4230
+ type: article.types[0] ?? "Article",
4231
+ nameField: typeof attrs.headline === "string" ? attrs.headline : typeof attrs.name === "string" ? attrs.name : void 0,
4232
+ descriptionField: typeof attrs.articleBody === "string" ? attrs.articleBody : typeof attrs.description === "string" ? attrs.description : void 0
4233
+ };
4234
+ }
4235
+ }
4236
+ return void 0;
4237
+ }
4238
+ function inferPageSchema(page) {
4239
+ let pageType = "unknown";
4240
+ let confidence = 0.5;
4241
+ const structuredData = page.structuredData;
4242
+ const metaTags = page.metaTags;
4243
+ const url = page.url ?? "";
4244
+ const forms = page.forms ?? [];
4245
+ const interactiveElements = page.interactiveElements ?? [];
4246
+ const urlLower = normalizeString(url);
4247
+ const jsonLdTypes = [];
4248
+ for (const entity of structuredData ?? []) {
4249
+ jsonLdTypes.push(...entity.types);
4250
+ }
4251
+ const hasProduct = jsonLdTypes.some((t) => /^product$/i.test(t));
4252
+ const hasArticle = jsonLdTypes.some(
4253
+ (t) => /^(article|newsarticle|blogposting)$/i.test(t)
4254
+ );
4255
+ const hasEvent = jsonLdTypes.some((t) => /^event$/i.test(t));
4256
+ const hasSearchResults = jsonLdTypes.some((t) => /^searchresultspage$/i.test(t));
4257
+ if (hasProduct) {
4258
+ pageType = "product";
4259
+ confidence += 0.2;
4260
+ } else if (hasArticle) {
4261
+ pageType = "article";
4262
+ confidence += 0.2;
4263
+ } else if (hasEvent) {
4264
+ pageType = "form";
4265
+ confidence += 0.15;
4266
+ } else if (hasSearchResults) {
4267
+ pageType = "search";
4268
+ confidence += 0.2;
4269
+ }
4270
+ const ogType = metaTags?.["og:type"];
4271
+ if (ogType) {
4272
+ const normalized = normalizeString(ogType);
4273
+ if (/^product$/i.test(normalized) && pageType !== "product") {
4274
+ pageType = "product";
4275
+ confidence += 0.15;
4276
+ } else if (/^article|blog|news/i.test(normalized) && pageType !== "article") {
4277
+ pageType = "article";
4278
+ confidence += 0.15;
4279
+ }
4280
+ }
4281
+ if (/\/checkout|\/cart|\/payment|\/billing/i.test(urlLower) && pageType === "unknown") {
4282
+ pageType = "checkout";
4283
+ confidence += 0.1;
4284
+ } else if (/\/login|\/signin|\/auth/i.test(urlLower) && pageType === "unknown") {
4285
+ pageType = "login";
4286
+ confidence += 0.1;
4287
+ } else if (/\/dashboard|\/account|\/profile/i.test(urlLower) && pageType === "unknown") {
4288
+ pageType = "dashboard";
4289
+ confidence += 0.1;
4290
+ } else if (/\/search/i.test(urlLower) && pageType === "unknown") {
4291
+ pageType = "search";
4292
+ confidence += 0.1;
4293
+ }
4294
+ const hasFormWithSubmit = forms.some(
4295
+ (f) => f.fields.some(
4296
+ (el) => el.type === "submit" || normalizeString(el.inputType) === "submit" || normalizeString(el.name) === "submit"
4297
+ )
4298
+ );
4299
+ const hasPriceSelectors = interactiveElements.some(
4300
+ (el) => el.selector?.includes("price") || normalizeString(el.label).includes("price") || normalizeString(el.name).includes("price") || el.selector?.includes("cost") || el.selector?.includes("amount")
4301
+ );
4302
+ if (pageType === "unknown") {
4303
+ if (hasFormWithSubmit && forms.length > 0) {
4304
+ pageType = "form";
4305
+ confidence += 0.1;
4306
+ } else if (hasPriceSelectors) {
4307
+ pageType = "product";
4308
+ confidence += 0.1;
4309
+ }
4310
+ }
4311
+ if ((pageType === "checkout" || pageType === "unknown") && forms.length > 0) {
4312
+ if (hasFormWithSubmit) {
4313
+ if (pageType === "unknown") {
4314
+ pageType = "form";
4315
+ }
4316
+ confidence += 0.15;
4317
+ }
4318
+ }
4319
+ if (pageType === "unknown") {
4320
+ confidence = 0.5;
4321
+ }
4322
+ confidence = Math.min(0.95, confidence);
4323
+ const primaryEntity = extractPrimaryEntity(pageType, structuredData);
4324
+ const formFields = pageType === "form" || pageType === "checkout" || pageType === "login" ? mapFormFields(forms, interactiveElements) : void 0;
4325
+ const actionButtons = mapActionButtons(interactiveElements);
4326
+ return {
4327
+ pageType,
4328
+ primaryEntity,
4329
+ formFields,
4330
+ actionButtons,
4331
+ confidence
4332
+ };
4333
+ }
4104
4334
  const EMPTY_PAGE_CONTENT = {
4105
4335
  title: "",
4106
4336
  content: "",
@@ -4814,156 +5044,6 @@ const DIRECT_EXTRACTION_SCRIPT = String.raw`
4814
5044
  };
4815
5045
  })()
4816
5046
  `;
4817
- const SAFE_EXTRACTION_SCRIPT = String.raw`
4818
- (function() {
4819
- function getCleanBodyText() {
4820
- var removed = [];
4821
- document
4822
- .querySelectorAll('.__vessel-highlight-label[data-vessel-highlight]')
4823
- .forEach(function(label) {
4824
- var parent = label.parentNode;
4825
- if (!parent) return;
4826
- removed.push({ label: label, parent: parent, nextSibling: label.nextSibling });
4827
- parent.removeChild(label);
4828
- });
4829
- try {
4830
- return document.body?.innerText || document.documentElement?.innerText || "";
4831
- } finally {
4832
- for (var i = removed.length - 1; i >= 0; i--) {
4833
- var entry = removed[i];
4834
- entry.parent.insertBefore(entry.label, entry.nextSibling);
4835
- }
4836
- }
4837
- }
4838
-
4839
- function text(value) {
4840
- const trimmed = value == null ? "" : String(value).trim();
4841
- return trimmed || undefined;
4842
- }
4843
-
4844
- function labelFor(el) {
4845
- const aria = text(el.getAttribute && el.getAttribute("aria-label"));
4846
- if (aria) return aria;
4847
- const placeholder = text(el.getAttribute && el.getAttribute("placeholder"));
4848
- if (placeholder) return placeholder;
4849
- if (el.id) {
4850
- const directLabel = document.querySelector('label[for="' + String(el.id).replace(/["\\]/g, "\\$&") + '"]');
4851
- const labelText = text(directLabel && directLabel.textContent);
4852
- if (labelText) return labelText;
4853
- }
4854
- return text(el.textContent);
4855
- }
4856
-
4857
- let indexCounter = 0;
4858
- function nextIndex() {
4859
- indexCounter += 1;
4860
- return indexCounter;
4861
- }
4862
-
4863
- const headings = Array.from(document.querySelectorAll("h1, h2, h3, h4, h5, h6"))
4864
- .map((el) => {
4865
- const headingText = text(el.textContent);
4866
- if (!headingText) return null;
4867
- return { level: Number.parseInt(el.tagName[1], 10), text: headingText };
4868
- })
4869
- .filter(Boolean);
4870
-
4871
- const navigation = Array.from(document.querySelectorAll("nav a[href], [role='navigation'] a[href], header nav a[href]"))
4872
- .map((el) => {
4873
- const href = text(el.href || el.getAttribute("href"));
4874
- const linkText = text(el.textContent);
4875
- if (!href || href.startsWith("#") || !linkText) return null;
4876
- return {
4877
- type: "link",
4878
- text: linkText.slice(0, 100),
4879
- href: href.slice(0, 500),
4880
- context: "nav",
4881
- index: nextIndex(),
4882
- visible: true,
4883
- disabled: false,
4884
- };
4885
- })
4886
- .filter(Boolean);
4887
-
4888
- const interactiveElements = [];
4889
- Array.from(document.querySelectorAll("button, [role='button'], input[type='submit'], input[type='button']"))
4890
- .forEach((el) => {
4891
- interactiveElements.push({
4892
- type: "button",
4893
- text: text(el.textContent || el.value || el.getAttribute("aria-label") || "Button")?.slice(0, 100),
4894
- index: nextIndex(),
4895
- visible: true,
4896
- disabled: !!(el.hasAttribute && (el.hasAttribute("disabled") || el.getAttribute("aria-disabled") === "true")),
4897
- });
4898
- });
4899
-
4900
- Array.from(document.querySelectorAll("a[href]")).forEach((el) => {
4901
- const href = text(el.href || el.getAttribute("href"));
4902
- const linkText = text(el.textContent);
4903
- if (!href || href.startsWith("#") || !linkText) return;
4904
- interactiveElements.push({
4905
- type: "link",
4906
- text: linkText.slice(0, 100),
4907
- href: href.slice(0, 500),
4908
- index: nextIndex(),
4909
- visible: true,
4910
- disabled: false,
4911
- });
4912
- });
4913
-
4914
- Array.from(document.querySelectorAll("input:not([type='hidden']):not([type='submit']):not([type='button']), select, textarea"))
4915
- .forEach((el) => {
4916
- const tag = el.tagName.toLowerCase();
4917
- var elType = (el.type || "").toLowerCase();
4918
- interactiveElements.push({
4919
- type: tag === "select" ? "select" : tag === "textarea" ? "textarea" : "input",
4920
- label: labelFor(el)?.slice(0, 100),
4921
- inputType: text(el.getAttribute && el.getAttribute("type")),
4922
- placeholder: text(el.getAttribute && el.getAttribute("placeholder")),
4923
- value: shouldExposeFieldValue(el) ? text(el.value) : undefined,
4924
- options: tag === "select"
4925
- ? Array.from(el.options || []).map(function(option) { return { label: text(option.textContent || option.value) || option.value, value: option.value }; }).filter(function(o) { return o.label || o.value; }).slice(0, 25)
4926
- : undefined,
4927
- required: !!(el.hasAttribute && el.hasAttribute("required")) || undefined,
4928
- index: nextIndex(),
4929
- visible: true,
4930
- disabled: !!(el.hasAttribute && (el.hasAttribute("disabled") || el.getAttribute("aria-disabled") === "true")),
4931
- name: el.name || undefined,
4932
- autocomplete: text(el.getAttribute && el.getAttribute("autocomplete")),
4933
- checked: (elType === "checkbox" || elType === "radio") ? !!el.checked : undefined,
4934
- });
4935
- });
4936
-
4937
- const forms = Array.from(document.querySelectorAll("form")).map((form) => ({
4938
- id: text(form.id),
4939
- action: text(form.getAttribute("action")),
4940
- method: text(form.getAttribute("method")),
4941
- fields: [],
4942
- }));
4943
-
4944
- return {
4945
- title: document.title || "",
4946
- content: getCleanBodyText(),
4947
- htmlContent: "",
4948
- byline: "",
4949
- excerpt: "",
4950
- url: window.location.href || "",
4951
- headings,
4952
- navigation,
4953
- interactiveElements,
4954
- forms,
4955
- viewport: {
4956
- width: window.innerWidth || document.documentElement?.clientWidth || 0,
4957
- height: window.innerHeight || document.documentElement?.clientHeight || 0,
4958
- scrollX: 0,
4959
- scrollY: 0,
4960
- },
4961
- overlays: [],
4962
- dormantOverlays: [],
4963
- landmarks: [],
4964
- };
4965
- })()
4966
- `;
4967
5047
  function delay(ms) {
4968
5048
  return new Promise((resolve) => setTimeout(resolve, ms));
4969
5049
  }
@@ -5000,7 +5080,7 @@ async function executeScript(webContents, script) {
5000
5080
  } catch {
5001
5081
  return null;
5002
5082
  } finally {
5003
- if (typeof timer !== "undefined" && timer) {
5083
+ if (timer) {
5004
5084
  clearTimeout(timer);
5005
5085
  }
5006
5086
  }
@@ -5075,10 +5155,17 @@ function mergePageContent(candidates, webContents) {
5075
5155
  headings: mergedBase.headings,
5076
5156
  metaTags: mergedBase.metaTags
5077
5157
  });
5158
+ const pageSchema = inferPageSchema({
5159
+ ...mergedBase,
5160
+ structuredData: normalizedStructuredData,
5161
+ title: mergedBase.title || webContents.getTitle() || "",
5162
+ url: mergedBase.url || webContents.getURL() || ""
5163
+ });
5078
5164
  return {
5079
5165
  ...mergedBase,
5080
5166
  structuredData: normalizedStructuredData,
5081
5167
  pageIssues,
5168
+ pageSchema,
5082
5169
  title: mergedBase.title || webContents.getTitle() || "",
5083
5170
  url: mergedBase.url || webContents.getURL() || ""
5084
5171
  };
@@ -5104,13 +5191,12 @@ async function estimateExtractionTimeout(webContents) {
5104
5191
  }
5105
5192
  async function extractContentInner(webContents) {
5106
5193
  await waitForDomReady(webContents);
5107
- const [preloadResult, directResult, safeResult] = await Promise.all([
5194
+ const [preloadResult, directResult] = await Promise.all([
5108
5195
  executeScript(webContents, PRELOAD_EXTRACTION_SCRIPT),
5109
- executeScript(webContents, DIRECT_EXTRACTION_SCRIPT),
5110
- executeScript(webContents, SAFE_EXTRACTION_SCRIPT)
5196
+ executeScript(webContents, DIRECT_EXTRACTION_SCRIPT)
5111
5197
  ]);
5112
5198
  return mergePageContent(
5113
- [preloadResult, directResult, safeResult],
5199
+ [preloadResult, directResult],
5114
5200
  webContents
5115
5201
  );
5116
5202
  }
@@ -5167,7 +5253,8 @@ function normalizePageContent(value) {
5167
5253
  rdfa: Array.isArray(page.rdfa) ? page.rdfa : [],
5168
5254
  metaTags: page.metaTags && typeof page.metaTags === "object" && !Array.isArray(page.metaTags) ? page.metaTags : {},
5169
5255
  structuredData: Array.isArray(page.structuredData) ? page.structuredData : [],
5170
- pageIssues: Array.isArray(page.pageIssues) ? page.pageIssues : []
5256
+ pageIssues: Array.isArray(page.pageIssues) ? page.pageIssues : [],
5257
+ pageSchema: page.pageSchema
5171
5258
  };
5172
5259
  }
5173
5260
  const latestPageDiffs = /* @__PURE__ */ new Map();
@@ -5176,6 +5263,22 @@ const pendingPageSnapshotTimers = /* @__PURE__ */ new Map();
5176
5263
  const pendingPageSnapshotDueAt = /* @__PURE__ */ new Map();
5177
5264
  const lastMutationSnapshotAt = /* @__PURE__ */ new Map();
5178
5265
  const lastMutationActivityAt = /* @__PURE__ */ new Map();
5266
+ const destroyListenerAttached = /* @__PURE__ */ new WeakSet();
5267
+ function cleanupTimersForWcId(wcId) {
5268
+ const timer = pendingPageSnapshotTimers.get(wcId);
5269
+ if (timer) clearTimeout(timer);
5270
+ pendingPageSnapshotTimers.delete(wcId);
5271
+ pendingPageSnapshotDueAt.delete(wcId);
5272
+ lastMutationSnapshotAt.delete(wcId);
5273
+ lastMutationActivityAt.delete(wcId);
5274
+ }
5275
+ function attachDestroyCleanup(wc) {
5276
+ if (destroyListenerAttached.has(wc)) return;
5277
+ destroyListenerAttached.add(wc);
5278
+ wc.once("destroyed", () => {
5279
+ cleanupTimersForWcId(wc.id);
5280
+ });
5281
+ }
5179
5282
  const MIN_MUTATION_CAPTURE_INTERVAL_MS = 5e3;
5180
5283
  const SETTLE_AFTER_ACTIVITY_MS = 1500;
5181
5284
  function getLatestPageDiff(rawUrl) {
@@ -5222,6 +5325,7 @@ async function capturePageSnapshot(url, wc, sendToRendererViews) {
5222
5325
  sendToRendererViews(Channels.PAGE_CHANGED, enrichedDiff);
5223
5326
  } else {
5224
5327
  latestPageDiffs.delete(key);
5328
+ recentPageDiffBursts.delete(key);
5225
5329
  }
5226
5330
  } else {
5227
5331
  latestPageDiffs.delete(key);
@@ -5239,12 +5343,12 @@ function computeNextSnapshotDueAt(wcId, now, delayMs) {
5239
5343
  return Math.max(now + delayMs, earliestAllowedAt, stableAfterActivityAt);
5240
5344
  }
5241
5345
  function scheduleTimerAt(wc, sendToRendererViews, dueAt) {
5346
+ attachDestroyCleanup(wc);
5242
5347
  const wcId = wc.id;
5243
5348
  const existing = pendingPageSnapshotTimers.get(wcId);
5244
5349
  if (existing) clearTimeout(existing);
5245
5350
  const timer = setTimeout(() => {
5246
- pendingPageSnapshotTimers.delete(wcId);
5247
- pendingPageSnapshotDueAt.delete(wcId);
5351
+ cleanupTimersForWcId(wcId);
5248
5352
  if (wc.isDestroyed()) return;
5249
5353
  lastMutationSnapshotAt.set(wcId, Date.now());
5250
5354
  void capturePageSnapshot(wc.getURL(), wc, sendToRendererViews);
@@ -5568,9 +5672,9 @@ function resizeSidebarViews(state2) {
5568
5672
  const contentWidth = width - sidebarWidth;
5569
5673
  sidebarView.setBounds({
5570
5674
  x: width - sidebarWidth - resizeHandleOverlap,
5571
- y: 0,
5675
+ y: chromeHeight,
5572
5676
  width: sidebarWidth + resizeHandleOverlap,
5573
- height
5677
+ height: height - chromeHeight
5574
5678
  });
5575
5679
  if (uiState.devtoolsPanelOpen) {
5576
5680
  devtoolsPanelView.setBounds({
@@ -9733,7 +9837,17 @@ const TOOL_DEFINITIONS = [
9733
9837
  folderSummary: zod.z.string().optional().describe("Optional summary used if a new folder is created"),
9734
9838
  createFolderIfMissing: zod.z.boolean().optional().describe("Create folderName automatically when it does not exist"),
9735
9839
  note: zod.z.string().optional().describe("Optional note about why the page was saved"),
9736
- onDuplicate: zod.z.enum(["ask", "update", "duplicate"]).optional().describe("How to handle duplicate URLs in the same folder")
9840
+ onDuplicate: zod.z.enum(["ask", "update", "duplicate"]).optional().describe("How to handle duplicate URLs in the same folder"),
9841
+ intent: zod.z.string().optional().describe(
9842
+ "Human-readable description of what this bookmark is for (e.g. 'expense reporting')"
9843
+ ),
9844
+ expectedContent: zod.z.string().optional().describe(
9845
+ "Brief description of the content the agent should expect to find here"
9846
+ ),
9847
+ keyFields: zod.z.array(zod.z.string()).optional().describe(
9848
+ "Important form field names for this page (e.g. ['receipt_id', 'date', 'amount'])"
9849
+ ),
9850
+ agentHints: zod.z.record(zod.z.string(), zod.z.string()).optional().describe("Arbitrary key-value hints for the agent")
9737
9851
  },
9738
9852
  tier: 1
9739
9853
  },
@@ -9752,7 +9866,11 @@ const TOOL_DEFINITIONS = [
9752
9866
  folderSummary: zod.z.string().optional().describe("Optional summary for new folder"),
9753
9867
  createFolderIfMissing: zod.z.boolean().optional().describe("Create folderName automatically when it does not exist"),
9754
9868
  note: zod.z.string().optional().describe("Optional note"),
9755
- archive: zod.z.boolean().optional().describe('If true, organize into the default "Archive" folder')
9869
+ archive: zod.z.boolean().optional().describe('If true, organize into the default "Archive" folder'),
9870
+ intent: zod.z.string().optional().describe("Human-readable description of what this bookmark is for"),
9871
+ expectedContent: zod.z.string().optional().describe("Brief description of content the agent should expect"),
9872
+ keyFields: zod.z.array(zod.z.string()).optional().describe("Important form field names for this page"),
9873
+ agentHints: zod.z.record(zod.z.string(), zod.z.string()).optional().describe("Arbitrary key-value hints for the agent")
9756
9874
  },
9757
9875
  tier: 2
9758
9876
  },
@@ -10324,7 +10442,9 @@ const FIELD_WEIGHTS = {
10324
10442
  note: 5,
10325
10443
  folder: 3,
10326
10444
  folderSummary: 2,
10327
- url: 1
10445
+ url: 1,
10446
+ intent: 4,
10447
+ expectedContent: 3
10328
10448
  };
10329
10449
  function normalizeBookmarkSearchText(value) {
10330
10450
  let normalized = value.toLowerCase();
@@ -10355,7 +10475,9 @@ function getBookmarkSearchMatch(args) {
10355
10475
  url: args.url,
10356
10476
  note: args.note,
10357
10477
  folder: args.folder,
10358
- folderSummary: args.folderSummary
10478
+ folderSummary: args.folderSummary,
10479
+ intent: args.intent,
10480
+ expectedContent: args.expectedContent
10359
10481
  };
10360
10482
  for (const field of Object.keys(values)) {
10361
10483
  if (!bookmarkFieldMatchesQuery(values[field], normalizedQuery, tokens)) {
@@ -10495,7 +10617,9 @@ function searchBookmarks(query) {
10495
10617
  url: bookmark.url,
10496
10618
  note: bookmark.note,
10497
10619
  folder: folder?.name,
10498
- folderSummary: folder?.summary
10620
+ folderSummary: folder?.summary,
10621
+ intent: bookmark.intent,
10622
+ expectedContent: bookmark.expectedContent
10499
10623
  });
10500
10624
  return {
10501
10625
  bookmark,
@@ -10575,6 +10699,9 @@ function saveBookmarkWithPolicy(url, title, folderId, note, options) {
10575
10699
  if (note !== void 0) {
10576
10700
  bookmark2.note = note.trim() || void 0;
10577
10701
  }
10702
+ if (options?.extra) {
10703
+ Object.assign(bookmark2, options.extra);
10704
+ }
10578
10705
  bookmark2.savedAt = (/* @__PURE__ */ new Date()).toISOString();
10579
10706
  save();
10580
10707
  emit();
@@ -10590,7 +10717,8 @@ function saveBookmarkWithPolicy(url, title, folderId, note, options) {
10590
10717
  title: normalizedTitle,
10591
10718
  note: note?.trim() || void 0,
10592
10719
  folderId: targetId,
10593
- savedAt: (/* @__PURE__ */ new Date()).toISOString()
10720
+ savedAt: (/* @__PURE__ */ new Date()).toISOString(),
10721
+ ...options?.extra
10594
10722
  };
10595
10723
  state$1.bookmarks.push(bookmark);
10596
10724
  save();
@@ -10626,6 +10754,21 @@ function updateBookmark(id, updates) {
10626
10754
  if (typeof updates.folderId === "string") {
10627
10755
  bookmark.folderId = updates.folderId && updates.folderId !== UNSORTED_ID ? state$1.folders.find((item) => item.id === updates.folderId)?.id ?? UNSORTED_ID : UNSORTED_ID;
10628
10756
  }
10757
+ if (typeof updates.intent === "string") {
10758
+ bookmark.intent = updates.intent.trim() || void 0;
10759
+ }
10760
+ if (typeof updates.expectedContent === "string") {
10761
+ bookmark.expectedContent = updates.expectedContent.trim() || void 0;
10762
+ }
10763
+ if (updates.keyFields !== void 0) {
10764
+ bookmark.keyFields = updates.keyFields;
10765
+ }
10766
+ if (updates.pageSchema !== void 0) {
10767
+ bookmark.pageSchema = updates.pageSchema;
10768
+ }
10769
+ if (updates.agentHints !== void 0) {
10770
+ bookmark.agentHints = updates.agentHints;
10771
+ }
10629
10772
  save();
10630
10773
  emit();
10631
10774
  return { ...bookmark };
@@ -15530,7 +15673,17 @@ ${truncated}`;
15530
15673
  source.title,
15531
15674
  target.folderId,
15532
15675
  note,
15533
- { onDuplicate }
15676
+ {
15677
+ onDuplicate,
15678
+ extra: {
15679
+ intent: typeof args.intent === "string" && args.intent.trim() ? args.intent.trim() : void 0,
15680
+ expectedContent: typeof args.expectedContent === "string" && args.expectedContent.trim() ? args.expectedContent.trim() : void 0,
15681
+ keyFields: Array.isArray(args.keyFields) ? args.keyFields.filter(
15682
+ (f) => typeof f === "string"
15683
+ ) : void 0,
15684
+ agentHints: args.agentHints && typeof args.agentHints === "object" ? args.agentHints : void 0
15685
+ }
15686
+ }
15534
15687
  );
15535
15688
  if (result2.status === "conflict" && result2.existing) {
15536
15689
  return composeFolderAwareResponse$1(
@@ -15569,7 +15722,13 @@ ${truncated}`;
15569
15722
  const updated = updateBookmark(existing.id, {
15570
15723
  folderId: target.folderId,
15571
15724
  title: typeof args.title === "string" && args.title.trim() ? args.title.trim() : void 0,
15572
- note
15725
+ note,
15726
+ intent: typeof args.intent === "string" && args.intent.trim() ? args.intent.trim() : void 0,
15727
+ expectedContent: typeof args.expectedContent === "string" && args.expectedContent.trim() ? args.expectedContent.trim() : void 0,
15728
+ keyFields: Array.isArray(args.keyFields) ? args.keyFields.filter(
15729
+ (f) => typeof f === "string"
15730
+ ) : void 0,
15731
+ agentHints: args.agentHints && typeof args.agentHints === "object" ? args.agentHints : void 0
15573
15732
  });
15574
15733
  if (!updated) {
15575
15734
  return `Bookmark ${existing.id} not found`;
@@ -15580,12 +15739,25 @@ ${truncated}`;
15580
15739
  );
15581
15740
  }
15582
15741
  if ("error" in source) return `Error: ${source.error}`;
15583
- const bookmark = saveBookmark(
15742
+ const result2 = saveBookmarkWithPolicy(
15584
15743
  source.url,
15585
15744
  source.title,
15586
15745
  target.folderId,
15587
- note
15746
+ note,
15747
+ {
15748
+ onDuplicate: "update",
15749
+ extra: {
15750
+ intent: typeof args.intent === "string" && args.intent.trim() ? args.intent.trim() : void 0,
15751
+ expectedContent: typeof args.expectedContent === "string" && args.expectedContent.trim() ? args.expectedContent.trim() : void 0,
15752
+ keyFields: Array.isArray(args.keyFields) ? args.keyFields.filter(
15753
+ (f) => typeof f === "string"
15754
+ ) : void 0,
15755
+ agentHints: args.agentHints && typeof args.agentHints === "object" ? args.agentHints : void 0
15756
+ }
15757
+ }
15588
15758
  );
15759
+ const bookmark = result2.bookmark;
15760
+ if (!bookmark) return "Error: Bookmark save failed";
15589
15761
  return composeFolderAwareResponse$1(
15590
15762
  `Saved and organized "${bookmark.title}" (${bookmark.url}) into "${describeFolder$1(bookmark.folderId)}" (id=${bookmark.id})`,
15591
15763
  target.createdFolder
@@ -22770,7 +22942,7 @@ function registerWindowControlHandlers(mainWindow) {
22770
22942
  });
22771
22943
  }
22772
22944
  let activeChatProvider = null;
22773
- const VALID_APPROVAL_MODES = /* @__PURE__ */ new Set(["auto", "confirm-dangerous", "manual"]);
22945
+ const VALID_APPROVAL_MODES = ["auto", "confirm-dangerous", "manual"];
22774
22946
  function registerIpcHandlers(windowState, runtime2) {
22775
22947
  const { tabManager, chromeView, sidebarView, devtoolsPanelView, mainWindow } = windowState;
22776
22948
  let sidebarResizeRecoveryTimer = null;
@@ -22779,10 +22951,9 @@ function registerIpcHandlers(windowState, runtime2) {
22779
22951
  let pendingRuntimeState = null;
22780
22952
  const premiumApiOrigin = process.env.VESSEL_PREMIUM_API ? new URL(process.env.VESSEL_PREMIUM_API).origin : "https://vesselpremium.quantaintellect.com";
22781
22953
  const clearSidebarResizeRecoveryTimer = () => {
22782
- if (sidebarResizeRecoveryTimer) {
22783
- clearTimeout(sidebarResizeRecoveryTimer);
22784
- sidebarResizeRecoveryTimer = null;
22785
- }
22954
+ if (!sidebarResizeRecoveryTimer) return;
22955
+ clearTimeout(sidebarResizeRecoveryTimer);
22956
+ sidebarResizeRecoveryTimer = null;
22786
22957
  };
22787
22958
  const restoreSidebarLayoutAfterResize = () => {
22788
22959
  clearSidebarResizeRecoveryTimer();
@@ -22820,6 +22991,13 @@ function registerIpcHandlers(windowState, runtime2) {
22820
22991
  flushRuntimeUpdate();
22821
22992
  }, 32);
22822
22993
  };
22994
+ electron.app.on("before-quit", () => {
22995
+ if (runtimeUpdateTimer) {
22996
+ clearTimeout(runtimeUpdateTimer);
22997
+ runtimeUpdateTimer = null;
22998
+ }
22999
+ flushRuntimeUpdate();
23000
+ });
22823
23001
  const sendToRendererViews = (channel, ...args) => {
22824
23002
  chromeView.webContents.send(channel, ...args);
22825
23003
  sidebarView.webContents.send(channel, ...args);
@@ -22944,6 +23122,14 @@ function registerIpcHandlers(windowState, runtime2) {
22944
23122
  electron.ipcMain.handle(Channels.TAB_RELOAD, (_, id) => {
22945
23123
  tabManager.reloadTab(id);
22946
23124
  });
23125
+ electron.ipcMain.handle(Channels.TAB_TOGGLE_AD_BLOCK, (_, id) => {
23126
+ assertString(id, "id");
23127
+ const tab = tabManager.getTab(id);
23128
+ if (!tab) return null;
23129
+ const newState = !tab.state.adBlockingEnabled;
23130
+ tab.setAdBlockingEnabled(newState);
23131
+ return newState;
23132
+ });
22947
23133
  electron.ipcMain.handle(Channels.TAB_STATE_GET, () => ({
22948
23134
  tabs: tabManager.getAllStates(),
22949
23135
  activeId: tabManager.getActiveTabId() || ""
@@ -23042,7 +23228,15 @@ function registerIpcHandlers(windowState, runtime2) {
23042
23228
  sidebarResizeActive = true;
23043
23229
  clearSidebarResizeRecoveryTimer();
23044
23230
  const [width, height] = windowState.mainWindow.getContentSize();
23045
- windowState.sidebarView.setBounds({ x: 0, y: 0, width, height });
23231
+ const chromeHeight = windowState.uiState.focusMode ? 0 : 110;
23232
+ const sidebarWidth = windowState.uiState.sidebarWidth;
23233
+ const resizeHandleOverlap = 6;
23234
+ windowState.sidebarView.setBounds({
23235
+ x: width - sidebarWidth - resizeHandleOverlap,
23236
+ y: chromeHeight,
23237
+ width: sidebarWidth + resizeHandleOverlap,
23238
+ height: height - chromeHeight
23239
+ });
23046
23240
  scheduleSidebarResizeRecovery();
23047
23241
  });
23048
23242
  electron.ipcMain.handle(Channels.SIDEBAR_RESIZE, (_, width) => {
@@ -23050,7 +23244,6 @@ function registerIpcHandlers(windowState, runtime2) {
23050
23244
  const clamped = Math.max(240, Math.min(800, Math.round(width)));
23051
23245
  windowState.uiState.sidebarWidth = clamped;
23052
23246
  resizeSidebarViews(windowState);
23053
- scheduleSidebarResizeRecovery();
23054
23247
  return clamped;
23055
23248
  });
23056
23249
  electron.ipcMain.handle(Channels.SIDEBAR_RESIZE_COMMIT, () => {
@@ -23109,7 +23302,7 @@ function registerIpcHandlers(windowState, runtime2) {
23109
23302
  Channels.AGENT_SET_APPROVAL_MODE,
23110
23303
  (_, mode) => {
23111
23304
  assertString(mode, "mode");
23112
- if (!VALID_APPROVAL_MODES.has(mode)) {
23305
+ if (!VALID_APPROVAL_MODES.includes(mode)) {
23113
23306
  throw new Error(`Invalid approval mode: ${mode}`);
23114
23307
  }
23115
23308
  trackApprovalModeChanged(mode);
@@ -23149,9 +23342,16 @@ function registerIpcHandlers(windowState, runtime2) {
23149
23342
  );
23150
23343
  electron.ipcMain.handle(
23151
23344
  Channels.BOOKMARK_SAVE,
23152
- (_, url, title, folderId, note) => {
23345
+ (_, url, title, folderId, note, intent, expectedContent, keyFields, agentHints) => {
23153
23346
  trackBookmarkAction("save");
23154
- return saveBookmark(url, title, folderId, note);
23347
+ return saveBookmarkWithPolicy(url, title, folderId, note, {
23348
+ extra: {
23349
+ intent: intent?.trim() || void 0,
23350
+ expectedContent: expectedContent?.trim() || void 0,
23351
+ keyFields,
23352
+ agentHints
23353
+ }
23354
+ });
23155
23355
  }
23156
23356
  );
23157
23357
  electron.ipcMain.handle(Channels.BOOKMARK_REMOVE, (_, id) => {
@@ -23254,17 +23454,31 @@ function registerIpcHandlers(windowState, runtime2) {
23254
23454
  }
23255
23455
  });
23256
23456
  let findWiredWcId = null;
23457
+ let findResultListener = null;
23257
23458
  function wireFindEvents(wc) {
23258
23459
  if (findWiredWcId === wc.id) return;
23259
- if (findWiredWcId !== null) {
23460
+ if (findWiredWcId !== null && findResultListener) {
23260
23461
  const prev = tabManager.findTabByWebContentsId(findWiredWcId);
23261
- if (prev) prev.view.webContents.removeAllListeners("found-in-page");
23462
+ const prevWc = prev?.view.webContents;
23463
+ if (prevWc && !prevWc.isDestroyed()) {
23464
+ prevWc.removeListener("found-in-page", findResultListener);
23465
+ }
23262
23466
  }
23263
23467
  findWiredWcId = wc.id;
23264
- wc.on("found-in-page", (_event, result) => {
23468
+ if (wc.isDestroyed()) return;
23469
+ const listener = (_event, result) => {
23265
23470
  if (!chromeView.webContents.isDestroyed()) {
23266
23471
  chromeView.webContents.send(Channels.FIND_IN_PAGE_RESULT, result);
23267
23472
  }
23473
+ };
23474
+ findResultListener = listener;
23475
+ wc.on("found-in-page", listener);
23476
+ const capturedWcId = wc.id;
23477
+ wc.once("destroyed", () => {
23478
+ if (findWiredWcId === capturedWcId) {
23479
+ findWiredWcId = null;
23480
+ findResultListener = null;
23481
+ }
23268
23482
  });
23269
23483
  }
23270
23484
  electron.ipcMain.handle(Channels.FIND_IN_PAGE_START, (_, text, options) => {
@@ -23283,6 +23497,7 @@ function registerIpcHandlers(windowState, runtime2) {
23283
23497
  if (!tab) return null;
23284
23498
  const wc = tab.view.webContents;
23285
23499
  if (wc.isDestroyed()) return null;
23500
+ wireFindEvents(wc);
23286
23501
  return wc.findInPage("", { forward: forward ?? true, findNext: true });
23287
23502
  });
23288
23503
  electron.ipcMain.handle(Channels.FIND_IN_PAGE_STOP, (_, action) => {
@@ -23368,9 +23583,20 @@ function registerIpcHandlers(windowState, runtime2) {
23368
23583
  sendToRendererViews(Channels.PREMIUM_UPDATE, state2);
23369
23584
  return state2;
23370
23585
  });
23586
+ const PREMIUM_TRACKABLE_STEPS = [
23587
+ "chat_banner_viewed",
23588
+ "chat_banner_clicked",
23589
+ "settings_banner_viewed",
23590
+ "settings_banner_clicked",
23591
+ "welcome_banner_clicked",
23592
+ "premium_gate_seen",
23593
+ "premium_gate_clicked",
23594
+ "iteration_limit_seen",
23595
+ "iteration_limit_clicked"
23596
+ ];
23371
23597
  electron.ipcMain.handle(Channels.PREMIUM_TRACK_CONTEXT, (_, step) => {
23372
23598
  assertString(step, "step");
23373
- if (step === "chat_banner_viewed" || step === "chat_banner_clicked" || step === "settings_banner_viewed" || step === "settings_banner_clicked" || step === "welcome_banner_clicked" || step === "premium_gate_seen" || step === "premium_gate_clicked" || step === "iteration_limit_seen" || step === "iteration_limit_clicked") {
23599
+ if (PREMIUM_TRACKABLE_STEPS.includes(step)) {
23374
23600
  trackPremiumFunnel(step);
23375
23601
  }
23376
23602
  });
@@ -23382,6 +23608,21 @@ function registerIpcHandlers(windowState, runtime2) {
23382
23608
  }
23383
23609
  return result;
23384
23610
  });
23611
+ electron.ipcMain.handle(Channels.SESSION_LIST, () => {
23612
+ return listNamedSessions();
23613
+ });
23614
+ electron.ipcMain.handle(Channels.SESSION_SAVE, async (_, name) => {
23615
+ assertString(name, "name");
23616
+ return await saveNamedSession(tabManager, name);
23617
+ });
23618
+ electron.ipcMain.handle(Channels.SESSION_LOAD, async (_, name) => {
23619
+ assertString(name, "name");
23620
+ return await loadNamedSession(tabManager, name);
23621
+ });
23622
+ electron.ipcMain.handle(Channels.SESSION_DELETE, (_, name) => {
23623
+ assertString(name, "name");
23624
+ return deleteNamedSession(name);
23625
+ });
23385
23626
  registerVaultHandlers();
23386
23627
  registerWindowControlHandlers(mainWindow);
23387
23628
  electron.ipcMain.handle(Channels.AUTOMATION_GET_INSTALLED, () => {