code-puppy 0.0.332__py3-none-any.whl → 0.0.333__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 +65 -6
- code_puppy/keymap.py +8 -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.333.dist-info}/METADATA +1 -1
- {code_puppy-0.0.332.dist-info → code_puppy-0.0.333.dist-info}/RECORD +13 -12
- {code_puppy-0.0.332.data → code_puppy-0.0.333.data}/data/code_puppy/models.json +0 -0
- {code_puppy-0.0.332.data → code_puppy-0.0.333.data}/data/code_puppy/models_dev_api.json +0 -0
- {code_puppy-0.0.332.dist-info → code_puppy-0.0.333.dist-info}/WHEEL +0 -0
- {code_puppy-0.0.332.dist-info → code_puppy-0.0.333.dist-info}/entry_points.txt +0 -0
- {code_puppy-0.0.332.dist-info → code_puppy-0.0.333.dist-info}/licenses/LICENSE +0 -0
code_puppy/agents/base_agent.py
CHANGED
|
@@ -75,8 +75,6 @@ from code_puppy.model_factory import ModelFactory, make_model_settings
|
|
|
75
75
|
from code_puppy.summarization_agent import run_summarization_sync
|
|
76
76
|
from code_puppy.tools.agent_tools import _active_subagent_tasks
|
|
77
77
|
from code_puppy.tools.command_runner import (
|
|
78
|
-
_add_windows_ctrl_handler,
|
|
79
|
-
_remove_windows_ctrl_handler,
|
|
80
78
|
is_awaiting_user_input,
|
|
81
79
|
)
|
|
82
80
|
|
|
@@ -1384,15 +1382,14 @@ class BaseAgent(ABC):
|
|
|
1384
1382
|
def _print_thinking_banner() -> None:
|
|
1385
1383
|
"""Print the THINKING banner with spinner pause and line clear."""
|
|
1386
1384
|
nonlocal did_stream_anything
|
|
1387
|
-
import sys
|
|
1388
1385
|
import time
|
|
1389
1386
|
|
|
1390
1387
|
from code_puppy.config import get_banner_color
|
|
1391
1388
|
|
|
1392
1389
|
pause_all_spinners()
|
|
1393
1390
|
time.sleep(0.1) # Delay to let spinner fully clear
|
|
1394
|
-
|
|
1395
|
-
|
|
1391
|
+
# Clear line and print newline before banner
|
|
1392
|
+
console.print(" " * 50, end="\r")
|
|
1396
1393
|
console.print() # Newline before banner
|
|
1397
1394
|
# Bold banner with configurable color and lightning bolt
|
|
1398
1395
|
thinking_color = get_banner_color("thinking")
|
|
@@ -1402,21 +1399,19 @@ class BaseAgent(ABC):
|
|
|
1402
1399
|
),
|
|
1403
1400
|
end="",
|
|
1404
1401
|
)
|
|
1405
|
-
sys.stdout.flush()
|
|
1406
1402
|
did_stream_anything = True
|
|
1407
1403
|
|
|
1408
1404
|
def _print_response_banner() -> None:
|
|
1409
1405
|
"""Print the AGENT RESPONSE banner with spinner pause and line clear."""
|
|
1410
1406
|
nonlocal did_stream_anything
|
|
1411
|
-
import sys
|
|
1412
1407
|
import time
|
|
1413
1408
|
|
|
1414
1409
|
from code_puppy.config import get_banner_color
|
|
1415
1410
|
|
|
1416
1411
|
pause_all_spinners()
|
|
1417
1412
|
time.sleep(0.1) # Delay to let spinner fully clear
|
|
1418
|
-
|
|
1419
|
-
|
|
1413
|
+
# Clear line and print newline before banner
|
|
1414
|
+
console.print(" " * 50, end="\r")
|
|
1420
1415
|
console.print() # Newline before banner
|
|
1421
1416
|
response_color = get_banner_color("agent_response")
|
|
1422
1417
|
console.print(
|
|
@@ -1424,7 +1419,6 @@ class BaseAgent(ABC):
|
|
|
1424
1419
|
f"[bold white on {response_color}] AGENT RESPONSE [/bold white on {response_color}]"
|
|
1425
1420
|
)
|
|
1426
1421
|
)
|
|
1427
|
-
sys.stdout.flush()
|
|
1428
1422
|
did_stream_anything = True
|
|
1429
1423
|
|
|
1430
1424
|
async for event in events:
|
|
@@ -1448,8 +1442,8 @@ class BaseAgent(ABC):
|
|
|
1448
1442
|
# Buffer initial content if present
|
|
1449
1443
|
if part.content and part.content.strip():
|
|
1450
1444
|
text_buffer[event.index].append(part.content)
|
|
1451
|
-
#
|
|
1452
|
-
token_count[event.index] +=
|
|
1445
|
+
# Count chunks (each part counts as 1)
|
|
1446
|
+
token_count[event.index] += 1
|
|
1453
1447
|
elif isinstance(part, ToolCallPart):
|
|
1454
1448
|
streaming_parts.add(event.index)
|
|
1455
1449
|
tool_parts.add(event.index)
|
|
@@ -1467,24 +1461,20 @@ class BaseAgent(ABC):
|
|
|
1467
1461
|
if delta.content_delta:
|
|
1468
1462
|
# For text parts, show token counter then render at end
|
|
1469
1463
|
if event.index in text_parts:
|
|
1470
|
-
import sys
|
|
1471
|
-
|
|
1472
1464
|
# Print banner on first content
|
|
1473
1465
|
if event.index not in banner_printed:
|
|
1474
1466
|
_print_response_banner()
|
|
1475
1467
|
banner_printed.add(event.index)
|
|
1476
1468
|
# Accumulate text for final markdown render
|
|
1477
1469
|
text_buffer[event.index].append(delta.content_delta)
|
|
1478
|
-
#
|
|
1479
|
-
token_count[event.index] +=
|
|
1480
|
-
|
|
1481
|
-
)
|
|
1482
|
-
# 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)
|
|
1483
1473
|
count = token_count[event.index]
|
|
1484
|
-
|
|
1485
|
-
f"
|
|
1474
|
+
console.print(
|
|
1475
|
+
f" ⏳ Receiving... {count} chunks ",
|
|
1476
|
+
end="\r",
|
|
1486
1477
|
)
|
|
1487
|
-
sys.stdout.flush()
|
|
1488
1478
|
else:
|
|
1489
1479
|
# For thinking parts, stream immediately (dim)
|
|
1490
1480
|
if event.index not in banner_printed:
|
|
@@ -1493,34 +1483,30 @@ class BaseAgent(ABC):
|
|
|
1493
1483
|
escaped = escape(delta.content_delta)
|
|
1494
1484
|
console.print(f"[dim]{escaped}[/dim]", end="")
|
|
1495
1485
|
elif isinstance(delta, ToolCallPartDelta):
|
|
1496
|
-
|
|
1497
|
-
|
|
1498
|
-
# For tool calls, show token counter (use string repr for estimation)
|
|
1499
|
-
token_count[event.index] += len(str(delta)) // 3
|
|
1486
|
+
# For tool calls, count chunks received
|
|
1487
|
+
token_count[event.index] += 1
|
|
1500
1488
|
# Get tool name if available
|
|
1501
1489
|
tool_name = getattr(delta, "tool_name_delta", "")
|
|
1502
1490
|
count = token_count[event.index]
|
|
1503
1491
|
# Display with tool wrench icon and tool name
|
|
1504
1492
|
if tool_name:
|
|
1505
|
-
|
|
1506
|
-
f"
|
|
1493
|
+
console.print(
|
|
1494
|
+
f" 🔧 Calling {tool_name}... {count} chunks ",
|
|
1495
|
+
end="\r",
|
|
1507
1496
|
)
|
|
1508
1497
|
else:
|
|
1509
|
-
|
|
1510
|
-
f"
|
|
1498
|
+
console.print(
|
|
1499
|
+
f" 🔧 Calling tool... {count} chunks ",
|
|
1500
|
+
end="\r",
|
|
1511
1501
|
)
|
|
1512
|
-
sys.stdout.flush()
|
|
1513
1502
|
|
|
1514
1503
|
# PartEndEvent - finish the streaming with a newline
|
|
1515
1504
|
elif isinstance(event, PartEndEvent):
|
|
1516
1505
|
if event.index in streaming_parts:
|
|
1517
|
-
import sys
|
|
1518
|
-
|
|
1519
1506
|
# For text parts, clear counter line and render markdown
|
|
1520
1507
|
if event.index in text_parts:
|
|
1521
|
-
# Clear the
|
|
1522
|
-
|
|
1523
|
-
sys.stdout.flush()
|
|
1508
|
+
# Clear the chunk counter line by printing spaces and returning
|
|
1509
|
+
console.print(" " * 50, end="\r")
|
|
1524
1510
|
# Render the final markdown nicely
|
|
1525
1511
|
if event.index in text_buffer:
|
|
1526
1512
|
try:
|
|
@@ -1530,11 +1516,10 @@ class BaseAgent(ABC):
|
|
|
1530
1516
|
except Exception:
|
|
1531
1517
|
pass
|
|
1532
1518
|
del text_buffer[event.index]
|
|
1533
|
-
# For tool parts, clear the
|
|
1519
|
+
# For tool parts, clear the chunk counter line
|
|
1534
1520
|
elif event.index in tool_parts:
|
|
1535
|
-
# Clear the
|
|
1536
|
-
|
|
1537
|
-
sys.stdout.flush()
|
|
1521
|
+
# Clear the chunk counter line by printing spaces and returning
|
|
1522
|
+
console.print(" " * 50, end="\r")
|
|
1538
1523
|
# For thinking parts, just print newline
|
|
1539
1524
|
elif event.index in banner_printed:
|
|
1540
1525
|
console.print() # Final newline after streaming
|
|
@@ -1953,7 +1938,12 @@ class BaseAgent(ABC):
|
|
|
1953
1938
|
def graceful_sigint_handler(_sig, _frame):
|
|
1954
1939
|
# When using keyboard-based cancel, SIGINT should be a no-op
|
|
1955
1940
|
# (just show a hint to user about the configured cancel key)
|
|
1941
|
+
# Also reset terminal to prevent bricking on Windows+uvx
|
|
1956
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()
|
|
1957
1947
|
|
|
1958
1948
|
cancel_key = get_cancel_agent_display_name()
|
|
1959
1949
|
emit_info(f"Use {cancel_key} to cancel the agent task.")
|
|
@@ -1961,12 +1951,6 @@ class BaseAgent(ABC):
|
|
|
1961
1951
|
original_handler = None
|
|
1962
1952
|
key_listener_stop_event = None
|
|
1963
1953
|
_key_listener_thread = None
|
|
1964
|
-
windows_ctrl_handler = None
|
|
1965
|
-
|
|
1966
|
-
# Check if we're on Windows
|
|
1967
|
-
import sys
|
|
1968
|
-
|
|
1969
|
-
is_windows = sys.platform.startswith("win")
|
|
1970
1954
|
|
|
1971
1955
|
try:
|
|
1972
1956
|
if cancel_agent_uses_signal():
|
|
@@ -1974,27 +1958,10 @@ class BaseAgent(ABC):
|
|
|
1974
1958
|
original_handler = signal.signal(
|
|
1975
1959
|
signal.SIGINT, keyboard_interrupt_handler
|
|
1976
1960
|
)
|
|
1977
|
-
# On Windows, also use SetConsoleCtrlHandler for reliable Ctrl+C with uvx
|
|
1978
|
-
if is_windows:
|
|
1979
|
-
windows_ctrl_handler = _add_windows_ctrl_handler(
|
|
1980
|
-
schedule_agent_cancel
|
|
1981
|
-
)
|
|
1982
1961
|
else:
|
|
1983
1962
|
# Use keyboard listener for agent cancellation
|
|
1984
1963
|
# Set a graceful SIGINT handler that shows a hint
|
|
1985
1964
|
original_handler = signal.signal(signal.SIGINT, graceful_sigint_handler)
|
|
1986
|
-
# On Windows, SetConsoleCtrlHandler should also show the hint
|
|
1987
|
-
if is_windows:
|
|
1988
|
-
|
|
1989
|
-
def graceful_ctrl_handler():
|
|
1990
|
-
from code_puppy.keymap import get_cancel_agent_display_name
|
|
1991
|
-
|
|
1992
|
-
cancel_key = get_cancel_agent_display_name()
|
|
1993
|
-
emit_info(f"Use {cancel_key} to cancel the agent task.")
|
|
1994
|
-
|
|
1995
|
-
windows_ctrl_handler = _add_windows_ctrl_handler(
|
|
1996
|
-
graceful_ctrl_handler
|
|
1997
|
-
)
|
|
1998
1965
|
# Spawn keyboard listener with the cancel agent callback
|
|
1999
1966
|
key_listener_stop_event = threading.Event()
|
|
2000
1967
|
_key_listener_thread = self._spawn_ctrl_x_key_listener(
|
|
@@ -2024,17 +1991,8 @@ class BaseAgent(ABC):
|
|
|
2024
1991
|
# Stop keyboard listener if it was started
|
|
2025
1992
|
if key_listener_stop_event is not None:
|
|
2026
1993
|
key_listener_stop_event.set()
|
|
2027
|
-
# Remove Windows console handler
|
|
2028
|
-
if windows_ctrl_handler is not None:
|
|
2029
|
-
_remove_windows_ctrl_handler(windows_ctrl_handler)
|
|
2030
1994
|
# Restore original signal handler
|
|
2031
1995
|
if (
|
|
2032
1996
|
original_handler is not None
|
|
2033
1997
|
): # Explicit None check - SIG_DFL can be 0/falsy!
|
|
2034
1998
|
signal.signal(signal.SIGINT, original_handler)
|
|
2035
|
-
# Windows-specific: Reset terminal after Ctrl+C to prevent corruption
|
|
2036
|
-
# This fixes the issue where Enter key shows as 'm' after interrupting with uvx
|
|
2037
|
-
if is_windows:
|
|
2038
|
-
from code_puppy.terminal_utils import reset_windows_terminal_full
|
|
2039
|
-
|
|
2040
|
-
reset_windows_terminal_full()
|
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
|
|
|
@@ -421,10 +460,6 @@ async def interactive_mode(message_renderer, initial_command: str = None) -> Non
|
|
|
421
460
|
current_agent_task = None
|
|
422
461
|
|
|
423
462
|
while True:
|
|
424
|
-
# Windows-specific: Aggressively reset terminal at the start of every loop
|
|
425
|
-
# This fixes terminal corruption after Ctrl+C, especially when running via uvx
|
|
426
|
-
reset_windows_terminal_full()
|
|
427
|
-
|
|
428
463
|
from code_puppy.agents.agent_manager import get_current_agent
|
|
429
464
|
from code_puppy.messaging import emit_info
|
|
430
465
|
|
|
@@ -444,6 +479,15 @@ async def interactive_mode(message_renderer, initial_command: str = None) -> Non
|
|
|
444
479
|
task = await get_input_with_combined_completion(
|
|
445
480
|
get_prompt_with_active_model(), history_file=COMMAND_HISTORY_FILE
|
|
446
481
|
)
|
|
482
|
+
|
|
483
|
+
# Windows+uvx: Re-disable Ctrl+C after prompt_toolkit
|
|
484
|
+
# (prompt_toolkit restores console mode which re-enables Ctrl+C)
|
|
485
|
+
try:
|
|
486
|
+
from code_puppy.terminal_utils import ensure_ctrl_c_disabled
|
|
487
|
+
|
|
488
|
+
ensure_ctrl_c_disabled()
|
|
489
|
+
except ImportError:
|
|
490
|
+
pass
|
|
447
491
|
except ImportError:
|
|
448
492
|
# Fall back to basic input if prompt_toolkit is not available
|
|
449
493
|
task = input(">>> ")
|
|
@@ -608,8 +652,14 @@ async def interactive_mode(message_renderer, initial_command: str = None) -> Non
|
|
|
608
652
|
# Check if the task was cancelled (but don't show message if we just killed processes)
|
|
609
653
|
if result is None:
|
|
610
654
|
# Windows-specific: Reset terminal state after cancellation
|
|
611
|
-
|
|
612
|
-
|
|
655
|
+
reset_windows_terminal_ansi()
|
|
656
|
+
# Re-disable Ctrl+C if needed (uvx mode)
|
|
657
|
+
try:
|
|
658
|
+
from code_puppy.terminal_utils import ensure_ctrl_c_disabled
|
|
659
|
+
|
|
660
|
+
ensure_ctrl_c_disabled()
|
|
661
|
+
except ImportError:
|
|
662
|
+
pass
|
|
613
663
|
continue
|
|
614
664
|
# Get the structured response
|
|
615
665
|
agent_response = result.output
|
|
@@ -650,6 +700,15 @@ async def interactive_mode(message_renderer, initial_command: str = None) -> Non
|
|
|
650
700
|
|
|
651
701
|
auto_save_session_if_enabled()
|
|
652
702
|
|
|
703
|
+
# Re-disable Ctrl+C if needed (uvx mode) - must be done after
|
|
704
|
+
# each iteration as various operations may restore console mode
|
|
705
|
+
try:
|
|
706
|
+
from code_puppy.terminal_utils import ensure_ctrl_c_disabled
|
|
707
|
+
|
|
708
|
+
ensure_ctrl_c_disabled()
|
|
709
|
+
except ImportError:
|
|
710
|
+
pass
|
|
711
|
+
|
|
653
712
|
|
|
654
713
|
def prettier_code_blocks():
|
|
655
714
|
"""Configure Rich to use prettier code block rendering."""
|
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() == "":
|
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
|
|
@@ -44,70 +44,9 @@ def _truncate_line(line: str) -> str:
|
|
|
44
44
|
if sys.platform.startswith("win"):
|
|
45
45
|
import msvcrt
|
|
46
46
|
|
|
47
|
-
# Load kernel32 for PeekNamedPipe
|
|
47
|
+
# Load kernel32 for PeekNamedPipe
|
|
48
48
|
_kernel32 = ctypes.windll.kernel32
|
|
49
49
|
|
|
50
|
-
# SetConsoleCtrlHandler types for Ctrl+C handling on Windows
|
|
51
|
-
# This is more reliable than signal.SIGINT when running under uvx
|
|
52
|
-
_CTRL_C_EVENT = 0
|
|
53
|
-
_CTRL_BREAK_EVENT = 1
|
|
54
|
-
_HANDLER_ROUTINE = ctypes.WINFUNCTYPE(ctypes.c_int, ctypes.c_ulong)
|
|
55
|
-
|
|
56
|
-
# Track registered handlers to prevent garbage collection
|
|
57
|
-
_registered_console_handlers: list = []
|
|
58
|
-
|
|
59
|
-
def _add_windows_ctrl_handler(callback: Callable[[], None]) -> Optional[Callable]:
|
|
60
|
-
"""Register a Windows console control handler for Ctrl+C/Ctrl+Break.
|
|
61
|
-
|
|
62
|
-
Args:
|
|
63
|
-
callback: Function to call when Ctrl+C or Ctrl+Break is pressed.
|
|
64
|
-
Should take no arguments.
|
|
65
|
-
|
|
66
|
-
Returns:
|
|
67
|
-
The wrapped handler function (needed for removal), or None if failed.
|
|
68
|
-
"""
|
|
69
|
-
|
|
70
|
-
def handler(ctrl_type: int) -> int:
|
|
71
|
-
if ctrl_type in (_CTRL_C_EVENT, _CTRL_BREAK_EVENT):
|
|
72
|
-
try:
|
|
73
|
-
callback()
|
|
74
|
-
except Exception:
|
|
75
|
-
pass
|
|
76
|
-
return 1 # TRUE = we handled it, don't pass to next handler
|
|
77
|
-
return 0 # FALSE = let next handler deal with it
|
|
78
|
-
|
|
79
|
-
# Wrap in WINFUNCTYPE to make it callable from C
|
|
80
|
-
wrapped = _HANDLER_ROUTINE(handler)
|
|
81
|
-
# Keep reference to prevent GC
|
|
82
|
-
_registered_console_handlers.append(wrapped)
|
|
83
|
-
|
|
84
|
-
try:
|
|
85
|
-
if _kernel32.SetConsoleCtrlHandler(wrapped, True):
|
|
86
|
-
return wrapped
|
|
87
|
-
except Exception:
|
|
88
|
-
pass
|
|
89
|
-
return None
|
|
90
|
-
|
|
91
|
-
def _remove_windows_ctrl_handler(handler: Callable) -> bool:
|
|
92
|
-
"""Remove a previously registered Windows console control handler.
|
|
93
|
-
|
|
94
|
-
Args:
|
|
95
|
-
handler: The handler returned by _add_windows_ctrl_handler.
|
|
96
|
-
|
|
97
|
-
Returns:
|
|
98
|
-
True if successfully removed, False otherwise.
|
|
99
|
-
"""
|
|
100
|
-
if handler is None:
|
|
101
|
-
return False
|
|
102
|
-
try:
|
|
103
|
-
result = _kernel32.SetConsoleCtrlHandler(handler, False)
|
|
104
|
-
# Clean up our reference
|
|
105
|
-
if handler in _registered_console_handlers:
|
|
106
|
-
_registered_console_handlers.remove(handler)
|
|
107
|
-
return bool(result)
|
|
108
|
-
except Exception:
|
|
109
|
-
return False
|
|
110
|
-
|
|
111
50
|
def _win32_pipe_has_data(pipe) -> bool:
|
|
112
51
|
"""Check if a Windows pipe has data available without blocking.
|
|
113
52
|
|
|
@@ -148,15 +87,8 @@ if sys.platform.startswith("win"):
|
|
|
148
87
|
except (ValueError, OSError, ctypes.ArgumentError):
|
|
149
88
|
# Handle closed, invalid, or other errors
|
|
150
89
|
return False
|
|
151
|
-
|
|
152
90
|
else:
|
|
153
|
-
#
|
|
154
|
-
def _add_windows_ctrl_handler(callback: Callable[[], None]) -> Optional[Callable]:
|
|
155
|
-
return None
|
|
156
|
-
|
|
157
|
-
def _remove_windows_ctrl_handler(handler: Callable) -> bool:
|
|
158
|
-
return False
|
|
159
|
-
|
|
91
|
+
# POSIX stub - not used, but keeps the code clean
|
|
160
92
|
def _win32_pipe_has_data(pipe) -> bool:
|
|
161
93
|
return False
|
|
162
94
|
|
|
@@ -174,7 +106,6 @@ _USER_KILLED_PROCESSES = set()
|
|
|
174
106
|
_SHELL_CTRL_X_STOP_EVENT: Optional[threading.Event] = None
|
|
175
107
|
_SHELL_CTRL_X_THREAD: Optional[threading.Thread] = None
|
|
176
108
|
_ORIGINAL_SIGINT_HANDLER = None
|
|
177
|
-
_WINDOWS_CTRL_HANDLER = None # For SetConsoleCtrlHandler on Windows
|
|
178
109
|
|
|
179
110
|
# Stop event to signal reader threads to terminate
|
|
180
111
|
_READER_STOP_EVENT: Optional[threading.Event] = None
|
|
@@ -504,10 +435,8 @@ def _shell_command_keyboard_context():
|
|
|
504
435
|
1. Disables the agent's Ctrl-C handler (so it doesn't cancel the agent)
|
|
505
436
|
2. Enables a Ctrl-X listener to kill the running shell process
|
|
506
437
|
3. Restores the original Ctrl-C handler when done
|
|
507
|
-
4. On Windows, uses SetConsoleCtrlHandler for reliable Ctrl+C with uvx
|
|
508
438
|
"""
|
|
509
439
|
global _SHELL_CTRL_X_STOP_EVENT, _SHELL_CTRL_X_THREAD, _ORIGINAL_SIGINT_HANDLER
|
|
510
|
-
global _WINDOWS_CTRL_HANDLER
|
|
511
440
|
|
|
512
441
|
# Handler for Ctrl-X: kill all running shell processes
|
|
513
442
|
def handle_ctrl_x_press() -> None:
|
|
@@ -515,15 +444,11 @@ def _shell_command_keyboard_context():
|
|
|
515
444
|
kill_all_running_shell_processes()
|
|
516
445
|
|
|
517
446
|
# Handler for Ctrl-C during shell execution: just kill the shell process, don't cancel agent
|
|
518
|
-
def
|
|
447
|
+
def shell_sigint_handler(_sig, _frame):
|
|
519
448
|
"""During shell execution, Ctrl-C kills the shell but doesn't cancel the agent."""
|
|
520
449
|
emit_warning("\n🛑 Ctrl-C detected! Interrupting shell command...")
|
|
521
450
|
kill_all_running_shell_processes()
|
|
522
451
|
|
|
523
|
-
def shell_sigint_handler(_sig, _frame):
|
|
524
|
-
"""Signal handler wrapper for SIGINT."""
|
|
525
|
-
shell_ctrl_c_callback()
|
|
526
|
-
|
|
527
452
|
# Set up Ctrl-X listener
|
|
528
453
|
_SHELL_CTRL_X_STOP_EVENT = threading.Event()
|
|
529
454
|
_SHELL_CTRL_X_THREAD = _spawn_ctrl_x_key_listener(
|
|
@@ -531,12 +456,7 @@ def _shell_command_keyboard_context():
|
|
|
531
456
|
handle_ctrl_x_press,
|
|
532
457
|
)
|
|
533
458
|
|
|
534
|
-
#
|
|
535
|
-
# This works even when running under uvx where SIGINT doesn't propagate properly
|
|
536
|
-
if sys.platform.startswith("win"):
|
|
537
|
-
_WINDOWS_CTRL_HANDLER = _add_windows_ctrl_handler(shell_ctrl_c_callback)
|
|
538
|
-
|
|
539
|
-
# Also set SIGINT handler (works on POSIX, may work on some Windows setups)
|
|
459
|
+
# Replace SIGINT handler temporarily
|
|
540
460
|
try:
|
|
541
461
|
_ORIGINAL_SIGINT_HANDLER = signal.signal(signal.SIGINT, shell_sigint_handler)
|
|
542
462
|
except (ValueError, OSError):
|
|
@@ -556,10 +476,6 @@ def _shell_command_keyboard_context():
|
|
|
556
476
|
except Exception:
|
|
557
477
|
pass
|
|
558
478
|
|
|
559
|
-
# Remove Windows console handler
|
|
560
|
-
if _WINDOWS_CTRL_HANDLER is not None:
|
|
561
|
-
_remove_windows_ctrl_handler(_WINDOWS_CTRL_HANDLER)
|
|
562
|
-
|
|
563
479
|
# Restore original SIGINT handler
|
|
564
480
|
if _ORIGINAL_SIGINT_HANDLER is not None:
|
|
565
481
|
try:
|
|
@@ -571,7 +487,6 @@ def _shell_command_keyboard_context():
|
|
|
571
487
|
_SHELL_CTRL_X_STOP_EVENT = None
|
|
572
488
|
_SHELL_CTRL_X_THREAD = None
|
|
573
489
|
_ORIGINAL_SIGINT_HANDLER = None
|
|
574
|
-
_WINDOWS_CTRL_HANDLER = None
|
|
575
490
|
|
|
576
491
|
|
|
577
492
|
def run_shell_command_streaming(
|
|
@@ -0,0 +1,242 @@
|
|
|
1
|
+
"""Detect if code-puppy was launched via uvx on Windows.
|
|
2
|
+
|
|
3
|
+
This module provides utilities to detect the launch method of code-puppy,
|
|
4
|
+
specifically to handle signal differences when running via uvx on Windows.
|
|
5
|
+
|
|
6
|
+
On Windows, when launched via `uvx code-puppy`, Ctrl+C (SIGINT) gets captured
|
|
7
|
+
by uvx's process handling before reaching our Python process. To work around
|
|
8
|
+
this, we detect the uvx launch scenario and switch to Ctrl+K for cancellation.
|
|
9
|
+
|
|
10
|
+
Note: This issue is specific to uvx.exe, NOT uv.exe. Running via `uv run`
|
|
11
|
+
handles SIGINT correctly on Windows.
|
|
12
|
+
|
|
13
|
+
On non-Windows platforms, this is not an issue - Ctrl+C works fine with uvx.
|
|
14
|
+
"""
|
|
15
|
+
|
|
16
|
+
import os
|
|
17
|
+
import platform
|
|
18
|
+
import sys
|
|
19
|
+
from functools import lru_cache
|
|
20
|
+
from typing import Optional
|
|
21
|
+
|
|
22
|
+
# Cache the detection result - it won't change during runtime
|
|
23
|
+
_uvx_detection_cache: Optional[bool] = None
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def _get_parent_process_name_psutil(pid: int) -> Optional[str]:
|
|
27
|
+
"""Get parent process name using psutil (if available).
|
|
28
|
+
|
|
29
|
+
Args:
|
|
30
|
+
pid: Process ID to get parent name for
|
|
31
|
+
|
|
32
|
+
Returns:
|
|
33
|
+
Parent process name (lowercase) or None if not found
|
|
34
|
+
"""
|
|
35
|
+
try:
|
|
36
|
+
import psutil
|
|
37
|
+
|
|
38
|
+
proc = psutil.Process(pid)
|
|
39
|
+
parent = proc.parent()
|
|
40
|
+
if parent:
|
|
41
|
+
return parent.name().lower()
|
|
42
|
+
except Exception:
|
|
43
|
+
pass
|
|
44
|
+
return None
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def _get_parent_process_chain_psutil() -> list[str]:
|
|
48
|
+
"""Get the entire parent process chain using psutil.
|
|
49
|
+
|
|
50
|
+
Returns:
|
|
51
|
+
List of process names from current process up to init/System
|
|
52
|
+
"""
|
|
53
|
+
chain = []
|
|
54
|
+
try:
|
|
55
|
+
import psutil
|
|
56
|
+
|
|
57
|
+
proc = psutil.Process(os.getpid())
|
|
58
|
+
while proc:
|
|
59
|
+
chain.append(proc.name().lower())
|
|
60
|
+
parent = proc.parent()
|
|
61
|
+
if parent is None or parent.pid in (0, proc.pid):
|
|
62
|
+
break
|
|
63
|
+
proc = parent
|
|
64
|
+
except Exception:
|
|
65
|
+
pass
|
|
66
|
+
return chain
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
def _get_parent_process_chain_windows_ctypes() -> list[str]:
|
|
70
|
+
"""Get parent process chain on Windows using ctypes (no external deps).
|
|
71
|
+
|
|
72
|
+
This is a fallback when psutil is not available.
|
|
73
|
+
|
|
74
|
+
Returns:
|
|
75
|
+
List of process names from current process up to System
|
|
76
|
+
"""
|
|
77
|
+
if platform.system() != "Windows":
|
|
78
|
+
return []
|
|
79
|
+
|
|
80
|
+
chain = []
|
|
81
|
+
try:
|
|
82
|
+
import ctypes
|
|
83
|
+
from ctypes import wintypes
|
|
84
|
+
|
|
85
|
+
# Windows API constants
|
|
86
|
+
TH32CS_SNAPPROCESS = 0x00000002
|
|
87
|
+
INVALID_HANDLE_VALUE = -1
|
|
88
|
+
|
|
89
|
+
class PROCESSENTRY32(ctypes.Structure):
|
|
90
|
+
_fields_ = [
|
|
91
|
+
("dwSize", wintypes.DWORD),
|
|
92
|
+
("cntUsage", wintypes.DWORD),
|
|
93
|
+
("th32ProcessID", wintypes.DWORD),
|
|
94
|
+
("th32DefaultHeapID", ctypes.POINTER(wintypes.ULONG)),
|
|
95
|
+
("th32ModuleID", wintypes.DWORD),
|
|
96
|
+
("cntThreads", wintypes.DWORD),
|
|
97
|
+
("th32ParentProcessID", wintypes.DWORD),
|
|
98
|
+
("pcPriClassBase", wintypes.LONG),
|
|
99
|
+
("dwFlags", wintypes.DWORD),
|
|
100
|
+
("szExeFile", ctypes.c_char * 260),
|
|
101
|
+
]
|
|
102
|
+
|
|
103
|
+
kernel32 = ctypes.windll.kernel32
|
|
104
|
+
|
|
105
|
+
# Take a snapshot of all processes
|
|
106
|
+
snapshot = kernel32.CreateToolhelp32Snapshot(TH32CS_SNAPPROCESS, 0)
|
|
107
|
+
if snapshot == INVALID_HANDLE_VALUE:
|
|
108
|
+
return chain
|
|
109
|
+
|
|
110
|
+
try:
|
|
111
|
+
# Build a map of PID -> (parent_pid, exe_name)
|
|
112
|
+
process_map: dict[int, tuple[int, str]] = {}
|
|
113
|
+
pe = PROCESSENTRY32()
|
|
114
|
+
pe.dwSize = ctypes.sizeof(PROCESSENTRY32)
|
|
115
|
+
|
|
116
|
+
if kernel32.Process32First(snapshot, ctypes.byref(pe)):
|
|
117
|
+
while True:
|
|
118
|
+
pid = pe.th32ProcessID
|
|
119
|
+
parent_pid = pe.th32ParentProcessID
|
|
120
|
+
exe_name = pe.szExeFile.decode("utf-8", errors="ignore").lower()
|
|
121
|
+
process_map[pid] = (parent_pid, exe_name)
|
|
122
|
+
|
|
123
|
+
if not kernel32.Process32Next(snapshot, ctypes.byref(pe)):
|
|
124
|
+
break
|
|
125
|
+
|
|
126
|
+
# Traverse from current PID up the parent chain
|
|
127
|
+
current_pid = os.getpid()
|
|
128
|
+
visited = set() # Prevent infinite loops
|
|
129
|
+
|
|
130
|
+
while current_pid in process_map and current_pid not in visited:
|
|
131
|
+
visited.add(current_pid)
|
|
132
|
+
parent_pid, exe_name = process_map[current_pid]
|
|
133
|
+
chain.append(exe_name)
|
|
134
|
+
|
|
135
|
+
if parent_pid == 0 or parent_pid == current_pid:
|
|
136
|
+
break
|
|
137
|
+
current_pid = parent_pid
|
|
138
|
+
|
|
139
|
+
finally:
|
|
140
|
+
kernel32.CloseHandle(snapshot)
|
|
141
|
+
|
|
142
|
+
except Exception:
|
|
143
|
+
pass
|
|
144
|
+
|
|
145
|
+
return chain
|
|
146
|
+
|
|
147
|
+
|
|
148
|
+
def _get_parent_process_chain() -> list[str]:
|
|
149
|
+
"""Get the parent process chain using best available method.
|
|
150
|
+
|
|
151
|
+
Returns:
|
|
152
|
+
List of process names from current process up to init/System
|
|
153
|
+
"""
|
|
154
|
+
# Try psutil first (more reliable, cross-platform)
|
|
155
|
+
try:
|
|
156
|
+
import psutil # noqa: F401
|
|
157
|
+
|
|
158
|
+
return _get_parent_process_chain_psutil()
|
|
159
|
+
except ImportError:
|
|
160
|
+
pass
|
|
161
|
+
|
|
162
|
+
# Fall back to ctypes on Windows
|
|
163
|
+
if platform.system() == "Windows":
|
|
164
|
+
return _get_parent_process_chain_windows_ctypes()
|
|
165
|
+
|
|
166
|
+
return []
|
|
167
|
+
|
|
168
|
+
|
|
169
|
+
def _is_uvx_in_chain(chain: list[str]) -> bool:
|
|
170
|
+
"""Check if uvx is in the process chain.
|
|
171
|
+
|
|
172
|
+
Note: We only check for uvx.exe, NOT uv.exe. The uv.exe binary
|
|
173
|
+
(used by `uv run`) handles SIGINT correctly on Windows, but
|
|
174
|
+
uvx.exe captures it before it reaches Python.
|
|
175
|
+
|
|
176
|
+
Args:
|
|
177
|
+
chain: List of process names (lowercase)
|
|
178
|
+
|
|
179
|
+
Returns:
|
|
180
|
+
True if uvx.exe is found in the chain
|
|
181
|
+
"""
|
|
182
|
+
# Only uvx.exe has the SIGINT issue, not uv.exe
|
|
183
|
+
uvx_names = {"uvx.exe", "uvx"}
|
|
184
|
+
return any(name in uvx_names for name in chain)
|
|
185
|
+
|
|
186
|
+
|
|
187
|
+
@lru_cache(maxsize=1)
|
|
188
|
+
def is_launched_via_uvx() -> bool:
|
|
189
|
+
"""Detect if code-puppy was launched via uvx.
|
|
190
|
+
|
|
191
|
+
Traverses the parent process chain to find uvx.exe or uv.exe.
|
|
192
|
+
Result is cached for the lifetime of the process.
|
|
193
|
+
|
|
194
|
+
Returns:
|
|
195
|
+
True if launched via uvx, False otherwise
|
|
196
|
+
"""
|
|
197
|
+
chain = _get_parent_process_chain()
|
|
198
|
+
return _is_uvx_in_chain(chain)
|
|
199
|
+
|
|
200
|
+
|
|
201
|
+
def is_windows() -> bool:
|
|
202
|
+
"""Check if we're running on Windows.
|
|
203
|
+
|
|
204
|
+
Returns:
|
|
205
|
+
True if running on Windows, False otherwise
|
|
206
|
+
"""
|
|
207
|
+
return platform.system() == "Windows"
|
|
208
|
+
|
|
209
|
+
|
|
210
|
+
def should_use_alternate_cancel_key() -> bool:
|
|
211
|
+
"""Determine if we should use an alternate cancel key (Ctrl+K) instead of Ctrl+C.
|
|
212
|
+
|
|
213
|
+
This returns True when:
|
|
214
|
+
- Running on Windows AND
|
|
215
|
+
- Launched via uvx
|
|
216
|
+
|
|
217
|
+
In this scenario, Ctrl+C is captured by uvx before reaching Python,
|
|
218
|
+
so we need to use a different key (Ctrl+K) for agent cancellation.
|
|
219
|
+
|
|
220
|
+
Returns:
|
|
221
|
+
True if alternate cancel key should be used, False otherwise
|
|
222
|
+
"""
|
|
223
|
+
return is_windows() and is_launched_via_uvx()
|
|
224
|
+
|
|
225
|
+
|
|
226
|
+
def get_uvx_detection_info() -> dict:
|
|
227
|
+
"""Get diagnostic information about uvx detection.
|
|
228
|
+
|
|
229
|
+
Useful for debugging and testing.
|
|
230
|
+
|
|
231
|
+
Returns:
|
|
232
|
+
Dictionary with detection details
|
|
233
|
+
"""
|
|
234
|
+
chain = _get_parent_process_chain()
|
|
235
|
+
return {
|
|
236
|
+
"is_windows": is_windows(),
|
|
237
|
+
"is_launched_via_uvx": is_launched_via_uvx(),
|
|
238
|
+
"should_use_alternate_cancel_key": should_use_alternate_cancel_key(),
|
|
239
|
+
"parent_process_chain": chain,
|
|
240
|
+
"current_pid": os.getpid(),
|
|
241
|
+
"python_executable": sys.executable,
|
|
242
|
+
}
|
|
@@ -3,12 +3,12 @@ code_puppy/__main__.py,sha256=pDVssJOWP8A83iFkxMLY9YteHYat0EyWDQqMkKHpWp4,203
|
|
|
3
3
|
code_puppy/callbacks.py,sha256=hqTV--dNxG5vwWWm3MrEjmb8MZuHFFdmHePl23NXPHk,8621
|
|
4
4
|
code_puppy/chatgpt_codex_client.py,sha256=Om0ANB_kpHubhCwNzF9ENf8RvKBqs0IYzBLl_SNw0Vk,9833
|
|
5
5
|
code_puppy/claude_cache_client.py,sha256=hZr_YtXZSQvBoJFtRbbecKucYqJgoMopqUmm0IxFYGY,6071
|
|
6
|
-
code_puppy/cli_runner.py,sha256=
|
|
6
|
+
code_puppy/cli_runner.py,sha256=VkDaANmywBNoj6wCzaLlI1Z2yFC98U4BqHmSIcV4HCw,32150
|
|
7
7
|
code_puppy/config.py,sha256=qqeJrQP7gqADqeYqVzfksP7NYGROLrBQCuYic5PuQfY,52295
|
|
8
8
|
code_puppy/error_logging.py,sha256=a80OILCUtJhexI6a9GM-r5LqIdjvSRzggfgPp2jv1X0,3297
|
|
9
9
|
code_puppy/gemini_code_assist.py,sha256=KGS7sO5OLc83nDF3xxS-QiU6vxW9vcm6hmzilu79Ef8,13867
|
|
10
10
|
code_puppy/http_utils.py,sha256=w5mWYIGIWJZJvgvMahXs9BmdidoJvGn4CASDRY88a8o,13414
|
|
11
|
-
code_puppy/keymap.py,sha256=
|
|
11
|
+
code_puppy/keymap.py,sha256=IvMkTlB_bIqOWpbTpmftkdyjhtD5todXuEIw1zCZ4u0,3584
|
|
12
12
|
code_puppy/main.py,sha256=82r3vZy_XcyEsenLn82BnUusaoyL3Bpm_Th_jKgqecE,273
|
|
13
13
|
code_puppy/model_factory.py,sha256=H_a5nX462Q-dhX3g3ZY7dmBCIAUOd1aOSZa4HMxF1o4,34191
|
|
14
14
|
code_puppy/model_utils.py,sha256=NU8W8NW5F7QS_PXHaLeh55Air1koUV7IVYFP7Rz3XpY,3615
|
|
@@ -21,7 +21,8 @@ code_puppy/round_robin_model.py,sha256=kSawwPUiPgg0yg8r4AAVgvjzsWkptxpSORd75-HP7
|
|
|
21
21
|
code_puppy/session_storage.py,sha256=T4hOsAl9z0yz2JZCptjJBOnN8fCmkLZx5eLy1hTdv6Q,9631
|
|
22
22
|
code_puppy/status_display.py,sha256=qHzIQGAPEa2_-4gQSg7_rE1ihOosBq8WO73MWFNmmlo,8938
|
|
23
23
|
code_puppy/summarization_agent.py,sha256=6Pu_Wp_rF-HAhoX9u2uXTabRVkOZUYwRoMP1lzNS4ew,4485
|
|
24
|
-
code_puppy/terminal_utils.py,sha256=
|
|
24
|
+
code_puppy/terminal_utils.py,sha256=CxcNLfPwTDblI0AEtEwhfZ4DTfqqwHjM_A10QslaMBk,8220
|
|
25
|
+
code_puppy/uvx_detection.py,sha256=tP9X9Nvzow--KIqtqjgrHQkSxMJ3EevfoaeoB9VLY2o,7224
|
|
25
26
|
code_puppy/version_checker.py,sha256=aq2Mwxl1CR9sEFBgrPt3OQOowLOBUp9VaQYWJhuUv8Q,1780
|
|
26
27
|
code_puppy/agents/__init__.py,sha256=PtPB7Z5MSwmUKipgt_qxvIuGggcuVaYwNbnp1UP4tPc,518
|
|
27
28
|
code_puppy/agents/agent_c_reviewer.py,sha256=1kO_89hcrhlS4sJ6elDLSEx-h43jAaWGgvIL0SZUuKo,8214
|
|
@@ -39,7 +40,7 @@ code_puppy/agents/agent_qa_expert.py,sha256=5Ikb4U3SZQknUEfwlHZiyZXKqnffnOTQagr_
|
|
|
39
40
|
code_puppy/agents/agent_qa_kitten.py,sha256=5PeFFSwCFlTUvP6h5bGntx0xv5NmRwBiw0HnMqY8nLI,9107
|
|
40
41
|
code_puppy/agents/agent_security_auditor.py,sha256=SpiYNA0XAsIwBj7S2_EQPRslRUmF_-b89pIJyW7DYtY,12022
|
|
41
42
|
code_puppy/agents/agent_typescript_reviewer.py,sha256=vsnpp98xg6cIoFAEJrRTUM_i4wLEWGm5nJxs6fhHobM,10275
|
|
42
|
-
code_puppy/agents/base_agent.py,sha256=
|
|
43
|
+
code_puppy/agents/base_agent.py,sha256=r_znuUZJMv97Lh8zeSdS_KJzVGe7X3rAgBk3NZpIO7I,82855
|
|
43
44
|
code_puppy/agents/json_agent.py,sha256=lhopDJDoiSGHvD8A6t50hi9ZBoNRKgUywfxd0Po_Dzc,4886
|
|
44
45
|
code_puppy/agents/prompt_reviewer.py,sha256=JJrJ0m5q0Puxl8vFsyhAbY9ftU9n6c6UxEVdNct1E-Q,5558
|
|
45
46
|
code_puppy/command_line/__init__.py,sha256=y7WeRemfYppk8KVbCGeAIiTuiOszIURCDjOMZv_YRmU,45
|
|
@@ -144,7 +145,7 @@ code_puppy/plugins/shell_safety/register_callbacks.py,sha256=W3v664RR48Fdbbbltf_
|
|
|
144
145
|
code_puppy/prompts/codex_system_prompt.md,sha256=hEFTCziroLqZmqNle5kG34A8kvTteOWezCiVrAEKhE0,24400
|
|
145
146
|
code_puppy/tools/__init__.py,sha256=BVTZ85jLHgDANwOnUSOz3UDlp8VQDq4DoGF23BRlyWw,6032
|
|
146
147
|
code_puppy/tools/agent_tools.py,sha256=snBI6FlFtR03CbYKXwu53R48c_fRSuDIwcNdVUruLcA,21020
|
|
147
|
-
code_puppy/tools/command_runner.py,sha256=
|
|
148
|
+
code_puppy/tools/command_runner.py,sha256=WLesijwbXEsnyuIJvWZHbVVyoUAPQcTWJbz31pXPSi0,44325
|
|
148
149
|
code_puppy/tools/common.py,sha256=IboS6sbwN4a3FzHdfsZJtEFiyDUCszevI6LpH14ydEk,40561
|
|
149
150
|
code_puppy/tools/file_modifications.py,sha256=vz9n7R0AGDSdLUArZr_55yJLkyI30M8zreAppxIx02M,29380
|
|
150
151
|
code_puppy/tools/file_operations.py,sha256=CqhpuBnOFOcQCIYXOujskxq2VMLWYJhibYrH0YcPSfA,35692
|
|
@@ -159,10 +160,10 @@ code_puppy/tools/browser/browser_scripts.py,sha256=sNb8eLEyzhasy5hV4B9OjM8yIVMLV
|
|
|
159
160
|
code_puppy/tools/browser/browser_workflows.py,sha256=nitW42vCf0ieTX1gLabozTugNQ8phtoFzZbiAhw1V90,6491
|
|
160
161
|
code_puppy/tools/browser/camoufox_manager.py,sha256=RZjGOEftE5sI_tsercUyXFSZI2wpStXf-q0PdYh2G3I,8680
|
|
161
162
|
code_puppy/tools/browser/vqa_agent.py,sha256=DBn9HKloILqJSTSdNZzH_PYWT0B2h9VwmY6akFQI_uU,2913
|
|
162
|
-
code_puppy-0.0.
|
|
163
|
-
code_puppy-0.0.
|
|
164
|
-
code_puppy-0.0.
|
|
165
|
-
code_puppy-0.0.
|
|
166
|
-
code_puppy-0.0.
|
|
167
|
-
code_puppy-0.0.
|
|
168
|
-
code_puppy-0.0.
|
|
163
|
+
code_puppy-0.0.333.data/data/code_puppy/models.json,sha256=IPABdOrDw2OZJxa0XGBWSWmBRerV6_pIEmKVLRtUbAk,3105
|
|
164
|
+
code_puppy-0.0.333.data/data/code_puppy/models_dev_api.json,sha256=wHjkj-IM_fx1oHki6-GqtOoCrRMR0ScK0f-Iz0UEcy8,548187
|
|
165
|
+
code_puppy-0.0.333.dist-info/METADATA,sha256=VP7p42x7DQGz8JDM0nIF7MXuY41YkD0wIxBINx4334w,28854
|
|
166
|
+
code_puppy-0.0.333.dist-info/WHEEL,sha256=WLgqFyCfm_KASv4WHyYy0P3pM_m7J5L9k2skdKLirC8,87
|
|
167
|
+
code_puppy-0.0.333.dist-info/entry_points.txt,sha256=Tp4eQC99WY3HOKd3sdvb22vZODRq0XkZVNpXOag_KdI,91
|
|
168
|
+
code_puppy-0.0.333.dist-info/licenses/LICENSE,sha256=31u8x0SPgdOq3izJX41kgFazWsM43zPEF9eskzqbJMY,1075
|
|
169
|
+
code_puppy-0.0.333.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|