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,221 @@
1
+ """Browser workflow management tools for saving and reusing automation patterns."""
2
+
3
+ from pathlib import Path
4
+ from typing import Any, Dict
5
+
6
+ from pydantic_ai import RunContext
7
+
8
+ from code_puppy import config
9
+ from code_puppy.messaging import emit_error, emit_info, emit_success, emit_warning
10
+ from code_puppy.tools.common import generate_group_id
11
+
12
+
13
+ def get_workflows_directory() -> Path:
14
+ """Get the browser workflows directory, creating it if it doesn't exist (uses XDG_DATA_HOME)."""
15
+ data_dir = Path(config.DATA_DIR)
16
+ workflows_dir = data_dir / "browser_workflows"
17
+ workflows_dir.mkdir(parents=True, exist_ok=True, mode=0o700)
18
+ return workflows_dir
19
+
20
+
21
+ async def save_workflow(name: str, content: str) -> Dict[str, Any]:
22
+ """Save a browser workflow as a markdown file."""
23
+ group_id = generate_group_id("save_workflow", name)
24
+ emit_info(
25
+ f"SAVE WORKFLOW 💾 name='{name}'",
26
+ message_group=group_id,
27
+ )
28
+
29
+ try:
30
+ workflows_dir = get_workflows_directory()
31
+
32
+ # Clean up the filename - convert spaces to hyphens, handle special chars
33
+ import re
34
+
35
+ # Remove .md extension if present (we'll add it back at the end)
36
+ if name.lower().endswith(".md"):
37
+ name = name[:-3]
38
+
39
+ # Convert spaces to hyphens
40
+ safe_name = name.replace(" ", "-")
41
+
42
+ # Replace special characters with double hyphens
43
+ safe_name = re.sub(r"[^a-zA-Z0-9\-_]", "--", safe_name)
44
+
45
+ # Convert to lowercase
46
+ safe_name = safe_name.lower()
47
+
48
+ # Remove any leading/trailing hyphens and collapse multiple hyphens
49
+ safe_name = re.sub(r"^-+|-+$", "", safe_name)
50
+ safe_name = re.sub(r"-{3,}", "--", safe_name)
51
+
52
+ if not safe_name:
53
+ safe_name = "workflow"
54
+
55
+ # Ensure .md extension
56
+ if not safe_name.endswith(".md"):
57
+ safe_name += ".md"
58
+
59
+ workflow_path = workflows_dir / safe_name
60
+
61
+ # Write the workflow content
62
+ with open(workflow_path, "w", encoding="utf-8") as f:
63
+ f.write(content)
64
+
65
+ emit_success(
66
+ f"Workflow saved successfully: {workflow_path}",
67
+ message_group=group_id,
68
+ )
69
+
70
+ return {
71
+ "success": True,
72
+ "path": str(workflow_path),
73
+ "name": safe_name,
74
+ "size": len(content),
75
+ }
76
+
77
+ except Exception as e:
78
+ emit_error(
79
+ f"Failed to save workflow: {e}",
80
+ message_group=group_id,
81
+ )
82
+ return {"success": False, "error": str(e), "name": name}
83
+
84
+
85
+ async def list_workflows() -> Dict[str, Any]:
86
+ """List all available browser workflows."""
87
+ group_id = generate_group_id("list_workflows")
88
+ emit_info(
89
+ "LIST WORKFLOWS 📋",
90
+ message_group=group_id,
91
+ )
92
+
93
+ try:
94
+ workflows_dir = get_workflows_directory()
95
+
96
+ # Find all .md files in the workflows directory
97
+ workflow_files = list(workflows_dir.glob("*.md"))
98
+
99
+ workflows = []
100
+ for workflow_file in workflow_files:
101
+ try:
102
+ stat = workflow_file.stat()
103
+ workflows.append(
104
+ {
105
+ "name": workflow_file.name,
106
+ "path": str(workflow_file),
107
+ "size": stat.st_size,
108
+ "modified": stat.st_mtime,
109
+ }
110
+ )
111
+ except Exception as e:
112
+ emit_warning(f"Could not read {workflow_file}: {e}")
113
+
114
+ # Sort by modification time (newest first)
115
+ workflows.sort(key=lambda x: x["modified"], reverse=True)
116
+
117
+ emit_success(
118
+ f"Found {len(workflows)} workflow(s)",
119
+ message_group=group_id,
120
+ )
121
+
122
+ return {
123
+ "success": True,
124
+ "workflows": workflows,
125
+ "count": len(workflows),
126
+ "directory": str(workflows_dir),
127
+ }
128
+
129
+ except Exception as e:
130
+ emit_error(
131
+ f"Failed to list workflows: {e}",
132
+ message_group=group_id,
133
+ )
134
+ return {"success": False, "error": str(e)}
135
+
136
+
137
+ async def read_workflow(name: str) -> Dict[str, Any]:
138
+ """Read a saved browser workflow."""
139
+ group_id = generate_group_id("read_workflow", name)
140
+ emit_info(
141
+ f"READ WORKFLOW 📖 name='{name}'",
142
+ message_group=group_id,
143
+ )
144
+
145
+ try:
146
+ workflows_dir = get_workflows_directory()
147
+
148
+ # Handle both with and without .md extension
149
+ if not name.endswith(".md"):
150
+ name += ".md"
151
+
152
+ workflow_path = workflows_dir / name
153
+
154
+ if not workflow_path.exists():
155
+ emit_error(
156
+ f"Workflow not found: {name}",
157
+ message_group=group_id,
158
+ )
159
+ return {
160
+ "success": False,
161
+ "error": f"Workflow '{name}' not found",
162
+ "name": name,
163
+ }
164
+
165
+ # Read the workflow content
166
+ with open(workflow_path, "r", encoding="utf-8") as f:
167
+ content = f.read()
168
+
169
+ emit_success(
170
+ f"Workflow read successfully: {len(content)} characters",
171
+ message_group=group_id,
172
+ )
173
+
174
+ return {
175
+ "success": True,
176
+ "name": name,
177
+ "content": content,
178
+ "path": str(workflow_path),
179
+ "size": len(content),
180
+ }
181
+
182
+ except Exception as e:
183
+ emit_error(
184
+ f"Failed to read workflow: {e}",
185
+ message_group=group_id,
186
+ )
187
+ return {"success": False, "error": str(e), "name": name}
188
+
189
+
190
+ def register_save_workflow(agent):
191
+ """Register the save workflow tool."""
192
+
193
+ @agent.tool
194
+ async def browser_save_workflow(
195
+ context: RunContext,
196
+ name: str,
197
+ content: str,
198
+ ) -> Dict[str, Any]:
199
+ """Save a browser automation workflow to disk for future reuse."""
200
+ return await save_workflow(name, content)
201
+
202
+
203
+ def register_list_workflows(agent):
204
+ """Register the list workflows tool."""
205
+
206
+ @agent.tool
207
+ async def browser_list_workflows(context: RunContext) -> Dict[str, Any]:
208
+ """List all saved browser automation workflows."""
209
+ return await list_workflows()
210
+
211
+
212
+ def register_read_workflow(agent):
213
+ """Register the read workflow tool."""
214
+
215
+ @agent.tool
216
+ async def browser_read_workflow(
217
+ context: RunContext,
218
+ name: str,
219
+ ) -> Dict[str, Any]:
220
+ """Read the contents of a saved browser automation workflow."""
221
+ return await read_workflow(name)
@@ -0,0 +1,259 @@
1
+ """Chromium Terminal Manager - Simple Chromium browser for terminal use.
2
+
3
+ This module provides a browser manager for Chromium terminal automation.
4
+ Each instance gets its own ephemeral browser context, allowing multiple
5
+ terminal QA agents to run simultaneously without profile conflicts.
6
+ """
7
+
8
+ import logging
9
+ import uuid
10
+ from typing import Optional
11
+
12
+ from playwright.async_api import Browser, BrowserContext, Page, async_playwright
13
+
14
+ from code_puppy.messaging import emit_info, emit_success
15
+
16
+ logger = logging.getLogger(__name__)
17
+
18
+ # Store active manager instances by session ID
19
+ _active_managers: dict[str, "ChromiumTerminalManager"] = {}
20
+
21
+
22
+ class ChromiumTerminalManager:
23
+ """Browser manager for Chromium terminal automation.
24
+
25
+ Each instance gets its own ephemeral browser context, allowing multiple
26
+ terminal QA agents to run simultaneously without profile conflicts.
27
+
28
+ Key features:
29
+ - Ephemeral contexts (no profile locking issues)
30
+ - Multiple instances can run simultaneously
31
+ - Visible (headless=False) by default for terminal use
32
+ - Simple API: initialize, get_current_page, new_page, close
33
+
34
+ Usage:
35
+ manager = get_chromium_terminal_manager() # or with session_id
36
+ await manager.async_initialize()
37
+ page = await manager.get_current_page()
38
+ await page.goto("https://example.com")
39
+ await manager.close()
40
+ """
41
+
42
+ _browser: Optional[Browser] = None
43
+ _context: Optional[BrowserContext] = None
44
+ _playwright: Optional[object] = None
45
+ _initialized: bool = False
46
+
47
+ def __init__(self, session_id: Optional[str] = None) -> None:
48
+ """Initialize manager settings.
49
+
50
+ Args:
51
+ session_id: Optional session ID for tracking this instance.
52
+ If None, a UUID will be generated.
53
+ """
54
+ import os
55
+
56
+ self.session_id = session_id or str(uuid.uuid4())[:8]
57
+
58
+ # Default to headless=False - we want to see the terminal browser!
59
+ # Can override with CHROMIUM_HEADLESS=true if needed
60
+ self.headless = os.getenv("CHROMIUM_HEADLESS", "false").lower() == "true"
61
+
62
+ logger.debug(
63
+ f"ChromiumTerminalManager created: session={self.session_id}, "
64
+ f"headless={self.headless}"
65
+ )
66
+
67
+ async def async_initialize(self) -> None:
68
+ """Initialize the Chromium browser.
69
+
70
+ Launches a Chromium browser with an ephemeral context. The browser
71
+ runs in visible mode by default (headless=False) for terminal use.
72
+
73
+ Raises:
74
+ Exception: If browser initialization fails.
75
+ """
76
+ if self._initialized:
77
+ logger.debug(
78
+ f"ChromiumTerminalManager {self.session_id} already initialized"
79
+ )
80
+ return
81
+
82
+ try:
83
+ emit_info(
84
+ f"Initializing Chromium terminal browser (session: {self.session_id})..."
85
+ )
86
+
87
+ # Start Playwright
88
+ self._playwright = await async_playwright().start()
89
+
90
+ # Launch browser (not persistent - allows multiple instances)
91
+ self._browser = await self._playwright.chromium.launch(
92
+ headless=self.headless,
93
+ )
94
+
95
+ # Create ephemeral context
96
+ self._context = await self._browser.new_context()
97
+ self._initialized = True
98
+
99
+ emit_success(
100
+ f"Chromium terminal browser initialized (session: {self.session_id})"
101
+ )
102
+ logger.info(
103
+ f"Chromium initialized: session={self.session_id}, headless={self.headless}"
104
+ )
105
+
106
+ except Exception as e:
107
+ logger.error(f"Failed to initialize Chromium: {e}")
108
+ await self._cleanup()
109
+ raise
110
+
111
+ async def get_current_page(self) -> Optional[Page]:
112
+ """Get the currently active page, creating one if none exist.
113
+
114
+ Lazily initializes the browser if not already initialized.
115
+
116
+ Returns:
117
+ The current page, or None if context is unavailable.
118
+ """
119
+ if not self._initialized or not self._context:
120
+ await self.async_initialize()
121
+
122
+ if not self._context:
123
+ logger.warning("No browser context available")
124
+ return None
125
+
126
+ pages = self._context.pages
127
+ if pages:
128
+ return pages[0]
129
+
130
+ # Create a new blank page if none exist
131
+ logger.debug("No existing pages, creating new blank page")
132
+ return await self._context.new_page()
133
+
134
+ async def new_page(self, url: Optional[str] = None) -> Page:
135
+ """Create a new page, optionally navigating to a URL.
136
+
137
+ Lazily initializes the browser if not already initialized.
138
+
139
+ Args:
140
+ url: Optional URL to navigate to after creating the page.
141
+
142
+ Returns:
143
+ The newly created page.
144
+
145
+ Raises:
146
+ RuntimeError: If browser context is not available.
147
+ """
148
+ if not self._initialized:
149
+ await self.async_initialize()
150
+
151
+ if not self._context:
152
+ raise RuntimeError("Browser context not available")
153
+
154
+ page = await self._context.new_page()
155
+ logger.debug(f"Created new page{f' navigating to {url}' if url else ''}")
156
+
157
+ if url:
158
+ await page.goto(url)
159
+
160
+ return page
161
+
162
+ async def close_page(self, page: Page) -> None:
163
+ """Close a specific page.
164
+
165
+ Args:
166
+ page: The page to close.
167
+ """
168
+ await page.close()
169
+ logger.debug("Page closed")
170
+
171
+ async def get_all_pages(self) -> list[Page]:
172
+ """Get all open pages.
173
+
174
+ Returns:
175
+ List of all open pages, or empty list if no context.
176
+ """
177
+ if not self._context:
178
+ return []
179
+ return self._context.pages
180
+
181
+ async def _cleanup(self, silent: bool = False) -> None:
182
+ """Clean up browser resources.
183
+
184
+ Args:
185
+ silent: If True, suppress all errors (used during shutdown).
186
+ """
187
+ try:
188
+ if self._context:
189
+ try:
190
+ await self._context.close()
191
+ except Exception:
192
+ pass
193
+ self._context = None
194
+
195
+ if self._browser:
196
+ try:
197
+ await self._browser.close()
198
+ except Exception:
199
+ pass
200
+ self._browser = None
201
+
202
+ if self._playwright:
203
+ try:
204
+ await self._playwright.stop()
205
+ except Exception:
206
+ pass
207
+ self._playwright = None
208
+
209
+ self._initialized = False
210
+
211
+ # Remove from active managers
212
+ if self.session_id in _active_managers:
213
+ del _active_managers[self.session_id]
214
+
215
+ if not silent:
216
+ logger.debug(
217
+ f"Browser resources cleaned up (session: {self.session_id})"
218
+ )
219
+
220
+ except Exception as e:
221
+ if not silent:
222
+ logger.warning(f"Warning during cleanup: {e}")
223
+
224
+ async def close(self) -> None:
225
+ """Close the browser and clean up all resources.
226
+
227
+ This properly shuts down the browser and releases all resources.
228
+ Should be called when done with the browser.
229
+ """
230
+ await self._cleanup()
231
+ emit_info(f"Chromium terminal browser closed (session: {self.session_id})")
232
+
233
+
234
+ def get_chromium_terminal_manager(
235
+ session_id: Optional[str] = None,
236
+ ) -> ChromiumTerminalManager:
237
+ """Get or create a ChromiumTerminalManager instance.
238
+
239
+ Args:
240
+ session_id: Optional session ID. If provided and a manager with this
241
+ session exists, returns that manager. Otherwise creates a new one.
242
+ If None, uses 'default' as the session ID.
243
+
244
+ Returns:
245
+ A ChromiumTerminalManager instance.
246
+
247
+ Example:
248
+ # Default session (for single-agent use)
249
+ manager = get_chromium_terminal_manager()
250
+
251
+ # Named session (for multi-agent use)
252
+ manager = get_chromium_terminal_manager("agent-1")
253
+ """
254
+ session_id = session_id or "default"
255
+
256
+ if session_id not in _active_managers:
257
+ _active_managers[session_id] = ChromiumTerminalManager(session_id)
258
+
259
+ return _active_managers[session_id]