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
@@ -5,15 +5,16 @@ from typing import Any, Dict
5
5
 
6
6
  from pydantic_ai import RunContext
7
7
 
8
- from code_puppy.messaging import emit_info
8
+ from code_puppy import config
9
+ from code_puppy.messaging import emit_error, emit_info, emit_success, emit_warning
9
10
  from code_puppy.tools.common import generate_group_id
10
11
 
11
12
 
12
13
  def get_workflows_directory() -> Path:
13
- """Get the browser workflows directory, creating it if it doesn't exist."""
14
- home_dir = Path.home()
15
- workflows_dir = home_dir / ".code_puppy" / "browser_workflows"
16
- workflows_dir.mkdir(parents=True, exist_ok=True)
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)
17
18
  return workflows_dir
18
19
 
19
20
 
@@ -21,15 +22,33 @@ async def save_workflow(name: str, content: str) -> Dict[str, Any]:
21
22
  """Save a browser workflow as a markdown file."""
22
23
  group_id = generate_group_id("save_workflow", name)
23
24
  emit_info(
24
- f"[bold white on blue] SAVE WORKFLOW [/bold white on blue] 💾 name='{name}'",
25
+ f"SAVE WORKFLOW 💾 name='{name}'",
25
26
  message_group=group_id,
26
27
  )
27
28
 
28
29
  try:
29
30
  workflows_dir = get_workflows_directory()
30
31
 
31
- # Clean up the filename - remove spaces, special chars, etc.
32
- safe_name = "".join(c for c in name if c.isalnum() or c in ("-", "_")).lower()
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
+
33
52
  if not safe_name:
34
53
  safe_name = "workflow"
35
54
 
@@ -43,8 +62,8 @@ async def save_workflow(name: str, content: str) -> Dict[str, Any]:
43
62
  with open(workflow_path, "w", encoding="utf-8") as f:
44
63
  f.write(content)
45
64
 
46
- emit_info(
47
- f"[green]✅ Workflow saved successfully: {workflow_path}[/green]",
65
+ emit_success(
66
+ f"Workflow saved successfully: {workflow_path}",
48
67
  message_group=group_id,
49
68
  )
50
69
 
@@ -56,8 +75,8 @@ async def save_workflow(name: str, content: str) -> Dict[str, Any]:
56
75
  }
57
76
 
58
77
  except Exception as e:
59
- emit_info(
60
- f"[red]❌ Failed to save workflow: {e}[/red]",
78
+ emit_error(
79
+ f"Failed to save workflow: {e}",
61
80
  message_group=group_id,
62
81
  )
63
82
  return {"success": False, "error": str(e), "name": name}
@@ -67,7 +86,7 @@ async def list_workflows() -> Dict[str, Any]:
67
86
  """List all available browser workflows."""
68
87
  group_id = generate_group_id("list_workflows")
69
88
  emit_info(
70
- "[bold white on blue] LIST WORKFLOWS [/bold white on blue] 📋",
89
+ "LIST WORKFLOWS 📋",
71
90
  message_group=group_id,
72
91
  )
73
92
 
@@ -90,15 +109,13 @@ async def list_workflows() -> Dict[str, Any]:
90
109
  }
91
110
  )
92
111
  except Exception as e:
93
- emit_info(
94
- f"[yellow]Warning: Could not read {workflow_file}: {e}[/yellow]"
95
- )
112
+ emit_warning(f"Could not read {workflow_file}: {e}")
96
113
 
97
114
  # Sort by modification time (newest first)
98
115
  workflows.sort(key=lambda x: x["modified"], reverse=True)
99
116
 
100
- emit_info(
101
- f"[green]✅ Found {len(workflows)} workflow(s)[/green]",
117
+ emit_success(
118
+ f"Found {len(workflows)} workflow(s)",
102
119
  message_group=group_id,
103
120
  )
104
121
 
@@ -110,8 +127,8 @@ async def list_workflows() -> Dict[str, Any]:
110
127
  }
111
128
 
112
129
  except Exception as e:
113
- emit_info(
114
- f"[red]❌ Failed to list workflows: {e}[/red]",
130
+ emit_error(
131
+ f"Failed to list workflows: {e}",
115
132
  message_group=group_id,
116
133
  )
117
134
  return {"success": False, "error": str(e)}
@@ -121,7 +138,7 @@ async def read_workflow(name: str) -> Dict[str, Any]:
121
138
  """Read a saved browser workflow."""
122
139
  group_id = generate_group_id("read_workflow", name)
123
140
  emit_info(
124
- f"[bold white on blue] READ WORKFLOW [/bold white on blue] 📖 name='{name}'",
141
+ f"READ WORKFLOW 📖 name='{name}'",
125
142
  message_group=group_id,
126
143
  )
127
144
 
@@ -135,8 +152,8 @@ async def read_workflow(name: str) -> Dict[str, Any]:
135
152
  workflow_path = workflows_dir / name
136
153
 
137
154
  if not workflow_path.exists():
138
- emit_info(
139
- f"[red]❌ Workflow not found: {name}[/red]",
155
+ emit_error(
156
+ f"Workflow not found: {name}",
140
157
  message_group=group_id,
141
158
  )
142
159
  return {
@@ -149,8 +166,8 @@ async def read_workflow(name: str) -> Dict[str, Any]:
149
166
  with open(workflow_path, "r", encoding="utf-8") as f:
150
167
  content = f.read()
151
168
 
152
- emit_info(
153
- f"[green]✅ Workflow read successfully: {len(content)} characters[/green]",
169
+ emit_success(
170
+ f"Workflow read successfully: {len(content)} characters",
154
171
  message_group=group_id,
155
172
  )
156
173
 
@@ -163,8 +180,8 @@ async def read_workflow(name: str) -> Dict[str, Any]:
163
180
  }
164
181
 
165
182
  except Exception as e:
166
- emit_info(
167
- f"[red]❌ Failed to read workflow: {e}[/red]",
183
+ emit_error(
184
+ f"Failed to read workflow: {e}",
168
185
  message_group=group_id,
169
186
  )
170
187
  return {"success": False, "error": str(e), "name": 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]