@quanta-intellect/vessel-browser 0.1.26 → 0.1.28

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
@@ -2750,6 +2750,38 @@ function layoutViews(state2) {
2750
2750
  });
2751
2751
  }
2752
2752
  }
2753
+ function resizeSidebarViews(state2) {
2754
+ const { mainWindow, sidebarView, devtoolsPanelView, tabManager, uiState } = state2;
2755
+ const [width, height] = mainWindow.getContentSize();
2756
+ const chromeHeight = uiState.focusMode ? 0 : CHROME_HEIGHT;
2757
+ const sidebarWidth = uiState.sidebarOpen ? uiState.sidebarWidth : 0;
2758
+ const devtoolsHeight = uiState.devtoolsPanelOpen ? uiState.devtoolsPanelHeight : 0;
2759
+ const resizeHandleOverlap = 6;
2760
+ const contentWidth = width - sidebarWidth;
2761
+ sidebarView.setBounds({
2762
+ x: width - sidebarWidth - resizeHandleOverlap,
2763
+ y: 0,
2764
+ width: sidebarWidth + resizeHandleOverlap,
2765
+ height
2766
+ });
2767
+ if (uiState.devtoolsPanelOpen) {
2768
+ devtoolsPanelView.setBounds({
2769
+ x: 0,
2770
+ y: height - devtoolsHeight,
2771
+ width: contentWidth,
2772
+ height: devtoolsHeight
2773
+ });
2774
+ }
2775
+ const activeTab = tabManager.getActiveTab();
2776
+ if (activeTab) {
2777
+ activeTab.view.setBounds({
2778
+ x: 0,
2779
+ y: chromeHeight,
2780
+ width: contentWidth,
2781
+ height: height - chromeHeight - devtoolsHeight
2782
+ });
2783
+ }
2784
+ }
2753
2785
  const SEARCH_ENGINE_HOSTS = [
2754
2786
  "google.",
2755
2787
  "bing.com",
@@ -5391,8 +5423,8 @@ const PROVIDERS = {
5391
5423
  models: [],
5392
5424
  requiresApiKey: false,
5393
5425
  defaultBaseUrl: "http://localhost:8080/v1",
5394
- apiKeyPlaceholder: "",
5395
- apiKeyHint: "Any OpenAI-compatible API endpoint"
5426
+ apiKeyPlaceholder: "Bearer token or API key",
5427
+ apiKeyHint: "Optional only if your endpoint requires authentication"
5396
5428
  }
5397
5429
  };
5398
5430
  function toOpenAITools(tools) {
@@ -6978,27 +7010,38 @@ function detectPageType(page) {
6978
7010
  const hasPasswordField = page.forms.some(
6979
7011
  (f) => f.fields.some((el) => el.inputType === "password")
6980
7012
  );
6981
- const hasSearchInput = page.interactiveElements.some(
7013
+ const searchInputs = page.interactiveElements.filter(
6982
7014
  (el) => el.inputType === "search" || el.name === "q" || el.name === "query" || el.name === "search" || (el.placeholder || "").toLowerCase().includes("search")
6983
- ) || page.forms.some(
7015
+ );
7016
+ const hasSearchInput = searchInputs.length > 0 || page.forms.some(
6984
7017
  (f) => f.fields.some(
6985
7018
  (el) => el.inputType === "search" || el.name === "q" || el.name === "query"
6986
7019
  )
6987
7020
  );
7021
+ const hasVisibleSearchInput = searchInputs.some(
7022
+ (el) => el.visible === true && el.inViewport === true && el.obscured !== true && el.blockedByOverlay !== true
7023
+ );
6988
7024
  const formCount = page.forms.length;
6989
7025
  const hasCart = page.interactiveElements.some(
6990
7026
  (el) => (el.text || "").toLowerCase().includes("cart") || (el.text || "").toLowerCase().includes("checkout")
6991
7027
  ) || url.includes("cart") || url.includes("checkout");
6992
- const hasResults = page.interactiveElements.filter((el) => el.type === "link").length > 10;
7028
+ const contentLinks = page.interactiveElements.filter(
7029
+ (el) => el.type === "link" && el.context !== "nav" && el.context !== "header" && el.context !== "sidebar" && (el.href || "").startsWith("http")
7030
+ );
7031
+ const hasResults = contentLinks.length > 10;
6993
7032
  const hasPagination = page.interactiveElements.some(
6994
7033
  (el) => (el.text || "").toLowerCase() === "next" || el.text === "›" || el.text === "»" || (el.label || "").toLowerCase().includes("next page")
6995
7034
  );
7035
+ const listingLike = isSearchOrListingPage(page) || hasPagination || /[?&](d|q|query|search)=/.test(url) || /\/(search|results)\b/.test(url) || /\/p\/pl\b/.test(url);
6996
7036
  if (hasPasswordField) return "LOGIN";
6997
- if (hasSearchInput && !hasResults) return "SEARCH_READY";
6998
- if (hasResults && hasSearchInput) return "SEARCH_RESULTS";
7037
+ if (hasSearchInput && hasVisibleSearchInput && !listingLike) {
7038
+ return "SEARCH_READY";
7039
+ }
7040
+ if (hasResults && hasSearchInput && listingLike) return "SEARCH_RESULTS";
6999
7041
  if (hasCart) return "SHOPPING";
7000
7042
  if (formCount > 0 && !hasPasswordField) return "FORM";
7001
- if (hasPagination) return "PAGINATED_LIST";
7043
+ if (hasPagination && listingLike) return "PAGINATED_LIST";
7044
+ if (hasSearchInput && !listingLike) return "SEARCH_READY";
7002
7045
  if (page.content.length > 3e3 && page.interactiveElements.length < 10)
7003
7046
  return "ARTICLE";
7004
7047
  return "GENERAL";
@@ -7030,6 +7073,9 @@ function analyzePageIntent(page) {
7030
7073
  hints.push(
7031
7074
  "Suggested: vessel_search → auto-finds search box, types query, and submits"
7032
7075
  );
7076
+ hints.push(
7077
+ "Treat the visible site search box as the primary navigation control before jumping to direct URLs."
7078
+ );
7033
7079
  break;
7034
7080
  case "SEARCH_RESULTS":
7035
7081
  hints.push("Page type: SEARCH RESULTS");
@@ -7918,6 +7964,16 @@ const CONTEXT_HINTS = {
7918
7964
  function scoreForContext(toolName, pageType) {
7919
7965
  const def = defByName[toolName];
7920
7966
  if (!def) return 500;
7967
+ if (pageType === "SEARCH_READY") {
7968
+ if (toolName === "search") return -20;
7969
+ if (toolName === "type_text") return 5;
7970
+ if (toolName === "press_key") return 6;
7971
+ }
7972
+ if (pageType === "SEARCH_RESULTS") {
7973
+ if (toolName === "search") return -10;
7974
+ if (toolName === "type_text") return 12;
7975
+ if (toolName === "press_key") return 13;
7976
+ }
7921
7977
  const tier = def.tier ?? 1;
7922
7978
  if (tier === 0) return 0;
7923
7979
  const isRelevant = !def.relevance || def.relevance.includes(pageType);
@@ -11350,6 +11406,315 @@ async function submitForm$1(wc, args) {
11350
11406
  }
11351
11407
  return "Submitted form";
11352
11408
  }
11409
+ async function clickElementBySelector(wc, selector) {
11410
+ return clickResolvedSelector$1(wc, selector);
11411
+ }
11412
+ async function locateSearchTarget(wc, explicitSelector) {
11413
+ if (explicitSelector) {
11414
+ return { selector: explicitSelector, submitSelector: null };
11415
+ }
11416
+ return executePageScript(
11417
+ wc,
11418
+ `
11419
+ (function() {
11420
+ function text(value) {
11421
+ return value == null ? "" : String(value).trim();
11422
+ }
11423
+
11424
+ function normalize(value) {
11425
+ return text(value).toLowerCase();
11426
+ }
11427
+
11428
+ function isVisible(el) {
11429
+ if (!(el instanceof HTMLElement)) return true;
11430
+ const style = window.getComputedStyle(el);
11431
+ if (style.display === "none" || style.visibility === "hidden" || style.opacity === "0") {
11432
+ return false;
11433
+ }
11434
+ if (el.hasAttribute("hidden") || el.getAttribute("aria-hidden") === "true") {
11435
+ return false;
11436
+ }
11437
+ const rect = el.getBoundingClientRect();
11438
+ return rect.width > 0 && rect.height > 0;
11439
+ }
11440
+
11441
+ function inViewport(el) {
11442
+ if (!(el instanceof HTMLElement)) return true;
11443
+ const rect = el.getBoundingClientRect();
11444
+ const vw = window.innerWidth || document.documentElement?.clientWidth || 0;
11445
+ const vh = window.innerHeight || document.documentElement?.clientHeight || 0;
11446
+ return rect.bottom > 0 && rect.right > 0 && rect.top < vh && rect.left < vw;
11447
+ }
11448
+
11449
+ function escapeSelectorValue(value) {
11450
+ if (typeof CSS !== "undefined" && typeof CSS.escape === "function") {
11451
+ return CSS.escape(value);
11452
+ }
11453
+ return String(value).replace(/["\\\\]/g, "\\\\$&");
11454
+ }
11455
+
11456
+ function uniqueSelector(candidate) {
11457
+ if (!candidate) return null;
11458
+ try {
11459
+ return document.querySelectorAll(candidate).length === 1 ? candidate : null;
11460
+ } catch {
11461
+ return null;
11462
+ }
11463
+ }
11464
+
11465
+ function uniqueAttributeSelector(el, attribute) {
11466
+ const value = text(el.getAttribute && el.getAttribute(attribute));
11467
+ if (!value) return null;
11468
+ const candidate = el.tagName.toLowerCase() + "[" + attribute + "=\\"" + escapeSelectorValue(value) + "\\"]";
11469
+ return uniqueSelector(candidate);
11470
+ }
11471
+
11472
+ function selectorFor(el) {
11473
+ if (!el) return null;
11474
+ if (el.id) return "#" + escapeSelectorValue(el.id);
11475
+ for (const attribute of ["data-testid", "name", "form", "aria-label", "placeholder"]) {
11476
+ const candidate = uniqueAttributeSelector(el, attribute);
11477
+ if (candidate) return candidate;
11478
+ }
11479
+ const parts = [];
11480
+ let current = el;
11481
+ while (current) {
11482
+ if (current.id) {
11483
+ parts.unshift("#" + escapeSelectorValue(current.id));
11484
+ break;
11485
+ }
11486
+ const tag = current.tagName.toLowerCase();
11487
+ const parent = current.parentElement;
11488
+ if (!parent) {
11489
+ parts.unshift(tag);
11490
+ break;
11491
+ }
11492
+ const siblings = Array.from(parent.children).filter((child) => child.tagName === current.tagName);
11493
+ const index = siblings.indexOf(current) + 1;
11494
+ parts.unshift(siblings.length > 1 ? tag + ":nth-of-type(" + index + ")" : tag);
11495
+ current = parent;
11496
+ }
11497
+ const candidate = parts.join(" > ");
11498
+ return uniqueSelector(candidate) || candidate;
11499
+ }
11500
+
11501
+ function isDisabled(el) {
11502
+ return !!(el && el.hasAttribute && (el.hasAttribute("disabled") || el.getAttribute("aria-disabled") === "true"));
11503
+ }
11504
+
11505
+ function nearestSearchScope(input) {
11506
+ return input.closest('[role="search"], form, header, nav, [class*="search" i], [id*="search" i]');
11507
+ }
11508
+
11509
+ function collectCandidates() {
11510
+ const seen = new Set();
11511
+ const ordered = [];
11512
+ const specific = document.querySelectorAll(
11513
+ 'input[type="search"], input[name="q"], input[name="query"], input[name="search"], input[role="searchbox"], input[aria-label*="search" i], input[placeholder*="search" i]'
11514
+ );
11515
+ specific.forEach((el) => {
11516
+ if (!seen.has(el)) {
11517
+ seen.add(el);
11518
+ ordered.push(el);
11519
+ }
11520
+ });
11521
+ document.querySelectorAll('input[type="text"], input:not([type])').forEach((el) => {
11522
+ if (seen.has(el)) return;
11523
+ const scope = nearestSearchScope(el);
11524
+ if (!scope) return;
11525
+ seen.add(el);
11526
+ ordered.push(el);
11527
+ });
11528
+ return ordered;
11529
+ }
11530
+
11531
+ function scoreInput(el) {
11532
+ if (!(el instanceof HTMLInputElement)) return -1;
11533
+ if (isDisabled(el) || !isVisible(el)) return -1;
11534
+ const type = normalize(el.getAttribute("type") || el.type);
11535
+ if (type && !["search", "text", ""].includes(type)) return -1;
11536
+
11537
+ let score = 0;
11538
+ if (inViewport(el)) score += 120;
11539
+ const rect = el.getBoundingClientRect();
11540
+ score += Math.max(0, 40 - Math.min(40, Math.floor(Math.max(0, rect.top) / 20)));
11541
+
11542
+ const name = normalize(el.name);
11543
+ const placeholder = normalize(el.getAttribute("placeholder"));
11544
+ const aria = normalize(el.getAttribute("aria-label"));
11545
+ if (type === "search") score += 80;
11546
+ if (name === "q" || name === "query" || name === "search") score += 70;
11547
+ if (placeholder.includes("search")) score += 55;
11548
+ if (aria.includes("search")) score += 55;
11549
+
11550
+ const scope = nearestSearchScope(el);
11551
+ if (scope) {
11552
+ score += 35;
11553
+ const scopeRole = normalize(scope.getAttribute && scope.getAttribute("role"));
11554
+ const scopeLabel = normalize(
11555
+ [
11556
+ scope.id,
11557
+ scope.className,
11558
+ scope.getAttribute && scope.getAttribute("aria-label"),
11559
+ scope.getAttribute && scope.getAttribute("action"),
11560
+ ].filter(Boolean).join(" ")
11561
+ );
11562
+ if (scopeRole === "search") score += 35;
11563
+ if (scopeLabel.includes("search")) score += 30;
11564
+ const tag = normalize(scope.tagName);
11565
+ if (tag === "header" || tag === "nav") score += 20;
11566
+ }
11567
+
11568
+ return score;
11569
+ }
11570
+
11571
+ function pickSearchButton(input) {
11572
+ const scopes = [
11573
+ input.closest('[role="search"]'),
11574
+ input.closest('form'),
11575
+ nearestSearchScope(input),
11576
+ input.parentElement,
11577
+ ].filter(Boolean);
11578
+ const seen = new Set();
11579
+ let best = null;
11580
+ let bestScore = -1;
11581
+
11582
+ for (const scope of scopes) {
11583
+ scope.querySelectorAll('button, input[type="submit"], input[type="button"], [role="button"]').forEach((candidate) => {
11584
+ if (seen.has(candidate)) return;
11585
+ seen.add(candidate);
11586
+ if (!(candidate instanceof HTMLElement)) return;
11587
+ if (candidate === input || isDisabled(candidate) || !isVisible(candidate)) return;
11588
+
11589
+ const label = normalize(
11590
+ candidate.getAttribute("aria-label") ||
11591
+ candidate.textContent ||
11592
+ candidate.getAttribute("title") ||
11593
+ candidate.getAttribute("value")
11594
+ );
11595
+ const rect = candidate.getBoundingClientRect();
11596
+ const inputRect = input.getBoundingClientRect();
11597
+ const closeToInput =
11598
+ Math.abs(rect.top - inputRect.top) < 80 &&
11599
+ Math.abs(rect.left - inputRect.right) < 260;
11600
+
11601
+ let score = 0;
11602
+ if (inViewport(candidate)) score += 40;
11603
+ if (closeToInput) score += 35;
11604
+ if (label.includes("search") || label.includes("go") || label.includes("submit")) score += 45;
11605
+ if (candidate.getAttribute("type") === "submit") score += 20;
11606
+ if (candidate.closest('[role="search"]') === input.closest('[role="search"]')) score += 20;
11607
+ if (candidate.closest('form') && candidate.closest('form') === input.closest('form')) score += 15;
11608
+
11609
+ if (score > bestScore) {
11610
+ best = candidate;
11611
+ bestScore = score;
11612
+ }
11613
+ });
11614
+ }
11615
+
11616
+ return bestScore >= 35 ? best : null;
11617
+ }
11618
+
11619
+ let bestInput = null;
11620
+ let bestScore = -1;
11621
+ for (const candidate of collectCandidates()) {
11622
+ const score = scoreInput(candidate);
11623
+ if (score > bestScore) {
11624
+ bestInput = candidate;
11625
+ bestScore = score;
11626
+ }
11627
+ }
11628
+
11629
+ if (!bestInput) return null;
11630
+ const selector = selectorFor(bestInput);
11631
+ if (!selector) return null;
11632
+ const submit = pickSearchButton(bestInput);
11633
+ return {
11634
+ selector: selector,
11635
+ submitSelector: submit ? selectorFor(submit) : null,
11636
+ };
11637
+ })()
11638
+ `,
11639
+ {
11640
+ timeoutMs: 2200,
11641
+ label: "find search input"
11642
+ }
11643
+ );
11644
+ }
11645
+ async function searchPage(wc, args) {
11646
+ const query = String(args.query || "");
11647
+ if (!query) return "Error: No search query provided.";
11648
+ const queryLower = query.toLowerCase().trim();
11649
+ const buttonLikePatterns = [
11650
+ "add to cart",
11651
+ "add to bag",
11652
+ "add to basket",
11653
+ "buy now",
11654
+ "buy it now",
11655
+ "purchase",
11656
+ "continue shopping",
11657
+ "keep shopping",
11658
+ "view cart",
11659
+ "view bag",
11660
+ "view basket",
11661
+ "go to cart",
11662
+ "go to checkout",
11663
+ "checkout",
11664
+ "check out",
11665
+ "proceed to checkout",
11666
+ "place order",
11667
+ "submit",
11668
+ "subscribe",
11669
+ "sign up",
11670
+ "sign in",
11671
+ "log in",
11672
+ "register",
11673
+ "continue"
11674
+ ];
11675
+ if (buttonLikePatterns.some((p) => queryLower.includes(p))) {
11676
+ return `Error: "${query}" looks like a button label, not a search query. Use the click tool to interact with this element instead.`;
11677
+ }
11678
+ const searchInfo = await locateSearchTarget(
11679
+ wc,
11680
+ typeof args.selector === "string" ? args.selector : void 0
11681
+ );
11682
+ if (searchInfo === PAGE_SCRIPT_TIMEOUT) {
11683
+ return pageBusyError("search");
11684
+ }
11685
+ if (!searchInfo?.selector) {
11686
+ return 'Error: Could not find a visible search input. Try read_page(mode="visible_only") or provide a selector.';
11687
+ }
11688
+ const fillResult = await setElementValue$1(wc, searchInfo.selector, query);
11689
+ if (fillResult.startsWith("Error:")) {
11690
+ return fillResult;
11691
+ }
11692
+ await sleep$1(100);
11693
+ const beforeUrl = wc.getURL();
11694
+ const keyResult = await pressKey$1(wc, {
11695
+ key: "Enter",
11696
+ selector: searchInfo.selector
11697
+ });
11698
+ if (keyResult.startsWith("Error:")) {
11699
+ return keyResult;
11700
+ }
11701
+ await waitForPotentialNavigation$1(wc, beforeUrl, 3e3);
11702
+ let afterUrl = wc.getURL();
11703
+ if (afterUrl !== beforeUrl) {
11704
+ return `Searched "${query}" → ${afterUrl}`;
11705
+ }
11706
+ if (searchInfo.submitSelector) {
11707
+ const clickResult = await clickElementBySelector(wc, searchInfo.submitSelector);
11708
+ if (!clickResult.startsWith("Error:")) {
11709
+ await waitForPotentialNavigation$1(wc, beforeUrl, 3e3);
11710
+ afterUrl = wc.getURL();
11711
+ if (afterUrl !== beforeUrl) {
11712
+ return `Searched "${query}" (via search button) → ${afterUrl}`;
11713
+ }
11714
+ }
11715
+ }
11716
+ return `Searched "${query}" (same page — results may have loaded dynamically; inspect with read_page(mode="results_only") or read_page(mode="visible_only") before navigating directly elsewhere)`;
11717
+ }
11353
11718
  async function pressKey$1(wc, args) {
11354
11719
  const key = typeof args.key === "string" ? args.key.trim() : "";
11355
11720
  if (!key) return "Error: No key provided";
@@ -12396,164 +12761,7 @@ ${steps.join("\n")}`;
12396
12761
  }
12397
12762
  case "search": {
12398
12763
  if (!wc) return "Error: No active tab";
12399
- const query = String(args.query || "");
12400
- if (!query) return "Error: No search query provided.";
12401
- const queryLower = query.toLowerCase().trim();
12402
- const buttonLikePatterns = [
12403
- "add to cart",
12404
- "add to bag",
12405
- "add to basket",
12406
- "buy now",
12407
- "buy it now",
12408
- "purchase",
12409
- "continue shopping",
12410
- "keep shopping",
12411
- "view cart",
12412
- "view bag",
12413
- "view basket",
12414
- "go to cart",
12415
- "go to checkout",
12416
- "checkout",
12417
- "check out",
12418
- "proceed to checkout",
12419
- "place order",
12420
- "submit",
12421
- "subscribe",
12422
- "sign up",
12423
- "sign in",
12424
- "log in",
12425
- "register",
12426
- "continue"
12427
- ];
12428
- if (buttonLikePatterns.some((p) => queryLower.includes(p))) {
12429
- return `Error: "${query}" looks like a button label, not a search query. Use the click tool to interact with this element instead.`;
12430
- }
12431
- const searchInfo = args.selector ? { selector: args.selector, formAction: null, formMethod: null } : await executePageScript(
12432
- wc,
12433
- `
12434
- (function() {
12435
- var SELECTORS = 'input[type="search"], input[name="q"], input[name="query"], input[name="search"], input[role="searchbox"], input[aria-label*="search" i], input[placeholder*="search" i]';
12436
- function find() {
12437
- var el = document.querySelector(SELECTORS);
12438
- if (!el) {
12439
- var inputs = document.querySelectorAll('input[type="text"]');
12440
- for (var i = 0; i < inputs.length; i++) {
12441
- var form = inputs[i].closest('form');
12442
- if (form && (form.getAttribute('role') === 'search' || (form.action && form.action.includes('search')))) {
12443
- el = inputs[i];
12444
- break;
12445
- }
12446
- }
12447
- }
12448
- if (!el) return null;
12449
- var sel = el.id ? '#' + CSS.escape(el.id) : el.name ? 'input[name="' + el.name + '"]' : null;
12450
- var form = el.closest('form');
12451
- return {
12452
- selector: sel,
12453
- formAction: form ? form.action : null,
12454
- formMethod: form ? (form.method || 'GET').toUpperCase() : null,
12455
- inputName: el.name || 'q',
12456
- };
12457
- }
12458
- return new Promise(function(resolve) {
12459
- var result = find();
12460
- if (result) { resolve(result); return; }
12461
- var attempts = 0;
12462
- var timer = setInterval(function() {
12463
- result = find();
12464
- if (result || ++attempts >= 20) {
12465
- clearInterval(timer);
12466
- resolve(result);
12467
- }
12468
- }, 250);
12469
- });
12470
- })()
12471
- `,
12472
- {
12473
- timeoutMs: 1800,
12474
- label: "find search input"
12475
- }
12476
- );
12477
- if (searchInfo === PAGE_SCRIPT_TIMEOUT) {
12478
- return pageBusyError("search");
12479
- }
12480
- if (!searchInfo?.selector)
12481
- return "Error: Could not find search input. Try providing a selector.";
12482
- const fillResult = await setElementValue$1(
12483
- wc,
12484
- searchInfo.selector,
12485
- query
12486
- );
12487
- if (fillResult.startsWith("Error:")) {
12488
- return fillResult;
12489
- }
12490
- await sleep$1(100);
12491
- const focusResult = await executePageScript(
12492
- wc,
12493
- `
12494
- (function() {
12495
- var el = document.querySelector(${JSON.stringify(searchInfo.selector)});
12496
- if (el) el.focus();
12497
- })()
12498
- `,
12499
- {
12500
- label: "focus search input"
12501
- }
12502
- );
12503
- if (focusResult === PAGE_SCRIPT_TIMEOUT) {
12504
- return pageBusyError("search");
12505
- }
12506
- await sleep$1(50);
12507
- const beforeUrl = wc.getURL();
12508
- wc.sendInputEvent({ type: "keyDown", keyCode: "Return" });
12509
- await sleep$1(16);
12510
- wc.sendInputEvent({ type: "keyUp", keyCode: "Return" });
12511
- await waitForPotentialNavigation$1(wc, beforeUrl, 3e3);
12512
- let afterUrl = wc.getURL();
12513
- if (afterUrl !== beforeUrl) {
12514
- return `Searched "${query}" → ${afterUrl}`;
12515
- }
12516
- const clickedSubmit = await executePageScript(
12517
- wc,
12518
- `
12519
- (function() {
12520
- var form = document.querySelector(${JSON.stringify(searchInfo.selector)})?.closest('form');
12521
- if (!form) return false;
12522
- var btn = form.querySelector('button[type="submit"], input[type="submit"], button:not([type])');
12523
- if (btn) { btn.click(); return true; }
12524
- // Try any button in the form
12525
- var anyBtn = form.querySelector('button');
12526
- if (anyBtn) { anyBtn.click(); return true; }
12527
- return false;
12528
- })()
12529
- `,
12530
- {
12531
- label: "click search submit"
12532
- }
12533
- );
12534
- if (clickedSubmit === PAGE_SCRIPT_TIMEOUT) {
12535
- return pageBusyError("search");
12536
- }
12537
- if (clickedSubmit) {
12538
- await waitForPotentialNavigation$1(wc, beforeUrl, 3e3);
12539
- afterUrl = wc.getURL();
12540
- if (afterUrl !== beforeUrl) {
12541
- return `Searched "${query}" (via submit button) → ${afterUrl}`;
12542
- }
12543
- }
12544
- if (searchInfo.formAction && searchInfo.formMethod === "GET") {
12545
- try {
12546
- const url = new URL(searchInfo.formAction);
12547
- url.searchParams.set(searchInfo.inputName || "q", query);
12548
- assertSafeURL(url.toString());
12549
- wc.loadURL(url.toString());
12550
- await waitForPotentialNavigation$1(wc, beforeUrl);
12551
- afterUrl = wc.getURL();
12552
- return `Searched "${query}" (via direct URL) → ${afterUrl}`;
12553
- } catch {
12554
- }
12555
- }
12556
- return `Searched "${query}" (same page — results may have loaded dynamically)`;
12764
+ return searchPage(wc, args);
12557
12765
  }
12558
12766
  case "paginate": {
12559
12767
  if (!wc) return "Error: No active tab";
@@ -12808,6 +13016,7 @@ Instructions:
12808
13016
  - Use save_session after completing a login flow you may need again later, and load_session to resume that authenticated state in future runs.
12809
13017
  - Prefer select_option for dropdowns and submit_form for forms instead of guessing with clicks.
12810
13018
  - After navigating to a new site, DO NOT call read_page immediately. Instead, act on what you already know: use the search tool to search the site, type_text to enter queries in search bars, or click on known navigation patterns. You know what major sites look like — use that knowledge. Only call read_page if you're genuinely stuck and need to discover unfamiliar page structure.
13019
+ - On retail and marketplace sites (like Newegg, Amazon, Walmart, Etsy, eBay), prefer the site's visible search box, filters, and result pages over direct product URLs. Only navigate directly to a product page if the user gave you that URL or the site's own search UI is clearly unavailable after a reasonable attempt.
12811
13020
  - The page brief you start with is intentionally sparse. It is optimized for navigation speed, not completeness.
12812
13021
  - When you only need detail on one product/result/card/form section, use inspect_element instead of reading the page.
12813
13022
  - 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.
@@ -13755,6 +13964,23 @@ function getMcpAuthFilePath() {
13755
13964
  );
13756
13965
  return path$1.join(configDir, MCP_AUTH_FILENAME);
13757
13966
  }
13967
+ function readMcpAuthFile() {
13968
+ try {
13969
+ const raw = fs$1.readFileSync(getMcpAuthFilePath(), "utf8");
13970
+ const parsed = JSON.parse(raw);
13971
+ return parsed && typeof parsed === "object" ? parsed : null;
13972
+ } catch {
13973
+ return null;
13974
+ }
13975
+ }
13976
+ const MIN_TOKEN_LENGTH = 32;
13977
+ function getPersistentMcpAuthToken() {
13978
+ const existingToken = readMcpAuthFile()?.token?.trim();
13979
+ if (existingToken && existingToken.length >= MIN_TOKEN_LENGTH) {
13980
+ return existingToken;
13981
+ }
13982
+ return crypto$2.randomBytes(32).toString("hex");
13983
+ }
13758
13984
  function writeMcpAuthFile(endpoint, token) {
13759
13985
  try {
13760
13986
  const filePath = getMcpAuthFilePath();
@@ -13769,9 +13995,28 @@ function writeMcpAuthFile(endpoint, token) {
13769
13995
  }
13770
13996
  }
13771
13997
  function clearMcpAuthFile() {
13998
+ const existingToken = readMcpAuthFile()?.token?.trim();
13999
+ if (!existingToken) {
14000
+ try {
14001
+ fs$1.unlinkSync(getMcpAuthFilePath());
14002
+ } catch {
14003
+ }
14004
+ return;
14005
+ }
13772
14006
  try {
13773
- fs$1.unlinkSync(getMcpAuthFilePath());
13774
- } catch {
14007
+ const filePath = getMcpAuthFilePath();
14008
+ fs$1.mkdirSync(path$1.dirname(filePath), { recursive: true });
14009
+ fs$1.writeFileSync(
14010
+ filePath,
14011
+ JSON.stringify(
14012
+ { endpoint: "", token: existingToken, pid: null },
14013
+ null,
14014
+ 2
14015
+ ) + "\n",
14016
+ { mode: 384 }
14017
+ );
14018
+ } catch (err) {
14019
+ console.warn("[Vessel MCP] Failed to clear auth file:", err);
13775
14020
  }
13776
14021
  }
13777
14022
  function asTextResponse(text) {
@@ -18310,7 +18555,7 @@ function startMcpServer(tabManager, runtime2, port) {
18310
18555
  status: "starting",
18311
18556
  message: `Starting MCP server on port ${port}.`
18312
18557
  });
18313
- mcpAuthToken = crypto$2.randomBytes(32).toString("hex");
18558
+ mcpAuthToken = getPersistentMcpAuthToken();
18314
18559
  return new Promise((resolve) => {
18315
18560
  const server = http.createServer(async (req, res) => {
18316
18561
  const url = new URL(req.url || "/", `http://localhost:${port}`);
@@ -18337,7 +18582,11 @@ function startMcpServer(tabManager, runtime2, port) {
18337
18582
  return;
18338
18583
  }
18339
18584
  const authHeader = req.headers.authorization;
18340
- if (!authHeader || authHeader !== `Bearer ${mcpAuthToken}`) {
18585
+ const expected = `Bearer ${mcpAuthToken}`;
18586
+ const headerBuf = Buffer.from(authHeader ?? "");
18587
+ const expectedBuf = Buffer.from(expected);
18588
+ const tokenValid = headerBuf.length === expectedBuf.length && crypto$2.timingSafeEqual(headerBuf, expectedBuf);
18589
+ if (!authHeader || !tokenValid) {
18341
18590
  res.writeHead(401, { "Content-Type": "application/json" });
18342
18591
  res.end(JSON.stringify({ error: "Unauthorized — missing or invalid bearer token" }));
18343
18592
  return;
@@ -18964,29 +19213,31 @@ function registerIpcHandlers(windowState, runtime2) {
18964
19213
  return { accepted: false, reason: "busy" };
18965
19214
  }
18966
19215
  sendToRendererViews(Channels.AI_STREAM_START, query);
18967
- try {
18968
- activeChatProvider = createProvider(chatConfig);
18969
- trackProviderConfigured(chatConfig.id);
18970
- const activeTab = tabManager.getActiveTab();
18971
- await handleAIQuery(
18972
- query,
18973
- activeChatProvider,
18974
- activeTab?.view.webContents,
18975
- (chunk) => sendToRendererViews(Channels.AI_STREAM_CHUNK, chunk),
18976
- () => sendToRendererViews(Channels.AI_STREAM_END),
18977
- tabManager,
18978
- runtime2,
18979
- history
18980
- );
18981
- } catch (err) {
18982
- const msg = err instanceof Error ? err.message : "Unknown error";
18983
- sendToRendererViews(Channels.AI_STREAM_CHUNK, `
19216
+ (async () => {
19217
+ try {
19218
+ activeChatProvider = createProvider(chatConfig);
19219
+ trackProviderConfigured(chatConfig.id);
19220
+ const activeTab = tabManager.getActiveTab();
19221
+ await handleAIQuery(
19222
+ query,
19223
+ activeChatProvider,
19224
+ activeTab?.view.webContents,
19225
+ (chunk) => sendToRendererViews(Channels.AI_STREAM_CHUNK, chunk),
19226
+ () => sendToRendererViews(Channels.AI_STREAM_END),
19227
+ tabManager,
19228
+ runtime2,
19229
+ history
19230
+ );
19231
+ } catch (err) {
19232
+ const msg = err instanceof Error ? err.message : "Unknown error";
19233
+ sendToRendererViews(Channels.AI_STREAM_CHUNK, `
18984
19234
  [Error: ${msg}]`);
18985
- sendToRendererViews(Channels.AI_STREAM_END);
18986
- } finally {
18987
- activeChatProvider = null;
18988
- endAIStream("manual");
18989
- }
19235
+ sendToRendererViews(Channels.AI_STREAM_END);
19236
+ } finally {
19237
+ activeChatProvider = null;
19238
+ endAIStream("manual");
19239
+ }
19240
+ })();
18990
19241
  return { accepted: true };
18991
19242
  });
18992
19243
  electron.ipcMain.handle(Channels.AI_CANCEL, () => {
@@ -19043,6 +19294,7 @@ function registerIpcHandlers(windowState, runtime2) {
19043
19294
  assertNumber(width, "width");
19044
19295
  const clamped = Math.max(240, Math.min(800, Math.round(width)));
19045
19296
  windowState.uiState.sidebarWidth = clamped;
19297
+ resizeSidebarViews(windowState);
19046
19298
  return clamped;
19047
19299
  });
19048
19300
  electron.ipcMain.handle(Channels.SIDEBAR_RESIZE_COMMIT, () => {