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,545 @@
1
+ """Browser element interaction tools for clicking, typing, and form manipulation."""
2
+
3
+ from typing import Any, Dict, List, 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 click_element(
14
+ selector: str,
15
+ timeout: int = 10000,
16
+ force: bool = False,
17
+ button: str = "left",
18
+ modifiers: Optional[List[str]] = None,
19
+ ) -> Dict[str, Any]:
20
+ """Click on an element."""
21
+ group_id = generate_group_id("browser_click", selector[:100])
22
+ emit_info(
23
+ f"BROWSER CLICK 🖱️ selector='{selector}' button={button}",
24
+ message_group=group_id,
25
+ )
26
+ try:
27
+ browser_manager = get_session_browser_manager()
28
+ page = await browser_manager.get_current_page()
29
+
30
+ if not page:
31
+ return {"success": False, "error": "No active browser page available"}
32
+
33
+ # Find element - use .first to handle cases where selector matches multiple elements
34
+ # This avoids Playwright's strict mode violation errors
35
+ element = page.locator(selector).first
36
+
37
+ # Wait for element to be visible and enabled
38
+ await element.wait_for(state="visible", timeout=timeout)
39
+
40
+ # Click options
41
+ click_options = {
42
+ "force": force,
43
+ "button": button,
44
+ "timeout": timeout,
45
+ }
46
+
47
+ if modifiers:
48
+ click_options["modifiers"] = modifiers
49
+
50
+ await element.click(**click_options)
51
+
52
+ emit_success(f"Clicked element: {selector}", message_group=group_id)
53
+
54
+ return {"success": True, "selector": selector, "action": f"{button}_click"}
55
+
56
+ except Exception as e:
57
+ emit_error(f"Click failed: {str(e)}", message_group=group_id)
58
+ return {"success": False, "error": str(e), "selector": selector}
59
+
60
+
61
+ async def double_click_element(
62
+ selector: str,
63
+ timeout: int = 10000,
64
+ force: bool = False,
65
+ ) -> Dict[str, Any]:
66
+ """Double-click on an element."""
67
+ group_id = generate_group_id("browser_double_click", selector[:100])
68
+ emit_info(
69
+ f"BROWSER DOUBLE CLICK 🖱️🖱️ selector='{selector}'",
70
+ message_group=group_id,
71
+ )
72
+ try:
73
+ browser_manager = get_session_browser_manager()
74
+ page = await browser_manager.get_current_page()
75
+
76
+ if not page:
77
+ return {"success": False, "error": "No active browser page available"}
78
+
79
+ element = page.locator(selector).first
80
+ await element.wait_for(state="visible", timeout=timeout)
81
+ await element.dblclick(force=force, timeout=timeout)
82
+
83
+ emit_success(f"Double-clicked element: {selector}", message_group=group_id)
84
+
85
+ return {"success": True, "selector": selector, "action": "double_click"}
86
+
87
+ except Exception as e:
88
+ return {"success": False, "error": str(e), "selector": selector}
89
+
90
+
91
+ async def hover_element(
92
+ selector: str,
93
+ timeout: int = 10000,
94
+ force: bool = False,
95
+ ) -> Dict[str, Any]:
96
+ """Hover over an element."""
97
+ group_id = generate_group_id("browser_hover", selector[:100])
98
+ emit_info(
99
+ f"BROWSER HOVER 👆 selector='{selector}'",
100
+ message_group=group_id,
101
+ )
102
+ try:
103
+ browser_manager = get_session_browser_manager()
104
+ page = await browser_manager.get_current_page()
105
+
106
+ if not page:
107
+ return {"success": False, "error": "No active browser page available"}
108
+
109
+ element = page.locator(selector).first
110
+ await element.wait_for(state="visible", timeout=timeout)
111
+ await element.hover(force=force, timeout=timeout)
112
+
113
+ emit_success(f"Hovered over element: {selector}", message_group=group_id)
114
+
115
+ return {"success": True, "selector": selector, "action": "hover"}
116
+
117
+ except Exception as e:
118
+ return {"success": False, "error": str(e), "selector": selector}
119
+
120
+
121
+ async def set_element_text(
122
+ selector: str,
123
+ text: str,
124
+ clear_first: bool = True,
125
+ timeout: int = 10000,
126
+ ) -> Dict[str, Any]:
127
+ """Set text in an input element."""
128
+ group_id = generate_group_id("browser_set_text", f"{selector[:50]}_{text[:30]}")
129
+ emit_info(
130
+ f"BROWSER SET TEXT ✏️ selector='{selector}' text='{text[:50]}{'...' if len(text) > 50 else ''}'",
131
+ message_group=group_id,
132
+ )
133
+ try:
134
+ browser_manager = get_session_browser_manager()
135
+ page = await browser_manager.get_current_page()
136
+
137
+ if not page:
138
+ return {"success": False, "error": "No active browser page available"}
139
+
140
+ element = page.locator(selector).first
141
+ await element.wait_for(state="visible", timeout=timeout)
142
+
143
+ if clear_first:
144
+ await element.clear(timeout=timeout)
145
+
146
+ await element.fill(text, timeout=timeout)
147
+
148
+ emit_success(f"Set text in element: {selector}", message_group=group_id)
149
+
150
+ return {
151
+ "success": True,
152
+ "selector": selector,
153
+ "text": text,
154
+ "action": "set_text",
155
+ }
156
+
157
+ except Exception as e:
158
+ emit_error(f"Set text failed: {str(e)}", message_group=group_id)
159
+ return {"success": False, "error": str(e), "selector": selector, "text": text}
160
+
161
+
162
+ async def get_element_text(
163
+ selector: str,
164
+ timeout: int = 10000,
165
+ ) -> Dict[str, Any]:
166
+ """Get text content from an element."""
167
+ group_id = generate_group_id("browser_get_text", selector[:100])
168
+ emit_info(
169
+ f"BROWSER GET TEXT 📝 selector='{selector}'",
170
+ message_group=group_id,
171
+ )
172
+ try:
173
+ browser_manager = get_session_browser_manager()
174
+ page = await browser_manager.get_current_page()
175
+
176
+ if not page:
177
+ return {"success": False, "error": "No active browser page available"}
178
+
179
+ element = page.locator(selector).first
180
+ await element.wait_for(state="visible", timeout=timeout)
181
+
182
+ text = await element.text_content()
183
+
184
+ return {"success": True, "selector": selector, "text": text}
185
+
186
+ except Exception as e:
187
+ return {"success": False, "error": str(e), "selector": selector}
188
+
189
+
190
+ async def get_element_value(
191
+ selector: str,
192
+ timeout: int = 10000,
193
+ ) -> Dict[str, Any]:
194
+ """Get value from an input element."""
195
+ group_id = generate_group_id("browser_get_value", selector[:100])
196
+ emit_info(
197
+ f"BROWSER GET VALUE 📎 selector='{selector}'",
198
+ message_group=group_id,
199
+ )
200
+ try:
201
+ browser_manager = get_session_browser_manager()
202
+ page = await browser_manager.get_current_page()
203
+
204
+ if not page:
205
+ return {"success": False, "error": "No active browser page available"}
206
+
207
+ element = page.locator(selector).first
208
+ await element.wait_for(state="visible", timeout=timeout)
209
+
210
+ value = await element.input_value()
211
+
212
+ return {"success": True, "selector": selector, "value": value}
213
+
214
+ except Exception as e:
215
+ return {"success": False, "error": str(e), "selector": selector}
216
+
217
+
218
+ async def select_option(
219
+ selector: str,
220
+ value: Optional[str] = None,
221
+ label: Optional[str] = None,
222
+ index: Optional[int] = None,
223
+ timeout: int = 10000,
224
+ ) -> Dict[str, Any]:
225
+ """Select an option in a dropdown/select element."""
226
+ option_desc = value or label or str(index) if index is not None else "unknown"
227
+ group_id = generate_group_id(
228
+ "browser_select_option", f"{selector[:50]}_{option_desc}"
229
+ )
230
+ emit_info(
231
+ f"BROWSER SELECT OPTION 📄 selector='{selector}' option='{option_desc}'",
232
+ message_group=group_id,
233
+ )
234
+ try:
235
+ browser_manager = get_session_browser_manager()
236
+ page = await browser_manager.get_current_page()
237
+
238
+ if not page:
239
+ return {"success": False, "error": "No active browser page available"}
240
+
241
+ element = page.locator(selector).first
242
+ await element.wait_for(state="visible", timeout=timeout)
243
+
244
+ if value is not None:
245
+ await element.select_option(value=value, timeout=timeout)
246
+ selection = value
247
+ elif label is not None:
248
+ await element.select_option(label=label, timeout=timeout)
249
+ selection = label
250
+ elif index is not None:
251
+ await element.select_option(index=index, timeout=timeout)
252
+ selection = str(index)
253
+ else:
254
+ return {
255
+ "success": False,
256
+ "error": "Must specify value, label, or index",
257
+ "selector": selector,
258
+ }
259
+
260
+ emit_success(
261
+ f"Selected option in {selector}: {selection}",
262
+ message_group=group_id,
263
+ )
264
+
265
+ return {"success": True, "selector": selector, "selection": selection}
266
+
267
+ except Exception as e:
268
+ return {"success": False, "error": str(e), "selector": selector}
269
+
270
+
271
+ async def check_element(
272
+ selector: str,
273
+ timeout: int = 10000,
274
+ ) -> Dict[str, Any]:
275
+ """Check a checkbox or radio button."""
276
+ group_id = generate_group_id("browser_check", selector[:100])
277
+ emit_info(
278
+ f"BROWSER CHECK ☑️ selector='{selector}'",
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
+ element = page.locator(selector).first
289
+ await element.wait_for(state="visible", timeout=timeout)
290
+ await element.check(timeout=timeout)
291
+
292
+ emit_success(f"Checked element: {selector}", message_group=group_id)
293
+
294
+ return {"success": True, "selector": selector, "action": "check"}
295
+
296
+ except Exception as e:
297
+ return {"success": False, "error": str(e), "selector": selector}
298
+
299
+
300
+ async def uncheck_element(
301
+ selector: str,
302
+ timeout: int = 10000,
303
+ ) -> Dict[str, Any]:
304
+ """Uncheck a checkbox."""
305
+ group_id = generate_group_id("browser_uncheck", selector[:100])
306
+ emit_info(
307
+ f"BROWSER UNCHECK ☐️ selector='{selector}'",
308
+ message_group=group_id,
309
+ )
310
+ try:
311
+ browser_manager = get_session_browser_manager()
312
+ page = await browser_manager.get_current_page()
313
+
314
+ if not page:
315
+ return {"success": False, "error": "No active browser page available"}
316
+
317
+ element = page.locator(selector).first
318
+ await element.wait_for(state="visible", timeout=timeout)
319
+ await element.uncheck(timeout=timeout)
320
+
321
+ emit_success(f"Unchecked element: {selector}", message_group=group_id)
322
+
323
+ return {"success": True, "selector": selector, "action": "uncheck"}
324
+
325
+ except Exception as e:
326
+ return {"success": False, "error": str(e), "selector": selector}
327
+
328
+
329
+ # Tool registration functions
330
+ def register_click_element(agent):
331
+ """Register the click element tool."""
332
+
333
+ @agent.tool
334
+ async def browser_click(
335
+ context: RunContext,
336
+ selector: str,
337
+ timeout: int = 10000,
338
+ force: bool = False,
339
+ button: str = "left",
340
+ modifiers: Optional[List[str]] = None,
341
+ ) -> Dict[str, Any]:
342
+ """
343
+ Click on an element in the browser.
344
+
345
+ Args:
346
+ selector: CSS or XPath selector for the element
347
+ timeout: Timeout in milliseconds to wait for element
348
+ force: Skip actionability checks and force the click
349
+ button: Mouse button to click (left, right, middle)
350
+ modifiers: Modifier keys to hold (Alt, Control, Meta, Shift)
351
+
352
+ Returns:
353
+ Dict with click results
354
+ """
355
+ return await click_element(selector, timeout, force, button, modifiers)
356
+
357
+
358
+ def register_double_click_element(agent):
359
+ """Register the double-click element tool."""
360
+
361
+ @agent.tool
362
+ async def browser_double_click(
363
+ context: RunContext,
364
+ selector: str,
365
+ timeout: int = 10000,
366
+ force: bool = False,
367
+ ) -> Dict[str, Any]:
368
+ """
369
+ Double-click on an element in the browser.
370
+
371
+ Args:
372
+ selector: CSS or XPath selector for the element
373
+ timeout: Timeout in milliseconds to wait for element
374
+ force: Skip actionability checks and force the double-click
375
+
376
+ Returns:
377
+ Dict with double-click results
378
+ """
379
+ return await double_click_element(selector, timeout, force)
380
+
381
+
382
+ def register_hover_element(agent):
383
+ """Register the hover element tool."""
384
+
385
+ @agent.tool
386
+ async def browser_hover(
387
+ context: RunContext,
388
+ selector: str,
389
+ timeout: int = 10000,
390
+ force: bool = False,
391
+ ) -> Dict[str, Any]:
392
+ """
393
+ Hover over an element in the browser.
394
+
395
+ Args:
396
+ selector: CSS or XPath selector for the element
397
+ timeout: Timeout in milliseconds to wait for element
398
+ force: Skip actionability checks and force the hover
399
+
400
+ Returns:
401
+ Dict with hover results
402
+ """
403
+ return await hover_element(selector, timeout, force)
404
+
405
+
406
+ def register_set_element_text(agent):
407
+ """Register the set element text tool."""
408
+
409
+ @agent.tool
410
+ async def browser_set_text(
411
+ context: RunContext,
412
+ selector: str,
413
+ text: str,
414
+ clear_first: bool = True,
415
+ timeout: int = 10000,
416
+ ) -> Dict[str, Any]:
417
+ """
418
+ Set text in an input element.
419
+
420
+ Args:
421
+ selector: CSS or XPath selector for the input element
422
+ text: Text to enter
423
+ clear_first: Whether to clear existing text first
424
+ timeout: Timeout in milliseconds to wait for element
425
+
426
+ Returns:
427
+ Dict with text input results
428
+ """
429
+ return await set_element_text(selector, text, clear_first, timeout)
430
+
431
+
432
+ def register_get_element_text(agent):
433
+ """Register the get element text tool."""
434
+
435
+ @agent.tool
436
+ async def browser_get_text(
437
+ context: RunContext,
438
+ selector: str,
439
+ timeout: int = 10000,
440
+ ) -> Dict[str, Any]:
441
+ """
442
+ Get text content from an element.
443
+
444
+ Args:
445
+ selector: CSS or XPath selector for the element
446
+ timeout: Timeout in milliseconds to wait for element
447
+
448
+ Returns:
449
+ Dict with element text content
450
+ """
451
+ return await get_element_text(selector, timeout)
452
+
453
+
454
+ def register_get_element_value(agent):
455
+ """Register the get element value tool."""
456
+
457
+ @agent.tool
458
+ async def browser_get_value(
459
+ context: RunContext,
460
+ selector: str,
461
+ timeout: int = 10000,
462
+ ) -> Dict[str, Any]:
463
+ """
464
+ Get value from an input element.
465
+
466
+ Args:
467
+ selector: CSS or XPath selector for the input element
468
+ timeout: Timeout in milliseconds to wait for element
469
+
470
+ Returns:
471
+ Dict with element value
472
+ """
473
+ return await get_element_value(selector, timeout)
474
+
475
+
476
+ def register_select_option(agent):
477
+ """Register the select option tool."""
478
+
479
+ @agent.tool
480
+ async def browser_select_option(
481
+ context: RunContext,
482
+ selector: str,
483
+ value: Optional[str] = None,
484
+ label: Optional[str] = None,
485
+ index: Optional[int] = None,
486
+ timeout: int = 10000,
487
+ ) -> Dict[str, Any]:
488
+ """
489
+ Select an option in a dropdown/select element.
490
+
491
+ Args:
492
+ selector: CSS or XPath selector for the select element
493
+ value: Option value to select
494
+ label: Option label text to select
495
+ index: Option index to select (0-based)
496
+ timeout: Timeout in milliseconds to wait for element
497
+
498
+ Returns:
499
+ Dict with selection results
500
+ """
501
+ return await select_option(selector, value, label, index, timeout)
502
+
503
+
504
+ def register_browser_check(agent):
505
+ """Register checkbox/radio button check tool."""
506
+
507
+ @agent.tool
508
+ async def browser_check(
509
+ context: RunContext,
510
+ selector: str,
511
+ timeout: int = 10000,
512
+ ) -> Dict[str, Any]:
513
+ """
514
+ Check a checkbox or radio button.
515
+
516
+ Args:
517
+ selector: CSS or XPath selector for the checkbox/radio
518
+ timeout: Timeout in milliseconds to wait for element
519
+
520
+ Returns:
521
+ Dict with check results
522
+ """
523
+ return await check_element(selector, timeout)
524
+
525
+
526
+ def register_browser_uncheck(agent):
527
+ """Register checkbox uncheck tool."""
528
+
529
+ @agent.tool
530
+ async def browser_uncheck(
531
+ context: RunContext,
532
+ selector: str,
533
+ timeout: int = 10000,
534
+ ) -> Dict[str, Any]:
535
+ """
536
+ Uncheck a checkbox.
537
+
538
+ Args:
539
+ selector: CSS or XPath selector for the checkbox
540
+ timeout: Timeout in milliseconds to wait for element
541
+
542
+ Returns:
543
+ Dict with uncheck results
544
+ """
545
+ return await uncheck_element(selector, timeout)