code-puppy 0.0.96__py3-none-any.whl → 0.0.118__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/__init__.py +2 -5
- code_puppy/__main__.py +10 -0
- code_puppy/agent.py +125 -40
- code_puppy/agent_prompts.py +30 -24
- code_puppy/callbacks.py +152 -0
- code_puppy/command_line/command_handler.py +359 -0
- code_puppy/command_line/load_context_completion.py +59 -0
- code_puppy/command_line/model_picker_completion.py +14 -21
- code_puppy/command_line/motd.py +44 -28
- code_puppy/command_line/prompt_toolkit_completion.py +42 -23
- code_puppy/config.py +266 -26
- code_puppy/http_utils.py +122 -0
- code_puppy/main.py +570 -383
- code_puppy/message_history_processor.py +195 -104
- code_puppy/messaging/__init__.py +46 -0
- code_puppy/messaging/message_queue.py +288 -0
- code_puppy/messaging/queue_console.py +293 -0
- code_puppy/messaging/renderers.py +305 -0
- code_puppy/messaging/spinner/__init__.py +55 -0
- code_puppy/messaging/spinner/console_spinner.py +200 -0
- code_puppy/messaging/spinner/spinner_base.py +66 -0
- code_puppy/messaging/spinner/textual_spinner.py +97 -0
- code_puppy/model_factory.py +73 -105
- code_puppy/plugins/__init__.py +32 -0
- code_puppy/reopenable_async_client.py +225 -0
- code_puppy/state_management.py +60 -21
- code_puppy/summarization_agent.py +56 -35
- code_puppy/token_utils.py +7 -9
- code_puppy/tools/__init__.py +1 -4
- code_puppy/tools/command_runner.py +187 -32
- code_puppy/tools/common.py +44 -35
- code_puppy/tools/file_modifications.py +335 -118
- code_puppy/tools/file_operations.py +368 -95
- code_puppy/tools/token_check.py +27 -11
- code_puppy/tools/tools_content.py +53 -0
- code_puppy/tui/__init__.py +10 -0
- code_puppy/tui/app.py +1050 -0
- code_puppy/tui/components/__init__.py +21 -0
- code_puppy/tui/components/chat_view.py +512 -0
- code_puppy/tui/components/command_history_modal.py +218 -0
- code_puppy/tui/components/copy_button.py +139 -0
- code_puppy/tui/components/custom_widgets.py +58 -0
- code_puppy/tui/components/input_area.py +167 -0
- code_puppy/tui/components/sidebar.py +309 -0
- code_puppy/tui/components/status_bar.py +182 -0
- code_puppy/tui/messages.py +27 -0
- code_puppy/tui/models/__init__.py +8 -0
- code_puppy/tui/models/chat_message.py +25 -0
- code_puppy/tui/models/command_history.py +89 -0
- code_puppy/tui/models/enums.py +24 -0
- code_puppy/tui/screens/__init__.py +13 -0
- code_puppy/tui/screens/help.py +130 -0
- code_puppy/tui/screens/settings.py +256 -0
- code_puppy/tui/screens/tools.py +74 -0
- code_puppy/tui/tests/__init__.py +1 -0
- code_puppy/tui/tests/test_chat_message.py +28 -0
- code_puppy/tui/tests/test_chat_view.py +88 -0
- code_puppy/tui/tests/test_command_history.py +89 -0
- code_puppy/tui/tests/test_copy_button.py +191 -0
- code_puppy/tui/tests/test_custom_widgets.py +27 -0
- code_puppy/tui/tests/test_disclaimer.py +27 -0
- code_puppy/tui/tests/test_enums.py +15 -0
- code_puppy/tui/tests/test_file_browser.py +60 -0
- code_puppy/tui/tests/test_help.py +38 -0
- code_puppy/tui/tests/test_history_file_reader.py +107 -0
- code_puppy/tui/tests/test_input_area.py +33 -0
- code_puppy/tui/tests/test_settings.py +44 -0
- code_puppy/tui/tests/test_sidebar.py +33 -0
- code_puppy/tui/tests/test_sidebar_history.py +153 -0
- code_puppy/tui/tests/test_sidebar_history_navigation.py +132 -0
- code_puppy/tui/tests/test_status_bar.py +54 -0
- code_puppy/tui/tests/test_timestamped_history.py +52 -0
- code_puppy/tui/tests/test_tools.py +82 -0
- code_puppy/version_checker.py +26 -3
- {code_puppy-0.0.96.dist-info → code_puppy-0.0.118.dist-info}/METADATA +9 -2
- code_puppy-0.0.118.dist-info/RECORD +86 -0
- code_puppy-0.0.96.dist-info/RECORD +0 -32
- {code_puppy-0.0.96.data → code_puppy-0.0.118.data}/data/code_puppy/models.json +0 -0
- {code_puppy-0.0.96.dist-info → code_puppy-0.0.118.dist-info}/WHEEL +0 -0
- {code_puppy-0.0.96.dist-info → code_puppy-0.0.118.dist-info}/entry_points.txt +0 -0
- {code_puppy-0.0.96.dist-info → code_puppy-0.0.118.dist-info}/licenses/LICENSE +0 -0
code_puppy/main.py
CHANGED
|
@@ -1,168 +1,383 @@
|
|
|
1
1
|
import argparse
|
|
2
2
|
import asyncio
|
|
3
3
|
import os
|
|
4
|
+
import subprocess
|
|
4
5
|
import sys
|
|
6
|
+
import time
|
|
7
|
+
import webbrowser
|
|
5
8
|
|
|
6
|
-
from dotenv import load_dotenv
|
|
7
9
|
from rich.console import Console, ConsoleOptions, RenderResult
|
|
8
10
|
from rich.markdown import CodeBlock, Markdown
|
|
9
11
|
from rich.syntax import Syntax
|
|
10
12
|
from rich.text import Text
|
|
11
13
|
|
|
12
|
-
from code_puppy import __version__, state_management
|
|
13
|
-
from code_puppy.agent import get_code_generation_agent
|
|
14
|
+
from code_puppy import __version__, callbacks, plugins, state_management
|
|
15
|
+
from code_puppy.agent import get_code_generation_agent, get_custom_usage_limits
|
|
14
16
|
from code_puppy.command_line.prompt_toolkit_completion import (
|
|
15
17
|
get_input_with_combined_completion,
|
|
16
18
|
get_prompt_with_active_model,
|
|
17
19
|
)
|
|
18
|
-
from code_puppy.config import
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
20
|
+
from code_puppy.config import (
|
|
21
|
+
COMMAND_HISTORY_FILE,
|
|
22
|
+
ensure_config_exists,
|
|
23
|
+
initialize_command_history_file,
|
|
24
|
+
save_command_to_history,
|
|
25
|
+
)
|
|
26
|
+
from code_puppy.http_utils import find_available_port
|
|
27
|
+
from code_puppy.message_history_processor import (
|
|
28
|
+
message_history_accumulator,
|
|
29
|
+
prune_interrupted_tool_calls,
|
|
30
|
+
)
|
|
31
|
+
from code_puppy.state_management import is_tui_mode, set_tui_mode
|
|
23
32
|
from code_puppy.tools.common import console
|
|
24
|
-
from code_puppy.version_checker import
|
|
25
|
-
from code_puppy.message_history_processor import message_history_processor
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
# from code_puppy.tools import * # noqa: F403
|
|
29
|
-
import logfire
|
|
30
|
-
|
|
33
|
+
from code_puppy.version_checker import default_version_mismatch_behavior
|
|
31
34
|
|
|
32
|
-
|
|
33
|
-
def get_secret_file_path():
|
|
34
|
-
hidden_directory = os.path.join(os.path.expanduser("~"), ".agent_secret")
|
|
35
|
-
if not os.path.exists(hidden_directory):
|
|
36
|
-
os.makedirs(hidden_directory)
|
|
37
|
-
return os.path.join(hidden_directory, "history.txt")
|
|
35
|
+
plugins.load_plugin_callbacks()
|
|
38
36
|
|
|
39
37
|
|
|
40
38
|
async def main():
|
|
41
|
-
# Ensure the config directory and puppy.cfg with name info exist (prompt user if needed)
|
|
42
|
-
logfire.configure(
|
|
43
|
-
token="pylf_v1_us_8G5nLznQtHMRsL4hsNG5v3fPWKjyXbysrMgrQ1bV1wRP", console=False
|
|
44
|
-
)
|
|
45
|
-
logfire.instrument_pydantic_ai()
|
|
46
|
-
ensure_config_exists()
|
|
47
|
-
|
|
48
|
-
current_version = __version__
|
|
49
|
-
latest_version = fetch_latest_version("code-puppy")
|
|
50
|
-
console.print(f"Current version: {current_version}")
|
|
51
|
-
console.print(f"Latest version: {latest_version}")
|
|
52
|
-
if latest_version and latest_version != current_version:
|
|
53
|
-
console.print(
|
|
54
|
-
f"[bold yellow]A new version of code puppy is available: {latest_version}[/bold yellow]"
|
|
55
|
-
)
|
|
56
|
-
console.print("[bold green]Please consider updating![/bold green]")
|
|
57
|
-
global shutdown_flag
|
|
58
|
-
shutdown_flag = False # ensure this is initialized
|
|
59
|
-
|
|
60
|
-
# Load environment variables from .env file
|
|
61
|
-
load_dotenv()
|
|
62
|
-
|
|
63
|
-
# Set up argument parser
|
|
64
39
|
parser = argparse.ArgumentParser(description="Code Puppy - A code generation agent")
|
|
65
40
|
parser.add_argument(
|
|
66
|
-
"--
|
|
41
|
+
"--version",
|
|
42
|
+
"-v",
|
|
43
|
+
action="version",
|
|
44
|
+
version=f"{__version__}",
|
|
45
|
+
help="Show version and exit",
|
|
46
|
+
)
|
|
47
|
+
parser.add_argument(
|
|
48
|
+
"--interactive",
|
|
49
|
+
"-i",
|
|
50
|
+
action="store_true",
|
|
51
|
+
help="Run in interactive mode",
|
|
52
|
+
)
|
|
53
|
+
parser.add_argument("--tui", "-t", action="store_true", help="Run in TUI mode")
|
|
54
|
+
parser.add_argument(
|
|
55
|
+
"--web",
|
|
56
|
+
"-w",
|
|
57
|
+
action="store_true",
|
|
58
|
+
help="Run in web mode (serves TUI in browser)",
|
|
59
|
+
)
|
|
60
|
+
parser.add_argument(
|
|
61
|
+
"--prompt",
|
|
62
|
+
"-p",
|
|
63
|
+
type=str,
|
|
64
|
+
help="Execute a single prompt and exit (no interactive mode)",
|
|
65
|
+
)
|
|
66
|
+
parser.add_argument(
|
|
67
|
+
"command", nargs="*", help="Run a single command (deprecated, use -p instead)"
|
|
67
68
|
)
|
|
68
|
-
parser.add_argument("command", nargs="*", help="Run a single command")
|
|
69
69
|
args = parser.parse_args()
|
|
70
70
|
|
|
71
|
-
|
|
71
|
+
if args.tui or args.web:
|
|
72
|
+
set_tui_mode(True)
|
|
73
|
+
elif args.interactive or args.command or args.prompt:
|
|
74
|
+
set_tui_mode(False)
|
|
72
75
|
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
+
message_renderer = None
|
|
77
|
+
if not is_tui_mode():
|
|
78
|
+
from rich.console import Console
|
|
79
|
+
|
|
80
|
+
from code_puppy.messaging import (
|
|
81
|
+
SynchronousInteractiveRenderer,
|
|
82
|
+
get_global_queue,
|
|
83
|
+
)
|
|
84
|
+
|
|
85
|
+
message_queue = get_global_queue()
|
|
86
|
+
display_console = Console() # Separate console for rendering messages
|
|
87
|
+
message_renderer = SynchronousInteractiveRenderer(
|
|
88
|
+
message_queue, display_console
|
|
89
|
+
)
|
|
90
|
+
message_renderer.start()
|
|
91
|
+
|
|
92
|
+
if (
|
|
93
|
+
not args.tui
|
|
94
|
+
and not args.interactive
|
|
95
|
+
and not args.web
|
|
96
|
+
and not args.command
|
|
97
|
+
and not args.prompt
|
|
98
|
+
):
|
|
99
|
+
pass
|
|
100
|
+
|
|
101
|
+
initialize_command_history_file()
|
|
102
|
+
if args.web:
|
|
103
|
+
from rich.console import Console
|
|
104
|
+
|
|
105
|
+
direct_console = Console()
|
|
76
106
|
try:
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
"
|
|
107
|
+
# Find an available port for the web server
|
|
108
|
+
available_port = find_available_port()
|
|
109
|
+
if available_port is None:
|
|
110
|
+
direct_console.print(
|
|
111
|
+
"[bold red]Error:[/bold red] No available ports in range 8090-9010!"
|
|
112
|
+
)
|
|
113
|
+
sys.exit(1)
|
|
114
|
+
python_executable = sys.executable
|
|
115
|
+
serve_command = f"{python_executable} -m code_puppy --tui"
|
|
116
|
+
textual_serve_cmd = [
|
|
117
|
+
"textual",
|
|
118
|
+
"serve",
|
|
119
|
+
"-c",
|
|
120
|
+
serve_command,
|
|
121
|
+
"--port",
|
|
122
|
+
str(available_port),
|
|
123
|
+
]
|
|
124
|
+
direct_console.print(
|
|
125
|
+
"[bold blue]🌐 Starting Code Puppy web interface...[/bold blue]"
|
|
126
|
+
)
|
|
127
|
+
direct_console.print(f"[dim]Running: {' '.join(textual_serve_cmd)}[/dim]")
|
|
128
|
+
web_url = f"http://localhost:{available_port}"
|
|
129
|
+
direct_console.print(
|
|
130
|
+
f"[green]Web interface will be available at: {web_url}[/green]"
|
|
88
131
|
)
|
|
132
|
+
direct_console.print("[yellow]Press Ctrl+C to stop the server.[/yellow]\n")
|
|
133
|
+
process = subprocess.Popen(textual_serve_cmd)
|
|
134
|
+
time.sleep(2)
|
|
135
|
+
try:
|
|
136
|
+
direct_console.print(
|
|
137
|
+
"[cyan]🚀 Opening web interface in your default browser...[/cyan]"
|
|
138
|
+
)
|
|
139
|
+
webbrowser.open(web_url)
|
|
140
|
+
direct_console.print("[green]✅ Browser opened successfully![/green]\n")
|
|
141
|
+
except Exception as e:
|
|
142
|
+
direct_console.print(
|
|
143
|
+
f"[yellow]⚠️ Could not automatically open browser: {e}[/yellow]"
|
|
144
|
+
)
|
|
145
|
+
direct_console.print(
|
|
146
|
+
f"[yellow]Please manually open: {web_url}[/yellow]\n"
|
|
147
|
+
)
|
|
148
|
+
result = process.wait()
|
|
149
|
+
sys.exit(result)
|
|
89
150
|
except Exception as e:
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
151
|
+
direct_console.print(
|
|
152
|
+
f"[bold red]Error starting web interface:[/bold red] {str(e)}"
|
|
153
|
+
)
|
|
154
|
+
sys.exit(1)
|
|
155
|
+
from code_puppy.messaging import emit_system_message
|
|
156
|
+
|
|
157
|
+
emit_system_message("🐶 Code Puppy is Loading...")
|
|
158
|
+
|
|
159
|
+
available_port = find_available_port()
|
|
160
|
+
if available_port is None:
|
|
161
|
+
error_msg = "Error: No available ports in range 8090-9010!"
|
|
162
|
+
emit_system_message(f"[bold red]{error_msg}[/bold red]")
|
|
163
|
+
return
|
|
164
|
+
|
|
165
|
+
ensure_config_exists()
|
|
166
|
+
current_version = __version__
|
|
167
|
+
|
|
168
|
+
no_version_update = os.getenv("NO_VERSION_UPDATE", "").lower() in (
|
|
169
|
+
"1",
|
|
170
|
+
"true",
|
|
171
|
+
"yes",
|
|
172
|
+
"on",
|
|
173
|
+
)
|
|
174
|
+
if no_version_update:
|
|
175
|
+
version_msg = f"Current version: {current_version}"
|
|
176
|
+
update_disabled_msg = (
|
|
177
|
+
"Update phase disabled because NO_VERSION_UPDATE is set to 1 or true"
|
|
178
|
+
)
|
|
179
|
+
emit_system_message(version_msg)
|
|
180
|
+
emit_system_message(f"[dim]{update_disabled_msg}[/dim]")
|
|
93
181
|
else:
|
|
94
|
-
|
|
182
|
+
if len(callbacks.get_callbacks("version_check")):
|
|
183
|
+
await callbacks.on_version_check(current_version)
|
|
184
|
+
else:
|
|
185
|
+
default_version_mismatch_behavior(current_version)
|
|
186
|
+
|
|
187
|
+
await callbacks.on_startup()
|
|
188
|
+
|
|
189
|
+
global shutdown_flag
|
|
190
|
+
shutdown_flag = False
|
|
191
|
+
try:
|
|
192
|
+
initial_command = None
|
|
193
|
+
prompt_only_mode = False
|
|
194
|
+
|
|
195
|
+
if args.prompt:
|
|
196
|
+
initial_command = args.prompt
|
|
197
|
+
prompt_only_mode = True
|
|
198
|
+
elif args.command:
|
|
199
|
+
initial_command = " ".join(args.command)
|
|
200
|
+
prompt_only_mode = False
|
|
201
|
+
|
|
202
|
+
if prompt_only_mode:
|
|
203
|
+
await execute_single_prompt(initial_command, message_renderer)
|
|
204
|
+
elif is_tui_mode():
|
|
205
|
+
try:
|
|
206
|
+
from code_puppy.tui import run_textual_ui
|
|
207
|
+
|
|
208
|
+
await run_textual_ui(initial_command=initial_command)
|
|
209
|
+
except ImportError:
|
|
210
|
+
from code_puppy.messaging import emit_error, emit_warning
|
|
211
|
+
|
|
212
|
+
emit_error(
|
|
213
|
+
"Error: Textual UI not available. Install with: pip install textual"
|
|
214
|
+
)
|
|
215
|
+
emit_warning("Falling back to interactive mode...")
|
|
216
|
+
await interactive_mode(message_renderer)
|
|
217
|
+
except Exception as e:
|
|
218
|
+
from code_puppy.messaging import emit_error, emit_warning
|
|
219
|
+
|
|
220
|
+
emit_error(f"TUI Error: {str(e)}")
|
|
221
|
+
emit_warning("Falling back to interactive mode...")
|
|
222
|
+
await interactive_mode(message_renderer)
|
|
223
|
+
elif args.interactive or initial_command:
|
|
224
|
+
await interactive_mode(message_renderer, initial_command=initial_command)
|
|
225
|
+
else:
|
|
226
|
+
await prompt_then_interactive_mode(message_renderer)
|
|
227
|
+
finally:
|
|
228
|
+
if message_renderer:
|
|
229
|
+
message_renderer.stop()
|
|
230
|
+
await callbacks.on_shutdown()
|
|
95
231
|
|
|
96
232
|
|
|
97
233
|
# Add the file handling functionality for interactive mode
|
|
98
|
-
async def interactive_mode(
|
|
99
|
-
from code_puppy.command_line.
|
|
234
|
+
async def interactive_mode(message_renderer, initial_command: str = None) -> None:
|
|
235
|
+
from code_puppy.command_line.command_handler import handle_command
|
|
100
236
|
|
|
101
237
|
"""Run the agent in interactive mode."""
|
|
102
|
-
|
|
103
|
-
console.print("Type 'exit' or 'quit' to exit the interactive mode.")
|
|
104
|
-
console.print("Type 'clear' to reset the conversation history.")
|
|
105
|
-
console.print(
|
|
106
|
-
"Type [bold blue]@[/bold blue] for path completion, or [bold blue]~m[/bold blue] to pick a model."
|
|
107
|
-
)
|
|
238
|
+
from code_puppy.state_management import clear_message_history, get_message_history
|
|
108
239
|
|
|
109
|
-
|
|
110
|
-
|
|
240
|
+
clear_message_history()
|
|
241
|
+
display_console = message_renderer.console
|
|
242
|
+
from code_puppy.messaging import emit_info, emit_system_message
|
|
111
243
|
|
|
112
|
-
|
|
113
|
-
|
|
244
|
+
emit_info("[bold green]Code Puppy[/bold green] - Interactive Mode")
|
|
245
|
+
emit_system_message("Type '/exit' or '/quit' to exit the interactive mode.")
|
|
246
|
+
emit_system_message("Type 'clear' to reset the conversation history.")
|
|
247
|
+
emit_system_message(
|
|
248
|
+
"Type [bold blue]@[/bold blue] for path completion, or [bold blue]/m[/bold blue] to pick a model. Use [bold blue]Esc+Enter[/bold blue] for multi-line input."
|
|
249
|
+
)
|
|
250
|
+
emit_system_message(
|
|
251
|
+
"Press [bold red]Ctrl+C[/bold red] during processing to cancel the current task or inference."
|
|
252
|
+
)
|
|
253
|
+
from code_puppy.command_line.command_handler import COMMANDS_HELP
|
|
254
|
+
|
|
255
|
+
emit_system_message(COMMANDS_HELP)
|
|
114
256
|
try:
|
|
115
257
|
from code_puppy.command_line.motd import print_motd
|
|
116
258
|
|
|
117
259
|
print_motd(console, force=False)
|
|
118
260
|
except Exception as e:
|
|
119
|
-
|
|
261
|
+
from code_puppy.messaging import emit_warning
|
|
262
|
+
|
|
263
|
+
emit_warning(f"MOTD error: {e}")
|
|
264
|
+
from code_puppy.messaging import emit_info
|
|
265
|
+
|
|
266
|
+
emit_info("[bold cyan]Initializing agent...[/bold cyan]")
|
|
267
|
+
get_code_generation_agent()
|
|
268
|
+
if initial_command:
|
|
269
|
+
from code_puppy.messaging import emit_info, emit_system_message
|
|
270
|
+
|
|
271
|
+
emit_info(
|
|
272
|
+
f"[bold blue]Processing initial command:[/bold blue] {initial_command}"
|
|
273
|
+
)
|
|
274
|
+
|
|
275
|
+
try:
|
|
276
|
+
# Get the agent (already loaded above)
|
|
277
|
+
agent = get_code_generation_agent()
|
|
278
|
+
|
|
279
|
+
# Check if any tool is waiting for user input before showing spinner
|
|
280
|
+
try:
|
|
281
|
+
from code_puppy.tools.command_runner import is_awaiting_user_input
|
|
282
|
+
|
|
283
|
+
awaiting_input = is_awaiting_user_input()
|
|
284
|
+
except ImportError:
|
|
285
|
+
awaiting_input = False
|
|
286
|
+
|
|
287
|
+
# Run with or without spinner based on whether we're awaiting input
|
|
288
|
+
if awaiting_input:
|
|
289
|
+
# No spinner - just run the agent
|
|
290
|
+
try:
|
|
291
|
+
async with agent.run_mcp_servers():
|
|
292
|
+
response = await agent.run(
|
|
293
|
+
initial_command, usage_limits=get_custom_usage_limits()
|
|
294
|
+
)
|
|
295
|
+
except Exception as mcp_error:
|
|
296
|
+
from code_puppy.messaging import emit_warning
|
|
297
|
+
|
|
298
|
+
emit_warning(f"MCP server error: {str(mcp_error)}")
|
|
299
|
+
emit_warning("Running without MCP servers...")
|
|
300
|
+
# Run without MCP servers as fallback
|
|
301
|
+
response = await agent.run(
|
|
302
|
+
initial_command, usage_limits=get_custom_usage_limits()
|
|
303
|
+
)
|
|
304
|
+
else:
|
|
305
|
+
# Use our custom spinner for better compatibility with user input
|
|
306
|
+
from code_puppy.messaging.spinner import ConsoleSpinner
|
|
307
|
+
|
|
308
|
+
with ConsoleSpinner(console=display_console):
|
|
309
|
+
try:
|
|
310
|
+
async with agent.run_mcp_servers():
|
|
311
|
+
response = await agent.run(
|
|
312
|
+
initial_command, usage_limits=get_custom_usage_limits()
|
|
313
|
+
)
|
|
314
|
+
except Exception as mcp_error:
|
|
315
|
+
from code_puppy.messaging import emit_warning
|
|
316
|
+
|
|
317
|
+
emit_warning(f"MCP server error: {str(mcp_error)}")
|
|
318
|
+
emit_warning("Running without MCP servers...")
|
|
319
|
+
# Run without MCP servers as fallback
|
|
320
|
+
response = await agent.run(
|
|
321
|
+
initial_command, usage_limits=get_custom_usage_limits()
|
|
322
|
+
)
|
|
323
|
+
|
|
324
|
+
agent_response = response.output
|
|
325
|
+
|
|
326
|
+
emit_system_message(
|
|
327
|
+
f"\n[bold purple]AGENT RESPONSE: [/bold purple]\n{agent_response.output_message}"
|
|
328
|
+
)
|
|
329
|
+
new_msgs = response.all_messages()
|
|
330
|
+
message_history_accumulator(new_msgs)
|
|
331
|
+
|
|
332
|
+
emit_system_message("\n" + "=" * 50)
|
|
333
|
+
emit_info("[bold green]🐶 Continuing in Interactive Mode[/bold green]")
|
|
334
|
+
emit_system_message(
|
|
335
|
+
"Your command and response are preserved in the conversation history."
|
|
336
|
+
)
|
|
337
|
+
emit_system_message("=" * 50 + "\n")
|
|
338
|
+
|
|
339
|
+
except Exception as e:
|
|
340
|
+
from code_puppy.messaging import emit_error
|
|
341
|
+
|
|
342
|
+
emit_error(f"Error processing initial command: {str(e)}")
|
|
120
343
|
|
|
121
344
|
# Check if prompt_toolkit is installed
|
|
122
345
|
try:
|
|
123
|
-
|
|
346
|
+
from code_puppy.messaging import emit_system_message
|
|
124
347
|
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
console.print(
|
|
128
|
-
"[yellow]Warning: prompt_toolkit not installed. Installing now...[/yellow]"
|
|
348
|
+
emit_system_message(
|
|
349
|
+
"[dim]Using prompt_toolkit for enhanced tab completion[/dim]"
|
|
129
350
|
)
|
|
351
|
+
except ImportError:
|
|
352
|
+
from code_puppy.messaging import emit_warning
|
|
353
|
+
|
|
354
|
+
emit_warning("Warning: prompt_toolkit not installed. Installing now...")
|
|
130
355
|
try:
|
|
131
356
|
import subprocess
|
|
132
357
|
|
|
133
358
|
subprocess.check_call(
|
|
134
359
|
[sys.executable, "-m", "pip", "install", "prompt_toolkit"]
|
|
135
360
|
)
|
|
136
|
-
|
|
137
|
-
except Exception as e:
|
|
138
|
-
console.print(f"[bold red]Error installing prompt_toolkit: {e}[/bold red]")
|
|
139
|
-
console.print(
|
|
140
|
-
"[yellow]Falling back to basic input without tab completion[/yellow]"
|
|
141
|
-
)
|
|
361
|
+
from code_puppy.messaging import emit_success
|
|
142
362
|
|
|
143
|
-
|
|
144
|
-
history_file_path_prompt = os.path.expanduser("~/.code_puppy_history.txt")
|
|
145
|
-
history_dir = os.path.dirname(history_file_path_prompt)
|
|
146
|
-
|
|
147
|
-
# Ensure history directory exists
|
|
148
|
-
if history_dir and not os.path.exists(history_dir):
|
|
149
|
-
try:
|
|
150
|
-
os.makedirs(history_dir, exist_ok=True)
|
|
363
|
+
emit_success("Successfully installed prompt_toolkit")
|
|
151
364
|
except Exception as e:
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
)
|
|
365
|
+
from code_puppy.messaging import emit_error, emit_warning
|
|
366
|
+
|
|
367
|
+
emit_error(f"Error installing prompt_toolkit: {e}")
|
|
368
|
+
emit_warning("Falling back to basic input without tab completion")
|
|
155
369
|
|
|
156
370
|
while True:
|
|
157
|
-
|
|
371
|
+
from code_puppy.messaging import emit_info
|
|
372
|
+
|
|
373
|
+
emit_info("[bold blue]Enter your coding task:[/bold blue]")
|
|
158
374
|
|
|
159
375
|
try:
|
|
160
376
|
# Use prompt_toolkit for enhanced input with path completion
|
|
161
377
|
try:
|
|
162
378
|
# Use the async version of get_input_with_combined_completion
|
|
163
379
|
task = await get_input_with_combined_completion(
|
|
164
|
-
get_prompt_with_active_model(),
|
|
165
|
-
history_file=history_file_path_prompt,
|
|
380
|
+
get_prompt_with_active_model(), history_file=COMMAND_HISTORY_FILE
|
|
166
381
|
)
|
|
167
382
|
except ImportError:
|
|
168
383
|
# Fall back to basic input if prompt_toolkit is not available
|
|
@@ -170,317 +385,184 @@ async def interactive_mode(history_file_path: str) -> None:
|
|
|
170
385
|
|
|
171
386
|
except (KeyboardInterrupt, EOFError):
|
|
172
387
|
# Handle Ctrl+C or Ctrl+D
|
|
173
|
-
|
|
388
|
+
from code_puppy.messaging import emit_warning
|
|
389
|
+
|
|
390
|
+
emit_warning("\nInput cancelled")
|
|
174
391
|
continue
|
|
175
392
|
|
|
176
|
-
# Check for exit commands
|
|
177
|
-
if task.strip().lower() in ["exit", "quit"]
|
|
178
|
-
|
|
393
|
+
# Check for exit commands (plain text or command form)
|
|
394
|
+
if task.strip().lower() in ["exit", "quit"] or task.strip().lower() in [
|
|
395
|
+
"/exit",
|
|
396
|
+
"/quit",
|
|
397
|
+
]:
|
|
398
|
+
from code_puppy.messaging import emit_success
|
|
399
|
+
|
|
400
|
+
emit_success("Goodbye!")
|
|
401
|
+
# The renderer is stopped in the finally block of main().
|
|
179
402
|
break
|
|
180
403
|
|
|
181
|
-
# Check for clear command (supports both `clear` and
|
|
182
|
-
if task.strip().lower() in ("clear", "
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
)
|
|
404
|
+
# Check for clear command (supports both `clear` and `/clear`)
|
|
405
|
+
if task.strip().lower() in ("clear", "/clear"):
|
|
406
|
+
clear_message_history()
|
|
407
|
+
from code_puppy.messaging import emit_system_message, emit_warning
|
|
408
|
+
|
|
409
|
+
emit_warning("Conversation history cleared!")
|
|
410
|
+
emit_system_message("The agent will not remember previous interactions.\n")
|
|
188
411
|
continue
|
|
189
412
|
|
|
190
|
-
# Handle
|
|
191
|
-
if task.strip().startswith("
|
|
192
|
-
|
|
413
|
+
# Handle / commands before anything else
|
|
414
|
+
if task.strip().startswith("/"):
|
|
415
|
+
command_result = handle_command(task.strip())
|
|
416
|
+
if command_result is True:
|
|
193
417
|
continue
|
|
194
|
-
|
|
195
|
-
|
|
418
|
+
elif isinstance(command_result, str):
|
|
419
|
+
# Command returned a prompt to execute
|
|
420
|
+
task = command_result
|
|
421
|
+
elif command_result is False:
|
|
422
|
+
# Command not recognized, continue with normal processing
|
|
423
|
+
pass
|
|
196
424
|
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
425
|
+
if task.strip():
|
|
426
|
+
# Write to the secret file for permanent history with timestamp
|
|
427
|
+
save_command_to_history(task)
|
|
200
428
|
|
|
201
429
|
try:
|
|
202
430
|
prettier_code_blocks()
|
|
203
|
-
local_cancelled = False
|
|
204
|
-
|
|
205
|
-
# Initialize status display for tokens per second and loading messages
|
|
206
|
-
status_display = StatusDisplay(console)
|
|
207
431
|
|
|
208
|
-
#
|
|
209
|
-
|
|
432
|
+
# Store agent's full response
|
|
433
|
+
agent_response = None
|
|
210
434
|
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
Track real token counts from message history.
|
|
214
|
-
|
|
215
|
-
This async function runs in the background and periodically checks
|
|
216
|
-
the message history for new tokens. When new tokens are detected,
|
|
217
|
-
it updates the StatusDisplay with the incremental count to calculate
|
|
218
|
-
an accurate tokens-per-second rate.
|
|
435
|
+
# Get the agent (uses cached version from early initialization)
|
|
436
|
+
agent = get_code_generation_agent()
|
|
219
437
|
|
|
220
|
-
|
|
221
|
-
|
|
438
|
+
# Use our custom spinner for better compatibility with user input
|
|
439
|
+
from code_puppy.messaging import emit_warning
|
|
440
|
+
from code_puppy.messaging.spinner import ConsoleSpinner
|
|
222
441
|
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
from code_puppy.message_history_processor import (
|
|
226
|
-
estimate_tokens_for_message,
|
|
227
|
-
)
|
|
228
|
-
import json
|
|
229
|
-
import re
|
|
230
|
-
|
|
231
|
-
last_token_total = 0
|
|
232
|
-
last_sse_data = None
|
|
233
|
-
|
|
234
|
-
while status_display.is_active:
|
|
235
|
-
# Get real token count from message history
|
|
236
|
-
messages = get_message_history()
|
|
237
|
-
if messages:
|
|
238
|
-
# Calculate total tokens across all messages
|
|
239
|
-
current_token_total = sum(
|
|
240
|
-
estimate_tokens_for_message(msg) for msg in messages
|
|
241
|
-
)
|
|
442
|
+
# Create a simple flag to track cancellation locally
|
|
443
|
+
local_cancelled = False
|
|
242
444
|
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
445
|
+
# Run with spinner
|
|
446
|
+
with ConsoleSpinner(console=display_console):
|
|
447
|
+
# Use a separate asyncio task that we can cancel
|
|
448
|
+
async def run_agent_task():
|
|
449
|
+
try:
|
|
450
|
+
async with agent.run_mcp_servers():
|
|
451
|
+
return await agent.run(
|
|
452
|
+
task,
|
|
453
|
+
message_history=get_message_history(),
|
|
454
|
+
usage_limits=get_custom_usage_limits(),
|
|
247
455
|
)
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
elif (
|
|
259
|
-
isinstance(msg, dict)
|
|
260
|
-
and msg.get("role") == "assistant"
|
|
261
|
-
):
|
|
262
|
-
# Dictionary with 'role' key
|
|
263
|
-
content = msg.get("content", "")
|
|
264
|
-
# Support for ModelRequest/ModelResponse objects
|
|
265
|
-
elif (
|
|
266
|
-
hasattr(msg, "message")
|
|
267
|
-
and hasattr(msg.message, "role")
|
|
268
|
-
and msg.message.role == "assistant"
|
|
269
|
-
):
|
|
270
|
-
# Access content through the message attribute
|
|
271
|
-
content = (
|
|
272
|
-
msg.message.content
|
|
273
|
-
if hasattr(msg.message, "content")
|
|
274
|
-
else ""
|
|
275
|
-
)
|
|
276
|
-
else:
|
|
277
|
-
# Skip if not an assistant message or unrecognized format
|
|
278
|
-
continue
|
|
279
|
-
|
|
280
|
-
# Convert content to string if it's not already
|
|
281
|
-
if not isinstance(content, str):
|
|
282
|
-
try:
|
|
283
|
-
content = str(content)
|
|
284
|
-
except Exception:
|
|
285
|
-
continue
|
|
286
|
-
|
|
287
|
-
# Look for SSE usage data pattern in the message content
|
|
288
|
-
sse_matches = re.findall(
|
|
289
|
-
r'\{\s*"usage".*?"time_info".*?\}',
|
|
290
|
-
content,
|
|
291
|
-
re.DOTALL,
|
|
292
|
-
)
|
|
293
|
-
for match in sse_matches:
|
|
294
|
-
try:
|
|
295
|
-
# Parse the JSON data
|
|
296
|
-
sse_data = json.loads(match)
|
|
297
|
-
if (
|
|
298
|
-
sse_data != last_sse_data
|
|
299
|
-
): # Only process new data
|
|
300
|
-
# Check if we have time_info and completion_tokens
|
|
301
|
-
if (
|
|
302
|
-
"time_info" in sse_data
|
|
303
|
-
and "completion_time"
|
|
304
|
-
in sse_data["time_info"]
|
|
305
|
-
and "usage" in sse_data
|
|
306
|
-
and "completion_tokens"
|
|
307
|
-
in sse_data["usage"]
|
|
308
|
-
):
|
|
309
|
-
completion_time = float(
|
|
310
|
-
sse_data["time_info"][
|
|
311
|
-
"completion_time"
|
|
312
|
-
]
|
|
313
|
-
)
|
|
314
|
-
completion_tokens = int(
|
|
315
|
-
sse_data["usage"][
|
|
316
|
-
"completion_tokens"
|
|
317
|
-
]
|
|
318
|
-
)
|
|
319
|
-
|
|
320
|
-
# Update rate using the accurate SSE data
|
|
321
|
-
if (
|
|
322
|
-
completion_time > 0
|
|
323
|
-
and completion_tokens > 0
|
|
324
|
-
):
|
|
325
|
-
status_display.update_rate_from_sse(
|
|
326
|
-
completion_tokens,
|
|
327
|
-
completion_time,
|
|
328
|
-
)
|
|
329
|
-
last_sse_data = sse_data
|
|
330
|
-
except (json.JSONDecodeError, KeyError, ValueError):
|
|
331
|
-
# Ignore parsing errors and continue
|
|
332
|
-
pass
|
|
333
|
-
|
|
334
|
-
# Small sleep interval for responsive updates without excessive CPU usage
|
|
335
|
-
await asyncio.sleep(0.1)
|
|
336
|
-
|
|
337
|
-
async def wrap_agent_run(original_run, *args, **kwargs):
|
|
338
|
-
"""
|
|
339
|
-
Wraps the agent's run method to enable token tracking.
|
|
340
|
-
|
|
341
|
-
This wrapper preserves the original functionality while allowing
|
|
342
|
-
us to track tokens as they are generated by the model. No additional
|
|
343
|
-
logic is needed here since the token tracking happens in a separate task.
|
|
344
|
-
|
|
345
|
-
Args:
|
|
346
|
-
original_run: The original agent.run method
|
|
347
|
-
*args, **kwargs: Arguments to pass to the original run method
|
|
348
|
-
|
|
349
|
-
Returns:
|
|
350
|
-
The result from the original run method
|
|
351
|
-
"""
|
|
352
|
-
result = await original_run(*args, **kwargs)
|
|
353
|
-
return result
|
|
354
|
-
|
|
355
|
-
async def run_agent_task():
|
|
356
|
-
"""
|
|
357
|
-
Main task runner for the agent with token tracking.
|
|
358
|
-
|
|
359
|
-
This function:
|
|
360
|
-
1. Sets up the agent with token tracking
|
|
361
|
-
2. Starts the status display showing token rate
|
|
362
|
-
3. Runs the agent with the user's task
|
|
363
|
-
4. Ensures proper cleanup of all resources
|
|
364
|
-
|
|
365
|
-
Returns the agent's result or raises any exceptions that occurred.
|
|
366
|
-
"""
|
|
367
|
-
# Token tracking task reference for cleanup
|
|
368
|
-
token_tracking_task = None
|
|
456
|
+
except Exception as mcp_error:
|
|
457
|
+
# Handle MCP server errors
|
|
458
|
+
emit_warning(f"MCP server error: {str(mcp_error)}")
|
|
459
|
+
emit_warning("Running without MCP servers...")
|
|
460
|
+
# Run without MCP servers as fallback
|
|
461
|
+
return await agent.run(
|
|
462
|
+
task,
|
|
463
|
+
message_history=get_message_history(),
|
|
464
|
+
usage_limits=get_custom_usage_limits(),
|
|
465
|
+
)
|
|
369
466
|
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
agent = get_code_generation_agent()
|
|
467
|
+
# Create the task
|
|
468
|
+
agent_task = asyncio.create_task(run_agent_task())
|
|
373
469
|
|
|
374
|
-
|
|
375
|
-
|
|
470
|
+
# Set up signal handling for Ctrl+C
|
|
471
|
+
import signal
|
|
376
472
|
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
)
|
|
381
|
-
|
|
382
|
-
# Create a wrapper for the agent's run method
|
|
383
|
-
original_run = agent.run
|
|
473
|
+
from code_puppy.tools.command_runner import (
|
|
474
|
+
kill_all_running_shell_processes,
|
|
475
|
+
)
|
|
384
476
|
|
|
385
|
-
|
|
386
|
-
|
|
477
|
+
original_handler = None
|
|
478
|
+
|
|
479
|
+
# Ensure the interrupt handler only acts once per task
|
|
480
|
+
handled = False
|
|
481
|
+
|
|
482
|
+
def keyboard_interrupt_handler(sig, frame):
|
|
483
|
+
nonlocal local_cancelled
|
|
484
|
+
nonlocal handled
|
|
485
|
+
if handled:
|
|
486
|
+
return
|
|
487
|
+
handled = True
|
|
488
|
+
# First, nuke any running shell processes triggered by tools
|
|
489
|
+
try:
|
|
490
|
+
killed = kill_all_running_shell_processes()
|
|
491
|
+
if killed:
|
|
492
|
+
from code_puppy.messaging import emit_warning
|
|
493
|
+
|
|
494
|
+
emit_warning(
|
|
495
|
+
f"Cancelled {killed} running shell process(es)."
|
|
496
|
+
)
|
|
497
|
+
else:
|
|
498
|
+
# Then cancel the agent task
|
|
499
|
+
if not agent_task.done():
|
|
500
|
+
state_management._message_history = (
|
|
501
|
+
prune_interrupted_tool_calls(
|
|
502
|
+
state_management._message_history
|
|
503
|
+
)
|
|
504
|
+
)
|
|
505
|
+
agent_task.cancel()
|
|
506
|
+
local_cancelled = True
|
|
507
|
+
except Exception as e:
|
|
508
|
+
from code_puppy.messaging import emit_warning
|
|
387
509
|
|
|
388
|
-
|
|
510
|
+
emit_warning(f"Shell kill error: {e}")
|
|
511
|
+
# Don't call the original handler
|
|
512
|
+
# This prevents the application from exiting
|
|
389
513
|
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
514
|
+
try:
|
|
515
|
+
# Save original handler and set our custom one
|
|
516
|
+
original_handler = signal.getsignal(signal.SIGINT)
|
|
517
|
+
signal.signal(signal.SIGINT, keyboard_interrupt_handler)
|
|
518
|
+
|
|
519
|
+
# Wait for the task to complete or be cancelled
|
|
520
|
+
result = await agent_task
|
|
521
|
+
except asyncio.CancelledError:
|
|
522
|
+
# Task was cancelled by our handler
|
|
523
|
+
pass
|
|
399
524
|
finally:
|
|
400
|
-
#
|
|
401
|
-
if
|
|
402
|
-
|
|
403
|
-
if token_tracking_task and not token_tracking_task.done():
|
|
404
|
-
token_tracking_task.cancel()
|
|
405
|
-
if not agent_task.done():
|
|
406
|
-
set_message_history(
|
|
407
|
-
message_history_processor(get_message_history())
|
|
408
|
-
)
|
|
409
|
-
|
|
410
|
-
agent_task = asyncio.create_task(run_agent_task())
|
|
411
|
-
|
|
412
|
-
import signal
|
|
413
|
-
from code_puppy.tools import kill_all_running_shell_processes
|
|
525
|
+
# Restore original signal handler
|
|
526
|
+
if original_handler:
|
|
527
|
+
signal.signal(signal.SIGINT, original_handler)
|
|
414
528
|
|
|
415
|
-
|
|
529
|
+
# Check if the task was cancelled
|
|
530
|
+
if local_cancelled:
|
|
531
|
+
emit_warning("\n⚠️ Processing cancelled by user (Ctrl+C)")
|
|
532
|
+
# Skip the rest of this loop iteration
|
|
533
|
+
continue
|
|
534
|
+
# Get the structured response
|
|
535
|
+
agent_response = result.output
|
|
536
|
+
from code_puppy.messaging import emit_info
|
|
537
|
+
|
|
538
|
+
emit_system_message(
|
|
539
|
+
f"\n[bold purple]AGENT RESPONSE: [/bold purple]\n{agent_response}"
|
|
540
|
+
)
|
|
416
541
|
|
|
417
|
-
#
|
|
418
|
-
|
|
542
|
+
# Update message history - the agent's history processor will handle truncation
|
|
543
|
+
new_msgs = result.all_messages()
|
|
544
|
+
message_history_accumulator(new_msgs)
|
|
419
545
|
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
nonlocal handled
|
|
423
|
-
if handled:
|
|
424
|
-
return
|
|
425
|
-
handled = True
|
|
426
|
-
# First, nuke any running shell processes triggered by tools
|
|
427
|
-
try:
|
|
428
|
-
killed = kill_all_running_shell_processes()
|
|
429
|
-
if killed:
|
|
430
|
-
console.print(
|
|
431
|
-
f"[yellow]Cancelled {killed} running shell process(es).[/yellow]"
|
|
432
|
-
)
|
|
433
|
-
else:
|
|
434
|
-
# Then cancel the agent task
|
|
435
|
-
if not agent_task.done():
|
|
436
|
-
agent_task.cancel()
|
|
437
|
-
local_cancelled = True
|
|
438
|
-
except Exception as e:
|
|
439
|
-
console.print(f"[dim]Shell kill error: {e}[/dim]")
|
|
440
|
-
# On Windows, we need to reset the signal handler to avoid weird terminal behavior
|
|
441
|
-
if sys.platform.startswith("win"):
|
|
442
|
-
signal.signal(signal.SIGINT, original_handler or signal.SIG_DFL)
|
|
546
|
+
# Show context status
|
|
547
|
+
from code_puppy.messaging import emit_system_message
|
|
443
548
|
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
result = await agent_task
|
|
448
|
-
except asyncio.CancelledError:
|
|
449
|
-
pass
|
|
450
|
-
except KeyboardInterrupt:
|
|
451
|
-
# Handle Ctrl+C from terminal
|
|
452
|
-
keyboard_interrupt_handler(signal.SIGINT, None)
|
|
453
|
-
raise
|
|
454
|
-
finally:
|
|
455
|
-
if original_handler:
|
|
456
|
-
signal.signal(signal.SIGINT, original_handler)
|
|
549
|
+
emit_system_message(
|
|
550
|
+
f"Context: {len(get_message_history())} messages in history\n"
|
|
551
|
+
)
|
|
457
552
|
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
if result is not None and hasattr(result, "output"):
|
|
465
|
-
agent_response = result.output
|
|
466
|
-
console.print(agent_response)
|
|
467
|
-
filtered = message_history_processor(get_message_history())
|
|
468
|
-
set_message_history(filtered)
|
|
469
|
-
else:
|
|
470
|
-
console.print(
|
|
471
|
-
"[yellow]No result received from the agent[/yellow]"
|
|
472
|
-
)
|
|
473
|
-
# Still process history if possible
|
|
474
|
-
filtered = message_history_processor(get_message_history())
|
|
475
|
-
set_message_history(filtered)
|
|
553
|
+
# Ensure console output is flushed before next prompt
|
|
554
|
+
# This fixes the issue where prompt doesn't appear after agent response
|
|
555
|
+
display_console.file.flush() if hasattr(
|
|
556
|
+
display_console.file, "flush"
|
|
557
|
+
) else None
|
|
558
|
+
import time
|
|
476
559
|
|
|
477
|
-
#
|
|
478
|
-
console.print(
|
|
479
|
-
f"[dim]Context: {len(get_message_history())} messages in history[/dim]\n"
|
|
480
|
-
)
|
|
560
|
+
time.sleep(0.1) # Brief pause to ensure all messages are rendered
|
|
481
561
|
|
|
482
562
|
except Exception:
|
|
483
|
-
|
|
563
|
+
from code_puppy.messaging.queue_console import get_queue_console
|
|
564
|
+
|
|
565
|
+
get_queue_console().print_exception()
|
|
484
566
|
|
|
485
567
|
|
|
486
568
|
def prettier_code_blocks():
|
|
@@ -503,9 +585,114 @@ def prettier_code_blocks():
|
|
|
503
585
|
Markdown.elements["fence"] = SimpleCodeBlock
|
|
504
586
|
|
|
505
587
|
|
|
588
|
+
async def execute_single_prompt(prompt: str, message_renderer) -> None:
|
|
589
|
+
"""Execute a single prompt and exit (for -p flag)."""
|
|
590
|
+
from code_puppy.messaging import emit_info, emit_system_message
|
|
591
|
+
|
|
592
|
+
emit_info(f"[bold blue]Executing prompt:[/bold blue] {prompt}")
|
|
593
|
+
|
|
594
|
+
try:
|
|
595
|
+
# Get the agent
|
|
596
|
+
agent = get_code_generation_agent()
|
|
597
|
+
|
|
598
|
+
# Use our custom spinner for better compatibility with user input
|
|
599
|
+
from code_puppy.messaging.spinner import ConsoleSpinner
|
|
600
|
+
|
|
601
|
+
display_console = message_renderer.console
|
|
602
|
+
with ConsoleSpinner(console=display_console):
|
|
603
|
+
try:
|
|
604
|
+
async with agent.run_mcp_servers():
|
|
605
|
+
response = await agent.run(
|
|
606
|
+
prompt, usage_limits=get_custom_usage_limits()
|
|
607
|
+
)
|
|
608
|
+
except Exception as mcp_error:
|
|
609
|
+
from code_puppy.messaging import emit_warning
|
|
610
|
+
|
|
611
|
+
emit_warning(f"MCP server error: {str(mcp_error)}")
|
|
612
|
+
emit_warning("Running without MCP servers...")
|
|
613
|
+
# Run without MCP servers as fallback
|
|
614
|
+
response = await agent.run(
|
|
615
|
+
prompt, usage_limits=get_custom_usage_limits()
|
|
616
|
+
)
|
|
617
|
+
|
|
618
|
+
agent_response = response.output
|
|
619
|
+
emit_system_message(
|
|
620
|
+
f"\n[bold purple]AGENT RESPONSE: [/bold purple]\n{agent_response}"
|
|
621
|
+
)
|
|
622
|
+
|
|
623
|
+
except Exception as e:
|
|
624
|
+
from code_puppy.messaging import emit_error
|
|
625
|
+
|
|
626
|
+
emit_error(f"Error executing prompt: {str(e)}")
|
|
627
|
+
|
|
628
|
+
|
|
629
|
+
async def prompt_then_interactive_mode(message_renderer) -> None:
|
|
630
|
+
"""Prompt user for input, execute it, then continue in interactive mode."""
|
|
631
|
+
from code_puppy.messaging import emit_info, emit_system_message
|
|
632
|
+
|
|
633
|
+
emit_info("[bold green]🐶 Code Puppy[/bold green] - Enter your request")
|
|
634
|
+
emit_system_message(
|
|
635
|
+
"After processing your request, you'll continue in interactive mode."
|
|
636
|
+
)
|
|
637
|
+
|
|
638
|
+
try:
|
|
639
|
+
# Get user input
|
|
640
|
+
from code_puppy.command_line.prompt_toolkit_completion import (
|
|
641
|
+
get_input_with_combined_completion,
|
|
642
|
+
get_prompt_with_active_model,
|
|
643
|
+
)
|
|
644
|
+
from code_puppy.config import COMMAND_HISTORY_FILE
|
|
645
|
+
|
|
646
|
+
emit_info("[bold blue]What would you like me to help you with?[/bold blue]")
|
|
647
|
+
|
|
648
|
+
try:
|
|
649
|
+
# Use prompt_toolkit for enhanced input with path completion
|
|
650
|
+
user_prompt = await get_input_with_combined_completion(
|
|
651
|
+
get_prompt_with_active_model(), history_file=COMMAND_HISTORY_FILE
|
|
652
|
+
)
|
|
653
|
+
except ImportError:
|
|
654
|
+
# Fall back to basic input if prompt_toolkit is not available
|
|
655
|
+
user_prompt = input(">>> ")
|
|
656
|
+
|
|
657
|
+
if user_prompt.strip():
|
|
658
|
+
# Execute the prompt
|
|
659
|
+
await execute_single_prompt(user_prompt, message_renderer)
|
|
660
|
+
|
|
661
|
+
# Transition to interactive mode
|
|
662
|
+
emit_system_message("\n" + "=" * 50)
|
|
663
|
+
emit_info("[bold green]🐶 Continuing in Interactive Mode[/bold green]")
|
|
664
|
+
emit_system_message(
|
|
665
|
+
"Your request and response are preserved in the conversation history."
|
|
666
|
+
)
|
|
667
|
+
emit_system_message("=" * 50 + "\n")
|
|
668
|
+
|
|
669
|
+
# Continue in interactive mode with the initial command as history
|
|
670
|
+
await interactive_mode(message_renderer, initial_command=user_prompt)
|
|
671
|
+
else:
|
|
672
|
+
# No input provided, just go to interactive mode
|
|
673
|
+
await interactive_mode(message_renderer)
|
|
674
|
+
|
|
675
|
+
except (KeyboardInterrupt, EOFError):
|
|
676
|
+
from code_puppy.messaging import emit_warning
|
|
677
|
+
|
|
678
|
+
emit_warning("\nInput cancelled. Starting interactive mode...")
|
|
679
|
+
await interactive_mode(message_renderer)
|
|
680
|
+
except Exception as e:
|
|
681
|
+
from code_puppy.messaging import emit_error
|
|
682
|
+
|
|
683
|
+
emit_error(f"Error in prompt mode: {str(e)}")
|
|
684
|
+
emit_info("Falling back to interactive mode...")
|
|
685
|
+
await interactive_mode(message_renderer)
|
|
686
|
+
|
|
687
|
+
|
|
506
688
|
def main_entry():
|
|
507
689
|
"""Entry point for the installed CLI tool."""
|
|
508
|
-
|
|
690
|
+
try:
|
|
691
|
+
asyncio.run(main())
|
|
692
|
+
except KeyboardInterrupt:
|
|
693
|
+
# Just exit gracefully with no error message
|
|
694
|
+
callbacks.on_shutdown()
|
|
695
|
+
return 0
|
|
509
696
|
|
|
510
697
|
|
|
511
698
|
if __name__ == "__main__":
|