code-puppy 0.0.169__py3-none-any.whl → 0.0.366__py3-none-any.whl

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 (243) hide show
  1. code_puppy/__init__.py +7 -1
  2. code_puppy/agents/__init__.py +8 -8
  3. code_puppy/agents/agent_c_reviewer.py +155 -0
  4. code_puppy/agents/agent_code_puppy.py +9 -2
  5. code_puppy/agents/agent_code_reviewer.py +90 -0
  6. code_puppy/agents/agent_cpp_reviewer.py +132 -0
  7. code_puppy/agents/agent_creator_agent.py +48 -9
  8. code_puppy/agents/agent_golang_reviewer.py +151 -0
  9. code_puppy/agents/agent_javascript_reviewer.py +160 -0
  10. code_puppy/agents/agent_manager.py +146 -199
  11. code_puppy/agents/agent_pack_leader.py +383 -0
  12. code_puppy/agents/agent_planning.py +163 -0
  13. code_puppy/agents/agent_python_programmer.py +165 -0
  14. code_puppy/agents/agent_python_reviewer.py +90 -0
  15. code_puppy/agents/agent_qa_expert.py +163 -0
  16. code_puppy/agents/agent_qa_kitten.py +208 -0
  17. code_puppy/agents/agent_security_auditor.py +181 -0
  18. code_puppy/agents/agent_terminal_qa.py +323 -0
  19. code_puppy/agents/agent_typescript_reviewer.py +166 -0
  20. code_puppy/agents/base_agent.py +1713 -1
  21. code_puppy/agents/event_stream_handler.py +350 -0
  22. code_puppy/agents/json_agent.py +12 -1
  23. code_puppy/agents/pack/__init__.py +34 -0
  24. code_puppy/agents/pack/bloodhound.py +304 -0
  25. code_puppy/agents/pack/husky.py +321 -0
  26. code_puppy/agents/pack/retriever.py +393 -0
  27. code_puppy/agents/pack/shepherd.py +348 -0
  28. code_puppy/agents/pack/terrier.py +287 -0
  29. code_puppy/agents/pack/watchdog.py +367 -0
  30. code_puppy/agents/prompt_reviewer.py +145 -0
  31. code_puppy/agents/subagent_stream_handler.py +276 -0
  32. code_puppy/api/__init__.py +13 -0
  33. code_puppy/api/app.py +169 -0
  34. code_puppy/api/main.py +21 -0
  35. code_puppy/api/pty_manager.py +446 -0
  36. code_puppy/api/routers/__init__.py +12 -0
  37. code_puppy/api/routers/agents.py +36 -0
  38. code_puppy/api/routers/commands.py +217 -0
  39. code_puppy/api/routers/config.py +74 -0
  40. code_puppy/api/routers/sessions.py +232 -0
  41. code_puppy/api/templates/terminal.html +361 -0
  42. code_puppy/api/websocket.py +154 -0
  43. code_puppy/callbacks.py +174 -4
  44. code_puppy/chatgpt_codex_client.py +283 -0
  45. code_puppy/claude_cache_client.py +586 -0
  46. code_puppy/cli_runner.py +916 -0
  47. code_puppy/command_line/add_model_menu.py +1079 -0
  48. code_puppy/command_line/agent_menu.py +395 -0
  49. code_puppy/command_line/attachments.py +395 -0
  50. code_puppy/command_line/autosave_menu.py +605 -0
  51. code_puppy/command_line/clipboard.py +527 -0
  52. code_puppy/command_line/colors_menu.py +520 -0
  53. code_puppy/command_line/command_handler.py +233 -627
  54. code_puppy/command_line/command_registry.py +150 -0
  55. code_puppy/command_line/config_commands.py +715 -0
  56. code_puppy/command_line/core_commands.py +792 -0
  57. code_puppy/command_line/diff_menu.py +863 -0
  58. code_puppy/command_line/load_context_completion.py +15 -22
  59. code_puppy/command_line/mcp/base.py +1 -4
  60. code_puppy/command_line/mcp/catalog_server_installer.py +175 -0
  61. code_puppy/command_line/mcp/custom_server_form.py +688 -0
  62. code_puppy/command_line/mcp/custom_server_installer.py +195 -0
  63. code_puppy/command_line/mcp/edit_command.py +148 -0
  64. code_puppy/command_line/mcp/handler.py +9 -4
  65. code_puppy/command_line/mcp/help_command.py +6 -5
  66. code_puppy/command_line/mcp/install_command.py +16 -27
  67. code_puppy/command_line/mcp/install_menu.py +685 -0
  68. code_puppy/command_line/mcp/list_command.py +3 -3
  69. code_puppy/command_line/mcp/logs_command.py +174 -65
  70. code_puppy/command_line/mcp/remove_command.py +2 -2
  71. code_puppy/command_line/mcp/restart_command.py +12 -4
  72. code_puppy/command_line/mcp/search_command.py +17 -11
  73. code_puppy/command_line/mcp/start_all_command.py +22 -13
  74. code_puppy/command_line/mcp/start_command.py +50 -31
  75. code_puppy/command_line/mcp/status_command.py +6 -7
  76. code_puppy/command_line/mcp/stop_all_command.py +11 -8
  77. code_puppy/command_line/mcp/stop_command.py +11 -10
  78. code_puppy/command_line/mcp/test_command.py +2 -2
  79. code_puppy/command_line/mcp/utils.py +1 -1
  80. code_puppy/command_line/mcp/wizard_utils.py +22 -18
  81. code_puppy/command_line/mcp_completion.py +174 -0
  82. code_puppy/command_line/model_picker_completion.py +89 -30
  83. code_puppy/command_line/model_settings_menu.py +884 -0
  84. code_puppy/command_line/motd.py +14 -8
  85. code_puppy/command_line/onboarding_slides.py +179 -0
  86. code_puppy/command_line/onboarding_wizard.py +340 -0
  87. code_puppy/command_line/pin_command_completion.py +329 -0
  88. code_puppy/command_line/prompt_toolkit_completion.py +626 -75
  89. code_puppy/command_line/session_commands.py +296 -0
  90. code_puppy/command_line/utils.py +54 -0
  91. code_puppy/config.py +1181 -51
  92. code_puppy/error_logging.py +118 -0
  93. code_puppy/gemini_code_assist.py +385 -0
  94. code_puppy/gemini_model.py +602 -0
  95. code_puppy/http_utils.py +220 -104
  96. code_puppy/keymap.py +128 -0
  97. code_puppy/main.py +5 -594
  98. code_puppy/{mcp → mcp_}/__init__.py +17 -0
  99. code_puppy/{mcp → mcp_}/async_lifecycle.py +35 -4
  100. code_puppy/{mcp → mcp_}/blocking_startup.py +70 -43
  101. code_puppy/{mcp → mcp_}/captured_stdio_server.py +2 -2
  102. code_puppy/{mcp → mcp_}/config_wizard.py +5 -5
  103. code_puppy/{mcp → mcp_}/dashboard.py +15 -6
  104. code_puppy/{mcp → mcp_}/examples/retry_example.py +4 -1
  105. code_puppy/{mcp → mcp_}/managed_server.py +66 -39
  106. code_puppy/{mcp → mcp_}/manager.py +146 -52
  107. code_puppy/mcp_/mcp_logs.py +224 -0
  108. code_puppy/{mcp → mcp_}/registry.py +6 -6
  109. code_puppy/{mcp → mcp_}/server_registry_catalog.py +25 -8
  110. code_puppy/messaging/__init__.py +199 -2
  111. code_puppy/messaging/bus.py +610 -0
  112. code_puppy/messaging/commands.py +167 -0
  113. code_puppy/messaging/markdown_patches.py +57 -0
  114. code_puppy/messaging/message_queue.py +17 -48
  115. code_puppy/messaging/messages.py +500 -0
  116. code_puppy/messaging/queue_console.py +1 -24
  117. code_puppy/messaging/renderers.py +43 -146
  118. code_puppy/messaging/rich_renderer.py +1027 -0
  119. code_puppy/messaging/spinner/__init__.py +33 -5
  120. code_puppy/messaging/spinner/console_spinner.py +92 -52
  121. code_puppy/messaging/spinner/spinner_base.py +29 -0
  122. code_puppy/messaging/subagent_console.py +461 -0
  123. code_puppy/model_factory.py +686 -80
  124. code_puppy/model_utils.py +167 -0
  125. code_puppy/models.json +86 -104
  126. code_puppy/models_dev_api.json +1 -0
  127. code_puppy/models_dev_parser.py +592 -0
  128. code_puppy/plugins/__init__.py +164 -10
  129. code_puppy/plugins/antigravity_oauth/__init__.py +10 -0
  130. code_puppy/plugins/antigravity_oauth/accounts.py +406 -0
  131. code_puppy/plugins/antigravity_oauth/antigravity_model.py +704 -0
  132. code_puppy/plugins/antigravity_oauth/config.py +42 -0
  133. code_puppy/plugins/antigravity_oauth/constants.py +136 -0
  134. code_puppy/plugins/antigravity_oauth/oauth.py +478 -0
  135. code_puppy/plugins/antigravity_oauth/register_callbacks.py +406 -0
  136. code_puppy/plugins/antigravity_oauth/storage.py +271 -0
  137. code_puppy/plugins/antigravity_oauth/test_plugin.py +319 -0
  138. code_puppy/plugins/antigravity_oauth/token.py +167 -0
  139. code_puppy/plugins/antigravity_oauth/transport.py +767 -0
  140. code_puppy/plugins/antigravity_oauth/utils.py +169 -0
  141. code_puppy/plugins/chatgpt_oauth/__init__.py +8 -0
  142. code_puppy/plugins/chatgpt_oauth/config.py +52 -0
  143. code_puppy/plugins/chatgpt_oauth/oauth_flow.py +328 -0
  144. code_puppy/plugins/chatgpt_oauth/register_callbacks.py +94 -0
  145. code_puppy/plugins/chatgpt_oauth/test_plugin.py +293 -0
  146. code_puppy/plugins/chatgpt_oauth/utils.py +489 -0
  147. code_puppy/plugins/claude_code_oauth/README.md +167 -0
  148. code_puppy/plugins/claude_code_oauth/SETUP.md +93 -0
  149. code_puppy/plugins/claude_code_oauth/__init__.py +6 -0
  150. code_puppy/plugins/claude_code_oauth/config.py +50 -0
  151. code_puppy/plugins/claude_code_oauth/register_callbacks.py +308 -0
  152. code_puppy/plugins/claude_code_oauth/test_plugin.py +283 -0
  153. code_puppy/plugins/claude_code_oauth/utils.py +518 -0
  154. code_puppy/plugins/customizable_commands/__init__.py +0 -0
  155. code_puppy/plugins/customizable_commands/register_callbacks.py +169 -0
  156. code_puppy/plugins/example_custom_command/README.md +280 -0
  157. code_puppy/plugins/example_custom_command/register_callbacks.py +51 -0
  158. code_puppy/plugins/file_permission_handler/__init__.py +4 -0
  159. code_puppy/plugins/file_permission_handler/register_callbacks.py +523 -0
  160. code_puppy/plugins/frontend_emitter/__init__.py +25 -0
  161. code_puppy/plugins/frontend_emitter/emitter.py +121 -0
  162. code_puppy/plugins/frontend_emitter/register_callbacks.py +261 -0
  163. code_puppy/plugins/oauth_puppy_html.py +228 -0
  164. code_puppy/plugins/shell_safety/__init__.py +6 -0
  165. code_puppy/plugins/shell_safety/agent_shell_safety.py +69 -0
  166. code_puppy/plugins/shell_safety/command_cache.py +156 -0
  167. code_puppy/plugins/shell_safety/register_callbacks.py +202 -0
  168. code_puppy/prompts/antigravity_system_prompt.md +1 -0
  169. code_puppy/prompts/codex_system_prompt.md +310 -0
  170. code_puppy/pydantic_patches.py +131 -0
  171. code_puppy/reopenable_async_client.py +8 -8
  172. code_puppy/round_robin_model.py +10 -15
  173. code_puppy/session_storage.py +294 -0
  174. code_puppy/status_display.py +21 -4
  175. code_puppy/summarization_agent.py +52 -14
  176. code_puppy/terminal_utils.py +418 -0
  177. code_puppy/tools/__init__.py +139 -6
  178. code_puppy/tools/agent_tools.py +548 -49
  179. code_puppy/tools/browser/__init__.py +37 -0
  180. code_puppy/tools/browser/browser_control.py +289 -0
  181. code_puppy/tools/browser/browser_interactions.py +545 -0
  182. code_puppy/tools/browser/browser_locators.py +640 -0
  183. code_puppy/tools/browser/browser_manager.py +316 -0
  184. code_puppy/tools/browser/browser_navigation.py +251 -0
  185. code_puppy/tools/browser/browser_screenshot.py +179 -0
  186. code_puppy/tools/browser/browser_scripts.py +462 -0
  187. code_puppy/tools/browser/browser_workflows.py +221 -0
  188. code_puppy/tools/browser/chromium_terminal_manager.py +259 -0
  189. code_puppy/tools/browser/terminal_command_tools.py +521 -0
  190. code_puppy/tools/browser/terminal_screenshot_tools.py +556 -0
  191. code_puppy/tools/browser/terminal_tools.py +525 -0
  192. code_puppy/tools/command_runner.py +941 -153
  193. code_puppy/tools/common.py +1146 -6
  194. code_puppy/tools/display.py +84 -0
  195. code_puppy/tools/file_modifications.py +288 -89
  196. code_puppy/tools/file_operations.py +352 -266
  197. code_puppy/tools/subagent_context.py +158 -0
  198. code_puppy/uvx_detection.py +242 -0
  199. code_puppy/version_checker.py +30 -11
  200. code_puppy-0.0.366.data/data/code_puppy/models.json +110 -0
  201. code_puppy-0.0.366.data/data/code_puppy/models_dev_api.json +1 -0
  202. {code_puppy-0.0.169.dist-info → code_puppy-0.0.366.dist-info}/METADATA +184 -67
  203. code_puppy-0.0.366.dist-info/RECORD +217 -0
  204. {code_puppy-0.0.169.dist-info → code_puppy-0.0.366.dist-info}/WHEEL +1 -1
  205. {code_puppy-0.0.169.dist-info → code_puppy-0.0.366.dist-info}/entry_points.txt +1 -0
  206. code_puppy/agent.py +0 -231
  207. code_puppy/agents/agent_orchestrator.json +0 -26
  208. code_puppy/agents/runtime_manager.py +0 -272
  209. code_puppy/command_line/mcp/add_command.py +0 -183
  210. code_puppy/command_line/meta_command_handler.py +0 -153
  211. code_puppy/message_history_processor.py +0 -490
  212. code_puppy/messaging/spinner/textual_spinner.py +0 -101
  213. code_puppy/state_management.py +0 -200
  214. code_puppy/tui/__init__.py +0 -10
  215. code_puppy/tui/app.py +0 -986
  216. code_puppy/tui/components/__init__.py +0 -21
  217. code_puppy/tui/components/chat_view.py +0 -550
  218. code_puppy/tui/components/command_history_modal.py +0 -218
  219. code_puppy/tui/components/copy_button.py +0 -139
  220. code_puppy/tui/components/custom_widgets.py +0 -63
  221. code_puppy/tui/components/human_input_modal.py +0 -175
  222. code_puppy/tui/components/input_area.py +0 -167
  223. code_puppy/tui/components/sidebar.py +0 -309
  224. code_puppy/tui/components/status_bar.py +0 -182
  225. code_puppy/tui/messages.py +0 -27
  226. code_puppy/tui/models/__init__.py +0 -8
  227. code_puppy/tui/models/chat_message.py +0 -25
  228. code_puppy/tui/models/command_history.py +0 -89
  229. code_puppy/tui/models/enums.py +0 -24
  230. code_puppy/tui/screens/__init__.py +0 -15
  231. code_puppy/tui/screens/help.py +0 -130
  232. code_puppy/tui/screens/mcp_install_wizard.py +0 -803
  233. code_puppy/tui/screens/settings.py +0 -290
  234. code_puppy/tui/screens/tools.py +0 -74
  235. code_puppy-0.0.169.data/data/code_puppy/models.json +0 -128
  236. code_puppy-0.0.169.dist-info/RECORD +0 -112
  237. /code_puppy/{mcp → mcp_}/circuit_breaker.py +0 -0
  238. /code_puppy/{mcp → mcp_}/error_isolation.py +0 -0
  239. /code_puppy/{mcp → mcp_}/health_monitor.py +0 -0
  240. /code_puppy/{mcp → mcp_}/retry_manager.py +0 -0
  241. /code_puppy/{mcp → mcp_}/status_tracker.py +0 -0
  242. /code_puppy/{mcp → mcp_}/system_tools.py +0 -0
  243. {code_puppy-0.0.169.dist-info → code_puppy-0.0.366.dist-info}/licenses/LICENSE +0 -0
@@ -0,0 +1,179 @@
1
+ """Screenshot tool for browser automation.
2
+
3
+ Captures screenshots and returns them via ToolReturn with BinaryContent
4
+ so multimodal models can directly see and analyze - no separate VQA agent needed.
5
+ """
6
+
7
+ import time
8
+ from datetime import datetime
9
+ from pathlib import Path
10
+ from tempfile import gettempdir, mkdtemp
11
+ from typing import Any, Dict, Optional, Union
12
+
13
+ from pydantic_ai import BinaryContent, RunContext, ToolReturn
14
+
15
+ from code_puppy.messaging import emit_error, emit_info, emit_success
16
+ from code_puppy.tools.common import generate_group_id
17
+
18
+ from .browser_manager import get_session_browser_manager
19
+
20
+ _TEMP_SCREENSHOT_ROOT = Path(
21
+ mkdtemp(prefix="code_puppy_screenshots_", dir=gettempdir())
22
+ )
23
+
24
+
25
+ def _build_screenshot_path(timestamp: str) -> Path:
26
+ """Return the target path for a screenshot."""
27
+ filename = f"screenshot_{timestamp}.png"
28
+ return _TEMP_SCREENSHOT_ROOT / filename
29
+
30
+
31
+ async def _capture_screenshot(
32
+ page,
33
+ full_page: bool = False,
34
+ element_selector: Optional[str] = None,
35
+ save_screenshot: bool = True,
36
+ group_id: Optional[str] = None,
37
+ ) -> Dict[str, Any]:
38
+ """Internal screenshot capture function."""
39
+ try:
40
+ timestamp = datetime.now().strftime("%Y%m%d_%H%M%S_%f")
41
+
42
+ # Take screenshot
43
+ if element_selector:
44
+ element = await page.locator(element_selector).first
45
+ if not await element.is_visible():
46
+ return {
47
+ "success": False,
48
+ "error": f"Element '{element_selector}' is not visible",
49
+ }
50
+ screenshot_bytes = await element.screenshot()
51
+ else:
52
+ screenshot_bytes = await page.screenshot(full_page=full_page)
53
+
54
+ result: Dict[str, Any] = {
55
+ "success": True,
56
+ "screenshot_bytes": screenshot_bytes,
57
+ "timestamp": timestamp,
58
+ }
59
+
60
+ if save_screenshot:
61
+ screenshot_path = _build_screenshot_path(timestamp)
62
+ screenshot_path.parent.mkdir(parents=True, exist_ok=True)
63
+ with open(screenshot_path, "wb") as f:
64
+ f.write(screenshot_bytes)
65
+ result["screenshot_path"] = str(screenshot_path)
66
+
67
+ if group_id:
68
+ emit_success(
69
+ f"Screenshot saved: {screenshot_path}", message_group=group_id
70
+ )
71
+
72
+ return result
73
+
74
+ except Exception as e:
75
+ return {"success": False, "error": str(e)}
76
+
77
+
78
+ async def take_screenshot(
79
+ full_page: bool = False,
80
+ element_selector: Optional[str] = None,
81
+ save_screenshot: bool = True,
82
+ ) -> Union[ToolReturn, Dict[str, Any]]:
83
+ """Take a screenshot of the browser page.
84
+
85
+ Returns a ToolReturn with BinaryContent so multimodal models can
86
+ directly see and analyze the screenshot.
87
+
88
+ Args:
89
+ full_page: Whether to capture full page or just viewport.
90
+ element_selector: Optional selector to screenshot specific element.
91
+ save_screenshot: Whether to save the screenshot to disk.
92
+
93
+ Returns:
94
+ ToolReturn containing:
95
+ - return_value: Success message with screenshot path
96
+ - content: List with description and BinaryContent image
97
+ - metadata: Screenshot details (path, target, timestamp)
98
+ Or Dict with error info if failed.
99
+ """
100
+ target = element_selector or ("full_page" if full_page else "viewport")
101
+ group_id = generate_group_id("browser_screenshot", target)
102
+ emit_info(f"BROWSER SCREENSHOT 📷 target={target}", message_group=group_id)
103
+
104
+ try:
105
+ browser_manager = get_session_browser_manager()
106
+ page = await browser_manager.get_current_page()
107
+
108
+ if not page:
109
+ error_msg = "No active browser page. Navigate to a webpage first."
110
+ emit_error(error_msg, message_group=group_id)
111
+ return {"success": False, "error": error_msg}
112
+
113
+ result = await _capture_screenshot(
114
+ page,
115
+ full_page=full_page,
116
+ element_selector=element_selector,
117
+ save_screenshot=save_screenshot,
118
+ group_id=group_id,
119
+ )
120
+
121
+ if not result["success"]:
122
+ emit_error(result.get("error", "Screenshot failed"), message_group=group_id)
123
+ return {"success": False, "error": result.get("error")}
124
+
125
+ screenshot_path = result.get("screenshot_path", "(not saved)")
126
+
127
+ # Return as ToolReturn with BinaryContent so the model can SEE the image!
128
+ return ToolReturn(
129
+ return_value=f"Screenshot captured successfully. Saved to: {screenshot_path}",
130
+ content=[
131
+ f"Here's the browser screenshot ({target}):",
132
+ BinaryContent(
133
+ data=result["screenshot_bytes"],
134
+ media_type="image/png",
135
+ ),
136
+ "Please analyze what you see and describe any relevant details.",
137
+ ],
138
+ metadata={
139
+ "success": True,
140
+ "screenshot_path": screenshot_path,
141
+ "target": target,
142
+ "full_page": full_page,
143
+ "element_selector": element_selector,
144
+ "timestamp": time.time(),
145
+ },
146
+ )
147
+
148
+ except Exception as e:
149
+ error_msg = f"Screenshot failed: {str(e)}"
150
+ emit_error(error_msg, message_group=group_id)
151
+ return {"success": False, "error": error_msg}
152
+
153
+
154
+ def register_take_screenshot_and_analyze(agent):
155
+ """Register the screenshot tool."""
156
+
157
+ @agent.tool
158
+ async def browser_screenshot_analyze(
159
+ context: RunContext,
160
+ full_page: bool = False,
161
+ element_selector: Optional[str] = None,
162
+ ) -> Union[ToolReturn, Dict[str, Any]]:
163
+ """
164
+ Take a screenshot of the browser page.
165
+
166
+ Returns the screenshot via ToolReturn with BinaryContent that you can
167
+ see directly. Use this to see what's displayed in the browser.
168
+
169
+ Args:
170
+ full_page: Capture full page (True) or just viewport (False).
171
+ element_selector: Optional CSS selector to screenshot specific element.
172
+
173
+ Returns:
174
+ ToolReturn with the screenshot image you can analyze, or error dict.
175
+ """
176
+ return await take_screenshot(
177
+ full_page=full_page,
178
+ element_selector=element_selector,
179
+ )
@@ -0,0 +1,462 @@
1
+ """JavaScript execution and advanced page manipulation tools."""
2
+
3
+ from typing import Any, Dict, Optional
4
+
5
+ from pydantic_ai import RunContext
6
+
7
+ from code_puppy.messaging import emit_error, emit_info, emit_success
8
+ from code_puppy.tools.common import generate_group_id
9
+
10
+ from .browser_manager import get_session_browser_manager
11
+
12
+
13
+ async def execute_javascript(
14
+ script: str,
15
+ timeout: int = 30000,
16
+ ) -> Dict[str, Any]:
17
+ """Execute JavaScript code in the browser context."""
18
+ group_id = generate_group_id("browser_execute_js", script[:100])
19
+ emit_info(
20
+ f"BROWSER EXECUTE JS 📜 script='{script[:100]}{'...' if len(script) > 100 else ''}'",
21
+ message_group=group_id,
22
+ )
23
+ try:
24
+ browser_manager = get_session_browser_manager()
25
+ page = await browser_manager.get_current_page()
26
+
27
+ if not page:
28
+ return {"success": False, "error": "No active browser page available"}
29
+
30
+ # Execute JavaScript
31
+ # Note: page.evaluate() does NOT accept a timeout parameter
32
+ # The timeout arg to this function is kept for API compatibility but unused
33
+ result = await page.evaluate(script)
34
+
35
+ emit_success("JavaScript executed successfully", message_group=group_id)
36
+
37
+ return {"success": True, "script": script, "result": result}
38
+
39
+ except Exception as e:
40
+ emit_error(f"JavaScript execution failed: {str(e)}", message_group=group_id)
41
+ return {"success": False, "error": str(e), "script": script}
42
+
43
+
44
+ async def scroll_page(
45
+ direction: str = "down",
46
+ amount: int = 3,
47
+ element_selector: Optional[str] = None,
48
+ ) -> Dict[str, Any]:
49
+ """Scroll the page or a specific element."""
50
+ target = element_selector or "page"
51
+ group_id = generate_group_id("browser_scroll", f"{direction}_{amount}_{target}")
52
+ emit_info(
53
+ f"BROWSER SCROLL 📋 direction={direction} amount={amount} target='{target}'",
54
+ message_group=group_id,
55
+ )
56
+ try:
57
+ browser_manager = get_session_browser_manager()
58
+ page = await browser_manager.get_current_page()
59
+
60
+ if not page:
61
+ return {"success": False, "error": "No active browser page available"}
62
+
63
+ if element_selector:
64
+ # Scroll specific element
65
+ element = page.locator(element_selector).first
66
+ await element.scroll_into_view_if_needed()
67
+
68
+ # Get element's current scroll position and dimensions
69
+ scroll_info = await element.evaluate("""
70
+ el => {
71
+ const rect = el.getBoundingClientRect();
72
+ return {
73
+ scrollTop: el.scrollTop,
74
+ scrollLeft: el.scrollLeft,
75
+ scrollHeight: el.scrollHeight,
76
+ scrollWidth: el.scrollWidth,
77
+ clientHeight: el.clientHeight,
78
+ clientWidth: el.clientWidth
79
+ };
80
+ }
81
+ """)
82
+
83
+ # Calculate scroll amount based on element size
84
+ scroll_amount = scroll_info["clientHeight"] * amount / 3
85
+
86
+ if direction.lower() == "down":
87
+ await element.evaluate(f"el => el.scrollTop += {scroll_amount}")
88
+ elif direction.lower() == "up":
89
+ await element.evaluate(f"el => el.scrollTop -= {scroll_amount}")
90
+ elif direction.lower() == "left":
91
+ await element.evaluate(f"el => el.scrollLeft -= {scroll_amount}")
92
+ elif direction.lower() == "right":
93
+ await element.evaluate(f"el => el.scrollLeft += {scroll_amount}")
94
+
95
+ target = f"element '{element_selector}'"
96
+
97
+ else:
98
+ # Scroll page
99
+ viewport_height = await page.evaluate("() => window.innerHeight")
100
+ scroll_amount = viewport_height * amount / 3
101
+
102
+ if direction.lower() == "down":
103
+ await page.evaluate(f"window.scrollBy(0, {scroll_amount})")
104
+ elif direction.lower() == "up":
105
+ await page.evaluate(f"window.scrollBy(0, -{scroll_amount})")
106
+ elif direction.lower() == "left":
107
+ await page.evaluate(f"window.scrollBy(-{scroll_amount}, 0)")
108
+ elif direction.lower() == "right":
109
+ await page.evaluate(f"window.scrollBy({scroll_amount}, 0)")
110
+
111
+ target = "page"
112
+
113
+ # Get current scroll position
114
+ scroll_pos = await page.evaluate("""
115
+ () => ({
116
+ x: window.pageXOffset,
117
+ y: window.pageYOffset
118
+ })
119
+ """)
120
+
121
+ emit_success(f"Scrolled {target} {direction}", message_group=group_id)
122
+
123
+ return {
124
+ "success": True,
125
+ "direction": direction,
126
+ "amount": amount,
127
+ "target": target,
128
+ "scroll_position": scroll_pos,
129
+ }
130
+
131
+ except Exception as e:
132
+ return {
133
+ "success": False,
134
+ "error": str(e),
135
+ "direction": direction,
136
+ "element_selector": element_selector,
137
+ }
138
+
139
+
140
+ async def scroll_to_element(
141
+ selector: str,
142
+ timeout: int = 10000,
143
+ ) -> Dict[str, Any]:
144
+ """Scroll to bring an element into view."""
145
+ group_id = generate_group_id("browser_scroll_to_element", selector[:100])
146
+ emit_info(
147
+ f"BROWSER SCROLL TO ELEMENT 🎯 selector='{selector}'",
148
+ message_group=group_id,
149
+ )
150
+ try:
151
+ browser_manager = get_session_browser_manager()
152
+ page = await browser_manager.get_current_page()
153
+
154
+ if not page:
155
+ return {"success": False, "error": "No active browser page available"}
156
+
157
+ element = page.locator(selector).first
158
+ await element.wait_for(state="attached", timeout=timeout)
159
+ await element.scroll_into_view_if_needed()
160
+
161
+ # Check if element is now visible
162
+ is_visible = await element.is_visible()
163
+
164
+ emit_success(f"Scrolled to element: {selector}", message_group=group_id)
165
+
166
+ return {"success": True, "selector": selector, "visible": is_visible}
167
+
168
+ except Exception as e:
169
+ return {"success": False, "error": str(e), "selector": selector}
170
+
171
+
172
+ async def set_viewport_size(
173
+ width: int,
174
+ height: int,
175
+ ) -> Dict[str, Any]:
176
+ """Set the viewport size."""
177
+ group_id = generate_group_id("browser_set_viewport", f"{width}x{height}")
178
+ emit_info(
179
+ f"BROWSER SET VIEWPORT 🖥️ size={width}x{height}",
180
+ message_group=group_id,
181
+ )
182
+ try:
183
+ browser_manager = get_session_browser_manager()
184
+ page = await browser_manager.get_current_page()
185
+
186
+ if not page:
187
+ return {"success": False, "error": "No active browser page available"}
188
+
189
+ await page.set_viewport_size({"width": width, "height": height})
190
+
191
+ emit_success(
192
+ f"Set viewport size to {width}x{height}",
193
+ message_group=group_id,
194
+ )
195
+
196
+ return {"success": True, "width": width, "height": height}
197
+
198
+ except Exception as e:
199
+ return {"success": False, "error": str(e), "width": width, "height": height}
200
+
201
+
202
+ async def wait_for_element(
203
+ selector: str,
204
+ state: str = "visible",
205
+ timeout: int = 30000,
206
+ ) -> Dict[str, Any]:
207
+ """Wait for an element to reach a specific state."""
208
+ group_id = generate_group_id("browser_wait_for_element", f"{selector[:50]}_{state}")
209
+ emit_info(
210
+ f"BROWSER WAIT FOR ELEMENT ⏱️ selector='{selector}' state={state} timeout={timeout}ms",
211
+ message_group=group_id,
212
+ )
213
+ try:
214
+ browser_manager = get_session_browser_manager()
215
+ page = await browser_manager.get_current_page()
216
+
217
+ if not page:
218
+ return {"success": False, "error": "No active browser page available"}
219
+
220
+ element = page.locator(selector).first
221
+ await element.wait_for(state=state, timeout=timeout)
222
+
223
+ emit_success(f"Element {selector} is now {state}", message_group=group_id)
224
+
225
+ return {"success": True, "selector": selector, "state": state}
226
+
227
+ except Exception as e:
228
+ return {"success": False, "error": str(e), "selector": selector, "state": state}
229
+
230
+
231
+ async def highlight_element(
232
+ selector: str,
233
+ color: str = "red",
234
+ timeout: int = 10000,
235
+ ) -> Dict[str, Any]:
236
+ """Highlight an element with a colored border."""
237
+ group_id = generate_group_id(
238
+ "browser_highlight_element", f"{selector[:50]}_{color}"
239
+ )
240
+ emit_info(
241
+ f"BROWSER HIGHLIGHT ELEMENT 🔦 selector='{selector}' color={color}",
242
+ message_group=group_id,
243
+ )
244
+ try:
245
+ browser_manager = get_session_browser_manager()
246
+ page = await browser_manager.get_current_page()
247
+
248
+ if not page:
249
+ return {"success": False, "error": "No active browser page available"}
250
+
251
+ element = page.locator(selector).first
252
+ await element.wait_for(state="visible", timeout=timeout)
253
+
254
+ # Add highlight style
255
+ highlight_script = f"""
256
+ el => {{
257
+ el.style.outline = '3px solid {color}';
258
+ el.style.outlineOffset = '2px';
259
+ el.style.backgroundColor = '{color}20'; // 20% opacity
260
+ el.setAttribute('data-highlighted', 'true');
261
+ }}
262
+ """
263
+
264
+ await element.evaluate(highlight_script)
265
+
266
+ emit_success(f"Highlighted element: {selector}", message_group=group_id)
267
+
268
+ return {"success": True, "selector": selector, "color": color}
269
+
270
+ except Exception as e:
271
+ return {"success": False, "error": str(e), "selector": selector}
272
+
273
+
274
+ async def clear_highlights() -> Dict[str, Any]:
275
+ """Clear all element highlights."""
276
+ group_id = generate_group_id("browser_clear_highlights")
277
+ emit_info(
278
+ "BROWSER CLEAR HIGHLIGHTS 🧹",
279
+ message_group=group_id,
280
+ )
281
+ try:
282
+ browser_manager = get_session_browser_manager()
283
+ page = await browser_manager.get_current_page()
284
+
285
+ if not page:
286
+ return {"success": False, "error": "No active browser page available"}
287
+
288
+ # Remove all highlights
289
+ clear_script = """
290
+ () => {
291
+ const highlighted = document.querySelectorAll('[data-highlighted="true"]');
292
+ highlighted.forEach(el => {
293
+ el.style.outline = '';
294
+ el.style.outlineOffset = '';
295
+ el.style.backgroundColor = '';
296
+ el.removeAttribute('data-highlighted');
297
+ });
298
+ return highlighted.length;
299
+ }
300
+ """
301
+
302
+ count = await page.evaluate(clear_script)
303
+
304
+ emit_success(f"Cleared {count} highlights", message_group=group_id)
305
+
306
+ return {"success": True, "cleared_count": count}
307
+
308
+ except Exception as e:
309
+ return {"success": False, "error": str(e)}
310
+
311
+
312
+ # Tool registration functions
313
+ def register_execute_javascript(agent):
314
+ """Register the JavaScript execution tool."""
315
+
316
+ @agent.tool
317
+ async def browser_execute_js(
318
+ context: RunContext,
319
+ script: str,
320
+ timeout: int = 30000,
321
+ ) -> Dict[str, Any]:
322
+ """
323
+ Execute JavaScript code in the browser context.
324
+
325
+ Args:
326
+ script: JavaScript code to execute
327
+ timeout: Timeout in milliseconds
328
+
329
+ Returns:
330
+ Dict with execution results
331
+ """
332
+ return await execute_javascript(script, timeout)
333
+
334
+
335
+ def register_scroll_page(agent):
336
+ """Register the scroll page tool."""
337
+
338
+ @agent.tool
339
+ async def browser_scroll(
340
+ context: RunContext,
341
+ direction: str = "down",
342
+ amount: int = 3,
343
+ element_selector: Optional[str] = None,
344
+ ) -> Dict[str, Any]:
345
+ """
346
+ Scroll the page or a specific element.
347
+
348
+ Args:
349
+ direction: Scroll direction (up, down, left, right)
350
+ amount: Scroll amount multiplier (1-10)
351
+ element_selector: Optional selector to scroll specific element
352
+
353
+ Returns:
354
+ Dict with scroll results
355
+ """
356
+ return await scroll_page(direction, amount, element_selector)
357
+
358
+
359
+ def register_scroll_to_element(agent):
360
+ """Register the scroll to element tool."""
361
+
362
+ @agent.tool
363
+ async def browser_scroll_to_element(
364
+ context: RunContext,
365
+ selector: str,
366
+ timeout: int = 10000,
367
+ ) -> Dict[str, Any]:
368
+ """
369
+ Scroll to bring an element into view.
370
+
371
+ Args:
372
+ selector: CSS or XPath selector for the element
373
+ timeout: Timeout in milliseconds
374
+
375
+ Returns:
376
+ Dict with scroll results
377
+ """
378
+ return await scroll_to_element(selector, timeout)
379
+
380
+
381
+ def register_set_viewport_size(agent):
382
+ """Register the viewport size tool."""
383
+
384
+ @agent.tool
385
+ async def browser_set_viewport(
386
+ context: RunContext,
387
+ width: int,
388
+ height: int,
389
+ ) -> Dict[str, Any]:
390
+ """
391
+ Set the browser viewport size.
392
+
393
+ Args:
394
+ width: Viewport width in pixels
395
+ height: Viewport height in pixels
396
+
397
+ Returns:
398
+ Dict with viewport size results
399
+ """
400
+ return await set_viewport_size(width, height)
401
+
402
+
403
+ def register_wait_for_element(agent):
404
+ """Register the wait for element tool."""
405
+
406
+ @agent.tool
407
+ async def browser_wait_for_element(
408
+ context: RunContext,
409
+ selector: str,
410
+ state: str = "visible",
411
+ timeout: int = 30000,
412
+ ) -> Dict[str, Any]:
413
+ """
414
+ Wait for an element to reach a specific state.
415
+
416
+ Args:
417
+ selector: CSS or XPath selector for the element
418
+ state: State to wait for (visible, hidden, attached, detached)
419
+ timeout: Timeout in milliseconds
420
+
421
+ Returns:
422
+ Dict with wait results
423
+ """
424
+ return await wait_for_element(selector, state, timeout)
425
+
426
+
427
+ def register_browser_highlight_element(agent):
428
+ """Register the element highlighting tool."""
429
+
430
+ @agent.tool
431
+ async def browser_highlight_element(
432
+ context: RunContext,
433
+ selector: str,
434
+ color: str = "red",
435
+ timeout: int = 10000,
436
+ ) -> Dict[str, Any]:
437
+ """
438
+ Highlight an element with a colored border for visual identification.
439
+
440
+ Args:
441
+ selector: CSS or XPath selector for the element
442
+ color: Highlight color (red, blue, green, yellow, etc.)
443
+ timeout: Timeout in milliseconds
444
+
445
+ Returns:
446
+ Dict with highlight results
447
+ """
448
+ return await highlight_element(selector, color, timeout)
449
+
450
+
451
+ def register_browser_clear_highlights(agent):
452
+ """Register the clear highlights tool."""
453
+
454
+ @agent.tool
455
+ async def browser_clear_highlights(context: RunContext) -> Dict[str, Any]:
456
+ """
457
+ Clear all element highlights from the page.
458
+
459
+ Returns:
460
+ Dict with clear results
461
+ """
462
+ return await clear_highlights()