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.
Files changed (288) hide show
  1. code_puppy/__init__.py +10 -0
  2. code_puppy/__main__.py +10 -0
  3. code_puppy/agents/__init__.py +31 -0
  4. code_puppy/agents/agent_c_reviewer.py +155 -0
  5. code_puppy/agents/agent_code_puppy.py +117 -0
  6. code_puppy/agents/agent_code_reviewer.py +90 -0
  7. code_puppy/agents/agent_cpp_reviewer.py +132 -0
  8. code_puppy/agents/agent_creator_agent.py +638 -0
  9. code_puppy/agents/agent_golang_reviewer.py +151 -0
  10. code_puppy/agents/agent_helios.py +124 -0
  11. code_puppy/agents/agent_javascript_reviewer.py +160 -0
  12. code_puppy/agents/agent_manager.py +742 -0
  13. code_puppy/agents/agent_pack_leader.py +385 -0
  14. code_puppy/agents/agent_planning.py +165 -0
  15. code_puppy/agents/agent_python_programmer.py +169 -0
  16. code_puppy/agents/agent_python_reviewer.py +90 -0
  17. code_puppy/agents/agent_qa_expert.py +163 -0
  18. code_puppy/agents/agent_qa_kitten.py +208 -0
  19. code_puppy/agents/agent_scheduler.py +121 -0
  20. code_puppy/agents/agent_security_auditor.py +181 -0
  21. code_puppy/agents/agent_terminal_qa.py +323 -0
  22. code_puppy/agents/agent_typescript_reviewer.py +166 -0
  23. code_puppy/agents/base_agent.py +2156 -0
  24. code_puppy/agents/event_stream_handler.py +348 -0
  25. code_puppy/agents/json_agent.py +202 -0
  26. code_puppy/agents/pack/__init__.py +34 -0
  27. code_puppy/agents/pack/bloodhound.py +304 -0
  28. code_puppy/agents/pack/husky.py +327 -0
  29. code_puppy/agents/pack/retriever.py +393 -0
  30. code_puppy/agents/pack/shepherd.py +348 -0
  31. code_puppy/agents/pack/terrier.py +287 -0
  32. code_puppy/agents/pack/watchdog.py +367 -0
  33. code_puppy/agents/prompt_reviewer.py +145 -0
  34. code_puppy/agents/subagent_stream_handler.py +276 -0
  35. code_puppy/api/__init__.py +13 -0
  36. code_puppy/api/app.py +169 -0
  37. code_puppy/api/main.py +21 -0
  38. code_puppy/api/pty_manager.py +453 -0
  39. code_puppy/api/routers/__init__.py +12 -0
  40. code_puppy/api/routers/agents.py +36 -0
  41. code_puppy/api/routers/commands.py +217 -0
  42. code_puppy/api/routers/config.py +75 -0
  43. code_puppy/api/routers/sessions.py +234 -0
  44. code_puppy/api/templates/terminal.html +361 -0
  45. code_puppy/api/websocket.py +154 -0
  46. code_puppy/callbacks.py +692 -0
  47. code_puppy/chatgpt_codex_client.py +338 -0
  48. code_puppy/claude_cache_client.py +672 -0
  49. code_puppy/cli_runner.py +1073 -0
  50. code_puppy/command_line/__init__.py +1 -0
  51. code_puppy/command_line/add_model_menu.py +1092 -0
  52. code_puppy/command_line/agent_menu.py +662 -0
  53. code_puppy/command_line/attachments.py +395 -0
  54. code_puppy/command_line/autosave_menu.py +704 -0
  55. code_puppy/command_line/clipboard.py +527 -0
  56. code_puppy/command_line/colors_menu.py +532 -0
  57. code_puppy/command_line/command_handler.py +293 -0
  58. code_puppy/command_line/command_registry.py +150 -0
  59. code_puppy/command_line/config_commands.py +719 -0
  60. code_puppy/command_line/core_commands.py +867 -0
  61. code_puppy/command_line/diff_menu.py +865 -0
  62. code_puppy/command_line/file_path_completion.py +73 -0
  63. code_puppy/command_line/load_context_completion.py +52 -0
  64. code_puppy/command_line/mcp/__init__.py +10 -0
  65. code_puppy/command_line/mcp/base.py +32 -0
  66. code_puppy/command_line/mcp/catalog_server_installer.py +175 -0
  67. code_puppy/command_line/mcp/custom_server_form.py +688 -0
  68. code_puppy/command_line/mcp/custom_server_installer.py +195 -0
  69. code_puppy/command_line/mcp/edit_command.py +148 -0
  70. code_puppy/command_line/mcp/handler.py +138 -0
  71. code_puppy/command_line/mcp/help_command.py +147 -0
  72. code_puppy/command_line/mcp/install_command.py +214 -0
  73. code_puppy/command_line/mcp/install_menu.py +705 -0
  74. code_puppy/command_line/mcp/list_command.py +94 -0
  75. code_puppy/command_line/mcp/logs_command.py +235 -0
  76. code_puppy/command_line/mcp/remove_command.py +82 -0
  77. code_puppy/command_line/mcp/restart_command.py +100 -0
  78. code_puppy/command_line/mcp/search_command.py +123 -0
  79. code_puppy/command_line/mcp/start_all_command.py +135 -0
  80. code_puppy/command_line/mcp/start_command.py +117 -0
  81. code_puppy/command_line/mcp/status_command.py +184 -0
  82. code_puppy/command_line/mcp/stop_all_command.py +112 -0
  83. code_puppy/command_line/mcp/stop_command.py +80 -0
  84. code_puppy/command_line/mcp/test_command.py +107 -0
  85. code_puppy/command_line/mcp/utils.py +129 -0
  86. code_puppy/command_line/mcp/wizard_utils.py +334 -0
  87. code_puppy/command_line/mcp_completion.py +174 -0
  88. code_puppy/command_line/model_picker_completion.py +197 -0
  89. code_puppy/command_line/model_settings_menu.py +932 -0
  90. code_puppy/command_line/motd.py +96 -0
  91. code_puppy/command_line/onboarding_slides.py +179 -0
  92. code_puppy/command_line/onboarding_wizard.py +342 -0
  93. code_puppy/command_line/pin_command_completion.py +329 -0
  94. code_puppy/command_line/prompt_toolkit_completion.py +846 -0
  95. code_puppy/command_line/session_commands.py +302 -0
  96. code_puppy/command_line/shell_passthrough.py +145 -0
  97. code_puppy/command_line/skills_completion.py +160 -0
  98. code_puppy/command_line/uc_menu.py +893 -0
  99. code_puppy/command_line/utils.py +93 -0
  100. code_puppy/command_line/wiggum_state.py +78 -0
  101. code_puppy/config.py +1770 -0
  102. code_puppy/error_logging.py +134 -0
  103. code_puppy/gemini_code_assist.py +385 -0
  104. code_puppy/gemini_model.py +754 -0
  105. code_puppy/hook_engine/README.md +105 -0
  106. code_puppy/hook_engine/__init__.py +21 -0
  107. code_puppy/hook_engine/aliases.py +155 -0
  108. code_puppy/hook_engine/engine.py +221 -0
  109. code_puppy/hook_engine/executor.py +296 -0
  110. code_puppy/hook_engine/matcher.py +156 -0
  111. code_puppy/hook_engine/models.py +240 -0
  112. code_puppy/hook_engine/registry.py +106 -0
  113. code_puppy/hook_engine/validator.py +144 -0
  114. code_puppy/http_utils.py +361 -0
  115. code_puppy/keymap.py +128 -0
  116. code_puppy/main.py +10 -0
  117. code_puppy/mcp_/__init__.py +66 -0
  118. code_puppy/mcp_/async_lifecycle.py +286 -0
  119. code_puppy/mcp_/blocking_startup.py +469 -0
  120. code_puppy/mcp_/captured_stdio_server.py +275 -0
  121. code_puppy/mcp_/circuit_breaker.py +290 -0
  122. code_puppy/mcp_/config_wizard.py +507 -0
  123. code_puppy/mcp_/dashboard.py +308 -0
  124. code_puppy/mcp_/error_isolation.py +407 -0
  125. code_puppy/mcp_/examples/retry_example.py +226 -0
  126. code_puppy/mcp_/health_monitor.py +589 -0
  127. code_puppy/mcp_/managed_server.py +428 -0
  128. code_puppy/mcp_/manager.py +807 -0
  129. code_puppy/mcp_/mcp_logs.py +224 -0
  130. code_puppy/mcp_/registry.py +451 -0
  131. code_puppy/mcp_/retry_manager.py +337 -0
  132. code_puppy/mcp_/server_registry_catalog.py +1126 -0
  133. code_puppy/mcp_/status_tracker.py +355 -0
  134. code_puppy/mcp_/system_tools.py +209 -0
  135. code_puppy/mcp_prompts/__init__.py +1 -0
  136. code_puppy/mcp_prompts/hook_creator.py +103 -0
  137. code_puppy/messaging/__init__.py +255 -0
  138. code_puppy/messaging/bus.py +613 -0
  139. code_puppy/messaging/commands.py +167 -0
  140. code_puppy/messaging/markdown_patches.py +57 -0
  141. code_puppy/messaging/message_queue.py +361 -0
  142. code_puppy/messaging/messages.py +569 -0
  143. code_puppy/messaging/queue_console.py +271 -0
  144. code_puppy/messaging/renderers.py +311 -0
  145. code_puppy/messaging/rich_renderer.py +1158 -0
  146. code_puppy/messaging/spinner/__init__.py +83 -0
  147. code_puppy/messaging/spinner/console_spinner.py +240 -0
  148. code_puppy/messaging/spinner/spinner_base.py +95 -0
  149. code_puppy/messaging/subagent_console.py +460 -0
  150. code_puppy/model_factory.py +848 -0
  151. code_puppy/model_switching.py +63 -0
  152. code_puppy/model_utils.py +168 -0
  153. code_puppy/models.json +174 -0
  154. code_puppy/models_dev_api.json +1 -0
  155. code_puppy/models_dev_parser.py +592 -0
  156. code_puppy/plugins/__init__.py +186 -0
  157. code_puppy/plugins/agent_skills/__init__.py +22 -0
  158. code_puppy/plugins/agent_skills/config.py +175 -0
  159. code_puppy/plugins/agent_skills/discovery.py +136 -0
  160. code_puppy/plugins/agent_skills/downloader.py +392 -0
  161. code_puppy/plugins/agent_skills/installer.py +22 -0
  162. code_puppy/plugins/agent_skills/metadata.py +219 -0
  163. code_puppy/plugins/agent_skills/prompt_builder.py +60 -0
  164. code_puppy/plugins/agent_skills/register_callbacks.py +241 -0
  165. code_puppy/plugins/agent_skills/remote_catalog.py +322 -0
  166. code_puppy/plugins/agent_skills/skill_catalog.py +257 -0
  167. code_puppy/plugins/agent_skills/skills_install_menu.py +664 -0
  168. code_puppy/plugins/agent_skills/skills_menu.py +781 -0
  169. code_puppy/plugins/antigravity_oauth/__init__.py +10 -0
  170. code_puppy/plugins/antigravity_oauth/accounts.py +406 -0
  171. code_puppy/plugins/antigravity_oauth/antigravity_model.py +706 -0
  172. code_puppy/plugins/antigravity_oauth/config.py +42 -0
  173. code_puppy/plugins/antigravity_oauth/constants.py +133 -0
  174. code_puppy/plugins/antigravity_oauth/oauth.py +478 -0
  175. code_puppy/plugins/antigravity_oauth/register_callbacks.py +518 -0
  176. code_puppy/plugins/antigravity_oauth/storage.py +288 -0
  177. code_puppy/plugins/antigravity_oauth/test_plugin.py +319 -0
  178. code_puppy/plugins/antigravity_oauth/token.py +167 -0
  179. code_puppy/plugins/antigravity_oauth/transport.py +863 -0
  180. code_puppy/plugins/antigravity_oauth/utils.py +168 -0
  181. code_puppy/plugins/chatgpt_oauth/__init__.py +8 -0
  182. code_puppy/plugins/chatgpt_oauth/config.py +52 -0
  183. code_puppy/plugins/chatgpt_oauth/oauth_flow.py +329 -0
  184. code_puppy/plugins/chatgpt_oauth/register_callbacks.py +176 -0
  185. code_puppy/plugins/chatgpt_oauth/test_plugin.py +301 -0
  186. code_puppy/plugins/chatgpt_oauth/utils.py +523 -0
  187. code_puppy/plugins/claude_code_hooks/__init__.py +1 -0
  188. code_puppy/plugins/claude_code_hooks/config.py +137 -0
  189. code_puppy/plugins/claude_code_hooks/register_callbacks.py +175 -0
  190. code_puppy/plugins/claude_code_oauth/README.md +167 -0
  191. code_puppy/plugins/claude_code_oauth/SETUP.md +93 -0
  192. code_puppy/plugins/claude_code_oauth/__init__.py +25 -0
  193. code_puppy/plugins/claude_code_oauth/config.py +52 -0
  194. code_puppy/plugins/claude_code_oauth/register_callbacks.py +453 -0
  195. code_puppy/plugins/claude_code_oauth/test_plugin.py +283 -0
  196. code_puppy/plugins/claude_code_oauth/token_refresh_heartbeat.py +241 -0
  197. code_puppy/plugins/claude_code_oauth/utils.py +640 -0
  198. code_puppy/plugins/customizable_commands/__init__.py +0 -0
  199. code_puppy/plugins/customizable_commands/register_callbacks.py +152 -0
  200. code_puppy/plugins/example_custom_command/README.md +280 -0
  201. code_puppy/plugins/example_custom_command/register_callbacks.py +51 -0
  202. code_puppy/plugins/file_permission_handler/__init__.py +4 -0
  203. code_puppy/plugins/file_permission_handler/register_callbacks.py +470 -0
  204. code_puppy/plugins/frontend_emitter/__init__.py +25 -0
  205. code_puppy/plugins/frontend_emitter/emitter.py +121 -0
  206. code_puppy/plugins/frontend_emitter/register_callbacks.py +261 -0
  207. code_puppy/plugins/hook_creator/__init__.py +1 -0
  208. code_puppy/plugins/hook_creator/register_callbacks.py +33 -0
  209. code_puppy/plugins/hook_manager/__init__.py +1 -0
  210. code_puppy/plugins/hook_manager/config.py +290 -0
  211. code_puppy/plugins/hook_manager/hooks_menu.py +564 -0
  212. code_puppy/plugins/hook_manager/register_callbacks.py +227 -0
  213. code_puppy/plugins/oauth_puppy_html.py +228 -0
  214. code_puppy/plugins/scheduler/__init__.py +1 -0
  215. code_puppy/plugins/scheduler/register_callbacks.py +88 -0
  216. code_puppy/plugins/scheduler/scheduler_menu.py +522 -0
  217. code_puppy/plugins/scheduler/scheduler_wizard.py +341 -0
  218. code_puppy/plugins/shell_safety/__init__.py +6 -0
  219. code_puppy/plugins/shell_safety/agent_shell_safety.py +69 -0
  220. code_puppy/plugins/shell_safety/command_cache.py +156 -0
  221. code_puppy/plugins/shell_safety/register_callbacks.py +202 -0
  222. code_puppy/plugins/synthetic_status/__init__.py +1 -0
  223. code_puppy/plugins/synthetic_status/register_callbacks.py +132 -0
  224. code_puppy/plugins/synthetic_status/status_api.py +147 -0
  225. code_puppy/plugins/universal_constructor/__init__.py +13 -0
  226. code_puppy/plugins/universal_constructor/models.py +138 -0
  227. code_puppy/plugins/universal_constructor/register_callbacks.py +47 -0
  228. code_puppy/plugins/universal_constructor/registry.py +302 -0
  229. code_puppy/plugins/universal_constructor/sandbox.py +584 -0
  230. code_puppy/prompts/antigravity_system_prompt.md +1 -0
  231. code_puppy/pydantic_patches.py +356 -0
  232. code_puppy/reopenable_async_client.py +232 -0
  233. code_puppy/round_robin_model.py +150 -0
  234. code_puppy/scheduler/__init__.py +41 -0
  235. code_puppy/scheduler/__main__.py +9 -0
  236. code_puppy/scheduler/cli.py +118 -0
  237. code_puppy/scheduler/config.py +126 -0
  238. code_puppy/scheduler/daemon.py +280 -0
  239. code_puppy/scheduler/executor.py +155 -0
  240. code_puppy/scheduler/platform.py +19 -0
  241. code_puppy/scheduler/platform_unix.py +22 -0
  242. code_puppy/scheduler/platform_win.py +32 -0
  243. code_puppy/session_storage.py +338 -0
  244. code_puppy/status_display.py +257 -0
  245. code_puppy/summarization_agent.py +176 -0
  246. code_puppy/terminal_utils.py +418 -0
  247. code_puppy/tools/__init__.py +501 -0
  248. code_puppy/tools/agent_tools.py +603 -0
  249. code_puppy/tools/ask_user_question/__init__.py +26 -0
  250. code_puppy/tools/ask_user_question/constants.py +73 -0
  251. code_puppy/tools/ask_user_question/demo_tui.py +55 -0
  252. code_puppy/tools/ask_user_question/handler.py +232 -0
  253. code_puppy/tools/ask_user_question/models.py +304 -0
  254. code_puppy/tools/ask_user_question/registration.py +26 -0
  255. code_puppy/tools/ask_user_question/renderers.py +309 -0
  256. code_puppy/tools/ask_user_question/terminal_ui.py +329 -0
  257. code_puppy/tools/ask_user_question/theme.py +155 -0
  258. code_puppy/tools/ask_user_question/tui_loop.py +423 -0
  259. code_puppy/tools/browser/__init__.py +37 -0
  260. code_puppy/tools/browser/browser_control.py +289 -0
  261. code_puppy/tools/browser/browser_interactions.py +545 -0
  262. code_puppy/tools/browser/browser_locators.py +640 -0
  263. code_puppy/tools/browser/browser_manager.py +378 -0
  264. code_puppy/tools/browser/browser_navigation.py +251 -0
  265. code_puppy/tools/browser/browser_screenshot.py +179 -0
  266. code_puppy/tools/browser/browser_scripts.py +462 -0
  267. code_puppy/tools/browser/browser_workflows.py +221 -0
  268. code_puppy/tools/browser/chromium_terminal_manager.py +259 -0
  269. code_puppy/tools/browser/terminal_command_tools.py +534 -0
  270. code_puppy/tools/browser/terminal_screenshot_tools.py +552 -0
  271. code_puppy/tools/browser/terminal_tools.py +525 -0
  272. code_puppy/tools/command_runner.py +1346 -0
  273. code_puppy/tools/common.py +1409 -0
  274. code_puppy/tools/display.py +84 -0
  275. code_puppy/tools/file_modifications.py +886 -0
  276. code_puppy/tools/file_operations.py +802 -0
  277. code_puppy/tools/scheduler_tools.py +412 -0
  278. code_puppy/tools/skills_tools.py +244 -0
  279. code_puppy/tools/subagent_context.py +158 -0
  280. code_puppy/tools/tools_content.py +51 -0
  281. code_puppy/tools/universal_constructor.py +889 -0
  282. code_puppy/uvx_detection.py +242 -0
  283. code_puppy/version_checker.py +82 -0
  284. codepp-0.0.437.dist-info/METADATA +766 -0
  285. codepp-0.0.437.dist-info/RECORD +288 -0
  286. codepp-0.0.437.dist-info/WHEEL +4 -0
  287. codepp-0.0.437.dist-info/entry_points.txt +3 -0
  288. 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
+ ]