code-puppy 0.0.323__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 +74 -93
- code_puppy/cli_runner.py +105 -2
- code_puppy/command_line/add_model_menu.py +15 -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/models.json +2 -2
- code_puppy/plugins/__init__.py +12 -0
- 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.323.data → code_puppy-0.0.336.data}/data/code_puppy/models.json +2 -2
- {code_puppy-0.0.323.dist-info → code_puppy-0.0.336.dist-info}/METADATA +30 -1
- {code_puppy-0.0.323.dist-info → code_puppy-0.0.336.dist-info}/RECORD +46 -31
- {code_puppy-0.0.323.data → code_puppy-0.0.336.data}/data/code_puppy/models_dev_api.json +0 -0
- {code_puppy-0.0.323.dist-info → code_puppy-0.0.336.dist-info}/WHEEL +0 -0
- {code_puppy-0.0.323.dist-info → code_puppy-0.0.336.dist-info}/entry_points.txt +0 -0
- {code_puppy-0.0.323.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
|
|
@@ -1340,15 +1339,19 @@ class BaseAgent(ABC):
|
|
|
1340
1339
|
) -> None:
|
|
1341
1340
|
"""Handle streaming events from the agent run.
|
|
1342
1341
|
|
|
1343
|
-
This method processes streaming events and emits TextPart
|
|
1344
|
-
content with styled banners as they stream in.
|
|
1342
|
+
This method processes streaming events and emits TextPart, ThinkingPart,
|
|
1343
|
+
and ToolCallPart content with styled banners/tokens as they stream in.
|
|
1345
1344
|
|
|
1346
1345
|
Args:
|
|
1347
1346
|
ctx: The run context.
|
|
1348
1347
|
events: Async iterable of streaming events (PartStartEvent, PartDeltaEvent, etc.).
|
|
1349
1348
|
"""
|
|
1350
1349
|
from pydantic_ai import PartDeltaEvent, PartStartEvent
|
|
1351
|
-
from pydantic_ai.messages import
|
|
1350
|
+
from pydantic_ai.messages import (
|
|
1351
|
+
TextPartDelta,
|
|
1352
|
+
ThinkingPartDelta,
|
|
1353
|
+
ToolCallPartDelta,
|
|
1354
|
+
)
|
|
1352
1355
|
from rich.console import Console
|
|
1353
1356
|
from rich.markdown import Markdown
|
|
1354
1357
|
from rich.markup import escape
|
|
@@ -1364,29 +1367,29 @@ class BaseAgent(ABC):
|
|
|
1364
1367
|
# Fallback if console not set (shouldn't happen in normal use)
|
|
1365
1368
|
console = Console()
|
|
1366
1369
|
|
|
1367
|
-
# Track which part indices we're currently streaming (for Text/Thinking parts)
|
|
1370
|
+
# Track which part indices we're currently streaming (for Text/Thinking/Tool parts)
|
|
1368
1371
|
streaming_parts: set[int] = set()
|
|
1369
1372
|
thinking_parts: set[int] = (
|
|
1370
1373
|
set()
|
|
1371
1374
|
) # Track which parts are thinking (for dim style)
|
|
1372
1375
|
text_parts: set[int] = set() # Track which parts are text
|
|
1376
|
+
tool_parts: set[int] = set() # Track which parts are tool calls
|
|
1373
1377
|
banner_printed: set[int] = set() # Track if banner was already printed
|
|
1374
1378
|
text_buffer: dict[int, list[str]] = {} # Buffer text for final markdown render
|
|
1375
|
-
token_count: dict[int, int] = {} # Track token count per text part
|
|
1379
|
+
token_count: dict[int, int] = {} # Track token count per text/tool part
|
|
1376
1380
|
did_stream_anything = False # Track if we streamed any content
|
|
1377
1381
|
|
|
1378
1382
|
def _print_thinking_banner() -> None:
|
|
1379
1383
|
"""Print the THINKING banner with spinner pause and line clear."""
|
|
1380
1384
|
nonlocal did_stream_anything
|
|
1381
|
-
import sys
|
|
1382
1385
|
import time
|
|
1383
1386
|
|
|
1384
1387
|
from code_puppy.config import get_banner_color
|
|
1385
1388
|
|
|
1386
1389
|
pause_all_spinners()
|
|
1387
1390
|
time.sleep(0.1) # Delay to let spinner fully clear
|
|
1388
|
-
|
|
1389
|
-
|
|
1391
|
+
# Clear line and print newline before banner
|
|
1392
|
+
console.print(" " * 50, end="\r")
|
|
1390
1393
|
console.print() # Newline before banner
|
|
1391
1394
|
# Bold banner with configurable color and lightning bolt
|
|
1392
1395
|
thinking_color = get_banner_color("thinking")
|
|
@@ -1396,21 +1399,19 @@ class BaseAgent(ABC):
|
|
|
1396
1399
|
),
|
|
1397
1400
|
end="",
|
|
1398
1401
|
)
|
|
1399
|
-
sys.stdout.flush()
|
|
1400
1402
|
did_stream_anything = True
|
|
1401
1403
|
|
|
1402
1404
|
def _print_response_banner() -> None:
|
|
1403
1405
|
"""Print the AGENT RESPONSE banner with spinner pause and line clear."""
|
|
1404
1406
|
nonlocal did_stream_anything
|
|
1405
|
-
import sys
|
|
1406
1407
|
import time
|
|
1407
1408
|
|
|
1408
1409
|
from code_puppy.config import get_banner_color
|
|
1409
1410
|
|
|
1410
1411
|
pause_all_spinners()
|
|
1411
1412
|
time.sleep(0.1) # Delay to let spinner fully clear
|
|
1412
|
-
|
|
1413
|
-
|
|
1413
|
+
# Clear line and print newline before banner
|
|
1414
|
+
console.print(" " * 50, end="\r")
|
|
1414
1415
|
console.print() # Newline before banner
|
|
1415
1416
|
response_color = get_banner_color("agent_response")
|
|
1416
1417
|
console.print(
|
|
@@ -1418,7 +1419,6 @@ class BaseAgent(ABC):
|
|
|
1418
1419
|
f"[bold white on {response_color}] AGENT RESPONSE [/bold white on {response_color}]"
|
|
1419
1420
|
)
|
|
1420
1421
|
)
|
|
1421
|
-
sys.stdout.flush()
|
|
1422
1422
|
did_stream_anything = True
|
|
1423
1423
|
|
|
1424
1424
|
async for event in events:
|
|
@@ -1442,7 +1442,16 @@ class BaseAgent(ABC):
|
|
|
1442
1442
|
# Buffer initial content if present
|
|
1443
1443
|
if part.content and part.content.strip():
|
|
1444
1444
|
text_buffer[event.index].append(part.content)
|
|
1445
|
+
# Count chunks (each part counts as 1)
|
|
1445
1446
|
token_count[event.index] += 1
|
|
1447
|
+
elif isinstance(part, ToolCallPart):
|
|
1448
|
+
streaming_parts.add(event.index)
|
|
1449
|
+
tool_parts.add(event.index)
|
|
1450
|
+
token_count[event.index] = 0 # Initialize token counter
|
|
1451
|
+
# Track tool name for display
|
|
1452
|
+
banner_printed.add(
|
|
1453
|
+
event.index
|
|
1454
|
+
) # Use banner_printed to track if we've shown tool info
|
|
1446
1455
|
|
|
1447
1456
|
# PartDeltaEvent - stream the content as it arrives
|
|
1448
1457
|
elif isinstance(event, PartDeltaEvent):
|
|
@@ -1452,21 +1461,20 @@ class BaseAgent(ABC):
|
|
|
1452
1461
|
if delta.content_delta:
|
|
1453
1462
|
# For text parts, show token counter then render at end
|
|
1454
1463
|
if event.index in text_parts:
|
|
1455
|
-
import sys
|
|
1456
|
-
|
|
1457
1464
|
# Print banner on first content
|
|
1458
1465
|
if event.index not in banner_printed:
|
|
1459
1466
|
_print_response_banner()
|
|
1460
1467
|
banner_printed.add(event.index)
|
|
1461
1468
|
# Accumulate text for final markdown render
|
|
1462
1469
|
text_buffer[event.index].append(delta.content_delta)
|
|
1470
|
+
# Count chunks received
|
|
1463
1471
|
token_count[event.index] += 1
|
|
1464
|
-
# Update
|
|
1472
|
+
# Update chunk counter in place (single line)
|
|
1465
1473
|
count = token_count[event.index]
|
|
1466
|
-
|
|
1467
|
-
f"
|
|
1474
|
+
console.print(
|
|
1475
|
+
f" ⏳ Receiving... {count} chunks ",
|
|
1476
|
+
end="\r",
|
|
1468
1477
|
)
|
|
1469
|
-
sys.stdout.flush()
|
|
1470
1478
|
else:
|
|
1471
1479
|
# For thinking parts, stream immediately (dim)
|
|
1472
1480
|
if event.index not in banner_printed:
|
|
@@ -1474,17 +1482,31 @@ class BaseAgent(ABC):
|
|
|
1474
1482
|
banner_printed.add(event.index)
|
|
1475
1483
|
escaped = escape(delta.content_delta)
|
|
1476
1484
|
console.print(f"[dim]{escaped}[/dim]", end="")
|
|
1485
|
+
elif isinstance(delta, ToolCallPartDelta):
|
|
1486
|
+
# For tool calls, count chunks received
|
|
1487
|
+
token_count[event.index] += 1
|
|
1488
|
+
# Get tool name if available
|
|
1489
|
+
tool_name = getattr(delta, "tool_name_delta", "")
|
|
1490
|
+
count = token_count[event.index]
|
|
1491
|
+
# Display with tool wrench icon and tool name
|
|
1492
|
+
if tool_name:
|
|
1493
|
+
console.print(
|
|
1494
|
+
f" 🔧 Calling {tool_name}... {count} chunks ",
|
|
1495
|
+
end="\r",
|
|
1496
|
+
)
|
|
1497
|
+
else:
|
|
1498
|
+
console.print(
|
|
1499
|
+
f" 🔧 Calling tool... {count} chunks ",
|
|
1500
|
+
end="\r",
|
|
1501
|
+
)
|
|
1477
1502
|
|
|
1478
1503
|
# PartEndEvent - finish the streaming with a newline
|
|
1479
1504
|
elif isinstance(event, PartEndEvent):
|
|
1480
1505
|
if event.index in streaming_parts:
|
|
1481
1506
|
# For text parts, clear counter line and render markdown
|
|
1482
1507
|
if event.index in text_parts:
|
|
1483
|
-
|
|
1484
|
-
|
|
1485
|
-
# Clear the token counter line
|
|
1486
|
-
sys.stdout.write("\r\x1b[K")
|
|
1487
|
-
sys.stdout.flush()
|
|
1508
|
+
# Clear the chunk counter line by printing spaces and returning
|
|
1509
|
+
console.print(" " * 50, end="\r")
|
|
1488
1510
|
# Render the final markdown nicely
|
|
1489
1511
|
if event.index in text_buffer:
|
|
1490
1512
|
try:
|
|
@@ -1494,24 +1516,30 @@ class BaseAgent(ABC):
|
|
|
1494
1516
|
except Exception:
|
|
1495
1517
|
pass
|
|
1496
1518
|
del text_buffer[event.index]
|
|
1497
|
-
|
|
1498
|
-
|
|
1519
|
+
# For tool parts, clear the chunk counter line
|
|
1520
|
+
elif event.index in tool_parts:
|
|
1521
|
+
# Clear the chunk counter line by printing spaces and returning
|
|
1522
|
+
console.print(" " * 50, end="\r")
|
|
1499
1523
|
# For thinking parts, just print newline
|
|
1500
1524
|
elif event.index in banner_printed:
|
|
1501
1525
|
console.print() # Final newline after streaming
|
|
1526
|
+
|
|
1527
|
+
# Clean up token count
|
|
1528
|
+
token_count.pop(event.index, None)
|
|
1502
1529
|
# Clean up all tracking sets
|
|
1503
1530
|
streaming_parts.discard(event.index)
|
|
1504
1531
|
thinking_parts.discard(event.index)
|
|
1505
1532
|
text_parts.discard(event.index)
|
|
1533
|
+
tool_parts.discard(event.index)
|
|
1506
1534
|
banner_printed.discard(event.index)
|
|
1507
1535
|
|
|
1508
|
-
# Resume spinner if next part is NOT text/thinking (avoid race condition)
|
|
1509
|
-
# If next part is
|
|
1536
|
+
# Resume spinner if next part is NOT text/thinking/tool (avoid race condition)
|
|
1537
|
+
# If next part is None or handled differently, it's safe to resume
|
|
1510
1538
|
# Note: spinner itself handles blank line before appearing
|
|
1511
1539
|
from code_puppy.messaging.spinner import resume_all_spinners
|
|
1512
1540
|
|
|
1513
1541
|
next_kind = getattr(event, "next_part_kind", None)
|
|
1514
|
-
if next_kind not in ("text", "thinking"):
|
|
1542
|
+
if next_kind not in ("text", "thinking", "tool-call"):
|
|
1515
1543
|
resume_all_spinners()
|
|
1516
1544
|
|
|
1517
1545
|
# Spinner is resumed in PartEndEvent when appropriate (based on next_part_kind)
|
|
@@ -1910,73 +1938,35 @@ class BaseAgent(ABC):
|
|
|
1910
1938
|
def graceful_sigint_handler(_sig, _frame):
|
|
1911
1939
|
# When using keyboard-based cancel, SIGINT should be a no-op
|
|
1912
1940
|
# (just show a hint to user about the configured cancel key)
|
|
1941
|
+
# Also reset terminal to prevent bricking on Windows+uvx
|
|
1913
1942
|
from code_puppy.keymap import get_cancel_agent_display_name
|
|
1914
|
-
import
|
|
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()
|
|
1915
1947
|
|
|
1916
1948
|
cancel_key = get_cancel_agent_display_name()
|
|
1917
|
-
|
|
1918
|
-
# On Windows, we use keyboard listener, so SIGINT might still fire
|
|
1919
|
-
# but we handle cancellation via the key listener
|
|
1920
|
-
pass # Silent on Windows - the key listener handles it
|
|
1921
|
-
else:
|
|
1922
|
-
emit_info(f"Use {cancel_key} to cancel the agent task.")
|
|
1949
|
+
emit_info(f"Use {cancel_key} to cancel the agent task.")
|
|
1923
1950
|
|
|
1924
1951
|
original_handler = None
|
|
1925
1952
|
key_listener_stop_event = None
|
|
1926
1953
|
_key_listener_thread = None
|
|
1927
|
-
_windows_ctrl_handler = None # Store reference to prevent garbage collection
|
|
1928
1954
|
|
|
1929
1955
|
try:
|
|
1930
|
-
if
|
|
1931
|
-
#
|
|
1932
|
-
import ctypes
|
|
1933
|
-
|
|
1934
|
-
# Define the handler function type
|
|
1935
|
-
HANDLER_ROUTINE = ctypes.WINFUNCTYPE(ctypes.c_bool, ctypes.c_ulong)
|
|
1936
|
-
|
|
1937
|
-
def windows_ctrl_handler(ctrl_type):
|
|
1938
|
-
"""Handle Windows console control events."""
|
|
1939
|
-
CTRL_C_EVENT = 0
|
|
1940
|
-
CTRL_BREAK_EVENT = 1
|
|
1941
|
-
|
|
1942
|
-
if ctrl_type in (CTRL_C_EVENT, CTRL_BREAK_EVENT):
|
|
1943
|
-
# Check if we're awaiting user input
|
|
1944
|
-
if is_awaiting_user_input():
|
|
1945
|
-
return False # Let default handler run
|
|
1946
|
-
|
|
1947
|
-
# Schedule agent cancellation
|
|
1948
|
-
schedule_agent_cancel()
|
|
1949
|
-
return True # We handled it, don't terminate
|
|
1950
|
-
|
|
1951
|
-
return False # Let other handlers process it
|
|
1952
|
-
|
|
1953
|
-
# Create the callback - must keep reference alive!
|
|
1954
|
-
_windows_ctrl_handler = HANDLER_ROUTINE(windows_ctrl_handler)
|
|
1955
|
-
|
|
1956
|
-
# Register the handler
|
|
1957
|
-
kernel32 = ctypes.windll.kernel32
|
|
1958
|
-
if not kernel32.SetConsoleCtrlHandler(_windows_ctrl_handler, True):
|
|
1959
|
-
emit_warning("Failed to set Windows Ctrl+C handler")
|
|
1960
|
-
|
|
1961
|
-
# Also spawn keyboard listener for Ctrl+X (shell cancel) and other keys
|
|
1962
|
-
key_listener_stop_event = threading.Event()
|
|
1963
|
-
_key_listener_thread = self._spawn_ctrl_x_key_listener(
|
|
1964
|
-
key_listener_stop_event,
|
|
1965
|
-
on_escape=lambda: None, # Ctrl+X handled by command_runner
|
|
1966
|
-
on_cancel_agent=None, # Ctrl+C handled by SetConsoleCtrlHandler above
|
|
1967
|
-
)
|
|
1968
|
-
elif cancel_agent_uses_signal():
|
|
1969
|
-
# Unix with Ctrl+C: Use SIGINT-based cancellation
|
|
1956
|
+
if cancel_agent_uses_signal():
|
|
1957
|
+
# Use SIGINT-based cancellation (default Ctrl+C behavior)
|
|
1970
1958
|
original_handler = signal.signal(
|
|
1971
1959
|
signal.SIGINT, keyboard_interrupt_handler
|
|
1972
1960
|
)
|
|
1973
1961
|
else:
|
|
1974
|
-
#
|
|
1962
|
+
# Use keyboard listener for agent cancellation
|
|
1963
|
+
# Set a graceful SIGINT handler that shows a hint
|
|
1975
1964
|
original_handler = signal.signal(signal.SIGINT, graceful_sigint_handler)
|
|
1965
|
+
# Spawn keyboard listener with the cancel agent callback
|
|
1976
1966
|
key_listener_stop_event = threading.Event()
|
|
1977
1967
|
_key_listener_thread = self._spawn_ctrl_x_key_listener(
|
|
1978
1968
|
key_listener_stop_event,
|
|
1979
|
-
on_escape=lambda: None,
|
|
1969
|
+
on_escape=lambda: None, # Ctrl+X handled by command_runner
|
|
1980
1970
|
on_cancel_agent=schedule_agent_cancel,
|
|
1981
1971
|
)
|
|
1982
1972
|
|
|
@@ -2001,17 +1991,8 @@ class BaseAgent(ABC):
|
|
|
2001
1991
|
# Stop keyboard listener if it was started
|
|
2002
1992
|
if key_listener_stop_event is not None:
|
|
2003
1993
|
key_listener_stop_event.set()
|
|
2004
|
-
|
|
2005
|
-
|
|
2006
|
-
|
|
2007
|
-
|
|
2008
|
-
import ctypes
|
|
2009
|
-
|
|
2010
|
-
kernel32 = ctypes.windll.kernel32
|
|
2011
|
-
kernel32.SetConsoleCtrlHandler(_windows_ctrl_handler, False)
|
|
2012
|
-
except Exception:
|
|
2013
|
-
pass # Best effort cleanup
|
|
2014
|
-
|
|
2015
|
-
# Restore original signal handler (Unix)
|
|
2016
|
-
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!
|
|
2017
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()
|
|
@@ -571,6 +571,7 @@ class AddModelMenu:
|
|
|
571
571
|
"cerebras": "cerebras",
|
|
572
572
|
"cohere": "custom_openai",
|
|
573
573
|
"perplexity": "custom_openai",
|
|
574
|
+
"minimax": "custom_anthropic",
|
|
574
575
|
}
|
|
575
576
|
|
|
576
577
|
# Determine the model type
|
|
@@ -600,6 +601,16 @@ class AddModelMenu:
|
|
|
600
601
|
api_key_env = f"${provider.env[0]}" if provider.env else "$API_KEY"
|
|
601
602
|
config["custom_endpoint"] = {"url": api_url, "api_key": api_key_env}
|
|
602
603
|
|
|
604
|
+
# Special handling for minimax: uses custom_anthropic but needs custom_endpoint
|
|
605
|
+
# and the URL needs /v1 stripped (comes as https://api.minimax.io/anthropic/v1)
|
|
606
|
+
if provider.id == "minimax" and provider.api:
|
|
607
|
+
api_url = provider.api
|
|
608
|
+
# Strip /v1 suffix if present
|
|
609
|
+
if api_url.endswith("/v1"):
|
|
610
|
+
api_url = api_url[:-3]
|
|
611
|
+
api_key_env = f"${provider.env[0]}" if provider.env else "$API_KEY"
|
|
612
|
+
config["custom_endpoint"] = {"url": api_url, "api_key": api_key_env}
|
|
613
|
+
|
|
603
614
|
# Add context length if available
|
|
604
615
|
if model.context_length and model.context_length > 0:
|
|
605
616
|
config["context_length"] = model.context_length
|
|
@@ -984,6 +995,10 @@ class AddModelMenu:
|
|
|
984
995
|
# Reset awaiting input flag
|
|
985
996
|
set_awaiting_user_input(False)
|
|
986
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
|
+
|
|
987
1002
|
# Handle unsupported provider
|
|
988
1003
|
if self.result == "unsupported" and self.current_provider:
|
|
989
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:
|