haltija 1.1.21 → 1.2.2

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/dist/index.js CHANGED
@@ -1,12 +1,16 @@
1
1
  // @bun
2
2
  var __defProp = Object.defineProperty;
3
+ var __returnValue = (v) => v;
4
+ function __exportSetter(name, newValue) {
5
+ this[name] = __returnValue.bind(null, newValue);
6
+ }
3
7
  var __export = (target, all) => {
4
8
  for (var name in all)
5
9
  __defProp(target, name, {
6
10
  get: all[name],
7
11
  enumerable: true,
8
12
  configurable: true,
9
- set: (newValue) => all[name] = () => newValue
13
+ set: __exportSetter.bind(all, name)
10
14
  });
11
15
  };
12
16
  var __esm = (fn, res) => () => (fn && (res = fn(fn = 0)), res);
@@ -670,7 +674,7 @@ var injectorCode = `
670
674
  `;
671
675
 
672
676
  // src/version.ts
673
- var VERSION = "1.1.21";
677
+ var VERSION = "1.2.2";
674
678
 
675
679
  // src/embedded-assets.ts
676
680
  var APP_MD = `# Haltija App
@@ -1118,11 +1122,16 @@ Response: { tagName, id, className, textContent, attributes: {...} }
1118
1122
 
1119
1123
  | Name | Type | Description |
1120
1124
  |------|------|-------------|
1121
- | \`selector\` | string | CSS selector *(required)* |
1125
+ | \`ref\` | string,null | Ref ID from /tree output (e.g., 1, 42) - preferred for efficiency |
1126
+ | \`selector\` | string,null | CSS selector |
1122
1127
  | \`all\` | boolean,null | Return all matches (default false = first only) |
1123
1128
 
1124
1129
  **Examples:**
1125
1130
 
1131
+ - **by-ref**: Query element by ref ID from /tree
1132
+ \`\`\`json
1133
+ {"ref":"42"}
1134
+ \`\`\`
1126
1135
  - **by-id**: Find element by ID
1127
1136
  \`\`\`json
1128
1137
  {"selector":"#submit-btn"}
@@ -1172,13 +1181,18 @@ Use before clicking to verify element is visible and enabled.
1172
1181
 
1173
1182
  | Name | Type | Description |
1174
1183
  |------|------|-------------|
1175
- | \`selector\` | string | CSS selector *(required)* |
1184
+ | \`ref\` | string,null | Ref ID from /tree output (e.g., 1, 42) - preferred for efficiency |
1185
+ | \`selector\` | string,null | CSS selector |
1176
1186
  | \`fullStyles\` | boolean,null | Include all computed styles (default: false) |
1177
1187
  | \`matchedRules\` | boolean,null | Include matched CSS rules with specificity (default: false) |
1178
1188
  | \`window\` | string,null | Target window ID |
1179
1189
 
1180
1190
  **Examples:**
1181
1191
 
1192
+ - **by-ref**: Inspect element by ref ID from /tree
1193
+ \`\`\`json
1194
+ {"ref":"42"}
1195
+ \`\`\`
1182
1196
  - **check-button**: Verify button is clickable
1183
1197
  \`\`\`json
1184
1198
  {"selector":"#submit"}
@@ -1256,7 +1270,8 @@ Response: array of inspection objects
1256
1270
 
1257
1271
  | Name | Type | Description |
1258
1272
  |------|------|-------------|
1259
- | \`selector\` | string | CSS selector *(required)* |
1273
+ | \`ref\` | string,null | Ref ID from /tree output - returns single element as array |
1274
+ | \`selector\` | string,null | CSS selector |
1260
1275
  | \`limit\` | number,null | Max elements (default 10) |
1261
1276
  | \`fullStyles\` | boolean,null | Include all computed styles (default: false) |
1262
1277
  | \`matchedRules\` | boolean,null | Include matched CSS rules with specificity (default: false) |
@@ -1590,7 +1605,8 @@ Good for: sliders, resize handles, drag-and-drop reordering, range inputs.
1590
1605
 
1591
1606
  | Name | Type | Description |
1592
1607
  |------|------|-------------|
1593
- | \`selector\` | string | CSS selector of drag handle *(required)* |
1608
+ | \`ref\` | string,null | Ref ID from /tree output (e.g., 1, 42) - preferred for efficiency |
1609
+ | \`selector\` | string,null | CSS selector of drag handle |
1594
1610
  | \`deltaX\` | number,null | Horizontal distance in pixels |
1595
1611
  | \`deltaY\` | number,null | Vertical distance in pixels |
1596
1612
  | \`duration\` | number,null | Drag duration in ms (default 300) |
@@ -1598,6 +1614,10 @@ Good for: sliders, resize handles, drag-and-drop reordering, range inputs.
1598
1614
 
1599
1615
  **Examples:**
1600
1616
 
1617
+ - **by-ref**: Drag element by ref ID
1618
+ \`\`\`json
1619
+ {"ref":"15","deltaX":100}
1620
+ \`\`\`
1601
1621
  - **slider-right**: Move slider right
1602
1622
  \`\`\`json
1603
1623
  {"selector":".slider-handle","deltaX":100}
@@ -1625,7 +1645,8 @@ Great for showing users what you found or pointing out issues. Use /unhighlight
1625
1645
 
1626
1646
  | Name | Type | Description |
1627
1647
  |------|------|-------------|
1628
- | \`selector\` | string | CSS selector *(required)* |
1648
+ | \`ref\` | string,null | Ref ID from /tree output (e.g., 1, 42) - preferred for efficiency |
1649
+ | \`selector\` | string,null | CSS selector |
1629
1650
  | \`label\` | string,null | Label text to show |
1630
1651
  | \`color\` | string,null | CSS color (default #6366f1) |
1631
1652
  | \`duration\` | number,null | Auto-hide after ms (omit for manual) |
@@ -1633,6 +1654,10 @@ Great for showing users what you found or pointing out issues. Use /unhighlight
1633
1654
 
1634
1655
  **Examples:**
1635
1656
 
1657
+ - **by-ref**: Highlight element by ref ID
1658
+ \`\`\`json
1659
+ {"ref":"42","label":"Found it!"}
1660
+ \`\`\`
1636
1661
  - **point-out**: Show user where to click
1637
1662
  \`\`\`json
1638
1663
  {"selector":"#login-btn","label":"Click here"}
@@ -1662,16 +1687,17 @@ Remove any active highlight overlay created by /highlight.
1662
1687
 
1663
1688
  Smooth scroll with natural easing. Multiple modes:
1664
1689
 
1665
- - selector: Scroll element into view (most common)
1690
+ - ref/selector: Scroll element into view (most common)
1666
1691
  - x/y: Scroll to absolute position
1667
1692
  - deltaX/deltaY: Scroll relative to current position
1668
1693
 
1669
- At least one of selector, x, y, deltaX, or deltaY must be provided.
1694
+ At least one of ref, selector, x, y, deltaX, or deltaY must be provided.
1670
1695
 
1671
1696
  **Parameters:**
1672
1697
 
1673
1698
  | Name | Type | Description |
1674
1699
  |------|------|-------------|
1700
+ | \`ref\` | string,null | Ref ID from /tree output (e.g., 1, 42) - preferred for efficiency |
1675
1701
  | \`selector\` | string,null | CSS selector to scroll into view |
1676
1702
  | \`x\` | number,null | Absolute X position in pixels |
1677
1703
  | \`y\` | number,null | Absolute Y position in pixels |
@@ -1684,6 +1710,10 @@ At least one of selector, x, y, deltaX, or deltaY must be provided.
1684
1710
 
1685
1711
  **Examples:**
1686
1712
 
1713
+ - **by-ref**: Scroll element into view by ref ID
1714
+ \`\`\`json
1715
+ {"ref":"42"}
1716
+ \`\`\`
1687
1717
  - **to-element**: Scroll pricing section into view
1688
1718
  \`\`\`json
1689
1719
  {"selector":"#pricing"}
@@ -1775,13 +1805,18 @@ Response: { success: true, data: <return value> }
1775
1805
 
1776
1806
  | Name | Type | Description |
1777
1807
  |------|------|-------------|
1778
- | \`selector\` | string | CSS selector of the element *(required)* |
1808
+ | \`ref\` | string,null | Ref ID from /tree output (e.g., 1, 42) - preferred for efficiency |
1809
+ | \`selector\` | string,null | CSS selector of the element |
1779
1810
  | \`method\` | string | Method name to call or property name to get *(required)* |
1780
1811
  | \`args\` | array,null | Arguments to pass (omit to get property value) |
1781
1812
  | \`window\` | string,null | Target window ID |
1782
1813
 
1783
1814
  **Examples:**
1784
1815
 
1816
+ - **by-ref**: Get property by ref ID
1817
+ \`\`\`json
1818
+ {"ref":"42","method":"value"}
1819
+ \`\`\`
1785
1820
  - **get-value**: Get input value
1786
1821
  \`\`\`json
1787
1822
  {"selector":"#email","method":"value"}
@@ -1867,18 +1902,18 @@ Use /location after to verify navigation succeeded.
1867
1902
 
1868
1903
  **Refresh the page**
1869
1904
 
1870
- Hard reload the current page, bypassing cache. Use soft: true for cache-friendly reload.
1905
+ Hard reload the current page, bypassing all caches (CSS, JS, images). Use soft: true for cache-friendly reload.
1871
1906
 
1872
1907
  **Parameters:**
1873
1908
 
1874
1909
  | Name | Type | Description |
1875
1910
  |------|------|-------------|
1876
- | \`soft\` | boolean,null | Use cached resources if available (default false = hard refresh) |
1911
+ | \`soft\` | boolean,null | Use cached resources if available (default false = hard refresh that busts all caches) |
1877
1912
  | \`window\` | string,null | Target window ID |
1878
1913
 
1879
1914
  **Examples:**
1880
1915
 
1881
- - **hard**: Hard refresh (default, bypasses cache)
1916
+ - **hard**: Hard refresh (default, bypasses all caches)
1882
1917
  \`\`\`json
1883
1918
  {}
1884
1919
  \`\`\`
@@ -2515,16 +2550,20 @@ Use this when you see a blob URL in the DOM and need to access its content.
2515
2550
 
2516
2551
  **Capture a screenshot**
2517
2552
 
2518
- Capture the page or a specific element as base64 PNG/WebP/JPEG.
2553
+ Capture the page or a specific element as PNG/WebP/JPEG.
2519
2554
 
2520
2555
  Works automatically in the Haltija Desktop app. In browser widget mode, captures viewport only.
2521
2556
 
2522
- Response: { success, image: "data:image/png;base64,...", width, height, source }
2557
+ When file=true (default from CLI), saves to /tmp/haltija-screenshots/ and returns file path.
2558
+ When file=false, returns base64 data URL in response JSON.
2559
+
2560
+ Response: { success, path?, image?, width, height, source }
2523
2561
 
2524
2562
  **Parameters:**
2525
2563
 
2526
2564
  | Name | Type | Description |
2527
2565
  |------|------|-------------|
2566
+ | \`ref\` | string,null | Ref ID from /tree output - capture specific element |
2528
2567
  | \`selector\` | string,null | Element to capture (omit for full page) |
2529
2568
  | \`scale\` | number,null | Scale factor (default 1) |
2530
2569
  | \`maxWidth\` | number,null | Max width in pixels |
@@ -2532,6 +2571,7 @@ Response: { success, image: "data:image/png;base64,...", width, height, source }
2532
2571
  | \`window\` | string,null | Target window ID |
2533
2572
  | \`chyron\` | boolean,null | Burn page title, URL, timestamp into image (default true, set false for clean screenshot) |
2534
2573
  | \`delay\` | number,null | Wait ms before capturing (e.g. 1000 to let page settle after navigation) |
2574
+ | \`file\` | boolean,null | Save to disk and return file path instead of data URL (default true \u2014 pass false for base64) |
2535
2575
 
2536
2576
  **Examples:**
2537
2577
 
@@ -2539,6 +2579,10 @@ Response: { success, image: "data:image/png;base64,...", width, height, source }
2539
2579
  \`\`\`json
2540
2580
  {}
2541
2581
  \`\`\`
2582
+ - **by-ref**: Capture element by ref ID
2583
+ \`\`\`json
2584
+ {"ref":"42"}
2585
+ \`\`\`
2542
2586
  - **element**: Capture specific element
2543
2587
  \`\`\`json
2544
2588
  {"selector":"#chart"}
@@ -2584,6 +2628,76 @@ Great for debugging test failures - call this when something goes wrong.
2584
2628
  {"trigger":"test-failure","context":{"step":3,"error":"Element not found"}}
2585
2629
  \`\`\`
2586
2630
 
2631
+ ---
2632
+
2633
+ ### \`POST /video/start\`
2634
+
2635
+ **Start video recording**
2636
+
2637
+ Start recording the browser tab as WebM video. Requires the Haltija Desktop app.
2638
+
2639
+ The recording saves to /tmp/haltija-videos/ when stopped. Max duration is capped to prevent runaway recordings.
2640
+
2641
+ Response: { success, recordingId }
2642
+
2643
+ **Parameters:**
2644
+
2645
+ | Name | Type | Description |
2646
+ |------|------|-------------|
2647
+ | \`maxDuration\` | number,null | Max recording duration in seconds (default 60, max 300) |
2648
+ | \`window\` | string,null | Target window ID |
2649
+
2650
+ **Examples:**
2651
+
2652
+ - **start**: Start recording active tab
2653
+ \`\`\`json
2654
+ {}
2655
+ \`\`\`
2656
+ - **long**: Record up to 2 minutes
2657
+ \`\`\`json
2658
+ {"maxDuration":120}
2659
+ \`\`\`
2660
+
2661
+ ---
2662
+
2663
+ ### \`POST /video/stop\`
2664
+
2665
+ **Stop video recording**
2666
+
2667
+ Stop recording and save the video file.
2668
+
2669
+ Response: { success, path, duration, size }
2670
+
2671
+ **Parameters:**
2672
+
2673
+ | Name | Type | Description |
2674
+ |------|------|-------------|
2675
+ | \`window\` | string,null | Target window ID |
2676
+
2677
+ **Examples:**
2678
+
2679
+ - **stop**: Stop recording and get file path
2680
+ \`\`\`json
2681
+ {}
2682
+ \`\`\`
2683
+
2684
+ ---
2685
+
2686
+ ### \`GET /video/status\`
2687
+
2688
+ **Check video recording status**
2689
+
2690
+ Check if video recording is active.
2691
+
2692
+ Response: { recording, recordingId?, duration?, window? }
2693
+
2694
+ **Examples:**
2695
+
2696
+ - **check**: Check recording state
2697
+ \`\`\`json
2698
+ {}
2699
+ \`\`\`
2700
+
2587
2701
  ---
2588
2702
  `;
2589
2703
  var DOCS_MD = `# Haltija: Browser Control for AI Agents
@@ -2619,9 +2733,9 @@ hj --help # All commands
2619
2733
  ### See the Page
2620
2734
 
2621
2735
  - \`hj tree [selector, depth, includeText, ...]\` - Get DOM tree structure
2622
- - \`hj query [selector, all]\` - Query DOM elements by selector
2623
- - \`hj inspect [selector, fullStyles, matchedRules, ...]\` - Deep inspection of an element
2624
- - \`hj inspectAll [selector, limit, fullStyles, ...]\` - Inspect multiple elements
2736
+ - \`hj query [ref, selector, all]\` - Query DOM elements by selector
2737
+ - \`hj inspect [ref, selector, fullStyles, ...]\` - Deep inspection of an element
2738
+ - \`hj inspectAll [ref, selector, limit, ...]\` - Inspect multiple elements
2625
2739
  - \`hj find [text, tag, exact, ...]\` - Find elements by text content
2626
2740
  - \`hj form [selector, includeDisabled, includeHidden, ...]\` - Extract all form values as structured JSON
2627
2741
 
@@ -2630,12 +2744,12 @@ hj --help # All commands
2630
2744
  - \`hj click [ref, selector, text, ...]\` - Click an element
2631
2745
  - \`hj type [ref, selector, text, ...]\` - Type text into an element
2632
2746
  - \`hj key [key, ref, selector, ...]\` - Send keyboard input
2633
- - \`hj drag [selector, deltaX, deltaY, ...]\` - Drag from an element
2634
- - \`hj highlight [selector, label, color, ...]\` - Visually highlight an element
2747
+ - \`hj drag [ref, selector, deltaX, ...]\` - Drag from an element
2748
+ - \`hj highlight [ref, selector, label, ...]\` - Visually highlight an element
2635
2749
  - \`hj unhighlight\` - Remove highlight
2636
- - \`hj scroll [selector, x, y, ...]\` - Scroll to element or position
2750
+ - \`hj scroll [ref, selector, x, ...]\` - Scroll to element or position
2637
2751
  - \`hj wait [ms, forElement, hidden, ...]\` - Wait for time, element, or condition
2638
- - \`hj call [selector, method, args, ...]\` - Call a method or get a property on an element
2752
+ - \`hj call [ref, selector, method, ...]\` - Call a method or get a property on an element
2639
2753
 
2640
2754
  ### Navigate
2641
2755
 
@@ -2691,8 +2805,11 @@ hj --help # All commands
2691
2805
  - \`hj console\` - Get console output
2692
2806
  - \`hj eval [code, window]\` - Execute JavaScript
2693
2807
  - \`hj fetch [url, window]\` - Fetch a URL from within the tab context
2694
- - \`hj screenshot [selector, scale, maxWidth, ...]\` - Capture a screenshot
2808
+ - \`hj screenshot [ref, selector, scale, ...]\` - Capture a screenshot
2695
2809
  - \`hj snapshot [trigger, context]\` - Capture page snapshot
2810
+ - \`hj video-start [maxDuration, window]\` - Start video recording
2811
+ - \`hj video-stop [window]\` - Stop video recording
2812
+ - \`hj video-status\` - Check video recording status
2696
2813
 
2697
2814
  ## Tips
2698
2815
 
@@ -2856,6 +2973,15 @@ function getPlaygroundHtml() {
2856
2973
  <button id="btn-danger" class="btn-test danger" onclick="showOutput('Danger button clicked!')">Danger Button</button>
2857
2974
  </div>
2858
2975
 
2976
+ <h3>Background Colors</h3>
2977
+ <p>Set the page background to a solid color. Useful for verifying screenshots and video capture.</p>
2978
+ <div class="test-buttons">
2979
+ <button id="bg-red" class="btn-test" style="background:#ef4444;color:white" onclick="document.body.style.backgroundColor='#ef4444';showOutput('Background: red')">Red</button>
2980
+ <button id="bg-green" class="btn-test" style="background:#22c55e;color:white" onclick="document.body.style.backgroundColor='#22c55e';showOutput('Background: green')">Green</button>
2981
+ <button id="bg-blue" class="btn-test" style="background:#3b82f6;color:white" onclick="document.body.style.backgroundColor='#3b82f6';showOutput('Background: blue')">Blue</button>
2982
+ <button id="bg-reset" class="btn-test" style="background:#f8fafc;border:1px solid #cbd5e1" onclick="document.body.style.backgroundColor='#f8fafc';showOutput('Background: reset')">Reset</button>
2983
+ </div>
2984
+
2859
2985
  <h3>Form Inputs</h3>
2860
2986
  <div class="test-form">
2861
2987
  <div class="form-row">
@@ -2918,6 +3044,12 @@ function getPlaygroundHtml() {
2918
3044
  <pre><code>hj query "#output"</code></pre>
2919
3045
  </div>
2920
3046
 
3047
+ <p>Set background to red and take a screenshot:</p>
3048
+ <div class="code-block">
3049
+ <button class="copy-btn" data-copy-type="command" onclick="copyCode(this)">Copy</button>
3050
+ <pre><code>hj click "#bg-red" && hj screenshot</code></pre>
3051
+ </div>
3052
+
2921
3053
  <p>Get the page tree:</p>
2922
3054
  <div class="code-block">
2923
3055
  <button class="copy-btn" data-copy-type="command" onclick="copyCode(this)">Copy</button>
@@ -3572,10 +3704,16 @@ Use this to check if an element exists before clicking/typing. For detailed info
3572
3704
  Response: { tagName, id, className, textContent, attributes: {...} }`,
3573
3705
  category: "dom",
3574
3706
  input: L.object({
3575
- selector: L.string.describe("CSS selector"),
3707
+ ref: L.string.describe("Ref ID from /tree output (e.g., 1, 42) - preferred for efficiency").optional,
3708
+ selector: L.string.describe("CSS selector").optional,
3576
3709
  all: L.boolean.describe("Return all matches (default false = first only)").optional
3577
3710
  }),
3578
3711
  examples: [
3712
+ {
3713
+ name: "by-ref",
3714
+ input: { ref: "42" },
3715
+ description: "Query element by ref ID from /tree"
3716
+ },
3579
3717
  {
3580
3718
  name: "by-id",
3581
3719
  input: { selector: "#submit-btn" },
@@ -3602,14 +3740,14 @@ Response: { tagName, id, className, textContent, attributes: {...} }`,
3602
3740
  }
3603
3741
  ],
3604
3742
  invalidExamples: [
3605
- { name: "missing-selector", input: {}, error: "selector is required" },
3743
+ { name: "missing-target", input: {}, error: "ref or selector is required" },
3606
3744
  {
3607
3745
  name: "wrong-type",
3608
3746
  input: { selector: 123 },
3609
3747
  error: "selector must be string"
3610
3748
  }
3611
3749
  ],
3612
- hints: '"selector", --all | see: tree, inspect'
3750
+ hints: '@ref or "selector", --all | see: tree, inspect'
3613
3751
  });
3614
3752
  var inspect = endpoint({
3615
3753
  path: "/inspect",
@@ -3629,12 +3767,18 @@ Response includes:
3629
3767
  Use before clicking to verify element is visible and enabled.`,
3630
3768
  category: "dom",
3631
3769
  input: L.object({
3632
- selector: L.string.describe("CSS selector"),
3770
+ ref: L.string.describe("Ref ID from /tree output (e.g., 1, 42) - preferred for efficiency").optional,
3771
+ selector: L.string.describe("CSS selector").optional,
3633
3772
  fullStyles: L.boolean.describe("Include all computed styles (default: false)").optional,
3634
3773
  matchedRules: L.boolean.describe("Include matched CSS rules with specificity (default: false)").optional,
3635
3774
  window: L.string.describe("Target window ID").optional
3636
3775
  }),
3637
3776
  examples: [
3777
+ {
3778
+ name: "by-ref",
3779
+ input: { ref: "42" },
3780
+ description: "Inspect element by ref ID from /tree"
3781
+ },
3638
3782
  {
3639
3783
  name: "check-button",
3640
3784
  input: { selector: "#submit" },
@@ -3678,8 +3822,9 @@ Use before clicking to verify element is visible and enabled.`,
3678
3822
  }
3679
3823
  ],
3680
3824
  invalidExamples: [
3681
- { name: "missing-selector", input: {}, error: "selector is required" }
3682
- ]
3825
+ { name: "missing-target", input: {}, error: "ref or selector is required" }
3826
+ ],
3827
+ hints: '@ref or "selector", --styles, --rules, --ancestors | see: tree, query'
3683
3828
  });
3684
3829
  var inspectAll = endpoint({
3685
3830
  path: "/inspectAll",
@@ -3695,7 +3840,8 @@ Same detailed info as /inspect, but for multiple elements. Great for:
3695
3840
  Response: array of inspection objects`,
3696
3841
  category: "dom",
3697
3842
  input: L.object({
3698
- selector: L.string.describe("CSS selector"),
3843
+ ref: L.string.describe("Ref ID from /tree output - returns single element as array").optional,
3844
+ selector: L.string.describe("CSS selector").optional,
3699
3845
  limit: L.number.describe("Max elements (default 10)").optional,
3700
3846
  fullStyles: L.boolean.describe("Include all computed styles (default: false)").optional,
3701
3847
  matchedRules: L.boolean.describe("Include matched CSS rules with specificity (default: false)").optional,
@@ -3971,13 +4117,19 @@ var drag = endpoint({
3971
4117
  Good for: sliders, resize handles, drag-and-drop reordering, range inputs.`,
3972
4118
  category: "interaction",
3973
4119
  input: L.object({
3974
- selector: L.string.describe("CSS selector of drag handle"),
4120
+ ref: L.string.describe("Ref ID from /tree output (e.g., 1, 42) - preferred for efficiency").optional,
4121
+ selector: L.string.describe("CSS selector of drag handle").optional,
3975
4122
  deltaX: L.number.describe("Horizontal distance in pixels").optional,
3976
4123
  deltaY: L.number.describe("Vertical distance in pixels").optional,
3977
4124
  duration: L.number.describe("Drag duration in ms (default 300)").optional,
3978
4125
  window: L.string.describe("Target window ID").optional
3979
4126
  }),
3980
4127
  examples: [
4128
+ {
4129
+ name: "by-ref",
4130
+ input: { ref: "15", deltaX: 100 },
4131
+ description: "Drag element by ref ID"
4132
+ },
3981
4133
  {
3982
4134
  name: "slider-right",
3983
4135
  input: { selector: ".slider-handle", deltaX: 100 },
@@ -3996,12 +4148,12 @@ Good for: sliders, resize handles, drag-and-drop reordering, range inputs.`,
3996
4148
  ],
3997
4149
  invalidExamples: [
3998
4150
  {
3999
- name: "missing-selector",
4151
+ name: "missing-target",
4000
4152
  input: { deltaX: 100 },
4001
- error: "selector is required"
4153
+ error: "ref or selector is required"
4002
4154
  }
4003
4155
  ],
4004
- hints: '"selector" <deltaX> <deltaY>, --duration 500 | see: click, scroll'
4156
+ hints: '@ref or "selector" <deltaX> <deltaY>, --duration 500 | see: click, scroll'
4005
4157
  });
4006
4158
  var highlight = endpoint({
4007
4159
  path: "/highlight",
@@ -4012,13 +4164,19 @@ var highlight = endpoint({
4012
4164
  Great for showing users what you found or pointing out issues. Use /unhighlight to remove.`,
4013
4165
  category: "interaction",
4014
4166
  input: L.object({
4015
- selector: L.string.describe("CSS selector"),
4167
+ ref: L.string.describe("Ref ID from /tree output (e.g., 1, 42) - preferred for efficiency").optional,
4168
+ selector: L.string.describe("CSS selector").optional,
4016
4169
  label: L.string.describe("Label text to show").optional,
4017
4170
  color: L.string.describe("CSS color (default #6366f1)").optional,
4018
4171
  duration: L.number.describe("Auto-hide after ms (omit for manual)").optional,
4019
4172
  window: L.string.describe("Target window ID").optional
4020
4173
  }),
4021
4174
  examples: [
4175
+ {
4176
+ name: "by-ref",
4177
+ input: { ref: "42", label: "Found it!" },
4178
+ description: "Highlight element by ref ID"
4179
+ },
4022
4180
  {
4023
4181
  name: "point-out",
4024
4182
  input: { selector: "#login-btn", label: "Click here" },
@@ -4036,9 +4194,9 @@ Great for showing users what you found or pointing out issues. Use /unhighlight
4036
4194
  }
4037
4195
  ],
4038
4196
  invalidExamples: [
4039
- { name: "missing-selector", input: {}, error: "selector is required" }
4197
+ { name: "missing-target", input: {}, error: "ref or selector is required" }
4040
4198
  ],
4041
- hints: '"selector", --label "text", --color #f00, --duration 3000 | see: unhighlight, screenshot'
4199
+ hints: '@ref or "selector", --label "text", --color #f00, --duration 3000 | see: unhighlight, screenshot'
4042
4200
  });
4043
4201
  var unhighlight = endpoint({
4044
4202
  path: "/unhighlight",
@@ -4054,13 +4212,14 @@ var scroll = endpoint({
4054
4212
  summary: "Scroll to element or position",
4055
4213
  description: `Smooth scroll with natural easing. Multiple modes:
4056
4214
 
4057
- - selector: Scroll element into view (most common)
4215
+ - ref/selector: Scroll element into view (most common)
4058
4216
  - x/y: Scroll to absolute position
4059
4217
  - deltaX/deltaY: Scroll relative to current position
4060
4218
 
4061
- At least one of selector, x, y, deltaX, or deltaY must be provided.`,
4219
+ At least one of ref, selector, x, y, deltaX, or deltaY must be provided.`,
4062
4220
  category: "interaction",
4063
4221
  input: L.object({
4222
+ ref: L.string.describe("Ref ID from /tree output (e.g., 1, 42) - preferred for efficiency").optional,
4064
4223
  selector: L.string.describe("CSS selector to scroll into view").optional,
4065
4224
  x: L.number.describe("Absolute X position in pixels").optional,
4066
4225
  y: L.number.describe("Absolute Y position in pixels").optional,
@@ -4072,6 +4231,11 @@ At least one of selector, x, y, deltaX, or deltaY must be provided.`,
4072
4231
  window: L.string.describe("Target window ID").optional
4073
4232
  }),
4074
4233
  examples: [
4234
+ {
4235
+ name: "by-ref",
4236
+ input: { ref: "42" },
4237
+ description: "Scroll element into view by ref ID"
4238
+ },
4075
4239
  {
4076
4240
  name: "to-element",
4077
4241
  input: { selector: "#pricing" },
@@ -4094,7 +4258,7 @@ At least one of selector, x, y, deltaX, or deltaY must be provided.`,
4094
4258
  description: "Slow animated scroll"
4095
4259
  }
4096
4260
  ],
4097
- hints: '"selector" or <deltaY>, --duration 500 | see: click, wait'
4261
+ hints: '@ref or "selector" or <deltaY>, --duration 500 | see: click, wait'
4098
4262
  });
4099
4263
  var wait = endpoint({
4100
4264
  path: "/wait",
@@ -4265,17 +4429,17 @@ var refresh = endpoint({
4265
4429
  path: "/refresh",
4266
4430
  method: "POST",
4267
4431
  summary: "Refresh the page",
4268
- description: "Hard reload the current page, bypassing cache. Use soft: true for cache-friendly reload.",
4432
+ description: "Hard reload the current page, bypassing all caches (CSS, JS, images). Use soft: true for cache-friendly reload.",
4269
4433
  category: "navigation",
4270
4434
  input: L.object({
4271
- soft: L.boolean.describe("Use cached resources if available (default false = hard refresh)").optional,
4435
+ soft: L.boolean.describe("Use cached resources if available (default false = hard refresh that busts all caches)").optional,
4272
4436
  window: L.string.describe("Target window ID").optional
4273
4437
  }),
4274
4438
  examples: [
4275
4439
  {
4276
4440
  name: "hard",
4277
4441
  input: {},
4278
- description: "Hard refresh (default, bypasses cache)"
4442
+ description: "Hard refresh (default, bypasses all caches)"
4279
4443
  },
4280
4444
  {
4281
4445
  name: "soft",
@@ -4530,12 +4694,18 @@ Return value is JSON-serialized. Promises are awaited.
4530
4694
  Response: { success: true, data: <return value> }`,
4531
4695
  category: "interaction",
4532
4696
  input: L.object({
4533
- selector: L.string.describe("CSS selector of the element"),
4697
+ ref: L.string.describe("Ref ID from /tree output (e.g., 1, 42) - preferred for efficiency").optional,
4698
+ selector: L.string.describe("CSS selector of the element").optional,
4534
4699
  method: L.string.describe("Method name to call or property name to get"),
4535
4700
  args: L.array(L.any).describe("Arguments to pass (omit to get property value)").optional,
4536
4701
  window: L.string.describe("Target window ID").optional
4537
4702
  }),
4538
4703
  examples: [
4704
+ {
4705
+ name: "by-ref",
4706
+ input: { ref: "42", method: "value" },
4707
+ description: "Get property by ref ID"
4708
+ },
4539
4709
  {
4540
4710
  name: "get-value",
4541
4711
  input: { selector: "#email", method: "value" },
@@ -4607,38 +4777,49 @@ Response: { success: true, data: <return value> }`,
4607
4777
  ],
4608
4778
  invalidExamples: [
4609
4779
  {
4610
- name: "missing-selector",
4780
+ name: "missing-target",
4611
4781
  input: { method: "click" },
4612
- error: "selector is required"
4782
+ error: "ref or selector is required"
4613
4783
  },
4614
4784
  {
4615
4785
  name: "missing-method",
4616
4786
  input: { selector: "#btn" },
4617
4787
  error: "method is required"
4618
4788
  }
4619
- ]
4789
+ ],
4790
+ hints: '@ref or "selector" <method>, --args [...] | see: eval, inspect'
4620
4791
  });
4621
4792
  var screenshot = endpoint({
4622
4793
  path: "/screenshot",
4623
4794
  method: "POST",
4624
4795
  summary: "Capture a screenshot",
4625
- description: `Capture the page or a specific element as base64 PNG/WebP/JPEG.
4796
+ description: `Capture the page or a specific element as PNG/WebP/JPEG.
4626
4797
 
4627
4798
  Works automatically in the Haltija Desktop app. In browser widget mode, captures viewport only.
4628
4799
 
4629
- Response: { success, image: "data:image/png;base64,...", width, height, source }`,
4800
+ When file=true (default from CLI), saves to /tmp/haltija-screenshots/ and returns file path.
4801
+ When file=false, returns base64 data URL in response JSON.
4802
+
4803
+ Response: { success, path?, image?, width, height, source }`,
4630
4804
  category: "debug",
4631
4805
  input: L.object({
4806
+ ref: L.string.describe("Ref ID from /tree output - capture specific element").optional,
4632
4807
  selector: L.string.describe("Element to capture (omit for full page)").optional,
4633
4808
  scale: L.number.describe("Scale factor (default 1)").optional,
4634
4809
  maxWidth: L.number.describe("Max width in pixels").optional,
4635
4810
  maxHeight: L.number.describe("Max height in pixels").optional,
4636
4811
  window: L.string.describe("Target window ID").optional,
4637
4812
  chyron: L.boolean.describe("Burn page title, URL, timestamp into image (default true, set false for clean screenshot)").optional,
4638
- delay: L.number.describe("Wait ms before capturing (e.g. 1000 to let page settle after navigation)").optional
4813
+ delay: L.number.describe("Wait ms before capturing (e.g. 1000 to let page settle after navigation)").optional,
4814
+ file: L.boolean.describe("Save to disk and return file path instead of data URL (default true \u2014 pass false for base64)").optional
4639
4815
  }),
4640
4816
  examples: [
4641
4817
  { name: "full-page", input: {}, description: "Capture entire page with chyron showing URL/title" },
4818
+ {
4819
+ name: "by-ref",
4820
+ input: { ref: "42" },
4821
+ description: "Capture element by ref ID"
4822
+ },
4642
4823
  {
4643
4824
  name: "element",
4644
4825
  input: { selector: "#chart" },
@@ -5080,6 +5261,119 @@ Great for debugging test failures - call this when something goes wrong.`,
5080
5261
  }
5081
5262
  ]
5082
5263
  });
5264
+ var videoStart = endpoint({
5265
+ path: "/video/start",
5266
+ method: "POST",
5267
+ summary: "Start video recording",
5268
+ description: `Start recording the browser tab as WebM video. Requires the Haltija Desktop app.
5269
+
5270
+ The recording saves to /tmp/haltija-videos/ when stopped. Max duration is capped to prevent runaway recordings.
5271
+
5272
+ Response: { success, recordingId }`,
5273
+ category: "debug",
5274
+ input: L.object({
5275
+ maxDuration: L.number.describe("Max recording duration in seconds (default 60, max 300)").optional,
5276
+ window: L.string.describe("Target window ID").optional
5277
+ }),
5278
+ examples: [
5279
+ { name: "start", input: {}, description: "Start recording active tab" },
5280
+ { name: "long", input: { maxDuration: 120 }, description: "Record up to 2 minutes" }
5281
+ ],
5282
+ hints: "--maxDuration 120 | see: video-stop, video-status, screenshot"
5283
+ });
5284
+ var videoStop = endpoint({
5285
+ path: "/video/stop",
5286
+ method: "POST",
5287
+ summary: "Stop video recording",
5288
+ description: `Stop recording and save the video file.
5289
+
5290
+ Response: { success, path, duration, size }`,
5291
+ category: "debug",
5292
+ input: L.object({
5293
+ window: L.string.describe("Target window ID").optional
5294
+ }),
5295
+ examples: [
5296
+ { name: "stop", input: {}, description: "Stop recording and get file path" }
5297
+ ],
5298
+ hints: "| see: video-start, video-status"
5299
+ });
5300
+ var videoStatus = endpoint({
5301
+ path: "/video/status",
5302
+ method: "GET",
5303
+ summary: "Check video recording status",
5304
+ description: `Check if video recording is active.
5305
+
5306
+ Response: { recording, recordingId?, duration?, window? }`,
5307
+ category: "debug",
5308
+ input: L.object({}),
5309
+ examples: [
5310
+ { name: "check", input: {}, description: "Check recording state" }
5311
+ ],
5312
+ hints: "| see: video-start, video-stop"
5313
+ });
5314
+ var dialogConfigure = endpoint({
5315
+ path: "/dialog/configure",
5316
+ method: "POST",
5317
+ summary: "Configure native dialog auto-response policy",
5318
+ description: `Set how native browser dialogs (alert, confirm, prompt) are handled.
5319
+
5320
+ By default, Haltija intercepts all native dialogs and auto-responds:
5321
+ - alert: dismissed immediately
5322
+ - confirm: accepted (returns true)
5323
+ - prompt: dismissed (returns null)
5324
+
5325
+ Configure the policy **before** triggering actions that cause dialogs.
5326
+ Each dialog is logged and reported via the dialog/opened push event.
5327
+
5328
+ Response: { policy: { alert, confirm, prompt, beforeunload } }`,
5329
+ category: "dialog",
5330
+ input: L.object({
5331
+ alert: L.string.describe('"dismiss" (only option for alerts)').optional,
5332
+ confirm: L.string.describe('"accept" or "dismiss"').optional,
5333
+ prompt: L.any.describe('"dismiss" or { "response": "text" } to auto-fill').optional,
5334
+ beforeunload: L.string.describe('"allow" or "block" \u2014 controls page unload').optional,
5335
+ window: L.string.describe("Target window ID").optional
5336
+ }),
5337
+ examples: [
5338
+ {
5339
+ name: "accept-confirms",
5340
+ input: { confirm: "accept" },
5341
+ description: "Auto-accept all confirm dialogs"
5342
+ },
5343
+ {
5344
+ name: "dismiss-confirms",
5345
+ input: { confirm: "dismiss" },
5346
+ description: "Auto-dismiss (cancel) all confirm dialogs"
5347
+ },
5348
+ {
5349
+ name: "auto-fill-prompt",
5350
+ input: { prompt: { response: "my answer" } },
5351
+ description: "Auto-fill prompt dialogs with text"
5352
+ }
5353
+ ],
5354
+ cli: {
5355
+ name: "dialog-configure",
5356
+ args: [],
5357
+ flags: ["--confirm", "--prompt", "--beforeunload"]
5358
+ }
5359
+ });
5360
+ var dialogHistory = endpoint({
5361
+ path: "/dialog/history",
5362
+ method: "GET",
5363
+ summary: "Get recent dialog history",
5364
+ description: `Returns a list of recently intercepted native dialogs.
5365
+
5366
+ Each entry includes: type (alert/confirm/prompt), message, response given, timestamp.
5367
+ Buffer holds the last 50 dialogs.
5368
+
5369
+ Response: { history: [{ type, message, defaultValue?, response, timestamp }] }`,
5370
+ category: "dialog",
5371
+ cli: {
5372
+ name: "dialog-history",
5373
+ args: [],
5374
+ isGet: true
5375
+ }
5376
+ });
5083
5377
  var status = endpoint({
5084
5378
  path: "/status",
5085
5379
  method: "GET",
@@ -5287,6 +5581,11 @@ var endpoints = {
5287
5581
  testSuite,
5288
5582
  testValidate,
5289
5583
  snapshot,
5584
+ videoStart,
5585
+ videoStop,
5586
+ videoStatus,
5587
+ dialogConfigure,
5588
+ dialogHistory,
5290
5589
  status,
5291
5590
  stats,
5292
5591
  version,
@@ -5761,6 +6060,7 @@ registerHandler(click, async (body, ctx) => {
5761
6060
  registerHandler(query, async (body, ctx) => {
5762
6061
  const windowId = body.window || ctx.targetWindowId;
5763
6062
  const response = await ctx.requestFromBrowser("dom", "query", {
6063
+ ref: body.ref,
5764
6064
  selector: body.selector,
5765
6065
  all: body.all
5766
6066
  }, 5000, windowId);
@@ -5778,16 +6078,18 @@ registerHandler(fetchUrl, async (body, ctx) => {
5778
6078
  });
5779
6079
  registerHandler(call, async (body, ctx) => {
5780
6080
  const windowId = body.window || ctx.targetWindowId;
5781
- const selector = JSON.stringify(body.selector);
5782
- const resolveExpr = `(window.__haltija_resolveSelector || document.querySelector.bind(document))(${selector})`;
6081
+ const ref = body.ref;
6082
+ const selector = body.selector;
5783
6083
  const method = body.method;
5784
6084
  const args = body.args;
6085
+ const resolveExpr = ref ? `window.__haltija_refRegistry?.resolve(${JSON.stringify(ref)})` : `(window.__haltija_resolveSelector || document.querySelector.bind(document))(${JSON.stringify(selector)})`;
6086
+ const targetDesc = ref ? `@${ref}` : selector || "(none)";
5785
6087
  let code;
5786
6088
  if (args !== undefined) {
5787
6089
  const argsJson = JSON.stringify(args);
5788
6090
  code = `(function() {
5789
6091
  const el = ${resolveExpr};
5790
- if (!el) return { success: false, error: 'Element not found: ${body.selector.replace(/'/g, "\\'")}' };
6092
+ if (!el) return { success: false, error: 'Element not found: ${targetDesc.replace(/'/g, "\\'")}' };
5791
6093
  if (typeof el[${JSON.stringify(method)}] !== 'function') {
5792
6094
  return { success: false, error: 'Method not found: ${method}' };
5793
6095
  }
@@ -5801,7 +6103,7 @@ registerHandler(call, async (body, ctx) => {
5801
6103
  } else {
5802
6104
  code = `(function() {
5803
6105
  const el = ${resolveExpr};
5804
- if (!el) return { success: false, error: 'Element not found: ${body.selector.replace(/'/g, "\\'")}' };
6106
+ if (!el) return { success: false, error: 'Element not found: ${targetDesc.replace(/'/g, "\\'")}' };
5805
6107
  try {
5806
6108
  const value = el[${JSON.stringify(method)}];
5807
6109
  return { success: true, data: value };
@@ -5817,31 +6119,41 @@ registerHandler(call, async (body, ctx) => {
5817
6119
  return Response.json(response, { headers: ctx.headers });
5818
6120
  });
5819
6121
  registerHandler(drag, async (body, ctx) => {
6122
+ const ref = body.ref;
5820
6123
  const selector = body.selector;
5821
6124
  const deltaX = body.deltaX || 0;
5822
6125
  const deltaY = body.deltaY || 0;
5823
6126
  const duration = body.duration || 300;
5824
6127
  const steps = Math.max(5, Math.floor(duration / 16));
5825
6128
  const windowId = body.window || ctx.targetWindowId;
5826
- await ctx.requestFromBrowser("eval", "exec", {
5827
- code: `${qs(selector)}?.scrollIntoView({behavior: "smooth", block: "center"})`
5828
- }, 5000, windowId);
6129
+ const targetDesc = ref ? `@${ref}` : selector;
6130
+ if (ref) {
6131
+ await ctx.requestFromBrowser("eval", "exec", {
6132
+ code: `(window.__haltija_refRegistry?.resolve(${JSON.stringify(ref)}) || document.body)?.scrollIntoView({behavior: "smooth", block: "center"})`
6133
+ }, 5000, windowId);
6134
+ } else if (selector) {
6135
+ await ctx.requestFromBrowser("eval", "exec", {
6136
+ code: `${qs(selector)}?.scrollIntoView({behavior: "smooth", block: "center"})`
6137
+ }, 5000, windowId);
6138
+ }
5829
6139
  await sleep(100);
5830
- const inspectResponse = await ctx.requestFromBrowser("dom", "inspect", { selector }, 5000, windowId);
6140
+ const inspectResponse = await ctx.requestFromBrowser("dom", "inspect", { ref, selector }, 5000, windowId);
5831
6141
  if (!inspectResponse.success || !inspectResponse.data) {
5832
- return Response.json({ success: false, error: "Element not found" }, { headers: ctx.headers });
6142
+ return Response.json({ success: false, error: `Element not found: ${targetDesc}` }, { headers: ctx.headers });
5833
6143
  }
5834
6144
  const box = inspectResponse.data.box;
5835
6145
  const startX = box.x + box.width / 2;
5836
6146
  const startY = box.y + box.height / 2;
5837
6147
  for (const event of ["mouseenter", "mouseover", "mousemove"]) {
5838
6148
  await ctx.requestFromBrowser("events", "dispatch", {
6149
+ ref,
5839
6150
  selector,
5840
6151
  event,
5841
6152
  options: { clientX: startX, clientY: startY }
5842
6153
  }, 5000, windowId);
5843
6154
  }
5844
6155
  await ctx.requestFromBrowser("events", "dispatch", {
6156
+ ref,
5845
6157
  selector,
5846
6158
  event: "mousedown",
5847
6159
  options: { clientX: startX, clientY: startY }
@@ -5926,6 +6238,7 @@ registerHandler(key, async (body, ctx) => {
5926
6238
  registerHandler(inspect, async (body, ctx) => {
5927
6239
  const windowId = body.window || ctx.targetWindowId;
5928
6240
  const response = await ctx.requestFromBrowser("dom", "inspect", {
6241
+ ref: body.ref,
5929
6242
  selector: body.selector,
5930
6243
  fullStyles: body.fullStyles,
5931
6244
  matchedRules: body.matchedRules
@@ -5935,6 +6248,7 @@ registerHandler(inspect, async (body, ctx) => {
5935
6248
  registerHandler(inspectAll, async (body, ctx) => {
5936
6249
  const windowId = body.window || ctx.targetWindowId;
5937
6250
  const response = await ctx.requestFromBrowser("dom", "inspectAll", {
6251
+ ref: body.ref,
5938
6252
  selector: body.selector,
5939
6253
  limit: body.limit || 10,
5940
6254
  fullStyles: body.fullStyles,
@@ -5944,11 +6258,18 @@ registerHandler(inspectAll, async (body, ctx) => {
5944
6258
  });
5945
6259
  registerHandler(highlight, async (body, ctx) => {
5946
6260
  const windowId = body.window || ctx.targetWindowId;
5947
- await ctx.requestFromBrowser("eval", "exec", {
5948
- code: `${qs(body.selector)}?.scrollIntoView({behavior: "smooth", block: "center"})`
5949
- }, 5000, windowId);
6261
+ if (body.ref) {
6262
+ await ctx.requestFromBrowser("eval", "exec", {
6263
+ code: `(window.__haltija_refRegistry?.resolve(${JSON.stringify(body.ref)}) || document.body)?.scrollIntoView({behavior: "smooth", block: "center"})`
6264
+ }, 5000, windowId);
6265
+ } else if (body.selector) {
6266
+ await ctx.requestFromBrowser("eval", "exec", {
6267
+ code: `${qs(body.selector)}?.scrollIntoView({behavior: "smooth", block: "center"})`
6268
+ }, 5000, windowId);
6269
+ }
5950
6270
  await sleep(100);
5951
6271
  const response = await ctx.requestFromBrowser("dom", "highlight", {
6272
+ ref: body.ref,
5952
6273
  selector: body.selector,
5953
6274
  label: body.label,
5954
6275
  color: body.color,
@@ -5966,9 +6287,9 @@ registerHandler(navigate, async (body, ctx) => {
5966
6287
  return Response.json(response, { headers: ctx.headers });
5967
6288
  });
5968
6289
  registerHandler(refresh, async (body, ctx) => {
5969
- const hard = body.hard ?? false;
6290
+ const soft = body.soft ?? false;
5970
6291
  const windowId = body.window || ctx.targetWindowId;
5971
- const response = await ctx.requestFromBrowser("navigation", "refresh", { hard }, 5000, windowId);
6292
+ const response = await ctx.requestFromBrowser("navigation", "refresh", { soft }, 5000, windowId);
5972
6293
  return Response.json(response, { headers: ctx.headers });
5973
6294
  });
5974
6295
  registerHandler(tree, async (body, ctx) => {
@@ -5991,6 +6312,7 @@ registerHandler(screenshot, async (body, ctx) => {
5991
6312
  ctx.updateSessionAffinity(windowId);
5992
6313
  }
5993
6314
  const response = await ctx.requestFromBrowser("dom", "screenshot", {
6315
+ ref: body.ref,
5994
6316
  selector: body.selector,
5995
6317
  format: body.format,
5996
6318
  quality: body.quality,
@@ -6004,6 +6326,23 @@ registerHandler(screenshot, async (body, ctx) => {
6004
6326
  ...response,
6005
6327
  window: windowInfo || { id: windowId || "unknown", url: "unknown", title: "unknown" }
6006
6328
  };
6329
+ if (body.file !== false && enrichedResponse.data?.image) {
6330
+ const dataUrl = enrichedResponse.data.image;
6331
+ const match = dataUrl.match(/^data:image\/(\w+);base64,(.+)$/);
6332
+ if (match) {
6333
+ const ext = match[1] === "jpeg" ? "jpg" : match[1];
6334
+ const base64 = match[2];
6335
+ const dir = "/tmp/haltija-screenshots";
6336
+ const { mkdirSync, writeFileSync } = await import("fs");
6337
+ mkdirSync(dir, { recursive: true });
6338
+ const shortId = Math.random().toString(36).slice(2, 6);
6339
+ const filename = `hj-${Date.now()}-${shortId}.${ext}`;
6340
+ const filepath = `${dir}/${filename}`;
6341
+ writeFileSync(filepath, Buffer.from(base64, "base64"));
6342
+ enrichedResponse.data.path = filepath;
6343
+ delete enrichedResponse.data.image;
6344
+ }
6345
+ }
6007
6346
  return Response.json(enrichedResponse, { headers: ctx.headers });
6008
6347
  });
6009
6348
  registerHandler(tabsOpen, async (body, ctx) => {
@@ -6434,11 +6773,13 @@ registerHandler(scroll, async (body, ctx) => {
6434
6773
  };
6435
6774
  const easing = easings[${JSON.stringify(easing)}] || easings['ease-out'];
6436
6775
  `;
6437
- if (body.selector) {
6776
+ if (body.ref || body.selector) {
6777
+ const resolveCode = body.ref ? `window.__haltija_refRegistry?.resolve(${JSON.stringify(body.ref)})` : `(window.__haltija_resolveSelector || document.querySelector.bind(document))(${JSON.stringify(body.selector)})`;
6778
+ const targetDesc = body.ref ? `@${body.ref}` : body.selector;
6438
6779
  const code = `
6439
6780
  (async () => {
6440
- const el = (window.__haltija_resolveSelector || document.querySelector.bind(document))(${JSON.stringify(body.selector)});
6441
- if (!el) return { success: false, error: 'Element not found' };
6781
+ const el = ${resolveCode};
6782
+ if (!el) return { success: false, error: 'Element not found: ${targetDesc}' };
6442
6783
  const rect = el.getBoundingClientRect();
6443
6784
  const blockAlign = ${JSON.stringify(block)};
6444
6785
  let targetY;
@@ -6510,6 +6851,41 @@ registerHandler(scroll, async (body, ctx) => {
6510
6851
  return Response.json({ success: false, error: "Must provide selector, x/y coordinates, or deltaX/deltaY" }, { status: 400, headers: ctx.headers });
6511
6852
  }
6512
6853
  });
6854
+ registerHandler(videoStart, async (body, ctx) => {
6855
+ const windowId = body.window || ctx.targetWindowId;
6856
+ const maxDuration = Math.min(body.maxDuration || 60, 300);
6857
+ const response = await ctx.requestFromBrowser("video", "start", { maxDuration }, 1e4, windowId);
6858
+ return Response.json(response, { headers: ctx.headers });
6859
+ });
6860
+ registerHandler(videoStop, async (body, ctx) => {
6861
+ const windowId = body.window || ctx.targetWindowId;
6862
+ const response = await ctx.requestFromBrowser("video", "stop", {}, 30000, windowId);
6863
+ if (response.success && response.data) {
6864
+ return Response.json({
6865
+ success: true,
6866
+ path: response.data.path,
6867
+ duration: response.data.duration,
6868
+ size: response.data.size,
6869
+ format: response.data.format || "webm"
6870
+ }, { headers: ctx.headers });
6871
+ }
6872
+ return Response.json(response, { headers: ctx.headers });
6873
+ });
6874
+ registerHandler(videoStatus, async (_body, ctx) => {
6875
+ const windowId = ctx.targetWindowId;
6876
+ const response = await ctx.requestFromBrowser("video", "status", {}, 5000, windowId);
6877
+ return Response.json(response, { headers: ctx.headers });
6878
+ });
6879
+ registerHandler(dialogConfigure, async (body, ctx) => {
6880
+ const windowId = body.window || ctx.targetWindowId;
6881
+ const response = await ctx.requestFromBrowser("dialog", "configure", body, 5000, windowId);
6882
+ return Response.json(response, { headers: ctx.headers });
6883
+ });
6884
+ registerHandler(dialogHistory, async (_body, ctx) => {
6885
+ const windowId = ctx.targetWindowId;
6886
+ const response = await ctx.requestFromBrowser("dialog", "history", {}, 5000, windowId);
6887
+ return Response.json(response, { headers: ctx.headers });
6888
+ });
6513
6889
 
6514
6890
  // src/api-router.ts
6515
6891
  function isDeprecated(endpoint2) {
@@ -10012,15 +10388,7 @@ var serverConfig = {
10012
10388
  }
10013
10389
  const widgetSessionId = data.payload.serverSessionId;
10014
10390
  if (widgetSessionId && widgetSessionId !== SERVER_SESSION_ID) {
10015
- console.log(`${LOG_PREFIX} Widget session mismatch (${widgetSessionId} vs ${SERVER_SESSION_ID}), sending reload`);
10016
- wsTyped.send(JSON.stringify({
10017
- id: uid(),
10018
- channel: "system",
10019
- action: "reload",
10020
- payload: { reason: "session_mismatch" },
10021
- timestamp: Date.now(),
10022
- source: "server"
10023
- }));
10391
+ console.log(`${LOG_PREFIX} Widget from different session (${widgetSessionId.slice(0, 8)}... vs ${SERVER_SESSION_ID.slice(0, 8)}...), accepting anyway`);
10024
10392
  }
10025
10393
  if (windowId) {
10026
10394
  const recordingSession = activeRecordingSessions.get(windowId);