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,640 @@
1
+ """Browser element discovery tools using semantic locators and XPath."""
2
+
3
+ from typing import Any, Dict, Optional
4
+
5
+ from pydantic_ai import RunContext
6
+
7
+ from code_puppy.messaging import 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 find_by_role(
14
+ role: str,
15
+ name: Optional[str] = None,
16
+ exact: bool = False,
17
+ timeout: int = 10000,
18
+ ) -> Dict[str, Any]:
19
+ """Find elements by ARIA role."""
20
+ group_id = generate_group_id("browser_find_by_role", f"{role}_{name or 'any'}")
21
+ emit_info(
22
+ f"BROWSER FIND BY ROLE 🎨 role={role} name={name}",
23
+ message_group=group_id,
24
+ )
25
+ try:
26
+ browser_manager = get_session_browser_manager()
27
+ page = await browser_manager.get_current_page()
28
+
29
+ if not page:
30
+ return {"success": False, "error": "No active browser page available"}
31
+
32
+ # Build locator
33
+ locator = page.get_by_role(role, name=name, exact=exact)
34
+
35
+ # Wait for at least one element
36
+ await locator.first.wait_for(state="visible", timeout=timeout)
37
+
38
+ # Count elements
39
+ count = await locator.count()
40
+
41
+ # Get element info
42
+ elements = []
43
+ for i in range(min(count, 10)): # Limit to first 10 elements
44
+ element = locator.nth(i)
45
+ if await element.is_visible():
46
+ text = await element.text_content()
47
+ elements.append({"index": i, "text": text, "visible": True})
48
+
49
+ emit_success(
50
+ f"Found {count} elements with role '{role}'",
51
+ message_group=group_id,
52
+ )
53
+
54
+ return {
55
+ "success": True,
56
+ "role": role,
57
+ "name": name,
58
+ "count": count,
59
+ "elements": elements,
60
+ }
61
+
62
+ except Exception as e:
63
+ return {"success": False, "error": str(e), "role": role, "name": name}
64
+
65
+
66
+ async def find_by_text(
67
+ text: str,
68
+ exact: bool = False,
69
+ timeout: int = 10000,
70
+ ) -> Dict[str, Any]:
71
+ """Find elements containing specific text."""
72
+ group_id = generate_group_id("browser_find_by_text", text[:50])
73
+ emit_info(
74
+ f"BROWSER FIND BY TEXT 🔍 text='{text}' exact={exact}",
75
+ message_group=group_id,
76
+ )
77
+ try:
78
+ browser_manager = get_session_browser_manager()
79
+ page = await browser_manager.get_current_page()
80
+
81
+ if not page:
82
+ return {"success": False, "error": "No active browser page available"}
83
+
84
+ locator = page.get_by_text(text, exact=exact)
85
+
86
+ # Wait for at least one element
87
+ await locator.first.wait_for(state="visible", timeout=timeout)
88
+
89
+ count = await locator.count()
90
+
91
+ elements = []
92
+ for i in range(min(count, 10)):
93
+ element = locator.nth(i)
94
+ if await element.is_visible():
95
+ tag_name = await element.evaluate("el => el.tagName.toLowerCase()")
96
+ full_text = await element.text_content()
97
+ elements.append(
98
+ {"index": i, "tag": tag_name, "text": full_text, "visible": True}
99
+ )
100
+
101
+ emit_success(
102
+ f"Found {count} elements containing text '{text}'",
103
+ message_group=group_id,
104
+ )
105
+
106
+ return {
107
+ "success": True,
108
+ "search_text": text,
109
+ "exact": exact,
110
+ "count": count,
111
+ "elements": elements,
112
+ }
113
+
114
+ except Exception as e:
115
+ return {"success": False, "error": str(e), "search_text": text}
116
+
117
+
118
+ async def find_by_label(
119
+ text: str,
120
+ exact: bool = False,
121
+ timeout: int = 10000,
122
+ ) -> Dict[str, Any]:
123
+ """Find form elements by their associated label text."""
124
+ group_id = generate_group_id("browser_find_by_label", text[:50])
125
+ emit_info(
126
+ f"BROWSER FIND BY LABEL 🏷️ label='{text}' exact={exact}",
127
+ message_group=group_id,
128
+ )
129
+ try:
130
+ browser_manager = get_session_browser_manager()
131
+ page = await browser_manager.get_current_page()
132
+
133
+ if not page:
134
+ return {"success": False, "error": "No active browser page available"}
135
+
136
+ locator = page.get_by_label(text, exact=exact)
137
+
138
+ await locator.first.wait_for(state="visible", timeout=timeout)
139
+
140
+ count = await locator.count()
141
+
142
+ elements = []
143
+ for i in range(min(count, 10)):
144
+ element = locator.nth(i)
145
+ if await element.is_visible():
146
+ tag_name = await element.evaluate("el => el.tagName.toLowerCase()")
147
+ input_type = await element.get_attribute("type")
148
+ value = (
149
+ await element.input_value()
150
+ if tag_name in ["input", "textarea"]
151
+ else None
152
+ )
153
+
154
+ elements.append(
155
+ {
156
+ "index": i,
157
+ "tag": tag_name,
158
+ "type": input_type,
159
+ "value": value,
160
+ "visible": True,
161
+ }
162
+ )
163
+
164
+ emit_success(
165
+ f"Found {count} elements with label '{text}'",
166
+ message_group=group_id,
167
+ )
168
+
169
+ return {
170
+ "success": True,
171
+ "label_text": text,
172
+ "exact": exact,
173
+ "count": count,
174
+ "elements": elements,
175
+ }
176
+
177
+ except Exception as e:
178
+ return {"success": False, "error": str(e), "label_text": text}
179
+
180
+
181
+ async def find_by_placeholder(
182
+ text: str,
183
+ exact: bool = False,
184
+ timeout: int = 10000,
185
+ ) -> Dict[str, Any]:
186
+ """Find elements by placeholder text."""
187
+ group_id = generate_group_id("browser_find_by_placeholder", text[:50])
188
+ emit_info(
189
+ f"BROWSER FIND BY PLACEHOLDER 📝 placeholder='{text}' exact={exact}",
190
+ message_group=group_id,
191
+ )
192
+ try:
193
+ browser_manager = get_session_browser_manager()
194
+ page = await browser_manager.get_current_page()
195
+
196
+ if not page:
197
+ return {"success": False, "error": "No active browser page available"}
198
+
199
+ locator = page.get_by_placeholder(text, exact=exact)
200
+
201
+ await locator.first.wait_for(state="visible", timeout=timeout)
202
+
203
+ count = await locator.count()
204
+
205
+ elements = []
206
+ for i in range(min(count, 10)):
207
+ element = locator.nth(i)
208
+ if await element.is_visible():
209
+ tag_name = await element.evaluate("el => el.tagName.toLowerCase()")
210
+ placeholder = await element.get_attribute("placeholder")
211
+ value = await element.input_value()
212
+
213
+ elements.append(
214
+ {
215
+ "index": i,
216
+ "tag": tag_name,
217
+ "placeholder": placeholder,
218
+ "value": value,
219
+ "visible": True,
220
+ }
221
+ )
222
+
223
+ emit_success(
224
+ f"Found {count} elements with placeholder '{text}'",
225
+ message_group=group_id,
226
+ )
227
+
228
+ return {
229
+ "success": True,
230
+ "placeholder_text": text,
231
+ "exact": exact,
232
+ "count": count,
233
+ "elements": elements,
234
+ }
235
+
236
+ except Exception as e:
237
+ return {"success": False, "error": str(e), "placeholder_text": text}
238
+
239
+
240
+ async def find_by_test_id(
241
+ test_id: str,
242
+ timeout: int = 10000,
243
+ ) -> Dict[str, Any]:
244
+ """Find elements by test ID attribute."""
245
+ group_id = generate_group_id("browser_find_by_test_id", test_id)
246
+ emit_info(
247
+ f"BROWSER FIND BY TEST ID 🧪 test_id='{test_id}'",
248
+ message_group=group_id,
249
+ )
250
+ try:
251
+ browser_manager = get_session_browser_manager()
252
+ page = await browser_manager.get_current_page()
253
+
254
+ if not page:
255
+ return {"success": False, "error": "No active browser page available"}
256
+
257
+ locator = page.get_by_test_id(test_id)
258
+
259
+ await locator.first.wait_for(state="visible", timeout=timeout)
260
+
261
+ count = await locator.count()
262
+
263
+ elements = []
264
+ for i in range(min(count, 10)):
265
+ element = locator.nth(i)
266
+ if await element.is_visible():
267
+ tag_name = await element.evaluate("el => el.tagName.toLowerCase()")
268
+ text = await element.text_content()
269
+
270
+ elements.append(
271
+ {
272
+ "index": i,
273
+ "tag": tag_name,
274
+ "text": text,
275
+ "test_id": test_id,
276
+ "visible": True,
277
+ }
278
+ )
279
+
280
+ emit_success(
281
+ f"Found {count} elements with test-id '{test_id}'",
282
+ message_group=group_id,
283
+ )
284
+
285
+ return {
286
+ "success": True,
287
+ "test_id": test_id,
288
+ "count": count,
289
+ "elements": elements,
290
+ }
291
+
292
+ except Exception as e:
293
+ return {"success": False, "error": str(e), "test_id": test_id}
294
+
295
+
296
+ async def run_xpath_query(
297
+ xpath: str,
298
+ timeout: int = 10000,
299
+ ) -> Dict[str, Any]:
300
+ """Find elements using XPath selector."""
301
+ group_id = generate_group_id("browser_xpath_query", xpath[:100])
302
+ emit_info(
303
+ f"BROWSER XPATH QUERY 🔍 xpath='{xpath}'",
304
+ message_group=group_id,
305
+ )
306
+ try:
307
+ browser_manager = get_session_browser_manager()
308
+ page = await browser_manager.get_current_page()
309
+
310
+ if not page:
311
+ return {"success": False, "error": "No active browser page available"}
312
+
313
+ # Use page.locator with xpath
314
+ locator = page.locator(f"xpath={xpath}")
315
+
316
+ # Wait for at least one element
317
+ await locator.first.wait_for(state="visible", timeout=timeout)
318
+
319
+ count = await locator.count()
320
+
321
+ elements = []
322
+ for i in range(min(count, 10)):
323
+ element = locator.nth(i)
324
+ if await element.is_visible():
325
+ tag_name = await element.evaluate("el => el.tagName.toLowerCase()")
326
+ text = await element.text_content()
327
+ class_name = await element.get_attribute("class")
328
+ element_id = await element.get_attribute("id")
329
+
330
+ elements.append(
331
+ {
332
+ "index": i,
333
+ "tag": tag_name,
334
+ "text": text[:100] if text else None, # Truncate long text
335
+ "class": class_name,
336
+ "id": element_id,
337
+ "visible": True,
338
+ }
339
+ )
340
+
341
+ emit_success(
342
+ f"Found {count} elements with XPath '{xpath}'",
343
+ message_group=group_id,
344
+ )
345
+
346
+ return {"success": True, "xpath": xpath, "count": count, "elements": elements}
347
+
348
+ except Exception as e:
349
+ return {"success": False, "error": str(e), "xpath": xpath}
350
+
351
+
352
+ async def find_buttons(
353
+ text_filter: Optional[str] = None, timeout: int = 10000
354
+ ) -> Dict[str, Any]:
355
+ """Find all button elements on the page."""
356
+ group_id = generate_group_id("browser_find_buttons", text_filter or "all")
357
+ emit_info(
358
+ f"BROWSER FIND BUTTONS 🔘 filter='{text_filter or 'none'}'",
359
+ message_group=group_id,
360
+ )
361
+ try:
362
+ browser_manager = get_session_browser_manager()
363
+ page = await browser_manager.get_current_page()
364
+
365
+ if not page:
366
+ return {"success": False, "error": "No active browser page available"}
367
+
368
+ # Find buttons by role
369
+ locator = page.get_by_role("button")
370
+
371
+ count = await locator.count()
372
+
373
+ buttons = []
374
+ for i in range(min(count, 20)): # Limit to 20 buttons
375
+ button = locator.nth(i)
376
+ if await button.is_visible():
377
+ text = await button.text_content()
378
+ if text_filter and text_filter.lower() not in text.lower():
379
+ continue
380
+
381
+ buttons.append({"index": i, "text": text, "visible": True})
382
+
383
+ filtered_count = len(buttons)
384
+
385
+ emit_success(
386
+ f"Found {filtered_count} buttons"
387
+ + (f" containing '{text_filter}'" if text_filter else ""),
388
+ message_group=group_id,
389
+ )
390
+
391
+ return {
392
+ "success": True,
393
+ "text_filter": text_filter,
394
+ "total_count": count,
395
+ "filtered_count": filtered_count,
396
+ "buttons": buttons,
397
+ }
398
+
399
+ except Exception as e:
400
+ return {"success": False, "error": str(e), "text_filter": text_filter}
401
+
402
+
403
+ async def find_links(
404
+ text_filter: Optional[str] = None, timeout: int = 10000
405
+ ) -> Dict[str, Any]:
406
+ """Find all link elements on the page."""
407
+ group_id = generate_group_id("browser_find_links", text_filter or "all")
408
+ emit_info(
409
+ f"BROWSER FIND LINKS 🔗 filter='{text_filter or 'none'}'",
410
+ message_group=group_id,
411
+ )
412
+ try:
413
+ browser_manager = get_session_browser_manager()
414
+ page = await browser_manager.get_current_page()
415
+
416
+ if not page:
417
+ return {"success": False, "error": "No active browser page available"}
418
+
419
+ # Find links by role
420
+ locator = page.get_by_role("link")
421
+
422
+ count = await locator.count()
423
+
424
+ links = []
425
+ for i in range(min(count, 20)): # Limit to 20 links
426
+ link = locator.nth(i)
427
+ if await link.is_visible():
428
+ text = await link.text_content()
429
+ href = await link.get_attribute("href")
430
+
431
+ if text_filter and text_filter.lower() not in text.lower():
432
+ continue
433
+
434
+ links.append({"index": i, "text": text, "href": href, "visible": True})
435
+
436
+ filtered_count = len(links)
437
+
438
+ emit_success(
439
+ f"Found {filtered_count} links"
440
+ + (f" containing '{text_filter}'" if text_filter else ""),
441
+ message_group=group_id,
442
+ )
443
+
444
+ return {
445
+ "success": True,
446
+ "text_filter": text_filter,
447
+ "total_count": count,
448
+ "filtered_count": filtered_count,
449
+ "links": links,
450
+ }
451
+
452
+ except Exception as e:
453
+ return {"success": False, "error": str(e), "text_filter": text_filter}
454
+
455
+
456
+ # Tool registration functions
457
+ def register_find_by_role(agent):
458
+ """Register the find by role tool."""
459
+
460
+ @agent.tool
461
+ async def browser_find_by_role(
462
+ context: RunContext,
463
+ role: str,
464
+ name: Optional[str] = None,
465
+ exact: bool = False,
466
+ timeout: int = 10000,
467
+ ) -> Dict[str, Any]:
468
+ """
469
+ Find elements by ARIA role (recommended for accessibility).
470
+
471
+ Args:
472
+ role: ARIA role (button, link, textbox, heading, etc.)
473
+ name: Optional accessible name to filter by
474
+ exact: Whether to match name exactly
475
+ timeout: Timeout in milliseconds
476
+
477
+ Returns:
478
+ Dict with found elements and their properties
479
+ """
480
+ return await find_by_role(role, name, exact, timeout)
481
+
482
+
483
+ def register_find_by_text(agent):
484
+ """Register the find by text tool."""
485
+
486
+ @agent.tool
487
+ async def browser_find_by_text(
488
+ context: RunContext,
489
+ text: str,
490
+ exact: bool = False,
491
+ timeout: int = 10000,
492
+ ) -> Dict[str, Any]:
493
+ """
494
+ Find elements containing specific text content.
495
+
496
+ Args:
497
+ text: Text to search for
498
+ exact: Whether to match text exactly
499
+ timeout: Timeout in milliseconds
500
+
501
+ Returns:
502
+ Dict with found elements and their properties
503
+ """
504
+ return await find_by_text(text, exact, timeout)
505
+
506
+
507
+ def register_find_by_label(agent):
508
+ """Register the find by label tool."""
509
+
510
+ @agent.tool
511
+ async def browser_find_by_label(
512
+ context: RunContext,
513
+ text: str,
514
+ exact: bool = False,
515
+ timeout: int = 10000,
516
+ ) -> Dict[str, Any]:
517
+ """
518
+ Find form elements by their associated label text.
519
+
520
+ Args:
521
+ text: Label text to search for
522
+ exact: Whether to match label exactly
523
+ timeout: Timeout in milliseconds
524
+
525
+ Returns:
526
+ Dict with found form elements and their properties
527
+ """
528
+ return await find_by_label(text, exact, timeout)
529
+
530
+
531
+ def register_find_by_placeholder(agent):
532
+ """Register the find by placeholder tool."""
533
+
534
+ @agent.tool
535
+ async def browser_find_by_placeholder(
536
+ context: RunContext,
537
+ text: str,
538
+ exact: bool = False,
539
+ timeout: int = 10000,
540
+ ) -> Dict[str, Any]:
541
+ """
542
+ Find elements by placeholder text.
543
+
544
+ Args:
545
+ text: Placeholder text to search for
546
+ exact: Whether to match placeholder exactly
547
+ timeout: Timeout in milliseconds
548
+
549
+ Returns:
550
+ Dict with found elements and their properties
551
+ """
552
+ return await find_by_placeholder(text, exact, timeout)
553
+
554
+
555
+ def register_find_by_test_id(agent):
556
+ """Register the find by test ID tool."""
557
+
558
+ @agent.tool
559
+ async def browser_find_by_test_id(
560
+ context: RunContext,
561
+ test_id: str,
562
+ timeout: int = 10000,
563
+ ) -> Dict[str, Any]:
564
+ """
565
+ Find elements by test ID attribute (data-testid).
566
+
567
+ Args:
568
+ test_id: Test ID to search for
569
+ timeout: Timeout in milliseconds
570
+
571
+ Returns:
572
+ Dict with found elements and their properties
573
+ """
574
+ return await find_by_test_id(test_id, timeout)
575
+
576
+
577
+ def register_run_xpath_query(agent):
578
+ """Register the XPath query tool."""
579
+
580
+ @agent.tool
581
+ async def browser_xpath_query(
582
+ context: RunContext,
583
+ xpath: str,
584
+ timeout: int = 10000,
585
+ ) -> Dict[str, Any]:
586
+ """
587
+ Find elements using XPath selector (fallback when semantic locators fail).
588
+
589
+ Args:
590
+ xpath: XPath expression
591
+ timeout: Timeout in milliseconds
592
+
593
+ Returns:
594
+ Dict with found elements and their properties
595
+ """
596
+ return await run_xpath_query(xpath, timeout)
597
+
598
+
599
+ def register_find_buttons(agent):
600
+ """Register the find buttons tool."""
601
+
602
+ @agent.tool
603
+ async def browser_find_buttons(
604
+ context: RunContext,
605
+ text_filter: Optional[str] = None,
606
+ timeout: int = 10000,
607
+ ) -> Dict[str, Any]:
608
+ """
609
+ Find all button elements on the page.
610
+
611
+ Args:
612
+ text_filter: Optional text to filter buttons by
613
+ timeout: Timeout in milliseconds
614
+
615
+ Returns:
616
+ Dict with found buttons and their properties
617
+ """
618
+ return await find_buttons(text_filter, timeout)
619
+
620
+
621
+ def register_find_links(agent):
622
+ """Register the find links tool."""
623
+
624
+ @agent.tool
625
+ async def browser_find_links(
626
+ context: RunContext,
627
+ text_filter: Optional[str] = None,
628
+ timeout: int = 10000,
629
+ ) -> Dict[str, Any]:
630
+ """
631
+ Find all link elements on the page.
632
+
633
+ Args:
634
+ text_filter: Optional text to filter links by
635
+ timeout: Timeout in milliseconds
636
+
637
+ Returns:
638
+ Dict with found links and their properties
639
+ """
640
+ return await find_links(text_filter, timeout)