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