headless_browser_tool 0.1.1

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.
Files changed (74) hide show
  1. checksums.yaml +7 -0
  2. data/.claude/settings.json +21 -0
  3. data/.rubocop.yml +56 -0
  4. data/.ruby-version +1 -0
  5. data/CHANGELOG.md +5 -0
  6. data/CLAUDE.md +298 -0
  7. data/LICENSE.md +7 -0
  8. data/README.md +522 -0
  9. data/Rakefile +12 -0
  10. data/config.ru +8 -0
  11. data/exe/hbt +7 -0
  12. data/lib/headless_browser_tool/browser.rb +374 -0
  13. data/lib/headless_browser_tool/browser_adapter.rb +320 -0
  14. data/lib/headless_browser_tool/cli.rb +34 -0
  15. data/lib/headless_browser_tool/directory_setup.rb +25 -0
  16. data/lib/headless_browser_tool/logger.rb +31 -0
  17. data/lib/headless_browser_tool/server.rb +150 -0
  18. data/lib/headless_browser_tool/session_manager.rb +199 -0
  19. data/lib/headless_browser_tool/session_middleware.rb +158 -0
  20. data/lib/headless_browser_tool/session_persistence.rb +146 -0
  21. data/lib/headless_browser_tool/stdio_server.rb +73 -0
  22. data/lib/headless_browser_tool/strict_session_middleware.rb +88 -0
  23. data/lib/headless_browser_tool/tools/attach_file_tool.rb +40 -0
  24. data/lib/headless_browser_tool/tools/auto_narrate_tool.rb +155 -0
  25. data/lib/headless_browser_tool/tools/base_tool.rb +39 -0
  26. data/lib/headless_browser_tool/tools/check_tool.rb +35 -0
  27. data/lib/headless_browser_tool/tools/choose_tool.rb +56 -0
  28. data/lib/headless_browser_tool/tools/click_button_tool.rb +49 -0
  29. data/lib/headless_browser_tool/tools/click_link_tool.rb +48 -0
  30. data/lib/headless_browser_tool/tools/click_tool.rb +45 -0
  31. data/lib/headless_browser_tool/tools/close_window_tool.rb +31 -0
  32. data/lib/headless_browser_tool/tools/double_click_tool.rb +37 -0
  33. data/lib/headless_browser_tool/tools/drag_tool.rb +46 -0
  34. data/lib/headless_browser_tool/tools/evaluate_script_tool.rb +20 -0
  35. data/lib/headless_browser_tool/tools/execute_script_tool.rb +29 -0
  36. data/lib/headless_browser_tool/tools/fill_in_tool.rb +66 -0
  37. data/lib/headless_browser_tool/tools/find_all_tool.rb +42 -0
  38. data/lib/headless_browser_tool/tools/find_element_tool.rb +21 -0
  39. data/lib/headless_browser_tool/tools/find_elements_containing_text_tool.rb +259 -0
  40. data/lib/headless_browser_tool/tools/get_attribute_tool.rb +21 -0
  41. data/lib/headless_browser_tool/tools/get_current_path_tool.rb +16 -0
  42. data/lib/headless_browser_tool/tools/get_current_url_tool.rb +16 -0
  43. data/lib/headless_browser_tool/tools/get_narration_history_tool.rb +35 -0
  44. data/lib/headless_browser_tool/tools/get_page_context_tool.rb +188 -0
  45. data/lib/headless_browser_tool/tools/get_page_source_tool.rb +16 -0
  46. data/lib/headless_browser_tool/tools/get_page_title_tool.rb +16 -0
  47. data/lib/headless_browser_tool/tools/get_session_info_tool.rb +37 -0
  48. data/lib/headless_browser_tool/tools/get_text_tool.rb +20 -0
  49. data/lib/headless_browser_tool/tools/get_value_tool.rb +20 -0
  50. data/lib/headless_browser_tool/tools/get_window_handles_tool.rb +29 -0
  51. data/lib/headless_browser_tool/tools/go_back_tool.rb +29 -0
  52. data/lib/headless_browser_tool/tools/go_forward_tool.rb +29 -0
  53. data/lib/headless_browser_tool/tools/has_element_tool.rb +21 -0
  54. data/lib/headless_browser_tool/tools/has_text_tool.rb +21 -0
  55. data/lib/headless_browser_tool/tools/hover_tool.rb +38 -0
  56. data/lib/headless_browser_tool/tools/is_visible_tool.rb +20 -0
  57. data/lib/headless_browser_tool/tools/maximize_window_tool.rb +34 -0
  58. data/lib/headless_browser_tool/tools/open_new_window_tool.rb +25 -0
  59. data/lib/headless_browser_tool/tools/refresh_tool.rb +32 -0
  60. data/lib/headless_browser_tool/tools/resize_window_tool.rb +43 -0
  61. data/lib/headless_browser_tool/tools/right_click_tool.rb +37 -0
  62. data/lib/headless_browser_tool/tools/save_page_tool.rb +32 -0
  63. data/lib/headless_browser_tool/tools/screenshot_tool.rb +199 -0
  64. data/lib/headless_browser_tool/tools/search_page_tool.rb +224 -0
  65. data/lib/headless_browser_tool/tools/search_source_tool.rb +148 -0
  66. data/lib/headless_browser_tool/tools/select_tool.rb +44 -0
  67. data/lib/headless_browser_tool/tools/switch_to_window_tool.rb +30 -0
  68. data/lib/headless_browser_tool/tools/uncheck_tool.rb +35 -0
  69. data/lib/headless_browser_tool/tools/visit_tool.rb +27 -0
  70. data/lib/headless_browser_tool/tools/visual_diff_tool.rb +177 -0
  71. data/lib/headless_browser_tool/tools.rb +104 -0
  72. data/lib/headless_browser_tool/version.rb +5 -0
  73. data/lib/headless_browser_tool.rb +8 -0
  74. metadata +256 -0
@@ -0,0 +1,16 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "base_tool"
4
+
5
+ module HeadlessBrowserTool
6
+ module Tools
7
+ class GetCurrentUrlTool < BaseTool
8
+ tool_name "get_current_url"
9
+ description "Get the current page URL"
10
+
11
+ def execute
12
+ browser.get_current_url
13
+ end
14
+ end
15
+ end
16
+ end
@@ -0,0 +1,35 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "base_tool"
4
+
5
+ module HeadlessBrowserTool
6
+ module Tools
7
+ class GetNarrationHistoryTool < BaseTool
8
+ tool_name "get_narration_history"
9
+ description "Get the history of narrated events since auto_narrate was enabled"
10
+
11
+ def execute
12
+ history = browser.evaluate_script("window.getAINarration ? window.getAINarration() : []")
13
+
14
+ if history.nil? || history.empty?
15
+ "No narration history available. Enable auto_narrate first."
16
+ else
17
+ format_history(history)
18
+ end
19
+ end
20
+
21
+ private
22
+
23
+ def format_history(history)
24
+ output = ["🎬 Browser Event History:"]
25
+
26
+ history.last(20).each do |event|
27
+ time = Time.parse(event["timestamp"]).strftime("%H:%M:%S")
28
+ output << "[#{time}] #{event["message"]}"
29
+ end
30
+
31
+ output.join("\n")
32
+ end
33
+ end
34
+ end
35
+ end
@@ -0,0 +1,188 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "base_tool"
4
+
5
+ module HeadlessBrowserTool
6
+ module Tools
7
+ class GetPageContextTool < BaseTool
8
+ tool_name "get_page_context"
9
+ description "Get a structured understanding of the current page"
10
+
11
+ def execute
12
+ context_script = <<~JS
13
+ (() => {
14
+ // Helper to generate unique selector for elements
15
+ const getUniqueSelector = (elem) => {
16
+ if (elem.id) {
17
+ return '#' + CSS.escape(elem.id);
18
+ }
19
+
20
+ if (elem.className && typeof elem.className === 'string') {
21
+ const classes = elem.className.trim().split(/\\s+/)
22
+ .filter(c => c.length > 0)
23
+ .map(c => '.' + CSS.escape(c))
24
+ .join('');
25
+ if (classes && document.querySelectorAll(elem.tagName + classes).length === 1) {
26
+ return elem.tagName.toLowerCase() + classes;
27
+ }
28
+ }
29
+
30
+ // Build path from root
31
+ const path = [];
32
+ let current = elem;
33
+ while (current && current.nodeType === Node.ELEMENT_NODE) {
34
+ let selector = current.tagName.toLowerCase();
35
+ if (current.id) {
36
+ selector = '#' + CSS.escape(current.id);
37
+ path.unshift(selector);
38
+ break;
39
+ } else {
40
+ let sibling = current;
41
+ let nth = 1;
42
+ while (sibling.previousElementSibling) {
43
+ sibling = sibling.previousElementSibling;
44
+ if (sibling.tagName === current.tagName) nth++;
45
+ }
46
+ if (nth > 1) selector += ':nth-of-type(' + nth + ')';
47
+ }
48
+ path.unshift(selector);
49
+ current = current.parentElement;
50
+ }
51
+ return path.join(' > ');
52
+ };
53
+
54
+ // Identify page type
55
+ const identifyPageType = () => {
56
+ const url = window.location.href;
57
+ const title = document.title.toLowerCase();
58
+ const h1 = document.querySelector('h1')?.textContent.toLowerCase() || '';
59
+
60
+ if (url.includes('login') || title.includes('login') || h1.includes('login')) return 'login';
61
+ if (url.includes('search') || title.includes('search')) return 'search_results';
62
+ if (url.includes('cart') || title.includes('cart')) return 'shopping_cart';
63
+ if (url.includes('checkout')) return 'checkout';
64
+ if (document.querySelector('form[method="post"]')) return 'form_page';
65
+ if (document.querySelectorAll('article').length > 3) return 'article_list';
66
+ if (document.querySelector('article')) return 'article';
67
+ return 'general';
68
+ };
69
+
70
+ // Get main navigation
71
+ const getNavigation = () => {
72
+ const nav = document.querySelector('nav') || document.querySelector('[role="navigation"]');
73
+ if (!nav) return [];
74
+
75
+ return Array.from(nav.querySelectorAll('a')).slice(0, 10).map((a, index) => ({
76
+ text: a.textContent.trim(),
77
+ href: a.href,
78
+ selector: getUniqueSelector(a)
79
+ }));
80
+ };
81
+
82
+ // Get actionable elements
83
+ const getActions = () => {
84
+ const actions = [];
85
+
86
+ // Primary buttons
87
+ document.querySelectorAll('button[type="submit"], button.primary, button.btn-primary').forEach(btn => {
88
+ if (btn.offsetWidth > 0) {
89
+ actions.push({
90
+ type: 'primary_button',
91
+ text: btn.textContent.trim(),
92
+ selector: btn.className ? `.${btn.className.split(' ')[0]}` : 'button'
93
+ });
94
+ }
95
+ });
96
+
97
+ // Forms
98
+ document.querySelectorAll('form').forEach((form, index) => {
99
+ const formInputs = Array.from(form.querySelectorAll('input:not([type="hidden"]), textarea, select'))
100
+ .map(input => ({
101
+ name: input.name || input.id,
102
+ type: input.type || 'text',
103
+ required: input.required,
104
+ value: input.value,
105
+ selector: getUniqueSelector(input)
106
+ }));
107
+
108
+ if (formInputs.length > 0) {
109
+ actions.push({
110
+ type: 'form',
111
+ action: form.action,
112
+ method: form.method,
113
+ selector: getUniqueSelector(form),
114
+ inputs: formInputs
115
+ });
116
+ }
117
+ });
118
+
119
+ return actions;
120
+ };
121
+
122
+ // Get key content areas
123
+ const getContentAreas = () => {
124
+ const areas = {};
125
+
126
+ // Main content
127
+ const main = document.querySelector('main') || document.querySelector('[role="main"]') || document.querySelector('#content');
128
+ if (main) {
129
+ areas.main = main.textContent.trim().substring(0, 200) + '...';
130
+ }
131
+
132
+ // Headings structure
133
+ areas.headings = Array.from(document.querySelectorAll('h1, h2, h3')).slice(0, 10).map(h => ({
134
+ level: h.tagName,
135
+ text: h.textContent.trim(),
136
+ selector: getUniqueSelector(h)
137
+ }));
138
+
139
+ // Errors or alerts
140
+ const alerts = document.querySelectorAll('[role="alert"], .error, .alert, .warning, .success');
141
+ if (alerts.length > 0) {
142
+ areas.alerts = Array.from(alerts).map(a => ({
143
+ text: a.textContent.trim(),
144
+ selector: getUniqueSelector(a)
145
+ }));
146
+ }
147
+
148
+ return areas;
149
+ };
150
+
151
+ // Get data attributes that might be useful
152
+ const getDataAttributes = () => {
153
+ const elements = document.querySelectorAll('[data-testid], [data-test], [data-cy]');
154
+ return Array.from(elements).slice(0, 20).map(el => ({
155
+ testId: el.dataset.testid || el.dataset.test || el.dataset.cy,
156
+ tag: el.tagName.toLowerCase(),
157
+ text: el.textContent.trim().substring(0, 50),
158
+ selector: getUniqueSelector(el)
159
+ }));
160
+ };
161
+
162
+ return {
163
+ url: window.location.href,
164
+ title: document.title,
165
+ pageType: identifyPageType(),
166
+ navigation: getNavigation(),
167
+ actions: getActions(),
168
+ contentAreas: getContentAreas(),
169
+ testIds: getDataAttributes(),
170
+ metrics: {
171
+ loadTime: performance.timing.loadEventEnd - performance.timing.navigationStart,
172
+ domElements: document.querySelectorAll('*').length,
173
+ images: document.images.length,
174
+ scripts: document.scripts.length
175
+ }
176
+ };
177
+ })();
178
+ JS
179
+
180
+ context = browser.evaluate_script(context_script)
181
+ return { error: "Unable to get page context" } if context.nil?
182
+
183
+ # Return raw context data instead of formatted string
184
+ context
185
+ end
186
+ end
187
+ end
188
+ end
@@ -0,0 +1,16 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "base_tool"
4
+
5
+ module HeadlessBrowserTool
6
+ module Tools
7
+ class GetPageSourceTool < BaseTool
8
+ tool_name "get_page_source"
9
+ description "Get full HTML source of current page"
10
+
11
+ def execute
12
+ browser.get_page_source
13
+ end
14
+ end
15
+ end
16
+ end
@@ -0,0 +1,16 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "base_tool"
4
+
5
+ module HeadlessBrowserTool
6
+ module Tools
7
+ class GetPageTitleTool < BaseTool
8
+ tool_name "get_page_title"
9
+ description "Get the page title from <title> tag"
10
+
11
+ def execute
12
+ browser.get_page_title
13
+ end
14
+ end
15
+ end
16
+ end
@@ -0,0 +1,37 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "base_tool"
4
+
5
+ module HeadlessBrowserTool
6
+ module Tools
7
+ class GetSessionInfoTool < BaseTool
8
+ tool_name "get_session_info"
9
+ description "Get information about the current browser session"
10
+
11
+ def execute
12
+ if HeadlessBrowserTool::Server.single_session_mode
13
+ {
14
+ mode: "single_session",
15
+ session_id: "shared",
16
+ message: "Server is running in single session mode"
17
+ }
18
+ else
19
+ session_id = Thread.current[:hbt_session_id] || "default"
20
+ session_info = HeadlessBrowserTool::Server.session_manager.session_info
21
+
22
+ current_session = session_info[:session_data][session_id]
23
+
24
+ {
25
+ mode: "multi_session",
26
+ session_id: session_id,
27
+ created_at: current_session&.dig(:created_at),
28
+ last_activity: current_session&.dig(:last_activity),
29
+ idle_time: current_session&.dig(:idle_time),
30
+ active_sessions: session_info[:active_sessions],
31
+ total_sessions: session_info[:session_count]
32
+ }
33
+ end
34
+ end
35
+ end
36
+ end
37
+ end
@@ -0,0 +1,20 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "base_tool"
4
+
5
+ module HeadlessBrowserTool
6
+ module Tools
7
+ class GetTextTool < BaseTool
8
+ tool_name "get_text"
9
+ description "Get the visible text content of an element"
10
+
11
+ arguments do
12
+ required(:selector).filled(:string).description("CSS selector of the element")
13
+ end
14
+
15
+ def execute(selector:)
16
+ browser.get_text(selector)
17
+ end
18
+ end
19
+ end
20
+ end
@@ -0,0 +1,20 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "base_tool"
4
+
5
+ module HeadlessBrowserTool
6
+ module Tools
7
+ class GetValueTool < BaseTool
8
+ tool_name "get_value"
9
+ description "Get the value of an input field"
10
+
11
+ arguments do
12
+ required(:selector).filled(:string).description("CSS selector of the input field")
13
+ end
14
+
15
+ def execute(selector:)
16
+ browser.get_value(selector)
17
+ end
18
+ end
19
+ end
20
+ end
@@ -0,0 +1,29 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "base_tool"
4
+
5
+ module HeadlessBrowserTool
6
+ module Tools
7
+ class GetWindowHandlesTool < BaseTool
8
+ tool_name "get_window_handles"
9
+ description "Get array of all window handles"
10
+
11
+ def execute
12
+ handles = browser.get_window_handles
13
+ current_handle = browser.current_window_handle
14
+
15
+ {
16
+ current_window: current_handle,
17
+ windows: handles.map.with_index do |handle, index|
18
+ {
19
+ handle: handle,
20
+ index: index,
21
+ is_current: handle == current_handle
22
+ }
23
+ end,
24
+ total_windows: handles.size
25
+ }
26
+ end
27
+ end
28
+ end
29
+ end
@@ -0,0 +1,29 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "base_tool"
4
+
5
+ module HeadlessBrowserTool
6
+ module Tools
7
+ class GoBackTool < BaseTool
8
+ tool_name "go_back"
9
+ description "Navigate back in browser history"
10
+
11
+ def execute
12
+ url_before = browser.current_url
13
+ browser.title
14
+
15
+ browser.go_back
16
+
17
+ {
18
+ navigation: {
19
+ from: url_before,
20
+ to: browser.current_url,
21
+ title: browser.title,
22
+ navigated: browser.current_url != url_before
23
+ },
24
+ status: "navigated_back"
25
+ }
26
+ end
27
+ end
28
+ end
29
+ end
@@ -0,0 +1,29 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "base_tool"
4
+
5
+ module HeadlessBrowserTool
6
+ module Tools
7
+ class GoForwardTool < BaseTool
8
+ tool_name "go_forward"
9
+ description "Navigate forward in browser history"
10
+
11
+ def execute
12
+ url_before = browser.current_url
13
+ browser.title
14
+
15
+ browser.go_forward
16
+
17
+ {
18
+ navigation: {
19
+ from: url_before,
20
+ to: browser.current_url,
21
+ title: browser.title,
22
+ navigated: browser.current_url != url_before
23
+ },
24
+ status: "navigated_forward"
25
+ }
26
+ end
27
+ end
28
+ end
29
+ end
@@ -0,0 +1,21 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "base_tool"
4
+
5
+ module HeadlessBrowserTool
6
+ module Tools
7
+ class HasElementTool < BaseTool
8
+ tool_name "has_element"
9
+ description "Check if an element exists on the page"
10
+
11
+ arguments do
12
+ required(:selector).filled(:string).description("CSS selector of the element")
13
+ optional(:wait_seconds).filled(:integer).description("Optional timeout in seconds")
14
+ end
15
+
16
+ def execute(selector:, wait_seconds: nil)
17
+ browser.has_element?(selector, wait_seconds)
18
+ end
19
+ end
20
+ end
21
+ end
@@ -0,0 +1,21 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "base_tool"
4
+
5
+ module HeadlessBrowserTool
6
+ module Tools
7
+ class HasTextTool < BaseTool
8
+ tool_name "has_text"
9
+ description "Check if text appears on the page"
10
+
11
+ arguments do
12
+ required(:text).filled(:string).description("Text to search for")
13
+ optional(:wait_seconds).filled(:integer).description("Optional timeout in seconds")
14
+ end
15
+
16
+ def execute(text:, wait_seconds: nil)
17
+ browser.has_text?(text, wait_seconds)
18
+ end
19
+ end
20
+ end
21
+ end
@@ -0,0 +1,38 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "base_tool"
4
+
5
+ module HeadlessBrowserTool
6
+ module Tools
7
+ class HoverTool < BaseTool
8
+ tool_name "hover"
9
+ description "Hover over an element by CSS selector"
10
+
11
+ arguments do
12
+ required(:selector).filled(:string).description("CSS selector of the element to hover over")
13
+ end
14
+
15
+ def execute(selector:)
16
+ element = browser.find(selector)
17
+ element_info = {
18
+ tag_name: element.tag_name,
19
+ text: element.text.strip,
20
+ visible: element.visible?,
21
+ attributes: {
22
+ id: element[:id],
23
+ class: element[:class],
24
+ title: element[:title]
25
+ }.compact
26
+ }
27
+
28
+ browser.hover(selector)
29
+
30
+ {
31
+ selector: selector,
32
+ element: element_info,
33
+ status: "hovering"
34
+ }
35
+ end
36
+ end
37
+ end
38
+ end
@@ -0,0 +1,20 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "base_tool"
4
+
5
+ module HeadlessBrowserTool
6
+ module Tools
7
+ class IsVisibleTool < BaseTool
8
+ tool_name "is_visible"
9
+ description "Check if element is visible on page"
10
+
11
+ arguments do
12
+ required(:selector).filled(:string).description("CSS selector of the element to check")
13
+ end
14
+
15
+ def execute(selector:)
16
+ browser.is_visible?(selector)
17
+ end
18
+ end
19
+ end
20
+ end
@@ -0,0 +1,34 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "base_tool"
4
+
5
+ module HeadlessBrowserTool
6
+ module Tools
7
+ class MaximizeWindowTool < BaseTool
8
+ tool_name "maximize_window"
9
+ description "Maximize the browser window"
10
+
11
+ def execute
12
+ # Get window size before maximizing
13
+ size_before = browser.current_window_size
14
+
15
+ browser.maximize_window
16
+
17
+ # Get window size after maximizing
18
+ size_after = browser.current_window_size
19
+
20
+ {
21
+ size_before: {
22
+ width: size_before[0],
23
+ height: size_before[1]
24
+ },
25
+ size_after: {
26
+ width: size_after[0],
27
+ height: size_after[1]
28
+ },
29
+ status: "maximized"
30
+ }
31
+ end
32
+ end
33
+ end
34
+ end
@@ -0,0 +1,25 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "base_tool"
4
+
5
+ module HeadlessBrowserTool
6
+ module Tools
7
+ class OpenNewWindowTool < BaseTool
8
+ tool_name "open_new_window"
9
+ description "Open a new browser window/tab"
10
+
11
+ def execute
12
+ initial_windows = browser.windows
13
+ window_handle = browser.open_new_window
14
+
15
+ {
16
+ window_handle: window_handle,
17
+ total_windows: browser.windows.count,
18
+ previous_windows: initial_windows,
19
+ current_window: browser.current_window,
20
+ status: "opened"
21
+ }
22
+ end
23
+ end
24
+ end
25
+ end
@@ -0,0 +1,32 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "base_tool"
4
+
5
+ module HeadlessBrowserTool
6
+ module Tools
7
+ class RefreshTool < BaseTool
8
+ tool_name "refresh"
9
+ description "Reload the current page"
10
+
11
+ def execute
12
+ url_before = browser.current_url
13
+ title_before = browser.title
14
+
15
+ browser.refresh
16
+
17
+ # Brief wait for refresh to complete
18
+ sleep 0.1
19
+
20
+ {
21
+ url: browser.current_url,
22
+ title: browser.title,
23
+ changed: {
24
+ url: url_before != browser.current_url,
25
+ title: title_before != browser.title
26
+ },
27
+ status: "success"
28
+ }
29
+ end
30
+ end
31
+ end
32
+ end
@@ -0,0 +1,43 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "base_tool"
4
+
5
+ module HeadlessBrowserTool
6
+ module Tools
7
+ class ResizeWindowTool < BaseTool
8
+ tool_name "resize_window"
9
+ description "Resize the browser window"
10
+
11
+ arguments do
12
+ required(:width).filled(:integer).description("Window width in pixels")
13
+ required(:height).filled(:integer).description("Window height in pixels")
14
+ end
15
+
16
+ def execute(width:, height:)
17
+ # Get window size before resizing
18
+ size_before = browser.current_window_size
19
+
20
+ browser.resize_window(width, height)
21
+
22
+ # Get actual window size after resizing
23
+ size_after = browser.current_window_size
24
+
25
+ {
26
+ requested_size: {
27
+ width: width,
28
+ height: height
29
+ },
30
+ size_before: {
31
+ width: size_before[0],
32
+ height: size_before[1]
33
+ },
34
+ size_after: {
35
+ width: size_after[0],
36
+ height: size_after[1]
37
+ },
38
+ status: "resized"
39
+ }
40
+ end
41
+ end
42
+ end
43
+ end