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.
- code_puppy/__init__.py +7 -1
- code_puppy/agents/__init__.py +2 -0
- code_puppy/agents/agent_c_reviewer.py +59 -6
- code_puppy/agents/agent_code_puppy.py +7 -1
- code_puppy/agents/agent_code_reviewer.py +12 -2
- code_puppy/agents/agent_cpp_reviewer.py +73 -6
- code_puppy/agents/agent_creator_agent.py +45 -4
- code_puppy/agents/agent_golang_reviewer.py +92 -3
- code_puppy/agents/agent_javascript_reviewer.py +101 -8
- code_puppy/agents/agent_manager.py +81 -4
- code_puppy/agents/agent_pack_leader.py +383 -0
- code_puppy/agents/agent_planning.py +163 -0
- code_puppy/agents/agent_python_programmer.py +165 -0
- code_puppy/agents/agent_python_reviewer.py +28 -6
- code_puppy/agents/agent_qa_expert.py +98 -6
- code_puppy/agents/agent_qa_kitten.py +12 -7
- code_puppy/agents/agent_security_auditor.py +113 -3
- code_puppy/agents/agent_terminal_qa.py +323 -0
- code_puppy/agents/agent_typescript_reviewer.py +106 -7
- code_puppy/agents/base_agent.py +802 -176
- code_puppy/agents/event_stream_handler.py +350 -0
- code_puppy/agents/pack/__init__.py +34 -0
- code_puppy/agents/pack/bloodhound.py +304 -0
- code_puppy/agents/pack/husky.py +321 -0
- code_puppy/agents/pack/retriever.py +393 -0
- code_puppy/agents/pack/shepherd.py +348 -0
- code_puppy/agents/pack/terrier.py +287 -0
- code_puppy/agents/pack/watchdog.py +367 -0
- code_puppy/agents/prompt_reviewer.py +145 -0
- code_puppy/agents/subagent_stream_handler.py +276 -0
- code_puppy/api/__init__.py +13 -0
- code_puppy/api/app.py +169 -0
- code_puppy/api/main.py +21 -0
- code_puppy/api/pty_manager.py +446 -0
- code_puppy/api/routers/__init__.py +12 -0
- code_puppy/api/routers/agents.py +36 -0
- code_puppy/api/routers/commands.py +217 -0
- code_puppy/api/routers/config.py +74 -0
- code_puppy/api/routers/sessions.py +232 -0
- code_puppy/api/templates/terminal.html +361 -0
- code_puppy/api/websocket.py +154 -0
- code_puppy/callbacks.py +142 -4
- code_puppy/chatgpt_codex_client.py +283 -0
- code_puppy/claude_cache_client.py +586 -0
- code_puppy/cli_runner.py +916 -0
- code_puppy/command_line/add_model_menu.py +1079 -0
- code_puppy/command_line/agent_menu.py +395 -0
- code_puppy/command_line/attachments.py +10 -5
- code_puppy/command_line/autosave_menu.py +605 -0
- code_puppy/command_line/clipboard.py +527 -0
- code_puppy/command_line/colors_menu.py +520 -0
- code_puppy/command_line/command_handler.py +176 -738
- code_puppy/command_line/command_registry.py +150 -0
- code_puppy/command_line/config_commands.py +715 -0
- code_puppy/command_line/core_commands.py +792 -0
- code_puppy/command_line/diff_menu.py +863 -0
- code_puppy/command_line/load_context_completion.py +15 -22
- code_puppy/command_line/mcp/base.py +0 -3
- code_puppy/command_line/mcp/catalog_server_installer.py +175 -0
- code_puppy/command_line/mcp/custom_server_form.py +688 -0
- code_puppy/command_line/mcp/custom_server_installer.py +195 -0
- code_puppy/command_line/mcp/edit_command.py +148 -0
- code_puppy/command_line/mcp/handler.py +9 -4
- code_puppy/command_line/mcp/help_command.py +6 -5
- code_puppy/command_line/mcp/install_command.py +15 -26
- code_puppy/command_line/mcp/install_menu.py +685 -0
- code_puppy/command_line/mcp/list_command.py +2 -2
- code_puppy/command_line/mcp/logs_command.py +174 -65
- code_puppy/command_line/mcp/remove_command.py +2 -2
- code_puppy/command_line/mcp/restart_command.py +12 -4
- code_puppy/command_line/mcp/search_command.py +16 -10
- code_puppy/command_line/mcp/start_all_command.py +18 -6
- code_puppy/command_line/mcp/start_command.py +47 -25
- code_puppy/command_line/mcp/status_command.py +4 -5
- code_puppy/command_line/mcp/stop_all_command.py +7 -1
- code_puppy/command_line/mcp/stop_command.py +8 -4
- code_puppy/command_line/mcp/test_command.py +2 -2
- code_puppy/command_line/mcp/wizard_utils.py +20 -16
- code_puppy/command_line/mcp_completion.py +174 -0
- code_puppy/command_line/model_picker_completion.py +75 -25
- code_puppy/command_line/model_settings_menu.py +884 -0
- code_puppy/command_line/motd.py +14 -8
- code_puppy/command_line/onboarding_slides.py +179 -0
- code_puppy/command_line/onboarding_wizard.py +340 -0
- code_puppy/command_line/pin_command_completion.py +329 -0
- code_puppy/command_line/prompt_toolkit_completion.py +463 -63
- code_puppy/command_line/session_commands.py +296 -0
- code_puppy/command_line/utils.py +54 -0
- code_puppy/config.py +898 -112
- code_puppy/error_logging.py +118 -0
- code_puppy/gemini_code_assist.py +385 -0
- code_puppy/gemini_model.py +602 -0
- code_puppy/http_utils.py +210 -148
- code_puppy/keymap.py +128 -0
- code_puppy/main.py +5 -698
- code_puppy/mcp_/__init__.py +17 -0
- code_puppy/mcp_/async_lifecycle.py +35 -4
- code_puppy/mcp_/blocking_startup.py +70 -43
- code_puppy/mcp_/captured_stdio_server.py +2 -2
- code_puppy/mcp_/config_wizard.py +4 -4
- code_puppy/mcp_/dashboard.py +15 -6
- code_puppy/mcp_/managed_server.py +65 -38
- code_puppy/mcp_/manager.py +146 -52
- code_puppy/mcp_/mcp_logs.py +224 -0
- code_puppy/mcp_/registry.py +6 -6
- code_puppy/mcp_/server_registry_catalog.py +24 -5
- code_puppy/messaging/__init__.py +199 -2
- code_puppy/messaging/bus.py +610 -0
- code_puppy/messaging/commands.py +167 -0
- code_puppy/messaging/markdown_patches.py +57 -0
- code_puppy/messaging/message_queue.py +17 -48
- code_puppy/messaging/messages.py +500 -0
- code_puppy/messaging/queue_console.py +1 -24
- code_puppy/messaging/renderers.py +43 -146
- code_puppy/messaging/rich_renderer.py +1027 -0
- code_puppy/messaging/spinner/__init__.py +21 -5
- code_puppy/messaging/spinner/console_spinner.py +86 -51
- code_puppy/messaging/subagent_console.py +461 -0
- code_puppy/model_factory.py +634 -83
- code_puppy/model_utils.py +167 -0
- code_puppy/models.json +66 -68
- code_puppy/models_dev_api.json +1 -0
- code_puppy/models_dev_parser.py +592 -0
- code_puppy/plugins/__init__.py +164 -10
- code_puppy/plugins/antigravity_oauth/__init__.py +10 -0
- code_puppy/plugins/antigravity_oauth/accounts.py +406 -0
- code_puppy/plugins/antigravity_oauth/antigravity_model.py +704 -0
- code_puppy/plugins/antigravity_oauth/config.py +42 -0
- code_puppy/plugins/antigravity_oauth/constants.py +136 -0
- code_puppy/plugins/antigravity_oauth/oauth.py +478 -0
- code_puppy/plugins/antigravity_oauth/register_callbacks.py +406 -0
- code_puppy/plugins/antigravity_oauth/storage.py +271 -0
- code_puppy/plugins/antigravity_oauth/test_plugin.py +319 -0
- code_puppy/plugins/antigravity_oauth/token.py +167 -0
- code_puppy/plugins/antigravity_oauth/transport.py +767 -0
- code_puppy/plugins/antigravity_oauth/utils.py +169 -0
- code_puppy/plugins/chatgpt_oauth/__init__.py +8 -0
- code_puppy/plugins/chatgpt_oauth/config.py +52 -0
- code_puppy/plugins/chatgpt_oauth/oauth_flow.py +328 -0
- code_puppy/plugins/chatgpt_oauth/register_callbacks.py +94 -0
- code_puppy/plugins/chatgpt_oauth/test_plugin.py +293 -0
- code_puppy/plugins/chatgpt_oauth/utils.py +489 -0
- code_puppy/plugins/claude_code_oauth/README.md +167 -0
- code_puppy/plugins/claude_code_oauth/SETUP.md +93 -0
- code_puppy/plugins/claude_code_oauth/__init__.py +6 -0
- code_puppy/plugins/claude_code_oauth/config.py +50 -0
- code_puppy/plugins/claude_code_oauth/register_callbacks.py +308 -0
- code_puppy/plugins/claude_code_oauth/test_plugin.py +283 -0
- code_puppy/plugins/claude_code_oauth/utils.py +518 -0
- code_puppy/plugins/customizable_commands/__init__.py +0 -0
- code_puppy/plugins/customizable_commands/register_callbacks.py +169 -0
- code_puppy/plugins/example_custom_command/README.md +280 -0
- code_puppy/plugins/example_custom_command/register_callbacks.py +2 -2
- code_puppy/plugins/file_permission_handler/__init__.py +4 -0
- code_puppy/plugins/file_permission_handler/register_callbacks.py +523 -0
- code_puppy/plugins/frontend_emitter/__init__.py +25 -0
- code_puppy/plugins/frontend_emitter/emitter.py +121 -0
- code_puppy/plugins/frontend_emitter/register_callbacks.py +261 -0
- code_puppy/plugins/oauth_puppy_html.py +228 -0
- code_puppy/plugins/shell_safety/__init__.py +6 -0
- code_puppy/plugins/shell_safety/agent_shell_safety.py +69 -0
- code_puppy/plugins/shell_safety/command_cache.py +156 -0
- code_puppy/plugins/shell_safety/register_callbacks.py +202 -0
- code_puppy/prompts/antigravity_system_prompt.md +1 -0
- code_puppy/prompts/codex_system_prompt.md +310 -0
- code_puppy/pydantic_patches.py +131 -0
- code_puppy/reopenable_async_client.py +8 -8
- code_puppy/round_robin_model.py +9 -12
- code_puppy/session_storage.py +2 -1
- code_puppy/status_display.py +21 -4
- code_puppy/summarization_agent.py +41 -13
- code_puppy/terminal_utils.py +418 -0
- code_puppy/tools/__init__.py +37 -1
- code_puppy/tools/agent_tools.py +536 -52
- code_puppy/tools/browser/__init__.py +37 -0
- code_puppy/tools/browser/browser_control.py +19 -23
- code_puppy/tools/browser/browser_interactions.py +41 -48
- code_puppy/tools/browser/browser_locators.py +36 -38
- code_puppy/tools/browser/browser_manager.py +316 -0
- code_puppy/tools/browser/browser_navigation.py +16 -16
- code_puppy/tools/browser/browser_screenshot.py +79 -143
- code_puppy/tools/browser/browser_scripts.py +32 -42
- code_puppy/tools/browser/browser_workflows.py +44 -27
- code_puppy/tools/browser/chromium_terminal_manager.py +259 -0
- code_puppy/tools/browser/terminal_command_tools.py +521 -0
- code_puppy/tools/browser/terminal_screenshot_tools.py +556 -0
- code_puppy/tools/browser/terminal_tools.py +525 -0
- code_puppy/tools/command_runner.py +930 -147
- code_puppy/tools/common.py +1113 -5
- code_puppy/tools/display.py +84 -0
- code_puppy/tools/file_modifications.py +288 -89
- code_puppy/tools/file_operations.py +226 -154
- code_puppy/tools/subagent_context.py +158 -0
- code_puppy/uvx_detection.py +242 -0
- code_puppy/version_checker.py +30 -11
- code_puppy-0.0.366.data/data/code_puppy/models.json +110 -0
- code_puppy-0.0.366.data/data/code_puppy/models_dev_api.json +1 -0
- {code_puppy-0.0.214.dist-info → code_puppy-0.0.366.dist-info}/METADATA +149 -75
- code_puppy-0.0.366.dist-info/RECORD +217 -0
- {code_puppy-0.0.214.dist-info → code_puppy-0.0.366.dist-info}/WHEEL +1 -1
- code_puppy/command_line/mcp/add_command.py +0 -183
- code_puppy/messaging/spinner/textual_spinner.py +0 -106
- code_puppy/tools/browser/camoufox_manager.py +0 -216
- code_puppy/tools/browser/vqa_agent.py +0 -70
- code_puppy/tui/__init__.py +0 -10
- code_puppy/tui/app.py +0 -1105
- code_puppy/tui/components/__init__.py +0 -21
- code_puppy/tui/components/chat_view.py +0 -551
- code_puppy/tui/components/command_history_modal.py +0 -218
- code_puppy/tui/components/copy_button.py +0 -139
- code_puppy/tui/components/custom_widgets.py +0 -63
- code_puppy/tui/components/human_input_modal.py +0 -175
- code_puppy/tui/components/input_area.py +0 -167
- code_puppy/tui/components/sidebar.py +0 -309
- code_puppy/tui/components/status_bar.py +0 -185
- code_puppy/tui/messages.py +0 -27
- code_puppy/tui/models/__init__.py +0 -8
- code_puppy/tui/models/chat_message.py +0 -25
- code_puppy/tui/models/command_history.py +0 -89
- code_puppy/tui/models/enums.py +0 -24
- code_puppy/tui/screens/__init__.py +0 -17
- code_puppy/tui/screens/autosave_picker.py +0 -175
- code_puppy/tui/screens/help.py +0 -130
- code_puppy/tui/screens/mcp_install_wizard.py +0 -803
- code_puppy/tui/screens/settings.py +0 -306
- code_puppy/tui/screens/tools.py +0 -74
- code_puppy/tui_state.py +0 -55
- code_puppy-0.0.214.data/data/code_puppy/models.json +0 -112
- code_puppy-0.0.214.dist-info/RECORD +0 -131
- {code_puppy-0.0.214.dist-info → code_puppy-0.0.366.dist-info}/entry_points.txt +0 -0
- {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
|
|
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
|
-
|
|
15
|
-
workflows_dir =
|
|
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"
|
|
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 -
|
|
32
|
-
|
|
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
|
-
|
|
47
|
-
f"
|
|
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
|
-
|
|
60
|
-
f"
|
|
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
|
-
"
|
|
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
|
-
|
|
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
|
-
|
|
101
|
-
f"
|
|
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
|
-
|
|
114
|
-
f"
|
|
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"
|
|
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
|
-
|
|
139
|
-
f"
|
|
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
|
-
|
|
153
|
-
f"
|
|
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
|
-
|
|
167
|
-
f"
|
|
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]
|