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
code_puppy/tui/app.py DELETED
@@ -1,986 +0,0 @@
1
- """
2
- Main TUI application class.
3
- """
4
-
5
- from datetime import datetime, timezone
6
-
7
- from textual import on
8
- from textual.app import App, ComposeResult
9
- from textual.binding import Binding
10
- from textual.containers import Container
11
- from textual.events import Resize
12
- from textual.reactive import reactive
13
- from textual.widgets import Footer, ListView
14
-
15
- from code_puppy.agent import get_code_generation_agent, get_custom_usage_limits
16
- from code_puppy.agents.runtime_manager import get_runtime_agent_manager
17
- from code_puppy.command_line.command_handler import handle_command
18
- from code_puppy.config import (
19
- get_model_name,
20
- get_puppy_name,
21
- initialize_command_history_file,
22
- save_command_to_history,
23
- )
24
- from code_puppy.message_history_processor import (
25
- message_history_accumulator,
26
- prune_interrupted_tool_calls,
27
- )
28
-
29
- # Import our message queue system
30
- from code_puppy.messaging import TUIRenderer, get_global_queue
31
- from code_puppy.state_management import (
32
- clear_message_history,
33
- get_message_history,
34
- set_message_history,
35
- )
36
- from code_puppy.tui.components import (
37
- ChatView,
38
- CustomTextArea,
39
- InputArea,
40
- Sidebar,
41
- StatusBar,
42
- )
43
-
44
- from .. import state_management
45
-
46
- # Import shared message classes
47
- from .messages import CommandSelected, HistoryEntrySelected
48
- from .models import ChatMessage, MessageType
49
- from .screens import HelpScreen, MCPInstallWizardScreen, SettingsScreen, ToolsScreen
50
-
51
-
52
- class CodePuppyTUI(App):
53
- """Main Code Puppy TUI application."""
54
-
55
- TITLE = "Code Puppy - AI Code Assistant"
56
- SUB_TITLE = "TUI Mode"
57
-
58
- CSS = """
59
- Screen {
60
- layout: horizontal;
61
- }
62
-
63
- #main-area {
64
- layout: vertical;
65
- width: 1fr;
66
- min-width: 40;
67
- }
68
-
69
- #chat-container {
70
- height: 1fr;
71
- min-height: 10;
72
- }
73
- """
74
-
75
- BINDINGS = [
76
- Binding("ctrl+q", "quit", "Quit"),
77
- Binding("ctrl+c", "quit", "Quit"),
78
- Binding("ctrl+l", "clear_chat", "Clear Chat"),
79
- Binding("ctrl+1", "show_help", "Help"),
80
- Binding("ctrl+2", "toggle_sidebar", "History"),
81
- Binding("ctrl+3", "open_settings", "Settings"),
82
- Binding("ctrl+4", "show_tools", "Tools"),
83
- Binding("ctrl+5", "focus_input", "Focus Prompt"),
84
- Binding("ctrl+6", "focus_chat", "Focus Response"),
85
- Binding("ctrl+t", "open_mcp_wizard", "MCP Install Wizard"),
86
- ]
87
-
88
- # Reactive variables for app state
89
- current_model = reactive("")
90
- puppy_name = reactive("")
91
- current_agent = reactive("")
92
- agent_busy = reactive(False)
93
-
94
- def watch_agent_busy(self) -> None:
95
- """Watch for changes to agent_busy state."""
96
- # Update the submit/cancel button state when agent_busy changes
97
- self._update_submit_cancel_button(self.agent_busy)
98
-
99
- def watch_current_agent(self) -> None:
100
- """Watch for changes to current_agent and update title."""
101
- self._update_title()
102
-
103
- def _update_title(self) -> None:
104
- """Update the application title to include current agent."""
105
- if self.current_agent:
106
- self.title = f"Code Puppy - {self.current_agent}"
107
- self.sub_title = "TUI Mode"
108
- else:
109
- self.title = "Code Puppy - AI Code Assistant"
110
- self.sub_title = "TUI Mode"
111
-
112
- def _on_agent_reload(self, agent_id: str, agent_name: str) -> None:
113
- """Callback for when agent is reloaded/changed."""
114
- # Get the updated agent configuration
115
- from code_puppy.agents.agent_manager import get_current_agent_config
116
-
117
- current_agent_config = get_current_agent_config()
118
- new_agent_display = (
119
- current_agent_config.display_name if current_agent_config else "code-puppy"
120
- )
121
-
122
- # Update the reactive variable (this will trigger watch_current_agent)
123
- self.current_agent = new_agent_display
124
-
125
- # Add a system message to notify the user
126
- self.add_system_message(f"🔄 Switched to agent: {new_agent_display}")
127
-
128
- def __init__(self, initial_command: str = None, **kwargs):
129
- super().__init__(**kwargs)
130
- self.agent_manager = None
131
- self._current_worker = None
132
- self.initial_command = initial_command
133
-
134
- # Initialize message queue renderer
135
- self.message_queue = get_global_queue()
136
- self.message_renderer = TUIRenderer(self.message_queue, self)
137
- self._renderer_started = False
138
-
139
- def compose(self) -> ComposeResult:
140
- """Create the UI layout."""
141
- yield StatusBar()
142
- yield Sidebar()
143
- with Container(id="main-area"):
144
- with Container(id="chat-container"):
145
- yield ChatView(id="chat-view")
146
- yield InputArea()
147
- yield Footer()
148
-
149
- def on_mount(self) -> None:
150
- """Initialize the application when mounted."""
151
- # Register this app instance for global access
152
- from code_puppy.state_management import set_tui_app_instance
153
-
154
- set_tui_app_instance(self)
155
-
156
- # Register callback for agent reload events
157
- from code_puppy.callbacks import register_callback
158
-
159
- register_callback("agent_reload", self._on_agent_reload)
160
-
161
- # Load configuration
162
- self.current_model = get_model_name()
163
- self.puppy_name = get_puppy_name()
164
-
165
- # Get current agent information
166
- from code_puppy.agents.agent_manager import get_current_agent_config
167
-
168
- current_agent_config = get_current_agent_config()
169
- self.current_agent = (
170
- current_agent_config.display_name if current_agent_config else "code-puppy"
171
- )
172
-
173
- # Initial title update
174
- self._update_title()
175
-
176
- # Use runtime manager to ensure we always have the current agent
177
- self.agent_manager = get_runtime_agent_manager()
178
-
179
- # Update status bar
180
- status_bar = self.query_one(StatusBar)
181
- status_bar.current_model = self.current_model
182
- status_bar.puppy_name = self.puppy_name
183
- status_bar.agent_status = "Ready"
184
-
185
- # Add welcome message with YOLO mode notification
186
- self.add_system_message(
187
- "Welcome to Code Puppy 🐶!\n💨 YOLO mode is enabled in TUI: commands will execute without confirmation."
188
- )
189
-
190
- # Get current agent and display info
191
- get_code_generation_agent()
192
- self.add_system_message(
193
- f"🐕 Loaded agent '{self.puppy_name}' with model '{self.current_model}'"
194
- )
195
-
196
- # Start the message renderer EARLY to catch startup messages
197
- # Using call_after_refresh to start it as soon as possible after mount
198
- self.call_after_refresh(self.start_message_renderer_sync)
199
-
200
- # Apply responsive design adjustments
201
- self.apply_responsive_layout()
202
-
203
- # Auto-focus the input field so user can start typing immediately
204
- self.call_after_refresh(self.focus_input_field)
205
-
206
- # Process initial command if provided
207
- if self.initial_command:
208
- self.call_after_refresh(self.process_initial_command)
209
-
210
- def add_system_message(
211
- self, content: str, message_group: str = None, group_id: str = None
212
- ) -> None:
213
- """Add a system message to the chat."""
214
- # Support both parameter names for backward compatibility
215
- final_group_id = message_group or group_id
216
- message = ChatMessage(
217
- id=f"sys_{datetime.now(timezone.utc).timestamp()}",
218
- type=MessageType.SYSTEM,
219
- content=content,
220
- timestamp=datetime.now(timezone.utc),
221
- group_id=final_group_id,
222
- )
223
- chat_view = self.query_one("#chat-view", ChatView)
224
- chat_view.add_message(message)
225
-
226
- def add_system_message_rich(
227
- self, rich_content, message_group: str = None, group_id: str = None
228
- ) -> None:
229
- """Add a system message with Rich content (like Markdown) to the chat."""
230
- # Support both parameter names for backward compatibility
231
- final_group_id = message_group or group_id
232
- message = ChatMessage(
233
- id=f"sys_rich_{datetime.now(timezone.utc).timestamp()}",
234
- type=MessageType.SYSTEM,
235
- content=rich_content, # Store the Rich object directly
236
- timestamp=datetime.now(timezone.utc),
237
- group_id=final_group_id,
238
- )
239
- chat_view = self.query_one("#chat-view", ChatView)
240
- chat_view.add_message(message)
241
-
242
- def add_user_message(self, content: str, message_group: str = None) -> None:
243
- """Add a user message to the chat."""
244
- message = ChatMessage(
245
- id=f"user_{datetime.now(timezone.utc).timestamp()}",
246
- type=MessageType.USER,
247
- content=content,
248
- timestamp=datetime.now(timezone.utc),
249
- group_id=message_group,
250
- )
251
- chat_view = self.query_one("#chat-view", ChatView)
252
- chat_view.add_message(message)
253
-
254
- def add_agent_message(self, content: str, message_group: str = None) -> None:
255
- """Add an agent message to the chat."""
256
- message = ChatMessage(
257
- id=f"agent_{datetime.now(timezone.utc).timestamp()}",
258
- type=MessageType.AGENT_RESPONSE,
259
- content=content,
260
- timestamp=datetime.now(timezone.utc),
261
- group_id=message_group,
262
- )
263
- chat_view = self.query_one("#chat-view", ChatView)
264
- chat_view.add_message(message)
265
-
266
- def add_error_message(self, content: str, message_group: str = None) -> None:
267
- """Add an error message to the chat."""
268
- message = ChatMessage(
269
- id=f"error_{datetime.now(timezone.utc).timestamp()}",
270
- type=MessageType.ERROR,
271
- content=content,
272
- timestamp=datetime.now(timezone.utc),
273
- group_id=message_group,
274
- )
275
- chat_view = self.query_one("#chat-view", ChatView)
276
- chat_view.add_message(message)
277
-
278
- def add_agent_reasoning_message(
279
- self, content: str, message_group: str = None
280
- ) -> None:
281
- """Add an agent reasoning message to the chat."""
282
- message = ChatMessage(
283
- id=f"agent_reasoning_{datetime.now(timezone.utc).timestamp()}",
284
- type=MessageType.AGENT_REASONING,
285
- content=content,
286
- timestamp=datetime.now(timezone.utc),
287
- group_id=message_group,
288
- )
289
- chat_view = self.query_one("#chat-view", ChatView)
290
- chat_view.add_message(message)
291
-
292
- def add_planned_next_steps_message(
293
- self, content: str, message_group: str = None
294
- ) -> None:
295
- """Add an planned next steps to the chat."""
296
- message = ChatMessage(
297
- id=f"planned_next_steps_{datetime.now(timezone.utc).timestamp()}",
298
- type=MessageType.PLANNED_NEXT_STEPS,
299
- content=content,
300
- timestamp=datetime.now(timezone.utc),
301
- group_id=message_group,
302
- )
303
- chat_view = self.query_one("#chat-view", ChatView)
304
- chat_view.add_message(message)
305
-
306
- def on_custom_text_area_message_sent(
307
- self, event: CustomTextArea.MessageSent
308
- ) -> None:
309
- """Handle message sent from custom text area."""
310
- self.action_send_message()
311
-
312
- def on_input_area_submit_requested(self, event) -> None:
313
- """Handle submit button clicked."""
314
- self.action_send_message()
315
-
316
- def on_input_area_cancel_requested(self, event) -> None:
317
- """Handle cancel button clicked."""
318
- self.action_cancel_processing()
319
-
320
- async def on_key(self, event) -> None:
321
- """Handle app-level key events."""
322
- input_field = self.query_one("#input-field", CustomTextArea)
323
-
324
- # Only handle keys when input field is focused
325
- if input_field.has_focus:
326
- # Handle Ctrl+Enter for new lines (more reliable than Shift+Enter)
327
- if event.key == "ctrl+enter":
328
- input_field.insert("\\n")
329
- event.prevent_default()
330
- return
331
-
332
- # Check if a modal is currently active - if so, let the modal handle keys
333
- if hasattr(self, "_active_screen") and self._active_screen:
334
- # Don't handle keys at the app level when a modal is active
335
- return
336
-
337
- # Handle arrow keys for sidebar navigation when sidebar is visible
338
- if not input_field.has_focus:
339
- try:
340
- sidebar = self.query_one(Sidebar)
341
- if sidebar.display:
342
- # Handle navigation for the currently active tab
343
- tabs = self.query_one("#sidebar-tabs")
344
- active_tab = tabs.active
345
-
346
- if active_tab == "history-tab":
347
- history_list = self.query_one("#history-list", ListView)
348
- if event.key == "enter":
349
- if history_list.highlighted_child and hasattr(
350
- history_list.highlighted_child, "command_entry"
351
- ):
352
- # Show command history modal
353
- from .components.command_history_modal import (
354
- CommandHistoryModal,
355
- )
356
-
357
- # Make sure sidebar's current_history_index is synced with the ListView
358
- sidebar.current_history_index = history_list.index
359
-
360
- # Push the modal screen
361
- # The modal will get the command entries from the sidebar
362
- self.push_screen(CommandHistoryModal())
363
- event.prevent_default()
364
- return
365
- except Exception:
366
- pass
367
-
368
- def refresh_history_display(self) -> None:
369
- """Refresh the history display with the command history file."""
370
- try:
371
- sidebar = self.query_one(Sidebar)
372
- sidebar.load_command_history()
373
- except Exception:
374
- pass # Silently fail if history list not available
375
-
376
- def action_send_message(self) -> None:
377
- """Send the current message."""
378
- input_field = self.query_one("#input-field", CustomTextArea)
379
- message = input_field.text.strip()
380
-
381
- if message:
382
- # Clear input
383
- input_field.text = ""
384
-
385
- # Add user message to chat
386
- self.add_user_message(message)
387
-
388
- # Save command to history file with timestamp
389
- try:
390
- save_command_to_history(message)
391
- except Exception as e:
392
- self.add_error_message(f"Failed to save command history: {str(e)}")
393
-
394
- # Update button state
395
- self._update_submit_cancel_button(True)
396
-
397
- # Process the message asynchronously using Textual's worker system
398
- # Using exclusive=False to avoid TaskGroup conflicts with MCP servers
399
- self._current_worker = self.run_worker(
400
- self.process_message(message), exclusive=False
401
- )
402
-
403
- def _update_submit_cancel_button(self, is_cancel_mode: bool) -> None:
404
- """Update the submit/cancel button state."""
405
- try:
406
- from .components.input_area import SubmitCancelButton
407
-
408
- button = self.query_one(SubmitCancelButton)
409
- button.is_cancel_mode = is_cancel_mode
410
- except Exception:
411
- pass # Silently fail if button not found
412
-
413
- def action_cancel_processing(self) -> None:
414
- """Cancel the current message processing."""
415
- if hasattr(self, "_current_worker") and self._current_worker is not None:
416
- try:
417
- # First, kill any running shell processes (same as interactive mode Ctrl+C)
418
- from code_puppy.tools.command_runner import (
419
- kill_all_running_shell_processes,
420
- )
421
-
422
- killed = kill_all_running_shell_processes()
423
- if killed:
424
- self.add_system_message(
425
- f"🔥 Cancelled {killed} running shell process(es)"
426
- )
427
- # Don't stop spinner/agent - let the agent continue processing
428
- # Shell processes killed, but agent worker continues running
429
-
430
- else:
431
- # Only cancel the agent task if NO processes were killed
432
- self._current_worker.cancel()
433
- state_management._message_history = prune_interrupted_tool_calls(
434
- state_management.get_message_history()
435
- )
436
- self.add_system_message("⚠️ Processing cancelled by user")
437
- # Stop spinner and clear state only when agent is actually cancelled
438
- self._current_worker = None
439
- self.agent_busy = False
440
- self.stop_agent_progress()
441
- except Exception as e:
442
- self.add_error_message(f"Failed to cancel processing: {str(e)}")
443
- # Only clear state on exception if we haven't already done so
444
- if (
445
- hasattr(self, "_current_worker")
446
- and self._current_worker is not None
447
- ):
448
- self._current_worker = None
449
- self.agent_busy = False
450
- self.stop_agent_progress()
451
-
452
- async def process_message(self, message: str) -> None:
453
- """Process a user message asynchronously."""
454
- try:
455
- self.agent_busy = True
456
- self._update_submit_cancel_button(True)
457
- self.start_agent_progress("Thinking")
458
-
459
- # Handle commands
460
- if message.strip().startswith("/"):
461
- # Handle special commands directly
462
- if message.strip().lower() in ("clear", "/clear"):
463
- self.action_clear_chat()
464
- return
465
-
466
- # Let the command handler process all /agent commands
467
- # result will be handled by the command handler directly through messaging system
468
- if message.strip().startswith("/agent"):
469
- # The command handler will emit messages directly to our messaging system
470
- handle_command(message.strip())
471
- # Agent manager will automatically use the latest agent
472
- return
473
-
474
- # Handle exit commands
475
- if message.strip().lower() in ("/exit", "/quit"):
476
- self.add_system_message("Goodbye!")
477
- # Exit the application
478
- self.app.exit()
479
- return
480
-
481
- # Use the existing command handler
482
- # The command handler directly uses the messaging system, so we don't need to capture stdout
483
- try:
484
- result = handle_command(message.strip())
485
- if not result:
486
- self.add_system_message(f"Unknown command: {message}")
487
- except Exception as e:
488
- self.add_error_message(f"Error executing command: {str(e)}")
489
- return
490
-
491
- # Process with agent
492
- if self.agent_manager:
493
- try:
494
- self.update_agent_progress("Processing", 25)
495
-
496
- # Use agent_manager's run_with_mcp to handle MCP servers properly
497
- try:
498
- self.update_agent_progress("Processing", 50)
499
- result = await self.agent_manager.run_with_mcp(
500
- message,
501
- message_history=get_message_history(),
502
- usage_limits=get_custom_usage_limits(),
503
- )
504
-
505
- if not result or not hasattr(result, "output"):
506
- self.add_error_message("Invalid response format from agent")
507
- return
508
-
509
- self.update_agent_progress("Processing", 75)
510
- agent_response = result.output
511
- self.add_agent_message(agent_response)
512
-
513
- # Update message history
514
- new_msgs = result.new_messages()
515
- message_history_accumulator(new_msgs)
516
-
517
- # Refresh history display to show new interaction
518
- self.refresh_history_display()
519
-
520
- except Exception as eg:
521
- # Handle TaskGroup and other exceptions
522
- # BaseExceptionGroup is only available in Python 3.11+
523
- if hasattr(eg, "exceptions"):
524
- # Handle TaskGroup exceptions specifically (Python 3.11+)
525
- for e in eg.exceptions:
526
- self.add_error_message(f"MCP/Agent error: {str(e)}")
527
- else:
528
- # Handle regular exceptions
529
- self.add_error_message(f"MCP/Agent error: {str(eg)}")
530
- finally:
531
- set_message_history(
532
- prune_interrupted_tool_calls(get_message_history())
533
- )
534
- except Exception as agent_error:
535
- # Handle any other errors in agent processing
536
- self.add_error_message(
537
- f"Agent processing failed: {str(agent_error)}"
538
- )
539
- else:
540
- self.add_error_message("Agent manager not initialized")
541
-
542
- except Exception as e:
543
- self.add_error_message(f"Error processing message: {str(e)}")
544
- finally:
545
- self.agent_busy = False
546
- self._update_submit_cancel_button(False)
547
- self.stop_agent_progress()
548
-
549
- # Action methods
550
- def action_clear_chat(self) -> None:
551
- """Clear the chat history."""
552
- chat_view = self.query_one("#chat-view", ChatView)
553
- chat_view.clear_messages()
554
- clear_message_history()
555
- self.add_system_message("Chat history cleared")
556
-
557
- def action_show_help(self) -> None:
558
- """Show help information in a modal."""
559
- self.push_screen(HelpScreen())
560
-
561
- def action_toggle_sidebar(self) -> None:
562
- """Toggle sidebar visibility."""
563
- sidebar = self.query_one(Sidebar)
564
- sidebar.display = not sidebar.display
565
-
566
- # If sidebar is now visible, focus the history list to enable immediate keyboard navigation
567
- if sidebar.display:
568
- try:
569
- # Ensure history tab is active
570
- tabs = self.query_one("#sidebar-tabs")
571
- tabs.active = "history-tab"
572
-
573
- # Refresh the command history
574
- sidebar.load_command_history()
575
-
576
- # Focus the history list
577
- history_list = self.query_one("#history-list", ListView)
578
- history_list.focus()
579
-
580
- # If the list has items, get the first item for the modal
581
- if len(history_list.children) > 0:
582
- # Reset sidebar's internal index tracker to 0
583
- sidebar.current_history_index = 0
584
-
585
- # Set ListView index to match
586
- history_list.index = 0
587
-
588
- # Get the first item and show the command history modal
589
- first_item = history_list.children[0]
590
- if hasattr(first_item, "command_entry"):
591
- # command_entry = first_item.command_entry
592
-
593
- # Use call_after_refresh to allow UI to update first
594
- def show_modal():
595
- from .components.command_history_modal import (
596
- CommandHistoryModal,
597
- )
598
-
599
- # Get all command entries from the history list
600
- command_entries = []
601
- for i, child in enumerate(history_list.children):
602
- if hasattr(child, "command_entry"):
603
- command_entries.append(child.command_entry)
604
-
605
- # Push the modal screen
606
- # The modal will get the command entries from the sidebar
607
- self.push_screen(CommandHistoryModal())
608
-
609
- # Schedule modal to appear after UI refresh
610
- self.call_after_refresh(show_modal)
611
- except Exception as e:
612
- # Log the exception in debug mode but silently fail for end users
613
- import logging
614
-
615
- logging.debug(f"Error focusing history item: {str(e)}")
616
- pass
617
- else:
618
- # If sidebar is now hidden, focus the input field for a smooth workflow
619
- try:
620
- self.action_focus_input()
621
- except Exception:
622
- # Silently fail if there's an issue with focusing
623
- pass
624
-
625
- def action_focus_input(self) -> None:
626
- """Focus the input field."""
627
- input_field = self.query_one("#input-field", CustomTextArea)
628
- input_field.focus()
629
-
630
- def focus_input_field(self) -> None:
631
- """Focus the input field (used for auto-focus on startup)."""
632
- try:
633
- input_field = self.query_one("#input-field", CustomTextArea)
634
- input_field.focus()
635
- except Exception:
636
- pass # Silently handle if widget not ready yet
637
-
638
- def action_focus_chat(self) -> None:
639
- """Focus the chat area."""
640
- chat_view = self.query_one("#chat-view", ChatView)
641
- chat_view.focus()
642
-
643
- def action_show_tools(self) -> None:
644
- """Show the tools modal."""
645
- self.push_screen(ToolsScreen())
646
-
647
- def action_open_settings(self) -> None:
648
- """Open the settings configuration screen."""
649
-
650
- def handle_settings_result(result):
651
- if result and result.get("success"):
652
- # Update reactive variables
653
- from code_puppy.config import get_model_name, get_puppy_name
654
-
655
- self.puppy_name = get_puppy_name()
656
-
657
- # Handle model change if needed
658
- if result.get("model_changed"):
659
- new_model = get_model_name()
660
- self.current_model = new_model
661
- # Reinitialize agent with new model
662
- self.agent_manager.reload_agent()
663
-
664
- # Update status bar
665
- status_bar = self.query_one(StatusBar)
666
- status_bar.puppy_name = self.puppy_name
667
- status_bar.current_model = self.current_model
668
-
669
- # Show success message
670
- self.add_system_message(result.get("message", "Settings updated"))
671
- elif (
672
- result
673
- and not result.get("success")
674
- and "cancelled" not in result.get("message", "").lower()
675
- ):
676
- # Show error message (but not for cancellation)
677
- self.add_error_message(result.get("message", "Settings update failed"))
678
-
679
- self.push_screen(SettingsScreen(), handle_settings_result)
680
-
681
- def action_open_mcp_wizard(self) -> None:
682
- """Open the MCP Install Wizard."""
683
-
684
- def handle_wizard_result(result):
685
- if result and result.get("success"):
686
- # Show success message
687
- self.add_system_message(
688
- result.get("message", "MCP server installed successfully")
689
- )
690
-
691
- # If a server was installed, suggest starting it
692
- if result.get("server_name"):
693
- server_name = result["server_name"]
694
- self.add_system_message(
695
- f"💡 Use '/mcp start {server_name}' to start the server"
696
- )
697
- elif (
698
- result
699
- and not result.get("success")
700
- and "cancelled" not in result.get("message", "").lower()
701
- ):
702
- # Show error message (but not for cancellation)
703
- self.add_error_message(result.get("message", "MCP installation failed"))
704
-
705
- self.push_screen(MCPInstallWizardScreen(), handle_wizard_result)
706
-
707
- def process_initial_command(self) -> None:
708
- """Process the initial command provided when starting the TUI."""
709
- if self.initial_command:
710
- # Add the initial command to the input field
711
- input_field = self.query_one("#input-field", CustomTextArea)
712
- input_field.text = self.initial_command
713
-
714
- # Show that we're auto-executing the initial command
715
- self.add_system_message(
716
- f"🚀 Auto-executing initial command: {self.initial_command}"
717
- )
718
-
719
- # Automatically submit the message
720
- self.action_send_message()
721
-
722
- def show_history_details(self, history_entry: dict) -> None:
723
- """Show detailed information about a selected history entry."""
724
- try:
725
- timestamp = history_entry.get("timestamp", "Unknown time")
726
- description = history_entry.get("description", "No description")
727
- output = history_entry.get("output", "")
728
- awaiting_input = history_entry.get("awaiting_user_input", False)
729
-
730
- # Parse timestamp for better display with safe parsing
731
- def parse_timestamp_safely_for_details(timestamp_str: str) -> str:
732
- """Parse timestamp string safely for detailed display."""
733
- try:
734
- # Handle 'Z' suffix (common UTC format)
735
- cleaned_timestamp = timestamp_str.replace("Z", "+00:00")
736
- parsed_dt = datetime.fromisoformat(cleaned_timestamp)
737
-
738
- # If the datetime is naive (no timezone), assume UTC
739
- if parsed_dt.tzinfo is None:
740
- parsed_dt = parsed_dt.replace(tzinfo=timezone.utc)
741
-
742
- return parsed_dt.strftime("%Y-%m-%d %H:%M:%S")
743
- except (ValueError, AttributeError, TypeError):
744
- # Handle invalid timestamp formats gracefully
745
- return timestamp_str
746
-
747
- formatted_time = parse_timestamp_safely_for_details(timestamp)
748
-
749
- # Create detailed view content
750
- details = [
751
- f"Timestamp: {formatted_time}",
752
- f"Description: {description}",
753
- "",
754
- ]
755
-
756
- if output:
757
- details.extend(
758
- [
759
- "Output:",
760
- "─" * 40,
761
- output,
762
- "",
763
- ]
764
- )
765
-
766
- if awaiting_input:
767
- details.append("⚠️ Was awaiting user input")
768
-
769
- # Display details as a system message in the chat
770
- detail_text = "\\n".join(details)
771
- self.add_system_message(f"History Details:\\n{detail_text}")
772
-
773
- except Exception as e:
774
- self.add_error_message(f"Failed to show history details: {e}")
775
-
776
- # Progress and status methods
777
- def set_agent_status(self, status: str, show_progress: bool = False) -> None:
778
- """Update agent status and optionally show/hide progress bar."""
779
- try:
780
- # Update status bar
781
- status_bar = self.query_one(StatusBar)
782
- status_bar.agent_status = status
783
-
784
- # Update spinner visibility
785
- from .components.input_area import SimpleSpinnerWidget
786
-
787
- spinner = self.query_one("#spinner", SimpleSpinnerWidget)
788
- if show_progress:
789
- spinner.add_class("visible")
790
- spinner.display = True
791
- spinner.start_spinning()
792
- else:
793
- spinner.remove_class("visible")
794
- spinner.display = False
795
- spinner.stop_spinning()
796
-
797
- except Exception:
798
- pass # Silently fail if widgets not available
799
-
800
- def start_agent_progress(self, initial_status: str = "Thinking") -> None:
801
- """Start showing agent progress indicators."""
802
- self.set_agent_status(initial_status, show_progress=True)
803
-
804
- def update_agent_progress(self, status: str, progress: int = None) -> None:
805
- """Update agent progress during processing."""
806
- try:
807
- status_bar = self.query_one(StatusBar)
808
- status_bar.agent_status = status
809
- # Note: LoadingIndicator doesn't use progress values, it just spins
810
- except Exception:
811
- pass
812
-
813
- def stop_agent_progress(self) -> None:
814
- """Stop showing agent progress indicators."""
815
- self.set_agent_status("Ready", show_progress=False)
816
-
817
- def on_resize(self, event: Resize) -> None:
818
- """Handle terminal resize events to update responsive elements."""
819
- try:
820
- # Apply responsive layout adjustments
821
- self.apply_responsive_layout()
822
-
823
- # Update status bar to reflect new width
824
- status_bar = self.query_one(StatusBar)
825
- status_bar.update_status()
826
-
827
- # Refresh history display with new responsive truncation
828
- self.refresh_history_display()
829
-
830
- except Exception:
831
- pass # Silently handle resize errors
832
-
833
- def apply_responsive_layout(self) -> None:
834
- """Apply responsive layout adjustments based on terminal size."""
835
- try:
836
- terminal_width = self.size.width if hasattr(self, "size") else 80
837
- terminal_height = self.size.height if hasattr(self, "size") else 24
838
- sidebar = self.query_one(Sidebar)
839
-
840
- # Responsive sidebar width based on terminal width
841
- if terminal_width >= 120:
842
- sidebar.styles.width = 35
843
- elif terminal_width >= 100:
844
- sidebar.styles.width = 30
845
- elif terminal_width >= 80:
846
- sidebar.styles.width = 25
847
- elif terminal_width >= 60:
848
- sidebar.styles.width = 20
849
- else:
850
- sidebar.styles.width = 15
851
-
852
- # Auto-hide sidebar on very narrow terminals
853
- if terminal_width < 50:
854
- if sidebar.display:
855
- sidebar.display = False
856
- self.add_system_message(
857
- "💡 Sidebar auto-hidden for narrow terminal. Press Ctrl+2 to toggle."
858
- )
859
-
860
- # Adjust input area height for very short terminals
861
- if terminal_height < 20:
862
- input_area = self.query_one(InputArea)
863
- input_area.styles.height = 7
864
- else:
865
- input_area = self.query_one(InputArea)
866
- input_area.styles.height = 9
867
-
868
- except Exception:
869
- pass
870
-
871
- def start_message_renderer_sync(self):
872
- """Synchronous wrapper to start message renderer via run_worker."""
873
- self.run_worker(self.start_message_renderer(), exclusive=False)
874
-
875
- async def start_message_renderer(self):
876
- """Start the message renderer to consume messages from the queue."""
877
- if not self._renderer_started:
878
- self._renderer_started = True
879
-
880
- # Process any buffered startup messages first
881
- from io import StringIO
882
-
883
- from rich.console import Console
884
-
885
- from code_puppy.messaging import get_buffered_startup_messages
886
-
887
- buffered_messages = get_buffered_startup_messages()
888
-
889
- if buffered_messages:
890
- # Group startup messages into a single display
891
- startup_content_lines = []
892
-
893
- for message in buffered_messages:
894
- try:
895
- # Convert message content to string for grouping
896
- if hasattr(message.content, "__rich_console__"):
897
- # For Rich objects, render to plain text
898
- string_io = StringIO()
899
- # Use markup=False to prevent interpretation of square brackets as markup
900
- temp_console = Console(
901
- file=string_io,
902
- width=80,
903
- legacy_windows=False,
904
- markup=False,
905
- )
906
- temp_console.print(message.content)
907
- content_str = string_io.getvalue().rstrip("\n")
908
- else:
909
- content_str = str(message.content)
910
-
911
- startup_content_lines.append(content_str)
912
- except Exception as e:
913
- startup_content_lines.append(
914
- f"Error processing startup message: {e}"
915
- )
916
-
917
- # Create a single grouped startup message
918
- grouped_content = "\n".join(startup_content_lines)
919
- self.add_system_message(grouped_content)
920
-
921
- # Clear the startup buffer after processing
922
- self.message_queue.clear_startup_buffer()
923
-
924
- # Now start the regular message renderer
925
- await self.message_renderer.start()
926
-
927
- async def stop_message_renderer(self):
928
- """Stop the message renderer."""
929
- if self._renderer_started:
930
- self._renderer_started = False
931
- try:
932
- await self.message_renderer.stop()
933
- except Exception as e:
934
- # Log renderer stop errors but don't crash
935
- self.add_system_message(f"Renderer stop error: {e}")
936
-
937
- @on(HistoryEntrySelected)
938
- def on_history_entry_selected(self, event: HistoryEntrySelected) -> None:
939
- """Handle selection of a history entry from the sidebar."""
940
- # Display the history entry details
941
- self.show_history_details(event.history_entry)
942
-
943
- @on(CommandSelected)
944
- def on_command_selected(self, event: CommandSelected) -> None:
945
- """Handle selection of a command from the history modal."""
946
- # Set the command in the input field
947
- input_field = self.query_one("#input-field", CustomTextArea)
948
- input_field.text = event.command
949
-
950
- # Focus the input field for immediate editing
951
- input_field.focus()
952
-
953
- # Close the sidebar automatically for a smoother workflow
954
- sidebar = self.query_one(Sidebar)
955
- sidebar.display = False
956
-
957
- async def on_unmount(self):
958
- """Clean up when the app is unmounted."""
959
- try:
960
- # Unregister the agent reload callback
961
- from code_puppy.callbacks import unregister_callback
962
-
963
- unregister_callback("agent_reload", self._on_agent_reload)
964
-
965
- await self.stop_message_renderer()
966
- except Exception as e:
967
- # Log unmount errors but don't crash during cleanup
968
- try:
969
- self.add_system_message(f"Unmount cleanup error: {e}")
970
- except Exception:
971
- # If we can't even add a message, just ignore
972
- pass
973
-
974
-
975
- async def run_textual_ui(initial_command: str = None):
976
- """Run the Textual UI interface."""
977
- # Always enable YOLO mode in TUI mode for a smoother experience
978
- from code_puppy.config import set_config_value
979
-
980
- # Initialize the command history file
981
- initialize_command_history_file()
982
-
983
- set_config_value("yolo_mode", "true")
984
-
985
- app = CodePuppyTUI(initial_command=initial_command)
986
- await app.run_async()