@quanta-intellect/vessel-browser 0.1.13 → 0.1.15

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.
@@ -2123,6 +2123,21 @@ function generateStableSelector(el) {
2123
2123
  }
2124
2124
  return uniqueSelector(document2, parts.join(" > ")) || parts.join(" > ");
2125
2125
  }
2126
+ function looksLikeCorrectOption(value) {
2127
+ const text = getTrimmedText(value);
2128
+ if (!text) return void 0;
2129
+ if (/\b(correct|right choice|this is correct|correct answer|pick this|select this|choose this|right answer)\b/i.test(
2130
+ text
2131
+ )) {
2132
+ return true;
2133
+ }
2134
+ if (/\b(wrong|incorrect|not this|don't pick|do not pick|bad option|decoy)\b/i.test(
2135
+ text
2136
+ )) {
2137
+ return false;
2138
+ }
2139
+ return void 0;
2140
+ }
2126
2141
  let elementIndex = 0;
2127
2142
  const elementSelectors = {};
2128
2143
  let indexedElements = /* @__PURE__ */ new WeakMap();
@@ -2135,7 +2150,8 @@ function collectShadowRoots(root) {
2135
2150
  const shadowRoots = [];
2136
2151
  let walked = 0;
2137
2152
  const walk = (node, depth) => {
2138
- if (depth > MAX_SHADOW_DEPTH || shadowRoots.length >= MAX_SHADOW_HOSTS) return;
2153
+ if (depth > MAX_SHADOW_DEPTH || shadowRoots.length >= MAX_SHADOW_HOSTS)
2154
+ return;
2139
2155
  const tw = document.createTreeWalker(node, NodeFilter.SHOW_ELEMENT);
2140
2156
  let el = tw.nextNode();
2141
2157
  while (el && walked < MAX_WALK_ELEMENTS && shadowRoots.length < MAX_SHADOW_HOSTS) {
@@ -2306,6 +2322,192 @@ function getOverlayType(el) {
2306
2322
  }
2307
2323
  return "overlay";
2308
2324
  }
2325
+ function touchesViewportEdge(rect) {
2326
+ const viewportWidth = window.innerWidth || document.documentElement?.clientWidth || 0;
2327
+ const viewportHeight = window.innerHeight || document.documentElement?.clientHeight || 0;
2328
+ const edgePadding = 24;
2329
+ return rect.left <= edgePadding || rect.top <= edgePadding || rect.right >= viewportWidth - edgePadding || rect.bottom >= viewportHeight - edgePadding;
2330
+ }
2331
+ function hasFixedAncestor(el) {
2332
+ let current = el.parentElement;
2333
+ while (current && current !== document.body) {
2334
+ const position = window.getComputedStyle(current).position;
2335
+ if (position === "fixed" || position === "sticky") return true;
2336
+ current = current.parentElement;
2337
+ }
2338
+ return false;
2339
+ }
2340
+ function getEffectiveZIndex(el, style = window.getComputedStyle(el)) {
2341
+ const own = parseZIndex(style);
2342
+ if (own > 0) return own;
2343
+ let current = el.parentElement;
2344
+ while (current && current !== document.body) {
2345
+ const parentZ = parseZIndex(window.getComputedStyle(current));
2346
+ if (parentZ > 0) return parentZ;
2347
+ current = current.parentElement;
2348
+ }
2349
+ return 0;
2350
+ }
2351
+ function looksLikeDrawer(el, style, rect, areaRatio) {
2352
+ if (rect.width < 220 || rect.height < 160 || areaRatio < 0.08) return false;
2353
+ if (!touchesViewportEdge(rect)) return false;
2354
+ if (style.position === "fixed" || style.position === "sticky") {
2355
+ return getEffectiveZIndex(el, style) >= 5;
2356
+ }
2357
+ if (style.position === "absolute" && hasFixedAncestor(el)) {
2358
+ return getEffectiveZIndex(el, style) >= 5;
2359
+ }
2360
+ return false;
2361
+ }
2362
+ function looksLikeCartConfirmation(el) {
2363
+ const text = (el.textContent || "").slice(0, 500).toLowerCase();
2364
+ const signals = [
2365
+ "added to cart",
2366
+ "added to bag",
2367
+ "added to basket",
2368
+ "added to your cart",
2369
+ "added to your bag",
2370
+ "added to your basket"
2371
+ ];
2372
+ return signals.some((signal) => text.includes(signal));
2373
+ }
2374
+ function getControlTextData(el) {
2375
+ if (el instanceof HTMLInputElement && (el.type === "radio" || el.type === "checkbox")) {
2376
+ const label = getInputLabel(el);
2377
+ if (label) return { text: label, source: "label" };
2378
+ }
2379
+ const aria = getTrimmedText(el.getAttribute("aria-label"));
2380
+ if (aria) return { text: aria, source: "aria-label" };
2381
+ const textContent = getTrimmedText(el.textContent);
2382
+ if (textContent) return { text: textContent, source: "textContent" };
2383
+ if (el instanceof HTMLInputElement) {
2384
+ const value = getTrimmedText(el.value) || getTrimmedText(el.getAttribute("value"));
2385
+ if (value) return { text: value, source: "value" };
2386
+ }
2387
+ const valueAttr = getTrimmedText(el.getAttribute("value"));
2388
+ if (valueAttr) return { text: valueAttr, source: "value" };
2389
+ const title = getTrimmedText(el.getAttribute("title"));
2390
+ if (title) return { text: title, source: "title" };
2391
+ return {};
2392
+ }
2393
+ function getOverlayActionKind(el, label) {
2394
+ const lower = label.toLowerCase();
2395
+ const attrText = [
2396
+ el.getAttribute("id"),
2397
+ typeof el.className === "string" ? el.className : "",
2398
+ el.getAttribute("name"),
2399
+ el.getAttribute("title")
2400
+ ].filter(Boolean).join(" ").toLowerCase();
2401
+ if (el.getAttribute("role") === "radio" || el instanceof HTMLInputElement && el.type === "radio") {
2402
+ return "radio";
2403
+ }
2404
+ if (/close|dismiss|skip|cancel|not now|maybe later|no thanks|reject|decline/.test(
2405
+ lower
2406
+ ) || /modal-close|overlay-close/.test(attrText)) {
2407
+ return "dismiss";
2408
+ }
2409
+ if (/accept|agree|allow/.test(lower) && /cookie|consent|privacy|gdpr|onetrust|cookiebot/.test(
2410
+ `${lower} ${attrText}`
2411
+ )) {
2412
+ return "accept";
2413
+ }
2414
+ if (/submit|continue|next|confirm|done|ok|start|proceed/.test(lower)) {
2415
+ return "submit";
2416
+ }
2417
+ return "action";
2418
+ }
2419
+ function getOverlayActionPriority(action) {
2420
+ switch (action.kind) {
2421
+ case "dismiss":
2422
+ return 40;
2423
+ case "accept":
2424
+ return 35;
2425
+ case "submit":
2426
+ return 30;
2427
+ case "radio":
2428
+ return 20;
2429
+ default:
2430
+ return 10;
2431
+ }
2432
+ }
2433
+ function collectOverlayRadioOptions(root) {
2434
+ const seen = /* @__PURE__ */ new Set();
2435
+ const options = [];
2436
+ root.querySelectorAll('[role="radio"], input[type="radio"]').forEach((node) => {
2437
+ if (!(node instanceof HTMLElement) || !isElementVisible(node)) return;
2438
+ const data = getControlTextData(node);
2439
+ if (!data.text) return;
2440
+ const selector = generateSelector(node);
2441
+ const key = selector || data.text;
2442
+ if (seen.has(key)) return;
2443
+ seen.add(key);
2444
+ const checked = node.getAttribute("aria-checked") === "true" || (node instanceof HTMLInputElement ? node.checked : false);
2445
+ options.push({
2446
+ label: data.text.slice(0, 100),
2447
+ selector,
2448
+ checked,
2449
+ labelSource: data.source,
2450
+ looksCorrect: looksLikeCorrectOption(data.text)
2451
+ });
2452
+ });
2453
+ return options.slice(0, 8);
2454
+ }
2455
+ function collectOverlayActions(root) {
2456
+ const seen = /* @__PURE__ */ new Set();
2457
+ const actions = [];
2458
+ root.querySelectorAll(
2459
+ 'button, [role="button"], a[href], input[type="button"], input[type="submit"], [role="radio"], input[type="radio"]'
2460
+ ).forEach((node) => {
2461
+ if (!(node instanceof HTMLElement) || !isElementVisible(node)) return;
2462
+ const selector = generateSelector(node);
2463
+ if (!selector || seen.has(selector)) return;
2464
+ let data = getControlTextData(node);
2465
+ if (!data.text) {
2466
+ const attrText = [
2467
+ node.id,
2468
+ typeof node.className === "string" ? node.className : ""
2469
+ ].filter(Boolean).join(" ").toLowerCase();
2470
+ if (/onetrust|consent|cookie|banner|gdpr|trustarc|cookiebot/.test(
2471
+ attrText
2472
+ )) {
2473
+ data = {
2474
+ text: attrText.includes("accept") ? "Accept cookies" : attrText.includes("reject") ? "Reject cookies" : attrText.includes("close") ? "Close" : "Consent button",
2475
+ source: "fallback"
2476
+ };
2477
+ }
2478
+ }
2479
+ if (!data.text) return;
2480
+ seen.add(selector);
2481
+ actions.push({
2482
+ label: data.text.slice(0, 100),
2483
+ selector,
2484
+ kind: getOverlayActionKind(node, data.text),
2485
+ disabled: isElementDisabled(node)
2486
+ });
2487
+ });
2488
+ return actions.sort((a, b) => getOverlayActionPriority(b) - getOverlayActionPriority(a)).slice(0, 10);
2489
+ }
2490
+ function getOverlayMessage(el) {
2491
+ const heading = el.querySelector("h1, h2, h3, h4, h5, h6");
2492
+ return getTrimmedText(heading?.textContent)?.slice(0, 160) || getNodeTextByIds(el.getAttribute("aria-describedby"))?.slice(0, 160) || getTrimmedText(el.textContent)?.slice(0, 160);
2493
+ }
2494
+ function classifyOverlayKind(args) {
2495
+ const haystack = [
2496
+ args.node.id,
2497
+ typeof args.node.className === "string" ? args.node.className : "",
2498
+ args.node.getAttribute("role"),
2499
+ args.node.getAttribute("aria-label"),
2500
+ args.node.textContent
2501
+ ].filter(Boolean).join(" ").toLowerCase();
2502
+ if (/cookie|consent|privacy|gdpr|onetrust|cookiebot|trustarc/.test(haystack)) {
2503
+ return "cookie_consent";
2504
+ }
2505
+ if (args.cartConfirm) return "cart_confirmation";
2506
+ if (args.radioOptions.length > 0) return "selection_modal";
2507
+ if (args.drawerLike) return "drawer";
2508
+ if (/alert|warning|notice|success|error/.test(haystack)) return "alert";
2509
+ return "overlay";
2510
+ }
2309
2511
  function detectOverlays() {
2310
2512
  if (!document.body) return [];
2311
2513
  const viewportWidth = window.innerWidth || document.documentElement?.clientWidth || 0;
@@ -2320,23 +2522,39 @@ function detectOverlays() {
2320
2522
  if (style.pointerEvents === "none") return;
2321
2523
  const rect = node.getBoundingClientRect();
2322
2524
  if (!isInViewportRect(rect)) return;
2323
- const position = style.position;
2324
- const zIndex = parseZIndex(style);
2325
- const areaRatio = rect.width * rect.height / viewportArea;
2326
2525
  const overlayType = getOverlayType(node);
2327
2526
  const dialogLike = overlayType === "dialog" || overlayType === "modal";
2328
- const blockingSurface = (position === "fixed" || position === "sticky") && zIndex >= 10 && areaRatio >= 0.3 && getViewportCenterCoverage(rect);
2329
- if (!dialogLike && !blockingSurface) return;
2527
+ const areaRatio = rect.width * rect.height / viewportArea;
2528
+ const drawerLike = looksLikeDrawer(node, style, rect, areaRatio);
2529
+ const cartConfirm = !dialogLike && !drawerLike && (style.position === "fixed" || style.position === "sticky" || style.position === "absolute") && rect.width >= 160 && rect.height >= 100 && looksLikeCartConfirmation(node);
2530
+ const blockingSurface = dialogLike || drawerLike || cartConfirm || (style.position === "fixed" || style.position === "sticky") && parseZIndex(style) >= 10 && areaRatio >= 0.3 && getViewportCenterCoverage(rect);
2531
+ if (!blockingSurface && overlayType !== "dialog" && overlayType !== "modal") {
2532
+ return;
2533
+ }
2534
+ const actions = collectOverlayActions(node);
2535
+ const radioOptions = collectOverlayRadioOptions(node);
2330
2536
  seen.add(node);
2331
2537
  overlays.push({
2332
2538
  element: node,
2333
2539
  type: overlayType ?? "overlay",
2540
+ kind: classifyOverlayKind({
2541
+ node,
2542
+ drawerLike,
2543
+ cartConfirm,
2544
+ radioOptions
2545
+ }),
2334
2546
  role: getTrimmedText(node.getAttribute("role")) || void 0,
2335
2547
  label: getOverlayLabel(node),
2336
2548
  selector: generateSelector(node),
2337
2549
  text: getTrimmedText(node.textContent)?.slice(0, 160),
2338
- blocksInteraction: dialogLike || blockingSurface,
2339
- zIndex
2550
+ message: getOverlayMessage(node),
2551
+ blocksInteraction: blockingSurface,
2552
+ dismissSelector: actions.find((action) => action.kind === "dismiss")?.selector,
2553
+ acceptSelector: actions.find((action) => action.kind === "accept")?.selector,
2554
+ submitSelector: actions.find((action) => action.kind === "submit")?.selector,
2555
+ actions,
2556
+ radioOptions,
2557
+ zIndex: parseZIndex(style)
2340
2558
  });
2341
2559
  });
2342
2560
  return overlays.sort((a, b) => {
@@ -2499,6 +2717,46 @@ function getInputLabel(el) {
2499
2717
  }
2500
2718
  return getTrimmedText(el.getAttribute("aria-label")) || getNodeTextByIds(el.getAttribute("aria-labelledby")) || getTrimmedText(el.getAttribute("placeholder")) || void 0;
2501
2719
  }
2720
+ function getInputLabelWithSource(el) {
2721
+ if (el.id) {
2722
+ const label = document.querySelector(
2723
+ `label[for="${escapeSelectorValue(el.id)}"]`
2724
+ );
2725
+ const text = getTrimmedText(label?.textContent);
2726
+ if (text) return { label: text, source: "label" };
2727
+ }
2728
+ const parentLabel = el.closest("label");
2729
+ if (parentLabel) {
2730
+ const clone = parentLabel.cloneNode(true);
2731
+ clone.querySelectorAll("input, select, textarea").forEach((input) => {
2732
+ input.remove();
2733
+ });
2734
+ const text = getTrimmedText(clone.textContent);
2735
+ if (text) return { label: text, source: "label" };
2736
+ }
2737
+ const ariaLabel = getTrimmedText(el.getAttribute("aria-label"));
2738
+ if (ariaLabel) return { label: ariaLabel, source: "aria-label" };
2739
+ const labelledBy = getNodeTextByIds(el.getAttribute("aria-labelledby"));
2740
+ if (labelledBy) return { label: labelledBy, source: "label" };
2741
+ const placeholder = getTrimmedText(el.getAttribute("placeholder"));
2742
+ if (placeholder) return { label: placeholder, source: "placeholder" };
2743
+ return {};
2744
+ }
2745
+ function getButtonTextWithSource(el) {
2746
+ const textContent = getTrimmedText(el.textContent);
2747
+ if (textContent) return { text: textContent, source: "text" };
2748
+ const value = el instanceof HTMLInputElement || el instanceof HTMLButtonElement ? getTrimmedText(el.value) : getTrimmedText(el.getAttribute("value"));
2749
+ if (value) return { text: value, source: "value" };
2750
+ const ariaLabel = getTrimmedText(el.getAttribute("aria-label"));
2751
+ if (ariaLabel) return { text: ariaLabel, source: "aria-label" };
2752
+ return { text: "Button", source: "text" };
2753
+ }
2754
+ function getParentOverlaySelector(el) {
2755
+ const overlay = activeOverlays.find(
2756
+ (candidate) => candidate.element === el || candidate.element.contains(el) || el instanceof HTMLElement && el.contains(candidate.element)
2757
+ );
2758
+ return overlay?.selector;
2759
+ }
2502
2760
  function getElementRole(el) {
2503
2761
  return getTrimmedText(el.getAttribute("role")) || (el.tagName.toLowerCase() === "a" ? "link" : el.tagName.toLowerCase() === "button" ? "button" : void 0);
2504
2762
  }
@@ -2534,6 +2792,7 @@ function getAriaBoolean(el, attr) {
2534
2792
  function buildBaseMetadata(el) {
2535
2793
  return {
2536
2794
  context: getElementContext(el),
2795
+ parentOverlay: getParentOverlaySelector(el),
2537
2796
  selector: generateSelector(el),
2538
2797
  index: assignIndex(el),
2539
2798
  role: getElementRole(el),
@@ -2608,12 +2867,15 @@ function extractInteractiveElements() {
2608
2867
  deepQuerySelectorAll(
2609
2868
  'button, [role="button"], input[type="submit"], input[type="button"]'
2610
2869
  ).forEach((btn) => {
2611
- const input = btn;
2612
- const text = btn.textContent?.trim() || input.value || btn.getAttribute("aria-label") || "Button";
2870
+ const { text, source } = getButtonTextWithSource(btn);
2871
+ const role = getElementRole(btn);
2613
2872
  elements.push({
2614
2873
  type: "button",
2615
- text: text.slice(0, 100),
2616
- ...buildBaseMetadata(btn)
2874
+ text: text?.slice(0, 100),
2875
+ labelSource: source,
2876
+ ...buildBaseMetadata(btn),
2877
+ role,
2878
+ looksCorrect: role === "radio" ? looksLikeCorrectOption(text) : void 0
2617
2879
  });
2618
2880
  });
2619
2881
  deepQuerySelectorAll("a[href]").forEach((link) => {
@@ -2635,15 +2897,24 @@ function extractInteractiveElements() {
2635
2897
  ).forEach((input) => {
2636
2898
  const element = input;
2637
2899
  const tag = input.tagName.toLowerCase();
2900
+ const label = getInputLabelWithSource(element);
2901
+ const role = getElementRole(input);
2902
+ const radioText = role === "radio" || element instanceof HTMLInputElement && element.type === "radio" ? getTrimmedText(
2903
+ element.getAttribute("value") || element.getAttribute("aria-label") || label.label
2904
+ ) : void 0;
2638
2905
  elements.push({
2639
2906
  type: tag === "select" ? "select" : tag === "textarea" ? "textarea" : "input",
2640
- label: getInputLabel(element)?.slice(0, 100),
2907
+ label: label.label?.slice(0, 100),
2908
+ labelSource: label.source,
2641
2909
  inputType: element.getAttribute("type") || void 0,
2642
2910
  placeholder: element.getAttribute("placeholder") || void 0,
2643
2911
  required: element.hasAttribute("required") || void 0,
2644
2912
  value: getElementValue(element),
2645
2913
  options: element instanceof HTMLSelectElement ? getSelectOptions(element) : void 0,
2646
2914
  ...buildBaseMetadata(input),
2915
+ role,
2916
+ text: radioText?.slice(0, 100),
2917
+ looksCorrect: radioText || label.label ? looksLikeCorrectOption(radioText || label.label) : void 0,
2647
2918
  ...getFieldMetadata(element)
2648
2919
  });
2649
2920
  });
@@ -2666,15 +2937,24 @@ function extractForms() {
2666
2937
  ).forEach((input) => {
2667
2938
  const element = input;
2668
2939
  const tag = input.tagName.toLowerCase();
2940
+ const label = getInputLabelWithSource(element);
2941
+ const role = getElementRole(input);
2942
+ const radioText = role === "radio" || element instanceof HTMLInputElement && element.type === "radio" ? getTrimmedText(
2943
+ element.getAttribute("value") || element.getAttribute("aria-label") || label.label
2944
+ ) : void 0;
2669
2945
  fields.push({
2670
2946
  type: tag === "select" ? "select" : tag === "textarea" ? "textarea" : "input",
2671
- label: getInputLabel(element)?.slice(0, 100),
2947
+ label: label.label?.slice(0, 100),
2948
+ labelSource: label.source,
2672
2949
  inputType: element.getAttribute("type") || void 0,
2673
2950
  placeholder: element.getAttribute("placeholder") || void 0,
2674
2951
  required: element.hasAttribute("required") || void 0,
2675
2952
  value: getElementValue(element),
2676
2953
  options: element instanceof HTMLSelectElement ? getSelectOptions(element) : void 0,
2677
2954
  ...buildBaseMetadata(input),
2955
+ role,
2956
+ text: radioText?.slice(0, 100),
2957
+ looksCorrect: radioText || label.label ? looksLikeCorrectOption(radioText || label.label) : void 0,
2678
2958
  ...getFieldMetadata(element)
2679
2959
  });
2680
2960
  });
@@ -2683,11 +2963,11 @@ function extractForms() {
2683
2963
  "button, input[type='submit'], input[type='image']"
2684
2964
  )
2685
2965
  ).filter((control) => isSubmitControlForForm(control, form)).forEach((btn) => {
2686
- const input = btn;
2687
- const text = btn.textContent?.trim() || input.value || btn.getAttribute("aria-label") || "Submit";
2966
+ const { text, source } = getButtonTextWithSource(btn);
2688
2967
  fields.push({
2689
2968
  type: "button",
2690
- text: text.slice(0, 100),
2969
+ text: text?.slice(0, 100),
2970
+ labelSource: source,
2691
2971
  ...buildBaseMetadata(btn)
2692
2972
  });
2693
2973
  });
@@ -2729,7 +3009,9 @@ function extractLandmarks() {
2729
3009
  }
2730
3010
  function extractJsonLd() {
2731
3011
  const results = [];
2732
- const scripts = document.querySelectorAll('script[type="application/ld+json"]');
3012
+ const scripts = document.querySelectorAll(
3013
+ 'script[type="application/ld+json"]'
3014
+ );
2733
3015
  for (const script of scripts) {
2734
3016
  try {
2735
3017
  const parsed = JSON.parse(script.textContent || "");
@@ -2828,7 +3110,9 @@ function extractRdfa() {
2828
3110
  }
2829
3111
  function withHighlightLabelsRemoved(read) {
2830
3112
  const labels = Array.from(
2831
- document.querySelectorAll(".__vessel-highlight-label[data-vessel-highlight]")
3113
+ document.querySelectorAll(
3114
+ ".__vessel-highlight-label[data-vessel-highlight]"
3115
+ )
2832
3116
  ).filter((node) => node instanceof HTMLElement);
2833
3117
  const removed = labels.map((label) => {
2834
3118
  const parent = label.parentNode;
@@ -63,6 +63,17 @@ const Channels = {
63
63
  // DevTools panel
64
64
  DEVTOOLS_PANEL_TOGGLE: "devtools-panel:toggle",
65
65
  DEVTOOLS_PANEL_STATE: "devtools-panel:state",
66
+ DEVTOOLS_PANEL_RESIZE: "devtools-panel:resize",
67
+ // Find in page
68
+ FIND_IN_PAGE_START: "find:start",
69
+ FIND_IN_PAGE_NEXT: "find:next",
70
+ FIND_IN_PAGE_STOP: "find:stop",
71
+ FIND_IN_PAGE_RESULT: "find:result",
72
+ // Browsing history
73
+ HISTORY_GET: "history:get",
74
+ HISTORY_SEARCH: "history:search",
75
+ HISTORY_CLEAR: "history:clear",
76
+ HISTORY_UPDATE: "history:update",
66
77
  // Window controls
67
78
  WINDOW_MINIMIZE: "window:minimize",
68
79
  WINDOW_MAXIMIZE: "window:maximize",
@@ -185,13 +196,33 @@ const api = {
185
196
  },
186
197
  devtoolsPanel: {
187
198
  toggle: () => electron.ipcRenderer.invoke(Channels.DEVTOOLS_PANEL_TOGGLE),
188
- resize: (height) => electron.ipcRenderer.invoke("devtools-panel:resize", height),
199
+ resize: (height) => electron.ipcRenderer.invoke(Channels.DEVTOOLS_PANEL_RESIZE, height),
189
200
  onStateUpdate: (cb) => {
190
201
  const handler = (_, state) => cb(state);
191
202
  electron.ipcRenderer.on(Channels.DEVTOOLS_PANEL_STATE, handler);
192
203
  return () => electron.ipcRenderer.removeListener(Channels.DEVTOOLS_PANEL_STATE, handler);
193
204
  }
194
205
  },
206
+ find: {
207
+ start: (text, options) => electron.ipcRenderer.invoke(Channels.FIND_IN_PAGE_START, text, options),
208
+ next: (forward) => electron.ipcRenderer.invoke(Channels.FIND_IN_PAGE_NEXT, forward),
209
+ stop: (action) => electron.ipcRenderer.invoke(Channels.FIND_IN_PAGE_STOP, action),
210
+ onResult: (cb) => {
211
+ const handler = (_, result) => cb(result);
212
+ electron.ipcRenderer.on(Channels.FIND_IN_PAGE_RESULT, handler);
213
+ return () => electron.ipcRenderer.removeListener(Channels.FIND_IN_PAGE_RESULT, handler);
214
+ }
215
+ },
216
+ history: {
217
+ get: () => electron.ipcRenderer.invoke(Channels.HISTORY_GET),
218
+ search: (query) => electron.ipcRenderer.invoke(Channels.HISTORY_SEARCH, query),
219
+ clear: () => electron.ipcRenderer.invoke(Channels.HISTORY_CLEAR),
220
+ onUpdate: (cb) => {
221
+ const handler = (_, state) => cb(state);
222
+ electron.ipcRenderer.on(Channels.HISTORY_UPDATE, handler);
223
+ return () => electron.ipcRenderer.removeListener(Channels.HISTORY_UPDATE, handler);
224
+ }
225
+ },
195
226
  window: {
196
227
  minimize: () => electron.ipcRenderer.invoke(Channels.WINDOW_MINIMIZE),
197
228
  maximize: () => electron.ipcRenderer.invoke(Channels.WINDOW_MAXIMIZE),