codepp 0.0.437__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 +10 -0
- code_puppy/__main__.py +10 -0
- code_puppy/agents/__init__.py +31 -0
- code_puppy/agents/agent_c_reviewer.py +155 -0
- code_puppy/agents/agent_code_puppy.py +117 -0
- code_puppy/agents/agent_code_reviewer.py +90 -0
- code_puppy/agents/agent_cpp_reviewer.py +132 -0
- code_puppy/agents/agent_creator_agent.py +638 -0
- code_puppy/agents/agent_golang_reviewer.py +151 -0
- code_puppy/agents/agent_helios.py +124 -0
- code_puppy/agents/agent_javascript_reviewer.py +160 -0
- code_puppy/agents/agent_manager.py +742 -0
- code_puppy/agents/agent_pack_leader.py +385 -0
- code_puppy/agents/agent_planning.py +165 -0
- code_puppy/agents/agent_python_programmer.py +169 -0
- code_puppy/agents/agent_python_reviewer.py +90 -0
- code_puppy/agents/agent_qa_expert.py +163 -0
- code_puppy/agents/agent_qa_kitten.py +208 -0
- code_puppy/agents/agent_scheduler.py +121 -0
- code_puppy/agents/agent_security_auditor.py +181 -0
- code_puppy/agents/agent_terminal_qa.py +323 -0
- code_puppy/agents/agent_typescript_reviewer.py +166 -0
- code_puppy/agents/base_agent.py +2156 -0
- code_puppy/agents/event_stream_handler.py +348 -0
- code_puppy/agents/json_agent.py +202 -0
- code_puppy/agents/pack/__init__.py +34 -0
- code_puppy/agents/pack/bloodhound.py +304 -0
- code_puppy/agents/pack/husky.py +327 -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 +453 -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 +75 -0
- code_puppy/api/routers/sessions.py +234 -0
- code_puppy/api/templates/terminal.html +361 -0
- code_puppy/api/websocket.py +154 -0
- code_puppy/callbacks.py +692 -0
- code_puppy/chatgpt_codex_client.py +338 -0
- code_puppy/claude_cache_client.py +672 -0
- code_puppy/cli_runner.py +1073 -0
- code_puppy/command_line/__init__.py +1 -0
- code_puppy/command_line/add_model_menu.py +1092 -0
- code_puppy/command_line/agent_menu.py +662 -0
- code_puppy/command_line/attachments.py +395 -0
- code_puppy/command_line/autosave_menu.py +704 -0
- code_puppy/command_line/clipboard.py +527 -0
- code_puppy/command_line/colors_menu.py +532 -0
- code_puppy/command_line/command_handler.py +293 -0
- code_puppy/command_line/command_registry.py +150 -0
- code_puppy/command_line/config_commands.py +719 -0
- code_puppy/command_line/core_commands.py +867 -0
- code_puppy/command_line/diff_menu.py +865 -0
- code_puppy/command_line/file_path_completion.py +73 -0
- code_puppy/command_line/load_context_completion.py +52 -0
- code_puppy/command_line/mcp/__init__.py +10 -0
- code_puppy/command_line/mcp/base.py +32 -0
- 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 +138 -0
- code_puppy/command_line/mcp/help_command.py +147 -0
- code_puppy/command_line/mcp/install_command.py +214 -0
- code_puppy/command_line/mcp/install_menu.py +705 -0
- code_puppy/command_line/mcp/list_command.py +94 -0
- code_puppy/command_line/mcp/logs_command.py +235 -0
- code_puppy/command_line/mcp/remove_command.py +82 -0
- code_puppy/command_line/mcp/restart_command.py +100 -0
- code_puppy/command_line/mcp/search_command.py +123 -0
- code_puppy/command_line/mcp/start_all_command.py +135 -0
- code_puppy/command_line/mcp/start_command.py +117 -0
- code_puppy/command_line/mcp/status_command.py +184 -0
- code_puppy/command_line/mcp/stop_all_command.py +112 -0
- code_puppy/command_line/mcp/stop_command.py +80 -0
- code_puppy/command_line/mcp/test_command.py +107 -0
- code_puppy/command_line/mcp/utils.py +129 -0
- code_puppy/command_line/mcp/wizard_utils.py +334 -0
- code_puppy/command_line/mcp_completion.py +174 -0
- code_puppy/command_line/model_picker_completion.py +197 -0
- code_puppy/command_line/model_settings_menu.py +932 -0
- code_puppy/command_line/motd.py +96 -0
- code_puppy/command_line/onboarding_slides.py +179 -0
- code_puppy/command_line/onboarding_wizard.py +342 -0
- code_puppy/command_line/pin_command_completion.py +329 -0
- code_puppy/command_line/prompt_toolkit_completion.py +846 -0
- code_puppy/command_line/session_commands.py +302 -0
- code_puppy/command_line/shell_passthrough.py +145 -0
- code_puppy/command_line/skills_completion.py +160 -0
- code_puppy/command_line/uc_menu.py +893 -0
- code_puppy/command_line/utils.py +93 -0
- code_puppy/command_line/wiggum_state.py +78 -0
- code_puppy/config.py +1770 -0
- code_puppy/error_logging.py +134 -0
- code_puppy/gemini_code_assist.py +385 -0
- code_puppy/gemini_model.py +754 -0
- code_puppy/hook_engine/README.md +105 -0
- code_puppy/hook_engine/__init__.py +21 -0
- code_puppy/hook_engine/aliases.py +155 -0
- code_puppy/hook_engine/engine.py +221 -0
- code_puppy/hook_engine/executor.py +296 -0
- code_puppy/hook_engine/matcher.py +156 -0
- code_puppy/hook_engine/models.py +240 -0
- code_puppy/hook_engine/registry.py +106 -0
- code_puppy/hook_engine/validator.py +144 -0
- code_puppy/http_utils.py +361 -0
- code_puppy/keymap.py +128 -0
- code_puppy/main.py +10 -0
- code_puppy/mcp_/__init__.py +66 -0
- code_puppy/mcp_/async_lifecycle.py +286 -0
- code_puppy/mcp_/blocking_startup.py +469 -0
- code_puppy/mcp_/captured_stdio_server.py +275 -0
- code_puppy/mcp_/circuit_breaker.py +290 -0
- code_puppy/mcp_/config_wizard.py +507 -0
- code_puppy/mcp_/dashboard.py +308 -0
- code_puppy/mcp_/error_isolation.py +407 -0
- code_puppy/mcp_/examples/retry_example.py +226 -0
- code_puppy/mcp_/health_monitor.py +589 -0
- code_puppy/mcp_/managed_server.py +428 -0
- code_puppy/mcp_/manager.py +807 -0
- code_puppy/mcp_/mcp_logs.py +224 -0
- code_puppy/mcp_/registry.py +451 -0
- code_puppy/mcp_/retry_manager.py +337 -0
- code_puppy/mcp_/server_registry_catalog.py +1126 -0
- code_puppy/mcp_/status_tracker.py +355 -0
- code_puppy/mcp_/system_tools.py +209 -0
- code_puppy/mcp_prompts/__init__.py +1 -0
- code_puppy/mcp_prompts/hook_creator.py +103 -0
- code_puppy/messaging/__init__.py +255 -0
- code_puppy/messaging/bus.py +613 -0
- code_puppy/messaging/commands.py +167 -0
- code_puppy/messaging/markdown_patches.py +57 -0
- code_puppy/messaging/message_queue.py +361 -0
- code_puppy/messaging/messages.py +569 -0
- code_puppy/messaging/queue_console.py +271 -0
- code_puppy/messaging/renderers.py +311 -0
- code_puppy/messaging/rich_renderer.py +1158 -0
- code_puppy/messaging/spinner/__init__.py +83 -0
- code_puppy/messaging/spinner/console_spinner.py +240 -0
- code_puppy/messaging/spinner/spinner_base.py +95 -0
- code_puppy/messaging/subagent_console.py +460 -0
- code_puppy/model_factory.py +848 -0
- code_puppy/model_switching.py +63 -0
- code_puppy/model_utils.py +168 -0
- code_puppy/models.json +174 -0
- code_puppy/models_dev_api.json +1 -0
- code_puppy/models_dev_parser.py +592 -0
- code_puppy/plugins/__init__.py +186 -0
- code_puppy/plugins/agent_skills/__init__.py +22 -0
- code_puppy/plugins/agent_skills/config.py +175 -0
- code_puppy/plugins/agent_skills/discovery.py +136 -0
- code_puppy/plugins/agent_skills/downloader.py +392 -0
- code_puppy/plugins/agent_skills/installer.py +22 -0
- code_puppy/plugins/agent_skills/metadata.py +219 -0
- code_puppy/plugins/agent_skills/prompt_builder.py +60 -0
- code_puppy/plugins/agent_skills/register_callbacks.py +241 -0
- code_puppy/plugins/agent_skills/remote_catalog.py +322 -0
- code_puppy/plugins/agent_skills/skill_catalog.py +257 -0
- code_puppy/plugins/agent_skills/skills_install_menu.py +664 -0
- code_puppy/plugins/agent_skills/skills_menu.py +781 -0
- 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 +706 -0
- code_puppy/plugins/antigravity_oauth/config.py +42 -0
- code_puppy/plugins/antigravity_oauth/constants.py +133 -0
- code_puppy/plugins/antigravity_oauth/oauth.py +478 -0
- code_puppy/plugins/antigravity_oauth/register_callbacks.py +518 -0
- code_puppy/plugins/antigravity_oauth/storage.py +288 -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 +863 -0
- code_puppy/plugins/antigravity_oauth/utils.py +168 -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 +329 -0
- code_puppy/plugins/chatgpt_oauth/register_callbacks.py +176 -0
- code_puppy/plugins/chatgpt_oauth/test_plugin.py +301 -0
- code_puppy/plugins/chatgpt_oauth/utils.py +523 -0
- code_puppy/plugins/claude_code_hooks/__init__.py +1 -0
- code_puppy/plugins/claude_code_hooks/config.py +137 -0
- code_puppy/plugins/claude_code_hooks/register_callbacks.py +175 -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 +25 -0
- code_puppy/plugins/claude_code_oauth/config.py +52 -0
- code_puppy/plugins/claude_code_oauth/register_callbacks.py +453 -0
- code_puppy/plugins/claude_code_oauth/test_plugin.py +283 -0
- code_puppy/plugins/claude_code_oauth/token_refresh_heartbeat.py +241 -0
- code_puppy/plugins/claude_code_oauth/utils.py +640 -0
- code_puppy/plugins/customizable_commands/__init__.py +0 -0
- code_puppy/plugins/customizable_commands/register_callbacks.py +152 -0
- code_puppy/plugins/example_custom_command/README.md +280 -0
- code_puppy/plugins/example_custom_command/register_callbacks.py +51 -0
- code_puppy/plugins/file_permission_handler/__init__.py +4 -0
- code_puppy/plugins/file_permission_handler/register_callbacks.py +470 -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/hook_creator/__init__.py +1 -0
- code_puppy/plugins/hook_creator/register_callbacks.py +33 -0
- code_puppy/plugins/hook_manager/__init__.py +1 -0
- code_puppy/plugins/hook_manager/config.py +290 -0
- code_puppy/plugins/hook_manager/hooks_menu.py +564 -0
- code_puppy/plugins/hook_manager/register_callbacks.py +227 -0
- code_puppy/plugins/oauth_puppy_html.py +228 -0
- code_puppy/plugins/scheduler/__init__.py +1 -0
- code_puppy/plugins/scheduler/register_callbacks.py +88 -0
- code_puppy/plugins/scheduler/scheduler_menu.py +522 -0
- code_puppy/plugins/scheduler/scheduler_wizard.py +341 -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/plugins/synthetic_status/__init__.py +1 -0
- code_puppy/plugins/synthetic_status/register_callbacks.py +132 -0
- code_puppy/plugins/synthetic_status/status_api.py +147 -0
- code_puppy/plugins/universal_constructor/__init__.py +13 -0
- code_puppy/plugins/universal_constructor/models.py +138 -0
- code_puppy/plugins/universal_constructor/register_callbacks.py +47 -0
- code_puppy/plugins/universal_constructor/registry.py +302 -0
- code_puppy/plugins/universal_constructor/sandbox.py +584 -0
- code_puppy/prompts/antigravity_system_prompt.md +1 -0
- code_puppy/pydantic_patches.py +356 -0
- code_puppy/reopenable_async_client.py +232 -0
- code_puppy/round_robin_model.py +150 -0
- code_puppy/scheduler/__init__.py +41 -0
- code_puppy/scheduler/__main__.py +9 -0
- code_puppy/scheduler/cli.py +118 -0
- code_puppy/scheduler/config.py +126 -0
- code_puppy/scheduler/daemon.py +280 -0
- code_puppy/scheduler/executor.py +155 -0
- code_puppy/scheduler/platform.py +19 -0
- code_puppy/scheduler/platform_unix.py +22 -0
- code_puppy/scheduler/platform_win.py +32 -0
- code_puppy/session_storage.py +338 -0
- code_puppy/status_display.py +257 -0
- code_puppy/summarization_agent.py +176 -0
- code_puppy/terminal_utils.py +418 -0
- code_puppy/tools/__init__.py +501 -0
- code_puppy/tools/agent_tools.py +603 -0
- code_puppy/tools/ask_user_question/__init__.py +26 -0
- code_puppy/tools/ask_user_question/constants.py +73 -0
- code_puppy/tools/ask_user_question/demo_tui.py +55 -0
- code_puppy/tools/ask_user_question/handler.py +232 -0
- code_puppy/tools/ask_user_question/models.py +304 -0
- code_puppy/tools/ask_user_question/registration.py +26 -0
- code_puppy/tools/ask_user_question/renderers.py +309 -0
- code_puppy/tools/ask_user_question/terminal_ui.py +329 -0
- code_puppy/tools/ask_user_question/theme.py +155 -0
- code_puppy/tools/ask_user_question/tui_loop.py +423 -0
- code_puppy/tools/browser/__init__.py +37 -0
- code_puppy/tools/browser/browser_control.py +289 -0
- code_puppy/tools/browser/browser_interactions.py +545 -0
- code_puppy/tools/browser/browser_locators.py +640 -0
- code_puppy/tools/browser/browser_manager.py +378 -0
- code_puppy/tools/browser/browser_navigation.py +251 -0
- code_puppy/tools/browser/browser_screenshot.py +179 -0
- code_puppy/tools/browser/browser_scripts.py +462 -0
- code_puppy/tools/browser/browser_workflows.py +221 -0
- code_puppy/tools/browser/chromium_terminal_manager.py +259 -0
- code_puppy/tools/browser/terminal_command_tools.py +534 -0
- code_puppy/tools/browser/terminal_screenshot_tools.py +552 -0
- code_puppy/tools/browser/terminal_tools.py +525 -0
- code_puppy/tools/command_runner.py +1346 -0
- code_puppy/tools/common.py +1409 -0
- code_puppy/tools/display.py +84 -0
- code_puppy/tools/file_modifications.py +886 -0
- code_puppy/tools/file_operations.py +802 -0
- code_puppy/tools/scheduler_tools.py +412 -0
- code_puppy/tools/skills_tools.py +244 -0
- code_puppy/tools/subagent_context.py +158 -0
- code_puppy/tools/tools_content.py +51 -0
- code_puppy/tools/universal_constructor.py +889 -0
- code_puppy/uvx_detection.py +242 -0
- code_puppy/version_checker.py +82 -0
- codepp-0.0.437.dist-info/METADATA +766 -0
- codepp-0.0.437.dist-info/RECORD +288 -0
- codepp-0.0.437.dist-info/WHEEL +4 -0
- codepp-0.0.437.dist-info/entry_points.txt +3 -0
- codepp-0.0.437.dist-info/licenses/LICENSE +21 -0
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
"""Theme configuration for ask_user_question TUI.
|
|
2
|
+
|
|
3
|
+
This module provides theming support that integrates with code-puppy's
|
|
4
|
+
color configuration system. It allows the TUI to inherit colors from
|
|
5
|
+
the global configuration.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
from typing import TYPE_CHECKING, Mapping, NamedTuple, TypeVar
|
|
11
|
+
|
|
12
|
+
if TYPE_CHECKING:
|
|
13
|
+
from collections.abc import Callable
|
|
14
|
+
|
|
15
|
+
__all__ = ["TUIColors", "RichColors", "get_tui_colors", "get_rich_colors"]
|
|
16
|
+
|
|
17
|
+
# Cached config getter to avoid repeated imports
|
|
18
|
+
_config_getter: "Callable[[str], str | None] | None" = None
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def _get_config_value(key: str) -> str | None:
|
|
22
|
+
"""Safely get a config value, caching the import for performance."""
|
|
23
|
+
global _config_getter
|
|
24
|
+
if _config_getter is None:
|
|
25
|
+
try:
|
|
26
|
+
from code_puppy.config import get_value
|
|
27
|
+
|
|
28
|
+
_config_getter = get_value
|
|
29
|
+
except ImportError:
|
|
30
|
+
_config_getter = lambda _: None # noqa: E731
|
|
31
|
+
return _config_getter(key)
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
_T = TypeVar("_T", bound=NamedTuple)
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def _apply_config_overrides(default: _T, config_map: Mapping[str, str]) -> _T:
|
|
38
|
+
"""Apply config overrides to a color scheme.
|
|
39
|
+
|
|
40
|
+
Args:
|
|
41
|
+
default: Default NamedTuple instance
|
|
42
|
+
config_map: Mapping of field names to config keys
|
|
43
|
+
|
|
44
|
+
Returns:
|
|
45
|
+
New NamedTuple with overrides applied
|
|
46
|
+
"""
|
|
47
|
+
overrides = {}
|
|
48
|
+
for field, config_key in config_map.items():
|
|
49
|
+
value = _get_config_value(config_key)
|
|
50
|
+
if value:
|
|
51
|
+
overrides[field] = value
|
|
52
|
+
return default._replace(**overrides) if overrides else default
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
class TUIColors(NamedTuple):
|
|
56
|
+
"""Color scheme for the ask_user_question TUI."""
|
|
57
|
+
|
|
58
|
+
# Header and title colors
|
|
59
|
+
header_bold: str = "bold cyan"
|
|
60
|
+
header_dim: str = "fg:ansicyan dim"
|
|
61
|
+
|
|
62
|
+
# Cursor and selection colors
|
|
63
|
+
cursor_active: str = "fg:ansigreen bold"
|
|
64
|
+
cursor_inactive: str = "fg:ansiwhite"
|
|
65
|
+
selected: str = "fg:ansicyan"
|
|
66
|
+
selected_check: str = "fg:ansigreen"
|
|
67
|
+
|
|
68
|
+
# Text colors
|
|
69
|
+
text_normal: str = ""
|
|
70
|
+
text_dim: str = "fg:ansiwhite dim"
|
|
71
|
+
text_warning: str = "fg:ansiyellow bold"
|
|
72
|
+
|
|
73
|
+
# Help text colors
|
|
74
|
+
help_key: str = "fg:ansigreen"
|
|
75
|
+
help_text: str = "fg:ansiwhite dim"
|
|
76
|
+
|
|
77
|
+
# Error colors
|
|
78
|
+
error: str = "fg:ansired"
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
# Create defaults after class definitions
|
|
82
|
+
_DEFAULT_TUI = TUIColors()
|
|
83
|
+
|
|
84
|
+
# Mapping of configurable TUI color fields to config keys
|
|
85
|
+
_TUI_CONFIG_MAP: dict[str, str] = {
|
|
86
|
+
"header_bold": "tui_header_color",
|
|
87
|
+
"cursor_active": "tui_cursor_color",
|
|
88
|
+
"selected": "tui_selected_color",
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
def get_tui_colors() -> TUIColors:
|
|
93
|
+
"""Get the current TUI color scheme.
|
|
94
|
+
|
|
95
|
+
Loads colors from code-puppy's configuration system for custom theming.
|
|
96
|
+
Falls back to defaults for any missing config values.
|
|
97
|
+
|
|
98
|
+
Returns:
|
|
99
|
+
TUIColors instance with the current theme.
|
|
100
|
+
"""
|
|
101
|
+
return _apply_config_overrides(_DEFAULT_TUI, _TUI_CONFIG_MAP)
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
# Rich console color mappings for the right panel
|
|
105
|
+
class RichColors(NamedTuple):
|
|
106
|
+
"""Rich markup colors for the question panel."""
|
|
107
|
+
|
|
108
|
+
# Header colors (Rich markup format)
|
|
109
|
+
header: str = "bold cyan"
|
|
110
|
+
progress: str = "dim"
|
|
111
|
+
|
|
112
|
+
# Question text
|
|
113
|
+
question: str = "bold"
|
|
114
|
+
question_hint: str = "dim"
|
|
115
|
+
|
|
116
|
+
# Option colors
|
|
117
|
+
cursor: str = "green bold"
|
|
118
|
+
selected: str = "cyan"
|
|
119
|
+
normal: str = ""
|
|
120
|
+
description: str = "dim"
|
|
121
|
+
|
|
122
|
+
# Input field
|
|
123
|
+
input_label: str = "bold yellow"
|
|
124
|
+
input_text: str = "green"
|
|
125
|
+
input_hint: str = "dim"
|
|
126
|
+
|
|
127
|
+
# Help overlay
|
|
128
|
+
help_border: str = "bold cyan"
|
|
129
|
+
help_title: str = "bold cyan"
|
|
130
|
+
help_section: str = "bold"
|
|
131
|
+
help_key: str = "green"
|
|
132
|
+
help_close: str = "dim"
|
|
133
|
+
|
|
134
|
+
# Timeout warning
|
|
135
|
+
timeout_warning: str = "bold yellow"
|
|
136
|
+
|
|
137
|
+
|
|
138
|
+
_DEFAULT_RICH = RichColors()
|
|
139
|
+
|
|
140
|
+
# Mapping of configurable Rich color fields to config keys
|
|
141
|
+
_RICH_CONFIG_MAP: dict[str, str] = {
|
|
142
|
+
"header": "tui_rich_header_color",
|
|
143
|
+
"cursor": "tui_rich_cursor_color",
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
|
|
147
|
+
def get_rich_colors() -> RichColors:
|
|
148
|
+
"""Get Rich console colors for the question panel.
|
|
149
|
+
|
|
150
|
+
Falls back to defaults for any missing config values.
|
|
151
|
+
|
|
152
|
+
Returns:
|
|
153
|
+
RichColors instance with current theme.
|
|
154
|
+
"""
|
|
155
|
+
return _apply_config_overrides(_DEFAULT_RICH, _RICH_CONFIG_MAP)
|
|
@@ -0,0 +1,423 @@
|
|
|
1
|
+
"""TUI loop and keyboard handlers for ask_user_question.
|
|
2
|
+
|
|
3
|
+
This module contains the main TUI application loop and all keyboard bindings.
|
|
4
|
+
Separated from terminal_ui.py to keep files under 600 lines.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
import asyncio
|
|
10
|
+
import shutil
|
|
11
|
+
import sys
|
|
12
|
+
from dataclasses import dataclass
|
|
13
|
+
from typing import TYPE_CHECKING, Callable
|
|
14
|
+
|
|
15
|
+
from prompt_toolkit import Application
|
|
16
|
+
from prompt_toolkit.application import run_in_terminal
|
|
17
|
+
from prompt_toolkit.formatted_text import ANSI, FormattedText
|
|
18
|
+
from prompt_toolkit.key_binding import KeyBindings, KeyPressEvent
|
|
19
|
+
from prompt_toolkit.layout import Layout, VSplit, Window
|
|
20
|
+
from prompt_toolkit.layout.controls import FormattedTextControl
|
|
21
|
+
from prompt_toolkit.layout.dimension import Dimension
|
|
22
|
+
from prompt_toolkit.output import create_output
|
|
23
|
+
from prompt_toolkit.output.color_depth import ColorDepth
|
|
24
|
+
from prompt_toolkit.widgets import Frame
|
|
25
|
+
|
|
26
|
+
from .constants import (
|
|
27
|
+
ARROW_DOWN,
|
|
28
|
+
ARROW_LEFT,
|
|
29
|
+
ARROW_RIGHT,
|
|
30
|
+
ARROW_UP,
|
|
31
|
+
CHECK_MARK,
|
|
32
|
+
CURSOR_TRIANGLE,
|
|
33
|
+
)
|
|
34
|
+
from .renderers import render_question_panel
|
|
35
|
+
from .theme import get_rich_colors, get_tui_colors
|
|
36
|
+
|
|
37
|
+
if TYPE_CHECKING:
|
|
38
|
+
from .models import QuestionAnswer
|
|
39
|
+
from .terminal_ui import QuestionUIState
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def _wait_for_keypress() -> None:
|
|
43
|
+
"""Block until any key is pressed, reading directly from the terminal.
|
|
44
|
+
|
|
45
|
+
On Unix: switches to raw mode so a single keypress returns immediately.
|
|
46
|
+
On Windows: uses msvcrt.getch() which already reads a single key.
|
|
47
|
+
Called inside run_in_terminal's cooked-mode context.
|
|
48
|
+
"""
|
|
49
|
+
try:
|
|
50
|
+
# Windows
|
|
51
|
+
import msvcrt
|
|
52
|
+
|
|
53
|
+
msvcrt.getch()
|
|
54
|
+
except ImportError:
|
|
55
|
+
# Unix / macOS
|
|
56
|
+
import select
|
|
57
|
+
import termios
|
|
58
|
+
import tty
|
|
59
|
+
|
|
60
|
+
fd = sys.__stdin__.fileno()
|
|
61
|
+
old_settings = termios.tcgetattr(fd)
|
|
62
|
+
try:
|
|
63
|
+
tty.setraw(fd)
|
|
64
|
+
ch = sys.__stdin__.read(1)
|
|
65
|
+
# Arrow/F-keys send multi-byte escape sequences (e.g. \x1b[A).
|
|
66
|
+
# Drain trailing bytes so they don't leak into prompt_toolkit.
|
|
67
|
+
if ch == "\x1b":
|
|
68
|
+
while select.select([sys.__stdin__], [], [], 0.01)[0]:
|
|
69
|
+
sys.__stdin__.read(1)
|
|
70
|
+
finally:
|
|
71
|
+
termios.tcsetattr(fd, termios.TCSADRAIN, old_settings)
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
@dataclass(slots=True)
|
|
75
|
+
class TUIResult:
|
|
76
|
+
"""Result holder for the TUI interaction."""
|
|
77
|
+
|
|
78
|
+
cancelled: bool = False
|
|
79
|
+
confirmed: bool = False
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
async def run_question_tui(
|
|
83
|
+
state: QuestionUIState,
|
|
84
|
+
) -> tuple[list[QuestionAnswer], bool, bool]:
|
|
85
|
+
"""Run the main question TUI loop.
|
|
86
|
+
|
|
87
|
+
Returns:
|
|
88
|
+
Tuple of (answers, cancelled, timed_out)
|
|
89
|
+
"""
|
|
90
|
+
result = TUIResult()
|
|
91
|
+
timed_out = False
|
|
92
|
+
kb = KeyBindings()
|
|
93
|
+
|
|
94
|
+
# --- Factory for dual-mode handlers (vim keys that type in text mode) ---
|
|
95
|
+
def make_dual_mode_handler(
|
|
96
|
+
char: str, action: Callable[[], None]
|
|
97
|
+
) -> Callable[[KeyPressEvent], None]:
|
|
98
|
+
"""Create handler that types char in text mode, calls action otherwise."""
|
|
99
|
+
|
|
100
|
+
def handler(event: KeyPressEvent) -> None:
|
|
101
|
+
state.reset_activity_timer()
|
|
102
|
+
if state.entering_other_text:
|
|
103
|
+
state.other_text_buffer += char
|
|
104
|
+
else:
|
|
105
|
+
action()
|
|
106
|
+
event.app.invalidate()
|
|
107
|
+
|
|
108
|
+
return handler
|
|
109
|
+
|
|
110
|
+
# --- Factory for arrow key navigation (don't type in text mode) ---
|
|
111
|
+
def make_arrow_handler(
|
|
112
|
+
action: Callable[[], None],
|
|
113
|
+
) -> Callable[[KeyPressEvent], None]:
|
|
114
|
+
"""Create handler that only fires when not in text input mode."""
|
|
115
|
+
|
|
116
|
+
def handler(event: KeyPressEvent) -> None:
|
|
117
|
+
state.reset_activity_timer()
|
|
118
|
+
if not state.entering_other_text:
|
|
119
|
+
action()
|
|
120
|
+
event.app.invalidate()
|
|
121
|
+
|
|
122
|
+
return handler
|
|
123
|
+
|
|
124
|
+
kb.add("up")(make_arrow_handler(state.move_cursor_up))
|
|
125
|
+
kb.add("down")(make_arrow_handler(state.move_cursor_down))
|
|
126
|
+
kb.add("left")(make_arrow_handler(state.prev_question))
|
|
127
|
+
kb.add("right")(make_arrow_handler(state.next_question))
|
|
128
|
+
|
|
129
|
+
# --- Vim-style navigation (types letter in text mode) ---
|
|
130
|
+
kb.add("k")(make_dual_mode_handler("k", state.move_cursor_up))
|
|
131
|
+
kb.add("j")(make_dual_mode_handler("j", state.move_cursor_down))
|
|
132
|
+
kb.add("h")(make_dual_mode_handler("h", state.prev_question))
|
|
133
|
+
kb.add("l")(make_dual_mode_handler("l", state.next_question))
|
|
134
|
+
kb.add("g")(make_dual_mode_handler("g", state.jump_to_first))
|
|
135
|
+
kb.add("G")(make_dual_mode_handler("G", state.jump_to_last))
|
|
136
|
+
|
|
137
|
+
# --- Selection controls (also dual-mode) ---
|
|
138
|
+
def _toggle_help() -> None:
|
|
139
|
+
state.show_help = not state.show_help
|
|
140
|
+
|
|
141
|
+
kb.add("a")(make_dual_mode_handler("a", state.select_all_options))
|
|
142
|
+
kb.add("n")(make_dual_mode_handler("n", state.select_no_options))
|
|
143
|
+
kb.add("?")(make_dual_mode_handler("?", _toggle_help))
|
|
144
|
+
|
|
145
|
+
@kb.add("space")
|
|
146
|
+
def toggle_option(event: KeyPressEvent) -> None:
|
|
147
|
+
"""Toggle/select the current option.
|
|
148
|
+
|
|
149
|
+
For multi-select: toggles the checkbox
|
|
150
|
+
For single-select: selects the radio button (without advancing)
|
|
151
|
+
"""
|
|
152
|
+
state.reset_activity_timer()
|
|
153
|
+
if state.entering_other_text:
|
|
154
|
+
state.other_text_buffer += " "
|
|
155
|
+
event.app.invalidate()
|
|
156
|
+
return
|
|
157
|
+
|
|
158
|
+
# Check if current option is "Other"
|
|
159
|
+
if state.is_other_option(state.current_cursor):
|
|
160
|
+
state.enter_other_text_mode()
|
|
161
|
+
event.app.invalidate()
|
|
162
|
+
return
|
|
163
|
+
|
|
164
|
+
if state.current_question.multi_select:
|
|
165
|
+
# Toggle checkbox
|
|
166
|
+
state.toggle_current_option()
|
|
167
|
+
else:
|
|
168
|
+
# Select radio button (doesn't advance)
|
|
169
|
+
state.select_current_option()
|
|
170
|
+
event.app.invalidate()
|
|
171
|
+
|
|
172
|
+
@kb.add("enter")
|
|
173
|
+
def advance_question(event: KeyPressEvent) -> None:
|
|
174
|
+
"""Select current option and advance, or submit if confirming selection.
|
|
175
|
+
|
|
176
|
+
Behavior:
|
|
177
|
+
- Selects the current option (for single-select) or enters Other mode
|
|
178
|
+
- Advances to next question if not on last
|
|
179
|
+
- On last question: only submits if cursor is on an already-selected option
|
|
180
|
+
(i.e., user is confirming their choice by pressing Enter on it again)
|
|
181
|
+
"""
|
|
182
|
+
state.reset_activity_timer()
|
|
183
|
+
if state.entering_other_text:
|
|
184
|
+
# Confirm the "Other" text using centralized method
|
|
185
|
+
state.commit_other_text()
|
|
186
|
+
event.app.invalidate()
|
|
187
|
+
return
|
|
188
|
+
|
|
189
|
+
# Check if current option is "Other"
|
|
190
|
+
if state.is_other_option(state.current_cursor):
|
|
191
|
+
state.enter_other_text_mode()
|
|
192
|
+
event.app.invalidate()
|
|
193
|
+
return
|
|
194
|
+
|
|
195
|
+
is_last_question = state.current_question_index == len(state.questions) - 1
|
|
196
|
+
cursor_is_on_selected = state.is_option_selected(state.current_cursor)
|
|
197
|
+
|
|
198
|
+
# For single-select, select the current option when pressing Enter
|
|
199
|
+
if not state.current_question.multi_select:
|
|
200
|
+
state.select_current_option()
|
|
201
|
+
|
|
202
|
+
# Advance to next question if not on the last one
|
|
203
|
+
if not is_last_question:
|
|
204
|
+
state.next_question()
|
|
205
|
+
event.app.invalidate()
|
|
206
|
+
else:
|
|
207
|
+
# On the last question:
|
|
208
|
+
# Only submit if cursor was already on the selected option (confirming)
|
|
209
|
+
# This prevents accidental submission when browsing options
|
|
210
|
+
if cursor_is_on_selected:
|
|
211
|
+
result.confirmed = True
|
|
212
|
+
event.app.exit()
|
|
213
|
+
else:
|
|
214
|
+
# Just selected a new option, update display but don't submit
|
|
215
|
+
# User needs to press Enter again to confirm
|
|
216
|
+
event.app.invalidate()
|
|
217
|
+
|
|
218
|
+
@kb.add("c-s")
|
|
219
|
+
def submit_all(event: KeyPressEvent) -> None:
|
|
220
|
+
"""Ctrl+S submits all answers immediately from any question."""
|
|
221
|
+
state.reset_activity_timer()
|
|
222
|
+
# If entering other text, save it first before submitting
|
|
223
|
+
if state.entering_other_text:
|
|
224
|
+
state.commit_other_text()
|
|
225
|
+
result.confirmed = True
|
|
226
|
+
event.app.exit()
|
|
227
|
+
|
|
228
|
+
@kb.add("escape")
|
|
229
|
+
def cancel(event: KeyPressEvent) -> None:
|
|
230
|
+
state.reset_activity_timer()
|
|
231
|
+
if state.entering_other_text:
|
|
232
|
+
state.entering_other_text = False
|
|
233
|
+
state.other_text_buffer = ""
|
|
234
|
+
event.app.invalidate()
|
|
235
|
+
return
|
|
236
|
+
result.cancelled = True
|
|
237
|
+
event.app.exit()
|
|
238
|
+
|
|
239
|
+
@kb.add("c-c")
|
|
240
|
+
def ctrl_c_cancel(event: KeyPressEvent) -> None:
|
|
241
|
+
result.cancelled = True
|
|
242
|
+
event.app.exit()
|
|
243
|
+
|
|
244
|
+
@kb.add("tab")
|
|
245
|
+
def toggle_peek(event: KeyPressEvent) -> None:
|
|
246
|
+
"""Peek behind the TUI to see terminal output.
|
|
247
|
+
|
|
248
|
+
Uses prompt_toolkit's run_in_terminal to properly suspend rendering,
|
|
249
|
+
exit alt screen, wait for a keypress, then restore everything with
|
|
250
|
+
a full repaint. This prevents resize events from clobbering the
|
|
251
|
+
main screen during peek and ensures borders render correctly on return.
|
|
252
|
+
"""
|
|
253
|
+
state.reset_activity_timer()
|
|
254
|
+
|
|
255
|
+
def _peek() -> None:
|
|
256
|
+
sys.__stdout__.write(
|
|
257
|
+
"\n \033[2mPress any key to return to questions...\033[0m\n"
|
|
258
|
+
)
|
|
259
|
+
sys.__stdout__.flush()
|
|
260
|
+
_wait_for_keypress()
|
|
261
|
+
state.reset_activity_timer()
|
|
262
|
+
|
|
263
|
+
run_in_terminal(_peek, in_executor=True)
|
|
264
|
+
|
|
265
|
+
@kb.add("<any>")
|
|
266
|
+
def handle_text_input(event: KeyPressEvent) -> None:
|
|
267
|
+
state.reset_activity_timer()
|
|
268
|
+
if state.entering_other_text:
|
|
269
|
+
char = event.data
|
|
270
|
+
if char and len(char) == 1 and ord(char) >= 32:
|
|
271
|
+
state.other_text_buffer += char
|
|
272
|
+
event.app.invalidate()
|
|
273
|
+
|
|
274
|
+
@kb.add("backspace")
|
|
275
|
+
def handle_backspace(event: KeyPressEvent) -> None:
|
|
276
|
+
if state.entering_other_text and state.other_text_buffer:
|
|
277
|
+
state.other_text_buffer = state.other_text_buffer[:-1]
|
|
278
|
+
event.app.invalidate()
|
|
279
|
+
|
|
280
|
+
# --- Panel rendering ---
|
|
281
|
+
# Cache colors once per session to avoid repeated config lookups
|
|
282
|
+
tui_colors = get_tui_colors()
|
|
283
|
+
rich_colors = get_rich_colors()
|
|
284
|
+
|
|
285
|
+
def get_left_panel_text() -> FormattedText:
|
|
286
|
+
"""Generate the left panel with question headers."""
|
|
287
|
+
pad = " "
|
|
288
|
+
lines: list[tuple[str, str]] = [
|
|
289
|
+
("", pad),
|
|
290
|
+
(tui_colors.header_bold, "Questions"),
|
|
291
|
+
("", "\n\n"),
|
|
292
|
+
]
|
|
293
|
+
|
|
294
|
+
for i, question in enumerate(state.questions):
|
|
295
|
+
is_current = i == state.current_question_index
|
|
296
|
+
is_answered = state.is_question_answered(i)
|
|
297
|
+
cursor = f"{CURSOR_TRIANGLE} " if is_current else " "
|
|
298
|
+
status = f"{CHECK_MARK} " if is_answered else " "
|
|
299
|
+
|
|
300
|
+
# Determine styles based on state
|
|
301
|
+
cursor_style = (
|
|
302
|
+
tui_colors.cursor_active if is_current else tui_colors.cursor_inactive
|
|
303
|
+
)
|
|
304
|
+
content_style = (
|
|
305
|
+
tui_colors.selected_check
|
|
306
|
+
if is_answered
|
|
307
|
+
else tui_colors.cursor_active
|
|
308
|
+
if is_current
|
|
309
|
+
else tui_colors.text_dim
|
|
310
|
+
)
|
|
311
|
+
|
|
312
|
+
lines.append(("", pad))
|
|
313
|
+
if is_answered:
|
|
314
|
+
# Answered: cursor and status+header use different styles
|
|
315
|
+
lines.append((cursor_style, cursor))
|
|
316
|
+
lines.append((content_style, status + question.header))
|
|
317
|
+
else:
|
|
318
|
+
# Not answered: cursor+status+header all use same style
|
|
319
|
+
lines.append((content_style, cursor + status + question.header))
|
|
320
|
+
lines.append(("", "\n"))
|
|
321
|
+
|
|
322
|
+
# Footer with keyboard shortcuts
|
|
323
|
+
lines.extend(
|
|
324
|
+
[
|
|
325
|
+
("", "\n"),
|
|
326
|
+
("", pad),
|
|
327
|
+
(tui_colors.header_dim, f"{ARROW_LEFT}{ARROW_RIGHT} Switch question"),
|
|
328
|
+
("", "\n"),
|
|
329
|
+
("", pad),
|
|
330
|
+
(tui_colors.header_dim, f"{ARROW_UP}{ARROW_DOWN} Navigate options"),
|
|
331
|
+
("", "\n"),
|
|
332
|
+
("", "\n"),
|
|
333
|
+
("", pad),
|
|
334
|
+
(tui_colors.help_key, "Ctrl+S"),
|
|
335
|
+
(tui_colors.header_dim, " Submit"),
|
|
336
|
+
("", "\n"),
|
|
337
|
+
("", pad),
|
|
338
|
+
(tui_colors.help_key, "Tab"),
|
|
339
|
+
(tui_colors.header_dim, " Peek behind"),
|
|
340
|
+
]
|
|
341
|
+
)
|
|
342
|
+
|
|
343
|
+
return FormattedText(lines)
|
|
344
|
+
|
|
345
|
+
def get_right_panel_text() -> ANSI:
|
|
346
|
+
"""Generate the right panel with current question and options."""
|
|
347
|
+
# Calculate available width: terminal minus left panel, minus frame borders (4 chars)
|
|
348
|
+
term_width = shutil.get_terminal_size().columns
|
|
349
|
+
available = term_width - left_panel_width - 4
|
|
350
|
+
return render_question_panel(
|
|
351
|
+
state, colors=rich_colors, available_width=available
|
|
352
|
+
)
|
|
353
|
+
|
|
354
|
+
# --- Layout ---
|
|
355
|
+
# Calculate dynamic left panel width based on longest header
|
|
356
|
+
left_panel_width = state.get_left_panel_width()
|
|
357
|
+
|
|
358
|
+
left_panel = Window(
|
|
359
|
+
content=FormattedTextControl(lambda: get_left_panel_text()),
|
|
360
|
+
width=Dimension(preferred=left_panel_width, max=left_panel_width),
|
|
361
|
+
)
|
|
362
|
+
|
|
363
|
+
right_panel = Window(
|
|
364
|
+
content=FormattedTextControl(lambda: get_right_panel_text()),
|
|
365
|
+
wrap_lines=True,
|
|
366
|
+
# Right panel takes remaining space
|
|
367
|
+
)
|
|
368
|
+
|
|
369
|
+
root_container = VSplit(
|
|
370
|
+
[
|
|
371
|
+
Frame(left_panel, title=""),
|
|
372
|
+
Frame(right_panel, title=""),
|
|
373
|
+
]
|
|
374
|
+
)
|
|
375
|
+
|
|
376
|
+
layout = Layout(root_container)
|
|
377
|
+
|
|
378
|
+
# Create output that writes to the real terminal, bypassing any stdout capture
|
|
379
|
+
output = create_output(stdout=sys.__stdout__)
|
|
380
|
+
|
|
381
|
+
app = Application(
|
|
382
|
+
layout=layout,
|
|
383
|
+
key_bindings=kb,
|
|
384
|
+
full_screen=True,
|
|
385
|
+
mouse_support=False,
|
|
386
|
+
color_depth=ColorDepth.DEPTH_24_BIT,
|
|
387
|
+
output=output,
|
|
388
|
+
)
|
|
389
|
+
|
|
390
|
+
# Timeout checker background task
|
|
391
|
+
async def timeout_checker() -> None:
|
|
392
|
+
nonlocal timed_out
|
|
393
|
+
while True:
|
|
394
|
+
await asyncio.sleep(1)
|
|
395
|
+
if state.is_timed_out():
|
|
396
|
+
timed_out = True
|
|
397
|
+
app.exit()
|
|
398
|
+
return
|
|
399
|
+
app.invalidate()
|
|
400
|
+
|
|
401
|
+
timeout_task = asyncio.create_task(timeout_checker())
|
|
402
|
+
app_exception: BaseException | None = None
|
|
403
|
+
|
|
404
|
+
try:
|
|
405
|
+
await app.run_async()
|
|
406
|
+
except BaseException as e:
|
|
407
|
+
app_exception = e
|
|
408
|
+
finally:
|
|
409
|
+
timeout_task.cancel()
|
|
410
|
+
# Use asyncio.gather with return_exceptions to avoid race conditions
|
|
411
|
+
await asyncio.gather(timeout_task, return_exceptions=True)
|
|
412
|
+
|
|
413
|
+
# Re-raise any exception from app.run_async() after cleanup
|
|
414
|
+
if app_exception is not None:
|
|
415
|
+
raise app_exception
|
|
416
|
+
|
|
417
|
+
if timed_out:
|
|
418
|
+
return ([], False, True)
|
|
419
|
+
|
|
420
|
+
if result.cancelled:
|
|
421
|
+
return ([], True, False)
|
|
422
|
+
|
|
423
|
+
return (state.build_answers(), False, False)
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
"""Browser tools for terminal automation.
|
|
2
|
+
|
|
3
|
+
This module provides browser-based terminal automation tools.
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
from code_puppy.config import get_banner_color
|
|
7
|
+
|
|
8
|
+
from .browser_manager import (
|
|
9
|
+
cleanup_all_browsers,
|
|
10
|
+
get_browser_session,
|
|
11
|
+
get_session_browser_manager,
|
|
12
|
+
set_browser_session,
|
|
13
|
+
)
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def format_terminal_banner(text: str) -> str:
|
|
17
|
+
"""Format a terminal tool banner with the configured terminal_tool color.
|
|
18
|
+
|
|
19
|
+
Returns Rich markup string that can be used with Text.from_markup().
|
|
20
|
+
|
|
21
|
+
Args:
|
|
22
|
+
text: The banner text (e.g., "TERMINAL OPEN 🖥️ localhost:8765")
|
|
23
|
+
|
|
24
|
+
Returns:
|
|
25
|
+
Rich markup formatted string
|
|
26
|
+
"""
|
|
27
|
+
color = get_banner_color("terminal_tool")
|
|
28
|
+
return f"[bold white on {color}] {text} [/bold white on {color}]"
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
__all__ = [
|
|
32
|
+
"format_terminal_banner",
|
|
33
|
+
"cleanup_all_browsers",
|
|
34
|
+
"get_browser_session",
|
|
35
|
+
"get_session_browser_manager",
|
|
36
|
+
"set_browser_session",
|
|
37
|
+
]
|