code-puppy 0.0.302__py3-none-any.whl → 0.0.323__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 (65) hide show
  1. code_puppy/agents/base_agent.py +373 -46
  2. code_puppy/chatgpt_codex_client.py +283 -0
  3. code_puppy/cli_runner.py +795 -0
  4. code_puppy/command_line/add_model_menu.py +8 -1
  5. code_puppy/command_line/autosave_menu.py +266 -35
  6. code_puppy/command_line/colors_menu.py +515 -0
  7. code_puppy/command_line/command_handler.py +8 -2
  8. code_puppy/command_line/config_commands.py +59 -10
  9. code_puppy/command_line/core_commands.py +19 -7
  10. code_puppy/command_line/mcp/edit_command.py +3 -1
  11. code_puppy/command_line/mcp/handler.py +7 -2
  12. code_puppy/command_line/mcp/install_command.py +8 -3
  13. code_puppy/command_line/mcp/logs_command.py +173 -64
  14. code_puppy/command_line/mcp/restart_command.py +7 -2
  15. code_puppy/command_line/mcp/search_command.py +10 -4
  16. code_puppy/command_line/mcp/start_all_command.py +16 -6
  17. code_puppy/command_line/mcp/start_command.py +3 -1
  18. code_puppy/command_line/mcp/status_command.py +2 -1
  19. code_puppy/command_line/mcp/stop_all_command.py +5 -1
  20. code_puppy/command_line/mcp/stop_command.py +3 -1
  21. code_puppy/command_line/mcp/wizard_utils.py +10 -4
  22. code_puppy/command_line/model_settings_menu.py +53 -7
  23. code_puppy/command_line/prompt_toolkit_completion.py +16 -2
  24. code_puppy/command_line/session_commands.py +11 -4
  25. code_puppy/config.py +103 -15
  26. code_puppy/keymap.py +8 -2
  27. code_puppy/main.py +5 -828
  28. code_puppy/mcp_/__init__.py +17 -0
  29. code_puppy/mcp_/blocking_startup.py +61 -32
  30. code_puppy/mcp_/config_wizard.py +5 -1
  31. code_puppy/mcp_/managed_server.py +23 -3
  32. code_puppy/mcp_/manager.py +65 -0
  33. code_puppy/mcp_/mcp_logs.py +224 -0
  34. code_puppy/messaging/__init__.py +20 -4
  35. code_puppy/messaging/bus.py +64 -0
  36. code_puppy/messaging/markdown_patches.py +57 -0
  37. code_puppy/messaging/messages.py +16 -0
  38. code_puppy/messaging/renderers.py +21 -9
  39. code_puppy/messaging/rich_renderer.py +113 -67
  40. code_puppy/messaging/spinner/console_spinner.py +34 -0
  41. code_puppy/model_factory.py +185 -30
  42. code_puppy/model_utils.py +57 -48
  43. code_puppy/models.json +19 -5
  44. code_puppy/plugins/chatgpt_oauth/config.py +5 -1
  45. code_puppy/plugins/chatgpt_oauth/oauth_flow.py +5 -6
  46. code_puppy/plugins/chatgpt_oauth/register_callbacks.py +3 -3
  47. code_puppy/plugins/chatgpt_oauth/test_plugin.py +26 -11
  48. code_puppy/plugins/chatgpt_oauth/utils.py +180 -65
  49. code_puppy/plugins/claude_code_oauth/register_callbacks.py +28 -0
  50. code_puppy/plugins/claude_code_oauth/utils.py +1 -0
  51. code_puppy/plugins/shell_safety/agent_shell_safety.py +1 -118
  52. code_puppy/plugins/shell_safety/register_callbacks.py +44 -3
  53. code_puppy/prompts/codex_system_prompt.md +310 -0
  54. code_puppy/pydantic_patches.py +131 -0
  55. code_puppy/terminal_utils.py +126 -0
  56. code_puppy/tools/agent_tools.py +34 -9
  57. code_puppy/tools/command_runner.py +361 -32
  58. code_puppy/tools/file_operations.py +33 -45
  59. {code_puppy-0.0.302.data → code_puppy-0.0.323.data}/data/code_puppy/models.json +19 -5
  60. {code_puppy-0.0.302.dist-info → code_puppy-0.0.323.dist-info}/METADATA +1 -1
  61. {code_puppy-0.0.302.dist-info → code_puppy-0.0.323.dist-info}/RECORD +65 -57
  62. {code_puppy-0.0.302.data → code_puppy-0.0.323.data}/data/code_puppy/models_dev_api.json +0 -0
  63. {code_puppy-0.0.302.dist-info → code_puppy-0.0.323.dist-info}/WHEEL +0 -0
  64. {code_puppy-0.0.302.dist-info → code_puppy-0.0.323.dist-info}/entry_points.txt +0 -0
  65. {code_puppy-0.0.302.dist-info → code_puppy-0.0.323.dist-info}/licenses/LICENSE +0 -0
@@ -0,0 +1,515 @@
1
+ """Interactive TUI for configuring banner colors.
2
+
3
+ Similar to diff_menu.py but for customizing the banner background colors
4
+ for different tool outputs (THINKING, SHELL COMMAND, READ FILE, etc.).
5
+
6
+ Use /colors to launch the TUI and customize your banners!
7
+ """
8
+
9
+ import io
10
+ import sys
11
+ import time
12
+ from typing import Callable, Optional
13
+
14
+ from prompt_toolkit import Application
15
+ from prompt_toolkit.formatted_text import ANSI, FormattedText
16
+ from prompt_toolkit.key_binding import KeyBindings
17
+ from prompt_toolkit.layout import Layout, VSplit, Window
18
+ from prompt_toolkit.layout.controls import FormattedTextControl
19
+ from prompt_toolkit.widgets import Frame
20
+ from rich.console import Console
21
+
22
+ # Banner display names with icons
23
+ BANNER_DISPLAY_INFO = {
24
+ "thinking": ("THINKING", "⚡"),
25
+ "agent_response": ("AGENT RESPONSE", ""),
26
+ "shell_command": ("SHELL COMMAND", "🚀"),
27
+ "read_file": ("READ FILE", "📂"),
28
+ "edit_file": ("EDIT FILE", "✏️"),
29
+ "grep": ("GREP", "📂"),
30
+ "directory_listing": ("DIRECTORY LISTING", "📂"),
31
+ "agent_reasoning": ("AGENT REASONING", ""),
32
+ "invoke_agent": ("🤖 INVOKE AGENT", ""),
33
+ "subagent_response": ("✓ AGENT RESPONSE", ""),
34
+ "list_agents": ("LIST AGENTS", ""),
35
+ }
36
+
37
+ # Sample content to show after each banner
38
+ BANNER_SAMPLE_CONTENT = {
39
+ "thinking": "Let me analyze this code structure and figure out the best approach...",
40
+ "agent_response": "I've implemented the feature you requested. The changes include...",
41
+ "shell_command": "$ npm run test -- --silent\n⏱ Timeout: 60s",
42
+ "read_file": "/path/to/file.py (lines 1-50)",
43
+ "edit_file": "MODIFY /path/to/file.py\n--- a/file.py\n+++ b/file.py",
44
+ "grep": "/src for 'handleClick'\n📄 Button.tsx (3 matches)",
45
+ "directory_listing": "/src (recursive=True)\n📁 components/\n └── Button.tsx",
46
+ "agent_reasoning": "Current reasoning:\nI need to refactor this function...",
47
+ "invoke_agent": "code-reviewer (New session)\nSession: review-auth-abc123",
48
+ "subagent_response": "code-reviewer\nThe code looks good overall...",
49
+ "list_agents": "- code-puppy: Code Puppy 🐶\n- planning-agent: Planning Agent",
50
+ }
51
+
52
+ # Available background colors grouped by theme
53
+ BANNER_COLORS = {
54
+ # Cool colors
55
+ "blue": "blue",
56
+ "dark blue": "dark_blue",
57
+ "navy blue": "navy_blue",
58
+ "deep sky blue": "deep_sky_blue4",
59
+ "steel blue": "steel_blue",
60
+ "dodger blue": "dodger_blue3",
61
+ # Cyans & Teals
62
+ "dark cyan": "dark_cyan",
63
+ "cyan": "cyan4",
64
+ "teal": "dark_turquoise",
65
+ "aquamarine": "aquamarine1",
66
+ # Greens
67
+ "green": "green4",
68
+ "dark green": "dark_green",
69
+ "sea green": "dark_sea_green4",
70
+ "spring green": "spring_green4",
71
+ "chartreuse": "chartreuse4",
72
+ # Purples & Magentas
73
+ "purple": "purple",
74
+ "dark magenta": "dark_magenta",
75
+ "medium purple": "medium_purple4",
76
+ "dark violet": "dark_violet",
77
+ "plum": "plum4",
78
+ "orchid": "dark_orchid",
79
+ # Reds & Oranges
80
+ "red": "red3",
81
+ "dark red": "dark_red",
82
+ "indian red": "indian_red",
83
+ "orange red": "orange_red1",
84
+ "orange": "dark_orange3",
85
+ # Yellows & Golds
86
+ "gold": "gold3",
87
+ "dark goldenrod": "dark_goldenrod",
88
+ "olive": "dark_olive_green3",
89
+ # Grays
90
+ "grey30": "grey30",
91
+ "grey37": "grey37",
92
+ "grey42": "grey42",
93
+ "grey50": "grey50",
94
+ "grey58": "grey58",
95
+ "dark slate gray": "dark_slate_gray3",
96
+ # Pink tones
97
+ "hot pink": "hot_pink3",
98
+ "deep pink": "deep_pink4",
99
+ "pale violet red": "pale_violet_red1",
100
+ }
101
+
102
+
103
+ class ColorConfiguration:
104
+ """Holds the current banner color configuration state."""
105
+
106
+ def __init__(self):
107
+ """Initialize configuration from current settings."""
108
+ from code_puppy.config import get_all_banner_colors
109
+
110
+ self.current_colors = get_all_banner_colors()
111
+ self.original_colors = self.current_colors.copy()
112
+ self.selected_banner_index = 0
113
+ self.banner_keys = list(BANNER_DISPLAY_INFO.keys())
114
+
115
+ def has_changes(self) -> bool:
116
+ """Check if any changes have been made."""
117
+ return self.current_colors != self.original_colors
118
+
119
+ def get_current_banner_key(self) -> str:
120
+ """Get the currently selected banner key."""
121
+ return self.banner_keys[self.selected_banner_index]
122
+
123
+ def get_current_banner_color(self) -> str:
124
+ """Get the color of the currently selected banner."""
125
+ return self.current_colors[self.get_current_banner_key()]
126
+
127
+ def set_current_banner_color(self, color: str):
128
+ """Set the color of the currently selected banner."""
129
+ self.current_colors[self.get_current_banner_key()] = color
130
+
131
+ def next_banner(self):
132
+ """Cycle to the next banner."""
133
+ self.selected_banner_index = (self.selected_banner_index + 1) % len(
134
+ self.banner_keys
135
+ )
136
+
137
+ def prev_banner(self):
138
+ """Cycle to the previous banner."""
139
+ self.selected_banner_index = (self.selected_banner_index - 1) % len(
140
+ self.banner_keys
141
+ )
142
+
143
+
144
+ async def interactive_colors_picker() -> Optional[dict]:
145
+ """Show an interactive full-screen terminal UI to configure banner colors.
146
+
147
+ Returns:
148
+ A dict with changes or None if cancelled
149
+ """
150
+ from code_puppy.tools.command_runner import set_awaiting_user_input
151
+
152
+ config = ColorConfiguration()
153
+
154
+ set_awaiting_user_input(True)
155
+
156
+ # Enter alternate screen buffer once for entire session
157
+ sys.stdout.write("\033[?1049h") # Enter alternate buffer
158
+ sys.stdout.write("\033[2J\033[H") # Clear and home
159
+ sys.stdout.flush()
160
+ time.sleep(0.1) # Minimal delay for state sync
161
+
162
+ try:
163
+ # Main menu loop
164
+ while True:
165
+ choices = []
166
+ for key in config.banner_keys:
167
+ display_name, icon = BANNER_DISPLAY_INFO[key]
168
+ current_color = config.current_colors[key]
169
+ choices.append(f"{display_name} [{current_color}]")
170
+
171
+ # Add action items
172
+ if config.has_changes():
173
+ choices.append("─── Actions ───")
174
+ choices.append("💾 Save & Exit")
175
+ choices.append("🔄 Reset All to Defaults")
176
+ choices.append("❌ Discard & Exit")
177
+ else:
178
+ choices.append("─── Actions ───")
179
+ choices.append("🔄 Reset All to Defaults")
180
+ choices.append("❌ Exit")
181
+
182
+ def dummy_update(choice: str):
183
+ pass
184
+
185
+ def get_main_preview():
186
+ return _get_preview_text_for_prompt_toolkit(config)
187
+
188
+ try:
189
+ selected = await _split_panel_selector(
190
+ "Banner Color Configuration",
191
+ choices,
192
+ dummy_update,
193
+ get_preview=get_main_preview,
194
+ config=config,
195
+ )
196
+ except KeyboardInterrupt:
197
+ break
198
+
199
+ # Handle selection
200
+ if selected is None:
201
+ break
202
+ elif "Save & Exit" in selected:
203
+ break
204
+ elif "Reset All" in selected:
205
+ from code_puppy.config import DEFAULT_BANNER_COLORS
206
+
207
+ config.current_colors = DEFAULT_BANNER_COLORS.copy()
208
+ elif "Discard" in selected or selected == "❌ Exit":
209
+ config.current_colors = config.original_colors.copy()
210
+ break
211
+ elif "───" in selected:
212
+ # Separator - do nothing
213
+ pass
214
+ else:
215
+ # A banner was selected - show color picker
216
+ # Find which banner was selected
217
+ for i, key in enumerate(config.banner_keys):
218
+ display_name, _ = BANNER_DISPLAY_INFO[key]
219
+ if selected.startswith(display_name):
220
+ config.selected_banner_index = i
221
+ await _handle_color_menu(config)
222
+ break
223
+
224
+ except Exception:
225
+ # Silent error - just exit cleanly
226
+ return None
227
+ finally:
228
+ set_awaiting_user_input(False)
229
+ # Exit alternate screen buffer once at end
230
+ sys.stdout.write("\033[?1049l") # Exit alternate buffer
231
+ sys.stdout.flush()
232
+
233
+ # Return changes if any
234
+ if config.has_changes():
235
+ return config.current_colors
236
+
237
+ return None
238
+
239
+
240
+ async def _split_panel_selector(
241
+ title: str,
242
+ choices: list[str],
243
+ on_change: Callable[[str], None],
244
+ get_preview: Callable[[], ANSI],
245
+ config: Optional[ColorConfiguration] = None,
246
+ ) -> Optional[str]:
247
+ """Split-panel selector with menu on left and live preview on right."""
248
+ selected_index = [0]
249
+ result = [None]
250
+
251
+ def get_left_panel_text():
252
+ """Generate the selector menu text."""
253
+ try:
254
+ lines = []
255
+ lines.append(("bold cyan", title))
256
+ lines.append(("", "\n\n"))
257
+
258
+ if not choices:
259
+ lines.append(("fg:ansiyellow", "No choices available"))
260
+ lines.append(("", "\n"))
261
+ else:
262
+ for i, choice in enumerate(choices):
263
+ # Skip separator lines for selection highlighting
264
+ if "───" in choice:
265
+ lines.append(("fg:ansigray", f" {choice}"))
266
+ lines.append(("", "\n"))
267
+ elif i == selected_index[0]:
268
+ lines.append(("fg:ansigreen", "▶ "))
269
+ lines.append(("fg:ansigreen bold", choice))
270
+ lines.append(("", "\n"))
271
+ else:
272
+ lines.append(("", " "))
273
+ lines.append(("", choice))
274
+ lines.append(("", "\n"))
275
+
276
+ lines.append(("", "\n"))
277
+ lines.append(
278
+ ("fg:ansicyan", "↑↓ Navigate │ Enter Select │ Ctrl-C Cancel")
279
+ )
280
+ return FormattedText(lines)
281
+ except Exception as e:
282
+ return FormattedText([("fg:ansired", f"Error: {e}")])
283
+
284
+ def get_right_panel_text():
285
+ """Generate the preview panel text."""
286
+ try:
287
+ preview = get_preview()
288
+ return preview
289
+ except Exception as e:
290
+ return FormattedText([("fg:ansired", f"Preview error: {e}")])
291
+
292
+ kb = KeyBindings()
293
+
294
+ @kb.add("up")
295
+ def move_up(event):
296
+ if choices:
297
+ # Skip separator lines
298
+ new_idx = (selected_index[0] - 1) % len(choices)
299
+ while "───" in choices[new_idx]:
300
+ new_idx = (new_idx - 1) % len(choices)
301
+ selected_index[0] = new_idx
302
+ on_change(choices[selected_index[0]])
303
+ event.app.invalidate()
304
+
305
+ @kb.add("down")
306
+ def move_down(event):
307
+ if choices:
308
+ # Skip separator lines
309
+ new_idx = (selected_index[0] + 1) % len(choices)
310
+ while "───" in choices[new_idx]:
311
+ new_idx = (new_idx + 1) % len(choices)
312
+ selected_index[0] = new_idx
313
+ on_change(choices[selected_index[0]])
314
+ event.app.invalidate()
315
+
316
+ @kb.add("enter")
317
+ def accept(event):
318
+ if choices:
319
+ result[0] = choices[selected_index[0]]
320
+ else:
321
+ result[0] = None
322
+ event.app.exit()
323
+
324
+ @kb.add("c-c")
325
+ def cancel(event):
326
+ result[0] = None
327
+ event.app.exit()
328
+
329
+ # Create split layout with left (selector) and right (preview) panels
330
+ left_panel = Window(
331
+ content=FormattedTextControl(lambda: get_left_panel_text()),
332
+ width=45,
333
+ )
334
+
335
+ right_panel = Window(
336
+ content=FormattedTextControl(lambda: get_right_panel_text()),
337
+ )
338
+
339
+ # Create vertical split (side-by-side panels)
340
+ root_container = VSplit(
341
+ [
342
+ Frame(left_panel, title="Menu"),
343
+ Frame(right_panel, title="Preview"),
344
+ ]
345
+ )
346
+
347
+ layout = Layout(root_container)
348
+ app = Application(
349
+ layout=layout,
350
+ key_bindings=kb,
351
+ full_screen=False,
352
+ mouse_support=False,
353
+ color_depth="DEPTH_24_BIT",
354
+ )
355
+
356
+ sys.stdout.flush()
357
+
358
+ # Trigger initial update only if choices is not empty
359
+ if choices:
360
+ on_change(choices[selected_index[0]])
361
+
362
+ # Clear the current buffer
363
+ sys.stdout.write("\033[2J\033[H")
364
+ sys.stdout.flush()
365
+
366
+ # Run application
367
+ await app.run_async()
368
+
369
+ if result[0] is None:
370
+ raise KeyboardInterrupt()
371
+
372
+ return result[0]
373
+
374
+
375
+ def _get_preview_text_for_prompt_toolkit(config: ColorConfiguration) -> ANSI:
376
+ """Get preview as ANSI for embedding in selector with live colors.
377
+
378
+ Returns ANSI-formatted text that prompt_toolkit can render with full colors.
379
+ """
380
+ # Build preview showing all banners with their current colors
381
+ buffer = io.StringIO()
382
+ console = Console(
383
+ file=buffer,
384
+ force_terminal=True,
385
+ width=70,
386
+ legacy_windows=False,
387
+ color_system="truecolor",
388
+ no_color=False,
389
+ force_interactive=True,
390
+ )
391
+
392
+ # Header
393
+ console.print("[bold]═" * 60 + "[/bold]")
394
+ console.print("[bold cyan] LIVE PREVIEW - Banner Colors[/bold cyan]")
395
+ console.print("[bold]═" * 60 + "[/bold]")
396
+ console.print()
397
+
398
+ # Show each banner with its current color
399
+ for key in config.banner_keys:
400
+ display_name, icon = BANNER_DISPLAY_INFO[key]
401
+ color = config.current_colors[key]
402
+ sample = BANNER_SAMPLE_CONTENT[key]
403
+
404
+ # Highlight the currently selected banner
405
+ is_selected = key == config.get_current_banner_key()
406
+ if is_selected:
407
+ console.print("[bold yellow]▶[/bold yellow] ", end="")
408
+ else:
409
+ console.print(" ", end="")
410
+
411
+ # Print the banner with its configured color
412
+ icon_str = f" {icon}" if icon else ""
413
+ banner_text = (
414
+ f"[bold white on {color}] {display_name} [/bold white on {color}]{icon_str}"
415
+ )
416
+ console.print(banner_text)
417
+
418
+ # Print sample content (dimmed)
419
+ sample_lines = sample.split("\n")
420
+ for line in sample_lines[:2]: # Only show first 2 lines
421
+ if is_selected:
422
+ console.print(f" [dim]{line}[/dim]")
423
+ else:
424
+ console.print(f" [dim]{line}[/dim]")
425
+ console.print()
426
+
427
+ console.print("[bold]═" * 60 + "[/bold]")
428
+
429
+ ansi_output = buffer.getvalue()
430
+ return ANSI(ansi_output)
431
+
432
+
433
+ async def _handle_color_menu(config: ColorConfiguration) -> None:
434
+ """Handle color selection for the current banner."""
435
+ banner_key = config.get_current_banner_key()
436
+ display_name, _ = BANNER_DISPLAY_INFO[banner_key]
437
+ current_color = config.get_current_banner_color()
438
+ title = f"Select color for {display_name}:"
439
+
440
+ # Build choices with color names
441
+ choices = []
442
+ for name, color_value in BANNER_COLORS.items():
443
+ marker = " ← current" if color_value == current_color else ""
444
+ choices.append(f"{name}{marker}")
445
+
446
+ # Store original color for potential cancellation
447
+ original_color = current_color
448
+
449
+ # Callback for live preview updates
450
+ def update_preview(selected_choice: str):
451
+ color_name = selected_choice.replace(" ← current", "").strip()
452
+ selected_color = BANNER_COLORS.get(color_name, "blue")
453
+ config.set_current_banner_color(selected_color)
454
+
455
+ def get_preview_header():
456
+ return _get_single_banner_preview(config)
457
+
458
+ try:
459
+ await _split_panel_selector(
460
+ title,
461
+ choices,
462
+ update_preview,
463
+ get_preview=get_preview_header,
464
+ config=config,
465
+ )
466
+ except KeyboardInterrupt:
467
+ # Restore original color on cancel
468
+ config.set_current_banner_color(original_color)
469
+ except Exception:
470
+ pass # Silent error handling
471
+
472
+
473
+ def _get_single_banner_preview(config: ColorConfiguration) -> ANSI:
474
+ """Get preview for a single banner being edited."""
475
+ buffer = io.StringIO()
476
+ console = Console(
477
+ file=buffer,
478
+ force_terminal=True,
479
+ width=70,
480
+ legacy_windows=False,
481
+ color_system="truecolor",
482
+ no_color=False,
483
+ force_interactive=True,
484
+ )
485
+
486
+ banner_key = config.get_current_banner_key()
487
+ display_name, icon = BANNER_DISPLAY_INFO[banner_key]
488
+ color = config.get_current_banner_color()
489
+ sample = BANNER_SAMPLE_CONTENT[banner_key]
490
+
491
+ # Header
492
+ console.print("[bold]═" * 60 + "[/bold]")
493
+ console.print(f"[bold cyan] Editing: {display_name}[/bold cyan]")
494
+ console.print(f" Current Color: [bold]{color}[/bold]")
495
+ console.print("[bold]═" * 60 + "[/bold]")
496
+ console.print()
497
+
498
+ # Show the banner large
499
+ icon_str = f" {icon}" if icon else ""
500
+ banner_text = (
501
+ f"[bold white on {color}] {display_name} [/bold white on {color}]{icon_str}"
502
+ )
503
+ console.print(banner_text)
504
+ console.print()
505
+
506
+ # Show sample content
507
+ console.print("[dim]Sample output:[/dim]")
508
+ for line in sample.split("\n"):
509
+ console.print(f"[dim]{line}[/dim]")
510
+
511
+ console.print()
512
+ console.print("[bold]═" * 60 + "[/bold]")
513
+
514
+ ansi_output = buffer.getvalue()
515
+ return ANSI(ansi_output)
@@ -168,6 +168,8 @@ def handle_command(command: str):
168
168
  Returns:
169
169
  True if the command was handled, False if not, or a string to be processed as user input
170
170
  """
171
+ from rich.text import Text
172
+
171
173
  from code_puppy.command_line.command_registry import get_command
172
174
  from code_puppy.messaging import emit_info, emit_warning
173
175
 
@@ -261,7 +263,9 @@ def handle_command(command: str):
261
263
 
262
264
  if name:
263
265
  emit_warning(
264
- f"Unknown command: {command}\n[dim]Type /help for options.[/dim]"
266
+ Text.from_markup(
267
+ f"Unknown command: {command}\n[dim]Type /help for options.[/dim]"
268
+ )
265
269
  )
266
270
  else:
267
271
  # Show current model ONLY here
@@ -269,7 +273,9 @@ def handle_command(command: str):
269
273
 
270
274
  current_model = get_active_model()
271
275
  emit_info(
272
- f"[bold green]Current Model:[/bold green] [cyan]{current_model}[/cyan]"
276
+ Text.from_markup(
277
+ f"[bold green]Current Model:[/bold green] [cyan]{current_model}[/cyan]"
278
+ )
273
279
  )
274
280
  return True
275
281
 
@@ -27,6 +27,8 @@ def get_commands_help():
27
27
  )
28
28
  def handle_show_command(command: str) -> bool:
29
29
  """Show current puppy configuration."""
30
+ from rich.text import Text
31
+
30
32
  from code_puppy.agents import get_current_agent
31
33
  from code_puppy.command_line.model_picker_completion import get_active_model
32
34
  from code_puppy.config import (
@@ -79,14 +81,14 @@ def handle_show_command(command: str) -> bool:
79
81
  [bold]temperature:[/bold] [cyan]{effective_temperature if effective_temperature is not None else "(model default)"}[/cyan]{" (per-model)" if effective_temperature != global_temperature and effective_temperature is not None else ""}
80
82
 
81
83
  """
82
- emit_info(status_msg)
84
+ emit_info(Text.from_markup(status_msg))
83
85
  return True
84
86
 
85
87
 
86
88
  @register_command(
87
89
  name="reasoning",
88
90
  description="Set OpenAI reasoning effort for GPT-5 models (e.g., /reasoning high)",
89
- usage="/reasoning <low|medium|high>",
91
+ usage="/reasoning <minimal|low|medium|high|xhigh>",
90
92
  category="config",
91
93
  )
92
94
  def handle_reasoning_command(command: str) -> bool:
@@ -95,7 +97,7 @@ def handle_reasoning_command(command: str) -> bool:
95
97
 
96
98
  tokens = command.split()
97
99
  if len(tokens) != 2:
98
- emit_warning("Usage: /reasoning <low|medium|high>")
100
+ emit_warning("Usage: /reasoning <minimal|low|medium|high|xhigh>")
99
101
  return True
100
102
 
101
103
  effort = tokens[1]
@@ -171,6 +173,8 @@ def handle_verbosity_command(command: str) -> bool:
171
173
  )
172
174
  def handle_set_command(command: str) -> bool:
173
175
  """Set configuration values."""
176
+ from rich.text import Text
177
+
174
178
  from code_puppy.config import set_config_value
175
179
  from code_puppy.messaging import emit_error, emit_info, emit_success, emit_warning
176
180
 
@@ -197,14 +201,18 @@ def handle_set_command(command: str) -> bool:
197
201
  "\n [cyan]auto_save_session[/cyan] Auto-save chat after every response (true/false)"
198
202
  )
199
203
  emit_warning(
200
- f"Usage: /set KEY=VALUE or /set KEY VALUE\nConfig keys: {', '.join(config_keys)}\n[dim]Note: compaction_strategy can be 'summarization' or 'truncation'[/dim]{session_help}"
204
+ Text.from_markup(
205
+ f"Usage: /set KEY=VALUE or /set KEY VALUE\nConfig keys: {', '.join(config_keys)}\n[dim]Note: compaction_strategy can be 'summarization' or 'truncation'[/dim]{session_help}"
206
+ )
201
207
  )
202
208
  return True
203
209
  if key:
204
210
  # Check if we're toggling DBOS enablement
205
211
  if key == "enable_dbos":
206
212
  emit_info(
207
- "[yellow]⚠️ DBOS configuration changed. Please restart Code Puppy for this change to take effect.[/yellow]"
213
+ Text.from_markup(
214
+ "[yellow]⚠️ DBOS configuration changed. Please restart Code Puppy for this change to take effect.[/yellow]"
215
+ )
208
216
  )
209
217
 
210
218
  set_config_value(key, value)
@@ -539,6 +547,37 @@ def handle_diff_command(command: str) -> bool:
539
547
  return True
540
548
 
541
549
 
550
+ @register_command(
551
+ name="colors",
552
+ description="Configure banner colors for tool outputs (THINKING, SHELL COMMAND, etc.)",
553
+ usage="/colors",
554
+ category="config",
555
+ )
556
+ def handle_colors_command(command: str) -> bool:
557
+ """Configure banner colors via interactive TUI."""
558
+ import asyncio
559
+ import concurrent.futures
560
+
561
+ from code_puppy.command_line.colors_menu import interactive_colors_picker
562
+ from code_puppy.config import set_banner_color
563
+ from code_puppy.messaging import emit_error, emit_success
564
+
565
+ # Show interactive picker for banner color configuration
566
+ with concurrent.futures.ThreadPoolExecutor() as executor:
567
+ future = executor.submit(lambda: asyncio.run(interactive_colors_picker()))
568
+ result = future.result(timeout=300) # 5 min timeout
569
+
570
+ if result:
571
+ # Apply the changes
572
+ try:
573
+ for banner_name, color in result.items():
574
+ set_banner_color(banner_name, color)
575
+ emit_success("Banner colors saved! 🎨")
576
+ except Exception as e:
577
+ emit_error(f"Failed to apply banner color settings: {e}")
578
+ return True
579
+
580
+
542
581
  # ============================================================================
543
582
  # UTILITY FUNCTIONS
544
583
  # ============================================================================
@@ -550,6 +589,8 @@ def _show_color_options(color_type: str):
550
589
  # ============================================================================
551
590
 
552
591
  """Show available Rich color options organized by category."""
592
+ from rich.text import Text
593
+
553
594
  from code_puppy.messaging import emit_info
554
595
 
555
596
  # Standard Rich colors organized by category
@@ -607,11 +648,15 @@ def _show_color_options(color_type: str):
607
648
  ("sea_green1", "🟢"),
608
649
  ]
609
650
  emit_info(
610
- "[bold white on green]🎨 Recommended Colors for Additions:[/bold white on green]"
651
+ Text.from_markup(
652
+ "[bold white on green]🎨 Recommended Colors for Additions:[/bold white on green]"
653
+ )
611
654
  )
612
655
  for color, emoji in suggestions:
613
656
  emit_info(
614
- f" [cyan]{color:<16}[/cyan] [white on {color}]■■■■■■■■■■[/white on {color}] {emoji}"
657
+ Text.from_markup(
658
+ f" [cyan]{color:<16}[/cyan] [white on {color}]■■■■■■■■■■[/white on {color}] {emoji}"
659
+ )
615
660
  )
616
661
  elif color_type == "deletions":
617
662
  suggestions = [
@@ -622,11 +667,15 @@ def _show_color_options(color_type: str):
622
667
  ("dark_red", "🔴"),
623
668
  ]
624
669
  emit_info(
625
- "[bold white on orange1]🎨 Recommended Colors for Deletions:[/bold white on orange1]"
670
+ Text.from_markup(
671
+ "[bold white on orange1]🎨 Recommended Colors for Deletions:[/bold white on orange1]"
672
+ )
626
673
  )
627
674
  for color, emoji in suggestions:
628
675
  emit_info(
629
- f" [cyan]{color:<16}[/cyan] [white on {color}]■■■■■■■■■■[/white on {color}] {emoji}"
676
+ Text.from_markup(
677
+ f" [cyan]{color:<16}[/cyan] [white on {color}]■■■■■■■■■■[/white on {color}] {emoji}"
678
+ )
630
679
  )
631
680
 
632
681
  emit_info("\n🎨 All Available Rich Colors:")
@@ -636,7 +685,7 @@ def _show_color_options(color_type: str):
636
685
  for i in range(0, len(colors), 4):
637
686
  row = colors[i : i + 4]
638
687
  row_text = " ".join([f"[{color}]■[/{color}] {color}" for color, _ in row])
639
- emit_info(f" {row_text}")
688
+ emit_info(Text.from_markup(f" {row_text}"))
640
689
 
641
690
  emit_info("\nUsage: /diff {color_type} <color_name>")
642
691
  emit_info("All diffs use white text on your chosen background colors")