code-puppy 0.0.214__py3-none-any.whl → 0.0.366__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 +7 -1
- code_puppy/agents/__init__.py +2 -0
- code_puppy/agents/agent_c_reviewer.py +59 -6
- code_puppy/agents/agent_code_puppy.py +7 -1
- code_puppy/agents/agent_code_reviewer.py +12 -2
- code_puppy/agents/agent_cpp_reviewer.py +73 -6
- code_puppy/agents/agent_creator_agent.py +45 -4
- code_puppy/agents/agent_golang_reviewer.py +92 -3
- code_puppy/agents/agent_javascript_reviewer.py +101 -8
- code_puppy/agents/agent_manager.py +81 -4
- code_puppy/agents/agent_pack_leader.py +383 -0
- code_puppy/agents/agent_planning.py +163 -0
- code_puppy/agents/agent_python_programmer.py +165 -0
- code_puppy/agents/agent_python_reviewer.py +28 -6
- code_puppy/agents/agent_qa_expert.py +98 -6
- code_puppy/agents/agent_qa_kitten.py +12 -7
- code_puppy/agents/agent_security_auditor.py +113 -3
- code_puppy/agents/agent_terminal_qa.py +323 -0
- code_puppy/agents/agent_typescript_reviewer.py +106 -7
- code_puppy/agents/base_agent.py +802 -176
- code_puppy/agents/event_stream_handler.py +350 -0
- code_puppy/agents/pack/__init__.py +34 -0
- code_puppy/agents/pack/bloodhound.py +304 -0
- code_puppy/agents/pack/husky.py +321 -0
- code_puppy/agents/pack/retriever.py +393 -0
- code_puppy/agents/pack/shepherd.py +348 -0
- code_puppy/agents/pack/terrier.py +287 -0
- code_puppy/agents/pack/watchdog.py +367 -0
- code_puppy/agents/prompt_reviewer.py +145 -0
- code_puppy/agents/subagent_stream_handler.py +276 -0
- code_puppy/api/__init__.py +13 -0
- code_puppy/api/app.py +169 -0
- code_puppy/api/main.py +21 -0
- code_puppy/api/pty_manager.py +446 -0
- code_puppy/api/routers/__init__.py +12 -0
- code_puppy/api/routers/agents.py +36 -0
- code_puppy/api/routers/commands.py +217 -0
- code_puppy/api/routers/config.py +74 -0
- code_puppy/api/routers/sessions.py +232 -0
- code_puppy/api/templates/terminal.html +361 -0
- code_puppy/api/websocket.py +154 -0
- code_puppy/callbacks.py +142 -4
- code_puppy/chatgpt_codex_client.py +283 -0
- code_puppy/claude_cache_client.py +586 -0
- code_puppy/cli_runner.py +916 -0
- code_puppy/command_line/add_model_menu.py +1079 -0
- code_puppy/command_line/agent_menu.py +395 -0
- code_puppy/command_line/attachments.py +10 -5
- code_puppy/command_line/autosave_menu.py +605 -0
- code_puppy/command_line/clipboard.py +527 -0
- code_puppy/command_line/colors_menu.py +520 -0
- code_puppy/command_line/command_handler.py +176 -738
- code_puppy/command_line/command_registry.py +150 -0
- code_puppy/command_line/config_commands.py +715 -0
- code_puppy/command_line/core_commands.py +792 -0
- code_puppy/command_line/diff_menu.py +863 -0
- code_puppy/command_line/load_context_completion.py +15 -22
- code_puppy/command_line/mcp/base.py +0 -3
- code_puppy/command_line/mcp/catalog_server_installer.py +175 -0
- code_puppy/command_line/mcp/custom_server_form.py +688 -0
- code_puppy/command_line/mcp/custom_server_installer.py +195 -0
- code_puppy/command_line/mcp/edit_command.py +148 -0
- code_puppy/command_line/mcp/handler.py +9 -4
- code_puppy/command_line/mcp/help_command.py +6 -5
- code_puppy/command_line/mcp/install_command.py +15 -26
- code_puppy/command_line/mcp/install_menu.py +685 -0
- code_puppy/command_line/mcp/list_command.py +2 -2
- code_puppy/command_line/mcp/logs_command.py +174 -65
- code_puppy/command_line/mcp/remove_command.py +2 -2
- code_puppy/command_line/mcp/restart_command.py +12 -4
- code_puppy/command_line/mcp/search_command.py +16 -10
- code_puppy/command_line/mcp/start_all_command.py +18 -6
- code_puppy/command_line/mcp/start_command.py +47 -25
- code_puppy/command_line/mcp/status_command.py +4 -5
- code_puppy/command_line/mcp/stop_all_command.py +7 -1
- code_puppy/command_line/mcp/stop_command.py +8 -4
- code_puppy/command_line/mcp/test_command.py +2 -2
- code_puppy/command_line/mcp/wizard_utils.py +20 -16
- code_puppy/command_line/mcp_completion.py +174 -0
- code_puppy/command_line/model_picker_completion.py +75 -25
- code_puppy/command_line/model_settings_menu.py +884 -0
- code_puppy/command_line/motd.py +14 -8
- code_puppy/command_line/onboarding_slides.py +179 -0
- code_puppy/command_line/onboarding_wizard.py +340 -0
- code_puppy/command_line/pin_command_completion.py +329 -0
- code_puppy/command_line/prompt_toolkit_completion.py +463 -63
- code_puppy/command_line/session_commands.py +296 -0
- code_puppy/command_line/utils.py +54 -0
- code_puppy/config.py +898 -112
- code_puppy/error_logging.py +118 -0
- code_puppy/gemini_code_assist.py +385 -0
- code_puppy/gemini_model.py +602 -0
- code_puppy/http_utils.py +210 -148
- code_puppy/keymap.py +128 -0
- code_puppy/main.py +5 -698
- code_puppy/mcp_/__init__.py +17 -0
- code_puppy/mcp_/async_lifecycle.py +35 -4
- code_puppy/mcp_/blocking_startup.py +70 -43
- code_puppy/mcp_/captured_stdio_server.py +2 -2
- code_puppy/mcp_/config_wizard.py +4 -4
- code_puppy/mcp_/dashboard.py +15 -6
- code_puppy/mcp_/managed_server.py +65 -38
- code_puppy/mcp_/manager.py +146 -52
- code_puppy/mcp_/mcp_logs.py +224 -0
- code_puppy/mcp_/registry.py +6 -6
- code_puppy/mcp_/server_registry_catalog.py +24 -5
- code_puppy/messaging/__init__.py +199 -2
- code_puppy/messaging/bus.py +610 -0
- code_puppy/messaging/commands.py +167 -0
- code_puppy/messaging/markdown_patches.py +57 -0
- code_puppy/messaging/message_queue.py +17 -48
- code_puppy/messaging/messages.py +500 -0
- code_puppy/messaging/queue_console.py +1 -24
- code_puppy/messaging/renderers.py +43 -146
- code_puppy/messaging/rich_renderer.py +1027 -0
- code_puppy/messaging/spinner/__init__.py +21 -5
- code_puppy/messaging/spinner/console_spinner.py +86 -51
- code_puppy/messaging/subagent_console.py +461 -0
- code_puppy/model_factory.py +634 -83
- code_puppy/model_utils.py +167 -0
- code_puppy/models.json +66 -68
- code_puppy/models_dev_api.json +1 -0
- code_puppy/models_dev_parser.py +592 -0
- code_puppy/plugins/__init__.py +164 -10
- 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 +704 -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 +767 -0
- code_puppy/plugins/antigravity_oauth/utils.py +169 -0
- code_puppy/plugins/chatgpt_oauth/__init__.py +8 -0
- code_puppy/plugins/chatgpt_oauth/config.py +52 -0
- code_puppy/plugins/chatgpt_oauth/oauth_flow.py +328 -0
- code_puppy/plugins/chatgpt_oauth/register_callbacks.py +94 -0
- code_puppy/plugins/chatgpt_oauth/test_plugin.py +293 -0
- code_puppy/plugins/chatgpt_oauth/utils.py +489 -0
- code_puppy/plugins/claude_code_oauth/README.md +167 -0
- code_puppy/plugins/claude_code_oauth/SETUP.md +93 -0
- code_puppy/plugins/claude_code_oauth/__init__.py +6 -0
- code_puppy/plugins/claude_code_oauth/config.py +50 -0
- code_puppy/plugins/claude_code_oauth/register_callbacks.py +308 -0
- code_puppy/plugins/claude_code_oauth/test_plugin.py +283 -0
- code_puppy/plugins/claude_code_oauth/utils.py +518 -0
- code_puppy/plugins/customizable_commands/__init__.py +0 -0
- code_puppy/plugins/customizable_commands/register_callbacks.py +169 -0
- code_puppy/plugins/example_custom_command/README.md +280 -0
- code_puppy/plugins/example_custom_command/register_callbacks.py +2 -2
- code_puppy/plugins/file_permission_handler/__init__.py +4 -0
- code_puppy/plugins/file_permission_handler/register_callbacks.py +523 -0
- code_puppy/plugins/frontend_emitter/__init__.py +25 -0
- code_puppy/plugins/frontend_emitter/emitter.py +121 -0
- code_puppy/plugins/frontend_emitter/register_callbacks.py +261 -0
- code_puppy/plugins/oauth_puppy_html.py +228 -0
- code_puppy/plugins/shell_safety/__init__.py +6 -0
- code_puppy/plugins/shell_safety/agent_shell_safety.py +69 -0
- code_puppy/plugins/shell_safety/command_cache.py +156 -0
- code_puppy/plugins/shell_safety/register_callbacks.py +202 -0
- code_puppy/prompts/antigravity_system_prompt.md +1 -0
- code_puppy/prompts/codex_system_prompt.md +310 -0
- code_puppy/pydantic_patches.py +131 -0
- code_puppy/reopenable_async_client.py +8 -8
- code_puppy/round_robin_model.py +9 -12
- code_puppy/session_storage.py +2 -1
- code_puppy/status_display.py +21 -4
- code_puppy/summarization_agent.py +41 -13
- code_puppy/terminal_utils.py +418 -0
- code_puppy/tools/__init__.py +37 -1
- code_puppy/tools/agent_tools.py +536 -52
- code_puppy/tools/browser/__init__.py +37 -0
- code_puppy/tools/browser/browser_control.py +19 -23
- code_puppy/tools/browser/browser_interactions.py +41 -48
- code_puppy/tools/browser/browser_locators.py +36 -38
- code_puppy/tools/browser/browser_manager.py +316 -0
- code_puppy/tools/browser/browser_navigation.py +16 -16
- code_puppy/tools/browser/browser_screenshot.py +79 -143
- code_puppy/tools/browser/browser_scripts.py +32 -42
- code_puppy/tools/browser/browser_workflows.py +44 -27
- code_puppy/tools/browser/chromium_terminal_manager.py +259 -0
- code_puppy/tools/browser/terminal_command_tools.py +521 -0
- code_puppy/tools/browser/terminal_screenshot_tools.py +556 -0
- code_puppy/tools/browser/terminal_tools.py +525 -0
- code_puppy/tools/command_runner.py +930 -147
- code_puppy/tools/common.py +1113 -5
- code_puppy/tools/display.py +84 -0
- code_puppy/tools/file_modifications.py +288 -89
- code_puppy/tools/file_operations.py +226 -154
- code_puppy/tools/subagent_context.py +158 -0
- code_puppy/uvx_detection.py +242 -0
- code_puppy/version_checker.py +30 -11
- code_puppy-0.0.366.data/data/code_puppy/models.json +110 -0
- code_puppy-0.0.366.data/data/code_puppy/models_dev_api.json +1 -0
- {code_puppy-0.0.214.dist-info → code_puppy-0.0.366.dist-info}/METADATA +149 -75
- code_puppy-0.0.366.dist-info/RECORD +217 -0
- {code_puppy-0.0.214.dist-info → code_puppy-0.0.366.dist-info}/WHEEL +1 -1
- code_puppy/command_line/mcp/add_command.py +0 -183
- code_puppy/messaging/spinner/textual_spinner.py +0 -106
- code_puppy/tools/browser/camoufox_manager.py +0 -216
- code_puppy/tools/browser/vqa_agent.py +0 -70
- code_puppy/tui/__init__.py +0 -10
- code_puppy/tui/app.py +0 -1105
- code_puppy/tui/components/__init__.py +0 -21
- code_puppy/tui/components/chat_view.py +0 -551
- code_puppy/tui/components/command_history_modal.py +0 -218
- code_puppy/tui/components/copy_button.py +0 -139
- code_puppy/tui/components/custom_widgets.py +0 -63
- code_puppy/tui/components/human_input_modal.py +0 -175
- code_puppy/tui/components/input_area.py +0 -167
- code_puppy/tui/components/sidebar.py +0 -309
- code_puppy/tui/components/status_bar.py +0 -185
- code_puppy/tui/messages.py +0 -27
- code_puppy/tui/models/__init__.py +0 -8
- code_puppy/tui/models/chat_message.py +0 -25
- code_puppy/tui/models/command_history.py +0 -89
- code_puppy/tui/models/enums.py +0 -24
- code_puppy/tui/screens/__init__.py +0 -17
- code_puppy/tui/screens/autosave_picker.py +0 -175
- code_puppy/tui/screens/help.py +0 -130
- code_puppy/tui/screens/mcp_install_wizard.py +0 -803
- code_puppy/tui/screens/settings.py +0 -306
- code_puppy/tui/screens/tools.py +0 -74
- code_puppy/tui_state.py +0 -55
- code_puppy-0.0.214.data/data/code_puppy/models.json +0 -112
- code_puppy-0.0.214.dist-info/RECORD +0 -131
- {code_puppy-0.0.214.dist-info → code_puppy-0.0.366.dist-info}/entry_points.txt +0 -0
- {code_puppy-0.0.214.dist-info → code_puppy-0.0.366.dist-info}/licenses/LICENSE +0 -0
code_puppy/cli_runner.py
ADDED
|
@@ -0,0 +1,916 @@
|
|
|
1
|
+
"""CLI runner for Code Puppy.
|
|
2
|
+
|
|
3
|
+
Contains the main application logic, interactive mode, and entry point.
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
# Apply pydantic-ai patches BEFORE any pydantic-ai imports
|
|
7
|
+
from code_puppy.pydantic_patches import apply_all_patches
|
|
8
|
+
|
|
9
|
+
apply_all_patches()
|
|
10
|
+
|
|
11
|
+
import argparse
|
|
12
|
+
import asyncio
|
|
13
|
+
import os
|
|
14
|
+
import sys
|
|
15
|
+
import time
|
|
16
|
+
import traceback
|
|
17
|
+
from pathlib import Path
|
|
18
|
+
|
|
19
|
+
from dbos import DBOS, DBOSConfig
|
|
20
|
+
from rich.console import Console
|
|
21
|
+
|
|
22
|
+
from code_puppy import __version__, callbacks, plugins
|
|
23
|
+
from code_puppy.agents import get_current_agent
|
|
24
|
+
from code_puppy.command_line.attachments import parse_prompt_attachments
|
|
25
|
+
from code_puppy.command_line.clipboard import get_clipboard_manager
|
|
26
|
+
from code_puppy.config import (
|
|
27
|
+
AUTOSAVE_DIR,
|
|
28
|
+
COMMAND_HISTORY_FILE,
|
|
29
|
+
DBOS_DATABASE_URL,
|
|
30
|
+
ensure_config_exists,
|
|
31
|
+
finalize_autosave_session,
|
|
32
|
+
get_use_dbos,
|
|
33
|
+
initialize_command_history_file,
|
|
34
|
+
save_command_to_history,
|
|
35
|
+
)
|
|
36
|
+
from code_puppy.http_utils import find_available_port
|
|
37
|
+
from code_puppy.keymap import (
|
|
38
|
+
KeymapError,
|
|
39
|
+
get_cancel_agent_display_name,
|
|
40
|
+
validate_cancel_agent_key,
|
|
41
|
+
)
|
|
42
|
+
from code_puppy.messaging import emit_info
|
|
43
|
+
from code_puppy.terminal_utils import (
|
|
44
|
+
print_truecolor_warning,
|
|
45
|
+
reset_unix_terminal,
|
|
46
|
+
reset_windows_terminal_ansi,
|
|
47
|
+
reset_windows_terminal_full,
|
|
48
|
+
)
|
|
49
|
+
from code_puppy.tools.common import console
|
|
50
|
+
from code_puppy.version_checker import default_version_mismatch_behavior
|
|
51
|
+
|
|
52
|
+
plugins.load_plugin_callbacks()
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
async def main():
|
|
56
|
+
"""Main async entry point for Code Puppy CLI."""
|
|
57
|
+
parser = argparse.ArgumentParser(description="Code Puppy - A code generation agent")
|
|
58
|
+
parser.add_argument(
|
|
59
|
+
"--version",
|
|
60
|
+
"-v",
|
|
61
|
+
action="version",
|
|
62
|
+
version=f"{__version__}",
|
|
63
|
+
help="Show version and exit",
|
|
64
|
+
)
|
|
65
|
+
parser.add_argument(
|
|
66
|
+
"--interactive",
|
|
67
|
+
"-i",
|
|
68
|
+
action="store_true",
|
|
69
|
+
help="Run in interactive mode",
|
|
70
|
+
)
|
|
71
|
+
parser.add_argument(
|
|
72
|
+
"--prompt",
|
|
73
|
+
"-p",
|
|
74
|
+
type=str,
|
|
75
|
+
help="Execute a single prompt and exit (no interactive mode)",
|
|
76
|
+
)
|
|
77
|
+
parser.add_argument(
|
|
78
|
+
"--agent",
|
|
79
|
+
"-a",
|
|
80
|
+
type=str,
|
|
81
|
+
help="Specify which agent to use (e.g., --agent code-puppy)",
|
|
82
|
+
)
|
|
83
|
+
parser.add_argument(
|
|
84
|
+
"--model",
|
|
85
|
+
"-m",
|
|
86
|
+
type=str,
|
|
87
|
+
help="Specify which model to use (e.g., --model gpt-5)",
|
|
88
|
+
)
|
|
89
|
+
parser.add_argument(
|
|
90
|
+
"command", nargs="*", help="Run a single command (deprecated, use -p instead)"
|
|
91
|
+
)
|
|
92
|
+
args = parser.parse_args()
|
|
93
|
+
|
|
94
|
+
from code_puppy.messaging import (
|
|
95
|
+
RichConsoleRenderer,
|
|
96
|
+
SynchronousInteractiveRenderer,
|
|
97
|
+
get_global_queue,
|
|
98
|
+
get_message_bus,
|
|
99
|
+
)
|
|
100
|
+
|
|
101
|
+
# Create a shared console for both renderers
|
|
102
|
+
display_console = Console()
|
|
103
|
+
|
|
104
|
+
# Legacy renderer for backward compatibility (emits via get_global_queue)
|
|
105
|
+
message_queue = get_global_queue()
|
|
106
|
+
message_renderer = SynchronousInteractiveRenderer(message_queue, display_console)
|
|
107
|
+
message_renderer.start()
|
|
108
|
+
|
|
109
|
+
# New MessageBus renderer for structured messages (tools emit here)
|
|
110
|
+
message_bus = get_message_bus()
|
|
111
|
+
bus_renderer = RichConsoleRenderer(message_bus, display_console)
|
|
112
|
+
bus_renderer.start()
|
|
113
|
+
|
|
114
|
+
initialize_command_history_file()
|
|
115
|
+
from code_puppy.messaging import emit_error, emit_system_message
|
|
116
|
+
|
|
117
|
+
# Show the awesome Code Puppy logo when entering interactive mode
|
|
118
|
+
# This happens when: no -p flag (prompt-only mode) is used
|
|
119
|
+
# The logo should appear for both `code-puppy` and `code-puppy -i`
|
|
120
|
+
if not args.prompt:
|
|
121
|
+
try:
|
|
122
|
+
import pyfiglet
|
|
123
|
+
|
|
124
|
+
intro_lines = pyfiglet.figlet_format(
|
|
125
|
+
"CODE PUPPY", font="ansi_shadow"
|
|
126
|
+
).split("\n")
|
|
127
|
+
|
|
128
|
+
# Simple blue to green gradient (top to bottom)
|
|
129
|
+
gradient_colors = ["bright_blue", "bright_cyan", "bright_green"]
|
|
130
|
+
display_console.print("\n")
|
|
131
|
+
|
|
132
|
+
lines = []
|
|
133
|
+
# Apply gradient line by line
|
|
134
|
+
for line_num, line in enumerate(intro_lines):
|
|
135
|
+
if line.strip():
|
|
136
|
+
# Use line position to determine color (top blue, middle cyan, bottom green)
|
|
137
|
+
color_idx = min(line_num // 2, len(gradient_colors) - 1)
|
|
138
|
+
color = gradient_colors[color_idx]
|
|
139
|
+
lines.append(f"[{color}]{line}[/{color}]")
|
|
140
|
+
else:
|
|
141
|
+
lines.append("")
|
|
142
|
+
# Print directly to console to avoid the 'dim' style from emit_system_message
|
|
143
|
+
display_console.print("\n".join(lines))
|
|
144
|
+
except ImportError:
|
|
145
|
+
emit_system_message("🐶 Code Puppy is Loading...")
|
|
146
|
+
|
|
147
|
+
# Truecolor warning moved to interactive_mode() so it prints LAST
|
|
148
|
+
# after all the help stuff - max visibility for the ugly red box!
|
|
149
|
+
|
|
150
|
+
available_port = find_available_port()
|
|
151
|
+
if available_port is None:
|
|
152
|
+
emit_error("No available ports in range 8090-9010!")
|
|
153
|
+
return
|
|
154
|
+
|
|
155
|
+
# Early model setting if specified via command line
|
|
156
|
+
# This happens before ensure_config_exists() to ensure config is set up correctly
|
|
157
|
+
early_model = None
|
|
158
|
+
if args.model:
|
|
159
|
+
early_model = args.model.strip()
|
|
160
|
+
from code_puppy.config import set_model_name
|
|
161
|
+
|
|
162
|
+
set_model_name(early_model)
|
|
163
|
+
|
|
164
|
+
ensure_config_exists()
|
|
165
|
+
|
|
166
|
+
# Validate cancel_agent_key configuration early
|
|
167
|
+
try:
|
|
168
|
+
validate_cancel_agent_key()
|
|
169
|
+
except KeymapError as e:
|
|
170
|
+
from code_puppy.messaging import emit_error
|
|
171
|
+
|
|
172
|
+
emit_error(str(e))
|
|
173
|
+
sys.exit(1)
|
|
174
|
+
|
|
175
|
+
# Show uvx detection notice if we're on Windows + uvx
|
|
176
|
+
# Also disable Ctrl+C at the console level to prevent terminal bricking
|
|
177
|
+
try:
|
|
178
|
+
from code_puppy.uvx_detection import should_use_alternate_cancel_key
|
|
179
|
+
|
|
180
|
+
if should_use_alternate_cancel_key():
|
|
181
|
+
from code_puppy.terminal_utils import (
|
|
182
|
+
disable_windows_ctrl_c,
|
|
183
|
+
set_keep_ctrl_c_disabled,
|
|
184
|
+
)
|
|
185
|
+
|
|
186
|
+
# Disable Ctrl+C at the console input level
|
|
187
|
+
# This prevents Ctrl+C from being processed as a signal at all
|
|
188
|
+
disable_windows_ctrl_c()
|
|
189
|
+
|
|
190
|
+
# Set flag to keep it disabled (prompt_toolkit may re-enable it)
|
|
191
|
+
set_keep_ctrl_c_disabled(True)
|
|
192
|
+
|
|
193
|
+
# Use print directly - emit_system_message can get cleared by ANSI codes
|
|
194
|
+
print(
|
|
195
|
+
"🔧 Detected uvx launch on Windows - using Ctrl+K for cancellation "
|
|
196
|
+
"(Ctrl+C is disabled to prevent terminal issues)"
|
|
197
|
+
)
|
|
198
|
+
|
|
199
|
+
# Also install a SIGINT handler as backup
|
|
200
|
+
import signal
|
|
201
|
+
|
|
202
|
+
from code_puppy.terminal_utils import reset_windows_terminal_full
|
|
203
|
+
|
|
204
|
+
def _uvx_protective_sigint_handler(_sig, _frame):
|
|
205
|
+
"""Protective SIGINT handler for Windows+uvx."""
|
|
206
|
+
reset_windows_terminal_full()
|
|
207
|
+
# Re-disable Ctrl+C in case something re-enabled it
|
|
208
|
+
disable_windows_ctrl_c()
|
|
209
|
+
|
|
210
|
+
signal.signal(signal.SIGINT, _uvx_protective_sigint_handler)
|
|
211
|
+
except ImportError:
|
|
212
|
+
pass # uvx_detection module not available, ignore
|
|
213
|
+
|
|
214
|
+
# Load API keys from puppy.cfg into environment variables
|
|
215
|
+
from code_puppy.config import load_api_keys_to_environment
|
|
216
|
+
|
|
217
|
+
load_api_keys_to_environment()
|
|
218
|
+
|
|
219
|
+
# Handle model validation from command line (validation happens here, setting was earlier)
|
|
220
|
+
if args.model:
|
|
221
|
+
from code_puppy.config import _validate_model_exists
|
|
222
|
+
|
|
223
|
+
model_name = args.model.strip()
|
|
224
|
+
try:
|
|
225
|
+
# Validate that the model exists in models.json
|
|
226
|
+
if not _validate_model_exists(model_name):
|
|
227
|
+
from code_puppy.model_factory import ModelFactory
|
|
228
|
+
|
|
229
|
+
models_config = ModelFactory.load_config()
|
|
230
|
+
available_models = list(models_config.keys()) if models_config else []
|
|
231
|
+
|
|
232
|
+
emit_error(f"Model '{model_name}' not found")
|
|
233
|
+
emit_system_message(f"Available models: {', '.join(available_models)}")
|
|
234
|
+
sys.exit(1)
|
|
235
|
+
|
|
236
|
+
# Model is valid, show confirmation (already set earlier)
|
|
237
|
+
emit_system_message(f"🎯 Using model: {model_name}")
|
|
238
|
+
except Exception as e:
|
|
239
|
+
emit_error(f"Error validating model: {str(e)}")
|
|
240
|
+
sys.exit(1)
|
|
241
|
+
|
|
242
|
+
# Handle agent selection from command line
|
|
243
|
+
if args.agent:
|
|
244
|
+
from code_puppy.agents.agent_manager import (
|
|
245
|
+
get_available_agents,
|
|
246
|
+
set_current_agent,
|
|
247
|
+
)
|
|
248
|
+
|
|
249
|
+
agent_name = args.agent.lower()
|
|
250
|
+
try:
|
|
251
|
+
# First check if the agent exists by getting available agents
|
|
252
|
+
available_agents = get_available_agents()
|
|
253
|
+
if agent_name not in available_agents:
|
|
254
|
+
emit_error(f"Agent '{agent_name}' not found")
|
|
255
|
+
emit_system_message(
|
|
256
|
+
f"Available agents: {', '.join(available_agents.keys())}"
|
|
257
|
+
)
|
|
258
|
+
sys.exit(1)
|
|
259
|
+
|
|
260
|
+
# Agent exists, set it
|
|
261
|
+
set_current_agent(agent_name)
|
|
262
|
+
emit_system_message(f"🤖 Using agent: {agent_name}")
|
|
263
|
+
except Exception as e:
|
|
264
|
+
emit_error(f"Error setting agent: {str(e)}")
|
|
265
|
+
sys.exit(1)
|
|
266
|
+
|
|
267
|
+
current_version = __version__
|
|
268
|
+
|
|
269
|
+
no_version_update = os.getenv("NO_VERSION_UPDATE", "").lower() in (
|
|
270
|
+
"1",
|
|
271
|
+
"true",
|
|
272
|
+
"yes",
|
|
273
|
+
"on",
|
|
274
|
+
)
|
|
275
|
+
if no_version_update:
|
|
276
|
+
version_msg = f"Current version: {current_version}"
|
|
277
|
+
update_disabled_msg = (
|
|
278
|
+
"Update phase disabled because NO_VERSION_UPDATE is set to 1 or true"
|
|
279
|
+
)
|
|
280
|
+
emit_system_message(version_msg)
|
|
281
|
+
emit_system_message(update_disabled_msg)
|
|
282
|
+
else:
|
|
283
|
+
if len(callbacks.get_callbacks("version_check")):
|
|
284
|
+
await callbacks.on_version_check(current_version)
|
|
285
|
+
else:
|
|
286
|
+
default_version_mismatch_behavior(current_version)
|
|
287
|
+
|
|
288
|
+
await callbacks.on_startup()
|
|
289
|
+
|
|
290
|
+
# Initialize DBOS if not disabled
|
|
291
|
+
if get_use_dbos():
|
|
292
|
+
# Append a Unix timestamp in ms to the version for uniqueness
|
|
293
|
+
dbos_app_version = os.environ.get(
|
|
294
|
+
"DBOS_APP_VERSION", f"{current_version}-{int(time.time() * 1000)}"
|
|
295
|
+
)
|
|
296
|
+
dbos_config: DBOSConfig = {
|
|
297
|
+
"name": "dbos-code-puppy",
|
|
298
|
+
"system_database_url": DBOS_DATABASE_URL,
|
|
299
|
+
"run_admin_server": False,
|
|
300
|
+
"conductor_key": os.environ.get(
|
|
301
|
+
"DBOS_CONDUCTOR_KEY"
|
|
302
|
+
), # Optional, if set in env, connect to conductor
|
|
303
|
+
"log_level": os.environ.get(
|
|
304
|
+
"DBOS_LOG_LEVEL", "ERROR"
|
|
305
|
+
), # Default to ERROR level to suppress verbose logs
|
|
306
|
+
"application_version": dbos_app_version, # Match DBOS app version to Code Puppy version
|
|
307
|
+
}
|
|
308
|
+
try:
|
|
309
|
+
DBOS(config=dbos_config)
|
|
310
|
+
DBOS.launch()
|
|
311
|
+
except Exception as e:
|
|
312
|
+
emit_error(f"Error initializing DBOS: {e}")
|
|
313
|
+
sys.exit(1)
|
|
314
|
+
else:
|
|
315
|
+
pass
|
|
316
|
+
|
|
317
|
+
global shutdown_flag
|
|
318
|
+
shutdown_flag = False
|
|
319
|
+
try:
|
|
320
|
+
initial_command = None
|
|
321
|
+
prompt_only_mode = False
|
|
322
|
+
|
|
323
|
+
if args.prompt:
|
|
324
|
+
initial_command = args.prompt
|
|
325
|
+
prompt_only_mode = True
|
|
326
|
+
elif args.command:
|
|
327
|
+
initial_command = " ".join(args.command)
|
|
328
|
+
prompt_only_mode = False
|
|
329
|
+
|
|
330
|
+
if prompt_only_mode:
|
|
331
|
+
await execute_single_prompt(initial_command, message_renderer)
|
|
332
|
+
else:
|
|
333
|
+
# Default to interactive mode (no args = same as -i)
|
|
334
|
+
await interactive_mode(message_renderer, initial_command=initial_command)
|
|
335
|
+
finally:
|
|
336
|
+
if message_renderer:
|
|
337
|
+
message_renderer.stop()
|
|
338
|
+
if bus_renderer:
|
|
339
|
+
bus_renderer.stop()
|
|
340
|
+
await callbacks.on_shutdown()
|
|
341
|
+
if get_use_dbos():
|
|
342
|
+
DBOS.destroy()
|
|
343
|
+
|
|
344
|
+
|
|
345
|
+
async def interactive_mode(message_renderer, initial_command: str = None) -> None:
|
|
346
|
+
"""Run the agent in interactive mode."""
|
|
347
|
+
from code_puppy.command_line.command_handler import handle_command
|
|
348
|
+
|
|
349
|
+
display_console = message_renderer.console
|
|
350
|
+
from code_puppy.messaging import emit_info, emit_system_message
|
|
351
|
+
|
|
352
|
+
emit_system_message("Type '/exit' or '/quit' to exit the interactive mode.")
|
|
353
|
+
emit_system_message("Type 'clear' to reset the conversation history.")
|
|
354
|
+
emit_system_message("Type /help to view all commands")
|
|
355
|
+
emit_system_message(
|
|
356
|
+
"Type @ for path completion, or /model to pick a model. Toggle multiline with Alt+M or F2; newline: Ctrl+J."
|
|
357
|
+
)
|
|
358
|
+
emit_system_message("Paste images: Ctrl+V (even on Mac!), F3, or /paste command.")
|
|
359
|
+
import platform
|
|
360
|
+
|
|
361
|
+
if platform.system() == "Darwin":
|
|
362
|
+
emit_system_message(
|
|
363
|
+
"💡 macOS tip: Use Ctrl+V (not Cmd+V) to paste images in terminal."
|
|
364
|
+
)
|
|
365
|
+
cancel_key = get_cancel_agent_display_name()
|
|
366
|
+
emit_system_message(
|
|
367
|
+
f"Press {cancel_key} during processing to cancel the current task or inference. Use Ctrl+X to interrupt running shell commands."
|
|
368
|
+
)
|
|
369
|
+
emit_system_message(
|
|
370
|
+
"Use /autosave_load to manually load a previous autosave session."
|
|
371
|
+
)
|
|
372
|
+
emit_system_message(
|
|
373
|
+
"Use /diff to configure diff highlighting colors for file changes."
|
|
374
|
+
)
|
|
375
|
+
emit_system_message("To re-run the tutorial, use /tutorial.")
|
|
376
|
+
try:
|
|
377
|
+
from code_puppy.command_line.motd import print_motd
|
|
378
|
+
|
|
379
|
+
print_motd(console, force=False)
|
|
380
|
+
except Exception as e:
|
|
381
|
+
from code_puppy.messaging import emit_warning
|
|
382
|
+
|
|
383
|
+
emit_warning(f"MOTD error: {e}")
|
|
384
|
+
|
|
385
|
+
# Print truecolor warning LAST so it's the most visible thing on startup
|
|
386
|
+
# Big ugly red box should be impossible to miss! 🔴
|
|
387
|
+
print_truecolor_warning(display_console)
|
|
388
|
+
|
|
389
|
+
# Initialize the runtime agent manager
|
|
390
|
+
if initial_command:
|
|
391
|
+
from code_puppy.agents import get_current_agent
|
|
392
|
+
from code_puppy.messaging import emit_info, emit_success, emit_system_message
|
|
393
|
+
|
|
394
|
+
agent = get_current_agent()
|
|
395
|
+
emit_info(f"Processing initial command: {initial_command}")
|
|
396
|
+
|
|
397
|
+
try:
|
|
398
|
+
# Check if any tool is waiting for user input before showing spinner
|
|
399
|
+
try:
|
|
400
|
+
from code_puppy.tools.command_runner import is_awaiting_user_input
|
|
401
|
+
|
|
402
|
+
awaiting_input = is_awaiting_user_input()
|
|
403
|
+
except ImportError:
|
|
404
|
+
awaiting_input = False
|
|
405
|
+
|
|
406
|
+
# Run with or without spinner based on whether we're awaiting input
|
|
407
|
+
response, agent_task = await run_prompt_with_attachments(
|
|
408
|
+
agent,
|
|
409
|
+
initial_command,
|
|
410
|
+
spinner_console=display_console,
|
|
411
|
+
use_spinner=not awaiting_input,
|
|
412
|
+
)
|
|
413
|
+
if response is not None:
|
|
414
|
+
agent_response = response.output
|
|
415
|
+
|
|
416
|
+
# Update the agent's message history with the complete conversation
|
|
417
|
+
# including the final assistant response
|
|
418
|
+
if hasattr(response, "all_messages"):
|
|
419
|
+
agent.set_message_history(list(response.all_messages()))
|
|
420
|
+
|
|
421
|
+
# Emit structured message for proper markdown rendering
|
|
422
|
+
from code_puppy.messaging import get_message_bus
|
|
423
|
+
from code_puppy.messaging.messages import AgentResponseMessage
|
|
424
|
+
|
|
425
|
+
response_msg = AgentResponseMessage(
|
|
426
|
+
content=agent_response,
|
|
427
|
+
is_markdown=True,
|
|
428
|
+
)
|
|
429
|
+
get_message_bus().emit(response_msg)
|
|
430
|
+
|
|
431
|
+
emit_success("🐶 Continuing in Interactive Mode")
|
|
432
|
+
emit_system_message(
|
|
433
|
+
"Your command and response are preserved in the conversation history."
|
|
434
|
+
)
|
|
435
|
+
|
|
436
|
+
except Exception as e:
|
|
437
|
+
from code_puppy.messaging import emit_error
|
|
438
|
+
|
|
439
|
+
emit_error(f"Error processing initial command: {str(e)}")
|
|
440
|
+
|
|
441
|
+
# Check if prompt_toolkit is installed
|
|
442
|
+
try:
|
|
443
|
+
from code_puppy.command_line.prompt_toolkit_completion import (
|
|
444
|
+
get_input_with_combined_completion,
|
|
445
|
+
get_prompt_with_active_model,
|
|
446
|
+
)
|
|
447
|
+
except ImportError:
|
|
448
|
+
from code_puppy.messaging import emit_warning
|
|
449
|
+
|
|
450
|
+
emit_warning("Warning: prompt_toolkit not installed. Installing now...")
|
|
451
|
+
try:
|
|
452
|
+
import subprocess
|
|
453
|
+
|
|
454
|
+
subprocess.check_call(
|
|
455
|
+
[sys.executable, "-m", "pip", "install", "--quiet", "prompt_toolkit"]
|
|
456
|
+
)
|
|
457
|
+
from code_puppy.messaging import emit_success
|
|
458
|
+
|
|
459
|
+
emit_success("Successfully installed prompt_toolkit")
|
|
460
|
+
from code_puppy.command_line.prompt_toolkit_completion import (
|
|
461
|
+
get_input_with_combined_completion,
|
|
462
|
+
get_prompt_with_active_model,
|
|
463
|
+
)
|
|
464
|
+
except Exception as e:
|
|
465
|
+
from code_puppy.messaging import emit_error, emit_warning
|
|
466
|
+
|
|
467
|
+
emit_error(f"Error installing prompt_toolkit: {e}")
|
|
468
|
+
emit_warning("Falling back to basic input without tab completion")
|
|
469
|
+
|
|
470
|
+
# Autosave loading is now manual - use /autosave_load command
|
|
471
|
+
|
|
472
|
+
# Auto-run tutorial on first startup
|
|
473
|
+
try:
|
|
474
|
+
from code_puppy.command_line.onboarding_wizard import should_show_onboarding
|
|
475
|
+
|
|
476
|
+
if should_show_onboarding():
|
|
477
|
+
import asyncio
|
|
478
|
+
import concurrent.futures
|
|
479
|
+
|
|
480
|
+
from code_puppy.command_line.onboarding_wizard import run_onboarding_wizard
|
|
481
|
+
from code_puppy.config import set_model_name
|
|
482
|
+
from code_puppy.messaging import emit_info
|
|
483
|
+
|
|
484
|
+
with concurrent.futures.ThreadPoolExecutor() as executor:
|
|
485
|
+
future = executor.submit(lambda: asyncio.run(run_onboarding_wizard()))
|
|
486
|
+
result = future.result(timeout=300)
|
|
487
|
+
|
|
488
|
+
if result == "chatgpt":
|
|
489
|
+
emit_info("🔐 Starting ChatGPT OAuth flow...")
|
|
490
|
+
from code_puppy.plugins.chatgpt_oauth.oauth_flow import run_oauth_flow
|
|
491
|
+
|
|
492
|
+
run_oauth_flow()
|
|
493
|
+
set_model_name("chatgpt-gpt-5.2-codex")
|
|
494
|
+
elif result == "claude":
|
|
495
|
+
emit_info("🔐 Starting Claude Code OAuth flow...")
|
|
496
|
+
from code_puppy.plugins.claude_code_oauth.register_callbacks import (
|
|
497
|
+
_perform_authentication,
|
|
498
|
+
)
|
|
499
|
+
|
|
500
|
+
_perform_authentication()
|
|
501
|
+
set_model_name("claude-code-claude-opus-4-5-20251101")
|
|
502
|
+
elif result == "completed":
|
|
503
|
+
emit_info("🎉 Tutorial complete! Happy coding!")
|
|
504
|
+
elif result == "skipped":
|
|
505
|
+
emit_info("⏭️ Tutorial skipped. Run /tutorial anytime!")
|
|
506
|
+
except Exception as e:
|
|
507
|
+
from code_puppy.messaging import emit_warning
|
|
508
|
+
|
|
509
|
+
emit_warning(f"Tutorial auto-start failed: {e}")
|
|
510
|
+
|
|
511
|
+
# Track the current agent task for cancellation on quit
|
|
512
|
+
current_agent_task = None
|
|
513
|
+
|
|
514
|
+
while True:
|
|
515
|
+
from code_puppy.agents.agent_manager import get_current_agent
|
|
516
|
+
from code_puppy.messaging import emit_info
|
|
517
|
+
|
|
518
|
+
# Get the custom prompt from the current agent, or use default
|
|
519
|
+
current_agent = get_current_agent()
|
|
520
|
+
user_prompt = current_agent.get_user_prompt() or "Enter your coding task:"
|
|
521
|
+
|
|
522
|
+
emit_info(f"{user_prompt}\n")
|
|
523
|
+
|
|
524
|
+
try:
|
|
525
|
+
# Use prompt_toolkit for enhanced input with path completion
|
|
526
|
+
try:
|
|
527
|
+
# Windows-specific: Reset terminal state before prompting
|
|
528
|
+
reset_windows_terminal_ansi()
|
|
529
|
+
|
|
530
|
+
# Use the async version of get_input_with_combined_completion
|
|
531
|
+
task = await get_input_with_combined_completion(
|
|
532
|
+
get_prompt_with_active_model(), history_file=COMMAND_HISTORY_FILE
|
|
533
|
+
)
|
|
534
|
+
|
|
535
|
+
# Windows+uvx: Re-disable Ctrl+C after prompt_toolkit
|
|
536
|
+
# (prompt_toolkit restores console mode which re-enables Ctrl+C)
|
|
537
|
+
try:
|
|
538
|
+
from code_puppy.terminal_utils import ensure_ctrl_c_disabled
|
|
539
|
+
|
|
540
|
+
ensure_ctrl_c_disabled()
|
|
541
|
+
except ImportError:
|
|
542
|
+
pass
|
|
543
|
+
except ImportError:
|
|
544
|
+
# Fall back to basic input if prompt_toolkit is not available
|
|
545
|
+
task = input(">>> ")
|
|
546
|
+
|
|
547
|
+
except (KeyboardInterrupt, EOFError):
|
|
548
|
+
# Handle Ctrl+C or Ctrl+D
|
|
549
|
+
# Windows-specific: Reset terminal state after interrupt to prevent
|
|
550
|
+
# the terminal from becoming unresponsive (can't type characters)
|
|
551
|
+
reset_windows_terminal_full()
|
|
552
|
+
from code_puppy.messaging import emit_warning
|
|
553
|
+
|
|
554
|
+
emit_warning("\nInput cancelled")
|
|
555
|
+
continue
|
|
556
|
+
|
|
557
|
+
# Check for exit commands (plain text or command form)
|
|
558
|
+
if task.strip().lower() in ["exit", "quit"] or task.strip().lower() in [
|
|
559
|
+
"/exit",
|
|
560
|
+
"/quit",
|
|
561
|
+
]:
|
|
562
|
+
import asyncio
|
|
563
|
+
|
|
564
|
+
from code_puppy.messaging import emit_success
|
|
565
|
+
|
|
566
|
+
emit_success("Goodbye!")
|
|
567
|
+
|
|
568
|
+
# Cancel any running agent task for clean shutdown
|
|
569
|
+
if current_agent_task and not current_agent_task.done():
|
|
570
|
+
emit_info("Cancelling running agent task...")
|
|
571
|
+
current_agent_task.cancel()
|
|
572
|
+
try:
|
|
573
|
+
await current_agent_task
|
|
574
|
+
except asyncio.CancelledError:
|
|
575
|
+
pass # Expected when cancelling
|
|
576
|
+
|
|
577
|
+
# The renderer is stopped in the finally block of main().
|
|
578
|
+
break
|
|
579
|
+
|
|
580
|
+
# Check for clear command (supports both `clear` and `/clear`)
|
|
581
|
+
if task.strip().lower() in ("clear", "/clear"):
|
|
582
|
+
from code_puppy.command_line.clipboard import get_clipboard_manager
|
|
583
|
+
from code_puppy.messaging import (
|
|
584
|
+
emit_info,
|
|
585
|
+
emit_system_message,
|
|
586
|
+
emit_warning,
|
|
587
|
+
)
|
|
588
|
+
|
|
589
|
+
agent = get_current_agent()
|
|
590
|
+
new_session_id = finalize_autosave_session()
|
|
591
|
+
agent.clear_message_history()
|
|
592
|
+
emit_warning("Conversation history cleared!")
|
|
593
|
+
emit_system_message("The agent will not remember previous interactions.")
|
|
594
|
+
emit_info(f"Auto-save session rotated to: {new_session_id}")
|
|
595
|
+
|
|
596
|
+
# Also clear pending clipboard images
|
|
597
|
+
clipboard_manager = get_clipboard_manager()
|
|
598
|
+
clipboard_count = clipboard_manager.get_pending_count()
|
|
599
|
+
clipboard_manager.clear_pending()
|
|
600
|
+
if clipboard_count > 0:
|
|
601
|
+
emit_info(f"Cleared {clipboard_count} pending clipboard image(s)")
|
|
602
|
+
continue
|
|
603
|
+
|
|
604
|
+
# Parse attachments first so leading paths aren't misread as commands
|
|
605
|
+
processed_for_commands = parse_prompt_attachments(task)
|
|
606
|
+
cleaned_for_commands = (processed_for_commands.prompt or "").strip()
|
|
607
|
+
|
|
608
|
+
# Handle / commands based on cleaned prompt (after stripping attachments)
|
|
609
|
+
if cleaned_for_commands.startswith("/"):
|
|
610
|
+
try:
|
|
611
|
+
command_result = handle_command(cleaned_for_commands)
|
|
612
|
+
except Exception as e:
|
|
613
|
+
from code_puppy.messaging import emit_error
|
|
614
|
+
|
|
615
|
+
emit_error(f"Command error: {e}")
|
|
616
|
+
# Continue interactive loop instead of exiting
|
|
617
|
+
continue
|
|
618
|
+
if command_result is True:
|
|
619
|
+
continue
|
|
620
|
+
elif isinstance(command_result, str):
|
|
621
|
+
if command_result == "__AUTOSAVE_LOAD__":
|
|
622
|
+
# Handle async autosave loading
|
|
623
|
+
try:
|
|
624
|
+
# Check if we're in a real interactive terminal
|
|
625
|
+
# (not pexpect/tests) - interactive picker requires proper TTY
|
|
626
|
+
use_interactive_picker = (
|
|
627
|
+
sys.stdin.isatty() and sys.stdout.isatty()
|
|
628
|
+
)
|
|
629
|
+
|
|
630
|
+
# Allow environment variable override for tests
|
|
631
|
+
if os.getenv("CODE_PUPPY_NO_TUI") == "1":
|
|
632
|
+
use_interactive_picker = False
|
|
633
|
+
|
|
634
|
+
if use_interactive_picker:
|
|
635
|
+
# Use interactive picker for terminal sessions
|
|
636
|
+
from code_puppy.agents.agent_manager import (
|
|
637
|
+
get_current_agent,
|
|
638
|
+
)
|
|
639
|
+
from code_puppy.command_line.autosave_menu import (
|
|
640
|
+
interactive_autosave_picker,
|
|
641
|
+
)
|
|
642
|
+
from code_puppy.config import (
|
|
643
|
+
set_current_autosave_from_session_name,
|
|
644
|
+
)
|
|
645
|
+
from code_puppy.messaging import (
|
|
646
|
+
emit_error,
|
|
647
|
+
emit_success,
|
|
648
|
+
emit_warning,
|
|
649
|
+
)
|
|
650
|
+
from code_puppy.session_storage import (
|
|
651
|
+
load_session,
|
|
652
|
+
restore_autosave_interactively,
|
|
653
|
+
)
|
|
654
|
+
|
|
655
|
+
chosen_session = await interactive_autosave_picker()
|
|
656
|
+
|
|
657
|
+
if not chosen_session:
|
|
658
|
+
emit_warning("Autosave load cancelled")
|
|
659
|
+
continue
|
|
660
|
+
|
|
661
|
+
# Load the session
|
|
662
|
+
base_dir = Path(AUTOSAVE_DIR)
|
|
663
|
+
history = load_session(chosen_session, base_dir)
|
|
664
|
+
|
|
665
|
+
agent = get_current_agent()
|
|
666
|
+
agent.set_message_history(history)
|
|
667
|
+
|
|
668
|
+
# Set current autosave session
|
|
669
|
+
set_current_autosave_from_session_name(chosen_session)
|
|
670
|
+
|
|
671
|
+
total_tokens = sum(
|
|
672
|
+
agent.estimate_tokens_for_message(msg)
|
|
673
|
+
for msg in history
|
|
674
|
+
)
|
|
675
|
+
session_path = base_dir / f"{chosen_session}.pkl"
|
|
676
|
+
|
|
677
|
+
emit_success(
|
|
678
|
+
f"✅ Autosave loaded: {len(history)} messages ({total_tokens} tokens)\n"
|
|
679
|
+
f"📁 From: {session_path}"
|
|
680
|
+
)
|
|
681
|
+
else:
|
|
682
|
+
# Fall back to old text-based picker for tests/non-TTY environments
|
|
683
|
+
await restore_autosave_interactively(Path(AUTOSAVE_DIR))
|
|
684
|
+
|
|
685
|
+
except Exception as e:
|
|
686
|
+
from code_puppy.messaging import emit_error
|
|
687
|
+
|
|
688
|
+
emit_error(f"Failed to load autosave: {e}")
|
|
689
|
+
continue
|
|
690
|
+
else:
|
|
691
|
+
# Command returned a prompt to execute
|
|
692
|
+
task = command_result
|
|
693
|
+
elif command_result is False:
|
|
694
|
+
# Command not recognized, continue with normal processing
|
|
695
|
+
pass
|
|
696
|
+
|
|
697
|
+
if task.strip():
|
|
698
|
+
# Write to the secret file for permanent history with timestamp
|
|
699
|
+
save_command_to_history(task)
|
|
700
|
+
|
|
701
|
+
try:
|
|
702
|
+
# No need to get agent directly - use manager's run methods
|
|
703
|
+
|
|
704
|
+
# Use our custom helper to enable attachment handling with spinner support
|
|
705
|
+
result, current_agent_task = await run_prompt_with_attachments(
|
|
706
|
+
current_agent,
|
|
707
|
+
task,
|
|
708
|
+
spinner_console=message_renderer.console,
|
|
709
|
+
)
|
|
710
|
+
# Check if the task was cancelled (but don't show message if we just killed processes)
|
|
711
|
+
if result is None:
|
|
712
|
+
# Windows-specific: Reset terminal state after cancellation
|
|
713
|
+
reset_windows_terminal_ansi()
|
|
714
|
+
# Re-disable Ctrl+C if needed (uvx mode)
|
|
715
|
+
try:
|
|
716
|
+
from code_puppy.terminal_utils import ensure_ctrl_c_disabled
|
|
717
|
+
|
|
718
|
+
ensure_ctrl_c_disabled()
|
|
719
|
+
except ImportError:
|
|
720
|
+
pass
|
|
721
|
+
continue
|
|
722
|
+
# Get the structured response
|
|
723
|
+
agent_response = result.output
|
|
724
|
+
|
|
725
|
+
# Emit structured message for proper markdown rendering
|
|
726
|
+
from code_puppy.messaging import get_message_bus
|
|
727
|
+
from code_puppy.messaging.messages import AgentResponseMessage
|
|
728
|
+
|
|
729
|
+
response_msg = AgentResponseMessage(
|
|
730
|
+
content=agent_response,
|
|
731
|
+
is_markdown=True,
|
|
732
|
+
)
|
|
733
|
+
get_message_bus().emit(response_msg)
|
|
734
|
+
|
|
735
|
+
# Update the agent's message history with the complete conversation
|
|
736
|
+
# including the final assistant response. The history_processors callback
|
|
737
|
+
# may not capture the final message, so we use result.all_messages()
|
|
738
|
+
# to ensure the autosave includes the complete conversation.
|
|
739
|
+
if hasattr(result, "all_messages"):
|
|
740
|
+
current_agent.set_message_history(list(result.all_messages()))
|
|
741
|
+
|
|
742
|
+
# Ensure console output is flushed before next prompt
|
|
743
|
+
# This fixes the issue where prompt doesn't appear after agent response
|
|
744
|
+
display_console.file.flush() if hasattr(
|
|
745
|
+
display_console.file, "flush"
|
|
746
|
+
) else None
|
|
747
|
+
import time
|
|
748
|
+
|
|
749
|
+
time.sleep(0.1) # Brief pause to ensure all messages are rendered
|
|
750
|
+
|
|
751
|
+
except Exception:
|
|
752
|
+
from code_puppy.messaging.queue_console import get_queue_console
|
|
753
|
+
|
|
754
|
+
get_queue_console().print_exception()
|
|
755
|
+
|
|
756
|
+
# Auto-save session if enabled (moved outside the try block to avoid being swallowed)
|
|
757
|
+
from code_puppy.config import auto_save_session_if_enabled
|
|
758
|
+
|
|
759
|
+
auto_save_session_if_enabled()
|
|
760
|
+
|
|
761
|
+
# Re-disable Ctrl+C if needed (uvx mode) - must be done after
|
|
762
|
+
# each iteration as various operations may restore console mode
|
|
763
|
+
try:
|
|
764
|
+
from code_puppy.terminal_utils import ensure_ctrl_c_disabled
|
|
765
|
+
|
|
766
|
+
ensure_ctrl_c_disabled()
|
|
767
|
+
except ImportError:
|
|
768
|
+
pass
|
|
769
|
+
|
|
770
|
+
|
|
771
|
+
async def run_prompt_with_attachments(
|
|
772
|
+
agent,
|
|
773
|
+
raw_prompt: str,
|
|
774
|
+
*,
|
|
775
|
+
spinner_console=None,
|
|
776
|
+
use_spinner: bool = True,
|
|
777
|
+
):
|
|
778
|
+
"""Run the agent after parsing CLI attachments for image/document support.
|
|
779
|
+
|
|
780
|
+
Returns:
|
|
781
|
+
tuple: (result, task) where result is the agent response and task is the asyncio task
|
|
782
|
+
"""
|
|
783
|
+
import asyncio
|
|
784
|
+
import re
|
|
785
|
+
|
|
786
|
+
from code_puppy.messaging import emit_system_message, emit_warning
|
|
787
|
+
|
|
788
|
+
processed_prompt = parse_prompt_attachments(raw_prompt)
|
|
789
|
+
|
|
790
|
+
for warning in processed_prompt.warnings:
|
|
791
|
+
emit_warning(warning)
|
|
792
|
+
|
|
793
|
+
# Get clipboard images and merge with file attachments
|
|
794
|
+
clipboard_manager = get_clipboard_manager()
|
|
795
|
+
clipboard_images = clipboard_manager.get_pending_images()
|
|
796
|
+
|
|
797
|
+
# Clear pending clipboard images after retrieval
|
|
798
|
+
clipboard_manager.clear_pending()
|
|
799
|
+
|
|
800
|
+
# Build summary of all attachments
|
|
801
|
+
summary_parts = []
|
|
802
|
+
if processed_prompt.attachments:
|
|
803
|
+
summary_parts.append(f"files: {len(processed_prompt.attachments)}")
|
|
804
|
+
if clipboard_images:
|
|
805
|
+
summary_parts.append(f"clipboard images: {len(clipboard_images)}")
|
|
806
|
+
if processed_prompt.link_attachments:
|
|
807
|
+
summary_parts.append(f"urls: {len(processed_prompt.link_attachments)}")
|
|
808
|
+
if summary_parts:
|
|
809
|
+
emit_system_message("Attachments detected -> " + ", ".join(summary_parts))
|
|
810
|
+
|
|
811
|
+
# Clean up clipboard placeholders from the prompt text
|
|
812
|
+
cleaned_prompt = processed_prompt.prompt
|
|
813
|
+
if clipboard_images and cleaned_prompt:
|
|
814
|
+
cleaned_prompt = re.sub(
|
|
815
|
+
r"\[📋 clipboard image \d+\]\s*", "", cleaned_prompt
|
|
816
|
+
).strip()
|
|
817
|
+
|
|
818
|
+
if not cleaned_prompt:
|
|
819
|
+
emit_warning(
|
|
820
|
+
"Prompt is empty after removing attachments; add instructions and retry."
|
|
821
|
+
)
|
|
822
|
+
return None, None
|
|
823
|
+
|
|
824
|
+
# Combine file attachments with clipboard images
|
|
825
|
+
attachments = [attachment.content for attachment in processed_prompt.attachments]
|
|
826
|
+
attachments.extend(clipboard_images) # Add clipboard images
|
|
827
|
+
|
|
828
|
+
link_attachments = [link.url_part for link in processed_prompt.link_attachments]
|
|
829
|
+
|
|
830
|
+
# IMPORTANT: Set the shared console for streaming output so it
|
|
831
|
+
# uses the same console as the spinner. This prevents Live display conflicts
|
|
832
|
+
# that cause line duplication during markdown streaming.
|
|
833
|
+
from code_puppy.agents.event_stream_handler import set_streaming_console
|
|
834
|
+
|
|
835
|
+
set_streaming_console(spinner_console)
|
|
836
|
+
|
|
837
|
+
# Create the agent task first so we can track and cancel it
|
|
838
|
+
agent_task = asyncio.create_task(
|
|
839
|
+
agent.run_with_mcp(
|
|
840
|
+
cleaned_prompt, # Use cleaned prompt (clipboard placeholders removed)
|
|
841
|
+
attachments=attachments,
|
|
842
|
+
link_attachments=link_attachments,
|
|
843
|
+
)
|
|
844
|
+
)
|
|
845
|
+
|
|
846
|
+
if use_spinner and spinner_console is not None:
|
|
847
|
+
from code_puppy.messaging.spinner import ConsoleSpinner
|
|
848
|
+
|
|
849
|
+
with ConsoleSpinner(console=spinner_console):
|
|
850
|
+
try:
|
|
851
|
+
result = await agent_task
|
|
852
|
+
return result, agent_task
|
|
853
|
+
except asyncio.CancelledError:
|
|
854
|
+
emit_info("Agent task cancelled")
|
|
855
|
+
return None, agent_task
|
|
856
|
+
else:
|
|
857
|
+
try:
|
|
858
|
+
result = await agent_task
|
|
859
|
+
return result, agent_task
|
|
860
|
+
except asyncio.CancelledError:
|
|
861
|
+
emit_info("Agent task cancelled")
|
|
862
|
+
return None, agent_task
|
|
863
|
+
|
|
864
|
+
|
|
865
|
+
async def execute_single_prompt(prompt: str, message_renderer) -> None:
|
|
866
|
+
"""Execute a single prompt and exit (for -p flag)."""
|
|
867
|
+
from code_puppy.messaging import emit_info
|
|
868
|
+
|
|
869
|
+
emit_info(f"Executing prompt: {prompt}")
|
|
870
|
+
|
|
871
|
+
try:
|
|
872
|
+
# Get agent through runtime manager and use helper for attachments
|
|
873
|
+
agent = get_current_agent()
|
|
874
|
+
response = await run_prompt_with_attachments(
|
|
875
|
+
agent,
|
|
876
|
+
prompt,
|
|
877
|
+
spinner_console=message_renderer.console,
|
|
878
|
+
)
|
|
879
|
+
if response is None:
|
|
880
|
+
return
|
|
881
|
+
|
|
882
|
+
agent_response = response.output
|
|
883
|
+
|
|
884
|
+
# Emit structured message for proper markdown rendering
|
|
885
|
+
from code_puppy.messaging import get_message_bus
|
|
886
|
+
from code_puppy.messaging.messages import AgentResponseMessage
|
|
887
|
+
|
|
888
|
+
response_msg = AgentResponseMessage(
|
|
889
|
+
content=agent_response,
|
|
890
|
+
is_markdown=True,
|
|
891
|
+
)
|
|
892
|
+
get_message_bus().emit(response_msg)
|
|
893
|
+
|
|
894
|
+
except asyncio.CancelledError:
|
|
895
|
+
from code_puppy.messaging import emit_warning
|
|
896
|
+
|
|
897
|
+
emit_warning("Execution cancelled by user")
|
|
898
|
+
except Exception as e:
|
|
899
|
+
from code_puppy.messaging import emit_error
|
|
900
|
+
|
|
901
|
+
emit_error(f"Error executing prompt: {str(e)}")
|
|
902
|
+
|
|
903
|
+
|
|
904
|
+
def main_entry():
|
|
905
|
+
"""Entry point for the installed CLI tool."""
|
|
906
|
+
try:
|
|
907
|
+
asyncio.run(main())
|
|
908
|
+
except KeyboardInterrupt:
|
|
909
|
+
# Note: Using sys.stderr for crash output - messaging system may not be available
|
|
910
|
+
sys.stderr.write(traceback.format_exc())
|
|
911
|
+
if get_use_dbos():
|
|
912
|
+
DBOS.destroy()
|
|
913
|
+
return 0
|
|
914
|
+
finally:
|
|
915
|
+
# Reset terminal on Unix-like systems (not Windows)
|
|
916
|
+
reset_unix_terminal()
|