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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (231) hide show
  1. code_puppy/__init__.py +7 -1
  2. code_puppy/agents/__init__.py +2 -0
  3. code_puppy/agents/agent_c_reviewer.py +59 -6
  4. code_puppy/agents/agent_code_puppy.py +7 -1
  5. code_puppy/agents/agent_code_reviewer.py +12 -2
  6. code_puppy/agents/agent_cpp_reviewer.py +73 -6
  7. code_puppy/agents/agent_creator_agent.py +45 -4
  8. code_puppy/agents/agent_golang_reviewer.py +92 -3
  9. code_puppy/agents/agent_javascript_reviewer.py +101 -8
  10. code_puppy/agents/agent_manager.py +81 -4
  11. code_puppy/agents/agent_pack_leader.py +383 -0
  12. code_puppy/agents/agent_planning.py +163 -0
  13. code_puppy/agents/agent_python_programmer.py +165 -0
  14. code_puppy/agents/agent_python_reviewer.py +28 -6
  15. code_puppy/agents/agent_qa_expert.py +98 -6
  16. code_puppy/agents/agent_qa_kitten.py +12 -7
  17. code_puppy/agents/agent_security_auditor.py +113 -3
  18. code_puppy/agents/agent_terminal_qa.py +323 -0
  19. code_puppy/agents/agent_typescript_reviewer.py +106 -7
  20. code_puppy/agents/base_agent.py +802 -176
  21. code_puppy/agents/event_stream_handler.py +350 -0
  22. code_puppy/agents/pack/__init__.py +34 -0
  23. code_puppy/agents/pack/bloodhound.py +304 -0
  24. code_puppy/agents/pack/husky.py +321 -0
  25. code_puppy/agents/pack/retriever.py +393 -0
  26. code_puppy/agents/pack/shepherd.py +348 -0
  27. code_puppy/agents/pack/terrier.py +287 -0
  28. code_puppy/agents/pack/watchdog.py +367 -0
  29. code_puppy/agents/prompt_reviewer.py +145 -0
  30. code_puppy/agents/subagent_stream_handler.py +276 -0
  31. code_puppy/api/__init__.py +13 -0
  32. code_puppy/api/app.py +169 -0
  33. code_puppy/api/main.py +21 -0
  34. code_puppy/api/pty_manager.py +446 -0
  35. code_puppy/api/routers/__init__.py +12 -0
  36. code_puppy/api/routers/agents.py +36 -0
  37. code_puppy/api/routers/commands.py +217 -0
  38. code_puppy/api/routers/config.py +74 -0
  39. code_puppy/api/routers/sessions.py +232 -0
  40. code_puppy/api/templates/terminal.html +361 -0
  41. code_puppy/api/websocket.py +154 -0
  42. code_puppy/callbacks.py +142 -4
  43. code_puppy/chatgpt_codex_client.py +283 -0
  44. code_puppy/claude_cache_client.py +586 -0
  45. code_puppy/cli_runner.py +916 -0
  46. code_puppy/command_line/add_model_menu.py +1079 -0
  47. code_puppy/command_line/agent_menu.py +395 -0
  48. code_puppy/command_line/attachments.py +10 -5
  49. code_puppy/command_line/autosave_menu.py +605 -0
  50. code_puppy/command_line/clipboard.py +527 -0
  51. code_puppy/command_line/colors_menu.py +520 -0
  52. code_puppy/command_line/command_handler.py +176 -738
  53. code_puppy/command_line/command_registry.py +150 -0
  54. code_puppy/command_line/config_commands.py +715 -0
  55. code_puppy/command_line/core_commands.py +792 -0
  56. code_puppy/command_line/diff_menu.py +863 -0
  57. code_puppy/command_line/load_context_completion.py +15 -22
  58. code_puppy/command_line/mcp/base.py +0 -3
  59. code_puppy/command_line/mcp/catalog_server_installer.py +175 -0
  60. code_puppy/command_line/mcp/custom_server_form.py +688 -0
  61. code_puppy/command_line/mcp/custom_server_installer.py +195 -0
  62. code_puppy/command_line/mcp/edit_command.py +148 -0
  63. code_puppy/command_line/mcp/handler.py +9 -4
  64. code_puppy/command_line/mcp/help_command.py +6 -5
  65. code_puppy/command_line/mcp/install_command.py +15 -26
  66. code_puppy/command_line/mcp/install_menu.py +685 -0
  67. code_puppy/command_line/mcp/list_command.py +2 -2
  68. code_puppy/command_line/mcp/logs_command.py +174 -65
  69. code_puppy/command_line/mcp/remove_command.py +2 -2
  70. code_puppy/command_line/mcp/restart_command.py +12 -4
  71. code_puppy/command_line/mcp/search_command.py +16 -10
  72. code_puppy/command_line/mcp/start_all_command.py +18 -6
  73. code_puppy/command_line/mcp/start_command.py +47 -25
  74. code_puppy/command_line/mcp/status_command.py +4 -5
  75. code_puppy/command_line/mcp/stop_all_command.py +7 -1
  76. code_puppy/command_line/mcp/stop_command.py +8 -4
  77. code_puppy/command_line/mcp/test_command.py +2 -2
  78. code_puppy/command_line/mcp/wizard_utils.py +20 -16
  79. code_puppy/command_line/mcp_completion.py +174 -0
  80. code_puppy/command_line/model_picker_completion.py +75 -25
  81. code_puppy/command_line/model_settings_menu.py +884 -0
  82. code_puppy/command_line/motd.py +14 -8
  83. code_puppy/command_line/onboarding_slides.py +179 -0
  84. code_puppy/command_line/onboarding_wizard.py +340 -0
  85. code_puppy/command_line/pin_command_completion.py +329 -0
  86. code_puppy/command_line/prompt_toolkit_completion.py +463 -63
  87. code_puppy/command_line/session_commands.py +296 -0
  88. code_puppy/command_line/utils.py +54 -0
  89. code_puppy/config.py +898 -112
  90. code_puppy/error_logging.py +118 -0
  91. code_puppy/gemini_code_assist.py +385 -0
  92. code_puppy/gemini_model.py +602 -0
  93. code_puppy/http_utils.py +210 -148
  94. code_puppy/keymap.py +128 -0
  95. code_puppy/main.py +5 -698
  96. code_puppy/mcp_/__init__.py +17 -0
  97. code_puppy/mcp_/async_lifecycle.py +35 -4
  98. code_puppy/mcp_/blocking_startup.py +70 -43
  99. code_puppy/mcp_/captured_stdio_server.py +2 -2
  100. code_puppy/mcp_/config_wizard.py +4 -4
  101. code_puppy/mcp_/dashboard.py +15 -6
  102. code_puppy/mcp_/managed_server.py +65 -38
  103. code_puppy/mcp_/manager.py +146 -52
  104. code_puppy/mcp_/mcp_logs.py +224 -0
  105. code_puppy/mcp_/registry.py +6 -6
  106. code_puppy/mcp_/server_registry_catalog.py +24 -5
  107. code_puppy/messaging/__init__.py +199 -2
  108. code_puppy/messaging/bus.py +610 -0
  109. code_puppy/messaging/commands.py +167 -0
  110. code_puppy/messaging/markdown_patches.py +57 -0
  111. code_puppy/messaging/message_queue.py +17 -48
  112. code_puppy/messaging/messages.py +500 -0
  113. code_puppy/messaging/queue_console.py +1 -24
  114. code_puppy/messaging/renderers.py +43 -146
  115. code_puppy/messaging/rich_renderer.py +1027 -0
  116. code_puppy/messaging/spinner/__init__.py +21 -5
  117. code_puppy/messaging/spinner/console_spinner.py +86 -51
  118. code_puppy/messaging/subagent_console.py +461 -0
  119. code_puppy/model_factory.py +634 -83
  120. code_puppy/model_utils.py +167 -0
  121. code_puppy/models.json +66 -68
  122. code_puppy/models_dev_api.json +1 -0
  123. code_puppy/models_dev_parser.py +592 -0
  124. code_puppy/plugins/__init__.py +164 -10
  125. code_puppy/plugins/antigravity_oauth/__init__.py +10 -0
  126. code_puppy/plugins/antigravity_oauth/accounts.py +406 -0
  127. code_puppy/plugins/antigravity_oauth/antigravity_model.py +704 -0
  128. code_puppy/plugins/antigravity_oauth/config.py +42 -0
  129. code_puppy/plugins/antigravity_oauth/constants.py +136 -0
  130. code_puppy/plugins/antigravity_oauth/oauth.py +478 -0
  131. code_puppy/plugins/antigravity_oauth/register_callbacks.py +406 -0
  132. code_puppy/plugins/antigravity_oauth/storage.py +271 -0
  133. code_puppy/plugins/antigravity_oauth/test_plugin.py +319 -0
  134. code_puppy/plugins/antigravity_oauth/token.py +167 -0
  135. code_puppy/plugins/antigravity_oauth/transport.py +767 -0
  136. code_puppy/plugins/antigravity_oauth/utils.py +169 -0
  137. code_puppy/plugins/chatgpt_oauth/__init__.py +8 -0
  138. code_puppy/plugins/chatgpt_oauth/config.py +52 -0
  139. code_puppy/plugins/chatgpt_oauth/oauth_flow.py +328 -0
  140. code_puppy/plugins/chatgpt_oauth/register_callbacks.py +94 -0
  141. code_puppy/plugins/chatgpt_oauth/test_plugin.py +293 -0
  142. code_puppy/plugins/chatgpt_oauth/utils.py +489 -0
  143. code_puppy/plugins/claude_code_oauth/README.md +167 -0
  144. code_puppy/plugins/claude_code_oauth/SETUP.md +93 -0
  145. code_puppy/plugins/claude_code_oauth/__init__.py +6 -0
  146. code_puppy/plugins/claude_code_oauth/config.py +50 -0
  147. code_puppy/plugins/claude_code_oauth/register_callbacks.py +308 -0
  148. code_puppy/plugins/claude_code_oauth/test_plugin.py +283 -0
  149. code_puppy/plugins/claude_code_oauth/utils.py +518 -0
  150. code_puppy/plugins/customizable_commands/__init__.py +0 -0
  151. code_puppy/plugins/customizable_commands/register_callbacks.py +169 -0
  152. code_puppy/plugins/example_custom_command/README.md +280 -0
  153. code_puppy/plugins/example_custom_command/register_callbacks.py +2 -2
  154. code_puppy/plugins/file_permission_handler/__init__.py +4 -0
  155. code_puppy/plugins/file_permission_handler/register_callbacks.py +523 -0
  156. code_puppy/plugins/frontend_emitter/__init__.py +25 -0
  157. code_puppy/plugins/frontend_emitter/emitter.py +121 -0
  158. code_puppy/plugins/frontend_emitter/register_callbacks.py +261 -0
  159. code_puppy/plugins/oauth_puppy_html.py +228 -0
  160. code_puppy/plugins/shell_safety/__init__.py +6 -0
  161. code_puppy/plugins/shell_safety/agent_shell_safety.py +69 -0
  162. code_puppy/plugins/shell_safety/command_cache.py +156 -0
  163. code_puppy/plugins/shell_safety/register_callbacks.py +202 -0
  164. code_puppy/prompts/antigravity_system_prompt.md +1 -0
  165. code_puppy/prompts/codex_system_prompt.md +310 -0
  166. code_puppy/pydantic_patches.py +131 -0
  167. code_puppy/reopenable_async_client.py +8 -8
  168. code_puppy/round_robin_model.py +9 -12
  169. code_puppy/session_storage.py +2 -1
  170. code_puppy/status_display.py +21 -4
  171. code_puppy/summarization_agent.py +41 -13
  172. code_puppy/terminal_utils.py +418 -0
  173. code_puppy/tools/__init__.py +37 -1
  174. code_puppy/tools/agent_tools.py +536 -52
  175. code_puppy/tools/browser/__init__.py +37 -0
  176. code_puppy/tools/browser/browser_control.py +19 -23
  177. code_puppy/tools/browser/browser_interactions.py +41 -48
  178. code_puppy/tools/browser/browser_locators.py +36 -38
  179. code_puppy/tools/browser/browser_manager.py +316 -0
  180. code_puppy/tools/browser/browser_navigation.py +16 -16
  181. code_puppy/tools/browser/browser_screenshot.py +79 -143
  182. code_puppy/tools/browser/browser_scripts.py +32 -42
  183. code_puppy/tools/browser/browser_workflows.py +44 -27
  184. code_puppy/tools/browser/chromium_terminal_manager.py +259 -0
  185. code_puppy/tools/browser/terminal_command_tools.py +521 -0
  186. code_puppy/tools/browser/terminal_screenshot_tools.py +556 -0
  187. code_puppy/tools/browser/terminal_tools.py +525 -0
  188. code_puppy/tools/command_runner.py +930 -147
  189. code_puppy/tools/common.py +1113 -5
  190. code_puppy/tools/display.py +84 -0
  191. code_puppy/tools/file_modifications.py +288 -89
  192. code_puppy/tools/file_operations.py +226 -154
  193. code_puppy/tools/subagent_context.py +158 -0
  194. code_puppy/uvx_detection.py +242 -0
  195. code_puppy/version_checker.py +30 -11
  196. code_puppy-0.0.366.data/data/code_puppy/models.json +110 -0
  197. code_puppy-0.0.366.data/data/code_puppy/models_dev_api.json +1 -0
  198. {code_puppy-0.0.214.dist-info → code_puppy-0.0.366.dist-info}/METADATA +149 -75
  199. code_puppy-0.0.366.dist-info/RECORD +217 -0
  200. {code_puppy-0.0.214.dist-info → code_puppy-0.0.366.dist-info}/WHEEL +1 -1
  201. code_puppy/command_line/mcp/add_command.py +0 -183
  202. code_puppy/messaging/spinner/textual_spinner.py +0 -106
  203. code_puppy/tools/browser/camoufox_manager.py +0 -216
  204. code_puppy/tools/browser/vqa_agent.py +0 -70
  205. code_puppy/tui/__init__.py +0 -10
  206. code_puppy/tui/app.py +0 -1105
  207. code_puppy/tui/components/__init__.py +0 -21
  208. code_puppy/tui/components/chat_view.py +0 -551
  209. code_puppy/tui/components/command_history_modal.py +0 -218
  210. code_puppy/tui/components/copy_button.py +0 -139
  211. code_puppy/tui/components/custom_widgets.py +0 -63
  212. code_puppy/tui/components/human_input_modal.py +0 -175
  213. code_puppy/tui/components/input_area.py +0 -167
  214. code_puppy/tui/components/sidebar.py +0 -309
  215. code_puppy/tui/components/status_bar.py +0 -185
  216. code_puppy/tui/messages.py +0 -27
  217. code_puppy/tui/models/__init__.py +0 -8
  218. code_puppy/tui/models/chat_message.py +0 -25
  219. code_puppy/tui/models/command_history.py +0 -89
  220. code_puppy/tui/models/enums.py +0 -24
  221. code_puppy/tui/screens/__init__.py +0 -17
  222. code_puppy/tui/screens/autosave_picker.py +0 -175
  223. code_puppy/tui/screens/help.py +0 -130
  224. code_puppy/tui/screens/mcp_install_wizard.py +0 -803
  225. code_puppy/tui/screens/settings.py +0 -306
  226. code_puppy/tui/screens/tools.py +0 -74
  227. code_puppy/tui_state.py +0 -55
  228. code_puppy-0.0.214.data/data/code_puppy/models.json +0 -112
  229. code_puppy-0.0.214.dist-info/RECORD +0 -131
  230. {code_puppy-0.0.214.dist-info → code_puppy-0.0.366.dist-info}/entry_points.txt +0 -0
  231. {code_puppy-0.0.214.dist-info → code_puppy-0.0.366.dist-info}/licenses/LICENSE +0 -0
@@ -0,0 +1,863 @@
1
+ """Interactive nested menu for diff configuration.
2
+
3
+ Now using the fixed arrow_select_async with proper HTML escaping.
4
+ Supports cycling through all supported languages with left/right arrows!
5
+ """
6
+
7
+ import io
8
+ import sys
9
+ import time
10
+ from typing import Callable, Optional
11
+
12
+ from prompt_toolkit import Application
13
+ from prompt_toolkit.formatted_text import ANSI, FormattedText
14
+ from prompt_toolkit.key_binding import KeyBindings
15
+ from prompt_toolkit.layout import Layout, VSplit, Window
16
+ from prompt_toolkit.layout.controls import FormattedTextControl
17
+ from prompt_toolkit.widgets import Frame
18
+ from rich.console import Console
19
+
20
+ # Sample code snippets for each language
21
+ LANGUAGE_SAMPLES = {
22
+ "python": (
23
+ "calculator.py",
24
+ """--- a/calculator.py
25
+ +++ b/calculator.py
26
+ @@ -1,12 +1,15 @@
27
+ def calculate_total(items, tax_rate=0.08):
28
+ + # Calculate total price
29
+ + total = 0
30
+ + for item in items:
31
+ + total += item['price']
32
+ - # Calculate subtotal with discount
33
+ - subtotal = sum(item['price'] * item.get('quantity', 1) for item in items)
34
+ - discount = subtotal * 0.1 if subtotal > 100 else 0
35
+
36
+ + # Add tax
37
+ + tax = total * tax_rate
38
+ + final_total = total + tax
39
+ - # Apply tax to discounted amount
40
+ - taxable_amount = subtotal - discount
41
+ - tax = round(taxable_amount * tax_rate, 2)
42
+ - final_total = taxable_amount + tax
43
+
44
+ + return final_total
45
+ - return {
46
+ - 'subtotal': subtotal,
47
+ - 'discount': discount,
48
+ - 'tax': tax,
49
+ - 'total': final_total
50
+ - }""",
51
+ ),
52
+ "javascript": (
53
+ "app.js",
54
+ """--- a/app.js
55
+ +++ b/app.js
56
+ @@ -1,10 +1,12 @@
57
+ -function fetchUserData(userId) {
58
+ - return fetch(`/api/users/${userId}`)
59
+ - .then(response => response.json())
60
+ - .then(data => {
61
+ - return data.user;
62
+ - })
63
+ - .catch(error => console.error(error));
64
+ +async function fetchUserData(userId) {
65
+ + try {
66
+ + const response = await fetch(`/api/users/${userId}`);
67
+ + const data = await response.json();
68
+ + return data.user;
69
+ + } catch (error) {
70
+ + console.error('Failed to fetch user:', error);
71
+ + throw error;
72
+ + }
73
+ }""",
74
+ ),
75
+ "typescript": (
76
+ "service.ts",
77
+ """--- a/service.ts
78
+ +++ b/service.ts
79
+ @@ -1,8 +1,11 @@
80
+ -class UserService {
81
+ - getUser(id: number) {
82
+ - return this.http.get(`/users/${id}`);
83
+ +interface User {
84
+ + id: number;
85
+ + name: string;
86
+ +}
87
+ +
88
+ +class UserService {
89
+ + async getUser(id: number): Promise<User> {
90
+ + const response = await this.http.get<User>(`/users/${id}`);
91
+ + return response.data;
92
+ }
93
+ - deleteUser(id: number) {
94
+ - return this.http.delete(`/users/${id}`);
95
+ - }
96
+ }""",
97
+ ),
98
+ "rust": (
99
+ "main.rs",
100
+ """--- a/main.rs
101
+ +++ b/main.rs
102
+ @@ -1,8 +1,10 @@
103
+ -fn calculate_sum(numbers: Vec<i32>) -> i32 {
104
+ - let mut total = 0;
105
+ - for num in numbers {
106
+ - total = total + num;
107
+ +fn calculate_sum(numbers: &[i32]) -> i32 {
108
+ + numbers.iter().sum()
109
+ +}
110
+ +
111
+ +fn calculate_average(numbers: &[i32]) -> f64 {
112
+ + if numbers.is_empty() {
113
+ + return 0.0;
114
+ }
115
+ - total
116
+ + calculate_sum(numbers) as f64 / numbers.len() as f64
117
+ }""",
118
+ ),
119
+ "go": (
120
+ "handler.go",
121
+ """--- a/handler.go
122
+ +++ b/handler.go
123
+ @@ -1,10 +1,15 @@
124
+ -func HandleRequest(w http.ResponseWriter, r *http.Request) {
125
+ - data := getData()
126
+ - json.NewEncoder(w).Encode(data)
127
+ +func HandleRequest(w http.ResponseWriter, r *http.Request) error {
128
+ + data, err := getData()
129
+ + if err != nil {
130
+ + http.Error(w, err.Error(), http.StatusInternalServerError)
131
+ + return err
132
+ + }
133
+ + w.Header().Set("Content-Type", "application/json")
134
+ + return json.NewEncoder(w).Encode(data)
135
+ }
136
+
137
+ -func getData() map[string]interface{} {
138
+ - return map[string]interface{}{"status": "ok"}
139
+ +func getData() (map[string]interface{}, error) {
140
+ + return map[string]interface{}{"status": "ok"}, nil
141
+ }""",
142
+ ),
143
+ "java": (
144
+ "Calculator.java",
145
+ """--- a/Calculator.java
146
+ +++ b/Calculator.java
147
+ @@ -1,8 +1,12 @@
148
+ public class Calculator {
149
+ - public int add(int a, int b) {
150
+ - return a + b;
151
+ + public double calculateTotal(List<Double> prices) {
152
+ + return prices.stream()
153
+ + .reduce(0.0, Double::sum);
154
+ }
155
+
156
+ - public int multiply(int a, int b) {
157
+ - return a * b;
158
+ + public double calculateAverage(List<Double> prices) {
159
+ + if (prices.isEmpty()) {
160
+ + return 0.0;
161
+ + }
162
+ + return calculateTotal(prices) / prices.size();
163
+ }
164
+ }""",
165
+ ),
166
+ "ruby": (
167
+ "calculator.rb",
168
+ """--- a/calculator.rb
169
+ +++ b/calculator.rb
170
+ @@ -1,8 +1,10 @@
171
+ class Calculator
172
+ - def add(a, b)
173
+ - a + b
174
+ + def calculate_total(items)
175
+ + items.sum { |item| item[:price] }
176
+ end
177
+
178
+ - def multiply(a, b)
179
+ - a * b
180
+ + def calculate_average(items)
181
+ + return 0 if items.empty?
182
+ +
183
+ + calculate_total(items) / items.size.to_f
184
+ end
185
+ end""",
186
+ ),
187
+ "csharp": (
188
+ "Calculator.cs",
189
+ """--- a/Calculator.cs
190
+ +++ b/Calculator.cs
191
+ @@ -1,10 +1,14 @@
192
+ -public class Calculator {
193
+ - public int Add(int a, int b) {
194
+ - return a + b;
195
+ +public class Calculator
196
+ +{
197
+ + public decimal CalculateTotal(IEnumerable<decimal> prices)
198
+ + {
199
+ + return prices.Sum();
200
+ }
201
+
202
+ - public int Multiply(int a, int b) {
203
+ - return a * b;
204
+ + public decimal CalculateAverage(IEnumerable<decimal> prices)
205
+ + {
206
+ + var priceList = prices.ToList();
207
+ + return priceList.Any() ? priceList.Average() : 0m;
208
+ }
209
+ }""",
210
+ ),
211
+ "php": (
212
+ "Calculator.php",
213
+ """--- a/Calculator.php
214
+ +++ b/Calculator.php
215
+ @@ -1,10 +1,14 @@
216
+ <?php
217
+ class Calculator {
218
+ - public function add($a, $b) {
219
+ - return $a + $b;
220
+ + public function calculateTotal(array $items): float {
221
+ + return array_sum(array_column($items, 'price'));
222
+ }
223
+
224
+ - public function multiply($a, $b) {
225
+ - return $a * $b;
226
+ + public function calculateAverage(array $items): float {
227
+ + if (empty($items)) {
228
+ + return 0.0;
229
+ + }
230
+ + return $this->calculateTotal($items) / count($items);
231
+ }
232
+ }""",
233
+ ),
234
+ "html": (
235
+ "index.html",
236
+ """--- a/index.html
237
+ +++ b/index.html
238
+ @@ -1,5 +1,8 @@
239
+ <div class="container">
240
+ - <h1>Welcome</h1>
241
+ - <p>Hello World</p>
242
+ + <header>
243
+ + <h1>Welcome to Our Site</h1>
244
+ + <nav>
245
+ + <a href="#home">Home</a>
246
+ + <a href="#about">About</a>
247
+ + </nav>
248
+ + </header>
249
+ </div>""",
250
+ ),
251
+ "css": (
252
+ "styles.css",
253
+ """--- a/styles.css
254
+ +++ b/styles.css
255
+ @@ -1,5 +1,10 @@
256
+ .container {
257
+ - width: 100%;
258
+ - padding: 20px;
259
+ + max-width: 1200px;
260
+ + margin: 0 auto;
261
+ + padding: 2rem;
262
+ +}
263
+ +
264
+ +.container header {
265
+ + display: flex;
266
+ + justify-content: space-between;
267
+ + align-items: center;
268
+ }""",
269
+ ),
270
+ "json": (
271
+ "config.json",
272
+ """--- a/config.json
273
+ +++ b/config.json
274
+ @@ -1,5 +1,8 @@
275
+ {
276
+ - "name": "app",
277
+ - "version": "1.0.0"
278
+ + "name": "my-awesome-app",
279
+ + "version": "2.0.0",
280
+ + "description": "An awesome application",
281
+ + "author": "Code Puppy",
282
+ + "license": "MIT"
283
+ }""",
284
+ ),
285
+ "yaml": (
286
+ "config.yml",
287
+ """--- a/config.yml
288
+ +++ b/config.yml
289
+ @@ -1,4 +1,8 @@
290
+ app:
291
+ name: myapp
292
+ - version: 1.0
293
+ + version: 2.0
294
+ + environment: production
295
+ +
296
+ +database:
297
+ + host: localhost
298
+ + port: 5432""",
299
+ ),
300
+ "bash": (
301
+ "deploy.sh",
302
+ """--- a/deploy.sh
303
+ +++ b/deploy.sh
304
+ @@ -1,5 +1,9 @@
305
+ #!/bin/bash
306
+ -echo \"Deploying...\"
307
+ -npm run build
308
+ +set -e
309
+ +
310
+ +echo \"Starting deployment...\"
311
+ +npm run build --production
312
+ +npm run test
313
+ +echo \"Deployment complete!\"""",
314
+ ),
315
+ "sql": (
316
+ "schema.sql",
317
+ """--- a/schema.sql
318
+ +++ b/schema.sql
319
+ @@ -1,5 +1,9 @@
320
+ CREATE TABLE users (
321
+ id INTEGER PRIMARY KEY,
322
+ - name TEXT
323
+ + name TEXT NOT NULL,
324
+ + email TEXT UNIQUE NOT NULL,
325
+ + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
326
+ );
327
+ +
328
+ +CREATE INDEX idx_users_email ON users(email);""",
329
+ ),
330
+ }
331
+
332
+ # Get all supported languages in a consistent order
333
+ SUPPORTED_LANGUAGES = [
334
+ "python",
335
+ "javascript",
336
+ "typescript",
337
+ "rust",
338
+ "go",
339
+ "java",
340
+ "ruby",
341
+ "csharp",
342
+ "php",
343
+ "html",
344
+ "css",
345
+ "json",
346
+ "yaml",
347
+ "bash",
348
+ "sql",
349
+ ]
350
+
351
+
352
+ class DiffConfiguration:
353
+ """Holds the current diff configuration state."""
354
+
355
+ def __init__(self):
356
+ """Initialize configuration from current settings."""
357
+ from code_puppy.config import (
358
+ get_diff_addition_color,
359
+ get_diff_deletion_color,
360
+ )
361
+
362
+ self.current_add_color = get_diff_addition_color()
363
+ self.current_del_color = get_diff_deletion_color()
364
+ self.original_add_color = self.current_add_color
365
+ self.original_del_color = self.current_del_color
366
+ self.current_language_index = 0 # Track current language for preview
367
+
368
+ def has_changes(self) -> bool:
369
+ """Check if any changes have been made."""
370
+ return (
371
+ self.current_add_color != self.original_add_color
372
+ or self.current_del_color != self.original_del_color
373
+ )
374
+
375
+ def next_language(self):
376
+ """Cycle to the next language."""
377
+ self.current_language_index = (self.current_language_index + 1) % len(
378
+ SUPPORTED_LANGUAGES
379
+ )
380
+
381
+ def prev_language(self):
382
+ """Cycle to the previous language."""
383
+ self.current_language_index = (self.current_language_index - 1) % len(
384
+ SUPPORTED_LANGUAGES
385
+ )
386
+
387
+ def get_current_language(self) -> str:
388
+ """Get the currently selected language."""
389
+ return SUPPORTED_LANGUAGES[self.current_language_index]
390
+
391
+
392
+ async def interactive_diff_picker() -> Optional[dict]:
393
+ """Show an interactive full-screen terminal UI to configure diff settings.
394
+
395
+ Returns:
396
+ A dict with changes or None if cancelled
397
+ """
398
+ from code_puppy.tools.command_runner import set_awaiting_user_input
399
+
400
+ config = DiffConfiguration()
401
+
402
+ set_awaiting_user_input(True)
403
+
404
+ # Enter alternate screen buffer once for entire session
405
+ sys.stdout.write("\033[?1049h") # Enter alternate buffer
406
+ sys.stdout.write("\033[2J\033[H") # Clear and home
407
+ sys.stdout.flush()
408
+ time.sleep(0.1) # Minimal delay for state sync
409
+
410
+ try:
411
+ # Main menu loop
412
+ while True:
413
+ choices = [
414
+ "Configure Addition Color",
415
+ "Configure Deletion Color",
416
+ ]
417
+
418
+ if config.has_changes():
419
+ choices.append("Save & Exit")
420
+ else:
421
+ choices.append("Exit")
422
+
423
+ # Dummy update function for main menu (config doesn't change on navigation)
424
+ def dummy_update(choice: str):
425
+ pass
426
+
427
+ def get_main_preview():
428
+ return _get_preview_text_for_prompt_toolkit(config)
429
+
430
+ try:
431
+ selected = await _split_panel_selector(
432
+ "Diff Color Configuration",
433
+ choices,
434
+ dummy_update,
435
+ get_preview=get_main_preview,
436
+ config=config,
437
+ )
438
+ except KeyboardInterrupt:
439
+ break
440
+
441
+ # Handle selection
442
+ if "Addition" in selected:
443
+ await _handle_color_menu(config, "additions")
444
+ elif "Deletion" in selected:
445
+ await _handle_color_menu(config, "deletions")
446
+ else:
447
+ # Exit
448
+ break
449
+
450
+ except Exception:
451
+ # Silent error - just exit cleanly
452
+ return None
453
+ finally:
454
+ set_awaiting_user_input(False)
455
+ # Exit alternate screen buffer once at end
456
+ sys.stdout.write("\033[?1049l") # Exit alternate buffer
457
+ sys.stdout.flush()
458
+
459
+ # Clear exit message
460
+ from code_puppy.messaging import emit_info
461
+
462
+ emit_info("✓ Exited diff color configuration")
463
+
464
+ # Return changes if any
465
+ if config.has_changes():
466
+ return {
467
+ "add_color": config.current_add_color,
468
+ "del_color": config.current_del_color,
469
+ }
470
+
471
+ return None
472
+
473
+
474
+ async def _split_panel_selector(
475
+ title: str,
476
+ choices: list[str],
477
+ on_change: Callable[[str], None],
478
+ get_preview: Callable[[], ANSI],
479
+ config: Optional[DiffConfiguration] = None,
480
+ ) -> Optional[str]:
481
+ """Split-panel selector with menu on left and live preview on right.
482
+
483
+ Supports left/right arrow navigation through languages if config is provided.
484
+ """
485
+ selected_index = [0]
486
+ result = [None]
487
+
488
+ def get_left_panel_text():
489
+ """Generate the selector menu text."""
490
+ try:
491
+ lines = []
492
+ lines.append(("bold cyan", title))
493
+ lines.append(("", "\n\n"))
494
+
495
+ if not choices:
496
+ lines.append(("fg:ansiyellow", "No choices available"))
497
+ lines.append(("", "\n"))
498
+ else:
499
+ for i, choice in enumerate(choices):
500
+ if i == selected_index[0]:
501
+ lines.append(("fg:ansigreen", "▶ "))
502
+ lines.append(("fg:ansigreen bold", choice))
503
+ else:
504
+ lines.append(("", " "))
505
+ lines.append(("", choice))
506
+ lines.append(("", "\n"))
507
+
508
+ lines.append(("", "\n"))
509
+
510
+ # Add language navigation hint if config is available
511
+ if config is not None:
512
+ current_lang = config.get_current_language()
513
+ lang_hint = f"Language: {current_lang.upper()} (←→ to change)"
514
+ lines.append(("fg:ansiyellow", lang_hint))
515
+ lines.append(("", "\n"))
516
+
517
+ lines.append(
518
+ ("fg:ansicyan", "↑↓ Navigate │ Enter Confirm │ Ctrl-C Cancel")
519
+ )
520
+ return FormattedText(lines)
521
+ except Exception as e:
522
+ return FormattedText([("fg:ansired", f"Error: {e}")])
523
+
524
+ def get_right_panel_text():
525
+ """Generate the preview panel text."""
526
+ try:
527
+ preview = get_preview()
528
+ # get_preview() now returns ANSI, which is already FormattedText-compatible
529
+ return preview
530
+ except Exception as e:
531
+ return FormattedText([("fg:ansired", f"Preview error: {e}")])
532
+
533
+ kb = KeyBindings()
534
+
535
+ @kb.add("up")
536
+ def move_up(event):
537
+ if choices:
538
+ selected_index[0] = (selected_index[0] - 1) % len(choices)
539
+ on_change(choices[selected_index[0]])
540
+ event.app.invalidate()
541
+
542
+ @kb.add("down")
543
+ def move_down(event):
544
+ if choices:
545
+ selected_index[0] = (selected_index[0] + 1) % len(choices)
546
+ on_change(choices[selected_index[0]])
547
+ event.app.invalidate()
548
+
549
+ @kb.add("left")
550
+ def prev_lang(event):
551
+ if config is not None:
552
+ config.prev_language()
553
+ event.app.invalidate()
554
+
555
+ @kb.add("right")
556
+ def next_lang(event):
557
+ if config is not None:
558
+ config.next_language()
559
+ event.app.invalidate()
560
+
561
+ @kb.add("enter")
562
+ def accept(event):
563
+ if choices:
564
+ result[0] = choices[selected_index[0]]
565
+ else:
566
+ result[0] = None
567
+ event.app.exit()
568
+
569
+ @kb.add("c-c")
570
+ def cancel(event):
571
+ result[0] = None
572
+ event.app.exit()
573
+
574
+ # Create split layout with left (selector) and right (preview) panels
575
+ left_panel = Window(
576
+ content=FormattedTextControl(lambda: get_left_panel_text()),
577
+ width=50,
578
+ )
579
+
580
+ right_panel = Window(
581
+ content=FormattedTextControl(lambda: get_right_panel_text()),
582
+ )
583
+
584
+ # Create vertical split (side-by-side panels)
585
+ root_container = VSplit(
586
+ [
587
+ Frame(left_panel, title="Menu"),
588
+ Frame(right_panel, title="Preview"),
589
+ ]
590
+ )
591
+
592
+ layout = Layout(root_container)
593
+ app = Application(
594
+ layout=layout,
595
+ key_bindings=kb,
596
+ full_screen=False, # Don't use full_screen to avoid buffer issues
597
+ mouse_support=False,
598
+ color_depth="DEPTH_24_BIT", # Enable truecolor support
599
+ )
600
+
601
+ sys.stdout.flush()
602
+ sys.stdout.flush()
603
+
604
+ # Trigger initial update only if choices is not empty
605
+ if choices:
606
+ on_change(choices[selected_index[0]])
607
+
608
+ # Just clear the current buffer (don't switch buffers)
609
+ sys.stdout.write("\033[2J\033[H") # Clear screen within current buffer
610
+ sys.stdout.flush()
611
+
612
+ # Run application (stays in same alternate buffer)
613
+ await app.run_async()
614
+
615
+ if result[0] is None:
616
+ raise KeyboardInterrupt()
617
+
618
+ return result[0]
619
+
620
+
621
+ ADDITION_COLORS = {
622
+ # primary first (darkened)
623
+ "dark green": "#0b3e0b",
624
+ "darker green": "#0b1f0b",
625
+ "dark aqua": "#164952",
626
+ "deep teal": "#143f3c",
627
+ # blues (darkened)
628
+ "sky blue": "#406884",
629
+ "soft blue": "#315c78",
630
+ "steel blue": "#20394e",
631
+ "forest teal": "#124831",
632
+ "cool teal": "#1b4b54",
633
+ "marine aqua": "#275860",
634
+ "slate blue": "#304f69",
635
+ "deep steel": "#1e3748",
636
+ "shadow olive": "#2f3a15",
637
+ "deep moss": "#1f3310",
638
+ # G
639
+ "midnight spruce": "#0f3a29",
640
+ "shadow jade": "#0d4a3a",
641
+ # B
642
+ "abyss blue": "#0d2f4d",
643
+ "midnight fjord": "#133552",
644
+ # I
645
+ "dusky indigo": "#1a234d",
646
+ "nocturne indigo": "#161d3f",
647
+ # V
648
+ "midnight violet": "#2a1a3f",
649
+ "deep amethyst": "#3a2860",
650
+ }
651
+
652
+ DELETION_COLORS = {
653
+ # primary first (darkened)
654
+ "dark red": "#4a0f0f",
655
+ # pinks / reds (darkened)
656
+ "pink": "#7f143b",
657
+ "soft red": "#741f3c",
658
+ "salmon": "#842848",
659
+ "rose": "#681c35",
660
+ "deep rose": "#4f1428",
661
+ # oranges (darkened)
662
+ "burnt orange": "#753b10",
663
+ "deep orange": "#5b2b0d",
664
+ # yellows (darkened)
665
+ "amber": "#69551c",
666
+ # reds (darkened)
667
+ "red": "#5d0b0b",
668
+ "ruby": "#5b141f",
669
+ "wine": "#390e1a",
670
+ # purples (darkened)
671
+ "purple": "#5a4284",
672
+ "soft purple": "#503977",
673
+ "violet": "#432758",
674
+ # ROYGBIV deletions (unchanged)
675
+ # R
676
+ "ember crimson": "#5a0e12",
677
+ "smoked ruby": "#4b0b16",
678
+ # O
679
+ "molten orange": "#70340c",
680
+ "baked amber": "#5c2b0a",
681
+ # Y
682
+ "burnt ochre": "#5a4110",
683
+ "tawny umber": "#4c3810",
684
+ # G
685
+ "swamp olive": "#3c3a14",
686
+ "bog green": "#343410",
687
+ # B
688
+ "dusky petrol": "#2a3744",
689
+ "warm slate": "#263038",
690
+ # I
691
+ "wine indigo": "#311b3f",
692
+ "mulberry dusk": "#3f1f52",
693
+ # V
694
+ "garnet plum": "#4a1e3a",
695
+ "dusky magenta": "#5a1f4c",
696
+ }
697
+
698
+
699
+ def _convert_rich_color_to_prompt_toolkit(color: str) -> str:
700
+ """Convert Rich color names to prompt-toolkit compatible names."""
701
+ # Hex colors pass through as-is
702
+ if color.startswith("#"):
703
+ return color
704
+ # Map bright_ colors to ansi equivalents
705
+ if color.startswith("bright_"):
706
+ return "ansi" + color.replace("bright_", "")
707
+ # Basic terminal colors
708
+ if color.lower() in [
709
+ "black",
710
+ "red",
711
+ "green",
712
+ "yellow",
713
+ "blue",
714
+ "magenta",
715
+ "cyan",
716
+ "white",
717
+ "gray",
718
+ "grey",
719
+ ]:
720
+ return color.lower()
721
+ # Default safe fallback for unknown colors
722
+ return "white"
723
+
724
+
725
+ def _get_preview_text_for_prompt_toolkit(config: DiffConfiguration) -> ANSI:
726
+ """Get preview as ANSI for embedding in selector with live colors.
727
+
728
+ Returns ANSI-formatted text that prompt_toolkit can render with full colors.
729
+ """
730
+ from code_puppy.tools.common import format_diff_with_colors
731
+
732
+ # Get the current language and its sample
733
+ current_lang = config.get_current_language()
734
+ filename, sample_diff = LANGUAGE_SAMPLES.get(
735
+ current_lang,
736
+ LANGUAGE_SAMPLES["python"], # Fallback to Python
737
+ )
738
+
739
+ # Build header with current settings info using Rich markup
740
+ header_parts = []
741
+ header_parts.append("[bold]═" * 50 + "[/bold]")
742
+ header_parts.append(
743
+ "[bold cyan] LIVE PREVIEW - Syntax Highlighted Diff[/bold cyan]"
744
+ )
745
+ header_parts.append("[bold]═" * 50 + "[/bold]")
746
+ header_parts.append("")
747
+ header_parts.append(f" Addition Color: [bold]{config.current_add_color}[/bold]")
748
+ header_parts.append(f" Deletion Color: [bold]{config.current_del_color}[/bold]")
749
+ header_parts.append("")
750
+ header_parts.append(
751
+ f" [bold yellow]Language: {current_lang.upper()}[/bold yellow] "
752
+ f"[dim](← → to cycle)[/dim]"
753
+ )
754
+ header_parts.append("")
755
+ header_parts.append(f"[bold] Example ({filename}):[/bold]")
756
+ header_parts.append("")
757
+
758
+ header_text = "\n".join(header_parts)
759
+
760
+ # Temporarily override config to use current preview settings
761
+ from code_puppy.config import (
762
+ get_diff_addition_color,
763
+ get_diff_deletion_color,
764
+ set_diff_addition_color,
765
+ set_diff_deletion_color,
766
+ )
767
+
768
+ # Save original values
769
+ original_add_color = get_diff_addition_color()
770
+ original_del_color = get_diff_deletion_color()
771
+
772
+ try:
773
+ # Temporarily set config to preview values
774
+ set_diff_addition_color(config.current_add_color)
775
+ set_diff_deletion_color(config.current_del_color)
776
+
777
+ # Get the formatted diff (either Rich Text or Rich markup string)
778
+ formatted_diff = format_diff_with_colors(sample_diff)
779
+
780
+ # Render everything with Rich Console to get ANSI output with proper color support
781
+ buffer = io.StringIO()
782
+ console = Console(
783
+ file=buffer,
784
+ force_terminal=True,
785
+ width=90,
786
+ legacy_windows=False,
787
+ color_system="truecolor",
788
+ no_color=False,
789
+ force_interactive=True, # Force interactive mode for better color support
790
+ )
791
+
792
+ # Print header
793
+ console.print(header_text, end="\n")
794
+
795
+ # Print diff (handles both Text objects and markup strings)
796
+ console.print(formatted_diff, end="\n\n")
797
+
798
+ # Print footer
799
+ console.print("[bold]═" * 50 + "[/bold]", end="")
800
+
801
+ ansi_output = buffer.getvalue()
802
+
803
+ finally:
804
+ # Restore original config values
805
+ set_diff_addition_color(original_add_color)
806
+ set_diff_deletion_color(original_del_color)
807
+
808
+ # Wrap in ANSI() so prompt_toolkit can render it
809
+ return ANSI(ansi_output)
810
+
811
+
812
+ async def _handle_color_menu(config: DiffConfiguration, color_type: str) -> None:
813
+ """Handle color selection with live preview updates."""
814
+ # Text mode only (highlighted disabled)
815
+ if color_type == "additions":
816
+ color_dict = ADDITION_COLORS
817
+ current = config.current_add_color
818
+ title = "Select addition color:"
819
+ else:
820
+ color_dict = DELETION_COLORS
821
+ current = config.current_del_color
822
+ title = "Select deletion color:"
823
+
824
+ # Build choices with nice names
825
+ choices = []
826
+ for name, color_value in color_dict.items():
827
+ marker = " ← current" if color_value == current else ""
828
+ choices.append(f"{name}{marker}")
829
+
830
+ # Store original color for potential cancellation
831
+ original_color = current
832
+
833
+ # Callback for live preview updates
834
+ def update_preview(selected_choice: str):
835
+ # Extract color name and look up the actual color value
836
+ color_name = selected_choice.replace(" ← current", "").strip()
837
+ selected_color = color_dict.get(color_name, list(color_dict.values())[0])
838
+ if color_type == "additions":
839
+ config.current_add_color = selected_color
840
+ else:
841
+ config.current_del_color = selected_color
842
+
843
+ # Function to get live preview header with colored diff
844
+ def get_preview_header():
845
+ return _get_preview_text_for_prompt_toolkit(config)
846
+
847
+ try:
848
+ # Use split panel selector with live preview (pass config for language switching)
849
+ await _split_panel_selector(
850
+ title,
851
+ choices,
852
+ update_preview,
853
+ get_preview=get_preview_header,
854
+ config=config,
855
+ )
856
+ except KeyboardInterrupt:
857
+ # Restore original color on cancel
858
+ if color_type == "additions":
859
+ config.current_add_color = original_color
860
+ else:
861
+ config.current_del_color = original_color
862
+ except Exception:
863
+ pass # Silent error handling