code-puppy 0.0.325__py3-none-any.whl → 0.0.336__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 +41 -103
- code_puppy/cli_runner.py +105 -2
- code_puppy/command_line/add_model_menu.py +4 -0
- code_puppy/command_line/autosave_menu.py +5 -0
- code_puppy/command_line/colors_menu.py +5 -0
- code_puppy/command_line/config_commands.py +24 -1
- code_puppy/command_line/core_commands.py +51 -0
- code_puppy/command_line/diff_menu.py +5 -0
- code_puppy/command_line/mcp/custom_server_form.py +4 -0
- code_puppy/command_line/mcp/install_menu.py +5 -1
- code_puppy/command_line/model_settings_menu.py +5 -0
- code_puppy/command_line/motd.py +13 -7
- code_puppy/command_line/onboarding_slides.py +180 -0
- code_puppy/command_line/onboarding_wizard.py +340 -0
- code_puppy/config.py +3 -2
- code_puppy/http_utils.py +155 -196
- code_puppy/keymap.py +10 -8
- code_puppy/messaging/rich_renderer.py +101 -19
- code_puppy/model_factory.py +86 -15
- code_puppy/plugins/antigravity_oauth/__init__.py +10 -0
- code_puppy/plugins/antigravity_oauth/accounts.py +406 -0
- code_puppy/plugins/antigravity_oauth/antigravity_model.py +653 -0
- code_puppy/plugins/antigravity_oauth/config.py +42 -0
- code_puppy/plugins/antigravity_oauth/constants.py +136 -0
- code_puppy/plugins/antigravity_oauth/oauth.py +478 -0
- code_puppy/plugins/antigravity_oauth/register_callbacks.py +406 -0
- code_puppy/plugins/antigravity_oauth/storage.py +271 -0
- code_puppy/plugins/antigravity_oauth/test_plugin.py +319 -0
- code_puppy/plugins/antigravity_oauth/token.py +167 -0
- code_puppy/plugins/antigravity_oauth/transport.py +664 -0
- code_puppy/plugins/antigravity_oauth/utils.py +169 -0
- code_puppy/plugins/chatgpt_oauth/register_callbacks.py +2 -0
- code_puppy/plugins/claude_code_oauth/register_callbacks.py +2 -0
- code_puppy/reopenable_async_client.py +8 -8
- code_puppy/terminal_utils.py +168 -3
- code_puppy/tools/command_runner.py +42 -54
- code_puppy/uvx_detection.py +242 -0
- {code_puppy-0.0.325.dist-info → code_puppy-0.0.336.dist-info}/METADATA +30 -1
- {code_puppy-0.0.325.dist-info → code_puppy-0.0.336.dist-info}/RECORD +44 -29
- {code_puppy-0.0.325.data → code_puppy-0.0.336.data}/data/code_puppy/models.json +0 -0
- {code_puppy-0.0.325.data → code_puppy-0.0.336.data}/data/code_puppy/models_dev_api.json +0 -0
- {code_puppy-0.0.325.dist-info → code_puppy-0.0.336.dist-info}/WHEEL +0 -0
- {code_puppy-0.0.325.dist-info → code_puppy-0.0.336.dist-info}/entry_points.txt +0 -0
- {code_puppy-0.0.325.dist-info → code_puppy-0.0.336.dist-info}/licenses/LICENSE +0 -0
code_puppy/agents/base_agent.py
CHANGED
|
@@ -4,7 +4,6 @@ import asyncio
|
|
|
4
4
|
import json
|
|
5
5
|
import math
|
|
6
6
|
import signal
|
|
7
|
-
import sys
|
|
8
7
|
import threading
|
|
9
8
|
import uuid
|
|
10
9
|
from abc import ABC, abstractmethod
|
|
@@ -1383,15 +1382,14 @@ class BaseAgent(ABC):
|
|
|
1383
1382
|
def _print_thinking_banner() -> None:
|
|
1384
1383
|
"""Print the THINKING banner with spinner pause and line clear."""
|
|
1385
1384
|
nonlocal did_stream_anything
|
|
1386
|
-
import sys
|
|
1387
1385
|
import time
|
|
1388
1386
|
|
|
1389
1387
|
from code_puppy.config import get_banner_color
|
|
1390
1388
|
|
|
1391
1389
|
pause_all_spinners()
|
|
1392
1390
|
time.sleep(0.1) # Delay to let spinner fully clear
|
|
1393
|
-
|
|
1394
|
-
|
|
1391
|
+
# Clear line and print newline before banner
|
|
1392
|
+
console.print(" " * 50, end="\r")
|
|
1395
1393
|
console.print() # Newline before banner
|
|
1396
1394
|
# Bold banner with configurable color and lightning bolt
|
|
1397
1395
|
thinking_color = get_banner_color("thinking")
|
|
@@ -1401,21 +1399,19 @@ class BaseAgent(ABC):
|
|
|
1401
1399
|
),
|
|
1402
1400
|
end="",
|
|
1403
1401
|
)
|
|
1404
|
-
sys.stdout.flush()
|
|
1405
1402
|
did_stream_anything = True
|
|
1406
1403
|
|
|
1407
1404
|
def _print_response_banner() -> None:
|
|
1408
1405
|
"""Print the AGENT RESPONSE banner with spinner pause and line clear."""
|
|
1409
1406
|
nonlocal did_stream_anything
|
|
1410
|
-
import sys
|
|
1411
1407
|
import time
|
|
1412
1408
|
|
|
1413
1409
|
from code_puppy.config import get_banner_color
|
|
1414
1410
|
|
|
1415
1411
|
pause_all_spinners()
|
|
1416
1412
|
time.sleep(0.1) # Delay to let spinner fully clear
|
|
1417
|
-
|
|
1418
|
-
|
|
1413
|
+
# Clear line and print newline before banner
|
|
1414
|
+
console.print(" " * 50, end="\r")
|
|
1419
1415
|
console.print() # Newline before banner
|
|
1420
1416
|
response_color = get_banner_color("agent_response")
|
|
1421
1417
|
console.print(
|
|
@@ -1423,7 +1419,6 @@ class BaseAgent(ABC):
|
|
|
1423
1419
|
f"[bold white on {response_color}] AGENT RESPONSE [/bold white on {response_color}]"
|
|
1424
1420
|
)
|
|
1425
1421
|
)
|
|
1426
|
-
sys.stdout.flush()
|
|
1427
1422
|
did_stream_anything = True
|
|
1428
1423
|
|
|
1429
1424
|
async for event in events:
|
|
@@ -1447,8 +1442,8 @@ class BaseAgent(ABC):
|
|
|
1447
1442
|
# Buffer initial content if present
|
|
1448
1443
|
if part.content and part.content.strip():
|
|
1449
1444
|
text_buffer[event.index].append(part.content)
|
|
1450
|
-
#
|
|
1451
|
-
token_count[event.index] +=
|
|
1445
|
+
# Count chunks (each part counts as 1)
|
|
1446
|
+
token_count[event.index] += 1
|
|
1452
1447
|
elif isinstance(part, ToolCallPart):
|
|
1453
1448
|
streaming_parts.add(event.index)
|
|
1454
1449
|
tool_parts.add(event.index)
|
|
@@ -1466,24 +1461,20 @@ class BaseAgent(ABC):
|
|
|
1466
1461
|
if delta.content_delta:
|
|
1467
1462
|
# For text parts, show token counter then render at end
|
|
1468
1463
|
if event.index in text_parts:
|
|
1469
|
-
import sys
|
|
1470
|
-
|
|
1471
1464
|
# Print banner on first content
|
|
1472
1465
|
if event.index not in banner_printed:
|
|
1473
1466
|
_print_response_banner()
|
|
1474
1467
|
banner_printed.add(event.index)
|
|
1475
1468
|
# Accumulate text for final markdown render
|
|
1476
1469
|
text_buffer[event.index].append(delta.content_delta)
|
|
1477
|
-
#
|
|
1478
|
-
token_count[event.index] +=
|
|
1479
|
-
|
|
1480
|
-
)
|
|
1481
|
-
# Update token counter in place (single line)
|
|
1470
|
+
# Count chunks received
|
|
1471
|
+
token_count[event.index] += 1
|
|
1472
|
+
# Update chunk counter in place (single line)
|
|
1482
1473
|
count = token_count[event.index]
|
|
1483
|
-
|
|
1484
|
-
f"
|
|
1474
|
+
console.print(
|
|
1475
|
+
f" ⏳ Receiving... {count} chunks ",
|
|
1476
|
+
end="\r",
|
|
1485
1477
|
)
|
|
1486
|
-
sys.stdout.flush()
|
|
1487
1478
|
else:
|
|
1488
1479
|
# For thinking parts, stream immediately (dim)
|
|
1489
1480
|
if event.index not in banner_printed:
|
|
@@ -1492,34 +1483,30 @@ class BaseAgent(ABC):
|
|
|
1492
1483
|
escaped = escape(delta.content_delta)
|
|
1493
1484
|
console.print(f"[dim]{escaped}[/dim]", end="")
|
|
1494
1485
|
elif isinstance(delta, ToolCallPartDelta):
|
|
1495
|
-
|
|
1496
|
-
|
|
1497
|
-
# For tool calls, show token counter (use string repr for estimation)
|
|
1498
|
-
token_count[event.index] += len(str(delta)) // 3
|
|
1486
|
+
# For tool calls, count chunks received
|
|
1487
|
+
token_count[event.index] += 1
|
|
1499
1488
|
# Get tool name if available
|
|
1500
1489
|
tool_name = getattr(delta, "tool_name_delta", "")
|
|
1501
1490
|
count = token_count[event.index]
|
|
1502
1491
|
# Display with tool wrench icon and tool name
|
|
1503
1492
|
if tool_name:
|
|
1504
|
-
|
|
1505
|
-
f"
|
|
1493
|
+
console.print(
|
|
1494
|
+
f" 🔧 Calling {tool_name}... {count} chunks ",
|
|
1495
|
+
end="\r",
|
|
1506
1496
|
)
|
|
1507
1497
|
else:
|
|
1508
|
-
|
|
1509
|
-
f"
|
|
1498
|
+
console.print(
|
|
1499
|
+
f" 🔧 Calling tool... {count} chunks ",
|
|
1500
|
+
end="\r",
|
|
1510
1501
|
)
|
|
1511
|
-
sys.stdout.flush()
|
|
1512
1502
|
|
|
1513
1503
|
# PartEndEvent - finish the streaming with a newline
|
|
1514
1504
|
elif isinstance(event, PartEndEvent):
|
|
1515
1505
|
if event.index in streaming_parts:
|
|
1516
|
-
import sys
|
|
1517
|
-
|
|
1518
1506
|
# For text parts, clear counter line and render markdown
|
|
1519
1507
|
if event.index in text_parts:
|
|
1520
|
-
# Clear the
|
|
1521
|
-
|
|
1522
|
-
sys.stdout.flush()
|
|
1508
|
+
# Clear the chunk counter line by printing spaces and returning
|
|
1509
|
+
console.print(" " * 50, end="\r")
|
|
1523
1510
|
# Render the final markdown nicely
|
|
1524
1511
|
if event.index in text_buffer:
|
|
1525
1512
|
try:
|
|
@@ -1529,11 +1516,10 @@ class BaseAgent(ABC):
|
|
|
1529
1516
|
except Exception:
|
|
1530
1517
|
pass
|
|
1531
1518
|
del text_buffer[event.index]
|
|
1532
|
-
# For tool parts, clear the
|
|
1519
|
+
# For tool parts, clear the chunk counter line
|
|
1533
1520
|
elif event.index in tool_parts:
|
|
1534
|
-
# Clear the
|
|
1535
|
-
|
|
1536
|
-
sys.stdout.flush()
|
|
1521
|
+
# Clear the chunk counter line by printing spaces and returning
|
|
1522
|
+
console.print(" " * 50, end="\r")
|
|
1537
1523
|
# For thinking parts, just print newline
|
|
1538
1524
|
elif event.index in banner_printed:
|
|
1539
1525
|
console.print() # Final newline after streaming
|
|
@@ -1952,74 +1938,35 @@ class BaseAgent(ABC):
|
|
|
1952
1938
|
def graceful_sigint_handler(_sig, _frame):
|
|
1953
1939
|
# When using keyboard-based cancel, SIGINT should be a no-op
|
|
1954
1940
|
# (just show a hint to user about the configured cancel key)
|
|
1955
|
-
|
|
1956
|
-
|
|
1941
|
+
# Also reset terminal to prevent bricking on Windows+uvx
|
|
1957
1942
|
from code_puppy.keymap import get_cancel_agent_display_name
|
|
1943
|
+
from code_puppy.terminal_utils import reset_windows_terminal_full
|
|
1944
|
+
|
|
1945
|
+
# Reset terminal state first to prevent bricking
|
|
1946
|
+
reset_windows_terminal_full()
|
|
1958
1947
|
|
|
1959
1948
|
cancel_key = get_cancel_agent_display_name()
|
|
1960
|
-
|
|
1961
|
-
# On Windows, we use keyboard listener, so SIGINT might still fire
|
|
1962
|
-
# but we handle cancellation via the key listener
|
|
1963
|
-
pass # Silent on Windows - the key listener handles it
|
|
1964
|
-
else:
|
|
1965
|
-
emit_info(f"Use {cancel_key} to cancel the agent task.")
|
|
1949
|
+
emit_info(f"Use {cancel_key} to cancel the agent task.")
|
|
1966
1950
|
|
|
1967
1951
|
original_handler = None
|
|
1968
1952
|
key_listener_stop_event = None
|
|
1969
1953
|
_key_listener_thread = None
|
|
1970
|
-
_windows_ctrl_handler = None # Store reference to prevent garbage collection
|
|
1971
1954
|
|
|
1972
1955
|
try:
|
|
1973
|
-
if
|
|
1974
|
-
#
|
|
1975
|
-
import ctypes
|
|
1976
|
-
|
|
1977
|
-
# Define the handler function type
|
|
1978
|
-
HANDLER_ROUTINE = ctypes.WINFUNCTYPE(ctypes.c_bool, ctypes.c_ulong)
|
|
1979
|
-
|
|
1980
|
-
def windows_ctrl_handler(ctrl_type):
|
|
1981
|
-
"""Handle Windows console control events."""
|
|
1982
|
-
CTRL_C_EVENT = 0
|
|
1983
|
-
CTRL_BREAK_EVENT = 1
|
|
1984
|
-
|
|
1985
|
-
if ctrl_type in (CTRL_C_EVENT, CTRL_BREAK_EVENT):
|
|
1986
|
-
# Check if we're awaiting user input
|
|
1987
|
-
if is_awaiting_user_input():
|
|
1988
|
-
return False # Let default handler run
|
|
1989
|
-
|
|
1990
|
-
# Schedule agent cancellation
|
|
1991
|
-
schedule_agent_cancel()
|
|
1992
|
-
return True # We handled it, don't terminate
|
|
1993
|
-
|
|
1994
|
-
return False # Let other handlers process it
|
|
1995
|
-
|
|
1996
|
-
# Create the callback - must keep reference alive!
|
|
1997
|
-
_windows_ctrl_handler = HANDLER_ROUTINE(windows_ctrl_handler)
|
|
1998
|
-
|
|
1999
|
-
# Register the handler
|
|
2000
|
-
kernel32 = ctypes.windll.kernel32
|
|
2001
|
-
if not kernel32.SetConsoleCtrlHandler(_windows_ctrl_handler, True):
|
|
2002
|
-
emit_warning("Failed to set Windows Ctrl+C handler")
|
|
2003
|
-
|
|
2004
|
-
# Also spawn keyboard listener for Ctrl+X (shell cancel) and other keys
|
|
2005
|
-
key_listener_stop_event = threading.Event()
|
|
2006
|
-
_key_listener_thread = self._spawn_ctrl_x_key_listener(
|
|
2007
|
-
key_listener_stop_event,
|
|
2008
|
-
on_escape=lambda: None, # Ctrl+X handled by command_runner
|
|
2009
|
-
on_cancel_agent=None, # Ctrl+C handled by SetConsoleCtrlHandler above
|
|
2010
|
-
)
|
|
2011
|
-
elif cancel_agent_uses_signal():
|
|
2012
|
-
# Unix with Ctrl+C: Use SIGINT-based cancellation
|
|
1956
|
+
if cancel_agent_uses_signal():
|
|
1957
|
+
# Use SIGINT-based cancellation (default Ctrl+C behavior)
|
|
2013
1958
|
original_handler = signal.signal(
|
|
2014
1959
|
signal.SIGINT, keyboard_interrupt_handler
|
|
2015
1960
|
)
|
|
2016
1961
|
else:
|
|
2017
|
-
#
|
|
1962
|
+
# Use keyboard listener for agent cancellation
|
|
1963
|
+
# Set a graceful SIGINT handler that shows a hint
|
|
2018
1964
|
original_handler = signal.signal(signal.SIGINT, graceful_sigint_handler)
|
|
1965
|
+
# Spawn keyboard listener with the cancel agent callback
|
|
2019
1966
|
key_listener_stop_event = threading.Event()
|
|
2020
1967
|
_key_listener_thread = self._spawn_ctrl_x_key_listener(
|
|
2021
1968
|
key_listener_stop_event,
|
|
2022
|
-
on_escape=lambda: None,
|
|
1969
|
+
on_escape=lambda: None, # Ctrl+X handled by command_runner
|
|
2023
1970
|
on_cancel_agent=schedule_agent_cancel,
|
|
2024
1971
|
)
|
|
2025
1972
|
|
|
@@ -2044,17 +1991,8 @@ class BaseAgent(ABC):
|
|
|
2044
1991
|
# Stop keyboard listener if it was started
|
|
2045
1992
|
if key_listener_stop_event is not None:
|
|
2046
1993
|
key_listener_stop_event.set()
|
|
2047
|
-
|
|
2048
|
-
|
|
2049
|
-
|
|
2050
|
-
|
|
2051
|
-
import ctypes
|
|
2052
|
-
|
|
2053
|
-
kernel32 = ctypes.windll.kernel32
|
|
2054
|
-
kernel32.SetConsoleCtrlHandler(_windows_ctrl_handler, False)
|
|
2055
|
-
except Exception:
|
|
2056
|
-
pass # Best effort cleanup
|
|
2057
|
-
|
|
2058
|
-
# Restore original signal handler (Unix)
|
|
2059
|
-
if original_handler is not None:
|
|
1994
|
+
# Restore original signal handler
|
|
1995
|
+
if (
|
|
1996
|
+
original_handler is not None
|
|
1997
|
+
): # Explicit None check - SIG_DFL can be 0/falsy!
|
|
2060
1998
|
signal.signal(signal.SIGINT, original_handler)
|
code_puppy/cli_runner.py
CHANGED
|
@@ -171,6 +171,45 @@ async def main():
|
|
|
171
171
|
emit_error(str(e))
|
|
172
172
|
sys.exit(1)
|
|
173
173
|
|
|
174
|
+
# Show uvx detection notice if we're on Windows + uvx
|
|
175
|
+
# Also disable Ctrl+C at the console level to prevent terminal bricking
|
|
176
|
+
try:
|
|
177
|
+
from code_puppy.uvx_detection import should_use_alternate_cancel_key
|
|
178
|
+
|
|
179
|
+
if should_use_alternate_cancel_key():
|
|
180
|
+
from code_puppy.terminal_utils import (
|
|
181
|
+
disable_windows_ctrl_c,
|
|
182
|
+
set_keep_ctrl_c_disabled,
|
|
183
|
+
)
|
|
184
|
+
|
|
185
|
+
# Disable Ctrl+C at the console input level
|
|
186
|
+
# This prevents Ctrl+C from being processed as a signal at all
|
|
187
|
+
disable_windows_ctrl_c()
|
|
188
|
+
|
|
189
|
+
# Set flag to keep it disabled (prompt_toolkit may re-enable it)
|
|
190
|
+
set_keep_ctrl_c_disabled(True)
|
|
191
|
+
|
|
192
|
+
# Use print directly - emit_system_message can get cleared by ANSI codes
|
|
193
|
+
print(
|
|
194
|
+
"🔧 Detected uvx launch on Windows - using Ctrl+K for cancellation "
|
|
195
|
+
"(Ctrl+C is disabled to prevent terminal issues)"
|
|
196
|
+
)
|
|
197
|
+
|
|
198
|
+
# Also install a SIGINT handler as backup
|
|
199
|
+
import signal
|
|
200
|
+
|
|
201
|
+
from code_puppy.terminal_utils import reset_windows_terminal_full
|
|
202
|
+
|
|
203
|
+
def _uvx_protective_sigint_handler(_sig, _frame):
|
|
204
|
+
"""Protective SIGINT handler for Windows+uvx."""
|
|
205
|
+
reset_windows_terminal_full()
|
|
206
|
+
# Re-disable Ctrl+C in case something re-enabled it
|
|
207
|
+
disable_windows_ctrl_c()
|
|
208
|
+
|
|
209
|
+
signal.signal(signal.SIGINT, _uvx_protective_sigint_handler)
|
|
210
|
+
except ImportError:
|
|
211
|
+
pass # uvx_detection module not available, ignore
|
|
212
|
+
|
|
174
213
|
# Load API keys from puppy.cfg into environment variables
|
|
175
214
|
from code_puppy.config import load_api_keys_to_environment
|
|
176
215
|
|
|
@@ -325,6 +364,7 @@ async def interactive_mode(message_renderer, initial_command: str = None) -> Non
|
|
|
325
364
|
emit_system_message(
|
|
326
365
|
"Use /diff to configure diff highlighting colors for file changes."
|
|
327
366
|
)
|
|
367
|
+
emit_system_message("To re-run the tutorial, use /tutorial.")
|
|
328
368
|
try:
|
|
329
369
|
from code_puppy.command_line.motd import print_motd
|
|
330
370
|
|
|
@@ -417,6 +457,45 @@ async def interactive_mode(message_renderer, initial_command: str = None) -> Non
|
|
|
417
457
|
|
|
418
458
|
# Autosave loading is now manual - use /autosave_load command
|
|
419
459
|
|
|
460
|
+
# Auto-run tutorial on first startup
|
|
461
|
+
try:
|
|
462
|
+
from code_puppy.command_line.onboarding_wizard import should_show_onboarding
|
|
463
|
+
|
|
464
|
+
if should_show_onboarding():
|
|
465
|
+
import asyncio
|
|
466
|
+
import concurrent.futures
|
|
467
|
+
|
|
468
|
+
from code_puppy.command_line.onboarding_wizard import run_onboarding_wizard
|
|
469
|
+
from code_puppy.config import set_model_name
|
|
470
|
+
from code_puppy.messaging import emit_info
|
|
471
|
+
|
|
472
|
+
with concurrent.futures.ThreadPoolExecutor() as executor:
|
|
473
|
+
future = executor.submit(lambda: asyncio.run(run_onboarding_wizard()))
|
|
474
|
+
result = future.result(timeout=300)
|
|
475
|
+
|
|
476
|
+
if result == "chatgpt":
|
|
477
|
+
emit_info("🔐 Starting ChatGPT OAuth flow...")
|
|
478
|
+
from code_puppy.plugins.chatgpt_oauth.oauth_flow import run_oauth_flow
|
|
479
|
+
|
|
480
|
+
run_oauth_flow()
|
|
481
|
+
set_model_name("chatgpt-gpt-5.2-codex")
|
|
482
|
+
elif result == "claude":
|
|
483
|
+
emit_info("🔐 Starting Claude Code OAuth flow...")
|
|
484
|
+
from code_puppy.plugins.claude_code_oauth.register_callbacks import (
|
|
485
|
+
_perform_authentication,
|
|
486
|
+
)
|
|
487
|
+
|
|
488
|
+
_perform_authentication()
|
|
489
|
+
set_model_name("claude-code-claude-opus-4-5-20251101")
|
|
490
|
+
elif result == "completed":
|
|
491
|
+
emit_info("🎉 Tutorial complete! Happy coding!")
|
|
492
|
+
elif result == "skipped":
|
|
493
|
+
emit_info("⏭️ Tutorial skipped. Run /tutorial anytime!")
|
|
494
|
+
except Exception as e:
|
|
495
|
+
from code_puppy.messaging import emit_warning
|
|
496
|
+
|
|
497
|
+
emit_warning(f"Tutorial auto-start failed: {e}")
|
|
498
|
+
|
|
420
499
|
# Track the current agent task for cancellation on quit
|
|
421
500
|
current_agent_task = None
|
|
422
501
|
|
|
@@ -440,6 +519,15 @@ async def interactive_mode(message_renderer, initial_command: str = None) -> Non
|
|
|
440
519
|
task = await get_input_with_combined_completion(
|
|
441
520
|
get_prompt_with_active_model(), history_file=COMMAND_HISTORY_FILE
|
|
442
521
|
)
|
|
522
|
+
|
|
523
|
+
# Windows+uvx: Re-disable Ctrl+C after prompt_toolkit
|
|
524
|
+
# (prompt_toolkit restores console mode which re-enables Ctrl+C)
|
|
525
|
+
try:
|
|
526
|
+
from code_puppy.terminal_utils import ensure_ctrl_c_disabled
|
|
527
|
+
|
|
528
|
+
ensure_ctrl_c_disabled()
|
|
529
|
+
except ImportError:
|
|
530
|
+
pass
|
|
443
531
|
except ImportError:
|
|
444
532
|
# Fall back to basic input if prompt_toolkit is not available
|
|
445
533
|
task = input(">>> ")
|
|
@@ -605,6 +693,13 @@ async def interactive_mode(message_renderer, initial_command: str = None) -> Non
|
|
|
605
693
|
if result is None:
|
|
606
694
|
# Windows-specific: Reset terminal state after cancellation
|
|
607
695
|
reset_windows_terminal_ansi()
|
|
696
|
+
# Re-disable Ctrl+C if needed (uvx mode)
|
|
697
|
+
try:
|
|
698
|
+
from code_puppy.terminal_utils import ensure_ctrl_c_disabled
|
|
699
|
+
|
|
700
|
+
ensure_ctrl_c_disabled()
|
|
701
|
+
except ImportError:
|
|
702
|
+
pass
|
|
608
703
|
continue
|
|
609
704
|
# Get the structured response
|
|
610
705
|
agent_response = result.output
|
|
@@ -645,6 +740,15 @@ async def interactive_mode(message_renderer, initial_command: str = None) -> Non
|
|
|
645
740
|
|
|
646
741
|
auto_save_session_if_enabled()
|
|
647
742
|
|
|
743
|
+
# Re-disable Ctrl+C if needed (uvx mode) - must be done after
|
|
744
|
+
# each iteration as various operations may restore console mode
|
|
745
|
+
try:
|
|
746
|
+
from code_puppy.terminal_utils import ensure_ctrl_c_disabled
|
|
747
|
+
|
|
748
|
+
ensure_ctrl_c_disabled()
|
|
749
|
+
except ImportError:
|
|
750
|
+
pass
|
|
751
|
+
|
|
648
752
|
|
|
649
753
|
def prettier_code_blocks():
|
|
650
754
|
"""Configure Rich to use prettier code block rendering."""
|
|
@@ -790,6 +894,5 @@ def main_entry():
|
|
|
790
894
|
DBOS.destroy()
|
|
791
895
|
return 0
|
|
792
896
|
finally:
|
|
793
|
-
# Reset terminal on
|
|
794
|
-
reset_windows_terminal_full() # Safe no-op on non-Windows
|
|
897
|
+
# Reset terminal on Unix-like systems (not Windows)
|
|
795
898
|
reset_unix_terminal()
|
|
@@ -995,6 +995,10 @@ class AddModelMenu:
|
|
|
995
995
|
# Reset awaiting input flag
|
|
996
996
|
set_awaiting_user_input(False)
|
|
997
997
|
|
|
998
|
+
# Clear exit message (unless we're about to prompt for more input)
|
|
999
|
+
if self.result not in ("pending_credentials", "pending_custom_model"):
|
|
1000
|
+
emit_info("✓ Exited model browser")
|
|
1001
|
+
|
|
998
1002
|
# Handle unsupported provider
|
|
999
1003
|
if self.result == "unsupported" and self.current_provider:
|
|
1000
1004
|
reason = UNSUPPORTED_PROVIDERS.get(
|
|
@@ -603,4 +603,9 @@ async def interactive_autosave_picker() -> Optional[str]:
|
|
|
603
603
|
# Reset awaiting input flag
|
|
604
604
|
set_awaiting_user_input(False)
|
|
605
605
|
|
|
606
|
+
# Clear exit message
|
|
607
|
+
from code_puppy.messaging import emit_info
|
|
608
|
+
|
|
609
|
+
emit_info("✓ Exited session browser")
|
|
610
|
+
|
|
606
611
|
return result[0]
|
|
@@ -230,6 +230,11 @@ async def interactive_colors_picker() -> Optional[dict]:
|
|
|
230
230
|
sys.stdout.write("\033[?1049l") # Exit alternate buffer
|
|
231
231
|
sys.stdout.flush()
|
|
232
232
|
|
|
233
|
+
# Clear exit message
|
|
234
|
+
from code_puppy.messaging import emit_info
|
|
235
|
+
|
|
236
|
+
emit_info("✓ Exited banner color configuration")
|
|
237
|
+
|
|
233
238
|
# Return changes if any
|
|
234
239
|
if config.has_changes():
|
|
235
240
|
return config.current_colors
|
|
@@ -46,6 +46,7 @@ def handle_show_command(command: str) -> bool:
|
|
|
46
46
|
get_use_dbos,
|
|
47
47
|
get_yolo_mode,
|
|
48
48
|
)
|
|
49
|
+
from code_puppy.keymap import get_cancel_agent_display_name
|
|
49
50
|
from code_puppy.messaging import emit_info
|
|
50
51
|
|
|
51
52
|
puppy_name = get_puppy_name()
|
|
@@ -79,6 +80,7 @@ def handle_show_command(command: str) -> bool:
|
|
|
79
80
|
[bold]reasoning_effort:[/bold] [cyan]{get_openai_reasoning_effort()}[/cyan]
|
|
80
81
|
[bold]verbosity:[/bold] [cyan]{get_openai_verbosity()}[/cyan]
|
|
81
82
|
[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 ""}
|
|
83
|
+
[bold]cancel_agent_key:[/bold] [cyan]{get_cancel_agent_display_name()}[/cyan] (options: ctrl+c, ctrl+k, ctrl+q)
|
|
82
84
|
|
|
83
85
|
"""
|
|
84
86
|
emit_info(Text.from_markup(status_msg))
|
|
@@ -200,9 +202,13 @@ def handle_set_command(command: str) -> bool:
|
|
|
200
202
|
"\n[yellow]Session Management[/yellow]"
|
|
201
203
|
"\n [cyan]auto_save_session[/cyan] Auto-save chat after every response (true/false)"
|
|
202
204
|
)
|
|
205
|
+
keymap_help = (
|
|
206
|
+
"\n[yellow]Keyboard Shortcuts[/yellow]"
|
|
207
|
+
"\n [cyan]cancel_agent_key[/cyan] Key to cancel agent tasks (ctrl+c, ctrl+k, or ctrl+q)"
|
|
208
|
+
)
|
|
203
209
|
emit_warning(
|
|
204
210
|
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}"
|
|
211
|
+
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}{keymap_help}"
|
|
206
212
|
)
|
|
207
213
|
)
|
|
208
214
|
return True
|
|
@@ -215,6 +221,23 @@ def handle_set_command(command: str) -> bool:
|
|
|
215
221
|
)
|
|
216
222
|
)
|
|
217
223
|
|
|
224
|
+
# Validate cancel_agent_key before setting
|
|
225
|
+
if key == "cancel_agent_key":
|
|
226
|
+
from code_puppy.keymap import VALID_CANCEL_KEYS
|
|
227
|
+
|
|
228
|
+
normalized_value = value.strip().lower()
|
|
229
|
+
if normalized_value not in VALID_CANCEL_KEYS:
|
|
230
|
+
emit_error(
|
|
231
|
+
f"Invalid cancel_agent_key '{value}'. Valid options: {', '.join(sorted(VALID_CANCEL_KEYS))}"
|
|
232
|
+
)
|
|
233
|
+
return True
|
|
234
|
+
value = normalized_value # Use normalized value
|
|
235
|
+
emit_info(
|
|
236
|
+
Text.from_markup(
|
|
237
|
+
"[yellow]⚠️ cancel_agent_key changed. Please restart Code Puppy for this change to take effect.[/yellow]"
|
|
238
|
+
)
|
|
239
|
+
)
|
|
240
|
+
|
|
218
241
|
set_config_value(key, value)
|
|
219
242
|
emit_success(f'Set {key} = "{value}" in puppy.cfg!')
|
|
220
243
|
|
|
@@ -115,6 +115,57 @@ def handle_motd_command(command: str) -> bool:
|
|
|
115
115
|
return True
|
|
116
116
|
|
|
117
117
|
|
|
118
|
+
@register_command(
|
|
119
|
+
name="tutorial",
|
|
120
|
+
description="Run the interactive tutorial wizard",
|
|
121
|
+
usage="/tutorial",
|
|
122
|
+
category="core",
|
|
123
|
+
)
|
|
124
|
+
def handle_tutorial_command(command: str) -> bool:
|
|
125
|
+
"""Run the interactive tutorial wizard.
|
|
126
|
+
|
|
127
|
+
Usage:
|
|
128
|
+
/tutorial - Run the tutorial (can be run anytime)
|
|
129
|
+
"""
|
|
130
|
+
import asyncio
|
|
131
|
+
import concurrent.futures
|
|
132
|
+
|
|
133
|
+
from code_puppy.command_line.onboarding_wizard import (
|
|
134
|
+
reset_onboarding,
|
|
135
|
+
run_onboarding_wizard,
|
|
136
|
+
)
|
|
137
|
+
from code_puppy.config import set_model_name
|
|
138
|
+
|
|
139
|
+
# Always reset so user can re-run the tutorial anytime
|
|
140
|
+
reset_onboarding()
|
|
141
|
+
|
|
142
|
+
# Run the async wizard in a thread pool (same pattern as agent picker)
|
|
143
|
+
with concurrent.futures.ThreadPoolExecutor() as executor:
|
|
144
|
+
future = executor.submit(lambda: asyncio.run(run_onboarding_wizard()))
|
|
145
|
+
result = future.result(timeout=300) # 5 min timeout
|
|
146
|
+
|
|
147
|
+
if result == "chatgpt":
|
|
148
|
+
emit_info("🔐 Starting ChatGPT OAuth flow...")
|
|
149
|
+
from code_puppy.plugins.chatgpt_oauth.oauth_flow import run_oauth_flow
|
|
150
|
+
|
|
151
|
+
run_oauth_flow()
|
|
152
|
+
set_model_name("chatgpt-gpt-5.2-codex")
|
|
153
|
+
elif result == "claude":
|
|
154
|
+
emit_info("🔐 Starting Claude Code OAuth flow...")
|
|
155
|
+
from code_puppy.plugins.claude_code_oauth.register_callbacks import (
|
|
156
|
+
_perform_authentication,
|
|
157
|
+
)
|
|
158
|
+
|
|
159
|
+
_perform_authentication()
|
|
160
|
+
set_model_name("claude-code-claude-opus-4-5-20251101")
|
|
161
|
+
elif result == "completed":
|
|
162
|
+
emit_info("🎉 Tutorial complete! Happy coding!")
|
|
163
|
+
elif result == "skipped":
|
|
164
|
+
emit_info("⏭️ Tutorial skipped. Run /tutorial anytime!")
|
|
165
|
+
|
|
166
|
+
return True
|
|
167
|
+
|
|
168
|
+
|
|
118
169
|
@register_command(
|
|
119
170
|
name="exit",
|
|
120
171
|
description="Exit interactive mode",
|
|
@@ -456,6 +456,11 @@ async def interactive_diff_picker() -> Optional[dict]:
|
|
|
456
456
|
sys.stdout.write("\033[?1049l") # Exit alternate buffer
|
|
457
457
|
sys.stdout.flush()
|
|
458
458
|
|
|
459
|
+
# Clear exit message
|
|
460
|
+
from code_puppy.messaging import emit_info
|
|
461
|
+
|
|
462
|
+
emit_info("✓ Exited diff color configuration")
|
|
463
|
+
|
|
459
464
|
# Return changes if any
|
|
460
465
|
if config.has_changes():
|
|
461
466
|
return {
|
|
@@ -604,6 +604,10 @@ class CustomServerForm:
|
|
|
604
604
|
sys.stdout.flush()
|
|
605
605
|
set_awaiting_user_input(False)
|
|
606
606
|
|
|
607
|
+
# Clear exit message if not installing
|
|
608
|
+
if self.result != "installed":
|
|
609
|
+
emit_info("✓ Exited custom server form")
|
|
610
|
+
|
|
607
611
|
# Handle result
|
|
608
612
|
if self.result == "installed":
|
|
609
613
|
if self.edit_mode:
|
|
@@ -16,7 +16,7 @@ from prompt_toolkit.layout import Dimension, Layout, VSplit, Window
|
|
|
16
16
|
from prompt_toolkit.layout.controls import FormattedTextControl
|
|
17
17
|
from prompt_toolkit.widgets import Frame
|
|
18
18
|
|
|
19
|
-
from code_puppy.messaging import emit_error, emit_warning
|
|
19
|
+
from code_puppy.messaging import emit_error, emit_info, emit_warning
|
|
20
20
|
from code_puppy.tools.command_runner import set_awaiting_user_input
|
|
21
21
|
|
|
22
22
|
from .catalog_server_installer import (
|
|
@@ -635,6 +635,10 @@ class MCPInstallMenu:
|
|
|
635
635
|
sys.stdout.flush()
|
|
636
636
|
set_awaiting_user_input(False)
|
|
637
637
|
|
|
638
|
+
# Clear exit message (unless we're about to prompt for more input)
|
|
639
|
+
if self.result not in ("pending_custom", "pending_install"):
|
|
640
|
+
emit_info("✓ Exited MCP server browser")
|
|
641
|
+
|
|
638
642
|
# Handle custom server after TUI exits
|
|
639
643
|
if self.result == "pending_custom":
|
|
640
644
|
success = run_custom_server_form(self.manager)
|
|
@@ -835,6 +835,11 @@ class ModelSettingsMenu:
|
|
|
835
835
|
sys.stdout.flush()
|
|
836
836
|
set_awaiting_user_input(False)
|
|
837
837
|
|
|
838
|
+
# Clear exit message
|
|
839
|
+
from code_puppy.messaging import emit_info
|
|
840
|
+
|
|
841
|
+
emit_info("✓ Exited model settings")
|
|
842
|
+
|
|
838
843
|
return self.result_changed
|
|
839
844
|
|
|
840
845
|
|
code_puppy/command_line/motd.py
CHANGED
|
@@ -8,13 +8,19 @@ import os
|
|
|
8
8
|
from code_puppy.config import CONFIG_DIR
|
|
9
9
|
from code_puppy.messaging import emit_info
|
|
10
10
|
|
|
11
|
-
MOTD_VERSION = "
|
|
12
|
-
MOTD_MESSAGE = """
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
-
|
|
11
|
+
MOTD_VERSION = "2026-01-01"
|
|
12
|
+
MOTD_MESSAGE = """
|
|
13
|
+
# 🐶 Happy New Year! January 1st, 2026 🎉
|
|
14
|
+
Reminder that Code Puppy supports three different OAuth subscriptions:
|
|
15
|
+
|
|
16
|
+
### Claude Code - `/claude-code-auth`
|
|
17
|
+
- Opus / Haiku / Sonnet
|
|
18
|
+
|
|
19
|
+
### ChatGPT Pro/Plus - `/chatgpt-auth`
|
|
20
|
+
- gpt-5.2 and gpt-5.2 codex
|
|
21
|
+
|
|
22
|
+
### Google Antigravity - `/antigravity-auth`
|
|
23
|
+
- Gemini 3 Pro, Flash, and Anthropic models including Opus and Sonnet.
|
|
18
24
|
"""
|
|
19
25
|
MOTD_TRACK_FILE = os.path.join(CONFIG_DIR, "motd.txt")
|
|
20
26
|
|