code-puppy 0.0.214__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 (231) hide show
  1. code_puppy/__init__.py +7 -1
  2. code_puppy/agents/__init__.py +2 -0
  3. code_puppy/agents/agent_c_reviewer.py +59 -6
  4. code_puppy/agents/agent_code_puppy.py +7 -1
  5. code_puppy/agents/agent_code_reviewer.py +12 -2
  6. code_puppy/agents/agent_cpp_reviewer.py +73 -6
  7. code_puppy/agents/agent_creator_agent.py +45 -4
  8. code_puppy/agents/agent_golang_reviewer.py +92 -3
  9. code_puppy/agents/agent_javascript_reviewer.py +101 -8
  10. code_puppy/agents/agent_manager.py +81 -4
  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 +28 -6
  15. code_puppy/agents/agent_qa_expert.py +98 -6
  16. code_puppy/agents/agent_qa_kitten.py +12 -7
  17. code_puppy/agents/agent_security_auditor.py +113 -3
  18. code_puppy/agents/agent_terminal_qa.py +323 -0
  19. code_puppy/agents/agent_typescript_reviewer.py +106 -7
  20. code_puppy/agents/base_agent.py +802 -176
  21. code_puppy/agents/event_stream_handler.py +350 -0
  22. code_puppy/agents/pack/__init__.py +34 -0
  23. code_puppy/agents/pack/bloodhound.py +304 -0
  24. code_puppy/agents/pack/husky.py +321 -0
  25. code_puppy/agents/pack/retriever.py +393 -0
  26. code_puppy/agents/pack/shepherd.py +348 -0
  27. code_puppy/agents/pack/terrier.py +287 -0
  28. code_puppy/agents/pack/watchdog.py +367 -0
  29. code_puppy/agents/prompt_reviewer.py +145 -0
  30. code_puppy/agents/subagent_stream_handler.py +276 -0
  31. code_puppy/api/__init__.py +13 -0
  32. code_puppy/api/app.py +169 -0
  33. code_puppy/api/main.py +21 -0
  34. code_puppy/api/pty_manager.py +446 -0
  35. code_puppy/api/routers/__init__.py +12 -0
  36. code_puppy/api/routers/agents.py +36 -0
  37. code_puppy/api/routers/commands.py +217 -0
  38. code_puppy/api/routers/config.py +74 -0
  39. code_puppy/api/routers/sessions.py +232 -0
  40. code_puppy/api/templates/terminal.html +361 -0
  41. code_puppy/api/websocket.py +154 -0
  42. code_puppy/callbacks.py +142 -4
  43. code_puppy/chatgpt_codex_client.py +283 -0
  44. code_puppy/claude_cache_client.py +586 -0
  45. code_puppy/cli_runner.py +916 -0
  46. code_puppy/command_line/add_model_menu.py +1079 -0
  47. code_puppy/command_line/agent_menu.py +395 -0
  48. code_puppy/command_line/attachments.py +10 -5
  49. code_puppy/command_line/autosave_menu.py +605 -0
  50. code_puppy/command_line/clipboard.py +527 -0
  51. code_puppy/command_line/colors_menu.py +520 -0
  52. code_puppy/command_line/command_handler.py +176 -738
  53. code_puppy/command_line/command_registry.py +150 -0
  54. code_puppy/command_line/config_commands.py +715 -0
  55. code_puppy/command_line/core_commands.py +792 -0
  56. code_puppy/command_line/diff_menu.py +863 -0
  57. code_puppy/command_line/load_context_completion.py +15 -22
  58. code_puppy/command_line/mcp/base.py +0 -3
  59. code_puppy/command_line/mcp/catalog_server_installer.py +175 -0
  60. code_puppy/command_line/mcp/custom_server_form.py +688 -0
  61. code_puppy/command_line/mcp/custom_server_installer.py +195 -0
  62. code_puppy/command_line/mcp/edit_command.py +148 -0
  63. code_puppy/command_line/mcp/handler.py +9 -4
  64. code_puppy/command_line/mcp/help_command.py +6 -5
  65. code_puppy/command_line/mcp/install_command.py +15 -26
  66. code_puppy/command_line/mcp/install_menu.py +685 -0
  67. code_puppy/command_line/mcp/list_command.py +2 -2
  68. code_puppy/command_line/mcp/logs_command.py +174 -65
  69. code_puppy/command_line/mcp/remove_command.py +2 -2
  70. code_puppy/command_line/mcp/restart_command.py +12 -4
  71. code_puppy/command_line/mcp/search_command.py +16 -10
  72. code_puppy/command_line/mcp/start_all_command.py +18 -6
  73. code_puppy/command_line/mcp/start_command.py +47 -25
  74. code_puppy/command_line/mcp/status_command.py +4 -5
  75. code_puppy/command_line/mcp/stop_all_command.py +7 -1
  76. code_puppy/command_line/mcp/stop_command.py +8 -4
  77. code_puppy/command_line/mcp/test_command.py +2 -2
  78. code_puppy/command_line/mcp/wizard_utils.py +20 -16
  79. code_puppy/command_line/mcp_completion.py +174 -0
  80. code_puppy/command_line/model_picker_completion.py +75 -25
  81. code_puppy/command_line/model_settings_menu.py +884 -0
  82. code_puppy/command_line/motd.py +14 -8
  83. code_puppy/command_line/onboarding_slides.py +179 -0
  84. code_puppy/command_line/onboarding_wizard.py +340 -0
  85. code_puppy/command_line/pin_command_completion.py +329 -0
  86. code_puppy/command_line/prompt_toolkit_completion.py +463 -63
  87. code_puppy/command_line/session_commands.py +296 -0
  88. code_puppy/command_line/utils.py +54 -0
  89. code_puppy/config.py +898 -112
  90. code_puppy/error_logging.py +118 -0
  91. code_puppy/gemini_code_assist.py +385 -0
  92. code_puppy/gemini_model.py +602 -0
  93. code_puppy/http_utils.py +210 -148
  94. code_puppy/keymap.py +128 -0
  95. code_puppy/main.py +5 -698
  96. code_puppy/mcp_/__init__.py +17 -0
  97. code_puppy/mcp_/async_lifecycle.py +35 -4
  98. code_puppy/mcp_/blocking_startup.py +70 -43
  99. code_puppy/mcp_/captured_stdio_server.py +2 -2
  100. code_puppy/mcp_/config_wizard.py +4 -4
  101. code_puppy/mcp_/dashboard.py +15 -6
  102. code_puppy/mcp_/managed_server.py +65 -38
  103. code_puppy/mcp_/manager.py +146 -52
  104. code_puppy/mcp_/mcp_logs.py +224 -0
  105. code_puppy/mcp_/registry.py +6 -6
  106. code_puppy/mcp_/server_registry_catalog.py +24 -5
  107. code_puppy/messaging/__init__.py +199 -2
  108. code_puppy/messaging/bus.py +610 -0
  109. code_puppy/messaging/commands.py +167 -0
  110. code_puppy/messaging/markdown_patches.py +57 -0
  111. code_puppy/messaging/message_queue.py +17 -48
  112. code_puppy/messaging/messages.py +500 -0
  113. code_puppy/messaging/queue_console.py +1 -24
  114. code_puppy/messaging/renderers.py +43 -146
  115. code_puppy/messaging/rich_renderer.py +1027 -0
  116. code_puppy/messaging/spinner/__init__.py +21 -5
  117. code_puppy/messaging/spinner/console_spinner.py +86 -51
  118. code_puppy/messaging/subagent_console.py +461 -0
  119. code_puppy/model_factory.py +634 -83
  120. code_puppy/model_utils.py +167 -0
  121. code_puppy/models.json +66 -68
  122. code_puppy/models_dev_api.json +1 -0
  123. code_puppy/models_dev_parser.py +592 -0
  124. code_puppy/plugins/__init__.py +164 -10
  125. code_puppy/plugins/antigravity_oauth/__init__.py +10 -0
  126. code_puppy/plugins/antigravity_oauth/accounts.py +406 -0
  127. code_puppy/plugins/antigravity_oauth/antigravity_model.py +704 -0
  128. code_puppy/plugins/antigravity_oauth/config.py +42 -0
  129. code_puppy/plugins/antigravity_oauth/constants.py +136 -0
  130. code_puppy/plugins/antigravity_oauth/oauth.py +478 -0
  131. code_puppy/plugins/antigravity_oauth/register_callbacks.py +406 -0
  132. code_puppy/plugins/antigravity_oauth/storage.py +271 -0
  133. code_puppy/plugins/antigravity_oauth/test_plugin.py +319 -0
  134. code_puppy/plugins/antigravity_oauth/token.py +167 -0
  135. code_puppy/plugins/antigravity_oauth/transport.py +767 -0
  136. code_puppy/plugins/antigravity_oauth/utils.py +169 -0
  137. code_puppy/plugins/chatgpt_oauth/__init__.py +8 -0
  138. code_puppy/plugins/chatgpt_oauth/config.py +52 -0
  139. code_puppy/plugins/chatgpt_oauth/oauth_flow.py +328 -0
  140. code_puppy/plugins/chatgpt_oauth/register_callbacks.py +94 -0
  141. code_puppy/plugins/chatgpt_oauth/test_plugin.py +293 -0
  142. code_puppy/plugins/chatgpt_oauth/utils.py +489 -0
  143. code_puppy/plugins/claude_code_oauth/README.md +167 -0
  144. code_puppy/plugins/claude_code_oauth/SETUP.md +93 -0
  145. code_puppy/plugins/claude_code_oauth/__init__.py +6 -0
  146. code_puppy/plugins/claude_code_oauth/config.py +50 -0
  147. code_puppy/plugins/claude_code_oauth/register_callbacks.py +308 -0
  148. code_puppy/plugins/claude_code_oauth/test_plugin.py +283 -0
  149. code_puppy/plugins/claude_code_oauth/utils.py +518 -0
  150. code_puppy/plugins/customizable_commands/__init__.py +0 -0
  151. code_puppy/plugins/customizable_commands/register_callbacks.py +169 -0
  152. code_puppy/plugins/example_custom_command/README.md +280 -0
  153. code_puppy/plugins/example_custom_command/register_callbacks.py +2 -2
  154. code_puppy/plugins/file_permission_handler/__init__.py +4 -0
  155. code_puppy/plugins/file_permission_handler/register_callbacks.py +523 -0
  156. code_puppy/plugins/frontend_emitter/__init__.py +25 -0
  157. code_puppy/plugins/frontend_emitter/emitter.py +121 -0
  158. code_puppy/plugins/frontend_emitter/register_callbacks.py +261 -0
  159. code_puppy/plugins/oauth_puppy_html.py +228 -0
  160. code_puppy/plugins/shell_safety/__init__.py +6 -0
  161. code_puppy/plugins/shell_safety/agent_shell_safety.py +69 -0
  162. code_puppy/plugins/shell_safety/command_cache.py +156 -0
  163. code_puppy/plugins/shell_safety/register_callbacks.py +202 -0
  164. code_puppy/prompts/antigravity_system_prompt.md +1 -0
  165. code_puppy/prompts/codex_system_prompt.md +310 -0
  166. code_puppy/pydantic_patches.py +131 -0
  167. code_puppy/reopenable_async_client.py +8 -8
  168. code_puppy/round_robin_model.py +9 -12
  169. code_puppy/session_storage.py +2 -1
  170. code_puppy/status_display.py +21 -4
  171. code_puppy/summarization_agent.py +41 -13
  172. code_puppy/terminal_utils.py +418 -0
  173. code_puppy/tools/__init__.py +37 -1
  174. code_puppy/tools/agent_tools.py +536 -52
  175. code_puppy/tools/browser/__init__.py +37 -0
  176. code_puppy/tools/browser/browser_control.py +19 -23
  177. code_puppy/tools/browser/browser_interactions.py +41 -48
  178. code_puppy/tools/browser/browser_locators.py +36 -38
  179. code_puppy/tools/browser/browser_manager.py +316 -0
  180. code_puppy/tools/browser/browser_navigation.py +16 -16
  181. code_puppy/tools/browser/browser_screenshot.py +79 -143
  182. code_puppy/tools/browser/browser_scripts.py +32 -42
  183. code_puppy/tools/browser/browser_workflows.py +44 -27
  184. code_puppy/tools/browser/chromium_terminal_manager.py +259 -0
  185. code_puppy/tools/browser/terminal_command_tools.py +521 -0
  186. code_puppy/tools/browser/terminal_screenshot_tools.py +556 -0
  187. code_puppy/tools/browser/terminal_tools.py +525 -0
  188. code_puppy/tools/command_runner.py +930 -147
  189. code_puppy/tools/common.py +1113 -5
  190. code_puppy/tools/display.py +84 -0
  191. code_puppy/tools/file_modifications.py +288 -89
  192. code_puppy/tools/file_operations.py +226 -154
  193. code_puppy/tools/subagent_context.py +158 -0
  194. code_puppy/uvx_detection.py +242 -0
  195. code_puppy/version_checker.py +30 -11
  196. code_puppy-0.0.366.data/data/code_puppy/models.json +110 -0
  197. code_puppy-0.0.366.data/data/code_puppy/models_dev_api.json +1 -0
  198. {code_puppy-0.0.214.dist-info → code_puppy-0.0.366.dist-info}/METADATA +149 -75
  199. code_puppy-0.0.366.dist-info/RECORD +217 -0
  200. {code_puppy-0.0.214.dist-info → code_puppy-0.0.366.dist-info}/WHEEL +1 -1
  201. code_puppy/command_line/mcp/add_command.py +0 -183
  202. code_puppy/messaging/spinner/textual_spinner.py +0 -106
  203. code_puppy/tools/browser/camoufox_manager.py +0 -216
  204. code_puppy/tools/browser/vqa_agent.py +0 -70
  205. code_puppy/tui/__init__.py +0 -10
  206. code_puppy/tui/app.py +0 -1105
  207. code_puppy/tui/components/__init__.py +0 -21
  208. code_puppy/tui/components/chat_view.py +0 -551
  209. code_puppy/tui/components/command_history_modal.py +0 -218
  210. code_puppy/tui/components/copy_button.py +0 -139
  211. code_puppy/tui/components/custom_widgets.py +0 -63
  212. code_puppy/tui/components/human_input_modal.py +0 -175
  213. code_puppy/tui/components/input_area.py +0 -167
  214. code_puppy/tui/components/sidebar.py +0 -309
  215. code_puppy/tui/components/status_bar.py +0 -185
  216. code_puppy/tui/messages.py +0 -27
  217. code_puppy/tui/models/__init__.py +0 -8
  218. code_puppy/tui/models/chat_message.py +0 -25
  219. code_puppy/tui/models/command_history.py +0 -89
  220. code_puppy/tui/models/enums.py +0 -24
  221. code_puppy/tui/screens/__init__.py +0 -17
  222. code_puppy/tui/screens/autosave_picker.py +0 -175
  223. code_puppy/tui/screens/help.py +0 -130
  224. code_puppy/tui/screens/mcp_install_wizard.py +0 -803
  225. code_puppy/tui/screens/settings.py +0 -306
  226. code_puppy/tui/screens/tools.py +0 -74
  227. code_puppy/tui_state.py +0 -55
  228. code_puppy-0.0.214.data/data/code_puppy/models.json +0 -112
  229. code_puppy-0.0.214.dist-info/RECORD +0 -131
  230. {code_puppy-0.0.214.dist-info → code_puppy-0.0.366.dist-info}/entry_points.txt +0 -0
  231. {code_puppy-0.0.214.dist-info → code_puppy-0.0.366.dist-info}/licenses/LICENSE +0 -0
@@ -0,0 +1,316 @@
1
+ """Playwright browser manager for browser automation.
2
+
3
+ Supports multiple simultaneous instances with unique profile directories.
4
+ """
5
+
6
+ import asyncio
7
+ import atexit
8
+ import contextvars
9
+ import os
10
+ from pathlib import Path
11
+ from typing import Optional
12
+
13
+ from playwright.async_api import Browser, BrowserContext, Page
14
+
15
+ from code_puppy import config
16
+ from code_puppy.messaging import emit_info, emit_success, emit_warning
17
+
18
+ # Store active manager instances by session ID
19
+ _active_managers: dict[str, "BrowserManager"] = {}
20
+
21
+ # Context variable for browser session - properly inherits through async tasks
22
+ # This allows parallel agent invocations to each have their own browser instance
23
+ _browser_session_var: contextvars.ContextVar[Optional[str]] = contextvars.ContextVar(
24
+ "browser_session", default=None
25
+ )
26
+
27
+
28
+ def set_browser_session(session_id: Optional[str]) -> contextvars.Token:
29
+ """Set the browser session ID for the current context.
30
+
31
+ This must be called BEFORE any tool calls that use the browser.
32
+ The context will properly propagate to all subsequent async calls.
33
+
34
+ Args:
35
+ session_id: The session ID to use for browser operations.
36
+
37
+ Returns:
38
+ A token that can be used to reset the context.
39
+ """
40
+ return _browser_session_var.set(session_id)
41
+
42
+
43
+ def get_browser_session() -> Optional[str]:
44
+ """Get the browser session ID for the current context.
45
+
46
+ Returns:
47
+ The current session ID, or None if not set.
48
+ """
49
+ return _browser_session_var.get()
50
+
51
+
52
+ def get_session_browser_manager() -> "BrowserManager":
53
+ """Get the BrowserManager for the current context's session.
54
+
55
+ This is the preferred way to get a browser manager in tool functions,
56
+ as it automatically uses the correct session ID for the current
57
+ agent context.
58
+
59
+ Returns:
60
+ A BrowserManager instance for the current session.
61
+ """
62
+ session_id = get_browser_session()
63
+ return get_browser_manager(session_id)
64
+
65
+
66
+ # Flag to track if cleanup has already run
67
+ _cleanup_done: bool = False
68
+
69
+
70
+ class BrowserManager:
71
+ """Browser manager for Playwright-based browser automation.
72
+
73
+ Supports multiple simultaneous instances, each with its own profile directory.
74
+ Uses Chromium by default for maximum compatibility.
75
+ """
76
+
77
+ _browser: Optional[Browser] = None
78
+ _context: Optional[BrowserContext] = None
79
+ _initialized: bool = False
80
+
81
+ def __init__(self, session_id: Optional[str] = None):
82
+ """Initialize manager settings.
83
+
84
+ Args:
85
+ session_id: Optional session ID for this instance.
86
+ If None, uses 'default' as the session ID.
87
+ """
88
+ self.session_id = session_id or "default"
89
+
90
+ # Default to headless=True (no browser spam during tests)
91
+ # Override with BROWSER_HEADLESS=false to see the browser
92
+ self.headless = os.getenv("BROWSER_HEADLESS", "true").lower() != "false"
93
+ self.homepage = "https://www.google.com"
94
+
95
+ # Unique profile directory per session for browser state
96
+ self.profile_dir = self._get_profile_directory()
97
+
98
+ def _get_profile_directory(self) -> Path:
99
+ """Get or create the profile directory for this session.
100
+
101
+ Each session gets its own profile directory under:
102
+ XDG_CACHE_HOME/code_puppy/browser_profiles/<session_id>/
103
+
104
+ This allows multiple instances to run simultaneously.
105
+ """
106
+ cache_dir = Path(config.CACHE_DIR)
107
+ profiles_base = cache_dir / "browser_profiles"
108
+ profile_path = profiles_base / self.session_id
109
+ profile_path.mkdir(parents=True, exist_ok=True, mode=0o700)
110
+ return profile_path
111
+
112
+ async def async_initialize(self) -> None:
113
+ """Initialize Chromium browser via Playwright."""
114
+ if self._initialized:
115
+ return
116
+
117
+ try:
118
+ emit_info(f"Initializing Chromium browser (session: {self.session_id})...")
119
+ await self._initialize_browser()
120
+ self._initialized = True
121
+
122
+ except Exception:
123
+ await self._cleanup()
124
+ raise
125
+
126
+ async def _initialize_browser(self) -> None:
127
+ """Initialize Playwright Chromium browser with persistent context."""
128
+ from playwright.async_api import async_playwright
129
+
130
+ emit_info(f"Using persistent profile: {self.profile_dir}")
131
+
132
+ pw = await async_playwright().start()
133
+ # Use persistent context directory for Chromium to preserve browser state
134
+ context = await pw.chromium.launch_persistent_context(
135
+ user_data_dir=str(self.profile_dir), headless=self.headless
136
+ )
137
+ self._context = context
138
+ self._browser = context.browser
139
+ self._initialized = True
140
+
141
+ async def get_current_page(self) -> Optional[Page]:
142
+ """Get the currently active page. Lazily creates one if none exist."""
143
+ if not self._initialized or not self._context:
144
+ await self.async_initialize()
145
+
146
+ if not self._context:
147
+ return None
148
+
149
+ pages = self._context.pages
150
+ if pages:
151
+ return pages[0]
152
+
153
+ # Lazily create a new blank page without navigation
154
+ return await self._context.new_page()
155
+
156
+ async def new_page(self, url: Optional[str] = None) -> Page:
157
+ """Create a new page and optionally navigate to URL."""
158
+ if not self._initialized:
159
+ await self.async_initialize()
160
+
161
+ page = await self._context.new_page()
162
+ if url:
163
+ await page.goto(url)
164
+ return page
165
+
166
+ async def close_page(self, page: Page) -> None:
167
+ """Close a specific page."""
168
+ await page.close()
169
+
170
+ async def get_all_pages(self) -> list[Page]:
171
+ """Get all open pages."""
172
+ if not self._context:
173
+ return []
174
+ return self._context.pages
175
+
176
+ async def _cleanup(self, silent: bool = False) -> None:
177
+ """Clean up browser resources and save persistent state.
178
+
179
+ Args:
180
+ silent: If True, suppress all errors (used during shutdown).
181
+ """
182
+ try:
183
+ # Save browser state before closing (cookies, localStorage, etc.)
184
+ if self._context:
185
+ try:
186
+ storage_state_path = self.profile_dir / "storage_state.json"
187
+ await self._context.storage_state(path=str(storage_state_path))
188
+ if not silent:
189
+ emit_success(f"Browser state saved to {storage_state_path}")
190
+ except Exception as e:
191
+ if not silent:
192
+ emit_warning(f"Could not save storage state: {e}")
193
+
194
+ try:
195
+ await self._context.close()
196
+ except Exception:
197
+ pass # Ignore errors during context close
198
+ self._context = None
199
+
200
+ if self._browser:
201
+ try:
202
+ await self._browser.close()
203
+ except Exception:
204
+ pass # Ignore errors during browser close
205
+ self._browser = None
206
+
207
+ self._initialized = False
208
+
209
+ # Remove from active managers
210
+ if self.session_id in _active_managers:
211
+ del _active_managers[self.session_id]
212
+
213
+ except Exception as e:
214
+ if not silent:
215
+ emit_warning(f"Warning during cleanup: {e}")
216
+
217
+ async def close(self) -> None:
218
+ """Close the browser and clean up resources."""
219
+ await self._cleanup()
220
+ emit_info(f"Browser closed (session: {self.session_id})")
221
+
222
+
223
+ def get_browser_manager(session_id: Optional[str] = None) -> BrowserManager:
224
+ """Get or create a BrowserManager instance.
225
+
226
+ Args:
227
+ session_id: Optional session ID. If provided and a manager with this
228
+ session exists, returns that manager. Otherwise creates a new one.
229
+ If None, uses 'default' as the session ID.
230
+
231
+ Returns:
232
+ A BrowserManager instance.
233
+
234
+ Example:
235
+ # Default session (for single-agent use)
236
+ manager = get_browser_manager()
237
+
238
+ # Named session (for multi-agent use)
239
+ manager = get_browser_manager("qa-agent-1")
240
+ """
241
+ session_id = session_id or "default"
242
+
243
+ if session_id not in _active_managers:
244
+ _active_managers[session_id] = BrowserManager(session_id)
245
+
246
+ return _active_managers[session_id]
247
+
248
+
249
+ async def cleanup_all_browsers() -> None:
250
+ """Close all active browser manager instances.
251
+
252
+ This should be called before application exit to ensure all browser
253
+ connections are properly closed and no dangling futures remain.
254
+ """
255
+ global _cleanup_done
256
+
257
+ if _cleanup_done:
258
+ return
259
+
260
+ _cleanup_done = True
261
+
262
+ # Get a copy of the keys since we'll be modifying the dict during cleanup
263
+ session_ids = list(_active_managers.keys())
264
+
265
+ for session_id in session_ids:
266
+ manager = _active_managers.get(session_id)
267
+ if manager and manager._initialized:
268
+ try:
269
+ await manager._cleanup(silent=True)
270
+ except Exception:
271
+ pass # Silently ignore all errors during exit cleanup
272
+
273
+
274
+ def _sync_cleanup_browsers() -> None:
275
+ """Synchronous cleanup wrapper for use with atexit.
276
+
277
+ Creates a new event loop to run the async cleanup since the main
278
+ event loop may have already been closed when atexit handlers run.
279
+ """
280
+ global _cleanup_done
281
+
282
+ if _cleanup_done or not _active_managers:
283
+ return
284
+
285
+ try:
286
+ # Try to get the running loop first
287
+ try:
288
+ loop = asyncio.get_running_loop()
289
+ # If we're in an async context, schedule the cleanup
290
+ # but this is unlikely in atexit handlers
291
+ loop.create_task(cleanup_all_browsers())
292
+ return
293
+ except RuntimeError:
294
+ pass # No running loop, which is expected in atexit
295
+
296
+ # Create a new event loop for cleanup
297
+ loop = asyncio.new_event_loop()
298
+ asyncio.set_event_loop(loop)
299
+ try:
300
+ loop.run_until_complete(cleanup_all_browsers())
301
+ finally:
302
+ loop.close()
303
+ except Exception:
304
+ # Silently swallow ALL errors during exit cleanup
305
+ # We don't want to spam the user with errors on exit
306
+ pass
307
+
308
+
309
+ # Register the cleanup handler with atexit
310
+ # This ensures browsers are closed even if close_browser() isn't explicitly called
311
+ atexit.register(_sync_cleanup_browsers)
312
+
313
+
314
+ # Backwards compatibility aliases
315
+ CamoufoxManager = BrowserManager
316
+ get_camoufox_manager = get_browser_manager
@@ -4,21 +4,21 @@ from typing import Any, Dict
4
4
 
5
5
  from pydantic_ai import RunContext
6
6
 
7
- from code_puppy.messaging import emit_info
7
+ from code_puppy.messaging import emit_error, emit_info, emit_success
8
8
  from code_puppy.tools.common import generate_group_id
9
9
 
10
- from .camoufox_manager import get_camoufox_manager
10
+ from .browser_manager import get_session_browser_manager
11
11
 
12
12
 
13
13
  async def navigate_to_url(url: str) -> Dict[str, Any]:
14
14
  """Navigate to a specific URL."""
15
15
  group_id = generate_group_id("browser_navigate", url)
16
16
  emit_info(
17
- f"[bold white on blue] BROWSER NAVIGATE [/bold white on blue] 🌐 {url}",
17
+ f"BROWSER NAVIGATE 🌐 {url}",
18
18
  message_group=group_id,
19
19
  )
20
20
  try:
21
- browser_manager = get_camoufox_manager()
21
+ browser_manager = get_session_browser_manager()
22
22
  page = await browser_manager.get_current_page()
23
23
 
24
24
  if not page:
@@ -31,12 +31,12 @@ async def navigate_to_url(url: str) -> Dict[str, Any]:
31
31
  final_url = page.url
32
32
  title = await page.title()
33
33
 
34
- emit_info(f"[green]Navigated to: {final_url}[/green]", message_group=group_id)
34
+ emit_success(f"Navigated to: {final_url}", message_group=group_id)
35
35
 
36
36
  return {"success": True, "url": final_url, "title": title, "requested_url": url}
37
37
 
38
38
  except Exception as e:
39
- emit_info(f"[red]Navigation failed: {str(e)}[/red]", message_group=group_id)
39
+ emit_error(f"Navigation failed: {str(e)}", message_group=group_id)
40
40
  return {"success": False, "error": str(e), "url": url}
41
41
 
42
42
 
@@ -44,11 +44,11 @@ async def get_page_info() -> Dict[str, Any]:
44
44
  """Get current page information."""
45
45
  group_id = generate_group_id("browser_get_page_info")
46
46
  emit_info(
47
- "[bold white on blue] BROWSER GET PAGE INFO [/bold white on blue] 📌",
47
+ "BROWSER GET PAGE INFO 📌",
48
48
  message_group=group_id,
49
49
  )
50
50
  try:
51
- browser_manager = get_camoufox_manager()
51
+ browser_manager = get_session_browser_manager()
52
52
  page = await browser_manager.get_current_page()
53
53
 
54
54
  if not page:
@@ -67,11 +67,11 @@ async def go_back() -> Dict[str, Any]:
67
67
  """Navigate back in browser history."""
68
68
  group_id = generate_group_id("browser_go_back")
69
69
  emit_info(
70
- "[bold white on blue] BROWSER GO BACK [/bold white on blue] ⬅️",
70
+ "BROWSER GO BACK ⬅️",
71
71
  message_group=group_id,
72
72
  )
73
73
  try:
74
- browser_manager = get_camoufox_manager()
74
+ browser_manager = get_session_browser_manager()
75
75
  page = await browser_manager.get_current_page()
76
76
 
77
77
  if not page:
@@ -89,11 +89,11 @@ async def go_forward() -> Dict[str, Any]:
89
89
  """Navigate forward in browser history."""
90
90
  group_id = generate_group_id("browser_go_forward")
91
91
  emit_info(
92
- "[bold white on blue] BROWSER GO FORWARD [/bold white on blue] ➡️",
92
+ "BROWSER GO FORWARD ➡️",
93
93
  message_group=group_id,
94
94
  )
95
95
  try:
96
- browser_manager = get_camoufox_manager()
96
+ browser_manager = get_session_browser_manager()
97
97
  page = await browser_manager.get_current_page()
98
98
 
99
99
  if not page:
@@ -111,11 +111,11 @@ async def reload_page(wait_until: str = "domcontentloaded") -> Dict[str, Any]:
111
111
  """Reload the current page."""
112
112
  group_id = generate_group_id("browser_reload", wait_until)
113
113
  emit_info(
114
- f"[bold white on blue] BROWSER RELOAD [/bold white on blue] 🔄 wait_until={wait_until}",
114
+ f"BROWSER RELOAD 🔄 wait_until={wait_until}",
115
115
  message_group=group_id,
116
116
  )
117
117
  try:
118
- browser_manager = get_camoufox_manager()
118
+ browser_manager = get_session_browser_manager()
119
119
  page = await browser_manager.get_current_page()
120
120
 
121
121
  if not page:
@@ -135,11 +135,11 @@ async def wait_for_load_state(
135
135
  """Wait for page to reach a specific load state."""
136
136
  group_id = generate_group_id("browser_wait_for_load", f"{state}_{timeout}")
137
137
  emit_info(
138
- f"[bold white on blue] BROWSER WAIT FOR LOAD [/bold white on blue] ⏱️ state={state} timeout={timeout}ms",
138
+ f"BROWSER WAIT FOR LOAD ⏱️ state={state} timeout={timeout}ms",
139
139
  message_group=group_id,
140
140
  )
141
141
  try:
142
- browser_manager = get_camoufox_manager()
142
+ browser_manager = get_session_browser_manager()
143
143
  page = await browser_manager.get_current_page()
144
144
 
145
145
  if not page: