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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (231) hide show
  1. code_puppy/__init__.py +7 -1
  2. code_puppy/agents/__init__.py +2 -0
  3. code_puppy/agents/agent_c_reviewer.py +59 -6
  4. code_puppy/agents/agent_code_puppy.py +7 -1
  5. code_puppy/agents/agent_code_reviewer.py +12 -2
  6. code_puppy/agents/agent_cpp_reviewer.py +73 -6
  7. code_puppy/agents/agent_creator_agent.py +45 -4
  8. code_puppy/agents/agent_golang_reviewer.py +92 -3
  9. code_puppy/agents/agent_javascript_reviewer.py +101 -8
  10. code_puppy/agents/agent_manager.py +81 -4
  11. code_puppy/agents/agent_pack_leader.py +383 -0
  12. code_puppy/agents/agent_planning.py +163 -0
  13. code_puppy/agents/agent_python_programmer.py +165 -0
  14. code_puppy/agents/agent_python_reviewer.py +28 -6
  15. code_puppy/agents/agent_qa_expert.py +98 -6
  16. code_puppy/agents/agent_qa_kitten.py +12 -7
  17. code_puppy/agents/agent_security_auditor.py +113 -3
  18. code_puppy/agents/agent_terminal_qa.py +323 -0
  19. code_puppy/agents/agent_typescript_reviewer.py +106 -7
  20. code_puppy/agents/base_agent.py +802 -176
  21. code_puppy/agents/event_stream_handler.py +350 -0
  22. code_puppy/agents/pack/__init__.py +34 -0
  23. code_puppy/agents/pack/bloodhound.py +304 -0
  24. code_puppy/agents/pack/husky.py +321 -0
  25. code_puppy/agents/pack/retriever.py +393 -0
  26. code_puppy/agents/pack/shepherd.py +348 -0
  27. code_puppy/agents/pack/terrier.py +287 -0
  28. code_puppy/agents/pack/watchdog.py +367 -0
  29. code_puppy/agents/prompt_reviewer.py +145 -0
  30. code_puppy/agents/subagent_stream_handler.py +276 -0
  31. code_puppy/api/__init__.py +13 -0
  32. code_puppy/api/app.py +169 -0
  33. code_puppy/api/main.py +21 -0
  34. code_puppy/api/pty_manager.py +446 -0
  35. code_puppy/api/routers/__init__.py +12 -0
  36. code_puppy/api/routers/agents.py +36 -0
  37. code_puppy/api/routers/commands.py +217 -0
  38. code_puppy/api/routers/config.py +74 -0
  39. code_puppy/api/routers/sessions.py +232 -0
  40. code_puppy/api/templates/terminal.html +361 -0
  41. code_puppy/api/websocket.py +154 -0
  42. code_puppy/callbacks.py +142 -4
  43. code_puppy/chatgpt_codex_client.py +283 -0
  44. code_puppy/claude_cache_client.py +586 -0
  45. code_puppy/cli_runner.py +916 -0
  46. code_puppy/command_line/add_model_menu.py +1079 -0
  47. code_puppy/command_line/agent_menu.py +395 -0
  48. code_puppy/command_line/attachments.py +10 -5
  49. code_puppy/command_line/autosave_menu.py +605 -0
  50. code_puppy/command_line/clipboard.py +527 -0
  51. code_puppy/command_line/colors_menu.py +520 -0
  52. code_puppy/command_line/command_handler.py +176 -738
  53. code_puppy/command_line/command_registry.py +150 -0
  54. code_puppy/command_line/config_commands.py +715 -0
  55. code_puppy/command_line/core_commands.py +792 -0
  56. code_puppy/command_line/diff_menu.py +863 -0
  57. code_puppy/command_line/load_context_completion.py +15 -22
  58. code_puppy/command_line/mcp/base.py +0 -3
  59. code_puppy/command_line/mcp/catalog_server_installer.py +175 -0
  60. code_puppy/command_line/mcp/custom_server_form.py +688 -0
  61. code_puppy/command_line/mcp/custom_server_installer.py +195 -0
  62. code_puppy/command_line/mcp/edit_command.py +148 -0
  63. code_puppy/command_line/mcp/handler.py +9 -4
  64. code_puppy/command_line/mcp/help_command.py +6 -5
  65. code_puppy/command_line/mcp/install_command.py +15 -26
  66. code_puppy/command_line/mcp/install_menu.py +685 -0
  67. code_puppy/command_line/mcp/list_command.py +2 -2
  68. code_puppy/command_line/mcp/logs_command.py +174 -65
  69. code_puppy/command_line/mcp/remove_command.py +2 -2
  70. code_puppy/command_line/mcp/restart_command.py +12 -4
  71. code_puppy/command_line/mcp/search_command.py +16 -10
  72. code_puppy/command_line/mcp/start_all_command.py +18 -6
  73. code_puppy/command_line/mcp/start_command.py +47 -25
  74. code_puppy/command_line/mcp/status_command.py +4 -5
  75. code_puppy/command_line/mcp/stop_all_command.py +7 -1
  76. code_puppy/command_line/mcp/stop_command.py +8 -4
  77. code_puppy/command_line/mcp/test_command.py +2 -2
  78. code_puppy/command_line/mcp/wizard_utils.py +20 -16
  79. code_puppy/command_line/mcp_completion.py +174 -0
  80. code_puppy/command_line/model_picker_completion.py +75 -25
  81. code_puppy/command_line/model_settings_menu.py +884 -0
  82. code_puppy/command_line/motd.py +14 -8
  83. code_puppy/command_line/onboarding_slides.py +179 -0
  84. code_puppy/command_line/onboarding_wizard.py +340 -0
  85. code_puppy/command_line/pin_command_completion.py +329 -0
  86. code_puppy/command_line/prompt_toolkit_completion.py +463 -63
  87. code_puppy/command_line/session_commands.py +296 -0
  88. code_puppy/command_line/utils.py +54 -0
  89. code_puppy/config.py +898 -112
  90. code_puppy/error_logging.py +118 -0
  91. code_puppy/gemini_code_assist.py +385 -0
  92. code_puppy/gemini_model.py +602 -0
  93. code_puppy/http_utils.py +210 -148
  94. code_puppy/keymap.py +128 -0
  95. code_puppy/main.py +5 -698
  96. code_puppy/mcp_/__init__.py +17 -0
  97. code_puppy/mcp_/async_lifecycle.py +35 -4
  98. code_puppy/mcp_/blocking_startup.py +70 -43
  99. code_puppy/mcp_/captured_stdio_server.py +2 -2
  100. code_puppy/mcp_/config_wizard.py +4 -4
  101. code_puppy/mcp_/dashboard.py +15 -6
  102. code_puppy/mcp_/managed_server.py +65 -38
  103. code_puppy/mcp_/manager.py +146 -52
  104. code_puppy/mcp_/mcp_logs.py +224 -0
  105. code_puppy/mcp_/registry.py +6 -6
  106. code_puppy/mcp_/server_registry_catalog.py +24 -5
  107. code_puppy/messaging/__init__.py +199 -2
  108. code_puppy/messaging/bus.py +610 -0
  109. code_puppy/messaging/commands.py +167 -0
  110. code_puppy/messaging/markdown_patches.py +57 -0
  111. code_puppy/messaging/message_queue.py +17 -48
  112. code_puppy/messaging/messages.py +500 -0
  113. code_puppy/messaging/queue_console.py +1 -24
  114. code_puppy/messaging/renderers.py +43 -146
  115. code_puppy/messaging/rich_renderer.py +1027 -0
  116. code_puppy/messaging/spinner/__init__.py +21 -5
  117. code_puppy/messaging/spinner/console_spinner.py +86 -51
  118. code_puppy/messaging/subagent_console.py +461 -0
  119. code_puppy/model_factory.py +634 -83
  120. code_puppy/model_utils.py +167 -0
  121. code_puppy/models.json +66 -68
  122. code_puppy/models_dev_api.json +1 -0
  123. code_puppy/models_dev_parser.py +592 -0
  124. code_puppy/plugins/__init__.py +164 -10
  125. code_puppy/plugins/antigravity_oauth/__init__.py +10 -0
  126. code_puppy/plugins/antigravity_oauth/accounts.py +406 -0
  127. code_puppy/plugins/antigravity_oauth/antigravity_model.py +704 -0
  128. code_puppy/plugins/antigravity_oauth/config.py +42 -0
  129. code_puppy/plugins/antigravity_oauth/constants.py +136 -0
  130. code_puppy/plugins/antigravity_oauth/oauth.py +478 -0
  131. code_puppy/plugins/antigravity_oauth/register_callbacks.py +406 -0
  132. code_puppy/plugins/antigravity_oauth/storage.py +271 -0
  133. code_puppy/plugins/antigravity_oauth/test_plugin.py +319 -0
  134. code_puppy/plugins/antigravity_oauth/token.py +167 -0
  135. code_puppy/plugins/antigravity_oauth/transport.py +767 -0
  136. code_puppy/plugins/antigravity_oauth/utils.py +169 -0
  137. code_puppy/plugins/chatgpt_oauth/__init__.py +8 -0
  138. code_puppy/plugins/chatgpt_oauth/config.py +52 -0
  139. code_puppy/plugins/chatgpt_oauth/oauth_flow.py +328 -0
  140. code_puppy/plugins/chatgpt_oauth/register_callbacks.py +94 -0
  141. code_puppy/plugins/chatgpt_oauth/test_plugin.py +293 -0
  142. code_puppy/plugins/chatgpt_oauth/utils.py +489 -0
  143. code_puppy/plugins/claude_code_oauth/README.md +167 -0
  144. code_puppy/plugins/claude_code_oauth/SETUP.md +93 -0
  145. code_puppy/plugins/claude_code_oauth/__init__.py +6 -0
  146. code_puppy/plugins/claude_code_oauth/config.py +50 -0
  147. code_puppy/plugins/claude_code_oauth/register_callbacks.py +308 -0
  148. code_puppy/plugins/claude_code_oauth/test_plugin.py +283 -0
  149. code_puppy/plugins/claude_code_oauth/utils.py +518 -0
  150. code_puppy/plugins/customizable_commands/__init__.py +0 -0
  151. code_puppy/plugins/customizable_commands/register_callbacks.py +169 -0
  152. code_puppy/plugins/example_custom_command/README.md +280 -0
  153. code_puppy/plugins/example_custom_command/register_callbacks.py +2 -2
  154. code_puppy/plugins/file_permission_handler/__init__.py +4 -0
  155. code_puppy/plugins/file_permission_handler/register_callbacks.py +523 -0
  156. code_puppy/plugins/frontend_emitter/__init__.py +25 -0
  157. code_puppy/plugins/frontend_emitter/emitter.py +121 -0
  158. code_puppy/plugins/frontend_emitter/register_callbacks.py +261 -0
  159. code_puppy/plugins/oauth_puppy_html.py +228 -0
  160. code_puppy/plugins/shell_safety/__init__.py +6 -0
  161. code_puppy/plugins/shell_safety/agent_shell_safety.py +69 -0
  162. code_puppy/plugins/shell_safety/command_cache.py +156 -0
  163. code_puppy/plugins/shell_safety/register_callbacks.py +202 -0
  164. code_puppy/prompts/antigravity_system_prompt.md +1 -0
  165. code_puppy/prompts/codex_system_prompt.md +310 -0
  166. code_puppy/pydantic_patches.py +131 -0
  167. code_puppy/reopenable_async_client.py +8 -8
  168. code_puppy/round_robin_model.py +9 -12
  169. code_puppy/session_storage.py +2 -1
  170. code_puppy/status_display.py +21 -4
  171. code_puppy/summarization_agent.py +41 -13
  172. code_puppy/terminal_utils.py +418 -0
  173. code_puppy/tools/__init__.py +37 -1
  174. code_puppy/tools/agent_tools.py +536 -52
  175. code_puppy/tools/browser/__init__.py +37 -0
  176. code_puppy/tools/browser/browser_control.py +19 -23
  177. code_puppy/tools/browser/browser_interactions.py +41 -48
  178. code_puppy/tools/browser/browser_locators.py +36 -38
  179. code_puppy/tools/browser/browser_manager.py +316 -0
  180. code_puppy/tools/browser/browser_navigation.py +16 -16
  181. code_puppy/tools/browser/browser_screenshot.py +79 -143
  182. code_puppy/tools/browser/browser_scripts.py +32 -42
  183. code_puppy/tools/browser/browser_workflows.py +44 -27
  184. code_puppy/tools/browser/chromium_terminal_manager.py +259 -0
  185. code_puppy/tools/browser/terminal_command_tools.py +521 -0
  186. code_puppy/tools/browser/terminal_screenshot_tools.py +556 -0
  187. code_puppy/tools/browser/terminal_tools.py +525 -0
  188. code_puppy/tools/command_runner.py +930 -147
  189. code_puppy/tools/common.py +1113 -5
  190. code_puppy/tools/display.py +84 -0
  191. code_puppy/tools/file_modifications.py +288 -89
  192. code_puppy/tools/file_operations.py +226 -154
  193. code_puppy/tools/subagent_context.py +158 -0
  194. code_puppy/uvx_detection.py +242 -0
  195. code_puppy/version_checker.py +30 -11
  196. code_puppy-0.0.366.data/data/code_puppy/models.json +110 -0
  197. code_puppy-0.0.366.data/data/code_puppy/models_dev_api.json +1 -0
  198. {code_puppy-0.0.214.dist-info → code_puppy-0.0.366.dist-info}/METADATA +149 -75
  199. code_puppy-0.0.366.dist-info/RECORD +217 -0
  200. {code_puppy-0.0.214.dist-info → code_puppy-0.0.366.dist-info}/WHEEL +1 -1
  201. code_puppy/command_line/mcp/add_command.py +0 -183
  202. code_puppy/messaging/spinner/textual_spinner.py +0 -106
  203. code_puppy/tools/browser/camoufox_manager.py +0 -216
  204. code_puppy/tools/browser/vqa_agent.py +0 -70
  205. code_puppy/tui/__init__.py +0 -10
  206. code_puppy/tui/app.py +0 -1105
  207. code_puppy/tui/components/__init__.py +0 -21
  208. code_puppy/tui/components/chat_view.py +0 -551
  209. code_puppy/tui/components/command_history_modal.py +0 -218
  210. code_puppy/tui/components/copy_button.py +0 -139
  211. code_puppy/tui/components/custom_widgets.py +0 -63
  212. code_puppy/tui/components/human_input_modal.py +0 -175
  213. code_puppy/tui/components/input_area.py +0 -167
  214. code_puppy/tui/components/sidebar.py +0 -309
  215. code_puppy/tui/components/status_bar.py +0 -185
  216. code_puppy/tui/messages.py +0 -27
  217. code_puppy/tui/models/__init__.py +0 -8
  218. code_puppy/tui/models/chat_message.py +0 -25
  219. code_puppy/tui/models/command_history.py +0 -89
  220. code_puppy/tui/models/enums.py +0 -24
  221. code_puppy/tui/screens/__init__.py +0 -17
  222. code_puppy/tui/screens/autosave_picker.py +0 -175
  223. code_puppy/tui/screens/help.py +0 -130
  224. code_puppy/tui/screens/mcp_install_wizard.py +0 -803
  225. code_puppy/tui/screens/settings.py +0 -306
  226. code_puppy/tui/screens/tools.py +0 -74
  227. code_puppy/tui_state.py +0 -55
  228. code_puppy-0.0.214.data/data/code_puppy/models.json +0 -112
  229. code_puppy-0.0.214.dist-info/RECORD +0 -131
  230. {code_puppy-0.0.214.dist-info → code_puppy-0.0.366.dist-info}/entry_points.txt +0 -0
  231. {code_puppy-0.0.214.dist-info → code_puppy-0.0.366.dist-info}/licenses/LICENSE +0 -0
@@ -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]
@@ -261,17 +261,22 @@ def _detect_path_tokens(prompt: str) -> tuple[list[_DetectedPath], list[str]]:
261
261
  candidate_path_token = stripped_joined
262
262
  candidate_placeholder = joined
263
263
  consumed_until = end_index
264
+ if len(candidate_path_token) > MAX_PATH_LENGTH:
265
+ continue
264
266
  try:
265
267
  last_path = _normalise_path(candidate_path_token)
266
268
  except AttachmentParsingError:
267
269
  # Suppress warnings for non-file spans; just skip quietly
268
270
  found_span = False
269
271
  break
270
- if last_path.exists() and last_path.is_file():
271
- path = last_path
272
- found_span = True
273
- # We'll rebuild escaped placeholder after this block
274
- break
272
+ try:
273
+ if last_path.exists() and last_path.is_file():
274
+ path = last_path
275
+ found_span = True
276
+ # We'll rebuild escaped placeholder after this block
277
+ break
278
+ except OSError:
279
+ continue
275
280
  if not found_span:
276
281
  # Quietly skip tokens that are not files
277
282
  index += 1