@quanta-intellect/vessel-browser 0.1.44 → 0.1.46

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
@@ -32,12 +32,14 @@ const defaults = {
32
32
  premium: {
33
33
  status: "free",
34
34
  customerId: "",
35
+ verificationToken: "",
35
36
  email: "",
36
37
  validatedAt: "",
37
38
  expiresAt: ""
38
39
  }
39
40
  };
40
41
  const SAVE_DEBOUNCE_MS$3 = 150;
42
+ const CHAT_PROVIDER_SECRET_FILENAME = "vessel-chat-provider-secret";
41
43
  const SETTABLE_KEYS = new Set(Object.keys(defaults));
42
44
  let settings = null;
43
45
  let settingsIssues = [];
@@ -52,6 +54,80 @@ function getUserDataPath() {
52
54
  function getSettingsPath() {
53
55
  return path.join(getUserDataPath(), "vessel-settings.json");
54
56
  }
57
+ function getChatProviderSecretPath() {
58
+ return path.join(getUserDataPath(), CHAT_PROVIDER_SECRET_FILENAME);
59
+ }
60
+ function canUseSafeStorage() {
61
+ try {
62
+ return electron.safeStorage.isEncryptionAvailable();
63
+ } catch {
64
+ return false;
65
+ }
66
+ }
67
+ function readStoredProviderSecret() {
68
+ try {
69
+ const raw = fs.readFileSync(getChatProviderSecretPath());
70
+ const decoded = canUseSafeStorage() && electron.safeStorage.decryptString ? electron.safeStorage.decryptString(raw) : raw.toString("utf-8");
71
+ const parsed = JSON.parse(decoded);
72
+ if (parsed && typeof parsed === "object" && typeof parsed.providerId === "string" && typeof parsed.apiKey === "string") {
73
+ return parsed;
74
+ }
75
+ } catch {
76
+ }
77
+ return null;
78
+ }
79
+ function writeStoredProviderSecret(secret) {
80
+ const filePath = getChatProviderSecretPath();
81
+ fs.mkdirSync(path.dirname(filePath), { recursive: true });
82
+ const payload = JSON.stringify(secret);
83
+ if (canUseSafeStorage()) {
84
+ const encrypted = electron.safeStorage.encryptString(payload);
85
+ fs.writeFileSync(filePath, encrypted, { mode: 384 });
86
+ return;
87
+ }
88
+ fs.writeFileSync(filePath, payload, { mode: 384 });
89
+ }
90
+ function clearStoredProviderSecret() {
91
+ try {
92
+ fs.unlinkSync(getChatProviderSecretPath());
93
+ } catch {
94
+ }
95
+ }
96
+ function mergeChatProviderSecret(provider) {
97
+ if (!provider) return null;
98
+ const stored = readStoredProviderSecret();
99
+ const legacyApiKey = provider.apiKey?.trim() || "";
100
+ const apiKey = stored?.providerId === provider.id ? stored.apiKey : legacyApiKey;
101
+ if (legacyApiKey && stored?.providerId !== provider.id) {
102
+ writeStoredProviderSecret({ providerId: provider.id, apiKey: legacyApiKey });
103
+ }
104
+ return {
105
+ ...provider,
106
+ apiKey,
107
+ hasApiKey: Boolean(apiKey)
108
+ };
109
+ }
110
+ function buildPersistedSettings(source) {
111
+ return {
112
+ ...source,
113
+ chatProvider: source.chatProvider ? {
114
+ ...source.chatProvider,
115
+ apiKey: "",
116
+ hasApiKey: source.chatProvider.hasApiKey || Boolean(source.chatProvider.apiKey)
117
+ } : null
118
+ };
119
+ }
120
+ function getRendererSettings() {
121
+ const current = loadSettings();
122
+ return {
123
+ ...current,
124
+ chatProvider: current.chatProvider ? {
125
+ ...current.chatProvider,
126
+ apiKey: "",
127
+ hasApiKey: Boolean(current.chatProvider.apiKey)
128
+ } : null
129
+ };
130
+ }
55
131
  function getSettingsLoadIssues() {
56
132
  return settingsIssues.map((issue) => ({ ...issue }));
57
133
  }
@@ -80,6 +156,7 @@ function loadSettings() {
80
156
  settings = {
81
157
  ...defaults,
82
158
  ...parsed,
159
+ chatProvider: mergeChatProviderSecret(parsed.chatProvider ?? null),
83
160
  mcpPort: sanitizePort(parsed.mcpPort ?? defaults.mcpPort),
84
161
  agentTranscriptMode: parsed.agentTranscriptMode === "off" || parsed.agentTranscriptMode === "summary" || parsed.agentTranscriptMode === "full" ? parsed.agentTranscriptMode : parsed.showAgentTranscript === false ? "off" : defaults.agentTranscriptMode
85
162
  };
@@ -104,7 +181,10 @@ function persistNow$3() {
104
181
  saveTimer$3 = null;
105
182
  }
106
183
  return fs.promises.mkdir(path.dirname(getSettingsPath()), { recursive: true }).then(
107
- () => fs.promises.writeFile(getSettingsPath(), JSON.stringify(settings, null, 2))
184
+ () => fs.promises.writeFile(
185
+ getSettingsPath(),
186
+ JSON.stringify(buildPersistedSettings(settings), null, 2)
187
+ )
108
188
  ).catch((err) => console.error("[Vessel] Failed to save settings:", err));
109
189
  }
110
190
  function saveSettings() {
@@ -121,6 +201,30 @@ function setSetting(key, value) {
121
201
  loadSettings();
122
202
  if (key === "mcpPort") {
123
203
  settings.mcpPort = sanitizePort(value);
204
+ } else if (key === "chatProvider") {
205
+ const nextProvider = value;
206
+ if (!nextProvider) {
207
+ clearStoredProviderSecret();
208
+ settings.chatProvider = null;
209
+ } else {
210
+ const existingSecret = readStoredProviderSecret();
211
+ const incomingApiKey = nextProvider.apiKey.trim();
212
+ const preserveExisting = !incomingApiKey && nextProvider.hasApiKey === true && existingSecret?.providerId === nextProvider.id;
213
+ const resolvedApiKey = preserveExisting ? existingSecret?.apiKey || "" : incomingApiKey;
214
+ if (resolvedApiKey) {
215
+ writeStoredProviderSecret({
216
+ providerId: nextProvider.id,
217
+ apiKey: resolvedApiKey
218
+ });
219
+ } else {
220
+ clearStoredProviderSecret();
221
+ }
222
+ settings.chatProvider = {
223
+ ...nextProvider,
224
+ apiKey: resolvedApiKey,
225
+ hasApiKey: Boolean(resolvedApiKey)
226
+ };
227
+ }
124
228
  } else {
125
229
  settings[key] = value;
126
230
  }
@@ -160,7 +264,31 @@ function checkDomainPolicy(url) {
160
264
  function matchesDomain(hostname, policyDomain) {
161
265
  return hostname === policyDomain || hostname.endsWith("." + policyDomain);
162
266
  }
267
+ const ALLOWED_SCHEMES = /* @__PURE__ */ new Set(["http:", "https:"]);
268
+ function isSafeNavigationURL(url) {
269
+ try {
270
+ const parsed = new URL(url);
271
+ return ALLOWED_SCHEMES.has(parsed.protocol);
272
+ } catch {
273
+ return false;
274
+ }
275
+ }
276
+ function assertSafeURL(url) {
277
+ if (!isSafeNavigationURL(url)) {
278
+ throw new Error(
279
+ `Blocked navigation to disallowed URL scheme: ${url.slice(0, 80)}`
280
+ );
281
+ }
282
+ }
283
+ function assertPermittedNavigationURL(url) {
284
+ assertSafeURL(url);
285
+ const policyError = checkDomainPolicy(url);
286
+ if (policyError) {
287
+ throw new Error(policyError);
288
+ }
289
+ }
163
290
  const MAX_CUSTOM_HISTORY = 50;
291
+ const READER_MODE_DATA_URL_PREFIX = "data:text/html;charset=utf-8,";
164
292
  class Tab {
165
293
  id;
166
294
  view;
@@ -181,6 +309,32 @@ class Tab {
181
309
  urlForwardStack = [];
182
310
  lastCommittedUrl = "";
183
311
  navigatingViaHistory = false;
312
+ isReaderModeDataUrl(url) {
313
+ return this._state.isReaderMode && url.startsWith(READER_MODE_DATA_URL_PREFIX);
314
+ }
315
+ getNavigationBlockReason(url) {
316
+ if (!url) {
317
+ return "Blocked navigation to empty URL";
318
+ }
319
+ if (url.startsWith("about:") || this.isReaderModeDataUrl(url)) {
320
+ return null;
321
+ }
322
+ try {
323
+ assertSafeURL(url);
324
+ } catch (error) {
325
+ return error instanceof Error ? error.message : "Blocked unsafe navigation";
326
+ }
327
+ return checkDomainPolicy(url);
328
+ }
329
+ guardedLoadURL(url, options) {
330
+ const blockReason = this.getNavigationBlockReason(url);
331
+ if (blockReason) {
332
+ console.warn(`[Tab] ${blockReason}`);
333
+ return blockReason;
334
+ }
335
+ void this.view.webContents.loadURL(url, options);
336
+ return null;
337
+ }
184
338
  constructor(id, url, onChange, options) {
185
339
  this.id = id;
186
340
  this.parentWindow = options?.parentWindow;
@@ -198,10 +352,11 @@ class Tab {
198
352
  nodeIntegration: false
199
353
  }
200
354
  });
355
+ const initialUrl = url || "about:blank";
201
356
  this._state = {
202
357
  id,
203
358
  title: "New Tab",
204
- url: url || "about:blank",
359
+ url: initialUrl,
205
360
  favicon: "",
206
361
  isLoading: false,
207
362
  canGoBack: false,
@@ -222,13 +377,22 @@ class Tab {
222
377
  });
223
378
  this.setupListeners();
224
379
  if (url) {
225
- this.lastCommittedUrl = url;
226
- this.view.webContents.loadURL(url);
380
+ const error = this.guardedLoadURL(url);
381
+ if (error) {
382
+ this._state.url = "about:blank";
383
+ } else {
384
+ this.lastCommittedUrl = url;
385
+ }
227
386
  }
228
387
  }
229
388
  setupListeners() {
230
389
  const wc = this.view.webContents;
231
390
  wc.setWindowOpenHandler(({ url, disposition }) => {
391
+ const error = this.getNavigationBlockReason(url);
392
+ if (error) {
393
+ console.warn(`[Tab] ${error}`);
394
+ return { action: "deny" };
395
+ }
232
396
  this.onOpenUrl?.({
233
397
  url,
234
398
  background: disposition === "background-tab",
@@ -236,6 +400,18 @@ class Tab {
236
400
  });
237
401
  return { action: "deny" };
238
402
  });
403
+ const blockNavigation = (event, url, context) => {
404
+ const error = this.getNavigationBlockReason(url);
405
+ if (!error) return;
406
+ event.preventDefault();
407
+ console.warn(`[Tab] ${context}: ${error}`);
408
+ };
409
+ wc.on("will-navigate", (event, url) => {
410
+ blockNavigation(event, url, "Blocked top-level navigation");
411
+ });
412
+ wc.on("will-redirect", (event, url) => {
413
+ blockNavigation(event, url, "Blocked redirect");
414
+ });
239
415
  const syncNavigationState = () => {
240
416
  this._state.title = wc.getTitle() || this._state.title || "New Tab";
241
417
  this._state.url = wc.getURL() || this._state.url;
@@ -402,17 +578,12 @@ class Tab {
402
578
  url = `https://duckduckgo.com/?q=${encodeURIComponent(url)}`;
403
579
  }
404
580
  }
405
- if (!/^https?:\/\//i.test(url) && !url.startsWith("about:")) {
406
- return `Blocked navigation to disallowed URL scheme: ${url.slice(0, 80)}`;
407
- }
408
- const policyError = checkDomainPolicy(url);
409
- if (policyError) return policyError;
410
581
  if (postBody) {
411
582
  const params = new URLSearchParams();
412
583
  for (const [key, value] of Object.entries(postBody)) {
413
584
  params.set(key, value);
414
585
  }
415
- this.view.webContents.loadURL(url, {
586
+ return this.guardedLoadURL(url, {
416
587
  method: "POST",
417
588
  extraHeaders: "Content-Type: application/x-www-form-urlencoded\r\n",
418
589
  postData: [
@@ -422,27 +593,39 @@ class Tab {
422
593
  }
423
594
  ]
424
595
  });
425
- } else {
426
- this.view.webContents.loadURL(url);
427
596
  }
428
- return null;
597
+ return this.guardedLoadURL(url);
429
598
  }
430
599
  goBack() {
431
600
  const previousUrl = this.urlHistory.pop();
432
601
  if (!previousUrl) return false;
602
+ const currentUrl = this.lastCommittedUrl;
433
603
  this.navigatingViaHistory = true;
434
- this.urlForwardStack.push(this.lastCommittedUrl);
604
+ this.urlForwardStack.push(currentUrl);
605
+ const error = this.guardedLoadURL(previousUrl);
606
+ if (error) {
607
+ this.navigatingViaHistory = false;
608
+ this.urlForwardStack.pop();
609
+ this.urlHistory.push(previousUrl);
610
+ return false;
611
+ }
435
612
  this.lastCommittedUrl = previousUrl;
436
- this.view.webContents.loadURL(previousUrl);
437
613
  return true;
438
614
  }
439
615
  goForward() {
440
616
  const nextUrl = this.urlForwardStack.pop();
441
617
  if (!nextUrl) return false;
618
+ const currentUrl = this.lastCommittedUrl;
442
619
  this.navigatingViaHistory = true;
443
- this.urlHistory.push(this.lastCommittedUrl);
620
+ this.urlHistory.push(currentUrl);
621
+ const error = this.guardedLoadURL(nextUrl);
622
+ if (error) {
623
+ this.navigatingViaHistory = false;
624
+ this.urlHistory.pop();
625
+ this.urlForwardStack.push(nextUrl);
626
+ return false;
627
+ }
444
628
  this.lastCommittedUrl = nextUrl;
445
- this.view.webContents.loadURL(nextUrl);
446
629
  return true;
447
630
  }
448
631
  canGoBack() {
@@ -2446,7 +2629,8 @@ const Channels = {
2446
2629
  DOWNLOAD_DONE: "download:done",
2447
2630
  // Premium
2448
2631
  PREMIUM_GET_STATE: "premium:get-state",
2449
- PREMIUM_ACTIVATE: "premium:activate",
2632
+ PREMIUM_ACTIVATION_START: "premium:activation-start",
2633
+ PREMIUM_ACTIVATION_VERIFY: "premium:activation-verify",
2450
2634
  PREMIUM_CHECKOUT: "premium:checkout",
2451
2635
  PREMIUM_PORTAL: "premium:portal",
2452
2636
  PREMIUM_RESET: "premium:reset",
@@ -3382,6 +3566,7 @@ function resetPremium() {
3382
3566
  const fresh = {
3383
3567
  status: "free",
3384
3568
  customerId: "",
3569
+ verificationToken: "",
3385
3570
  email: "",
3386
3571
  validatedAt: "",
3387
3572
  expiresAt: ""
@@ -3414,39 +3599,22 @@ async function getCheckoutUrl(email) {
3414
3599
  }
3415
3600
  }
3416
3601
  async function getPortalUrl() {
3417
- const { premium } = loadSettings();
3418
- if (!premium.customerId) {
3419
- return { ok: false, error: "No active subscription" };
3420
- }
3421
- try {
3422
- const res = await fetch(`${VERIFICATION_API}/portal`, {
3423
- method: "POST",
3424
- headers: { "Content-Type": "application/json" },
3425
- body: JSON.stringify({ customerId: premium.customerId })
3426
- });
3427
- if (!res.ok) {
3428
- return { ok: false, error: `HTTP ${res.status}` };
3429
- }
3430
- const { url } = await res.json();
3431
- return { ok: true, url };
3432
- } catch (err) {
3433
- return {
3434
- ok: false,
3435
- error: err instanceof Error ? err.message : "Failed to get portal URL"
3436
- };
3437
- }
3602
+ return {
3603
+ ok: false,
3604
+ error: "Billing portal access is temporarily disabled while we harden the premium API. Use the Stripe billing link from your subscription email for now."
3605
+ };
3438
3606
  }
3439
- async function verifySubscription(emailOrCustomerId) {
3607
+ async function verifySubscription(identifier) {
3440
3608
  const current = loadSettings().premium;
3441
- const identifier = emailOrCustomerId || current.customerId || current.email;
3442
- if (!identifier) {
3609
+ const verificationIdentifier = identifier || current.verificationToken || current.customerId;
3610
+ if (!verificationIdentifier) {
3443
3611
  return current;
3444
3612
  }
3445
3613
  try {
3446
3614
  const res = await fetch(`${VERIFICATION_API}/verify`, {
3447
3615
  method: "POST",
3448
3616
  headers: { "Content-Type": "application/json" },
3449
- body: JSON.stringify({ identifier })
3617
+ body: JSON.stringify({ identifier: verificationIdentifier })
3450
3618
  });
3451
3619
  if (!res.ok) {
3452
3620
  console.warn("[Vessel Premium] Verification API returned", res.status);
@@ -3455,8 +3623,9 @@ async function verifySubscription(emailOrCustomerId) {
3455
3623
  const data = await res.json();
3456
3624
  const updated = {
3457
3625
  status: data.status,
3458
- customerId: data.customerId,
3459
- email: data.email,
3626
+ customerId: data.customerId || current.customerId,
3627
+ verificationToken: data.verificationToken || verificationIdentifier,
3628
+ email: data.email || current.email,
3460
3629
  validatedAt: (/* @__PURE__ */ new Date()).toISOString(),
3461
3630
  expiresAt: data.expiresAt
3462
3631
  };
@@ -3467,34 +3636,104 @@ async function verifySubscription(emailOrCustomerId) {
3467
3636
  return current;
3468
3637
  }
3469
3638
  }
3470
- async function activateWithEmail(email) {
3471
- if (!email.trim()) {
3639
+ async function requestActivationCode(email) {
3640
+ const normalizedEmail = email.trim().toLowerCase();
3641
+ if (!normalizedEmail) {
3642
+ return { ok: false, error: "Email is required" };
3643
+ }
3644
+ try {
3645
+ const res = await fetch(`${VERIFICATION_API}/activate/start`, {
3646
+ method: "POST",
3647
+ headers: { "Content-Type": "application/json" },
3648
+ body: JSON.stringify({ email: normalizedEmail })
3649
+ });
3650
+ const data = await res.json().catch(() => ({}));
3651
+ if (!res.ok || !data.challengeToken) {
3652
+ return {
3653
+ ok: false,
3654
+ error: data.error || `HTTP ${res.status}`
3655
+ };
3656
+ }
3657
+ return {
3658
+ ok: true,
3659
+ email: normalizedEmail,
3660
+ challengeToken: data.challengeToken
3661
+ };
3662
+ } catch (err) {
3663
+ return {
3664
+ ok: false,
3665
+ error: err instanceof Error ? err.message : "Failed to send code"
3666
+ };
3667
+ }
3668
+ }
3669
+ async function verifyActivationCode(email, code, challengeToken) {
3670
+ const normalizedEmail = email.trim().toLowerCase();
3671
+ const trimmedCode = code.trim();
3672
+ if (!normalizedEmail) {
3472
3673
  return { ok: false, state: getPremiumState(), error: "Email is required" };
3473
3674
  }
3474
- const state2 = await verifySubscription(email.trim());
3475
- if (state2.status === "active" || state2.status === "trialing") {
3476
- return { ok: true, state: state2 };
3675
+ if (!trimmedCode) {
3676
+ return { ok: false, state: getPremiumState(), error: "Code is required" };
3677
+ }
3678
+ if (!challengeToken.trim()) {
3679
+ return {
3680
+ ok: false,
3681
+ state: getPremiumState(),
3682
+ error: "Request a new activation code and try again."
3683
+ };
3684
+ }
3685
+ try {
3686
+ const res = await fetch(`${VERIFICATION_API}/activate/verify`, {
3687
+ method: "POST",
3688
+ headers: { "Content-Type": "application/json" },
3689
+ body: JSON.stringify({
3690
+ email: normalizedEmail,
3691
+ code: trimmedCode,
3692
+ challengeToken: challengeToken.trim()
3693
+ })
3694
+ });
3695
+ const data = await res.json().catch(() => ({}));
3696
+ if (!res.ok) {
3697
+ return {
3698
+ ok: false,
3699
+ state: getPremiumState(),
3700
+ error: data.error || `HTTP ${res.status}`
3701
+ };
3702
+ }
3703
+ const updated = {
3704
+ status: data.status ?? "free",
3705
+ customerId: data.customerId || "",
3706
+ verificationToken: data.verificationToken || "",
3707
+ email: data.email || normalizedEmail,
3708
+ validatedAt: (/* @__PURE__ */ new Date()).toISOString(),
3709
+ expiresAt: data.expiresAt || ""
3710
+ };
3711
+ setSetting("premium", updated);
3712
+ return { ok: isPremiumActiveState(updated), state: updated };
3713
+ } catch (err) {
3714
+ return {
3715
+ ok: false,
3716
+ state: getPremiumState(),
3717
+ error: err instanceof Error ? err.message : "Failed to verify code"
3718
+ };
3477
3719
  }
3478
- return {
3479
- ok: false,
3480
- state: state2,
3481
- error: state2.status === "canceled" ? "Subscription is canceled. Resubscribe to continue." : state2.status === "past_due" ? "Subscription payment is past due. Update your payment method." : "No active subscription found for this email."
3482
- };
3483
3720
  }
3484
3721
  let revalidationTimer = null;
3485
3722
  function startBackgroundRevalidation() {
3486
3723
  if (revalidationTimer) return;
3487
3724
  const { premium } = loadSettings();
3488
- if (premium.customerId || premium.email) {
3725
+ const identifier = premium.verificationToken || premium.customerId;
3726
+ if (identifier) {
3489
3727
  const lastValidated = premium.validatedAt ? new Date(premium.validatedAt).getTime() : 0;
3490
3728
  if (Date.now() - lastValidated > REVALIDATION_INTERVAL_MS) {
3491
- void verifySubscription();
3729
+ void verifySubscription(identifier);
3492
3730
  }
3493
3731
  }
3494
3732
  revalidationTimer = setInterval(() => {
3495
3733
  const { premium: p } = loadSettings();
3496
- if (p.customerId || p.email) {
3497
- void verifySubscription();
3734
+ const currentIdentifier = p.verificationToken || p.customerId;
3735
+ if (currentIdentifier) {
3736
+ void verifySubscription(currentIdentifier);
3498
3737
  }
3499
3738
  }, REVALIDATION_INTERVAL_MS);
3500
3739
  }
@@ -4185,6 +4424,20 @@ const DIRECT_EXTRACTION_SCRIPT = String.raw`
4185
4424
  return meta;
4186
4425
  }
4187
4426
 
4427
+ function shouldExposeFieldValue(el) {
4428
+ if (!(el instanceof HTMLInputElement)) return false;
4429
+ var elType = (el.type || "").toLowerCase();
4430
+ if (elType !== "number") return false;
4431
+ var signals = [
4432
+ el.name,
4433
+ el.id,
4434
+ el.getAttribute("placeholder"),
4435
+ el.getAttribute("aria-label"),
4436
+ labelFor(el),
4437
+ ].filter(Boolean).join(" ").toLowerCase();
4438
+ return /\b(qty|quantity|count|items?)\b/.test(signals);
4439
+ }
4440
+
4188
4441
  function serializeInteractive(el, kind) {
4189
4442
  var base = {
4190
4443
  type: kind,
@@ -4219,7 +4472,6 @@ const DIRECT_EXTRACTION_SCRIPT = String.raw`
4219
4472
  return {
4220
4473
  ...base,
4221
4474
  label: labelFor(el)?.slice(0, 100),
4222
- value: text(el.value),
4223
4475
  options: 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),
4224
4476
  required: el.hasAttribute("required") || undefined,
4225
4477
  ...fieldMeta(el),
@@ -4231,7 +4483,6 @@ const DIRECT_EXTRACTION_SCRIPT = String.raw`
4231
4483
  ...base,
4232
4484
  label: labelFor(el)?.slice(0, 100),
4233
4485
  placeholder: text(el.getAttribute("placeholder")),
4234
- value: text(el.value),
4235
4486
  required: el.hasAttribute("required") || undefined,
4236
4487
  ...fieldMeta(el),
4237
4488
  };
@@ -4243,7 +4494,7 @@ const DIRECT_EXTRACTION_SCRIPT = String.raw`
4243
4494
  label: labelFor(el)?.slice(0, 100),
4244
4495
  inputType: text(el.getAttribute("type")),
4245
4496
  placeholder: text(el.getAttribute("placeholder")),
4246
- value: (elType === "password" || elType === "checkbox" || elType === "radio") ? undefined : text(el.value),
4497
+ value: shouldExposeFieldValue(el) ? text(el.value) : undefined,
4247
4498
  required: el.hasAttribute("required") || undefined,
4248
4499
  ...fieldMeta(el),
4249
4500
  };
@@ -4499,7 +4750,7 @@ const SAFE_EXTRACTION_SCRIPT = String.raw`
4499
4750
  label: labelFor(el)?.slice(0, 100),
4500
4751
  inputType: text(el.getAttribute && el.getAttribute("type")),
4501
4752
  placeholder: text(el.getAttribute && el.getAttribute("placeholder")),
4502
- value: tag === "select" ? text(el.value) : (elType === "password" || elType === "checkbox" || elType === "radio") ? undefined : text(el.value),
4753
+ value: shouldExposeFieldValue(el) ? text(el.value) : undefined,
4503
4754
  options: tag === "select"
4504
4755
  ? 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)
4505
4756
  : undefined,
@@ -4752,11 +5003,13 @@ function normalizePageContent(value) {
4752
5003
  function generateReaderHTML(page) {
4753
5004
  const escapedTitle = escapeHtml(page.title);
4754
5005
  const escapedByline = escapeHtml(page.byline);
5006
+ const renderedContent = renderReaderContent(page);
4755
5007
  return `<!DOCTYPE html>
4756
5008
  <html lang="en">
4757
5009
  <head>
4758
5010
  <meta charset="utf-8">
4759
5011
  <meta name="viewport" content="width=device-width, initial-scale=1">
5012
+ <meta http-equiv="Content-Security-Policy" content="default-src 'none'; style-src 'unsafe-inline'; img-src data:; base-uri 'none'; form-action 'none'">
4760
5013
  <title>${escapedTitle}</title>
4761
5014
  <style>
4762
5015
  * { margin: 0; padding: 0; box-sizing: border-box; }
@@ -4827,13 +5080,20 @@ function generateReaderHTML(page) {
4827
5080
  <div class="reader-container">
4828
5081
  <h1>${escapedTitle}</h1>
4829
5082
  ${escapedByline ? `<div class="byline">${escapedByline}</div>` : ""}
4830
- <div class="reader-content">${page.htmlContent || escapeHtml(page.content)}</div>
5083
+ <div class="reader-content">${renderedContent}</div>
4831
5084
  </div>
4832
5085
  </body>
4833
5086
  </html>`;
4834
5087
  }
5088
+ function renderReaderContent(page) {
5089
+ const source = (page.content || page.excerpt || "").trim();
5090
+ if (!source) {
5091
+ return "<p>No readable content was available for this page.</p>";
5092
+ }
5093
+ return source.split(/\n{2,}/).map((block) => block.trim()).filter(Boolean).map((block) => `<p>${escapeHtml(block).replace(/\n/g, "<br>")}</p>`).join("\n");
5094
+ }
4835
5095
  function escapeHtml(str) {
4836
- return str.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;");
5096
+ return str.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;").replace(/'/g, "&#39;");
4837
5097
  }
4838
5098
  const mcpStatusChangeListeners = /* @__PURE__ */ new Set();
4839
5099
  const runtimeHealthChangeListeners = /* @__PURE__ */ new Set();
@@ -4927,24 +5187,24 @@ function getVaultPath() {
4927
5187
  function getKeyPath() {
4928
5188
  return path$1.join(getVaultDir(), KEY_FILENAME);
4929
5189
  }
5190
+ function assertVaultSecretStorageAvailable() {
5191
+ if (!electron.safeStorage.isEncryptionAvailable()) {
5192
+ throw new Error(
5193
+ "Agent Credential Vault requires OS-backed secret storage. Enable Keychain, DPAPI, or libsecret support and restart Vessel."
5194
+ );
5195
+ }
5196
+ }
4930
5197
  function getOrCreateEncryptionKey() {
5198
+ assertVaultSecretStorageAvailable();
4931
5199
  const keyPath = getKeyPath();
4932
5200
  if (fs$1.existsSync(keyPath)) {
4933
5201
  const encryptedKey = fs$1.readFileSync(keyPath);
4934
- if (electron.safeStorage.isEncryptionAvailable()) {
4935
- return electron.safeStorage.decryptString ? Buffer.from(electron.safeStorage.decryptString(encryptedKey), "utf-8") : encryptedKey;
4936
- }
4937
- return encryptedKey;
5202
+ return Buffer.from(electron.safeStorage.decryptString(encryptedKey), "utf-8");
4938
5203
  }
4939
5204
  const key = crypto$2.randomBytes(32);
4940
5205
  fs$1.mkdirSync(path$1.dirname(keyPath), { recursive: true });
4941
- if (electron.safeStorage.isEncryptionAvailable()) {
4942
- const encrypted = electron.safeStorage.encryptString(key.toString("utf-8"));
4943
- fs$1.writeFileSync(keyPath, encrypted);
4944
- } else {
4945
- fs$1.writeFileSync(keyPath, key);
4946
- fs$1.chmodSync(keyPath, 384);
4947
- }
5206
+ const encrypted = electron.safeStorage.encryptString(key.toString("utf-8"));
5207
+ fs$1.writeFileSync(keyPath, encrypted, { mode: 384 });
4948
5208
  return key;
4949
5209
  }
4950
5210
  function encrypt(plaintext) {
@@ -4985,8 +5245,9 @@ function loadVault() {
4985
5245
  return cachedEntries;
4986
5246
  } catch (err) {
4987
5247
  console.error("[Vessel Vault] Failed to load vault:", err);
4988
- cachedEntries = [];
4989
- return cachedEntries;
5248
+ throw new Error(
5249
+ "Could not unlock the Agent Credential Vault. Check that OS secret storage is available and that the stored vault key can be decrypted."
5250
+ );
4990
5251
  }
4991
5252
  }
4992
5253
  function saveVault(entries) {
@@ -5380,7 +5641,7 @@ function buildPhaseReminder(userMessage, assistantText) {
5380
5641
  const multiClickSelectionSignals = /i(?:'| a)?ll start by clicking on the following books|i will start by clicking on the following books|i will click on the following books|clicked on five different book titles|clicked on \d+ different book titles|clicking through the selected titles|click each of the selected titles/.test(
5381
5642
  text
5382
5643
  );
5383
- const staleSelectionSignals = /cannot locate the elements to click|page structure is not being reliably captured|specific titles failed|page may have changed|stale-index/.test(
5644
+ const staleSelectionSignals = /cannot locate the elements to click|page structure is not being reliably captured|specific titles failed|page may have changed|stale-index|no visible area|not visible/.test(
5384
5645
  text
5385
5646
  );
5386
5647
  const intermediateCartDialogSignals = /(added to cart|has been added to the cart|cart confirmation)/.test(text) && /(continue shopping|search results page|return to the search results page|back button|go back)/.test(
@@ -6728,6 +6989,10 @@ function limitItems(items, max = MAX_STRUCTURED_ITEMS) {
6728
6989
  if (items.length <= max) return items;
6729
6990
  return items.slice(0, max);
6730
6991
  }
6992
+ function shouldRenderFieldValue(el) {
6993
+ const value = typeof el.value === "string" && el.value.trim() ? el.value.trim() : "";
6994
+ return Boolean(value) && isQuantityLike(el);
6995
+ }
6731
6996
  function formatElementMeta(el) {
6732
6997
  const meta = [];
6733
6998
  if (el.context && el.context !== "content") {
@@ -6795,7 +7060,7 @@ function formatElementMeta(el) {
6795
7060
  if (el.description) {
6796
7061
  meta.push(`desc="${el.description.slice(0, 80)}"`);
6797
7062
  }
6798
- if (el.value !== void 0 && el.value !== null && el.value !== "") {
7063
+ if (shouldRenderFieldValue(el)) {
6799
7064
  meta.push(`value="${el.value.slice(0, 60)}"`);
6800
7065
  }
6801
7066
  if (el.selector) {
@@ -6806,7 +7071,7 @@ function formatElementMeta(el) {
6806
7071
  }
6807
7072
  function summarizeElementValue(el) {
6808
7073
  const value = typeof el.value === "string" && el.value.trim() ? el.value.trim() : "";
6809
- if (!value) return null;
7074
+ if (!value || !shouldRenderFieldValue(el)) return null;
6810
7075
  if (el.type === "select") {
6811
7076
  return { label: "selected", value: value.slice(0, 60) };
6812
7077
  }
@@ -8212,6 +8477,8 @@ const COMPACT_FOCUS_INSTRUCTIONS = [
8212
8477
  "When read_page or inspect_element gives you an element index, prefer click(index=N) over selector-based clicks.",
8213
8478
  'If a product page has no visible purchase control, scroll and call read_page(mode="visible_only") again. Do not loop on generic inspect_element calls against navigation or unrelated regions.',
8214
8479
  "After adding an item to cart and going back, ALWAYS call read_page to see the current results. The system shows which products are already in your cart — do NOT click those again. Pick a DIFFERENT product from the list. If all visible results are already in cart, scroll down for more.",
8480
+ 'On search results pages, always call read_page(mode="results_only") first. Click products by their [#N] index from the Results section. Never click filter or sort links (e.g. Used, New, Format, Price).',
8481
+ "After go_back, always call read_page before clicking. The page may have changed.",
8215
8482
  "Keep your reasoning short. Prefer taking the next tool action over writing a long plan."
8216
8483
  ];
8217
8484
  function buildInstructionBlock(instructions) {
@@ -8563,11 +8830,11 @@ function buildCompactScopedContext(page, mode, pageType = detectPageType(page))
8563
8830
  const primaryResultElements = getPrimaryResultLinks(page);
8564
8831
  const primaryResults = primaryResultElements.map(formatElement);
8565
8832
  if (primaryResults.length > 0) {
8566
- pushSection(
8567
- lines,
8568
- "### Primary Results",
8569
- primaryResults
8570
- );
8833
+ lines.push("");
8834
+ lines.push("### Results — click one of these to open a product");
8835
+ lines.push(...primaryResults.map((item) => `- ${item}`));
8836
+ lines.push("");
8837
+ lines.push("IMPORTANT: Use click(index=N) on a result above. Do NOT click filter or sort links.");
8571
8838
  }
8572
8839
  if (pageType === "FORM" || pageType === "LOGIN" || mode === "forms_only") {
8573
8840
  pushSection(
@@ -8583,7 +8850,7 @@ function buildCompactScopedContext(page, mode, pageType = detectPageType(page))
8583
8850
  ).map(formatElement);
8584
8851
  pushSection(
8585
8852
  lines,
8586
- "### Visible Controls",
8853
+ "### Page Controls (filters, sorts — avoid when selecting products)",
8587
8854
  visibleControls
8588
8855
  );
8589
8856
  }
@@ -10136,22 +10403,6 @@ function formatDeadLinkMessage(label, result) {
10136
10403
  const status = result.statusCode ? `HTTP ${result.statusCode}` : "dead link";
10137
10404
  return `Skipped stale link "${label}" because ${destination} returned ${status}. Try a different link or URL instead.`;
10138
10405
  }
10139
- const ALLOWED_SCHEMES = /* @__PURE__ */ new Set(["http:", "https:"]);
10140
- function isSafeNavigationURL(url) {
10141
- try {
10142
- const parsed = new URL(url);
10143
- return ALLOWED_SCHEMES.has(parsed.protocol);
10144
- } catch {
10145
- return false;
10146
- }
10147
- }
10148
- function assertSafeURL(url) {
10149
- if (!isSafeNavigationURL(url)) {
10150
- throw new Error(
10151
- `Blocked navigation to disallowed URL scheme: ${url.slice(0, 80)}`
10152
- );
10153
- }
10154
- }
10155
10406
  async function captureScreenshot(wc) {
10156
10407
  for (let attempt = 0; attempt < 3; attempt += 1) {
10157
10408
  await new Promise((resolve) => setTimeout(resolve, 120 * (attempt + 1)));
@@ -10661,6 +10912,17 @@ function resolveTextTargetInDocument(doc, rawQuery, mode) {
10661
10912
  score += 30;
10662
10913
  }
10663
10914
  if (inViewport(el)) score += 25;
10915
+ if (tag === "a") {
10916
+ const href = htmlEl.href || "";
10917
+ const filterParams = /\b(condition|binding|format|availability|sort|filter|price|category_id)\b=[^&]/i;
10918
+ const filterPath = /\/(condition|binding|format|availability|sort|filter|price|category)\/[^/?#]+/i;
10919
+ if (filterParams.test(href) || filterPath.test(href)) {
10920
+ score -= 40;
10921
+ }
10922
+ if (/\b(used|new|paperback|hardcover|hardback|ebook|kindle|refine|clear all|remove filter)\b/.test(label)) {
10923
+ score -= 30;
10924
+ }
10925
+ }
10664
10926
  return score;
10665
10927
  }
10666
10928
  function regionBonus(el) {
@@ -10865,6 +11127,10 @@ function formatCompactToolResult(name, result) {
10865
11127
  const DEFAULT_PAGE_SCRIPT_TIMEOUT_MS = 1500;
10866
11128
  const QUIET_NAVIGATION_WINDOW_MS = 1200;
10867
11129
  const PAGE_SCRIPT_TIMEOUT = /* @__PURE__ */ Symbol("page-script-timeout");
11130
+ async function loadPermittedUrl$1(wc, url) {
11131
+ assertPermittedNavigationURL(url);
11132
+ await wc.loadURL(url);
11133
+ }
10868
11134
  function pageBusyError(action) {
10869
11135
  return `Error: Page is still busy; ${action} timed out waiting for page scripts. Retry in a moment.`;
10870
11136
  }
@@ -11080,9 +11346,9 @@ function waitForLoad$1(wc, timeout = 5e3) {
11080
11346
  finish();
11081
11347
  return;
11082
11348
  }
11083
- wc.on("did-finish-load", onLoadEvent);
11084
- wc.on("did-stop-loading", onLoadEvent);
11085
- wc.on("did-fail-load", onLoadEvent);
11349
+ wc.once("did-finish-load", onLoadEvent);
11350
+ wc.once("did-stop-loading", onLoadEvent);
11351
+ wc.once("did-fail-load", onLoadEvent);
11086
11352
  });
11087
11353
  }
11088
11354
  function waitForPotentialNavigation$1(wc, beforeUrl, timeout = 2500) {
@@ -11135,8 +11401,8 @@ function waitForPotentialNavigation$1(wc, beforeUrl, timeout = 2500) {
11135
11401
  wc.once("did-start-loading", onStart);
11136
11402
  wc.once("did-navigate", onNavigate);
11137
11403
  wc.once("did-navigate-in-page", onNavigateInPage);
11138
- wc.on("did-stop-loading", onNativeChange);
11139
- wc.on("page-title-updated", onNativeChange);
11404
+ wc.once("did-stop-loading", onNativeChange);
11405
+ wc.once("page-title-updated", onNativeChange);
11140
11406
  });
11141
11407
  }
11142
11408
  async function getPostNavSummary(wc) {
@@ -11336,7 +11602,7 @@ async function clickElement$1(wc, selector) {
11336
11602
 
11337
11603
  const rect = el.getBoundingClientRect();
11338
11604
  if (rect.width <= 0 || rect.height <= 0) {
11339
- return { error: "Error[hidden]: Element has no visible area" };
11605
+ return { error: "Error[hidden]: Element has no visible area. It may be inside a collapsed, lazy-loaded, or virtual-scroll section. Scroll toward it (scroll or scroll_to_element) then call read_page to refresh visible elements before clicking again." };
11340
11606
  }
11341
11607
 
11342
11608
  const points = samplePoints(rect);
@@ -11903,6 +12169,12 @@ function getCartAddedSummary(url) {
11903
12169
  Already in cart (${count} items):
11904
12170
  ${items}`;
11905
12171
  }
12172
+ function clearCartState() {
12173
+ cartAddedProducts.clear();
12174
+ recentCartClicks.clear();
12175
+ clickStreakUrl = null;
12176
+ clickStreakCount = 0;
12177
+ }
11906
12178
  async function buildCartSuccessSuffix(wc, productUrl, overlayHint) {
11907
12179
  const productTitle = await getProductPageTitle(wc);
11908
12180
  recordProductAddedToCart(productUrl, productTitle);
@@ -11968,8 +12240,7 @@ Go back and select a different product.`;
11968
12240
  const hrefMatch = typeof result === "string" ? result.match(/\nhref: (https?:\/\/\S+)/) : null;
11969
12241
  if (hrefMatch) {
11970
12242
  try {
11971
- assertSafeURL(hrefMatch[1]);
11972
- await wc.loadURL(hrefMatch[1]);
12243
+ await loadPermittedUrl$1(wc, hrefMatch[1]);
11973
12244
  await waitForLoad$1(wc, 8e3);
11974
12245
  const hrefUrl = wc.getURL();
11975
12246
  if (hrefUrl !== beforeUrl2) return `${result.split("\n")[0]} -> ${hrefUrl}`;
@@ -12036,8 +12307,7 @@ Go back and select a different product.`;
12036
12307
  const hrefMatch = typeof result === "string" ? result.match(/\nhref: (https?:\/\/\S+)/) : null;
12037
12308
  if (hrefMatch) {
12038
12309
  try {
12039
- assertSafeURL(hrefMatch[1]);
12040
- await wc.loadURL(hrefMatch[1]);
12310
+ await loadPermittedUrl$1(wc, hrefMatch[1]);
12041
12311
  await waitForLoad$1(wc, 8e3);
12042
12312
  const hrefUrl = wc.getURL();
12043
12313
  if (hrefUrl !== beforeUrl2) return `${result.split("\n")[0]} -> ${hrefUrl}`;
@@ -12132,8 +12402,7 @@ ${postActivationOverlayHint}`;
12132
12402
  const validation = await validateLinkDestination(elInfo.href);
12133
12403
  if (validation.status !== "dead") {
12134
12404
  try {
12135
- assertSafeURL(elInfo.href);
12136
- await wc.loadURL(elInfo.href);
12405
+ await loadPermittedUrl$1(wc, elInfo.href);
12137
12406
  await waitForLoad$1(wc, 8e3);
12138
12407
  const hrefFallbackUrl = wc.getURL();
12139
12408
  if (hrefFallbackUrl !== beforeUrl) {
@@ -13685,8 +13954,7 @@ async function submitForm$1(wc, args) {
13685
13954
  if (formInfo.params) {
13686
13955
  url.search = formInfo.params;
13687
13956
  }
13688
- assertSafeURL(url.toString());
13689
- wc.loadURL(url.toString());
13957
+ await loadPermittedUrl$1(wc, url.toString());
13690
13958
  await waitForPotentialNavigation$1(wc, beforeUrl);
13691
13959
  const afterUrl = wc.getURL();
13692
13960
  return afterUrl !== beforeUrl ? `Submitted form via GET -> ${afterUrl}` : "Submitted form via GET";
@@ -14323,8 +14591,7 @@ async function searchPage(wc, args) {
14323
14591
  const shortcut = buildSearchShortcut(wc.getURL(), query);
14324
14592
  if (shortcut) {
14325
14593
  const beforeUrl2 = wc.getURL();
14326
- assertSafeURL(shortcut.url);
14327
- wc.loadURL(shortcut.url);
14594
+ await loadPermittedUrl$1(wc, shortcut.url);
14328
14595
  await waitForPotentialNavigation$1(wc, beforeUrl2, 4e3);
14329
14596
  const afterUrl2 = wc.getURL();
14330
14597
  const applied = shortcut.appliedFilters.length > 0 ? ` (${shortcut.appliedFilters.join(", ")})` : "";
@@ -14480,6 +14747,18 @@ WARNING: You drifted to ${drift.targetDomain} but the task requires staying on $
14480
14747
  warnings += `${cartSummary}
14481
14748
  Select a DIFFERENT product that is not in the cart. Call read_page if needed to see available results.`;
14482
14749
  }
14750
+ if (ctx.toolProfile === "compact" && name === "go_back") {
14751
+ warnings += `
14752
+ Call read_page(mode="results_only") to see available products before clicking.`;
14753
+ }
14754
+ }
14755
+ if (name === "click" && ctx.toolProfile === "compact") {
14756
+ const filterParams = /\b(condition|binding|format|availability|sort|filter|price|category_id|view)\b=[^&]/i;
14757
+ const filterPath = /\/(condition|binding|format|availability|sort|filter|price|category)\/[^/?#]+/i;
14758
+ if (filterParams.test(currentUrl) || filterPath.test(currentUrl)) {
14759
+ warnings += `
14760
+ WARNING: The clicked link appears to be a filter or sort control, not a product. If you intended to click a product, call go_back and use click(index=N) on a result from read_page(mode="results_only").`;
14761
+ }
14483
14762
  }
14484
14763
  return `
14485
14764
  [state: url=${currentUrl}, title=${JSON.stringify(wc.getTitle() || "")}, canGoBack=${tab.canGoBack()}, canGoForward=${tab.canGoForward()}, loading=${wc.isLoading()}]${warnings}`;
@@ -15713,9 +15992,14 @@ async function handleAIQuery(query, provider, activeWebContents, onChunk, onEnd,
15713
15992
  const pageType = detectPageType(pageContent);
15714
15993
  const defaultReadMode = chooseAgentReadMode(pageContent);
15715
15994
  if (provider.agentToolProfile === "compact") {
15995
+ const prevGoal = runtime2.getState().taskTracker?.goal?.trim();
15716
15996
  runtime2.ensureTaskTracker(query, pageContent.url || activeWebContents.getURL());
15997
+ if (prevGoal !== query.trim()) {
15998
+ clearCartState();
15999
+ }
15717
16000
  } else {
15718
16001
  runtime2.clearTaskTracker();
16002
+ clearCartState();
15719
16003
  }
15720
16004
  const structuredContext = provider.agentToolProfile === "compact" ? buildCompactScopedContext(
15721
16005
  pageContent,
@@ -16739,6 +17023,10 @@ function clearMcpAuthFile() {
16739
17023
  function asTextResponse(text) {
16740
17024
  return { content: [{ type: "text", text }] };
16741
17025
  }
17026
+ async function loadPermittedUrl(wc, url) {
17027
+ assertPermittedNavigationURL(url);
17028
+ await wc.loadURL(url);
17029
+ }
16742
17030
  function asPromptResponse(text) {
16743
17031
  return {
16744
17032
  messages: [
@@ -16938,7 +17226,7 @@ async function clickElement(wc, selector) {
16938
17226
 
16939
17227
  const rect = el.getBoundingClientRect();
16940
17228
  if (rect.width <= 0 || rect.height <= 0) {
16941
- return { error: "Element is not visible" };
17229
+ return { error: "Element is not visible. It may be inside a collapsed, lazy-loaded, or virtual-scroll section. Scroll toward it (scroll or scroll_to_element) then call read_page to refresh visible elements before clicking again." };
16942
17230
  }
16943
17231
 
16944
17232
  const points = samplePoints(rect);
@@ -17058,7 +17346,7 @@ async function clickResolvedSelector(wc, selector) {
17058
17346
  const hrefMatch = typeof result === "string" ? result.match(/\nhref: (https?:\/\/\S+)/) : null;
17059
17347
  if (hrefMatch) {
17060
17348
  try {
17061
- await wc.loadURL(hrefMatch[1]);
17349
+ await loadPermittedUrl(wc, hrefMatch[1]);
17062
17350
  await waitForLoad(wc, 8e3);
17063
17351
  const hrefUrl = wc.getURL();
17064
17352
  if (hrefUrl !== beforeUrl2) return `${result.split("\n")[0]} -> ${hrefUrl}`;
@@ -17111,7 +17399,7 @@ ${overlayHint2}${actionsSuffix}`;
17111
17399
  const hrefMatch = typeof result === "string" ? result.match(/\nhref: (https?:\/\/\S+)/) : null;
17112
17400
  if (hrefMatch) {
17113
17401
  try {
17114
- await wc.loadURL(hrefMatch[1]);
17402
+ await loadPermittedUrl(wc, hrefMatch[1]);
17115
17403
  await waitForLoad(wc, 8e3);
17116
17404
  const hrefUrl = wc.getURL();
17117
17405
  if (hrefUrl !== beforeUrl2) return `${result.split("\n")[0]} -> ${hrefUrl}`;
@@ -17841,7 +18129,7 @@ async function hoverElement(wc, selector) {
17841
18129
  el.scrollIntoView({ behavior: 'instant', block: 'center', inline: 'center' });
17842
18130
  }
17843
18131
  const rect = el.getBoundingClientRect();
17844
- if (rect.width <= 0 || rect.height <= 0) return { error: 'Error[hidden]: Element has no visible area' };
18132
+ if (rect.width <= 0 || rect.height <= 0) return { error: 'Error[hidden]: Element has no visible area. It may be inside a collapsed, lazy-loaded, or virtual-scroll section. Scroll toward it then call read_page to refresh visible elements.' };
17845
18133
  el.dispatchEvent(new MouseEvent('mouseover', { bubbles: true, cancelable: true }));
17846
18134
  el.dispatchEvent(new MouseEvent('mouseenter', { bubbles: false }));
17847
18135
  const label = (el.textContent || el.tagName || 'Element').trim().slice(0, 80);
@@ -18022,8 +18310,7 @@ async function submitForm(wc, index, selector) {
18022
18310
  if (formInfo.params) {
18023
18311
  url.search = formInfo.params;
18024
18312
  }
18025
- assertSafeURL(url.toString());
18026
- wc.loadURL(url.toString());
18313
+ await loadPermittedUrl(wc, url.toString());
18027
18314
  await waitForPotentialNavigation(wc, beforeUrl);
18028
18315
  const afterUrl = wc.getURL();
18029
18316
  return afterUrl !== beforeUrl ? `Submitted form via GET -> ${afterUrl}` : "Submitted form via GET";
@@ -22175,7 +22462,7 @@ function registerIpcHandlers(windowState, runtime2) {
22175
22462
  return windowState.uiState.settingsOpen;
22176
22463
  });
22177
22464
  electron.ipcMain.handle(Channels.SETTINGS_GET, () => {
22178
- return loadSettings();
22465
+ return getRendererSettings();
22179
22466
  });
22180
22467
  electron.ipcMain.handle(Channels.SETTINGS_HEALTH_GET, () => getRuntimeHealth());
22181
22468
  electron.ipcMain.handle(Channels.SETTINGS_SET, async (_, key, value) => {
@@ -22193,8 +22480,9 @@ function registerIpcHandlers(windowState, runtime2) {
22193
22480
  await stopMcpServer();
22194
22481
  await startMcpServer(tabManager, runtime2, updatedSettings.mcpPort);
22195
22482
  }
22196
- sendToRendererViews(Channels.SETTINGS_UPDATE, updatedSettings);
22197
- return updatedSettings;
22483
+ const rendererSettings = getRendererSettings();
22484
+ sendToRendererViews(Channels.SETTINGS_UPDATE, rendererSettings);
22485
+ return rendererSettings;
22198
22486
  });
22199
22487
  electron.ipcMain.handle(Channels.AGENT_RUNTIME_GET, () => runtime2.getState());
22200
22488
  electron.ipcMain.handle(Channels.AGENT_PAUSE, () => runtime2.pause());
@@ -22409,21 +22697,44 @@ function registerIpcHandlers(windowState, runtime2) {
22409
22697
  electron.ipcMain.handle(Channels.PREMIUM_GET_STATE, () => {
22410
22698
  return getPremiumState();
22411
22699
  });
22412
- electron.ipcMain.handle(Channels.PREMIUM_ACTIVATE, async (_, email) => {
22700
+ electron.ipcMain.handle(Channels.PREMIUM_ACTIVATION_START, async (_, email) => {
22413
22701
  assertString(email, "email");
22414
22702
  if (!/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email.trim())) {
22415
- return { ok: false, state: getPremiumState(), error: "Invalid email format" };
22703
+ return { ok: false, error: "Invalid email format" };
22416
22704
  }
22417
22705
  trackPremiumFunnel("activation_attempted");
22418
- const result = await activateWithEmail(email);
22419
- if (result.ok) {
22420
- trackPremiumFunnel("activation_succeeded", { status: result.state.status });
22421
- sendToRendererViews(Channels.PREMIUM_UPDATE, result.state);
22422
- } else {
22423
- trackPremiumFunnel("activation_failed", { status: result.state.status });
22706
+ const result = await requestActivationCode(email);
22707
+ if (!result.ok) {
22708
+ trackPremiumFunnel("activation_failed");
22424
22709
  }
22425
22710
  return result;
22426
22711
  });
22712
+ electron.ipcMain.handle(
22713
+ Channels.PREMIUM_ACTIVATION_VERIFY,
22714
+ async (_, email, code, challengeToken) => {
22715
+ assertString(email, "email");
22716
+ assertString(code, "code");
22717
+ assertString(challengeToken, "challengeToken");
22718
+ if (!/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email.trim())) {
22719
+ return {
22720
+ ok: false,
22721
+ state: getPremiumState(),
22722
+ error: "Invalid email format"
22723
+ };
22724
+ }
22725
+ trackPremiumFunnel("activation_attempted");
22726
+ const result = await verifyActivationCode(email, code, challengeToken);
22727
+ if (result.ok) {
22728
+ trackPremiumFunnel("activation_succeeded", {
22729
+ status: result.state.status
22730
+ });
22731
+ sendToRendererViews(Channels.PREMIUM_UPDATE, result.state);
22732
+ } else {
22733
+ trackPremiumFunnel("activation_failed", { status: result.state.status });
22734
+ }
22735
+ return result;
22736
+ }
22737
+ );
22427
22738
  electron.ipcMain.handle(Channels.PREMIUM_CHECKOUT, async (_, email) => {
22428
22739
  trackPremiumFunnel("checkout_clicked");
22429
22740
  const result = await getCheckoutUrl(email);