code-puppy 0.0.332__py3-none-any.whl → 0.0.334__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 +30 -72
- code_puppy/cli_runner.py +66 -6
- code_puppy/command_line/config_commands.py +1 -1
- code_puppy/command_line/core_commands.py +51 -0
- code_puppy/command_line/onboarding_slides.py +416 -0
- code_puppy/command_line/onboarding_wizard.py +426 -0
- code_puppy/config.py +2 -2
- code_puppy/keymap.py +8 -0
- code_puppy/plugins/chatgpt_oauth/register_callbacks.py +2 -0
- code_puppy/plugins/claude_code_oauth/register_callbacks.py +2 -0
- code_puppy/terminal_utils.py +170 -9
- code_puppy/tools/command_runner.py +4 -89
- code_puppy/uvx_detection.py +242 -0
- {code_puppy-0.0.332.dist-info → code_puppy-0.0.334.dist-info}/METADATA +1 -1
- {code_puppy-0.0.332.dist-info → code_puppy-0.0.334.dist-info}/RECORD +20 -17
- {code_puppy-0.0.332.data → code_puppy-0.0.334.data}/data/code_puppy/models.json +0 -0
- {code_puppy-0.0.332.data → code_puppy-0.0.334.data}/data/code_puppy/models_dev_api.json +0 -0
- {code_puppy-0.0.332.dist-info → code_puppy-0.0.334.dist-info}/WHEEL +0 -0
- {code_puppy-0.0.332.dist-info → code_puppy-0.0.334.dist-info}/entry_points.txt +0 -0
- {code_puppy-0.0.332.dist-info → code_puppy-0.0.334.dist-info}/licenses/LICENSE +0 -0
|
@@ -0,0 +1,426 @@
|
|
|
1
|
+
"""Interactive TUI onboarding wizard for first-time Code Puppy users.
|
|
2
|
+
|
|
3
|
+
🐶 Welcome to Code Puppy! This wizard guides new users through initial setup,
|
|
4
|
+
model configuration, and feature discovery. Uses the same TUI patterns as
|
|
5
|
+
colors_menu.py and diff_menu.py for a consistent experience.
|
|
6
|
+
|
|
7
|
+
Usage:
|
|
8
|
+
from code_puppy.command_line.onboarding_wizard import (
|
|
9
|
+
should_show_onboarding,
|
|
10
|
+
run_onboarding_wizard,
|
|
11
|
+
)
|
|
12
|
+
|
|
13
|
+
if should_show_onboarding():
|
|
14
|
+
result = await run_onboarding_wizard()
|
|
15
|
+
# result: "chatgpt", "claude", "completed", "skipped", or None
|
|
16
|
+
"""
|
|
17
|
+
|
|
18
|
+
import io
|
|
19
|
+
import os
|
|
20
|
+
import sys
|
|
21
|
+
import time
|
|
22
|
+
from typing import List, Optional, Tuple
|
|
23
|
+
|
|
24
|
+
from prompt_toolkit import Application
|
|
25
|
+
from prompt_toolkit.formatted_text import ANSI, FormattedText
|
|
26
|
+
from prompt_toolkit.key_binding import KeyBindings
|
|
27
|
+
from prompt_toolkit.layout import Layout, Window
|
|
28
|
+
from prompt_toolkit.layout.controls import FormattedTextControl
|
|
29
|
+
from prompt_toolkit.widgets import Frame
|
|
30
|
+
from rich.console import Console
|
|
31
|
+
|
|
32
|
+
from code_puppy.config import CONFIG_DIR
|
|
33
|
+
|
|
34
|
+
from .onboarding_slides import (
|
|
35
|
+
MODEL_OPTIONS,
|
|
36
|
+
slide_agent_system,
|
|
37
|
+
slide_api_keys,
|
|
38
|
+
slide_appearance,
|
|
39
|
+
slide_complete,
|
|
40
|
+
slide_mcp_servers,
|
|
41
|
+
slide_model_pinning,
|
|
42
|
+
slide_model_settings,
|
|
43
|
+
slide_model_subscription,
|
|
44
|
+
slide_planning_agent,
|
|
45
|
+
slide_welcome,
|
|
46
|
+
)
|
|
47
|
+
|
|
48
|
+
# ============================================================================
|
|
49
|
+
# State Tracking (like motd.py pattern)
|
|
50
|
+
# ============================================================================
|
|
51
|
+
|
|
52
|
+
ONBOARDING_COMPLETE_FILE = os.path.join(CONFIG_DIR, "onboarding_complete")
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
def has_completed_onboarding() -> bool:
|
|
56
|
+
"""Check if the user has already completed onboarding.
|
|
57
|
+
|
|
58
|
+
Returns:
|
|
59
|
+
True if onboarding has been completed, False otherwise.
|
|
60
|
+
"""
|
|
61
|
+
return os.path.exists(ONBOARDING_COMPLETE_FILE)
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
def mark_onboarding_complete() -> None:
|
|
65
|
+
"""Mark onboarding as complete by creating the tracking file."""
|
|
66
|
+
os.makedirs(os.path.dirname(ONBOARDING_COMPLETE_FILE), exist_ok=True)
|
|
67
|
+
with open(ONBOARDING_COMPLETE_FILE, "w") as f:
|
|
68
|
+
f.write("completed\n")
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
def should_show_onboarding() -> bool:
|
|
72
|
+
"""Determine if the onboarding wizard should be shown.
|
|
73
|
+
|
|
74
|
+
Returns:
|
|
75
|
+
True if onboarding should be shown, False otherwise.
|
|
76
|
+
"""
|
|
77
|
+
return not has_completed_onboarding()
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
def reset_onboarding() -> None:
|
|
81
|
+
"""Reset onboarding state (useful for testing or re-running wizard)."""
|
|
82
|
+
if os.path.exists(ONBOARDING_COMPLETE_FILE):
|
|
83
|
+
os.remove(ONBOARDING_COMPLETE_FILE)
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
# ============================================================================
|
|
87
|
+
# Onboarding Wizard Class
|
|
88
|
+
# ============================================================================
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
class OnboardingWizard:
|
|
92
|
+
"""Interactive onboarding wizard with slide-based navigation.
|
|
93
|
+
|
|
94
|
+
Attributes:
|
|
95
|
+
current_slide: Index of the currently displayed slide (0-9)
|
|
96
|
+
selected_option: Index of selected option within current slide
|
|
97
|
+
trigger_oauth: OAuth provider to trigger after wizard ("chatgpt"/"claude")
|
|
98
|
+
model_choice: User's model subscription selection
|
|
99
|
+
"""
|
|
100
|
+
|
|
101
|
+
TOTAL_SLIDES = 10
|
|
102
|
+
|
|
103
|
+
def __init__(self):
|
|
104
|
+
"""Initialize the onboarding wizard state."""
|
|
105
|
+
self.current_slide = 0
|
|
106
|
+
self.selected_option = 0
|
|
107
|
+
self.trigger_oauth: Optional[str] = None
|
|
108
|
+
self.model_choice: Optional[str] = None
|
|
109
|
+
self.result: Optional[str] = None
|
|
110
|
+
self._should_exit = False
|
|
111
|
+
|
|
112
|
+
def get_progress_indicator(self) -> str:
|
|
113
|
+
"""Generate progress dots showing current slide position.
|
|
114
|
+
|
|
115
|
+
Returns:
|
|
116
|
+
String like "● ○ ○ ○ ○ ○ ○ ○ ○ ○" for slide 0.
|
|
117
|
+
"""
|
|
118
|
+
dots = []
|
|
119
|
+
for i in range(self.TOTAL_SLIDES):
|
|
120
|
+
if i == self.current_slide:
|
|
121
|
+
dots.append("●")
|
|
122
|
+
else:
|
|
123
|
+
dots.append("○")
|
|
124
|
+
return " ".join(dots)
|
|
125
|
+
|
|
126
|
+
def get_slide_content(self) -> str:
|
|
127
|
+
"""Get combined content for the current slide.
|
|
128
|
+
|
|
129
|
+
Returns:
|
|
130
|
+
Rich markup string for the slide content.
|
|
131
|
+
"""
|
|
132
|
+
if self.current_slide == 0:
|
|
133
|
+
return slide_welcome()
|
|
134
|
+
elif self.current_slide == 1:
|
|
135
|
+
options = self.get_options_for_slide()
|
|
136
|
+
return slide_model_subscription(self.selected_option, options)
|
|
137
|
+
elif self.current_slide == 2:
|
|
138
|
+
return slide_api_keys(self.model_choice)
|
|
139
|
+
elif self.current_slide == 3:
|
|
140
|
+
return slide_mcp_servers()
|
|
141
|
+
elif self.current_slide == 4:
|
|
142
|
+
return slide_appearance()
|
|
143
|
+
elif self.current_slide == 5:
|
|
144
|
+
return slide_agent_system()
|
|
145
|
+
elif self.current_slide == 6:
|
|
146
|
+
return slide_model_pinning()
|
|
147
|
+
elif self.current_slide == 7:
|
|
148
|
+
return slide_planning_agent()
|
|
149
|
+
elif self.current_slide == 8:
|
|
150
|
+
return slide_model_settings()
|
|
151
|
+
else: # slide 9
|
|
152
|
+
return slide_complete(self.trigger_oauth)
|
|
153
|
+
|
|
154
|
+
def get_options_for_slide(self) -> List[Tuple[str, str]]:
|
|
155
|
+
"""Get selectable options for the current slide.
|
|
156
|
+
|
|
157
|
+
Returns:
|
|
158
|
+
List of (id, label) tuples for options, or empty list if no options.
|
|
159
|
+
"""
|
|
160
|
+
if self.current_slide == 1: # Model subscription slide
|
|
161
|
+
return [(opt[0], opt[1]) for opt in MODEL_OPTIONS]
|
|
162
|
+
return []
|
|
163
|
+
|
|
164
|
+
def handle_option_select(self) -> None:
|
|
165
|
+
"""Handle selection of the current option."""
|
|
166
|
+
if self.current_slide == 1: # Model subscription
|
|
167
|
+
options = self.get_options_for_slide()
|
|
168
|
+
if 0 <= self.selected_option < len(options):
|
|
169
|
+
choice_id = options[self.selected_option][0]
|
|
170
|
+
self.model_choice = choice_id
|
|
171
|
+
|
|
172
|
+
# Set OAuth trigger for ChatGPT/Claude
|
|
173
|
+
if choice_id == "chatgpt":
|
|
174
|
+
self.trigger_oauth = "chatgpt"
|
|
175
|
+
elif choice_id == "claude":
|
|
176
|
+
self.trigger_oauth = "claude"
|
|
177
|
+
|
|
178
|
+
def next_slide(self) -> bool:
|
|
179
|
+
"""Move to the next slide.
|
|
180
|
+
|
|
181
|
+
Returns:
|
|
182
|
+
True if moved to next slide, False if at last slide.
|
|
183
|
+
"""
|
|
184
|
+
if self.current_slide < self.TOTAL_SLIDES - 1:
|
|
185
|
+
self.current_slide += 1
|
|
186
|
+
self.selected_option = 0
|
|
187
|
+
return True
|
|
188
|
+
return False
|
|
189
|
+
|
|
190
|
+
def prev_slide(self) -> bool:
|
|
191
|
+
"""Move to the previous slide.
|
|
192
|
+
|
|
193
|
+
Returns:
|
|
194
|
+
True if moved to previous slide, False if at first slide.
|
|
195
|
+
"""
|
|
196
|
+
if self.current_slide > 0:
|
|
197
|
+
self.current_slide -= 1
|
|
198
|
+
self.selected_option = 0
|
|
199
|
+
return True
|
|
200
|
+
return False
|
|
201
|
+
|
|
202
|
+
def next_option(self) -> None:
|
|
203
|
+
"""Move to the next option within the current slide."""
|
|
204
|
+
options = self.get_options_for_slide()
|
|
205
|
+
if options:
|
|
206
|
+
self.selected_option = (self.selected_option + 1) % len(options)
|
|
207
|
+
|
|
208
|
+
def prev_option(self) -> None:
|
|
209
|
+
"""Move to the previous option within the current slide."""
|
|
210
|
+
options = self.get_options_for_slide()
|
|
211
|
+
if options:
|
|
212
|
+
self.selected_option = (self.selected_option - 1) % len(options)
|
|
213
|
+
|
|
214
|
+
|
|
215
|
+
# ============================================================================
|
|
216
|
+
# TUI Rendering Functions
|
|
217
|
+
# ============================================================================
|
|
218
|
+
|
|
219
|
+
|
|
220
|
+
def _get_slide_panel_content(wizard: OnboardingWizard) -> ANSI:
|
|
221
|
+
"""Generate the centered slide content.
|
|
222
|
+
|
|
223
|
+
Args:
|
|
224
|
+
wizard: The OnboardingWizard instance.
|
|
225
|
+
|
|
226
|
+
Returns:
|
|
227
|
+
ANSI object with formatted slide content.
|
|
228
|
+
"""
|
|
229
|
+
buffer = io.StringIO()
|
|
230
|
+
console = Console(
|
|
231
|
+
file=buffer,
|
|
232
|
+
force_terminal=True,
|
|
233
|
+
width=80,
|
|
234
|
+
legacy_windows=False,
|
|
235
|
+
color_system="truecolor",
|
|
236
|
+
no_color=False,
|
|
237
|
+
force_interactive=True,
|
|
238
|
+
)
|
|
239
|
+
|
|
240
|
+
# Progress indicator
|
|
241
|
+
progress = wizard.get_progress_indicator()
|
|
242
|
+
console.print(f"[dim]{progress}[/dim]\n")
|
|
243
|
+
|
|
244
|
+
# Slide number
|
|
245
|
+
console.print(
|
|
246
|
+
f"[dim]Slide {wizard.current_slide + 1} of {wizard.TOTAL_SLIDES}[/dim]\n\n"
|
|
247
|
+
)
|
|
248
|
+
|
|
249
|
+
# Combined slide content
|
|
250
|
+
slide_content = wizard.get_slide_content()
|
|
251
|
+
console.print(slide_content)
|
|
252
|
+
|
|
253
|
+
return ANSI(buffer.getvalue())
|
|
254
|
+
|
|
255
|
+
|
|
256
|
+
def _get_navigation_hints(wizard: OnboardingWizard) -> FormattedText:
|
|
257
|
+
"""Generate navigation hints for the bottom of the screen.
|
|
258
|
+
|
|
259
|
+
Args:
|
|
260
|
+
wizard: The OnboardingWizard instance.
|
|
261
|
+
|
|
262
|
+
Returns:
|
|
263
|
+
FormattedText with navigation hints.
|
|
264
|
+
"""
|
|
265
|
+
hints = []
|
|
266
|
+
|
|
267
|
+
if wizard.current_slide > 0:
|
|
268
|
+
hints.append(("fg:ansicyan", "← Back "))
|
|
269
|
+
|
|
270
|
+
if wizard.current_slide < wizard.TOTAL_SLIDES - 1:
|
|
271
|
+
hints.append(("fg:ansicyan", "→ Next "))
|
|
272
|
+
else:
|
|
273
|
+
hints.append(("fg:ansigreen bold", "Enter: Finish "))
|
|
274
|
+
|
|
275
|
+
options = wizard.get_options_for_slide()
|
|
276
|
+
if options:
|
|
277
|
+
hints.append(("fg:ansicyan", "↑↓ Options "))
|
|
278
|
+
|
|
279
|
+
hints.append(("fg:ansiyellow", "ESC: Skip"))
|
|
280
|
+
|
|
281
|
+
return FormattedText(hints)
|
|
282
|
+
|
|
283
|
+
|
|
284
|
+
# ============================================================================
|
|
285
|
+
# Main Entry Point
|
|
286
|
+
# ============================================================================
|
|
287
|
+
|
|
288
|
+
|
|
289
|
+
async def run_onboarding_wizard() -> Optional[str]:
|
|
290
|
+
"""Run the interactive onboarding wizard.
|
|
291
|
+
|
|
292
|
+
Returns:
|
|
293
|
+
- "chatgpt" if user wants ChatGPT OAuth
|
|
294
|
+
- "claude" if user wants Claude OAuth
|
|
295
|
+
- "completed" if finished normally
|
|
296
|
+
- "skipped" if user pressed ESC
|
|
297
|
+
- None on error
|
|
298
|
+
"""
|
|
299
|
+
from code_puppy.tools.command_runner import set_awaiting_user_input
|
|
300
|
+
|
|
301
|
+
wizard = OnboardingWizard()
|
|
302
|
+
|
|
303
|
+
set_awaiting_user_input(True)
|
|
304
|
+
|
|
305
|
+
# Enter alternate screen buffer
|
|
306
|
+
sys.stdout.write("\033[?1049h") # Enter alternate buffer
|
|
307
|
+
sys.stdout.write("\033[2J\033[H") # Clear and home
|
|
308
|
+
sys.stdout.flush()
|
|
309
|
+
time.sleep(0.1) # Minimal delay for state sync
|
|
310
|
+
|
|
311
|
+
try:
|
|
312
|
+
# Set up key bindings
|
|
313
|
+
kb = KeyBindings()
|
|
314
|
+
|
|
315
|
+
@kb.add("right")
|
|
316
|
+
@kb.add("l")
|
|
317
|
+
def next_slide(event):
|
|
318
|
+
if wizard.current_slide == wizard.TOTAL_SLIDES - 1:
|
|
319
|
+
# On last slide, right arrow finishes
|
|
320
|
+
wizard.result = "completed"
|
|
321
|
+
wizard._should_exit = True
|
|
322
|
+
event.app.exit()
|
|
323
|
+
else:
|
|
324
|
+
wizard.next_slide()
|
|
325
|
+
event.app.invalidate()
|
|
326
|
+
|
|
327
|
+
@kb.add("left")
|
|
328
|
+
@kb.add("h")
|
|
329
|
+
def prev_slide(event):
|
|
330
|
+
wizard.prev_slide()
|
|
331
|
+
event.app.invalidate()
|
|
332
|
+
|
|
333
|
+
@kb.add("down")
|
|
334
|
+
@kb.add("j")
|
|
335
|
+
def next_option(event):
|
|
336
|
+
wizard.next_option()
|
|
337
|
+
event.app.invalidate()
|
|
338
|
+
|
|
339
|
+
@kb.add("up")
|
|
340
|
+
@kb.add("k")
|
|
341
|
+
def prev_option(event):
|
|
342
|
+
wizard.prev_option()
|
|
343
|
+
event.app.invalidate()
|
|
344
|
+
|
|
345
|
+
@kb.add("enter")
|
|
346
|
+
def select_or_next(event):
|
|
347
|
+
# Handle option selection on slides with options
|
|
348
|
+
options = wizard.get_options_for_slide()
|
|
349
|
+
if options:
|
|
350
|
+
wizard.handle_option_select()
|
|
351
|
+
|
|
352
|
+
# Move to next slide or finish
|
|
353
|
+
if wizard.current_slide == wizard.TOTAL_SLIDES - 1:
|
|
354
|
+
wizard.result = "completed"
|
|
355
|
+
wizard._should_exit = True
|
|
356
|
+
event.app.exit()
|
|
357
|
+
else:
|
|
358
|
+
wizard.next_slide()
|
|
359
|
+
event.app.invalidate()
|
|
360
|
+
|
|
361
|
+
@kb.add("escape")
|
|
362
|
+
def skip_wizard(event):
|
|
363
|
+
wizard.result = "skipped"
|
|
364
|
+
wizard._should_exit = True
|
|
365
|
+
event.app.exit()
|
|
366
|
+
|
|
367
|
+
@kb.add("c-c")
|
|
368
|
+
def cancel_wizard(event):
|
|
369
|
+
wizard.result = "skipped"
|
|
370
|
+
wizard._should_exit = True
|
|
371
|
+
event.app.exit()
|
|
372
|
+
|
|
373
|
+
# Create layout with single centered panel
|
|
374
|
+
slide_panel = Window(
|
|
375
|
+
content=FormattedTextControl(lambda: _get_slide_panel_content(wizard))
|
|
376
|
+
)
|
|
377
|
+
|
|
378
|
+
root_container = Frame(slide_panel, title="🐶 Welcome to Code Puppy!")
|
|
379
|
+
|
|
380
|
+
layout = Layout(root_container)
|
|
381
|
+
|
|
382
|
+
app = Application(
|
|
383
|
+
layout=layout,
|
|
384
|
+
key_bindings=kb,
|
|
385
|
+
full_screen=False,
|
|
386
|
+
mouse_support=False,
|
|
387
|
+
color_depth="DEPTH_24_BIT",
|
|
388
|
+
)
|
|
389
|
+
|
|
390
|
+
# Clear screen before running
|
|
391
|
+
sys.stdout.write("\033[2J\033[H")
|
|
392
|
+
sys.stdout.flush()
|
|
393
|
+
|
|
394
|
+
# Run the application
|
|
395
|
+
await app.run_async()
|
|
396
|
+
|
|
397
|
+
except KeyboardInterrupt:
|
|
398
|
+
wizard.result = "skipped"
|
|
399
|
+
except Exception:
|
|
400
|
+
wizard.result = None
|
|
401
|
+
finally:
|
|
402
|
+
set_awaiting_user_input(False)
|
|
403
|
+
# Exit alternate screen buffer
|
|
404
|
+
sys.stdout.write("\033[?1049l")
|
|
405
|
+
sys.stdout.flush()
|
|
406
|
+
|
|
407
|
+
# Mark onboarding as complete (even if skipped - they saw it)
|
|
408
|
+
if wizard.result in ("completed", "skipped"):
|
|
409
|
+
mark_onboarding_complete()
|
|
410
|
+
|
|
411
|
+
# Return OAuth trigger if selected, otherwise the result
|
|
412
|
+
if wizard.trigger_oauth:
|
|
413
|
+
return wizard.trigger_oauth
|
|
414
|
+
|
|
415
|
+
return wizard.result
|
|
416
|
+
|
|
417
|
+
|
|
418
|
+
async def run_onboarding_if_needed() -> Optional[str]:
|
|
419
|
+
"""Run onboarding wizard if user hasn't completed it yet.
|
|
420
|
+
|
|
421
|
+
Returns:
|
|
422
|
+
Result from run_onboarding_wizard() or None if not needed.
|
|
423
|
+
"""
|
|
424
|
+
if should_show_onboarding():
|
|
425
|
+
return await run_onboarding_wizard()
|
|
426
|
+
return None
|
code_puppy/config.py
CHANGED
|
@@ -1051,11 +1051,11 @@ def set_enable_dbos(enabled: bool) -> None:
|
|
|
1051
1051
|
set_config_value("enable_dbos", "true" if enabled else "false")
|
|
1052
1052
|
|
|
1053
1053
|
|
|
1054
|
-
def get_message_limit(default: int =
|
|
1054
|
+
def get_message_limit(default: int = 1000) -> int:
|
|
1055
1055
|
"""
|
|
1056
1056
|
Returns the user-configured message/request limit for the agent.
|
|
1057
1057
|
This controls how many steps/requests the agent can take.
|
|
1058
|
-
Defaults to
|
|
1058
|
+
Defaults to 1000 if unset or misconfigured.
|
|
1059
1059
|
Configurable by 'message_limit' key.
|
|
1060
1060
|
"""
|
|
1061
1061
|
val = get_value("message_limit")
|
code_puppy/keymap.py
CHANGED
|
@@ -55,11 +55,19 @@ class KeymapError(Exception):
|
|
|
55
55
|
def get_cancel_agent_key() -> str:
|
|
56
56
|
"""Get the configured cancel agent key from config.
|
|
57
57
|
|
|
58
|
+
On Windows when launched via uvx, this automatically returns "ctrl+k"
|
|
59
|
+
to work around uvx capturing Ctrl+C before it reaches Python.
|
|
60
|
+
|
|
58
61
|
Returns:
|
|
59
62
|
The key name (e.g., "ctrl+c", "ctrl+k") from config,
|
|
60
63
|
or the default if not configured.
|
|
61
64
|
"""
|
|
62
65
|
from code_puppy.config import get_value
|
|
66
|
+
from code_puppy.uvx_detection import should_use_alternate_cancel_key
|
|
67
|
+
|
|
68
|
+
# On Windows + uvx, force ctrl+k to bypass uvx's SIGINT capture
|
|
69
|
+
if should_use_alternate_cancel_key():
|
|
70
|
+
return "ctrl+k"
|
|
63
71
|
|
|
64
72
|
key = get_value("cancel_agent_key")
|
|
65
73
|
if key is None or key.strip() == "":
|
|
@@ -6,6 +6,7 @@ import os
|
|
|
6
6
|
from typing import List, Optional, Tuple
|
|
7
7
|
|
|
8
8
|
from code_puppy.callbacks import register_callback
|
|
9
|
+
from code_puppy.config import set_model_name
|
|
9
10
|
from code_puppy.messaging import emit_info, emit_success, emit_warning
|
|
10
11
|
|
|
11
12
|
from .config import CHATGPT_OAUTH_CONFIG, get_token_storage_path
|
|
@@ -75,6 +76,7 @@ def _handle_custom_command(command: str, name: str) -> Optional[bool]:
|
|
|
75
76
|
|
|
76
77
|
if name == "chatgpt-auth":
|
|
77
78
|
run_oauth_flow()
|
|
79
|
+
set_model_name("chatgpt-gpt-5.2-codex")
|
|
78
80
|
return True
|
|
79
81
|
|
|
80
82
|
if name == "chatgpt-status":
|
|
@@ -12,6 +12,7 @@ from typing import Any, Dict, List, Optional, Tuple
|
|
|
12
12
|
from urllib.parse import parse_qs, urlparse
|
|
13
13
|
|
|
14
14
|
from code_puppy.callbacks import register_callback
|
|
15
|
+
from code_puppy.config import set_model_name
|
|
15
16
|
from code_puppy.messaging import emit_error, emit_info, emit_success, emit_warning
|
|
16
17
|
|
|
17
18
|
from ..oauth_puppy_html import oauth_failure_html, oauth_success_html
|
|
@@ -260,6 +261,7 @@ def _handle_custom_command(command: str, name: str) -> Optional[bool]:
|
|
|
260
261
|
"Existing Claude Code tokens found. Continuing will overwrite them."
|
|
261
262
|
)
|
|
262
263
|
_perform_authentication()
|
|
264
|
+
set_model_name("claude-code-claude-opus-4-5-20251101")
|
|
263
265
|
return True
|
|
264
266
|
|
|
265
267
|
if name == "claude-code-status":
|
code_puppy/terminal_utils.py
CHANGED
|
@@ -6,6 +6,10 @@ Handles Windows console mode resets and Unix terminal sanity restoration.
|
|
|
6
6
|
import platform
|
|
7
7
|
import subprocess
|
|
8
8
|
import sys
|
|
9
|
+
from typing import Callable, Optional
|
|
10
|
+
|
|
11
|
+
# Store the original console ctrl handler so we can restore it if needed
|
|
12
|
+
_original_ctrl_handler: Optional[Callable] = None
|
|
9
13
|
|
|
10
14
|
|
|
11
15
|
def reset_windows_terminal_ansi() -> None:
|
|
@@ -86,21 +90,36 @@ def reset_windows_console_mode() -> None:
|
|
|
86
90
|
pass # Silently ignore errors - best effort reset
|
|
87
91
|
|
|
88
92
|
|
|
89
|
-
def
|
|
90
|
-
"""
|
|
93
|
+
def flush_windows_keyboard_buffer() -> None:
|
|
94
|
+
"""Flush the Windows keyboard buffer.
|
|
91
95
|
|
|
92
|
-
|
|
93
|
-
|
|
96
|
+
Clears any pending keyboard input that could interfere with
|
|
97
|
+
subsequent input operations after an interrupt.
|
|
98
|
+
"""
|
|
99
|
+
if platform.system() != "Windows":
|
|
100
|
+
return
|
|
94
101
|
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
102
|
+
try:
|
|
103
|
+
import msvcrt
|
|
104
|
+
|
|
105
|
+
while msvcrt.kbhit():
|
|
106
|
+
msvcrt.getch()
|
|
107
|
+
except Exception:
|
|
108
|
+
pass # Silently ignore errors - best effort flush
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
def reset_windows_terminal_full() -> None:
|
|
112
|
+
"""Perform a full Windows terminal reset (ANSI + console mode + keyboard buffer).
|
|
113
|
+
|
|
114
|
+
Combines ANSI reset, console mode reset, and keyboard buffer flush
|
|
115
|
+
for complete terminal state restoration after interrupts.
|
|
98
116
|
"""
|
|
99
117
|
if platform.system() != "Windows":
|
|
100
118
|
return
|
|
101
119
|
|
|
102
|
-
|
|
103
|
-
|
|
120
|
+
reset_windows_terminal_ansi()
|
|
121
|
+
reset_windows_console_mode()
|
|
122
|
+
flush_windows_keyboard_buffer()
|
|
104
123
|
|
|
105
124
|
|
|
106
125
|
def reset_unix_terminal() -> None:
|
|
@@ -128,3 +147,145 @@ def reset_terminal() -> None:
|
|
|
128
147
|
reset_windows_terminal_full()
|
|
129
148
|
else:
|
|
130
149
|
reset_unix_terminal()
|
|
150
|
+
|
|
151
|
+
|
|
152
|
+
def disable_windows_ctrl_c() -> bool:
|
|
153
|
+
"""Disable Ctrl+C processing at the Windows console input level.
|
|
154
|
+
|
|
155
|
+
This removes ENABLE_PROCESSED_INPUT from stdin, which prevents
|
|
156
|
+
Ctrl+C from being interpreted as a signal at all. Instead, it
|
|
157
|
+
becomes just a regular character (^C) that gets ignored.
|
|
158
|
+
|
|
159
|
+
This is more reliable than SetConsoleCtrlHandler because it
|
|
160
|
+
prevents Ctrl+C from being processed before it reaches any handler.
|
|
161
|
+
|
|
162
|
+
Returns:
|
|
163
|
+
True if successfully disabled, False otherwise.
|
|
164
|
+
"""
|
|
165
|
+
global _original_ctrl_handler
|
|
166
|
+
|
|
167
|
+
if platform.system() != "Windows":
|
|
168
|
+
return False
|
|
169
|
+
|
|
170
|
+
try:
|
|
171
|
+
import ctypes
|
|
172
|
+
|
|
173
|
+
kernel32 = ctypes.windll.kernel32
|
|
174
|
+
|
|
175
|
+
# Get stdin handle
|
|
176
|
+
STD_INPUT_HANDLE = -10
|
|
177
|
+
stdin_handle = kernel32.GetStdHandle(STD_INPUT_HANDLE)
|
|
178
|
+
|
|
179
|
+
# Get current console mode
|
|
180
|
+
mode = ctypes.c_ulong()
|
|
181
|
+
if not kernel32.GetConsoleMode(stdin_handle, ctypes.byref(mode)):
|
|
182
|
+
return False
|
|
183
|
+
|
|
184
|
+
# Save original mode for potential restoration
|
|
185
|
+
_original_ctrl_handler = mode.value
|
|
186
|
+
|
|
187
|
+
# Console mode flags
|
|
188
|
+
ENABLE_PROCESSED_INPUT = 0x0001 # This makes Ctrl+C generate signals
|
|
189
|
+
|
|
190
|
+
# Remove ENABLE_PROCESSED_INPUT to disable Ctrl+C signal generation
|
|
191
|
+
new_mode = mode.value & ~ENABLE_PROCESSED_INPUT
|
|
192
|
+
|
|
193
|
+
if kernel32.SetConsoleMode(stdin_handle, new_mode):
|
|
194
|
+
return True
|
|
195
|
+
return False
|
|
196
|
+
|
|
197
|
+
except Exception:
|
|
198
|
+
return False
|
|
199
|
+
|
|
200
|
+
|
|
201
|
+
def enable_windows_ctrl_c() -> bool:
|
|
202
|
+
"""Re-enable Ctrl+C at the Windows console level.
|
|
203
|
+
|
|
204
|
+
Restores the original console mode saved by disable_windows_ctrl_c().
|
|
205
|
+
|
|
206
|
+
Returns:
|
|
207
|
+
True if successfully re-enabled, False otherwise.
|
|
208
|
+
"""
|
|
209
|
+
global _original_ctrl_handler
|
|
210
|
+
|
|
211
|
+
if platform.system() != "Windows":
|
|
212
|
+
return False
|
|
213
|
+
|
|
214
|
+
if _original_ctrl_handler is None:
|
|
215
|
+
return True # Nothing to restore
|
|
216
|
+
|
|
217
|
+
try:
|
|
218
|
+
import ctypes
|
|
219
|
+
|
|
220
|
+
kernel32 = ctypes.windll.kernel32
|
|
221
|
+
|
|
222
|
+
# Get stdin handle
|
|
223
|
+
STD_INPUT_HANDLE = -10
|
|
224
|
+
stdin_handle = kernel32.GetStdHandle(STD_INPUT_HANDLE)
|
|
225
|
+
|
|
226
|
+
# Restore original mode
|
|
227
|
+
if kernel32.SetConsoleMode(stdin_handle, _original_ctrl_handler):
|
|
228
|
+
_original_ctrl_handler = None
|
|
229
|
+
return True
|
|
230
|
+
return False
|
|
231
|
+
|
|
232
|
+
except Exception:
|
|
233
|
+
return False
|
|
234
|
+
|
|
235
|
+
|
|
236
|
+
# Flag to track if we should keep Ctrl+C disabled
|
|
237
|
+
_keep_ctrl_c_disabled: bool = False
|
|
238
|
+
|
|
239
|
+
|
|
240
|
+
def set_keep_ctrl_c_disabled(value: bool) -> None:
|
|
241
|
+
"""Set whether Ctrl+C should be kept disabled.
|
|
242
|
+
|
|
243
|
+
When True, ensure_ctrl_c_disabled() will re-disable Ctrl+C
|
|
244
|
+
even if something else (like prompt_toolkit) re-enables it.
|
|
245
|
+
"""
|
|
246
|
+
global _keep_ctrl_c_disabled
|
|
247
|
+
_keep_ctrl_c_disabled = value
|
|
248
|
+
|
|
249
|
+
|
|
250
|
+
def ensure_ctrl_c_disabled() -> bool:
|
|
251
|
+
"""Ensure Ctrl+C is disabled if it should be.
|
|
252
|
+
|
|
253
|
+
Call this after operations that might restore console mode
|
|
254
|
+
(like prompt_toolkit input).
|
|
255
|
+
|
|
256
|
+
Returns:
|
|
257
|
+
True if Ctrl+C is now disabled (or wasn't needed), False on error.
|
|
258
|
+
"""
|
|
259
|
+
if not _keep_ctrl_c_disabled:
|
|
260
|
+
return True
|
|
261
|
+
|
|
262
|
+
if platform.system() != "Windows":
|
|
263
|
+
return True
|
|
264
|
+
|
|
265
|
+
try:
|
|
266
|
+
import ctypes
|
|
267
|
+
|
|
268
|
+
kernel32 = ctypes.windll.kernel32
|
|
269
|
+
|
|
270
|
+
# Get stdin handle
|
|
271
|
+
STD_INPUT_HANDLE = -10
|
|
272
|
+
stdin_handle = kernel32.GetStdHandle(STD_INPUT_HANDLE)
|
|
273
|
+
|
|
274
|
+
# Get current console mode
|
|
275
|
+
mode = ctypes.c_ulong()
|
|
276
|
+
if not kernel32.GetConsoleMode(stdin_handle, ctypes.byref(mode)):
|
|
277
|
+
return False
|
|
278
|
+
|
|
279
|
+
# Console mode flags
|
|
280
|
+
ENABLE_PROCESSED_INPUT = 0x0001
|
|
281
|
+
|
|
282
|
+
# Check if Ctrl+C processing is enabled
|
|
283
|
+
if mode.value & ENABLE_PROCESSED_INPUT:
|
|
284
|
+
# Disable it
|
|
285
|
+
new_mode = mode.value & ~ENABLE_PROCESSED_INPUT
|
|
286
|
+
return bool(kernel32.SetConsoleMode(stdin_handle, new_mode))
|
|
287
|
+
|
|
288
|
+
return True # Already disabled
|
|
289
|
+
|
|
290
|
+
except Exception:
|
|
291
|
+
return False
|