code-puppy 0.0.169__py3-none-any.whl → 0.0.366__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (243) hide show
  1. code_puppy/__init__.py +7 -1
  2. code_puppy/agents/__init__.py +8 -8
  3. code_puppy/agents/agent_c_reviewer.py +155 -0
  4. code_puppy/agents/agent_code_puppy.py +9 -2
  5. code_puppy/agents/agent_code_reviewer.py +90 -0
  6. code_puppy/agents/agent_cpp_reviewer.py +132 -0
  7. code_puppy/agents/agent_creator_agent.py +48 -9
  8. code_puppy/agents/agent_golang_reviewer.py +151 -0
  9. code_puppy/agents/agent_javascript_reviewer.py +160 -0
  10. code_puppy/agents/agent_manager.py +146 -199
  11. code_puppy/agents/agent_pack_leader.py +383 -0
  12. code_puppy/agents/agent_planning.py +163 -0
  13. code_puppy/agents/agent_python_programmer.py +165 -0
  14. code_puppy/agents/agent_python_reviewer.py +90 -0
  15. code_puppy/agents/agent_qa_expert.py +163 -0
  16. code_puppy/agents/agent_qa_kitten.py +208 -0
  17. code_puppy/agents/agent_security_auditor.py +181 -0
  18. code_puppy/agents/agent_terminal_qa.py +323 -0
  19. code_puppy/agents/agent_typescript_reviewer.py +166 -0
  20. code_puppy/agents/base_agent.py +1713 -1
  21. code_puppy/agents/event_stream_handler.py +350 -0
  22. code_puppy/agents/json_agent.py +12 -1
  23. code_puppy/agents/pack/__init__.py +34 -0
  24. code_puppy/agents/pack/bloodhound.py +304 -0
  25. code_puppy/agents/pack/husky.py +321 -0
  26. code_puppy/agents/pack/retriever.py +393 -0
  27. code_puppy/agents/pack/shepherd.py +348 -0
  28. code_puppy/agents/pack/terrier.py +287 -0
  29. code_puppy/agents/pack/watchdog.py +367 -0
  30. code_puppy/agents/prompt_reviewer.py +145 -0
  31. code_puppy/agents/subagent_stream_handler.py +276 -0
  32. code_puppy/api/__init__.py +13 -0
  33. code_puppy/api/app.py +169 -0
  34. code_puppy/api/main.py +21 -0
  35. code_puppy/api/pty_manager.py +446 -0
  36. code_puppy/api/routers/__init__.py +12 -0
  37. code_puppy/api/routers/agents.py +36 -0
  38. code_puppy/api/routers/commands.py +217 -0
  39. code_puppy/api/routers/config.py +74 -0
  40. code_puppy/api/routers/sessions.py +232 -0
  41. code_puppy/api/templates/terminal.html +361 -0
  42. code_puppy/api/websocket.py +154 -0
  43. code_puppy/callbacks.py +174 -4
  44. code_puppy/chatgpt_codex_client.py +283 -0
  45. code_puppy/claude_cache_client.py +586 -0
  46. code_puppy/cli_runner.py +916 -0
  47. code_puppy/command_line/add_model_menu.py +1079 -0
  48. code_puppy/command_line/agent_menu.py +395 -0
  49. code_puppy/command_line/attachments.py +395 -0
  50. code_puppy/command_line/autosave_menu.py +605 -0
  51. code_puppy/command_line/clipboard.py +527 -0
  52. code_puppy/command_line/colors_menu.py +520 -0
  53. code_puppy/command_line/command_handler.py +233 -627
  54. code_puppy/command_line/command_registry.py +150 -0
  55. code_puppy/command_line/config_commands.py +715 -0
  56. code_puppy/command_line/core_commands.py +792 -0
  57. code_puppy/command_line/diff_menu.py +863 -0
  58. code_puppy/command_line/load_context_completion.py +15 -22
  59. code_puppy/command_line/mcp/base.py +1 -4
  60. code_puppy/command_line/mcp/catalog_server_installer.py +175 -0
  61. code_puppy/command_line/mcp/custom_server_form.py +688 -0
  62. code_puppy/command_line/mcp/custom_server_installer.py +195 -0
  63. code_puppy/command_line/mcp/edit_command.py +148 -0
  64. code_puppy/command_line/mcp/handler.py +9 -4
  65. code_puppy/command_line/mcp/help_command.py +6 -5
  66. code_puppy/command_line/mcp/install_command.py +16 -27
  67. code_puppy/command_line/mcp/install_menu.py +685 -0
  68. code_puppy/command_line/mcp/list_command.py +3 -3
  69. code_puppy/command_line/mcp/logs_command.py +174 -65
  70. code_puppy/command_line/mcp/remove_command.py +2 -2
  71. code_puppy/command_line/mcp/restart_command.py +12 -4
  72. code_puppy/command_line/mcp/search_command.py +17 -11
  73. code_puppy/command_line/mcp/start_all_command.py +22 -13
  74. code_puppy/command_line/mcp/start_command.py +50 -31
  75. code_puppy/command_line/mcp/status_command.py +6 -7
  76. code_puppy/command_line/mcp/stop_all_command.py +11 -8
  77. code_puppy/command_line/mcp/stop_command.py +11 -10
  78. code_puppy/command_line/mcp/test_command.py +2 -2
  79. code_puppy/command_line/mcp/utils.py +1 -1
  80. code_puppy/command_line/mcp/wizard_utils.py +22 -18
  81. code_puppy/command_line/mcp_completion.py +174 -0
  82. code_puppy/command_line/model_picker_completion.py +89 -30
  83. code_puppy/command_line/model_settings_menu.py +884 -0
  84. code_puppy/command_line/motd.py +14 -8
  85. code_puppy/command_line/onboarding_slides.py +179 -0
  86. code_puppy/command_line/onboarding_wizard.py +340 -0
  87. code_puppy/command_line/pin_command_completion.py +329 -0
  88. code_puppy/command_line/prompt_toolkit_completion.py +626 -75
  89. code_puppy/command_line/session_commands.py +296 -0
  90. code_puppy/command_line/utils.py +54 -0
  91. code_puppy/config.py +1181 -51
  92. code_puppy/error_logging.py +118 -0
  93. code_puppy/gemini_code_assist.py +385 -0
  94. code_puppy/gemini_model.py +602 -0
  95. code_puppy/http_utils.py +220 -104
  96. code_puppy/keymap.py +128 -0
  97. code_puppy/main.py +5 -594
  98. code_puppy/{mcp → mcp_}/__init__.py +17 -0
  99. code_puppy/{mcp → mcp_}/async_lifecycle.py +35 -4
  100. code_puppy/{mcp → mcp_}/blocking_startup.py +70 -43
  101. code_puppy/{mcp → mcp_}/captured_stdio_server.py +2 -2
  102. code_puppy/{mcp → mcp_}/config_wizard.py +5 -5
  103. code_puppy/{mcp → mcp_}/dashboard.py +15 -6
  104. code_puppy/{mcp → mcp_}/examples/retry_example.py +4 -1
  105. code_puppy/{mcp → mcp_}/managed_server.py +66 -39
  106. code_puppy/{mcp → mcp_}/manager.py +146 -52
  107. code_puppy/mcp_/mcp_logs.py +224 -0
  108. code_puppy/{mcp → mcp_}/registry.py +6 -6
  109. code_puppy/{mcp → mcp_}/server_registry_catalog.py +25 -8
  110. code_puppy/messaging/__init__.py +199 -2
  111. code_puppy/messaging/bus.py +610 -0
  112. code_puppy/messaging/commands.py +167 -0
  113. code_puppy/messaging/markdown_patches.py +57 -0
  114. code_puppy/messaging/message_queue.py +17 -48
  115. code_puppy/messaging/messages.py +500 -0
  116. code_puppy/messaging/queue_console.py +1 -24
  117. code_puppy/messaging/renderers.py +43 -146
  118. code_puppy/messaging/rich_renderer.py +1027 -0
  119. code_puppy/messaging/spinner/__init__.py +33 -5
  120. code_puppy/messaging/spinner/console_spinner.py +92 -52
  121. code_puppy/messaging/spinner/spinner_base.py +29 -0
  122. code_puppy/messaging/subagent_console.py +461 -0
  123. code_puppy/model_factory.py +686 -80
  124. code_puppy/model_utils.py +167 -0
  125. code_puppy/models.json +86 -104
  126. code_puppy/models_dev_api.json +1 -0
  127. code_puppy/models_dev_parser.py +592 -0
  128. code_puppy/plugins/__init__.py +164 -10
  129. code_puppy/plugins/antigravity_oauth/__init__.py +10 -0
  130. code_puppy/plugins/antigravity_oauth/accounts.py +406 -0
  131. code_puppy/plugins/antigravity_oauth/antigravity_model.py +704 -0
  132. code_puppy/plugins/antigravity_oauth/config.py +42 -0
  133. code_puppy/plugins/antigravity_oauth/constants.py +136 -0
  134. code_puppy/plugins/antigravity_oauth/oauth.py +478 -0
  135. code_puppy/plugins/antigravity_oauth/register_callbacks.py +406 -0
  136. code_puppy/plugins/antigravity_oauth/storage.py +271 -0
  137. code_puppy/plugins/antigravity_oauth/test_plugin.py +319 -0
  138. code_puppy/plugins/antigravity_oauth/token.py +167 -0
  139. code_puppy/plugins/antigravity_oauth/transport.py +767 -0
  140. code_puppy/plugins/antigravity_oauth/utils.py +169 -0
  141. code_puppy/plugins/chatgpt_oauth/__init__.py +8 -0
  142. code_puppy/plugins/chatgpt_oauth/config.py +52 -0
  143. code_puppy/plugins/chatgpt_oauth/oauth_flow.py +328 -0
  144. code_puppy/plugins/chatgpt_oauth/register_callbacks.py +94 -0
  145. code_puppy/plugins/chatgpt_oauth/test_plugin.py +293 -0
  146. code_puppy/plugins/chatgpt_oauth/utils.py +489 -0
  147. code_puppy/plugins/claude_code_oauth/README.md +167 -0
  148. code_puppy/plugins/claude_code_oauth/SETUP.md +93 -0
  149. code_puppy/plugins/claude_code_oauth/__init__.py +6 -0
  150. code_puppy/plugins/claude_code_oauth/config.py +50 -0
  151. code_puppy/plugins/claude_code_oauth/register_callbacks.py +308 -0
  152. code_puppy/plugins/claude_code_oauth/test_plugin.py +283 -0
  153. code_puppy/plugins/claude_code_oauth/utils.py +518 -0
  154. code_puppy/plugins/customizable_commands/__init__.py +0 -0
  155. code_puppy/plugins/customizable_commands/register_callbacks.py +169 -0
  156. code_puppy/plugins/example_custom_command/README.md +280 -0
  157. code_puppy/plugins/example_custom_command/register_callbacks.py +51 -0
  158. code_puppy/plugins/file_permission_handler/__init__.py +4 -0
  159. code_puppy/plugins/file_permission_handler/register_callbacks.py +523 -0
  160. code_puppy/plugins/frontend_emitter/__init__.py +25 -0
  161. code_puppy/plugins/frontend_emitter/emitter.py +121 -0
  162. code_puppy/plugins/frontend_emitter/register_callbacks.py +261 -0
  163. code_puppy/plugins/oauth_puppy_html.py +228 -0
  164. code_puppy/plugins/shell_safety/__init__.py +6 -0
  165. code_puppy/plugins/shell_safety/agent_shell_safety.py +69 -0
  166. code_puppy/plugins/shell_safety/command_cache.py +156 -0
  167. code_puppy/plugins/shell_safety/register_callbacks.py +202 -0
  168. code_puppy/prompts/antigravity_system_prompt.md +1 -0
  169. code_puppy/prompts/codex_system_prompt.md +310 -0
  170. code_puppy/pydantic_patches.py +131 -0
  171. code_puppy/reopenable_async_client.py +8 -8
  172. code_puppy/round_robin_model.py +10 -15
  173. code_puppy/session_storage.py +294 -0
  174. code_puppy/status_display.py +21 -4
  175. code_puppy/summarization_agent.py +52 -14
  176. code_puppy/terminal_utils.py +418 -0
  177. code_puppy/tools/__init__.py +139 -6
  178. code_puppy/tools/agent_tools.py +548 -49
  179. code_puppy/tools/browser/__init__.py +37 -0
  180. code_puppy/tools/browser/browser_control.py +289 -0
  181. code_puppy/tools/browser/browser_interactions.py +545 -0
  182. code_puppy/tools/browser/browser_locators.py +640 -0
  183. code_puppy/tools/browser/browser_manager.py +316 -0
  184. code_puppy/tools/browser/browser_navigation.py +251 -0
  185. code_puppy/tools/browser/browser_screenshot.py +179 -0
  186. code_puppy/tools/browser/browser_scripts.py +462 -0
  187. code_puppy/tools/browser/browser_workflows.py +221 -0
  188. code_puppy/tools/browser/chromium_terminal_manager.py +259 -0
  189. code_puppy/tools/browser/terminal_command_tools.py +521 -0
  190. code_puppy/tools/browser/terminal_screenshot_tools.py +556 -0
  191. code_puppy/tools/browser/terminal_tools.py +525 -0
  192. code_puppy/tools/command_runner.py +941 -153
  193. code_puppy/tools/common.py +1146 -6
  194. code_puppy/tools/display.py +84 -0
  195. code_puppy/tools/file_modifications.py +288 -89
  196. code_puppy/tools/file_operations.py +352 -266
  197. code_puppy/tools/subagent_context.py +158 -0
  198. code_puppy/uvx_detection.py +242 -0
  199. code_puppy/version_checker.py +30 -11
  200. code_puppy-0.0.366.data/data/code_puppy/models.json +110 -0
  201. code_puppy-0.0.366.data/data/code_puppy/models_dev_api.json +1 -0
  202. {code_puppy-0.0.169.dist-info → code_puppy-0.0.366.dist-info}/METADATA +184 -67
  203. code_puppy-0.0.366.dist-info/RECORD +217 -0
  204. {code_puppy-0.0.169.dist-info → code_puppy-0.0.366.dist-info}/WHEEL +1 -1
  205. {code_puppy-0.0.169.dist-info → code_puppy-0.0.366.dist-info}/entry_points.txt +1 -0
  206. code_puppy/agent.py +0 -231
  207. code_puppy/agents/agent_orchestrator.json +0 -26
  208. code_puppy/agents/runtime_manager.py +0 -272
  209. code_puppy/command_line/mcp/add_command.py +0 -183
  210. code_puppy/command_line/meta_command_handler.py +0 -153
  211. code_puppy/message_history_processor.py +0 -490
  212. code_puppy/messaging/spinner/textual_spinner.py +0 -101
  213. code_puppy/state_management.py +0 -200
  214. code_puppy/tui/__init__.py +0 -10
  215. code_puppy/tui/app.py +0 -986
  216. code_puppy/tui/components/__init__.py +0 -21
  217. code_puppy/tui/components/chat_view.py +0 -550
  218. code_puppy/tui/components/command_history_modal.py +0 -218
  219. code_puppy/tui/components/copy_button.py +0 -139
  220. code_puppy/tui/components/custom_widgets.py +0 -63
  221. code_puppy/tui/components/human_input_modal.py +0 -175
  222. code_puppy/tui/components/input_area.py +0 -167
  223. code_puppy/tui/components/sidebar.py +0 -309
  224. code_puppy/tui/components/status_bar.py +0 -182
  225. code_puppy/tui/messages.py +0 -27
  226. code_puppy/tui/models/__init__.py +0 -8
  227. code_puppy/tui/models/chat_message.py +0 -25
  228. code_puppy/tui/models/command_history.py +0 -89
  229. code_puppy/tui/models/enums.py +0 -24
  230. code_puppy/tui/screens/__init__.py +0 -15
  231. code_puppy/tui/screens/help.py +0 -130
  232. code_puppy/tui/screens/mcp_install_wizard.py +0 -803
  233. code_puppy/tui/screens/settings.py +0 -290
  234. code_puppy/tui/screens/tools.py +0 -74
  235. code_puppy-0.0.169.data/data/code_puppy/models.json +0 -128
  236. code_puppy-0.0.169.dist-info/RECORD +0 -112
  237. /code_puppy/{mcp → mcp_}/circuit_breaker.py +0 -0
  238. /code_puppy/{mcp → mcp_}/error_isolation.py +0 -0
  239. /code_puppy/{mcp → mcp_}/health_monitor.py +0 -0
  240. /code_puppy/{mcp → mcp_}/retry_manager.py +0 -0
  241. /code_puppy/{mcp → mcp_}/status_tracker.py +0 -0
  242. /code_puppy/{mcp → mcp_}/system_tools.py +0 -0
  243. {code_puppy-0.0.169.dist-info → code_puppy-0.0.366.dist-info}/licenses/LICENSE +0 -0
@@ -0,0 +1,395 @@
1
+ """Interactive terminal UI for selecting agents.
2
+
3
+ Provides a split-panel interface for browsing and selecting agents
4
+ with live preview of agent details.
5
+ """
6
+
7
+ import sys
8
+ import time
9
+ import unicodedata
10
+ from typing import List, Optional, Tuple
11
+
12
+ from prompt_toolkit.application import Application
13
+ from prompt_toolkit.key_binding import KeyBindings
14
+ from prompt_toolkit.layout import Dimension, Layout, VSplit, Window
15
+ from prompt_toolkit.layout.controls import FormattedTextControl
16
+ from prompt_toolkit.widgets import Frame
17
+
18
+ from code_puppy.agents import (
19
+ get_agent_descriptions,
20
+ get_available_agents,
21
+ get_current_agent,
22
+ )
23
+ from code_puppy.tools.command_runner import set_awaiting_user_input
24
+
25
+ PAGE_SIZE = 10 # Agents per page
26
+
27
+
28
+ def _sanitize_display_text(text: str) -> str:
29
+ """Remove or replace characters that cause terminal rendering issues.
30
+
31
+ Args:
32
+ text: Text that may contain emojis or wide characters
33
+
34
+ Returns:
35
+ Sanitized text safe for prompt_toolkit rendering
36
+ """
37
+ # Keep only characters that render cleanly in terminals
38
+ # Be aggressive about stripping anything that could cause width issues
39
+ result = []
40
+ for char in text:
41
+ # Get unicode category
42
+ cat = unicodedata.category(char)
43
+ # Categories to KEEP:
44
+ # - L* (Letters): Lu, Ll, Lt, Lm, Lo
45
+ # - N* (Numbers): Nd, Nl, No
46
+ # - P* (Punctuation): Pc, Pd, Ps, Pe, Pi, Pf, Po
47
+ # - Zs (Space separator)
48
+ # - Sm (Math symbols like +, -, =)
49
+ # - Sc (Currency symbols like $, €)
50
+ # - Sk (Modifier symbols)
51
+ #
52
+ # Categories to SKIP (cause rendering issues):
53
+ # - So (Symbol, other) - emojis
54
+ # - Cf (Format) - ZWJ, etc.
55
+ # - Mn (Mark, nonspacing) - combining characters
56
+ # - Mc (Mark, spacing combining)
57
+ # - Me (Mark, enclosing)
58
+ # - Cn (Not assigned)
59
+ # - Co (Private use)
60
+ # - Cs (Surrogate)
61
+ safe_categories = (
62
+ "Lu",
63
+ "Ll",
64
+ "Lt",
65
+ "Lm",
66
+ "Lo", # Letters
67
+ "Nd",
68
+ "Nl",
69
+ "No", # Numbers
70
+ "Pc",
71
+ "Pd",
72
+ "Ps",
73
+ "Pe",
74
+ "Pi",
75
+ "Pf",
76
+ "Po", # Punctuation
77
+ "Zs", # Space
78
+ "Sm",
79
+ "Sc",
80
+ "Sk", # Safe symbols (math, currency, modifier)
81
+ )
82
+ if cat in safe_categories:
83
+ result.append(char)
84
+
85
+ # Clean up any double spaces left behind and strip
86
+ cleaned = " ".join("".join(result).split())
87
+ return cleaned
88
+
89
+
90
+ def _get_agent_entries() -> List[Tuple[str, str, str]]:
91
+ """Get all agents with their display names and descriptions.
92
+
93
+ Returns:
94
+ List of tuples (agent_name, display_name, description) sorted by name.
95
+ """
96
+ available = get_available_agents()
97
+ descriptions = get_agent_descriptions()
98
+
99
+ entries = []
100
+ for name, display_name in available.items():
101
+ description = descriptions.get(name, "No description available")
102
+ entries.append((name, display_name, description))
103
+
104
+ # Sort alphabetically by agent name
105
+ entries.sort(key=lambda x: x[0].lower())
106
+ return entries
107
+
108
+
109
+ def _render_menu_panel(
110
+ entries: List[Tuple[str, str, str]],
111
+ page: int,
112
+ selected_idx: int,
113
+ current_agent_name: str,
114
+ ) -> List:
115
+ """Render the left menu panel with pagination.
116
+
117
+ Args:
118
+ entries: List of (name, display_name, description) tuples
119
+ page: Current page number (0-indexed)
120
+ selected_idx: Currently selected index (global)
121
+ current_agent_name: Name of the current active agent
122
+
123
+ Returns:
124
+ List of (style, text) tuples for FormattedTextControl
125
+ """
126
+ lines = []
127
+ total_pages = (len(entries) + PAGE_SIZE - 1) // PAGE_SIZE if entries else 1
128
+ start_idx = page * PAGE_SIZE
129
+ end_idx = min(start_idx + PAGE_SIZE, len(entries))
130
+
131
+ lines.append(("bold", "Agents"))
132
+ lines.append(("fg:ansibrightblack", f" (Page {page + 1}/{total_pages})"))
133
+ lines.append(("", "\n\n"))
134
+
135
+ if not entries:
136
+ lines.append(("fg:yellow", " No agents found."))
137
+ lines.append(("", "\n\n"))
138
+ else:
139
+ # Show agents for current page
140
+ for i in range(start_idx, end_idx):
141
+ name, display_name, _ = entries[i]
142
+ is_selected = i == selected_idx
143
+ is_current = name == current_agent_name
144
+
145
+ # Sanitize display name to avoid emoji rendering issues
146
+ safe_display_name = _sanitize_display_text(display_name)
147
+
148
+ # Build the line
149
+ if is_selected:
150
+ lines.append(("fg:ansigreen", "▶ "))
151
+ lines.append(("fg:ansigreen bold", safe_display_name))
152
+ else:
153
+ lines.append(("", " "))
154
+ lines.append(("", safe_display_name))
155
+
156
+ # Add current marker
157
+ if is_current:
158
+ lines.append(("fg:ansicyan", " ← current"))
159
+
160
+ lines.append(("", "\n"))
161
+
162
+ # Navigation hints
163
+ lines.append(("", "\n"))
164
+ lines.append(("fg:ansibrightblack", " ↑↓ "))
165
+ lines.append(("", "Navigate\n"))
166
+ lines.append(("fg:ansibrightblack", " ←→ "))
167
+ lines.append(("", "Page\n"))
168
+ lines.append(("fg:green", " Enter "))
169
+ lines.append(("", "Select\n"))
170
+ lines.append(("fg:ansibrightred", " Ctrl+C "))
171
+ lines.append(("", "Cancel"))
172
+
173
+ return lines
174
+
175
+
176
+ def _render_preview_panel(
177
+ entry: Optional[Tuple[str, str, str]],
178
+ current_agent_name: str,
179
+ ) -> List:
180
+ """Render the right preview panel with agent details.
181
+
182
+ Args:
183
+ entry: Tuple of (name, display_name, description) or None
184
+ current_agent_name: Name of the current active agent
185
+
186
+ Returns:
187
+ List of (style, text) tuples for FormattedTextControl
188
+ """
189
+ lines = []
190
+
191
+ lines.append(("dim cyan", " AGENT DETAILS"))
192
+ lines.append(("", "\n\n"))
193
+
194
+ if not entry:
195
+ lines.append(("fg:yellow", " No agent selected."))
196
+ lines.append(("", "\n"))
197
+ return lines
198
+
199
+ name, display_name, description = entry
200
+ is_current = name == current_agent_name
201
+
202
+ # Sanitize text to avoid emoji rendering issues
203
+ safe_display_name = _sanitize_display_text(display_name)
204
+ safe_description = _sanitize_display_text(description)
205
+
206
+ # Agent name (identifier)
207
+ lines.append(("bold", "Name: "))
208
+ lines.append(("", name))
209
+ lines.append(("", "\n\n"))
210
+
211
+ # Display name
212
+ lines.append(("bold", "Display Name: "))
213
+ lines.append(("fg:ansicyan", safe_display_name))
214
+ lines.append(("", "\n\n"))
215
+
216
+ # Description
217
+ lines.append(("bold", "Description:"))
218
+ lines.append(("", "\n"))
219
+
220
+ # Wrap description to fit panel
221
+ desc_lines = safe_description.split("\n")
222
+ for desc_line in desc_lines:
223
+ # Word wrap long lines
224
+ words = desc_line.split()
225
+ current_line = ""
226
+ for word in words:
227
+ if len(current_line) + len(word) + 1 > 55:
228
+ lines.append(("fg:ansibrightblack", current_line))
229
+ lines.append(("", "\n"))
230
+ current_line = word
231
+ else:
232
+ if current_line == "":
233
+ current_line = word
234
+ else:
235
+ current_line += " " + word
236
+ if current_line.strip():
237
+ lines.append(("fg:ansibrightblack", current_line))
238
+ lines.append(("", "\n"))
239
+
240
+ lines.append(("", "\n"))
241
+
242
+ # Current status
243
+ lines.append(("bold", " Status: "))
244
+ if is_current:
245
+ lines.append(("fg:ansigreen bold", "✓ Currently Active"))
246
+ else:
247
+ lines.append(("fg:ansibrightblack", "Not active"))
248
+ lines.append(("", "\n"))
249
+
250
+ return lines
251
+
252
+
253
+ async def interactive_agent_picker() -> Optional[str]:
254
+ """Show interactive terminal UI to select an agent.
255
+
256
+ Returns:
257
+ Agent name to switch to, or None if cancelled.
258
+ """
259
+ entries = _get_agent_entries()
260
+ current_agent = get_current_agent()
261
+ current_agent_name = current_agent.name if current_agent else ""
262
+
263
+ if not entries:
264
+ from code_puppy.messaging import emit_info
265
+
266
+ emit_info("No agents found.")
267
+ return None
268
+
269
+ # State
270
+ selected_idx = [0] # Current selection (global index)
271
+ current_page = [0] # Current page
272
+ result = [None] # Selected agent name
273
+
274
+ total_pages = (len(entries) + PAGE_SIZE - 1) // PAGE_SIZE
275
+
276
+ def get_current_entry() -> Optional[Tuple[str, str, str]]:
277
+ if 0 <= selected_idx[0] < len(entries):
278
+ return entries[selected_idx[0]]
279
+ return None
280
+
281
+ # Build UI
282
+ menu_control = FormattedTextControl(text="")
283
+ preview_control = FormattedTextControl(text="")
284
+
285
+ def update_display():
286
+ """Update both panels."""
287
+ menu_control.text = _render_menu_panel(
288
+ entries, current_page[0], selected_idx[0], current_agent_name
289
+ )
290
+ preview_control.text = _render_preview_panel(
291
+ get_current_entry(), current_agent_name
292
+ )
293
+
294
+ menu_window = Window(
295
+ content=menu_control, wrap_lines=False, width=Dimension(weight=35)
296
+ )
297
+ preview_window = Window(
298
+ content=preview_control, wrap_lines=False, width=Dimension(weight=65)
299
+ )
300
+
301
+ menu_frame = Frame(menu_window, width=Dimension(weight=35), title="Agents")
302
+ preview_frame = Frame(preview_window, width=Dimension(weight=65), title="Preview")
303
+
304
+ root_container = VSplit(
305
+ [
306
+ menu_frame,
307
+ preview_frame,
308
+ ]
309
+ )
310
+
311
+ # Key bindings
312
+ kb = KeyBindings()
313
+
314
+ @kb.add("up")
315
+ def _(event):
316
+ if selected_idx[0] > 0:
317
+ selected_idx[0] -= 1
318
+ # Update page if needed
319
+ current_page[0] = selected_idx[0] // PAGE_SIZE
320
+ update_display()
321
+
322
+ @kb.add("down")
323
+ def _(event):
324
+ if selected_idx[0] < len(entries) - 1:
325
+ selected_idx[0] += 1
326
+ # Update page if needed
327
+ current_page[0] = selected_idx[0] // PAGE_SIZE
328
+ update_display()
329
+
330
+ @kb.add("left")
331
+ def _(event):
332
+ if current_page[0] > 0:
333
+ current_page[0] -= 1
334
+ selected_idx[0] = current_page[0] * PAGE_SIZE
335
+ update_display()
336
+
337
+ @kb.add("right")
338
+ def _(event):
339
+ if current_page[0] < total_pages - 1:
340
+ current_page[0] += 1
341
+ selected_idx[0] = current_page[0] * PAGE_SIZE
342
+ update_display()
343
+
344
+ @kb.add("enter")
345
+ def _(event):
346
+ entry = get_current_entry()
347
+ if entry:
348
+ result[0] = entry[0] # Store agent name
349
+ event.app.exit()
350
+
351
+ @kb.add("c-c")
352
+ def _(event):
353
+ result[0] = None
354
+ event.app.exit()
355
+
356
+ layout = Layout(root_container)
357
+ app = Application(
358
+ layout=layout,
359
+ key_bindings=kb,
360
+ full_screen=False,
361
+ mouse_support=False,
362
+ )
363
+
364
+ set_awaiting_user_input(True)
365
+
366
+ # Enter alternate screen buffer once for entire session
367
+ sys.stdout.write("\033[?1049h") # Enter alternate buffer
368
+ sys.stdout.write("\033[2J\033[H") # Clear and home
369
+ sys.stdout.flush()
370
+ time.sleep(0.05)
371
+
372
+ try:
373
+ # Initial display
374
+ update_display()
375
+
376
+ # Clear the current buffer
377
+ sys.stdout.write("\033[2J\033[H")
378
+ sys.stdout.flush()
379
+
380
+ # Run application
381
+ await app.run_async()
382
+
383
+ finally:
384
+ # Exit alternate screen buffer once at end
385
+ sys.stdout.write("\033[?1049l") # Exit alternate buffer
386
+ sys.stdout.flush()
387
+ # Reset awaiting input flag
388
+ set_awaiting_user_input(False)
389
+
390
+ # Clear exit message
391
+ from code_puppy.messaging import emit_info
392
+
393
+ emit_info("✓ Exited agent picker")
394
+
395
+ return result[0]