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
@@ -1,218 +0,0 @@
1
- """
2
- Modal component for displaying command history entries.
3
- """
4
-
5
- from textual import on
6
- from textual.app import ComposeResult
7
- from textual.containers import Container, Horizontal
8
- from textual.events import Key
9
- from textual.screen import ModalScreen
10
- from textual.widgets import Button, Label, Static
11
-
12
- from ..messages import CommandSelected
13
-
14
-
15
- class CommandHistoryModal(ModalScreen):
16
- """Modal for displaying a command history entry."""
17
-
18
- def __init__(self, **kwargs):
19
- """Initialize the modal with command history data.
20
-
21
- Args:
22
- **kwargs: Additional arguments to pass to the parent class
23
- """
24
- super().__init__(**kwargs)
25
-
26
- # Get the current command from the sidebar
27
- try:
28
- # We'll get everything from the sidebar on demand
29
- self.sidebar = None
30
- self.command = ""
31
- self.timestamp = ""
32
- except Exception:
33
- self.command = ""
34
- self.timestamp = ""
35
-
36
- # UI components to update
37
- self.command_display = None
38
- self.timestamp_display = None
39
-
40
- def on_mount(self) -> None:
41
- """Setup when the modal is mounted."""
42
- # Get the sidebar and current command entry
43
- try:
44
- self.sidebar = self.app.query_one("Sidebar")
45
- current_entry = self.sidebar.get_current_command_entry()
46
- self.command = current_entry["command"]
47
- self.timestamp = current_entry["timestamp"]
48
- self.update_display()
49
- except Exception as e:
50
- import logging
51
-
52
- logging.debug(f"Error initializing modal: {str(e)}")
53
-
54
- DEFAULT_CSS = """
55
- CommandHistoryModal {
56
- align: center middle;
57
- }
58
-
59
- #modal-container {
60
- width: 80%;
61
- max-width: 100;
62
- /* Set a definite height that's large enough but fits on screen */
63
- height: 22; /* Increased height to make room for navigation hint */
64
- min-height: 18;
65
- background: $surface;
66
- border: solid $primary;
67
- /* Increase vertical padding to add more space between elements */
68
- padding: 1 2;
69
- /* Use vertical layout to ensure proper element sizing */
70
- layout: vertical;
71
- }
72
-
73
- #timestamp-display {
74
- width: 100%;
75
- margin-bottom: 1;
76
- color: $text-muted;
77
- text-align: right;
78
- /* Fix the height */
79
- height: 1;
80
- margin-top: 0;
81
- }
82
-
83
- #command-display {
84
- width: 100%;
85
- /* Allow this container to grow/shrink as needed but keep buttons visible */
86
- min-height: 3;
87
- height: 1fr;
88
- max-height: 12;
89
- padding: 0 1;
90
- margin-bottom: 1;
91
- margin-top: 1;
92
- background: $surface-darken-1;
93
- border: solid $primary-darken-2;
94
- overflow: auto;
95
- }
96
-
97
- #nav-hint {
98
- width: 100%;
99
- color: $text;
100
- text-align: center;
101
- margin: 1 0;
102
- }
103
-
104
- .button-container {
105
- width: 100%;
106
- /* Fix the height to ensure buttons are always visible */
107
- height: 3;
108
- align-horizontal: right;
109
- margin-top: 1;
110
- }
111
-
112
- Button {
113
- margin-right: 1;
114
- }
115
-
116
- #use-button {
117
- background: $success;
118
- }
119
-
120
- #cancel-button {
121
- background: $primary-darken-1;
122
- }
123
- """
124
-
125
- def compose(self) -> ComposeResult:
126
- """Create the modal layout."""
127
- with Container(id="modal-container"):
128
- # Header with timestamp
129
- self.timestamp_display = Label(
130
- f"Timestamp: {self.timestamp}", id="timestamp-display"
131
- )
132
- yield self.timestamp_display
133
-
134
- # Scrollable content area that can expand/contract as needed
135
- # The content will scroll if it's too long, ensuring buttons remain visible
136
- with Container(id="command-display"):
137
- self.command_display = Static(self.command)
138
- yield self.command_display
139
-
140
- # Super simple navigation hint
141
- yield Label("Press Up/Down arrows to navigate history", id="nav-hint")
142
-
143
- # Fixed button container at the bottom
144
- with Horizontal(classes="button-container"):
145
- yield Button("Cancel", id="cancel-button", variant="default")
146
- yield Button("Use Command", id="use-button", variant="primary")
147
-
148
- def on_key(self, event: Key) -> None:
149
- """Handle key events for navigation."""
150
- # Handle arrow keys for navigation
151
- if event.key == "down":
152
- self.navigate_to_next_command()
153
- event.prevent_default()
154
- elif event.key == "up":
155
- self.navigate_to_previous_command()
156
- event.prevent_default()
157
- elif event.key == "escape":
158
- self.app.pop_screen()
159
- event.prevent_default()
160
-
161
- def navigate_to_next_command(self) -> None:
162
- """Navigate to the next command in history."""
163
- try:
164
- # Get the sidebar
165
- if not self.sidebar:
166
- self.sidebar = self.app.query_one("Sidebar")
167
-
168
- # Use sidebar's method to navigate
169
- if self.sidebar.navigate_to_next_command():
170
- # Get updated command entry
171
- current_entry = self.sidebar.get_current_command_entry()
172
- self.command = current_entry["command"]
173
- self.timestamp = current_entry["timestamp"]
174
- self.update_display()
175
- except Exception as e:
176
- # Log the error but don't crash
177
- import logging
178
-
179
- logging.debug(f"Error navigating to next command: {str(e)}")
180
-
181
- def navigate_to_previous_command(self) -> None:
182
- """Navigate to the previous command in history."""
183
- try:
184
- # Get the sidebar
185
- if not self.sidebar:
186
- self.sidebar = self.app.query_one("Sidebar")
187
-
188
- # Use sidebar's method to navigate
189
- if self.sidebar.navigate_to_previous_command():
190
- # Get updated command entry
191
- current_entry = self.sidebar.get_current_command_entry()
192
- self.command = current_entry["command"]
193
- self.timestamp = current_entry["timestamp"]
194
- self.update_display()
195
- except Exception as e:
196
- # Log the error but don't crash
197
- import logging
198
-
199
- logging.debug(f"Error navigating to previous command: {str(e)}")
200
-
201
- def update_display(self) -> None:
202
- """Update the display with the current command and timestamp."""
203
- if self.command_display:
204
- self.command_display.update(self.command)
205
- if self.timestamp_display:
206
- self.timestamp_display.update(f"Timestamp: {self.timestamp}")
207
-
208
- @on(Button.Pressed, "#use-button")
209
- def use_command(self) -> None:
210
- """Handle use button press."""
211
- # Post a message to the app with the selected command
212
- self.post_message(CommandSelected(self.command))
213
- self.app.pop_screen()
214
-
215
- @on(Button.Pressed, "#cancel-button")
216
- def cancel(self) -> None:
217
- """Handle cancel button press."""
218
- self.app.pop_screen()
@@ -1,139 +0,0 @@
1
- """
2
- Copy button component for copying agent responses to clipboard.
3
- """
4
-
5
- import subprocess
6
- import sys
7
- from typing import Optional
8
-
9
- from textual.binding import Binding
10
- from textual.events import Click
11
- from textual.message import Message
12
- from textual.widgets import Button
13
-
14
-
15
- class CopyButton(Button):
16
- """A button that copies associated text to the clipboard."""
17
-
18
- DEFAULT_CSS = """
19
- CopyButton {
20
- width: auto;
21
- height: 3;
22
- min-width: 8;
23
- margin: 0 1 1 1;
24
- padding: 0 1;
25
- background: $primary;
26
- color: $text;
27
- border: none;
28
- text-align: center;
29
- }
30
-
31
- CopyButton:hover {
32
- background: $accent;
33
- color: $text;
34
- }
35
-
36
- CopyButton:focus {
37
- background: $accent;
38
- color: $text;
39
- text-style: bold;
40
- }
41
-
42
- CopyButton.-pressed {
43
- background: $success;
44
- color: $text;
45
- }
46
- """
47
-
48
- BINDINGS = [
49
- Binding("enter", "press", "Copy", show=False),
50
- Binding("space", "press", "Copy", show=False),
51
- ]
52
-
53
- def __init__(self, text_to_copy: str, **kwargs):
54
- super().__init__("📋 Copy", **kwargs)
55
- self.text_to_copy = text_to_copy
56
- self._original_label = "📋 Copy"
57
- self._copied_label = "✅ Copied!"
58
-
59
- class CopyCompleted(Message):
60
- """Message sent when text is successfully copied."""
61
-
62
- def __init__(self, success: bool, error: Optional[str] = None):
63
- super().__init__()
64
- self.success = success
65
- self.error = error
66
-
67
- def copy_to_clipboard(self, text: str) -> tuple[bool, Optional[str]]:
68
- """
69
- Copy text to clipboard using platform-appropriate method.
70
-
71
- Returns:
72
- tuple: (success: bool, error_message: Optional[str])
73
- """
74
- try:
75
- if sys.platform == "darwin": # macOS
76
- subprocess.run(
77
- ["pbcopy"], input=text, text=True, check=True, capture_output=True
78
- )
79
- elif sys.platform == "win32": # Windows
80
- subprocess.run(
81
- ["clip"], input=text, text=True, check=True, capture_output=True
82
- )
83
- else: # Linux and other Unix-like systems
84
- # Try xclip first, then xsel as fallback
85
- try:
86
- subprocess.run(
87
- ["xclip", "-selection", "clipboard"],
88
- input=text,
89
- text=True,
90
- check=True,
91
- capture_output=True,
92
- )
93
- except (subprocess.CalledProcessError, FileNotFoundError):
94
- # Fallback to xsel
95
- subprocess.run(
96
- ["xsel", "--clipboard", "--input"],
97
- input=text,
98
- text=True,
99
- check=True,
100
- capture_output=True,
101
- )
102
-
103
- return True, None
104
-
105
- except subprocess.CalledProcessError as e:
106
- return False, f"Clipboard command failed: {e}"
107
- except FileNotFoundError:
108
- if sys.platform not in ["darwin", "win32"]:
109
- return (
110
- False,
111
- "Clipboard utilities not found. Please install xclip or xsel.",
112
- )
113
- else:
114
- return False, "System clipboard command not found."
115
- except Exception as e:
116
- return False, f"Unexpected error: {e}"
117
-
118
- def on_click(self, event: Click) -> None:
119
- """Handle button click to copy text."""
120
- self.action_press()
121
-
122
- def action_press(self) -> None:
123
- """Copy the text to clipboard and provide visual feedback."""
124
- success, error = self.copy_to_clipboard(self.text_to_copy)
125
-
126
- if success:
127
- # Visual feedback - change button text temporarily
128
- self.label = self._copied_label
129
- self.add_class("-pressed")
130
-
131
- # Reset button appearance after a short delay
132
- # self.set_timer(1.5, self._reset_button_appearance)
133
-
134
- # Send message about copy operation
135
- self.post_message(self.CopyCompleted(success, error))
136
-
137
- def update_text_to_copy(self, new_text: str) -> None:
138
- """Update the text that will be copied when button is pressed."""
139
- self.text_to_copy = new_text
@@ -1,63 +0,0 @@
1
- """
2
- Custom widget components for the TUI.
3
- """
4
-
5
- from textual.binding import Binding
6
- from textual.events import Key
7
- from textual.message import Message
8
- from textual.widgets import TextArea
9
-
10
-
11
- class CustomTextArea(TextArea):
12
- """Custom TextArea that sends a message with Enter and allows new lines with Shift+Enter."""
13
-
14
- # Define key bindings
15
- BINDINGS = [
16
- Binding("alt+enter", "insert_newline", ""),
17
- ]
18
-
19
- def __init__(self, *args, **kwargs):
20
- super().__init__(*args, **kwargs)
21
-
22
- def on_key(self, event):
23
- """Handle key events before they reach the internal _on_key handler."""
24
- # Let the binding system handle alt+enter
25
- if event.key == "alt+enter":
26
- # Don't prevent default - let the binding system handle it
27
- return
28
-
29
- # Handle escape+enter manually
30
- if event.key == "escape+enter":
31
- self.action_insert_newline()
32
- event.prevent_default()
33
- event.stop()
34
- return
35
-
36
- def _on_key(self, event: Key) -> None:
37
- """Override internal key handler to intercept Enter keys."""
38
- # Handle Enter key specifically
39
- if event.key == "enter":
40
- # Check if this key is part of an escape sequence (Alt+Enter)
41
- if hasattr(event, "is_cursor_sequence") or (
42
- hasattr(event, "meta") and event.meta
43
- ):
44
- # If it's part of an escape sequence, let the parent handle it
45
- # so that bindings can process it
46
- super()._on_key(event)
47
- return
48
-
49
- # This handles plain Enter only, not escape+enter
50
- self.post_message(self.MessageSent())
51
- return # Don't call super() to prevent default newline behavior
52
-
53
- # Let TextArea handle other keys
54
- super()._on_key(event)
55
-
56
- def action_insert_newline(self) -> None:
57
- """Action to insert a new line - called by shift+enter and escape+enter bindings."""
58
- self.insert("\n")
59
-
60
- class MessageSent(Message):
61
- """Message sent when Enter key is pressed (without Shift)."""
62
-
63
- pass
@@ -1,175 +0,0 @@
1
- """
2
- Modal component for human input requests.
3
- """
4
-
5
- from textual import on
6
- from textual.app import ComposeResult
7
- from textual.containers import Container, Horizontal
8
- from textual.events import Key
9
- from textual.screen import ModalScreen
10
- from textual.widgets import Button, Static, TextArea
11
-
12
- try:
13
- from .custom_widgets import CustomTextArea
14
- except ImportError:
15
- # Fallback to regular TextArea if CustomTextArea isn't available
16
- CustomTextArea = TextArea
17
-
18
-
19
- class HumanInputModal(ModalScreen):
20
- """Modal for requesting human input."""
21
-
22
- def __init__(self, prompt_text: str, prompt_id: str, **kwargs):
23
- """Initialize the modal with prompt information.
24
-
25
- Args:
26
- prompt_text: The prompt to display to the user
27
- prompt_id: Unique identifier for this prompt request
28
- **kwargs: Additional arguments to pass to the parent class
29
- """
30
- super().__init__(**kwargs)
31
- self.prompt_text = prompt_text
32
- self.prompt_id = prompt_id
33
- self.response = ""
34
- print(f"[DEBUG] Created HumanInputModal for prompt_id: {prompt_id}")
35
-
36
- DEFAULT_CSS = """
37
- HumanInputModal {
38
- align: center middle;
39
- }
40
-
41
- #modal-container {
42
- width: 80%;
43
- max-width: 80;
44
- height: 16;
45
- min-height: 12;
46
- background: $surface;
47
- border: solid $primary;
48
- padding: 1 2;
49
- layout: vertical;
50
- }
51
-
52
- #prompt-display {
53
- width: 100%;
54
- margin-bottom: 1;
55
- color: $text;
56
- text-align: left;
57
- height: auto;
58
- max-height: 6;
59
- overflow: auto;
60
- }
61
-
62
- #input-container {
63
- width: 100%;
64
- height: 4;
65
- margin-bottom: 1;
66
- }
67
-
68
- #response-input {
69
- width: 100%;
70
- height: 4;
71
- border: solid $primary;
72
- background: $surface-darken-1;
73
- }
74
-
75
- #button-container {
76
- width: 100%;
77
- height: 3;
78
- align: center bottom;
79
- layout: horizontal;
80
- }
81
-
82
- #submit-button, #cancel-button {
83
- width: auto;
84
- height: 3;
85
- margin: 0 1;
86
- min-width: 10;
87
- }
88
-
89
- #hint-text {
90
- width: 100%;
91
- color: $text-muted;
92
- text-align: center;
93
- height: 1;
94
- margin-top: 1;
95
- }
96
- """
97
-
98
- def compose(self) -> ComposeResult:
99
- """Create the modal layout."""
100
- with Container(id="modal-container"):
101
- yield Static(self.prompt_text, id="prompt-display")
102
- with Container(id="input-container"):
103
- yield CustomTextArea("", id="response-input")
104
- with Horizontal(id="button-container"):
105
- yield Button("Submit", id="submit-button", variant="primary")
106
- yield Button("Cancel", id="cancel-button", variant="default")
107
- yield Static("Enter to submit • Escape to cancel", id="hint-text")
108
-
109
- def on_mount(self) -> None:
110
- """Focus the input field when modal opens."""
111
- try:
112
- print("[DEBUG] Modal on_mount called")
113
- input_field = self.query_one("#response-input", CustomTextArea)
114
- input_field.focus()
115
- print("[DEBUG] Modal input field focused")
116
- except Exception as e:
117
- print(f"[DEBUG] Modal on_mount exception: {e}")
118
- import traceback
119
-
120
- traceback.print_exc()
121
-
122
- @on(Button.Pressed, "#submit-button")
123
- def on_submit_clicked(self) -> None:
124
- """Handle submit button click."""
125
- self._submit_response()
126
-
127
- @on(Button.Pressed, "#cancel-button")
128
- def on_cancel_clicked(self) -> None:
129
- """Handle cancel button click."""
130
- self._cancel_response()
131
-
132
- def on_key(self, event: Key) -> None:
133
- """Handle key events."""
134
- if event.key == "escape":
135
- self._cancel_response()
136
- event.prevent_default()
137
- elif event.key == "enter":
138
- # Check if we're in the text area and it's not multi-line
139
- try:
140
- input_field = self.query_one("#response-input", CustomTextArea)
141
- if input_field.has_focus and "\n" not in input_field.text:
142
- self._submit_response()
143
- event.prevent_default()
144
- except Exception:
145
- pass
146
-
147
- def _submit_response(self) -> None:
148
- """Submit the user's response."""
149
- try:
150
- input_field = self.query_one("#response-input", CustomTextArea)
151
- self.response = input_field.text.strip()
152
- print(f"[DEBUG] Modal submitting response: {self.response[:20]}...")
153
-
154
- # Provide the response back to the message queue
155
- from code_puppy.messaging import provide_prompt_response
156
-
157
- provide_prompt_response(self.prompt_id, self.response)
158
-
159
- # Close the modal using the same method as other modals
160
- self.app.pop_screen()
161
- except Exception as e:
162
- print(f"[DEBUG] Modal error during submit: {e}")
163
- # If something goes wrong, provide empty response
164
- from code_puppy.messaging import provide_prompt_response
165
-
166
- provide_prompt_response(self.prompt_id, "")
167
- self.app.pop_screen()
168
-
169
- def _cancel_response(self) -> None:
170
- """Cancel the input request."""
171
- print("[DEBUG] Modal cancelling response")
172
- from code_puppy.messaging import provide_prompt_response
173
-
174
- provide_prompt_response(self.prompt_id, "")
175
- self.app.pop_screen()