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
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
"""Common display utilities for rendering agent outputs.
|
|
2
|
+
|
|
3
|
+
This module provides non-streaming display functions for rendering
|
|
4
|
+
agent results and other structured content using termflow for markdown.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from typing import Optional
|
|
8
|
+
|
|
9
|
+
from rich.console import Console
|
|
10
|
+
|
|
11
|
+
from code_puppy.config import get_banner_color, get_subagent_verbose
|
|
12
|
+
from code_puppy.tools.subagent_context import is_subagent
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def display_non_streamed_result(
|
|
16
|
+
content: str,
|
|
17
|
+
console: Optional[Console] = None,
|
|
18
|
+
banner_text: str = "AGENT RESPONSE",
|
|
19
|
+
banner_name: str = "agent_response",
|
|
20
|
+
) -> None:
|
|
21
|
+
"""Display a non-streamed result with markdown rendering via termflow.
|
|
22
|
+
|
|
23
|
+
This function renders markdown content using termflow for beautiful
|
|
24
|
+
terminal output. Use this instead of streaming for sub-agent responses
|
|
25
|
+
or any other content that arrives all at once.
|
|
26
|
+
|
|
27
|
+
Args:
|
|
28
|
+
content: The content to display (can include markdown).
|
|
29
|
+
console: Rich Console to use for output. If None, creates a new one.
|
|
30
|
+
banner_text: Text to display in the banner (default: "AGENT RESPONSE").
|
|
31
|
+
banner_name: Banner config key for color lookup (default: "agent_response").
|
|
32
|
+
|
|
33
|
+
Example:
|
|
34
|
+
>>> display_non_streamed_result("# Hello\n\nThis is **bold** text.")
|
|
35
|
+
# Renders with AGENT RESPONSE banner and formatted markdown
|
|
36
|
+
"""
|
|
37
|
+
# Skip display for sub-agents unless verbose mode
|
|
38
|
+
if is_subagent() and not get_subagent_verbose():
|
|
39
|
+
return
|
|
40
|
+
|
|
41
|
+
import time
|
|
42
|
+
|
|
43
|
+
from rich.text import Text
|
|
44
|
+
from termflow import Parser as TermflowParser
|
|
45
|
+
from termflow import Renderer as TermflowRenderer
|
|
46
|
+
|
|
47
|
+
from code_puppy.messaging.spinner import pause_all_spinners, resume_all_spinners
|
|
48
|
+
|
|
49
|
+
if console is None:
|
|
50
|
+
console = Console()
|
|
51
|
+
|
|
52
|
+
# Pause spinners and give time to clear
|
|
53
|
+
pause_all_spinners()
|
|
54
|
+
time.sleep(0.1)
|
|
55
|
+
|
|
56
|
+
# Clear line and print banner
|
|
57
|
+
console.print(" " * 50, end="\r")
|
|
58
|
+
console.print() # Newline before banner
|
|
59
|
+
|
|
60
|
+
banner_color = get_banner_color(banner_name)
|
|
61
|
+
console.print(
|
|
62
|
+
Text.from_markup(
|
|
63
|
+
f"[bold white on {banner_color}] {banner_text} [/bold white on {banner_color}]"
|
|
64
|
+
)
|
|
65
|
+
)
|
|
66
|
+
|
|
67
|
+
# Use termflow for markdown rendering
|
|
68
|
+
parser = TermflowParser()
|
|
69
|
+
renderer = TermflowRenderer(output=console.file, width=console.width)
|
|
70
|
+
|
|
71
|
+
# Process content line by line
|
|
72
|
+
for line in content.split("\n"):
|
|
73
|
+
events = parser.parse_line(line)
|
|
74
|
+
renderer.render_all(events)
|
|
75
|
+
|
|
76
|
+
# Finalize to close any open markdown blocks
|
|
77
|
+
final_events = parser.finalize()
|
|
78
|
+
renderer.render_all(final_events)
|
|
79
|
+
|
|
80
|
+
# Resume spinners
|
|
81
|
+
resume_all_spinners()
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
__all__ = ["display_non_streamed_result"]
|
|
@@ -20,10 +20,58 @@ import json_repair
|
|
|
20
20
|
from pydantic import BaseModel
|
|
21
21
|
from pydantic_ai import RunContext
|
|
22
22
|
|
|
23
|
-
from code_puppy.
|
|
23
|
+
from code_puppy.callbacks import on_delete_file, on_edit_file
|
|
24
|
+
from code_puppy.messaging import ( # Structured messaging types
|
|
25
|
+
DiffLine,
|
|
26
|
+
DiffMessage,
|
|
27
|
+
emit_error,
|
|
28
|
+
emit_warning,
|
|
29
|
+
get_message_bus,
|
|
30
|
+
)
|
|
24
31
|
from code_puppy.tools.common import _find_best_window, generate_group_id
|
|
25
32
|
|
|
26
33
|
|
|
34
|
+
def _create_rejection_response(file_path: str) -> Dict[str, Any]:
|
|
35
|
+
"""Create a standardized rejection response with user feedback if available.
|
|
36
|
+
|
|
37
|
+
Args:
|
|
38
|
+
file_path: Path to the file that was rejected
|
|
39
|
+
|
|
40
|
+
Returns:
|
|
41
|
+
Dict containing rejection details and any user feedback
|
|
42
|
+
"""
|
|
43
|
+
# Check for user feedback from permission handler
|
|
44
|
+
try:
|
|
45
|
+
from code_puppy.plugins.file_permission_handler.register_callbacks import (
|
|
46
|
+
clear_user_feedback,
|
|
47
|
+
get_last_user_feedback,
|
|
48
|
+
)
|
|
49
|
+
|
|
50
|
+
user_feedback = get_last_user_feedback()
|
|
51
|
+
# Clear feedback after reading it
|
|
52
|
+
clear_user_feedback()
|
|
53
|
+
except ImportError:
|
|
54
|
+
user_feedback = None
|
|
55
|
+
|
|
56
|
+
rejection_message = (
|
|
57
|
+
"USER REJECTED: The user explicitly rejected these file changes."
|
|
58
|
+
)
|
|
59
|
+
if user_feedback:
|
|
60
|
+
rejection_message += f" User feedback: {user_feedback}"
|
|
61
|
+
else:
|
|
62
|
+
rejection_message += " Please do not retry the same changes or any other changes - immediately ask for clarification."
|
|
63
|
+
|
|
64
|
+
return {
|
|
65
|
+
"success": False,
|
|
66
|
+
"path": file_path,
|
|
67
|
+
"message": rejection_message,
|
|
68
|
+
"changed": False,
|
|
69
|
+
"user_rejection": True,
|
|
70
|
+
"rejection_type": "explicit_user_denial",
|
|
71
|
+
"user_feedback": user_feedback,
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
|
|
27
75
|
class DeleteSnippetPayload(BaseModel):
|
|
28
76
|
file_path: str
|
|
29
77
|
delete_snippet: str
|
|
@@ -48,57 +96,110 @@ class ContentPayload(BaseModel):
|
|
|
48
96
|
EditFilePayload = Union[DeleteSnippetPayload, ReplacementsPayload, ContentPayload]
|
|
49
97
|
|
|
50
98
|
|
|
51
|
-
def
|
|
52
|
-
"""
|
|
99
|
+
def _parse_diff_lines(diff_text: str) -> List[DiffLine]:
|
|
100
|
+
"""Parse unified diff text into structured DiffLine objects.
|
|
53
101
|
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
102
|
+
Args:
|
|
103
|
+
diff_text: Raw unified diff text
|
|
104
|
+
|
|
105
|
+
Returns:
|
|
106
|
+
List of DiffLine objects with line numbers and types
|
|
107
|
+
"""
|
|
108
|
+
if not diff_text or not diff_text.strip():
|
|
109
|
+
return []
|
|
110
|
+
|
|
111
|
+
diff_lines = []
|
|
112
|
+
line_number = 0
|
|
113
|
+
|
|
114
|
+
for line in diff_text.splitlines():
|
|
115
|
+
# Determine line type based on diff markers
|
|
116
|
+
if line.startswith("+") and not line.startswith("+++"):
|
|
117
|
+
line_type = "add"
|
|
118
|
+
line_number += 1
|
|
119
|
+
content = line[1:] # Remove the + prefix
|
|
120
|
+
elif line.startswith("-") and not line.startswith("---"):
|
|
121
|
+
line_type = "remove"
|
|
122
|
+
line_number += 1
|
|
123
|
+
content = line[1:] # Remove the - prefix
|
|
124
|
+
elif line.startswith("@@"):
|
|
125
|
+
# Parse hunk header to get line number
|
|
126
|
+
# Format: @@ -start,count +start,count @@
|
|
127
|
+
import re
|
|
128
|
+
|
|
129
|
+
match = re.search(r"@@ -\d+(?:,\d+)? \+(\d+)", line)
|
|
130
|
+
if match:
|
|
131
|
+
line_number = (
|
|
132
|
+
int(match.group(1)) - 1
|
|
133
|
+
) # Will be incremented on next line
|
|
134
|
+
line_type = "context"
|
|
135
|
+
content = line
|
|
136
|
+
elif line.startswith("---") or line.startswith("+++"):
|
|
137
|
+
# File headers - treat as context
|
|
138
|
+
line_type = "context"
|
|
139
|
+
content = line
|
|
140
|
+
else:
|
|
141
|
+
line_type = "context"
|
|
142
|
+
line_number += 1
|
|
143
|
+
content = line
|
|
144
|
+
|
|
145
|
+
diff_lines.append(
|
|
146
|
+
DiffLine(
|
|
147
|
+
line_number=max(1, line_number),
|
|
148
|
+
type=line_type,
|
|
149
|
+
content=content,
|
|
150
|
+
)
|
|
151
|
+
)
|
|
152
|
+
|
|
153
|
+
return diff_lines
|
|
154
|
+
|
|
155
|
+
|
|
156
|
+
def _emit_diff_message(
|
|
157
|
+
file_path: str,
|
|
158
|
+
operation: str,
|
|
159
|
+
diff_text: str,
|
|
160
|
+
old_content: str | None = None,
|
|
161
|
+
new_content: str | None = None,
|
|
162
|
+
) -> None:
|
|
163
|
+
"""Emit a structured DiffMessage for UI display.
|
|
164
|
+
|
|
165
|
+
Args:
|
|
166
|
+
file_path: Path to the file being modified
|
|
167
|
+
operation: One of 'create', 'modify', 'delete'
|
|
168
|
+
diff_text: Raw unified diff text
|
|
169
|
+
old_content: Original file content (optional)
|
|
170
|
+
new_content: New file content (optional)
|
|
171
|
+
"""
|
|
172
|
+
# Check if diff was already shown during permission prompt
|
|
173
|
+
try:
|
|
174
|
+
from code_puppy.plugins.file_permission_handler.register_callbacks import (
|
|
175
|
+
clear_diff_shown_flag,
|
|
176
|
+
was_diff_already_shown,
|
|
177
|
+
)
|
|
178
|
+
|
|
179
|
+
if was_diff_already_shown():
|
|
180
|
+
# Diff already displayed in permission panel, skip redundant display
|
|
181
|
+
clear_diff_shown_flag()
|
|
182
|
+
return
|
|
183
|
+
except ImportError:
|
|
184
|
+
pass # Permission handler not available, emit anyway
|
|
185
|
+
|
|
186
|
+
if not diff_text or not diff_text.strip():
|
|
187
|
+
return
|
|
188
|
+
|
|
189
|
+
diff_lines = _parse_diff_lines(diff_text)
|
|
190
|
+
|
|
191
|
+
diff_msg = DiffMessage(
|
|
192
|
+
path=file_path,
|
|
193
|
+
operation=operation,
|
|
194
|
+
old_content=old_content,
|
|
195
|
+
new_content=new_content,
|
|
196
|
+
diff_lines=diff_lines,
|
|
97
197
|
)
|
|
198
|
+
get_message_bus().emit(diff_msg)
|
|
98
199
|
|
|
99
200
|
|
|
100
201
|
def _log_error(
|
|
101
|
-
msg: str, exc: Exception | None = None, message_group: str = None
|
|
202
|
+
msg: str, exc: Exception | None = None, message_group: str | None = None
|
|
102
203
|
) -> None:
|
|
103
204
|
emit_error(f"{msg}", message_group=message_group)
|
|
104
205
|
if exc is not None:
|
|
@@ -106,28 +207,40 @@ def _log_error(
|
|
|
106
207
|
|
|
107
208
|
|
|
108
209
|
def _delete_snippet_from_file(
|
|
109
|
-
context: RunContext | None,
|
|
210
|
+
context: RunContext | None,
|
|
211
|
+
file_path: str,
|
|
212
|
+
snippet: str,
|
|
213
|
+
message_group: str | None = None,
|
|
110
214
|
) -> Dict[str, Any]:
|
|
111
215
|
file_path = os.path.abspath(file_path)
|
|
112
216
|
diff_text = ""
|
|
113
217
|
try:
|
|
114
218
|
if not os.path.exists(file_path) or not os.path.isfile(file_path):
|
|
115
219
|
return {"error": f"File '{file_path}' does not exist.", "diff": diff_text}
|
|
116
|
-
with open(file_path, "r", encoding="utf-8") as f:
|
|
220
|
+
with open(file_path, "r", encoding="utf-8", errors="surrogateescape") as f:
|
|
117
221
|
original = f.read()
|
|
222
|
+
# Sanitize any surrogate characters from reading
|
|
223
|
+
try:
|
|
224
|
+
original = original.encode("utf-8", errors="surrogatepass").decode(
|
|
225
|
+
"utf-8", errors="replace"
|
|
226
|
+
)
|
|
227
|
+
except (UnicodeEncodeError, UnicodeDecodeError):
|
|
228
|
+
pass
|
|
118
229
|
if snippet not in original:
|
|
119
230
|
return {
|
|
120
231
|
"error": f"Snippet not found in file '{file_path}'.",
|
|
121
232
|
"diff": diff_text,
|
|
122
233
|
}
|
|
123
234
|
modified = original.replace(snippet, "")
|
|
235
|
+
from code_puppy.config import get_diff_context_lines
|
|
236
|
+
|
|
124
237
|
diff_text = "".join(
|
|
125
238
|
difflib.unified_diff(
|
|
126
239
|
original.splitlines(keepends=True),
|
|
127
240
|
modified.splitlines(keepends=True),
|
|
128
241
|
fromfile=f"a/{os.path.basename(file_path)}",
|
|
129
242
|
tofile=f"b/{os.path.basename(file_path)}",
|
|
130
|
-
n=
|
|
243
|
+
n=get_diff_context_lines(),
|
|
131
244
|
)
|
|
132
245
|
)
|
|
133
246
|
with open(file_path, "w", encoding="utf-8") as f:
|
|
@@ -147,14 +260,22 @@ def _replace_in_file(
|
|
|
147
260
|
context: RunContext | None,
|
|
148
261
|
path: str,
|
|
149
262
|
replacements: List[Dict[str, str]],
|
|
150
|
-
message_group: str = None,
|
|
263
|
+
message_group: str | None = None,
|
|
151
264
|
) -> Dict[str, Any]:
|
|
152
265
|
"""Robust replacement engine with explicit edge‑case reporting."""
|
|
153
266
|
file_path = os.path.abspath(path)
|
|
154
267
|
|
|
155
|
-
with open(file_path, "r", encoding="utf-8") as f:
|
|
268
|
+
with open(file_path, "r", encoding="utf-8", errors="surrogateescape") as f:
|
|
156
269
|
original = f.read()
|
|
157
270
|
|
|
271
|
+
# Sanitize any surrogate characters from reading
|
|
272
|
+
try:
|
|
273
|
+
original = original.encode("utf-8", errors="surrogatepass").decode(
|
|
274
|
+
"utf-8", errors="replace"
|
|
275
|
+
)
|
|
276
|
+
except (UnicodeEncodeError, UnicodeDecodeError):
|
|
277
|
+
pass
|
|
278
|
+
|
|
158
279
|
modified = original
|
|
159
280
|
for rep in replacements:
|
|
160
281
|
old_snippet = rep.get("old_str", "")
|
|
@@ -197,13 +318,15 @@ def _replace_in_file(
|
|
|
197
318
|
"diff": "",
|
|
198
319
|
}
|
|
199
320
|
|
|
321
|
+
from code_puppy.config import get_diff_context_lines
|
|
322
|
+
|
|
200
323
|
diff_text = "".join(
|
|
201
324
|
difflib.unified_diff(
|
|
202
325
|
original.splitlines(keepends=True),
|
|
203
326
|
modified.splitlines(keepends=True),
|
|
204
327
|
fromfile=f"a/{os.path.basename(file_path)}",
|
|
205
328
|
tofile=f"b/{os.path.basename(file_path)}",
|
|
206
|
-
n=
|
|
329
|
+
n=get_diff_context_lines(),
|
|
207
330
|
)
|
|
208
331
|
)
|
|
209
332
|
with open(file_path, "w", encoding="utf-8") as f:
|
|
@@ -222,7 +345,7 @@ def _write_to_file(
|
|
|
222
345
|
path: str,
|
|
223
346
|
content: str,
|
|
224
347
|
overwrite: bool = False,
|
|
225
|
-
message_group: str = None,
|
|
348
|
+
message_group: str | None = None,
|
|
226
349
|
) -> Dict[str, Any]:
|
|
227
350
|
file_path = os.path.abspath(path)
|
|
228
351
|
|
|
@@ -237,12 +360,14 @@ def _write_to_file(
|
|
|
237
360
|
"diff": "",
|
|
238
361
|
}
|
|
239
362
|
|
|
363
|
+
from code_puppy.config import get_diff_context_lines
|
|
364
|
+
|
|
240
365
|
diff_lines = difflib.unified_diff(
|
|
241
366
|
[] if not exists else [""],
|
|
242
367
|
content.splitlines(keepends=True),
|
|
243
368
|
fromfile="/dev/null" if not exists else f"a/{os.path.basename(file_path)}",
|
|
244
369
|
tofile=f"b/{os.path.basename(file_path)}",
|
|
245
|
-
n=
|
|
370
|
+
n=get_diff_context_lines(),
|
|
246
371
|
)
|
|
247
372
|
diff_text = "".join(diff_lines)
|
|
248
373
|
|
|
@@ -265,18 +390,28 @@ def _write_to_file(
|
|
|
265
390
|
|
|
266
391
|
|
|
267
392
|
def delete_snippet_from_file(
|
|
268
|
-
context: RunContext, file_path: str, snippet: str, message_group: str = None
|
|
393
|
+
context: RunContext, file_path: str, snippet: str, message_group: str | None = None
|
|
269
394
|
) -> Dict[str, Any]:
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
395
|
+
# Use the plugin system for permission handling with operation data
|
|
396
|
+
from code_puppy.callbacks import on_file_permission
|
|
397
|
+
|
|
398
|
+
operation_data = {"snippet": snippet}
|
|
399
|
+
permission_results = on_file_permission(
|
|
400
|
+
context, file_path, "delete snippet from", None, message_group, operation_data
|
|
273
401
|
)
|
|
402
|
+
|
|
403
|
+
# If any permission handler denies the operation, return cancelled result
|
|
404
|
+
if permission_results and any(
|
|
405
|
+
not result for result in permission_results if result is not None
|
|
406
|
+
):
|
|
407
|
+
return _create_rejection_response(file_path)
|
|
408
|
+
|
|
274
409
|
res = _delete_snippet_from_file(
|
|
275
410
|
context, file_path, snippet, message_group=message_group
|
|
276
411
|
)
|
|
277
412
|
diff = res.get("diff", "")
|
|
278
413
|
if diff:
|
|
279
|
-
|
|
414
|
+
_emit_diff_message(file_path, "modify", diff)
|
|
280
415
|
return res
|
|
281
416
|
|
|
282
417
|
|
|
@@ -285,17 +420,30 @@ def write_to_file(
|
|
|
285
420
|
path: str,
|
|
286
421
|
content: str,
|
|
287
422
|
overwrite: bool,
|
|
288
|
-
message_group: str = None,
|
|
423
|
+
message_group: str | None = None,
|
|
289
424
|
) -> Dict[str, Any]:
|
|
290
|
-
|
|
291
|
-
|
|
425
|
+
# Use the plugin system for permission handling with operation data
|
|
426
|
+
from code_puppy.callbacks import on_file_permission
|
|
427
|
+
|
|
428
|
+
operation_data = {"content": content, "overwrite": overwrite}
|
|
429
|
+
permission_results = on_file_permission(
|
|
430
|
+
context, path, "write", None, message_group, operation_data
|
|
292
431
|
)
|
|
432
|
+
|
|
433
|
+
# If any permission handler denies the operation, return cancelled result
|
|
434
|
+
if permission_results and any(
|
|
435
|
+
not result for result in permission_results if result is not None
|
|
436
|
+
):
|
|
437
|
+
return _create_rejection_response(path)
|
|
438
|
+
|
|
293
439
|
res = _write_to_file(
|
|
294
440
|
context, path, content, overwrite=overwrite, message_group=message_group
|
|
295
441
|
)
|
|
296
442
|
diff = res.get("diff", "")
|
|
297
443
|
if diff:
|
|
298
|
-
|
|
444
|
+
# Determine operation type based on whether file existed
|
|
445
|
+
operation = "modify" if overwrite else "create"
|
|
446
|
+
_emit_diff_message(path, operation, diff, new_content=content)
|
|
299
447
|
return res
|
|
300
448
|
|
|
301
449
|
|
|
@@ -303,21 +451,31 @@ def replace_in_file(
|
|
|
303
451
|
context: RunContext,
|
|
304
452
|
path: str,
|
|
305
453
|
replacements: List[Dict[str, str]],
|
|
306
|
-
message_group: str = None,
|
|
454
|
+
message_group: str | None = None,
|
|
307
455
|
) -> Dict[str, Any]:
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
456
|
+
# Use the plugin system for permission handling with operation data
|
|
457
|
+
from code_puppy.callbacks import on_file_permission
|
|
458
|
+
|
|
459
|
+
operation_data = {"replacements": replacements}
|
|
460
|
+
permission_results = on_file_permission(
|
|
461
|
+
context, path, "replace text in", None, message_group, operation_data
|
|
311
462
|
)
|
|
463
|
+
|
|
464
|
+
# If any permission handler denies the operation, return cancelled result
|
|
465
|
+
if permission_results and any(
|
|
466
|
+
not result for result in permission_results if result is not None
|
|
467
|
+
):
|
|
468
|
+
return _create_rejection_response(path)
|
|
469
|
+
|
|
312
470
|
res = _replace_in_file(context, path, replacements, message_group=message_group)
|
|
313
471
|
diff = res.get("diff", "")
|
|
314
472
|
if diff:
|
|
315
|
-
|
|
473
|
+
_emit_diff_message(path, "modify", diff)
|
|
316
474
|
return res
|
|
317
475
|
|
|
318
476
|
|
|
319
477
|
def _edit_file(
|
|
320
|
-
context: RunContext, payload: EditFilePayload, group_id: str = None
|
|
478
|
+
context: RunContext, payload: EditFilePayload, group_id: str | None = None
|
|
321
479
|
) -> Dict[str, Any]:
|
|
322
480
|
"""
|
|
323
481
|
High-level implementation of the *edit_file* behaviour.
|
|
@@ -356,9 +514,6 @@ def _edit_file(
|
|
|
356
514
|
if group_id is None:
|
|
357
515
|
group_id = generate_group_id("edit_file", file_path)
|
|
358
516
|
|
|
359
|
-
emit_info(
|
|
360
|
-
"\n[bold white on blue] EDIT FILE [/bold white on blue]", message_group=group_id
|
|
361
|
-
)
|
|
362
517
|
try:
|
|
363
518
|
if isinstance(payload, DeleteSnippetPayload):
|
|
364
519
|
return delete_snippet_from_file(
|
|
@@ -411,25 +566,46 @@ def _edit_file(
|
|
|
411
566
|
|
|
412
567
|
|
|
413
568
|
def _delete_file(
|
|
414
|
-
context: RunContext, file_path: str, message_group: str = None
|
|
569
|
+
context: RunContext, file_path: str, message_group: str | None = None
|
|
415
570
|
) -> Dict[str, Any]:
|
|
416
|
-
emit_info(
|
|
417
|
-
f"🗑️ Deleting file [bold red]{file_path}[/bold red]", message_group=message_group
|
|
418
|
-
)
|
|
419
571
|
file_path = os.path.abspath(file_path)
|
|
572
|
+
|
|
573
|
+
# Use the plugin system for permission handling with operation data
|
|
574
|
+
from code_puppy.callbacks import on_file_permission
|
|
575
|
+
|
|
576
|
+
operation_data = {} # No additional data needed for delete operations
|
|
577
|
+
permission_results = on_file_permission(
|
|
578
|
+
context, file_path, "delete", None, message_group, operation_data
|
|
579
|
+
)
|
|
580
|
+
|
|
581
|
+
# If any permission handler denies the operation, return cancelled result
|
|
582
|
+
if permission_results and any(
|
|
583
|
+
not result for result in permission_results if result is not None
|
|
584
|
+
):
|
|
585
|
+
return _create_rejection_response(file_path)
|
|
586
|
+
|
|
420
587
|
try:
|
|
421
588
|
if not os.path.exists(file_path) or not os.path.isfile(file_path):
|
|
422
589
|
res = {"error": f"File '{file_path}' does not exist.", "diff": ""}
|
|
423
590
|
else:
|
|
424
|
-
with open(file_path, "r", encoding="utf-8") as f:
|
|
591
|
+
with open(file_path, "r", encoding="utf-8", errors="surrogateescape") as f:
|
|
425
592
|
original = f.read()
|
|
593
|
+
# Sanitize any surrogate characters from reading
|
|
594
|
+
try:
|
|
595
|
+
original = original.encode("utf-8", errors="surrogatepass").decode(
|
|
596
|
+
"utf-8", errors="replace"
|
|
597
|
+
)
|
|
598
|
+
except (UnicodeEncodeError, UnicodeDecodeError):
|
|
599
|
+
pass
|
|
600
|
+
from code_puppy.config import get_diff_context_lines
|
|
601
|
+
|
|
426
602
|
diff_text = "".join(
|
|
427
603
|
difflib.unified_diff(
|
|
428
604
|
original.splitlines(keepends=True),
|
|
429
605
|
[],
|
|
430
606
|
fromfile=f"a/{os.path.basename(file_path)}",
|
|
431
607
|
tofile=f"b/{os.path.basename(file_path)}",
|
|
432
|
-
n=
|
|
608
|
+
n=get_diff_context_lines(),
|
|
433
609
|
)
|
|
434
610
|
)
|
|
435
611
|
os.remove(file_path)
|
|
@@ -443,7 +619,10 @@ def _delete_file(
|
|
|
443
619
|
except Exception as exc:
|
|
444
620
|
_log_error("Unhandled exception in delete_file", exc)
|
|
445
621
|
res = {"error": str(exc), "diff": ""}
|
|
446
|
-
|
|
622
|
+
|
|
623
|
+
diff = res.get("diff", "")
|
|
624
|
+
if diff:
|
|
625
|
+
_emit_diff_message(file_path, "delete", diff)
|
|
447
626
|
return res
|
|
448
627
|
|
|
449
628
|
|
|
@@ -546,17 +725,17 @@ def register_edit_file(agent):
|
|
|
546
725
|
if isinstance(payload, str):
|
|
547
726
|
try:
|
|
548
727
|
# Fallback for weird models that just can't help but send json strings...
|
|
549
|
-
|
|
550
|
-
if "replacements" in
|
|
551
|
-
payload = ReplacementsPayload(**
|
|
552
|
-
elif "delete_snippet" in
|
|
553
|
-
payload = DeleteSnippetPayload(**
|
|
554
|
-
elif "content" in
|
|
555
|
-
payload = ContentPayload(**
|
|
728
|
+
payload_dict = json.loads(json_repair.repair_json(payload))
|
|
729
|
+
if "replacements" in payload_dict:
|
|
730
|
+
payload = ReplacementsPayload(**payload_dict)
|
|
731
|
+
elif "delete_snippet" in payload_dict:
|
|
732
|
+
payload = DeleteSnippetPayload(**payload_dict)
|
|
733
|
+
elif "content" in payload_dict:
|
|
734
|
+
payload = ContentPayload(**payload_dict)
|
|
556
735
|
else:
|
|
557
736
|
file_path = "Unknown"
|
|
558
|
-
if "file_path" in
|
|
559
|
-
file_path =
|
|
737
|
+
if "file_path" in payload_dict:
|
|
738
|
+
file_path = payload_dict["file_path"]
|
|
560
739
|
return {
|
|
561
740
|
"success": False,
|
|
562
741
|
"path": file_path,
|
|
@@ -575,6 +754,16 @@ def register_edit_file(agent):
|
|
|
575
754
|
result = _edit_file(context, payload)
|
|
576
755
|
if "diff" in result:
|
|
577
756
|
del result["diff"]
|
|
757
|
+
|
|
758
|
+
# Trigger edit_file callbacks to enhance the result with rejection details
|
|
759
|
+
enhanced_results = on_edit_file(context, result, payload)
|
|
760
|
+
if enhanced_results:
|
|
761
|
+
# Use the first non-None enhanced result
|
|
762
|
+
for enhanced_result in enhanced_results:
|
|
763
|
+
if enhanced_result is not None:
|
|
764
|
+
result = enhanced_result
|
|
765
|
+
break
|
|
766
|
+
|
|
578
767
|
return result
|
|
579
768
|
|
|
580
769
|
|
|
@@ -624,4 +813,14 @@ def register_delete_file(agent):
|
|
|
624
813
|
result = _delete_file(context, file_path, message_group=group_id)
|
|
625
814
|
if "diff" in result:
|
|
626
815
|
del result["diff"]
|
|
816
|
+
|
|
817
|
+
# Trigger delete_file callbacks to enhance the result with rejection details
|
|
818
|
+
enhanced_results = on_delete_file(context, result, file_path)
|
|
819
|
+
if enhanced_results:
|
|
820
|
+
# Use the first non-None enhanced result
|
|
821
|
+
for enhanced_result in enhanced_results:
|
|
822
|
+
if enhanced_result is not None:
|
|
823
|
+
result = enhanced_result
|
|
824
|
+
break
|
|
825
|
+
|
|
627
826
|
return result
|