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,685 @@
1
+ """Interactive terminal UI for browsing and installing MCP servers.
2
+
3
+ Provides a beautiful split-panel interface for browsing categories and servers
4
+ with live preview of server details and one-click installation.
5
+ """
6
+
7
+ import logging
8
+ import os
9
+ import sys
10
+ import time
11
+ from typing import List, Optional
12
+
13
+ from prompt_toolkit.application import Application
14
+ from prompt_toolkit.key_binding import KeyBindings
15
+ from prompt_toolkit.layout import Dimension, Layout, VSplit, Window
16
+ from prompt_toolkit.layout.controls import FormattedTextControl
17
+ from prompt_toolkit.widgets import Frame
18
+
19
+ from code_puppy.messaging import emit_error, emit_info, emit_warning
20
+ from code_puppy.tools.command_runner import set_awaiting_user_input
21
+
22
+ from .catalog_server_installer import (
23
+ install_catalog_server,
24
+ prompt_for_server_config,
25
+ )
26
+ from .custom_server_form import run_custom_server_form
27
+
28
+ logger = logging.getLogger(__name__)
29
+
30
+ PAGE_SIZE = 12 # Items per page
31
+
32
+ # Special category for custom servers
33
+ CUSTOM_SERVER_CATEGORY = "➕ Custom Server"
34
+
35
+
36
+ class MCPInstallMenu:
37
+ """Interactive TUI for browsing and installing MCP servers."""
38
+
39
+ def __init__(self, manager):
40
+ """Initialize the MCP server browser menu.
41
+
42
+ Args:
43
+ manager: MCP manager instance for server installation
44
+ """
45
+ self.manager = manager
46
+ self.catalog = None
47
+ self.categories: List[str] = []
48
+ self.current_category: Optional[str] = None
49
+ self.current_servers: List = []
50
+
51
+ # State management
52
+ self.view_mode = "categories" # "categories" or "servers"
53
+ self.selected_category_idx = 0
54
+ self.selected_server_idx = 0
55
+ self.current_page = 0
56
+ self.result = None # Track installation result
57
+
58
+ # Pending server for configuration
59
+ self.pending_server = None
60
+
61
+ # UI controls
62
+ self.menu_control = None
63
+ self.preview_control = None
64
+
65
+ # Initialize catalog
66
+ self._initialize_catalog()
67
+
68
+ def _initialize_catalog(self):
69
+ """Initialize the MCP server catalog with error handling."""
70
+ try:
71
+ from code_puppy.mcp_.server_registry_catalog import catalog
72
+
73
+ self.catalog = catalog
74
+ # Add custom server option as first category
75
+ self.categories = [CUSTOM_SERVER_CATEGORY] + self.catalog.list_categories()
76
+ if len(self.categories) <= 1: # Only custom category
77
+ emit_error("No categories found in server catalog")
78
+ except ImportError as e:
79
+ emit_error(f"Server catalog not available: {e}")
80
+ # Still allow custom servers even if catalog fails
81
+ self.categories = [CUSTOM_SERVER_CATEGORY]
82
+ except Exception as e:
83
+ emit_error(f"Error loading server catalog: {e}")
84
+ self.categories = [CUSTOM_SERVER_CATEGORY]
85
+
86
+ def _get_current_category(self) -> Optional[str]:
87
+ """Get the currently selected category."""
88
+ if 0 <= self.selected_category_idx < len(self.categories):
89
+ return self.categories[self.selected_category_idx]
90
+ return None
91
+
92
+ def _get_current_server(self):
93
+ """Get the currently selected server."""
94
+ if self.view_mode == "servers" and self.current_servers:
95
+ if 0 <= self.selected_server_idx < len(self.current_servers):
96
+ return self.current_servers[self.selected_server_idx]
97
+ return None
98
+
99
+ def _get_category_icon(self, category: str) -> str:
100
+ """Get an icon for a category."""
101
+ if category == CUSTOM_SERVER_CATEGORY:
102
+ return "➕"
103
+ icons = {
104
+ "Code": "💻",
105
+ "Storage": "💾",
106
+ "Database": "🗄️",
107
+ "Documentation": "📝",
108
+ "DevOps": "🔧",
109
+ "Monitoring": "📊",
110
+ "Package Management": "📦",
111
+ "Communication": "💬",
112
+ "AI": "🤖",
113
+ "Search": "🔍",
114
+ "Development": "🛠️",
115
+ "Cloud": "☁️",
116
+ }
117
+ return icons.get(category, "📁")
118
+
119
+ def _is_custom_server_selected(self) -> bool:
120
+ """Check if the custom server category is selected."""
121
+ return (
122
+ self.view_mode == "categories"
123
+ and self.selected_category_idx == 0
124
+ and len(self.categories) > 0
125
+ and self.categories[0] == CUSTOM_SERVER_CATEGORY
126
+ )
127
+
128
+ def _render_category_list(self) -> List:
129
+ """Render the category list panel."""
130
+ lines = []
131
+
132
+ lines.append(("bold cyan", " 📂 CATEGORIES"))
133
+ lines.append(("", "\n\n"))
134
+
135
+ if not self.categories:
136
+ lines.append(("fg:yellow", " No categories available."))
137
+ lines.append(("", "\n\n"))
138
+ self._render_navigation_hints(lines)
139
+ return lines
140
+
141
+ # Show categories for current page
142
+ total_pages = (len(self.categories) + PAGE_SIZE - 1) // PAGE_SIZE
143
+ start_idx = self.current_page * PAGE_SIZE
144
+ end_idx = min(start_idx + PAGE_SIZE, len(self.categories))
145
+
146
+ for i in range(start_idx, end_idx):
147
+ category = self.categories[i]
148
+ is_selected = i == self.selected_category_idx
149
+ icon = self._get_category_icon(category)
150
+
151
+ prefix = " > " if is_selected else " "
152
+
153
+ # Custom server category doesn't have a count
154
+ if category == CUSTOM_SERVER_CATEGORY:
155
+ label = f"{prefix}{icon} Custom Server (JSON)"
156
+ if is_selected:
157
+ lines.append(("fg:ansibrightgreen bold", label))
158
+ else:
159
+ lines.append(("fg:ansigreen", label))
160
+ else:
161
+ # Count servers in category
162
+ server_count = (
163
+ len(self.catalog.get_by_category(category)) if self.catalog else 0
164
+ )
165
+ label = f"{prefix}{icon} {category} ({server_count})"
166
+ if is_selected:
167
+ lines.append(("fg:ansibrightcyan bold", label))
168
+ else:
169
+ lines.append(("fg:ansibrightblack", label))
170
+
171
+ lines.append(("", "\n"))
172
+
173
+ lines.append(("", "\n"))
174
+ if total_pages > 1:
175
+ lines.append(
176
+ ("fg:ansibrightblack", f" Page {self.current_page + 1}/{total_pages}")
177
+ )
178
+ lines.append(("", "\n"))
179
+
180
+ self._render_navigation_hints(lines)
181
+ return lines
182
+
183
+ def _render_server_list(self) -> List:
184
+ """Render the server list panel."""
185
+ lines = []
186
+
187
+ if not self.current_category:
188
+ lines.append(("fg:yellow", " No category selected."))
189
+ lines.append(("", "\n\n"))
190
+ self._render_navigation_hints(lines)
191
+ return lines
192
+
193
+ icon = self._get_category_icon(self.current_category)
194
+ lines.append(("bold cyan", f" {icon} {self.current_category.upper()}"))
195
+ lines.append(("", "\n\n"))
196
+
197
+ if not self.current_servers:
198
+ lines.append(("fg:yellow", " No servers in this category."))
199
+ lines.append(("", "\n\n"))
200
+ self._render_navigation_hints(lines)
201
+ return lines
202
+
203
+ # Show servers for current page
204
+ total_pages = (len(self.current_servers) + PAGE_SIZE - 1) // PAGE_SIZE
205
+ start_idx = self.current_page * PAGE_SIZE
206
+ end_idx = min(start_idx + PAGE_SIZE, len(self.current_servers))
207
+
208
+ for i in range(start_idx, end_idx):
209
+ server = self.current_servers[i]
210
+ is_selected = i == self.selected_server_idx
211
+
212
+ # Create indicator icons
213
+ icons = []
214
+ if server.verified:
215
+ icons.append("✓")
216
+ if server.popular:
217
+ icons.append("⭐")
218
+
219
+ icon_str = " ".join(icons) + " " if icons else ""
220
+
221
+ prefix = " > " if is_selected else " "
222
+ label = f"{prefix}{icon_str}{server.display_name}"
223
+
224
+ if is_selected:
225
+ lines.append(("fg:ansibrightcyan bold", label))
226
+ else:
227
+ lines.append(("fg:ansibrightblack", label))
228
+
229
+ lines.append(("", "\n"))
230
+
231
+ lines.append(("", "\n"))
232
+ if total_pages > 1:
233
+ lines.append(
234
+ ("fg:ansibrightblack", f" Page {self.current_page + 1}/{total_pages}")
235
+ )
236
+ lines.append(("", "\n"))
237
+
238
+ self._render_navigation_hints(lines)
239
+ return lines
240
+
241
+ def _render_navigation_hints(self, lines: List):
242
+ """Render navigation hints at the bottom of the list panel."""
243
+ lines.append(("", "\n"))
244
+ lines.append(("fg:ansibrightblack", " ↑/↓ "))
245
+ lines.append(("", "Navigate "))
246
+ lines.append(("fg:ansibrightblack", "←/→ "))
247
+ lines.append(("", "Page\n"))
248
+ if self.view_mode == "categories":
249
+ lines.append(("fg:green", " Enter "))
250
+ lines.append(("", "Browse Servers\n"))
251
+ else:
252
+ lines.append(("fg:green", " Enter "))
253
+ lines.append(("", "Install Server\n"))
254
+ lines.append(("fg:ansibrightblack", " Esc/Back "))
255
+ lines.append(("", "Back\n"))
256
+ lines.append(("fg:ansired", " Ctrl+C "))
257
+ lines.append(("", "Cancel"))
258
+
259
+ def _render_details(self) -> List:
260
+ """Render the details panel."""
261
+ lines = []
262
+
263
+ lines.append(("bold cyan", " 📋 DETAILS"))
264
+ lines.append(("", "\n\n"))
265
+
266
+ if self.view_mode == "categories":
267
+ category = self._get_current_category()
268
+ if not category:
269
+ lines.append(("fg:yellow", " No category selected."))
270
+ return lines
271
+
272
+ # Special handling for custom server category
273
+ if category == CUSTOM_SERVER_CATEGORY:
274
+ return self._render_custom_server_details()
275
+
276
+ icon = self._get_category_icon(category)
277
+ lines.append(("bold", f" {icon} {category}"))
278
+ lines.append(("", "\n\n"))
279
+
280
+ # Show servers in this category
281
+ servers = self.catalog.get_by_category(category) if self.catalog else []
282
+ lines.append(("fg:ansibrightblack", f" {len(servers)} servers available"))
283
+ lines.append(("", "\n\n"))
284
+
285
+ # Show popular servers in this category
286
+ popular = [s for s in servers if s.popular]
287
+ if popular:
288
+ lines.append(("bold", " ⭐ Popular:"))
289
+ lines.append(("", "\n"))
290
+ for server in popular[:5]:
291
+ lines.append(("fg:ansibrightblack", f" • {server.display_name}"))
292
+ lines.append(("", "\n"))
293
+
294
+ else: # servers view
295
+ server = self._get_current_server()
296
+ if not server:
297
+ lines.append(("fg:yellow", " No server selected."))
298
+ return lines
299
+
300
+ # Server name with indicators
301
+ indicators = []
302
+ if server.verified:
303
+ indicators.append("✓ Verified")
304
+ if server.popular:
305
+ indicators.append("⭐ Popular")
306
+
307
+ lines.append(("bold", f" {server.display_name}"))
308
+ lines.append(("", "\n"))
309
+
310
+ if indicators:
311
+ lines.append(("fg:green", f" {' | '.join(indicators)}"))
312
+ lines.append(("", "\n"))
313
+
314
+ lines.append(("", "\n"))
315
+
316
+ # Description
317
+ lines.append(("bold", " Description:"))
318
+ lines.append(("", "\n"))
319
+ # Wrap description
320
+ desc = server.description or "No description available"
321
+ # Simple word wrap
322
+ words = desc.split()
323
+ line = " "
324
+ for word in words:
325
+ if len(line) + len(word) > 50:
326
+ lines.append(("fg:ansibrightblack", line))
327
+ lines.append(("", "\n"))
328
+ line = " " + word + " "
329
+ else:
330
+ line += word + " "
331
+ if line.strip():
332
+ lines.append(("fg:ansibrightblack", line))
333
+ lines.append(("", "\n"))
334
+
335
+ lines.append(("", "\n"))
336
+
337
+ # Type
338
+ lines.append(("bold", " Type:"))
339
+ lines.append(("", "\n"))
340
+ type_icons = {"stdio": "📟", "http": "🌐", "sse": "📡"}
341
+ type_icon = type_icons.get(server.type, "❓")
342
+ lines.append(("fg:ansibrightblack", f" {type_icon} {server.type}"))
343
+ lines.append(("", "\n\n"))
344
+
345
+ # Tags
346
+ if server.tags:
347
+ lines.append(("bold", " Tags:"))
348
+ lines.append(("", "\n"))
349
+ tag_line = " " + ", ".join(server.tags[:6])
350
+ lines.append(("fg:ansicyan", tag_line))
351
+ lines.append(("", "\n\n"))
352
+
353
+ # Requirements
354
+ requirements = server.get_requirements()
355
+
356
+ # Environment variables
357
+ env_vars = server.get_environment_vars()
358
+ if env_vars:
359
+ lines.append(("bold", " 🔑 Environment Variables:"))
360
+ lines.append(("", "\n"))
361
+ for var in env_vars:
362
+ # Check if already set
363
+ is_set = os.environ.get(var)
364
+ if is_set:
365
+ lines.append(("fg:green", f" ✓ {var}"))
366
+ else:
367
+ lines.append(("fg:yellow", f" ○ {var}"))
368
+ lines.append(("", "\n"))
369
+ lines.append(("", "\n"))
370
+
371
+ # Command line args
372
+ cmd_args = server.get_command_line_args()
373
+ if cmd_args:
374
+ lines.append(("bold", " ⚙️ Configuration:"))
375
+ lines.append(("", "\n"))
376
+ for arg in cmd_args:
377
+ name = arg.get("name", "unknown")
378
+ required = arg.get("required", True)
379
+ default = arg.get("default", "")
380
+ marker = "*" if required else "?"
381
+ default_str = f" [{default}]" if default else ""
382
+ lines.append(
383
+ ("fg:ansibrightblack", f" {marker} {name}{default_str}")
384
+ )
385
+ lines.append(("", "\n"))
386
+ lines.append(("", "\n"))
387
+
388
+ # Required tools
389
+ required_tools = requirements.required_tools
390
+ if required_tools:
391
+ lines.append(("bold", " 🛠️ Required Tools:"))
392
+ lines.append(("", "\n"))
393
+ lines.append(("fg:ansibrightblack", f" {', '.join(required_tools)}"))
394
+ lines.append(("", "\n\n"))
395
+
396
+ # Example usage
397
+ if server.example_usage:
398
+ lines.append(("bold", " 💡 Example:"))
399
+ lines.append(("", "\n"))
400
+ lines.append(("fg:ansibrightblack", f" {server.example_usage}"))
401
+ lines.append(("", "\n"))
402
+
403
+ return lines
404
+
405
+ def _render_custom_server_details(self) -> List:
406
+ """Render details for the custom server option."""
407
+ lines = []
408
+
409
+ lines.append(("bold cyan", " 📋 DETAILS"))
410
+ lines.append(("", "\n\n"))
411
+
412
+ lines.append(("bold green", " ➕ Add Custom MCP Server"))
413
+ lines.append(("", "\n\n"))
414
+
415
+ lines.append(("fg:ansibrightblack", " Add your own MCP server by providing"))
416
+ lines.append(("", "\n"))
417
+ lines.append(("fg:ansibrightblack", " a JSON configuration."))
418
+ lines.append(("", "\n\n"))
419
+
420
+ lines.append(("bold", " 📟 Supported Types:"))
421
+ lines.append(("", "\n\n"))
422
+
423
+ lines.append(("fg:ansicyan bold", " 1. stdio"))
424
+ lines.append(("", "\n"))
425
+ lines.append(("fg:ansibrightblack", " Runs a local command (npx, python,"))
426
+ lines.append(("", "\n"))
427
+ lines.append(("fg:ansibrightblack", " uvx, etc.) and communicates via"))
428
+ lines.append(("", "\n"))
429
+ lines.append(("fg:ansibrightblack", " stdin/stdout."))
430
+ lines.append(("", "\n\n"))
431
+
432
+ lines.append(("fg:ansicyan bold", " 2. http"))
433
+ lines.append(("", "\n"))
434
+ lines.append(("fg:ansibrightblack", " Connects to an HTTP endpoint that"))
435
+ lines.append(("", "\n"))
436
+ lines.append(("fg:ansibrightblack", " implements the MCP protocol."))
437
+ lines.append(("", "\n\n"))
438
+
439
+ lines.append(("fg:ansicyan bold", " 3. sse"))
440
+ lines.append(("", "\n"))
441
+ lines.append(("fg:ansibrightblack", " Connects via Server-Sent Events"))
442
+ lines.append(("", "\n"))
443
+ lines.append(("fg:ansibrightblack", " for real-time streaming."))
444
+ lines.append(("", "\n\n"))
445
+
446
+ lines.append(("bold", " 💡 Press Enter to configure"))
447
+ lines.append(("", "\n"))
448
+
449
+ return lines
450
+
451
+ def update_display(self):
452
+ """Update the display based on current state."""
453
+ if self.view_mode == "categories":
454
+ self.menu_control.text = self._render_category_list()
455
+ else:
456
+ self.menu_control.text = self._render_server_list()
457
+
458
+ self.preview_control.text = self._render_details()
459
+
460
+ def _enter_category(self):
461
+ """Enter the selected category to view its servers."""
462
+ category = self._get_current_category()
463
+ if not category:
464
+ return
465
+
466
+ # Handle custom server selection
467
+ if category == CUSTOM_SERVER_CATEGORY:
468
+ self.result = "pending_custom"
469
+ return # Signal to exit and prompt for custom config
470
+
471
+ if not self.catalog:
472
+ return
473
+
474
+ self.current_category = category
475
+ self.current_servers = self.catalog.get_by_category(category)
476
+ self.view_mode = "servers"
477
+ self.selected_server_idx = 0
478
+ self.current_page = 0
479
+ self.update_display()
480
+
481
+ def _go_back_to_categories(self):
482
+ """Go back to categories view."""
483
+ self.view_mode = "categories"
484
+ self.current_category = None
485
+ self.current_servers = []
486
+ self.selected_server_idx = 0
487
+ self.current_page = 0
488
+ self.update_display()
489
+
490
+ def _select_current_server(self):
491
+ """Select the current server for installation."""
492
+ server = self._get_current_server()
493
+ if server:
494
+ self.pending_server = server
495
+ self.result = "pending_install"
496
+
497
+ def run(self) -> bool:
498
+ """Run the interactive MCP server browser (synchronous).
499
+
500
+ Returns:
501
+ True if a server was installed, False otherwise
502
+ """
503
+ if not self.categories:
504
+ emit_warning("No MCP server catalog available.")
505
+ return False
506
+
507
+ # Build UI
508
+ self.menu_control = FormattedTextControl(text="")
509
+ self.preview_control = FormattedTextControl(text="")
510
+
511
+ menu_window = Window(
512
+ content=self.menu_control, wrap_lines=True, width=Dimension(weight=35)
513
+ )
514
+ preview_window = Window(
515
+ content=self.preview_control, wrap_lines=True, width=Dimension(weight=65)
516
+ )
517
+
518
+ menu_frame = Frame(menu_window, width=Dimension(weight=35), title="Browse")
519
+ preview_frame = Frame(
520
+ preview_window, width=Dimension(weight=65), title="Details"
521
+ )
522
+
523
+ root_container = VSplit([menu_frame, preview_frame])
524
+
525
+ # Key bindings
526
+ kb = KeyBindings()
527
+
528
+ @kb.add("up")
529
+ def _(event):
530
+ if self.view_mode == "categories":
531
+ if self.selected_category_idx > 0:
532
+ self.selected_category_idx -= 1
533
+ self.current_page = self.selected_category_idx // PAGE_SIZE
534
+ else: # servers view
535
+ if self.selected_server_idx > 0:
536
+ self.selected_server_idx -= 1
537
+ self.current_page = self.selected_server_idx // PAGE_SIZE
538
+ self.update_display()
539
+
540
+ @kb.add("down")
541
+ def _(event):
542
+ if self.view_mode == "categories":
543
+ if self.selected_category_idx < len(self.categories) - 1:
544
+ self.selected_category_idx += 1
545
+ self.current_page = self.selected_category_idx // PAGE_SIZE
546
+ else: # servers view
547
+ if self.selected_server_idx < len(self.current_servers) - 1:
548
+ self.selected_server_idx += 1
549
+ self.current_page = self.selected_server_idx // PAGE_SIZE
550
+ self.update_display()
551
+
552
+ @kb.add("left")
553
+ def _(event):
554
+ """Previous page."""
555
+ if self.current_page > 0:
556
+ self.current_page -= 1
557
+ if self.view_mode == "categories":
558
+ self.selected_category_idx = self.current_page * PAGE_SIZE
559
+ else:
560
+ self.selected_server_idx = self.current_page * PAGE_SIZE
561
+ self.update_display()
562
+
563
+ @kb.add("right")
564
+ def _(event):
565
+ """Next page."""
566
+ if self.view_mode == "categories":
567
+ total_items = len(self.categories)
568
+ else:
569
+ total_items = len(self.current_servers)
570
+
571
+ total_pages = (total_items + PAGE_SIZE - 1) // PAGE_SIZE
572
+ if self.current_page < total_pages - 1:
573
+ self.current_page += 1
574
+ if self.view_mode == "categories":
575
+ self.selected_category_idx = self.current_page * PAGE_SIZE
576
+ else:
577
+ self.selected_server_idx = self.current_page * PAGE_SIZE
578
+ self.update_display()
579
+
580
+ @kb.add("enter")
581
+ def _(event):
582
+ if self.view_mode == "categories":
583
+ self._enter_category()
584
+ # Exit if custom server was selected
585
+ if self.result == "pending_custom":
586
+ event.app.exit()
587
+ elif self.view_mode == "servers":
588
+ self._select_current_server()
589
+ event.app.exit()
590
+
591
+ @kb.add("escape")
592
+ def _(event):
593
+ if self.view_mode == "servers":
594
+ self._go_back_to_categories()
595
+
596
+ @kb.add("backspace")
597
+ def _(event):
598
+ if self.view_mode == "servers":
599
+ self._go_back_to_categories()
600
+
601
+ @kb.add("c-c")
602
+ def _(event):
603
+ event.app.exit()
604
+
605
+ layout = Layout(root_container)
606
+ app = Application(
607
+ layout=layout,
608
+ key_bindings=kb,
609
+ full_screen=False,
610
+ mouse_support=False,
611
+ )
612
+
613
+ set_awaiting_user_input(True)
614
+
615
+ # Enter alternate screen buffer
616
+ sys.stdout.write("\033[?1049h") # Enter alternate buffer
617
+ sys.stdout.write("\033[2J\033[H") # Clear and home
618
+ sys.stdout.flush()
619
+ time.sleep(0.05)
620
+
621
+ try:
622
+ # Initial display
623
+ self.update_display()
624
+
625
+ # Clear the current buffer
626
+ sys.stdout.write("\033[2J\033[H")
627
+ sys.stdout.flush()
628
+
629
+ # Run application
630
+ app.run(in_thread=True)
631
+
632
+ finally:
633
+ # Exit alternate screen buffer
634
+ sys.stdout.write("\033[?1049l")
635
+ sys.stdout.flush()
636
+ set_awaiting_user_input(False)
637
+
638
+ # Clear exit message (unless we're about to prompt for more input)
639
+ if self.result not in ("pending_custom", "pending_install"):
640
+ emit_info("✓ Exited MCP server browser")
641
+
642
+ # Handle custom server after TUI exits
643
+ if self.result == "pending_custom":
644
+ success = run_custom_server_form(self.manager)
645
+ if success:
646
+ try:
647
+ from code_puppy.agent import reload_mcp_servers
648
+
649
+ reload_mcp_servers()
650
+ except ImportError:
651
+ pass
652
+ return success
653
+
654
+ # Handle catalog server installation after TUI exits
655
+ if self.result == "pending_install" and self.pending_server:
656
+ config = prompt_for_server_config(self.manager, self.pending_server)
657
+ if config:
658
+ success = install_catalog_server(
659
+ self.manager, self.pending_server, config
660
+ )
661
+ if success:
662
+ # Reload MCP servers
663
+ try:
664
+ from code_puppy.agent import reload_mcp_servers
665
+
666
+ reload_mcp_servers()
667
+ except ImportError:
668
+ pass
669
+ return success
670
+ return False
671
+
672
+ return False
673
+
674
+
675
+ def run_mcp_install_menu(manager) -> bool:
676
+ """Run the MCP install menu.
677
+
678
+ Args:
679
+ manager: MCP manager instance
680
+
681
+ Returns:
682
+ True if a server was installed, False otherwise
683
+ """
684
+ menu = MCPInstallMenu(manager)
685
+ return menu.run()
@@ -9,7 +9,7 @@ from rich.table import Table
9
9
  from rich.text import Text
10
10
 
11
11
  from code_puppy.mcp_.managed_server import ServerState
12
- from code_puppy.messaging import emit_info
12
+ from code_puppy.messaging import emit_error, emit_info
13
13
 
14
14
  from .base import MCPCommandBase
15
15
  from .utils import format_state_indicator, format_uptime
@@ -91,4 +91,4 @@ class ListCommand(MCPCommandBase):
91
91
 
92
92
  except Exception as e:
93
93
  logger.error(f"Error listing MCP servers: {e}")
94
- emit_info(f"[red]Error listing servers: {e}[/red]", message_group=group_id)
94
+ emit_error(f"Error listing servers: {e}", message_group=group_id)