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.
- code_puppy/agents/base_agent.py +373 -46
- code_puppy/chatgpt_codex_client.py +283 -0
- code_puppy/cli_runner.py +795 -0
- code_puppy/command_line/add_model_menu.py +8 -1
- code_puppy/command_line/autosave_menu.py +266 -35
- code_puppy/command_line/colors_menu.py +515 -0
- code_puppy/command_line/command_handler.py +8 -2
- code_puppy/command_line/config_commands.py +59 -10
- code_puppy/command_line/core_commands.py +19 -7
- code_puppy/command_line/mcp/edit_command.py +3 -1
- code_puppy/command_line/mcp/handler.py +7 -2
- code_puppy/command_line/mcp/install_command.py +8 -3
- code_puppy/command_line/mcp/logs_command.py +173 -64
- code_puppy/command_line/mcp/restart_command.py +7 -2
- code_puppy/command_line/mcp/search_command.py +10 -4
- code_puppy/command_line/mcp/start_all_command.py +16 -6
- code_puppy/command_line/mcp/start_command.py +3 -1
- code_puppy/command_line/mcp/status_command.py +2 -1
- code_puppy/command_line/mcp/stop_all_command.py +5 -1
- code_puppy/command_line/mcp/stop_command.py +3 -1
- code_puppy/command_line/mcp/wizard_utils.py +10 -4
- code_puppy/command_line/model_settings_menu.py +53 -7
- code_puppy/command_line/prompt_toolkit_completion.py +16 -2
- code_puppy/command_line/session_commands.py +11 -4
- code_puppy/config.py +103 -15
- code_puppy/keymap.py +8 -2
- code_puppy/main.py +5 -828
- code_puppy/mcp_/__init__.py +17 -0
- code_puppy/mcp_/blocking_startup.py +61 -32
- code_puppy/mcp_/config_wizard.py +5 -1
- code_puppy/mcp_/managed_server.py +23 -3
- code_puppy/mcp_/manager.py +65 -0
- code_puppy/mcp_/mcp_logs.py +224 -0
- code_puppy/messaging/__init__.py +20 -4
- code_puppy/messaging/bus.py +64 -0
- code_puppy/messaging/markdown_patches.py +57 -0
- code_puppy/messaging/messages.py +16 -0
- code_puppy/messaging/renderers.py +21 -9
- code_puppy/messaging/rich_renderer.py +113 -67
- code_puppy/messaging/spinner/console_spinner.py +34 -0
- code_puppy/model_factory.py +185 -30
- code_puppy/model_utils.py +57 -48
- code_puppy/models.json +19 -5
- code_puppy/plugins/chatgpt_oauth/config.py +5 -1
- code_puppy/plugins/chatgpt_oauth/oauth_flow.py +5 -6
- code_puppy/plugins/chatgpt_oauth/register_callbacks.py +3 -3
- code_puppy/plugins/chatgpt_oauth/test_plugin.py +26 -11
- code_puppy/plugins/chatgpt_oauth/utils.py +180 -65
- code_puppy/plugins/claude_code_oauth/register_callbacks.py +28 -0
- code_puppy/plugins/claude_code_oauth/utils.py +1 -0
- code_puppy/plugins/shell_safety/agent_shell_safety.py +1 -118
- code_puppy/plugins/shell_safety/register_callbacks.py +44 -3
- code_puppy/prompts/codex_system_prompt.md +310 -0
- code_puppy/pydantic_patches.py +131 -0
- code_puppy/terminal_utils.py +126 -0
- code_puppy/tools/agent_tools.py +34 -9
- code_puppy/tools/command_runner.py +361 -32
- code_puppy/tools/file_operations.py +33 -45
- {code_puppy-0.0.302.data → code_puppy-0.0.323.data}/data/code_puppy/models.json +19 -5
- {code_puppy-0.0.302.dist-info → code_puppy-0.0.323.dist-info}/METADATA +1 -1
- {code_puppy-0.0.302.dist-info → code_puppy-0.0.323.dist-info}/RECORD +65 -57
- {code_puppy-0.0.302.data → code_puppy-0.0.323.data}/data/code_puppy/models_dev_api.json +0 -0
- {code_puppy-0.0.302.dist-info → code_puppy-0.0.323.dist-info}/WHEEL +0 -0
- {code_puppy-0.0.302.dist-info → code_puppy-0.0.323.dist-info}/entry_points.txt +0 -0
- {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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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")
|