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
|
@@ -0,0 +1,169 @@
|
|
|
1
|
+
"""Utility helpers for the Antigravity OAuth plugin."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import json
|
|
6
|
+
import logging
|
|
7
|
+
from typing import Any, Dict, List, Optional
|
|
8
|
+
|
|
9
|
+
from .config import (
|
|
10
|
+
ANTIGRAVITY_OAUTH_CONFIG,
|
|
11
|
+
get_antigravity_models_path,
|
|
12
|
+
get_token_storage_path,
|
|
13
|
+
)
|
|
14
|
+
from .constants import ANTIGRAVITY_ENDPOINT, ANTIGRAVITY_HEADERS, ANTIGRAVITY_MODELS
|
|
15
|
+
|
|
16
|
+
logger = logging.getLogger(__name__)
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def load_stored_tokens() -> Optional[Dict[str, Any]]:
|
|
20
|
+
"""Load stored OAuth tokens from disk."""
|
|
21
|
+
try:
|
|
22
|
+
token_path = get_token_storage_path()
|
|
23
|
+
if token_path.exists():
|
|
24
|
+
with open(token_path, "r", encoding="utf-8") as f:
|
|
25
|
+
return json.load(f)
|
|
26
|
+
except Exception as e:
|
|
27
|
+
logger.error("Failed to load tokens: %s", e)
|
|
28
|
+
return None
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def save_tokens(tokens: Dict[str, Any]) -> bool:
|
|
32
|
+
"""Save OAuth tokens to disk."""
|
|
33
|
+
try:
|
|
34
|
+
token_path = get_token_storage_path()
|
|
35
|
+
with open(token_path, "w", encoding="utf-8") as f:
|
|
36
|
+
json.dump(tokens, f, indent=2)
|
|
37
|
+
token_path.chmod(0o600)
|
|
38
|
+
return True
|
|
39
|
+
except Exception as e:
|
|
40
|
+
logger.error("Failed to save tokens: %s", e)
|
|
41
|
+
return False
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def load_antigravity_models() -> Dict[str, Any]:
|
|
45
|
+
"""Load configured Antigravity models from disk."""
|
|
46
|
+
try:
|
|
47
|
+
models_path = get_antigravity_models_path()
|
|
48
|
+
if models_path.exists():
|
|
49
|
+
with open(models_path, "r", encoding="utf-8") as f:
|
|
50
|
+
return json.load(f)
|
|
51
|
+
except Exception as e:
|
|
52
|
+
logger.error("Failed to load Antigravity models: %s", e)
|
|
53
|
+
return {}
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
def save_antigravity_models(models: Dict[str, Any]) -> bool:
|
|
57
|
+
"""Save Antigravity models configuration to disk."""
|
|
58
|
+
try:
|
|
59
|
+
models_path = get_antigravity_models_path()
|
|
60
|
+
with open(models_path, "w", encoding="utf-8") as f:
|
|
61
|
+
json.dump(models, f, indent=2)
|
|
62
|
+
return True
|
|
63
|
+
except Exception as e:
|
|
64
|
+
logger.error("Failed to save Antigravity models: %s", e)
|
|
65
|
+
return False
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
def add_models_to_config(access_token: str, project_id: str = "") -> bool:
|
|
69
|
+
"""Add all available Antigravity models to the configuration."""
|
|
70
|
+
try:
|
|
71
|
+
models_config: Dict[str, Any] = {}
|
|
72
|
+
prefix = ANTIGRAVITY_OAUTH_CONFIG["prefix"]
|
|
73
|
+
|
|
74
|
+
for model_id, model_info in ANTIGRAVITY_MODELS.items():
|
|
75
|
+
prefixed_name = f"{prefix}{model_id}"
|
|
76
|
+
|
|
77
|
+
# Build custom headers
|
|
78
|
+
headers = dict(ANTIGRAVITY_HEADERS)
|
|
79
|
+
|
|
80
|
+
# Use custom_gemini type with Antigravity transport
|
|
81
|
+
models_config[prefixed_name] = {
|
|
82
|
+
"type": "custom_gemini",
|
|
83
|
+
"name": model_id,
|
|
84
|
+
"custom_endpoint": {
|
|
85
|
+
"url": ANTIGRAVITY_ENDPOINT,
|
|
86
|
+
"api_key": access_token,
|
|
87
|
+
"headers": headers,
|
|
88
|
+
},
|
|
89
|
+
"project_id": project_id,
|
|
90
|
+
"context_length": model_info.get("context_length", 200000),
|
|
91
|
+
"family": model_info.get("family", "other"),
|
|
92
|
+
"oauth_source": "antigravity-plugin",
|
|
93
|
+
"antigravity": True, # Flag to use Antigravity transport
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
# Add thinking budget if present
|
|
97
|
+
if model_info.get("thinking_budget"):
|
|
98
|
+
models_config[prefixed_name]["thinking_budget"] = model_info[
|
|
99
|
+
"thinking_budget"
|
|
100
|
+
]
|
|
101
|
+
|
|
102
|
+
if save_antigravity_models(models_config):
|
|
103
|
+
logger.info("Added %d Antigravity models", len(models_config))
|
|
104
|
+
return True
|
|
105
|
+
|
|
106
|
+
except Exception as e:
|
|
107
|
+
logger.error("Error adding models to config: %s", e)
|
|
108
|
+
return False
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
def remove_antigravity_models() -> int:
|
|
112
|
+
"""Remove all Antigravity models from configuration."""
|
|
113
|
+
try:
|
|
114
|
+
models = load_antigravity_models()
|
|
115
|
+
to_remove = [
|
|
116
|
+
name
|
|
117
|
+
for name, config in models.items()
|
|
118
|
+
if config.get("oauth_source") == "antigravity-plugin"
|
|
119
|
+
]
|
|
120
|
+
|
|
121
|
+
if not to_remove:
|
|
122
|
+
return 0
|
|
123
|
+
|
|
124
|
+
for model_name in to_remove:
|
|
125
|
+
models.pop(model_name, None)
|
|
126
|
+
|
|
127
|
+
if save_antigravity_models(models):
|
|
128
|
+
return len(to_remove)
|
|
129
|
+
except Exception as e:
|
|
130
|
+
logger.error("Error removing Antigravity models: %s", e)
|
|
131
|
+
return 0
|
|
132
|
+
|
|
133
|
+
|
|
134
|
+
def get_model_families_summary() -> Dict[str, List[str]]:
|
|
135
|
+
"""Get a summary of available models by family."""
|
|
136
|
+
families: Dict[str, List[str]] = {
|
|
137
|
+
"gemini": [],
|
|
138
|
+
"claude": [],
|
|
139
|
+
"other": [],
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
for model_id, info in ANTIGRAVITY_MODELS.items():
|
|
143
|
+
family = info.get("family", "other")
|
|
144
|
+
if family in families:
|
|
145
|
+
families[family].append(model_id)
|
|
146
|
+
|
|
147
|
+
return families
|
|
148
|
+
|
|
149
|
+
|
|
150
|
+
def reload_current_agent() -> None:
|
|
151
|
+
"""Reload the current agent so new auth tokens are picked up immediately."""
|
|
152
|
+
try:
|
|
153
|
+
from code_puppy.agents import get_current_agent
|
|
154
|
+
|
|
155
|
+
current_agent = get_current_agent()
|
|
156
|
+
if current_agent is None:
|
|
157
|
+
logger.debug("No current agent to reload")
|
|
158
|
+
return
|
|
159
|
+
|
|
160
|
+
if hasattr(current_agent, "refresh_config"):
|
|
161
|
+
try:
|
|
162
|
+
current_agent.refresh_config()
|
|
163
|
+
except Exception:
|
|
164
|
+
pass
|
|
165
|
+
|
|
166
|
+
current_agent.reload_code_generation_agent()
|
|
167
|
+
logger.info("Active agent reloaded with new authentication")
|
|
168
|
+
except Exception as e:
|
|
169
|
+
logger.warning("Agent reload failed: %s", e)
|
|
@@ -6,6 +6,7 @@ import os
|
|
|
6
6
|
from typing import List, Optional, Tuple
|
|
7
7
|
|
|
8
8
|
from code_puppy.callbacks import register_callback
|
|
9
|
+
from code_puppy.config import set_model_name
|
|
9
10
|
from code_puppy.messaging import emit_info, emit_success, emit_warning
|
|
10
11
|
|
|
11
12
|
from .config import CHATGPT_OAUTH_CONFIG, get_token_storage_path
|
|
@@ -75,6 +76,7 @@ def _handle_custom_command(command: str, name: str) -> Optional[bool]:
|
|
|
75
76
|
|
|
76
77
|
if name == "chatgpt-auth":
|
|
77
78
|
run_oauth_flow()
|
|
79
|
+
set_model_name("chatgpt-gpt-5.2-codex")
|
|
78
80
|
return True
|
|
79
81
|
|
|
80
82
|
if name == "chatgpt-status":
|
|
@@ -12,6 +12,7 @@ from typing import Any, Dict, List, Optional, Tuple
|
|
|
12
12
|
from urllib.parse import parse_qs, urlparse
|
|
13
13
|
|
|
14
14
|
from code_puppy.callbacks import register_callback
|
|
15
|
+
from code_puppy.config import set_model_name
|
|
15
16
|
from code_puppy.messaging import emit_error, emit_info, emit_success, emit_warning
|
|
16
17
|
|
|
17
18
|
from ..oauth_puppy_html import oauth_failure_html, oauth_success_html
|
|
@@ -260,6 +261,7 @@ def _handle_custom_command(command: str, name: str) -> Optional[bool]:
|
|
|
260
261
|
"Existing Claude Code tokens found. Continuing will overwrite them."
|
|
261
262
|
)
|
|
262
263
|
_perform_authentication()
|
|
264
|
+
set_model_name("claude-code-claude-opus-4-5-20251101")
|
|
263
265
|
return True
|
|
264
266
|
|
|
265
267
|
if name == "claude-code-status":
|
|
@@ -54,13 +54,15 @@ class ReopenableAsyncClient:
|
|
|
54
54
|
if self._stream_context:
|
|
55
55
|
return await self._stream_context.__aexit__(exc_type, exc_val, exc_tb)
|
|
56
56
|
|
|
57
|
-
def __init__(self, **kwargs):
|
|
57
|
+
def __init__(self, client_class=None, **kwargs):
|
|
58
58
|
"""
|
|
59
59
|
Initialize the ReopenableAsyncClient.
|
|
60
60
|
|
|
61
61
|
Args:
|
|
62
|
-
|
|
62
|
+
client_class: Class to use for creating the internal client (defaults to httpx.AsyncClient)
|
|
63
|
+
**kwargs: All arguments that would be passed to the client constructor
|
|
63
64
|
"""
|
|
65
|
+
self._client_class = client_class or httpx.AsyncClient
|
|
64
66
|
self._client_kwargs = kwargs.copy()
|
|
65
67
|
self._client: Optional[httpx.AsyncClient] = None
|
|
66
68
|
self._is_closed = True
|
|
@@ -70,7 +72,7 @@ class ReopenableAsyncClient:
|
|
|
70
72
|
Ensure the underlying client is open and ready to use.
|
|
71
73
|
|
|
72
74
|
Returns:
|
|
73
|
-
The active
|
|
75
|
+
The active client instance
|
|
74
76
|
|
|
75
77
|
Raises:
|
|
76
78
|
RuntimeError: If client cannot be opened
|
|
@@ -80,12 +82,12 @@ class ReopenableAsyncClient:
|
|
|
80
82
|
return self._client
|
|
81
83
|
|
|
82
84
|
async def _create_client(self) -> None:
|
|
83
|
-
"""Create a new
|
|
85
|
+
"""Create a new client with the stored configuration."""
|
|
84
86
|
if self._client is not None and not self._is_closed:
|
|
85
87
|
# Close existing client first
|
|
86
88
|
await self._client.aclose()
|
|
87
89
|
|
|
88
|
-
self._client =
|
|
90
|
+
self._client = self._client_class(**self._client_kwargs)
|
|
89
91
|
self._is_closed = False
|
|
90
92
|
|
|
91
93
|
async def reopen(self) -> None:
|
|
@@ -171,14 +173,12 @@ class ReopenableAsyncClient:
|
|
|
171
173
|
"""
|
|
172
174
|
if self._client is None or self._is_closed:
|
|
173
175
|
# Create a temporary client just for building the request
|
|
174
|
-
temp_client =
|
|
176
|
+
temp_client = self._client_class(**self._client_kwargs)
|
|
175
177
|
try:
|
|
176
178
|
request = temp_client.build_request(method, url, **kwargs)
|
|
177
179
|
return request
|
|
178
180
|
finally:
|
|
179
181
|
# Clean up the temporary client synchronously if possible
|
|
180
|
-
# Note: This might leave a connection open, but it's better than
|
|
181
|
-
# making this method async just for building requests
|
|
182
182
|
pass
|
|
183
183
|
return self._client.build_request(method, url, **kwargs)
|
|
184
184
|
|
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,17 +90,36 @@ def reset_windows_console_mode() -> None:
|
|
|
86
90
|
pass # Silently ignore errors - best effort reset
|
|
87
91
|
|
|
88
92
|
|
|
93
|
+
def flush_windows_keyboard_buffer() -> None:
|
|
94
|
+
"""Flush the Windows keyboard buffer.
|
|
95
|
+
|
|
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
|
|
101
|
+
|
|
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
|
+
|
|
89
111
|
def reset_windows_terminal_full() -> None:
|
|
90
|
-
"""Perform a full Windows terminal reset (ANSI + console mode).
|
|
112
|
+
"""Perform a full Windows terminal reset (ANSI + console mode + keyboard buffer).
|
|
91
113
|
|
|
92
|
-
Combines
|
|
93
|
-
terminal state restoration after interrupts.
|
|
114
|
+
Combines ANSI reset, console mode reset, and keyboard buffer flush
|
|
115
|
+
for complete terminal state restoration after interrupts.
|
|
94
116
|
"""
|
|
95
117
|
if platform.system() != "Windows":
|
|
96
118
|
return
|
|
97
119
|
|
|
98
120
|
reset_windows_terminal_ansi()
|
|
99
121
|
reset_windows_console_mode()
|
|
122
|
+
flush_windows_keyboard_buffer()
|
|
100
123
|
|
|
101
124
|
|
|
102
125
|
def reset_unix_terminal() -> None:
|
|
@@ -124,3 +147,145 @@ def reset_terminal() -> None:
|
|
|
124
147
|
reset_windows_terminal_full()
|
|
125
148
|
else:
|
|
126
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
|
|
@@ -9,7 +9,7 @@ import threading
|
|
|
9
9
|
import time
|
|
10
10
|
import traceback
|
|
11
11
|
from contextlib import contextmanager
|
|
12
|
-
from typing import Callable, Literal, Optional, Set
|
|
12
|
+
from typing import Callable, List, Literal, Optional, Set
|
|
13
13
|
|
|
14
14
|
from pydantic import BaseModel
|
|
15
15
|
from pydantic_ai import RunContext
|
|
@@ -192,11 +192,6 @@ def kill_all_running_shell_processes() -> int:
|
|
|
192
192
|
"""Kill all currently tracked running shell processes and stop reader threads.
|
|
193
193
|
|
|
194
194
|
Returns the number of processes signaled.
|
|
195
|
-
|
|
196
|
-
Implementation notes:
|
|
197
|
-
- Atomically snapshot and clear the registry to prevent race conditions
|
|
198
|
-
- Deduplicate by PID to ensure each process is killed at most once
|
|
199
|
-
- Let exceptions from _kill_process_group propagate (tests expect this)
|
|
200
195
|
"""
|
|
201
196
|
global _READER_STOP_EVENT
|
|
202
197
|
|
|
@@ -204,52 +199,30 @@ def kill_all_running_shell_processes() -> int:
|
|
|
204
199
|
if _READER_STOP_EVENT:
|
|
205
200
|
_READER_STOP_EVENT.set()
|
|
206
201
|
|
|
207
|
-
|
|
208
|
-
# This prevents other threads from seeing/processing the same processes
|
|
202
|
+
procs: list[subprocess.Popen]
|
|
209
203
|
with _RUNNING_PROCESSES_LOCK:
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
# Deduplicate by pid to ensure at-most-one kill per process
|
|
214
|
-
seen_pids: set = set()
|
|
215
|
-
killed_count = 0
|
|
216
|
-
|
|
217
|
-
for proc in procs_snapshot:
|
|
218
|
-
if proc is None:
|
|
219
|
-
continue
|
|
220
|
-
|
|
221
|
-
pid = getattr(proc, "pid", None)
|
|
222
|
-
key = pid if pid is not None else id(proc)
|
|
223
|
-
|
|
224
|
-
if key in seen_pids:
|
|
225
|
-
continue
|
|
226
|
-
seen_pids.add(key)
|
|
227
|
-
|
|
228
|
-
# Close pipes first to unblock readline()
|
|
204
|
+
procs = list(_RUNNING_PROCESSES)
|
|
205
|
+
count = 0
|
|
206
|
+
for p in procs:
|
|
229
207
|
try:
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
if proc.poll() is None:
|
|
241
|
-
# Let exceptions bubble up (tests expect this behavior)
|
|
242
|
-
_kill_process_group(proc)
|
|
243
|
-
killed_count += 1
|
|
244
|
-
|
|
245
|
-
# Track user-killed PIDs
|
|
246
|
-
if pid is not None:
|
|
247
|
-
try:
|
|
248
|
-
_USER_KILLED_PROCESSES.add(pid)
|
|
249
|
-
except Exception:
|
|
250
|
-
pass # Non-fatal bookkeeping
|
|
208
|
+
# Close pipes first to unblock readline()
|
|
209
|
+
try:
|
|
210
|
+
if p.stdout and not p.stdout.closed:
|
|
211
|
+
p.stdout.close()
|
|
212
|
+
if p.stderr and not p.stderr.closed:
|
|
213
|
+
p.stderr.close()
|
|
214
|
+
if p.stdin and not p.stdin.closed:
|
|
215
|
+
p.stdin.close()
|
|
216
|
+
except (OSError, ValueError):
|
|
217
|
+
pass
|
|
251
218
|
|
|
252
|
-
|
|
219
|
+
if p.poll() is None:
|
|
220
|
+
_kill_process_group(p)
|
|
221
|
+
count += 1
|
|
222
|
+
_USER_KILLED_PROCESSES.add(p.pid)
|
|
223
|
+
finally:
|
|
224
|
+
_unregister_process(p)
|
|
225
|
+
return count
|
|
253
226
|
|
|
254
227
|
|
|
255
228
|
def get_running_shell_process_count() -> int:
|
|
@@ -1104,12 +1077,21 @@ class ReasoningOutput(BaseModel):
|
|
|
1104
1077
|
|
|
1105
1078
|
|
|
1106
1079
|
def share_your_reasoning(
|
|
1107
|
-
context: RunContext, reasoning: str, next_steps: str | None = None
|
|
1080
|
+
context: RunContext, reasoning: str, next_steps: str | List[str] | None = None
|
|
1108
1081
|
) -> ReasoningOutput:
|
|
1082
|
+
# Handle list of next steps by formatting them
|
|
1083
|
+
formatted_next_steps = next_steps
|
|
1084
|
+
if isinstance(next_steps, list):
|
|
1085
|
+
formatted_next_steps = "\n".join(
|
|
1086
|
+
[f"{i + 1}. {step}" for i, step in enumerate(next_steps)]
|
|
1087
|
+
)
|
|
1088
|
+
|
|
1109
1089
|
# Emit structured AgentReasoningMessage for the UI
|
|
1110
1090
|
reasoning_msg = AgentReasoningMessage(
|
|
1111
1091
|
reasoning=reasoning,
|
|
1112
|
-
next_steps=
|
|
1092
|
+
next_steps=formatted_next_steps
|
|
1093
|
+
if formatted_next_steps and formatted_next_steps.strip()
|
|
1094
|
+
else None,
|
|
1113
1095
|
)
|
|
1114
1096
|
get_message_bus().emit(reasoning_msg)
|
|
1115
1097
|
|
|
@@ -1197,7 +1179,9 @@ def register_agent_share_your_reasoning(agent):
|
|
|
1197
1179
|
|
|
1198
1180
|
@agent.tool
|
|
1199
1181
|
def agent_share_your_reasoning(
|
|
1200
|
-
context: RunContext,
|
|
1182
|
+
context: RunContext,
|
|
1183
|
+
reasoning: str = "",
|
|
1184
|
+
next_steps: str | List[str] | None = None,
|
|
1201
1185
|
) -> ReasoningOutput:
|
|
1202
1186
|
"""Share the agent's current reasoning and planned next steps with the user.
|
|
1203
1187
|
|
|
@@ -1211,8 +1195,8 @@ def register_agent_share_your_reasoning(agent):
|
|
|
1211
1195
|
reasoning for the current situation. This should be clear,
|
|
1212
1196
|
comprehensive, and explain the 'why' behind decisions.
|
|
1213
1197
|
next_steps: Planned upcoming actions or steps
|
|
1214
|
-
the agent intends to take. Can be
|
|
1215
|
-
are determined. Defaults to None.
|
|
1198
|
+
the agent intends to take. Can be a string or a list of strings.
|
|
1199
|
+
Can be None if no specific next steps are determined. Defaults to None.
|
|
1216
1200
|
|
|
1217
1201
|
Returns:
|
|
1218
1202
|
ReasoningOutput: A simple response object containing:
|
|
@@ -1223,6 +1207,10 @@ def register_agent_share_your_reasoning(agent):
|
|
|
1223
1207
|
>>> next_steps = "First, I'll list the directory contents, then read key files"
|
|
1224
1208
|
>>> result = agent_share_your_reasoning(ctx, reasoning, next_steps)
|
|
1225
1209
|
|
|
1210
|
+
>>> # Using a list for next steps
|
|
1211
|
+
>>> next_steps_list = ["List files", "Read README.md", "Run tests"]
|
|
1212
|
+
>>> result = agent_share_your_reasoning(ctx, reasoning, next_steps_list)
|
|
1213
|
+
|
|
1226
1214
|
Best Practice:
|
|
1227
1215
|
Use this tool frequently to maintain transparency. Call it:
|
|
1228
1216
|
- Before starting complex operations
|