codepp 0.0.437__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 +10 -0
- code_puppy/__main__.py +10 -0
- code_puppy/agents/__init__.py +31 -0
- code_puppy/agents/agent_c_reviewer.py +155 -0
- code_puppy/agents/agent_code_puppy.py +117 -0
- code_puppy/agents/agent_code_reviewer.py +90 -0
- code_puppy/agents/agent_cpp_reviewer.py +132 -0
- code_puppy/agents/agent_creator_agent.py +638 -0
- code_puppy/agents/agent_golang_reviewer.py +151 -0
- code_puppy/agents/agent_helios.py +124 -0
- code_puppy/agents/agent_javascript_reviewer.py +160 -0
- code_puppy/agents/agent_manager.py +742 -0
- code_puppy/agents/agent_pack_leader.py +385 -0
- code_puppy/agents/agent_planning.py +165 -0
- code_puppy/agents/agent_python_programmer.py +169 -0
- code_puppy/agents/agent_python_reviewer.py +90 -0
- code_puppy/agents/agent_qa_expert.py +163 -0
- code_puppy/agents/agent_qa_kitten.py +208 -0
- code_puppy/agents/agent_scheduler.py +121 -0
- code_puppy/agents/agent_security_auditor.py +181 -0
- code_puppy/agents/agent_terminal_qa.py +323 -0
- code_puppy/agents/agent_typescript_reviewer.py +166 -0
- code_puppy/agents/base_agent.py +2156 -0
- code_puppy/agents/event_stream_handler.py +348 -0
- code_puppy/agents/json_agent.py +202 -0
- code_puppy/agents/pack/__init__.py +34 -0
- code_puppy/agents/pack/bloodhound.py +304 -0
- code_puppy/agents/pack/husky.py +327 -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 +453 -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 +75 -0
- code_puppy/api/routers/sessions.py +234 -0
- code_puppy/api/templates/terminal.html +361 -0
- code_puppy/api/websocket.py +154 -0
- code_puppy/callbacks.py +692 -0
- code_puppy/chatgpt_codex_client.py +338 -0
- code_puppy/claude_cache_client.py +672 -0
- code_puppy/cli_runner.py +1073 -0
- code_puppy/command_line/__init__.py +1 -0
- code_puppy/command_line/add_model_menu.py +1092 -0
- code_puppy/command_line/agent_menu.py +662 -0
- code_puppy/command_line/attachments.py +395 -0
- code_puppy/command_line/autosave_menu.py +704 -0
- code_puppy/command_line/clipboard.py +527 -0
- code_puppy/command_line/colors_menu.py +532 -0
- code_puppy/command_line/command_handler.py +293 -0
- code_puppy/command_line/command_registry.py +150 -0
- code_puppy/command_line/config_commands.py +719 -0
- code_puppy/command_line/core_commands.py +867 -0
- code_puppy/command_line/diff_menu.py +865 -0
- code_puppy/command_line/file_path_completion.py +73 -0
- code_puppy/command_line/load_context_completion.py +52 -0
- code_puppy/command_line/mcp/__init__.py +10 -0
- code_puppy/command_line/mcp/base.py +32 -0
- 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 +138 -0
- code_puppy/command_line/mcp/help_command.py +147 -0
- code_puppy/command_line/mcp/install_command.py +214 -0
- code_puppy/command_line/mcp/install_menu.py +705 -0
- code_puppy/command_line/mcp/list_command.py +94 -0
- code_puppy/command_line/mcp/logs_command.py +235 -0
- code_puppy/command_line/mcp/remove_command.py +82 -0
- code_puppy/command_line/mcp/restart_command.py +100 -0
- code_puppy/command_line/mcp/search_command.py +123 -0
- code_puppy/command_line/mcp/start_all_command.py +135 -0
- code_puppy/command_line/mcp/start_command.py +117 -0
- code_puppy/command_line/mcp/status_command.py +184 -0
- code_puppy/command_line/mcp/stop_all_command.py +112 -0
- code_puppy/command_line/mcp/stop_command.py +80 -0
- code_puppy/command_line/mcp/test_command.py +107 -0
- code_puppy/command_line/mcp/utils.py +129 -0
- code_puppy/command_line/mcp/wizard_utils.py +334 -0
- code_puppy/command_line/mcp_completion.py +174 -0
- code_puppy/command_line/model_picker_completion.py +197 -0
- code_puppy/command_line/model_settings_menu.py +932 -0
- code_puppy/command_line/motd.py +96 -0
- code_puppy/command_line/onboarding_slides.py +179 -0
- code_puppy/command_line/onboarding_wizard.py +342 -0
- code_puppy/command_line/pin_command_completion.py +329 -0
- code_puppy/command_line/prompt_toolkit_completion.py +846 -0
- code_puppy/command_line/session_commands.py +302 -0
- code_puppy/command_line/shell_passthrough.py +145 -0
- code_puppy/command_line/skills_completion.py +160 -0
- code_puppy/command_line/uc_menu.py +893 -0
- code_puppy/command_line/utils.py +93 -0
- code_puppy/command_line/wiggum_state.py +78 -0
- code_puppy/config.py +1770 -0
- code_puppy/error_logging.py +134 -0
- code_puppy/gemini_code_assist.py +385 -0
- code_puppy/gemini_model.py +754 -0
- code_puppy/hook_engine/README.md +105 -0
- code_puppy/hook_engine/__init__.py +21 -0
- code_puppy/hook_engine/aliases.py +155 -0
- code_puppy/hook_engine/engine.py +221 -0
- code_puppy/hook_engine/executor.py +296 -0
- code_puppy/hook_engine/matcher.py +156 -0
- code_puppy/hook_engine/models.py +240 -0
- code_puppy/hook_engine/registry.py +106 -0
- code_puppy/hook_engine/validator.py +144 -0
- code_puppy/http_utils.py +361 -0
- code_puppy/keymap.py +128 -0
- code_puppy/main.py +10 -0
- code_puppy/mcp_/__init__.py +66 -0
- code_puppy/mcp_/async_lifecycle.py +286 -0
- code_puppy/mcp_/blocking_startup.py +469 -0
- code_puppy/mcp_/captured_stdio_server.py +275 -0
- code_puppy/mcp_/circuit_breaker.py +290 -0
- code_puppy/mcp_/config_wizard.py +507 -0
- code_puppy/mcp_/dashboard.py +308 -0
- code_puppy/mcp_/error_isolation.py +407 -0
- code_puppy/mcp_/examples/retry_example.py +226 -0
- code_puppy/mcp_/health_monitor.py +589 -0
- code_puppy/mcp_/managed_server.py +428 -0
- code_puppy/mcp_/manager.py +807 -0
- code_puppy/mcp_/mcp_logs.py +224 -0
- code_puppy/mcp_/registry.py +451 -0
- code_puppy/mcp_/retry_manager.py +337 -0
- code_puppy/mcp_/server_registry_catalog.py +1126 -0
- code_puppy/mcp_/status_tracker.py +355 -0
- code_puppy/mcp_/system_tools.py +209 -0
- code_puppy/mcp_prompts/__init__.py +1 -0
- code_puppy/mcp_prompts/hook_creator.py +103 -0
- code_puppy/messaging/__init__.py +255 -0
- code_puppy/messaging/bus.py +613 -0
- code_puppy/messaging/commands.py +167 -0
- code_puppy/messaging/markdown_patches.py +57 -0
- code_puppy/messaging/message_queue.py +361 -0
- code_puppy/messaging/messages.py +569 -0
- code_puppy/messaging/queue_console.py +271 -0
- code_puppy/messaging/renderers.py +311 -0
- code_puppy/messaging/rich_renderer.py +1158 -0
- code_puppy/messaging/spinner/__init__.py +83 -0
- code_puppy/messaging/spinner/console_spinner.py +240 -0
- code_puppy/messaging/spinner/spinner_base.py +95 -0
- code_puppy/messaging/subagent_console.py +460 -0
- code_puppy/model_factory.py +848 -0
- code_puppy/model_switching.py +63 -0
- code_puppy/model_utils.py +168 -0
- code_puppy/models.json +174 -0
- code_puppy/models_dev_api.json +1 -0
- code_puppy/models_dev_parser.py +592 -0
- code_puppy/plugins/__init__.py +186 -0
- code_puppy/plugins/agent_skills/__init__.py +22 -0
- code_puppy/plugins/agent_skills/config.py +175 -0
- code_puppy/plugins/agent_skills/discovery.py +136 -0
- code_puppy/plugins/agent_skills/downloader.py +392 -0
- code_puppy/plugins/agent_skills/installer.py +22 -0
- code_puppy/plugins/agent_skills/metadata.py +219 -0
- code_puppy/plugins/agent_skills/prompt_builder.py +60 -0
- code_puppy/plugins/agent_skills/register_callbacks.py +241 -0
- code_puppy/plugins/agent_skills/remote_catalog.py +322 -0
- code_puppy/plugins/agent_skills/skill_catalog.py +257 -0
- code_puppy/plugins/agent_skills/skills_install_menu.py +664 -0
- code_puppy/plugins/agent_skills/skills_menu.py +781 -0
- 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 +706 -0
- code_puppy/plugins/antigravity_oauth/config.py +42 -0
- code_puppy/plugins/antigravity_oauth/constants.py +133 -0
- code_puppy/plugins/antigravity_oauth/oauth.py +478 -0
- code_puppy/plugins/antigravity_oauth/register_callbacks.py +518 -0
- code_puppy/plugins/antigravity_oauth/storage.py +288 -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 +863 -0
- code_puppy/plugins/antigravity_oauth/utils.py +168 -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 +329 -0
- code_puppy/plugins/chatgpt_oauth/register_callbacks.py +176 -0
- code_puppy/plugins/chatgpt_oauth/test_plugin.py +301 -0
- code_puppy/plugins/chatgpt_oauth/utils.py +523 -0
- code_puppy/plugins/claude_code_hooks/__init__.py +1 -0
- code_puppy/plugins/claude_code_hooks/config.py +137 -0
- code_puppy/plugins/claude_code_hooks/register_callbacks.py +175 -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 +25 -0
- code_puppy/plugins/claude_code_oauth/config.py +52 -0
- code_puppy/plugins/claude_code_oauth/register_callbacks.py +453 -0
- code_puppy/plugins/claude_code_oauth/test_plugin.py +283 -0
- code_puppy/plugins/claude_code_oauth/token_refresh_heartbeat.py +241 -0
- code_puppy/plugins/claude_code_oauth/utils.py +640 -0
- code_puppy/plugins/customizable_commands/__init__.py +0 -0
- code_puppy/plugins/customizable_commands/register_callbacks.py +152 -0
- code_puppy/plugins/example_custom_command/README.md +280 -0
- code_puppy/plugins/example_custom_command/register_callbacks.py +51 -0
- code_puppy/plugins/file_permission_handler/__init__.py +4 -0
- code_puppy/plugins/file_permission_handler/register_callbacks.py +470 -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/hook_creator/__init__.py +1 -0
- code_puppy/plugins/hook_creator/register_callbacks.py +33 -0
- code_puppy/plugins/hook_manager/__init__.py +1 -0
- code_puppy/plugins/hook_manager/config.py +290 -0
- code_puppy/plugins/hook_manager/hooks_menu.py +564 -0
- code_puppy/plugins/hook_manager/register_callbacks.py +227 -0
- code_puppy/plugins/oauth_puppy_html.py +228 -0
- code_puppy/plugins/scheduler/__init__.py +1 -0
- code_puppy/plugins/scheduler/register_callbacks.py +88 -0
- code_puppy/plugins/scheduler/scheduler_menu.py +522 -0
- code_puppy/plugins/scheduler/scheduler_wizard.py +341 -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/plugins/synthetic_status/__init__.py +1 -0
- code_puppy/plugins/synthetic_status/register_callbacks.py +132 -0
- code_puppy/plugins/synthetic_status/status_api.py +147 -0
- code_puppy/plugins/universal_constructor/__init__.py +13 -0
- code_puppy/plugins/universal_constructor/models.py +138 -0
- code_puppy/plugins/universal_constructor/register_callbacks.py +47 -0
- code_puppy/plugins/universal_constructor/registry.py +302 -0
- code_puppy/plugins/universal_constructor/sandbox.py +584 -0
- code_puppy/prompts/antigravity_system_prompt.md +1 -0
- code_puppy/pydantic_patches.py +356 -0
- code_puppy/reopenable_async_client.py +232 -0
- code_puppy/round_robin_model.py +150 -0
- code_puppy/scheduler/__init__.py +41 -0
- code_puppy/scheduler/__main__.py +9 -0
- code_puppy/scheduler/cli.py +118 -0
- code_puppy/scheduler/config.py +126 -0
- code_puppy/scheduler/daemon.py +280 -0
- code_puppy/scheduler/executor.py +155 -0
- code_puppy/scheduler/platform.py +19 -0
- code_puppy/scheduler/platform_unix.py +22 -0
- code_puppy/scheduler/platform_win.py +32 -0
- code_puppy/session_storage.py +338 -0
- code_puppy/status_display.py +257 -0
- code_puppy/summarization_agent.py +176 -0
- code_puppy/terminal_utils.py +418 -0
- code_puppy/tools/__init__.py +501 -0
- code_puppy/tools/agent_tools.py +603 -0
- code_puppy/tools/ask_user_question/__init__.py +26 -0
- code_puppy/tools/ask_user_question/constants.py +73 -0
- code_puppy/tools/ask_user_question/demo_tui.py +55 -0
- code_puppy/tools/ask_user_question/handler.py +232 -0
- code_puppy/tools/ask_user_question/models.py +304 -0
- code_puppy/tools/ask_user_question/registration.py +26 -0
- code_puppy/tools/ask_user_question/renderers.py +309 -0
- code_puppy/tools/ask_user_question/terminal_ui.py +329 -0
- code_puppy/tools/ask_user_question/theme.py +155 -0
- code_puppy/tools/ask_user_question/tui_loop.py +423 -0
- code_puppy/tools/browser/__init__.py +37 -0
- code_puppy/tools/browser/browser_control.py +289 -0
- code_puppy/tools/browser/browser_interactions.py +545 -0
- code_puppy/tools/browser/browser_locators.py +640 -0
- code_puppy/tools/browser/browser_manager.py +378 -0
- code_puppy/tools/browser/browser_navigation.py +251 -0
- code_puppy/tools/browser/browser_screenshot.py +179 -0
- code_puppy/tools/browser/browser_scripts.py +462 -0
- code_puppy/tools/browser/browser_workflows.py +221 -0
- code_puppy/tools/browser/chromium_terminal_manager.py +259 -0
- code_puppy/tools/browser/terminal_command_tools.py +534 -0
- code_puppy/tools/browser/terminal_screenshot_tools.py +552 -0
- code_puppy/tools/browser/terminal_tools.py +525 -0
- code_puppy/tools/command_runner.py +1346 -0
- code_puppy/tools/common.py +1409 -0
- code_puppy/tools/display.py +84 -0
- code_puppy/tools/file_modifications.py +886 -0
- code_puppy/tools/file_operations.py +802 -0
- code_puppy/tools/scheduler_tools.py +412 -0
- code_puppy/tools/skills_tools.py +244 -0
- code_puppy/tools/subagent_context.py +158 -0
- code_puppy/tools/tools_content.py +51 -0
- code_puppy/tools/universal_constructor.py +889 -0
- code_puppy/uvx_detection.py +242 -0
- code_puppy/version_checker.py +82 -0
- codepp-0.0.437.dist-info/METADATA +766 -0
- codepp-0.0.437.dist-info/RECORD +288 -0
- codepp-0.0.437.dist-info/WHEEL +4 -0
- codepp-0.0.437.dist-info/entry_points.txt +3 -0
- codepp-0.0.437.dist-info/licenses/LICENSE +21 -0
|
@@ -0,0 +1,1409 @@
|
|
|
1
|
+
import asyncio
|
|
2
|
+
import fnmatch
|
|
3
|
+
import hashlib
|
|
4
|
+
import os
|
|
5
|
+
import sys
|
|
6
|
+
import time
|
|
7
|
+
from pathlib import Path
|
|
8
|
+
from typing import Callable, Optional, Tuple
|
|
9
|
+
|
|
10
|
+
from prompt_toolkit import Application
|
|
11
|
+
from prompt_toolkit.formatted_text import HTML
|
|
12
|
+
from prompt_toolkit.key_binding import KeyBindings
|
|
13
|
+
from prompt_toolkit.layout import Layout, Window
|
|
14
|
+
from prompt_toolkit.layout.controls import FormattedTextControl
|
|
15
|
+
from rapidfuzz.distance import JaroWinkler
|
|
16
|
+
from rich.console import Console
|
|
17
|
+
from rich.panel import Panel
|
|
18
|
+
from rich.prompt import Prompt
|
|
19
|
+
from rich.text import Text
|
|
20
|
+
|
|
21
|
+
# Syntax highlighting imports for "syntax" diff mode
|
|
22
|
+
try:
|
|
23
|
+
from pygments import lex
|
|
24
|
+
from pygments.lexers import TextLexer, get_lexer_by_name
|
|
25
|
+
from pygments.token import Token
|
|
26
|
+
|
|
27
|
+
PYGMENTS_AVAILABLE = True
|
|
28
|
+
except ImportError:
|
|
29
|
+
PYGMENTS_AVAILABLE = False
|
|
30
|
+
|
|
31
|
+
# Import our queue-based console system
|
|
32
|
+
try:
|
|
33
|
+
from code_puppy.messaging import (
|
|
34
|
+
emit_error,
|
|
35
|
+
emit_info,
|
|
36
|
+
emit_success,
|
|
37
|
+
emit_warning,
|
|
38
|
+
get_queue_console,
|
|
39
|
+
)
|
|
40
|
+
|
|
41
|
+
# Use queue console by default, but allow fallback
|
|
42
|
+
NO_COLOR = bool(int(os.environ.get("CODE_PUPPY_NO_COLOR", "0")))
|
|
43
|
+
_rich_console = Console(no_color=NO_COLOR)
|
|
44
|
+
console = get_queue_console()
|
|
45
|
+
# Set the fallback console for compatibility
|
|
46
|
+
console.fallback_console = _rich_console
|
|
47
|
+
except ImportError:
|
|
48
|
+
# Fallback to regular Rich console if messaging system not available
|
|
49
|
+
NO_COLOR = bool(int(os.environ.get("CODE_PUPPY_NO_COLOR", "0")))
|
|
50
|
+
console = Console(no_color=NO_COLOR)
|
|
51
|
+
|
|
52
|
+
# Provide fallback emit functions
|
|
53
|
+
def emit_error(msg: str) -> None:
|
|
54
|
+
console.print(f"[bold red]{msg}[/bold red]")
|
|
55
|
+
|
|
56
|
+
def emit_info(msg: str) -> None:
|
|
57
|
+
console.print(msg)
|
|
58
|
+
|
|
59
|
+
def emit_success(msg: str) -> None:
|
|
60
|
+
console.print(f"[bold green]{msg}[/bold green]")
|
|
61
|
+
|
|
62
|
+
def emit_warning(msg: str) -> None:
|
|
63
|
+
console.print(f"[bold yellow]{msg}[/bold yellow]")
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
def should_suppress_browser() -> bool:
|
|
67
|
+
"""Check if browsers should be suppressed (headless mode).
|
|
68
|
+
|
|
69
|
+
Returns:
|
|
70
|
+
True if browsers should be suppressed, False if they can open normally
|
|
71
|
+
|
|
72
|
+
This respects multiple headless mode controls:
|
|
73
|
+
- HEADLESS=true environment variable (suppresses ALL browsers)
|
|
74
|
+
- BROWSER_HEADLESS=true environment variable (for browser automation)
|
|
75
|
+
- CI=true environment variable (continuous integration)
|
|
76
|
+
- PYTEST_CURRENT_TEST environment variable (running under pytest)
|
|
77
|
+
"""
|
|
78
|
+
# Explicit headless mode
|
|
79
|
+
if os.getenv("HEADLESS", "").lower() == "true":
|
|
80
|
+
return True
|
|
81
|
+
|
|
82
|
+
# Browser-specific headless mode
|
|
83
|
+
if os.getenv("BROWSER_HEADLESS", "").lower() == "true":
|
|
84
|
+
return True
|
|
85
|
+
|
|
86
|
+
# Continuous integration environments
|
|
87
|
+
if os.getenv("CI", "").lower() == "true":
|
|
88
|
+
return True
|
|
89
|
+
|
|
90
|
+
# Running under pytest
|
|
91
|
+
if "PYTEST_CURRENT_TEST" in os.environ:
|
|
92
|
+
return True
|
|
93
|
+
|
|
94
|
+
# Default to allowing browsers
|
|
95
|
+
return False
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
# -------------------
|
|
99
|
+
# Shared ignore patterns/helpers
|
|
100
|
+
# Split into directory vs file patterns so tools can choose appropriately
|
|
101
|
+
# - list_files should ignore only directories (still show binary files inside non-ignored dirs)
|
|
102
|
+
# - grep should ignore both directories and files (avoid grepping binaries)
|
|
103
|
+
# -------------------
|
|
104
|
+
DIR_IGNORE_PATTERNS = [
|
|
105
|
+
# Version control
|
|
106
|
+
"**/.git/**",
|
|
107
|
+
"**/.git",
|
|
108
|
+
".git/**",
|
|
109
|
+
".git",
|
|
110
|
+
"**/.svn/**",
|
|
111
|
+
"**/.hg/**",
|
|
112
|
+
"**/.bzr/**",
|
|
113
|
+
# Node.js / JavaScript / TypeScript
|
|
114
|
+
"**/node_modules/**",
|
|
115
|
+
"**/node_modules/**/*.js",
|
|
116
|
+
"node_modules/**",
|
|
117
|
+
"node_modules",
|
|
118
|
+
"**/npm-debug.log*",
|
|
119
|
+
"**/yarn-debug.log*",
|
|
120
|
+
"**/yarn-error.log*",
|
|
121
|
+
"**/pnpm-debug.log*",
|
|
122
|
+
"**/.npm/**",
|
|
123
|
+
"**/.yarn/**",
|
|
124
|
+
"**/.pnpm-store/**",
|
|
125
|
+
"**/coverage/**",
|
|
126
|
+
"**/.nyc_output/**",
|
|
127
|
+
"**/dist/**",
|
|
128
|
+
"**/dist",
|
|
129
|
+
"**/build/**",
|
|
130
|
+
"**/build",
|
|
131
|
+
"**/.next/**",
|
|
132
|
+
"**/.nuxt/**",
|
|
133
|
+
"**/out/**",
|
|
134
|
+
"**/.cache/**",
|
|
135
|
+
"**/.parcel-cache/**",
|
|
136
|
+
"**/.vite/**",
|
|
137
|
+
"**/storybook-static/**",
|
|
138
|
+
"**/*.tsbuildinfo/**",
|
|
139
|
+
# Python
|
|
140
|
+
"**/__pycache__/**",
|
|
141
|
+
"**/__pycache__",
|
|
142
|
+
"__pycache__/**",
|
|
143
|
+
"__pycache__",
|
|
144
|
+
"**/*.pyc",
|
|
145
|
+
"**/*.pyo",
|
|
146
|
+
"**/*.pyd",
|
|
147
|
+
"**/.pytest_cache/**",
|
|
148
|
+
"**/.mypy_cache/**",
|
|
149
|
+
"**/.coverage",
|
|
150
|
+
"**/htmlcov/**",
|
|
151
|
+
"**/.tox/**",
|
|
152
|
+
"**/.nox/**",
|
|
153
|
+
"**/site-packages/**",
|
|
154
|
+
"**/.venv/**",
|
|
155
|
+
"**/.venv",
|
|
156
|
+
"**/venv/**",
|
|
157
|
+
"**/venv",
|
|
158
|
+
"**/env/**",
|
|
159
|
+
"**/ENV/**",
|
|
160
|
+
"**/.env",
|
|
161
|
+
"**/pip-wheel-metadata/**",
|
|
162
|
+
"**/*.egg-info/**",
|
|
163
|
+
"**/dist/**",
|
|
164
|
+
"**/wheels/**",
|
|
165
|
+
"**/pytest-reports/**",
|
|
166
|
+
# Java (Maven, Gradle, SBT)
|
|
167
|
+
"**/target/**",
|
|
168
|
+
"**/target",
|
|
169
|
+
"**/build/**",
|
|
170
|
+
"**/build",
|
|
171
|
+
"**/.gradle/**",
|
|
172
|
+
"**/gradle-app.setting",
|
|
173
|
+
"**/*.class",
|
|
174
|
+
"**/*.jar",
|
|
175
|
+
"**/*.war",
|
|
176
|
+
"**/*.ear",
|
|
177
|
+
"**/*.nar",
|
|
178
|
+
"**/hs_err_pid*",
|
|
179
|
+
"**/.classpath",
|
|
180
|
+
"**/.project",
|
|
181
|
+
"**/.settings/**",
|
|
182
|
+
"**/bin/**",
|
|
183
|
+
"**/project/target/**",
|
|
184
|
+
"**/project/project/**",
|
|
185
|
+
# Go
|
|
186
|
+
"**/vendor/**",
|
|
187
|
+
"**/*.exe",
|
|
188
|
+
"**/*.exe~",
|
|
189
|
+
"**/*.dll",
|
|
190
|
+
"**/*.so",
|
|
191
|
+
"**/*.dylib",
|
|
192
|
+
"**/*.test",
|
|
193
|
+
"**/*.out",
|
|
194
|
+
"**/go.work",
|
|
195
|
+
"**/go.work.sum",
|
|
196
|
+
# Rust
|
|
197
|
+
"**/target/**",
|
|
198
|
+
"**/Cargo.lock",
|
|
199
|
+
"**/*.pdb",
|
|
200
|
+
# Ruby
|
|
201
|
+
"**/vendor/**",
|
|
202
|
+
"**/.bundle/**",
|
|
203
|
+
"**/Gemfile.lock",
|
|
204
|
+
"**/*.gem",
|
|
205
|
+
"**/.rvm/**",
|
|
206
|
+
"**/.rbenv/**",
|
|
207
|
+
"**/coverage/**",
|
|
208
|
+
"**/.yardoc/**",
|
|
209
|
+
"**/doc/**",
|
|
210
|
+
"**/rdoc/**",
|
|
211
|
+
"**/.sass-cache/**",
|
|
212
|
+
"**/.jekyll-cache/**",
|
|
213
|
+
"**/_site/**",
|
|
214
|
+
# PHP
|
|
215
|
+
"**/vendor/**",
|
|
216
|
+
"**/composer.lock",
|
|
217
|
+
"**/.phpunit.result.cache",
|
|
218
|
+
"**/storage/logs/**",
|
|
219
|
+
"**/storage/framework/cache/**",
|
|
220
|
+
"**/storage/framework/sessions/**",
|
|
221
|
+
"**/storage/framework/testing/**",
|
|
222
|
+
"**/storage/framework/views/**",
|
|
223
|
+
"**/bootstrap/cache/**",
|
|
224
|
+
# .NET / C#
|
|
225
|
+
"**/bin/**",
|
|
226
|
+
"**/obj/**",
|
|
227
|
+
"**/packages/**",
|
|
228
|
+
"**/*.cache",
|
|
229
|
+
"**/*.dll",
|
|
230
|
+
"**/*.exe",
|
|
231
|
+
"**/*.pdb",
|
|
232
|
+
"**/*.user",
|
|
233
|
+
"**/*.suo",
|
|
234
|
+
"**/.vs/**",
|
|
235
|
+
"**/TestResults/**",
|
|
236
|
+
"**/BenchmarkDotNet.Artifacts/**",
|
|
237
|
+
# C/C++
|
|
238
|
+
"**/*.o",
|
|
239
|
+
"**/*.obj",
|
|
240
|
+
"**/*.so",
|
|
241
|
+
"**/*.dll",
|
|
242
|
+
"**/*.a",
|
|
243
|
+
"**/*.lib",
|
|
244
|
+
"**/*.dylib",
|
|
245
|
+
"**/*.exe",
|
|
246
|
+
"**/CMakeFiles/**",
|
|
247
|
+
"**/CMakeCache.txt",
|
|
248
|
+
"**/cmake_install.cmake",
|
|
249
|
+
"**/Makefile",
|
|
250
|
+
"**/compile_commands.json",
|
|
251
|
+
"**/.deps/**",
|
|
252
|
+
"**/.libs/**",
|
|
253
|
+
"**/autom4te.cache/**",
|
|
254
|
+
# Perl
|
|
255
|
+
"**/blib/**",
|
|
256
|
+
"**/_build/**",
|
|
257
|
+
"**/Build",
|
|
258
|
+
"**/Build.bat",
|
|
259
|
+
"**/*.tmp",
|
|
260
|
+
"**/*.bak",
|
|
261
|
+
"**/*.old",
|
|
262
|
+
"**/Makefile.old",
|
|
263
|
+
"**/MANIFEST.bak",
|
|
264
|
+
"**/META.yml",
|
|
265
|
+
"**/META.json",
|
|
266
|
+
"**/MYMETA.*",
|
|
267
|
+
"**/.prove",
|
|
268
|
+
# Scala
|
|
269
|
+
"**/target/**",
|
|
270
|
+
"**/project/target/**",
|
|
271
|
+
"**/project/project/**",
|
|
272
|
+
"**/.bloop/**",
|
|
273
|
+
"**/.metals/**",
|
|
274
|
+
"**/.ammonite/**",
|
|
275
|
+
"**/*.class",
|
|
276
|
+
# Elixir
|
|
277
|
+
"**/_build/**",
|
|
278
|
+
"**/deps/**",
|
|
279
|
+
"**/*.beam",
|
|
280
|
+
"**/.fetch",
|
|
281
|
+
"**/erl_crash.dump",
|
|
282
|
+
"**/*.ez",
|
|
283
|
+
"**/doc/**",
|
|
284
|
+
"**/.elixir_ls/**",
|
|
285
|
+
# Swift
|
|
286
|
+
"**/.build/**",
|
|
287
|
+
"**/Packages/**",
|
|
288
|
+
"**/*.xcodeproj/**",
|
|
289
|
+
"**/*.xcworkspace/**",
|
|
290
|
+
"**/DerivedData/**",
|
|
291
|
+
"**/xcuserdata/**",
|
|
292
|
+
"**/*.dSYM/**",
|
|
293
|
+
# Kotlin
|
|
294
|
+
"**/build/**",
|
|
295
|
+
"**/.gradle/**",
|
|
296
|
+
"**/*.class",
|
|
297
|
+
"**/*.jar",
|
|
298
|
+
"**/*.kotlin_module",
|
|
299
|
+
# Clojure
|
|
300
|
+
"**/target/**",
|
|
301
|
+
"**/.lein-**",
|
|
302
|
+
"**/.nrepl-port",
|
|
303
|
+
"**/pom.xml.asc",
|
|
304
|
+
"**/*.jar",
|
|
305
|
+
"**/*.class",
|
|
306
|
+
# Dart/Flutter
|
|
307
|
+
"**/.dart_tool/**",
|
|
308
|
+
"**/build/**",
|
|
309
|
+
"**/.packages",
|
|
310
|
+
"**/pubspec.lock",
|
|
311
|
+
"**/*.g.dart",
|
|
312
|
+
"**/*.freezed.dart",
|
|
313
|
+
"**/*.gr.dart",
|
|
314
|
+
# Haskell
|
|
315
|
+
"**/dist/**",
|
|
316
|
+
"**/dist-newstyle/**",
|
|
317
|
+
"**/.stack-work/**",
|
|
318
|
+
"**/*.hi",
|
|
319
|
+
"**/*.o",
|
|
320
|
+
"**/*.prof",
|
|
321
|
+
"**/*.aux",
|
|
322
|
+
"**/*.hp",
|
|
323
|
+
"**/*.eventlog",
|
|
324
|
+
"**/*.tix",
|
|
325
|
+
# Erlang
|
|
326
|
+
"**/ebin/**",
|
|
327
|
+
"**/rel/**",
|
|
328
|
+
"**/deps/**",
|
|
329
|
+
"**/*.beam",
|
|
330
|
+
"**/*.boot",
|
|
331
|
+
"**/*.plt",
|
|
332
|
+
"**/erl_crash.dump",
|
|
333
|
+
# Common cache and temp directories
|
|
334
|
+
"**/.cache/**",
|
|
335
|
+
"**/cache/**",
|
|
336
|
+
"**/tmp/**",
|
|
337
|
+
"**/temp/**",
|
|
338
|
+
"**/.tmp/**",
|
|
339
|
+
"**/.temp/**",
|
|
340
|
+
"**/logs/**",
|
|
341
|
+
"**/*.log",
|
|
342
|
+
"**/*.log.*",
|
|
343
|
+
# IDE and editor files
|
|
344
|
+
"**/.idea/**",
|
|
345
|
+
"**/.idea",
|
|
346
|
+
"**/.vscode/**",
|
|
347
|
+
"**/.vscode",
|
|
348
|
+
"**/*.swp",
|
|
349
|
+
"**/*.swo",
|
|
350
|
+
"**/*~",
|
|
351
|
+
"**/.#*",
|
|
352
|
+
"**/#*#",
|
|
353
|
+
"**/.emacs.d/auto-save-list/**",
|
|
354
|
+
"**/.vim/**",
|
|
355
|
+
"**/.netrwhist",
|
|
356
|
+
"**/Session.vim",
|
|
357
|
+
"**/.sublime-project",
|
|
358
|
+
"**/.sublime-workspace",
|
|
359
|
+
# OS-specific files
|
|
360
|
+
"**/.DS_Store",
|
|
361
|
+
".DS_Store",
|
|
362
|
+
"**/Thumbs.db",
|
|
363
|
+
"**/Desktop.ini",
|
|
364
|
+
"**/.directory",
|
|
365
|
+
"**/*.lnk",
|
|
366
|
+
# Common artifacts
|
|
367
|
+
"**/*.orig",
|
|
368
|
+
"**/*.rej",
|
|
369
|
+
"**/*.patch",
|
|
370
|
+
"**/*.diff",
|
|
371
|
+
"**/.*.orig",
|
|
372
|
+
"**/.*.rej",
|
|
373
|
+
# Backup files
|
|
374
|
+
"**/*~",
|
|
375
|
+
"**/*.bak",
|
|
376
|
+
"**/*.backup",
|
|
377
|
+
"**/*.old",
|
|
378
|
+
"**/*.save",
|
|
379
|
+
# Hidden files (but be careful with this one)
|
|
380
|
+
"**/.*", # Commented out as it might be too aggressive
|
|
381
|
+
# Directory-only section ends here
|
|
382
|
+
]
|
|
383
|
+
|
|
384
|
+
FILE_IGNORE_PATTERNS = [
|
|
385
|
+
# Binary image formats
|
|
386
|
+
"**/*.png",
|
|
387
|
+
"**/*.jpg",
|
|
388
|
+
"**/*.jpeg",
|
|
389
|
+
"**/*.gif",
|
|
390
|
+
"**/*.bmp",
|
|
391
|
+
"**/*.tiff",
|
|
392
|
+
"**/*.tif",
|
|
393
|
+
"**/*.webp",
|
|
394
|
+
"**/*.ico",
|
|
395
|
+
"**/*.svg",
|
|
396
|
+
# Binary document formats
|
|
397
|
+
"**/*.pdf",
|
|
398
|
+
"**/*.doc",
|
|
399
|
+
"**/*.docx",
|
|
400
|
+
"**/*.xls",
|
|
401
|
+
"**/*.xlsx",
|
|
402
|
+
"**/*.ppt",
|
|
403
|
+
"**/*.pptx",
|
|
404
|
+
# Archive formats
|
|
405
|
+
"**/*.zip",
|
|
406
|
+
"**/*.tar",
|
|
407
|
+
"**/*.gz",
|
|
408
|
+
"**/*.bz2",
|
|
409
|
+
"**/*.xz",
|
|
410
|
+
"**/*.rar",
|
|
411
|
+
"**/*.7z",
|
|
412
|
+
# Media files
|
|
413
|
+
"**/*.mp3",
|
|
414
|
+
"**/*.mp4",
|
|
415
|
+
"**/*.avi",
|
|
416
|
+
"**/*.mov",
|
|
417
|
+
"**/*.wmv",
|
|
418
|
+
"**/*.flv",
|
|
419
|
+
"**/*.wav",
|
|
420
|
+
"**/*.ogg",
|
|
421
|
+
# Font files
|
|
422
|
+
"**/*.ttf",
|
|
423
|
+
"**/*.otf",
|
|
424
|
+
"**/*.woff",
|
|
425
|
+
"**/*.woff2",
|
|
426
|
+
"**/*.eot",
|
|
427
|
+
# Other binary formats
|
|
428
|
+
"**/*.bin",
|
|
429
|
+
"**/*.dat",
|
|
430
|
+
"**/*.db",
|
|
431
|
+
"**/*.sqlite",
|
|
432
|
+
"**/*.sqlite3",
|
|
433
|
+
]
|
|
434
|
+
|
|
435
|
+
# Backwards compatibility for any imports still referring to IGNORE_PATTERNS
|
|
436
|
+
IGNORE_PATTERNS = DIR_IGNORE_PATTERNS + FILE_IGNORE_PATTERNS
|
|
437
|
+
|
|
438
|
+
|
|
439
|
+
def should_ignore_path(path: str) -> bool:
|
|
440
|
+
"""Return True if *path* matches any pattern in IGNORE_PATTERNS."""
|
|
441
|
+
# Convert path to Path object for better pattern matching
|
|
442
|
+
path_obj = Path(path)
|
|
443
|
+
|
|
444
|
+
for pattern in IGNORE_PATTERNS:
|
|
445
|
+
# Try pathlib's match method which handles ** patterns properly
|
|
446
|
+
try:
|
|
447
|
+
if path_obj.match(pattern):
|
|
448
|
+
return True
|
|
449
|
+
except ValueError:
|
|
450
|
+
# If pathlib can't handle the pattern, fall back to fnmatch
|
|
451
|
+
if fnmatch.fnmatch(path, pattern):
|
|
452
|
+
return True
|
|
453
|
+
|
|
454
|
+
# Additional check: if pattern contains **, try matching against
|
|
455
|
+
# different parts of the path to handle edge cases
|
|
456
|
+
if "**" in pattern:
|
|
457
|
+
# Convert pattern to handle different path representations
|
|
458
|
+
simplified_pattern = pattern.replace("**/", "").replace("/**", "")
|
|
459
|
+
|
|
460
|
+
# Check if any part of the path matches the simplified pattern
|
|
461
|
+
path_parts = path_obj.parts
|
|
462
|
+
for i in range(len(path_parts)):
|
|
463
|
+
subpath = Path(*path_parts[i:])
|
|
464
|
+
if fnmatch.fnmatch(str(subpath), simplified_pattern):
|
|
465
|
+
return True
|
|
466
|
+
# Also check individual parts
|
|
467
|
+
if fnmatch.fnmatch(path_parts[i], simplified_pattern):
|
|
468
|
+
return True
|
|
469
|
+
|
|
470
|
+
return False
|
|
471
|
+
|
|
472
|
+
|
|
473
|
+
def should_ignore_dir_path(path: str) -> bool:
|
|
474
|
+
"""Return True if path matches any directory ignore pattern (directories only)."""
|
|
475
|
+
path_obj = Path(path)
|
|
476
|
+
for pattern in DIR_IGNORE_PATTERNS:
|
|
477
|
+
try:
|
|
478
|
+
if path_obj.match(pattern):
|
|
479
|
+
return True
|
|
480
|
+
except ValueError:
|
|
481
|
+
if fnmatch.fnmatch(path, pattern):
|
|
482
|
+
return True
|
|
483
|
+
if "**" in pattern:
|
|
484
|
+
simplified = pattern.replace("**/", "").replace("/**", "")
|
|
485
|
+
parts = path_obj.parts
|
|
486
|
+
for i in range(len(parts)):
|
|
487
|
+
subpath = Path(*parts[i:])
|
|
488
|
+
if fnmatch.fnmatch(str(subpath), simplified):
|
|
489
|
+
return True
|
|
490
|
+
if fnmatch.fnmatch(parts[i], simplified):
|
|
491
|
+
return True
|
|
492
|
+
return False
|
|
493
|
+
|
|
494
|
+
|
|
495
|
+
# ============================================================================
|
|
496
|
+
# SYNTAX HIGHLIGHTING FOR DIFFS ("syntax" mode)
|
|
497
|
+
# ============================================================================
|
|
498
|
+
|
|
499
|
+
# Monokai color scheme - because we have taste 🎨
|
|
500
|
+
TOKEN_COLORS = (
|
|
501
|
+
{
|
|
502
|
+
Token.Keyword: "#f92672" if PYGMENTS_AVAILABLE else "magenta",
|
|
503
|
+
Token.Name.Builtin: "#66d9ef" if PYGMENTS_AVAILABLE else "cyan",
|
|
504
|
+
Token.Name.Function: "#a6e22e" if PYGMENTS_AVAILABLE else "green",
|
|
505
|
+
Token.String: "#e6db74" if PYGMENTS_AVAILABLE else "yellow",
|
|
506
|
+
Token.Number: "#ae81ff" if PYGMENTS_AVAILABLE else "magenta",
|
|
507
|
+
Token.Comment: "#75715e" if PYGMENTS_AVAILABLE else "bright_black",
|
|
508
|
+
Token.Operator: "#f92672" if PYGMENTS_AVAILABLE else "magenta",
|
|
509
|
+
}
|
|
510
|
+
if PYGMENTS_AVAILABLE
|
|
511
|
+
else {}
|
|
512
|
+
)
|
|
513
|
+
|
|
514
|
+
EXTENSION_TO_LEXER_NAME = {
|
|
515
|
+
".py": "python",
|
|
516
|
+
".js": "javascript",
|
|
517
|
+
".jsx": "jsx",
|
|
518
|
+
".ts": "typescript",
|
|
519
|
+
".tsx": "tsx",
|
|
520
|
+
".java": "java",
|
|
521
|
+
".c": "c",
|
|
522
|
+
".h": "c",
|
|
523
|
+
".cpp": "cpp",
|
|
524
|
+
".hpp": "cpp",
|
|
525
|
+
".cc": "cpp",
|
|
526
|
+
".cxx": "cpp",
|
|
527
|
+
".cs": "csharp",
|
|
528
|
+
".rs": "rust",
|
|
529
|
+
".go": "go",
|
|
530
|
+
".rb": "ruby",
|
|
531
|
+
".php": "php",
|
|
532
|
+
".html": "html",
|
|
533
|
+
".htm": "html",
|
|
534
|
+
".css": "css",
|
|
535
|
+
".scss": "scss",
|
|
536
|
+
".json": "json",
|
|
537
|
+
".yaml": "yaml",
|
|
538
|
+
".yml": "yaml",
|
|
539
|
+
".md": "markdown",
|
|
540
|
+
".sh": "bash",
|
|
541
|
+
".bash": "bash",
|
|
542
|
+
".sql": "sql",
|
|
543
|
+
".txt": "text",
|
|
544
|
+
}
|
|
545
|
+
|
|
546
|
+
|
|
547
|
+
def _get_lexer_for_extension(extension: str):
|
|
548
|
+
"""Get the appropriate Pygments lexer for a file extension.
|
|
549
|
+
|
|
550
|
+
Args:
|
|
551
|
+
extension: File extension (with or without leading dot)
|
|
552
|
+
|
|
553
|
+
Returns:
|
|
554
|
+
A Pygments lexer instance or None if Pygments not available
|
|
555
|
+
"""
|
|
556
|
+
if not PYGMENTS_AVAILABLE:
|
|
557
|
+
return None
|
|
558
|
+
|
|
559
|
+
# Normalize extension to have leading dot and be lowercase
|
|
560
|
+
if not extension.startswith("."):
|
|
561
|
+
extension = f".{extension}"
|
|
562
|
+
extension = extension.lower()
|
|
563
|
+
|
|
564
|
+
lexer_name = EXTENSION_TO_LEXER_NAME.get(extension, "text")
|
|
565
|
+
|
|
566
|
+
try:
|
|
567
|
+
return get_lexer_by_name(lexer_name)
|
|
568
|
+
except Exception:
|
|
569
|
+
# Fallback to plain text if lexer not found
|
|
570
|
+
return TextLexer()
|
|
571
|
+
|
|
572
|
+
|
|
573
|
+
def _get_token_color(token_type) -> str:
|
|
574
|
+
"""Get color for a token type from our Monokai scheme.
|
|
575
|
+
|
|
576
|
+
Args:
|
|
577
|
+
token_type: Pygments token type
|
|
578
|
+
|
|
579
|
+
Returns:
|
|
580
|
+
Hex color string or color name
|
|
581
|
+
"""
|
|
582
|
+
if not PYGMENTS_AVAILABLE:
|
|
583
|
+
return "#cccccc"
|
|
584
|
+
|
|
585
|
+
for ttype, color in TOKEN_COLORS.items():
|
|
586
|
+
if token_type in ttype:
|
|
587
|
+
return color
|
|
588
|
+
return "#cccccc" # Default light-grey for unmatched tokens
|
|
589
|
+
|
|
590
|
+
|
|
591
|
+
def _highlight_code_line(code: str, bg_color: str | None, lexer) -> Text:
|
|
592
|
+
"""Highlight a line of code with syntax highlighting and optional background color.
|
|
593
|
+
|
|
594
|
+
Args:
|
|
595
|
+
code: The code string to highlight
|
|
596
|
+
bg_color: Background color in hex format, or None for no background
|
|
597
|
+
lexer: Pygments lexer instance to use
|
|
598
|
+
|
|
599
|
+
Returns:
|
|
600
|
+
Rich Text object with styling applied
|
|
601
|
+
"""
|
|
602
|
+
if not PYGMENTS_AVAILABLE or lexer is None:
|
|
603
|
+
# Fallback: just return text with optional background
|
|
604
|
+
if bg_color:
|
|
605
|
+
return Text(code, style=f"on {bg_color}")
|
|
606
|
+
return Text(code)
|
|
607
|
+
|
|
608
|
+
text = Text()
|
|
609
|
+
|
|
610
|
+
for token_type, value in lex(code, lexer):
|
|
611
|
+
# Strip trailing newlines that Pygments adds
|
|
612
|
+
# Pygments lexer always adds a \n at the end of the last token
|
|
613
|
+
value = value.rstrip("\n")
|
|
614
|
+
|
|
615
|
+
# Skip if the value is now empty (was only whitespace/newlines)
|
|
616
|
+
if not value:
|
|
617
|
+
continue
|
|
618
|
+
|
|
619
|
+
fg_color = _get_token_color(token_type)
|
|
620
|
+
# Apply foreground color and optional background
|
|
621
|
+
if bg_color:
|
|
622
|
+
text.append(value, style=f"{fg_color} on {bg_color}")
|
|
623
|
+
else:
|
|
624
|
+
text.append(value, style=fg_color)
|
|
625
|
+
|
|
626
|
+
return text
|
|
627
|
+
|
|
628
|
+
|
|
629
|
+
def _extract_file_extension_from_diff(diff_text: str) -> str:
|
|
630
|
+
"""Extract file extension from diff headers.
|
|
631
|
+
|
|
632
|
+
Args:
|
|
633
|
+
diff_text: Unified diff text
|
|
634
|
+
|
|
635
|
+
Returns:
|
|
636
|
+
File extension (e.g., '.py') or '.txt' as fallback
|
|
637
|
+
"""
|
|
638
|
+
import re
|
|
639
|
+
|
|
640
|
+
# Look for +++ b/filename.ext or --- a/filename.ext headers
|
|
641
|
+
pattern = r"^(?:\+\+\+|---) [ab]/.*?(\.[a-zA-Z0-9]+)$"
|
|
642
|
+
|
|
643
|
+
for line in diff_text.split("\n")[:10]: # Check first 10 lines
|
|
644
|
+
match = re.search(pattern, line)
|
|
645
|
+
if match:
|
|
646
|
+
return match.group(1)
|
|
647
|
+
|
|
648
|
+
return ".txt" # Fallback to plain text
|
|
649
|
+
|
|
650
|
+
|
|
651
|
+
# ============================================================================
|
|
652
|
+
# COLOR PAIR OPTIMIZATION (for "highlighted" mode)
|
|
653
|
+
# ============================================================================
|
|
654
|
+
|
|
655
|
+
|
|
656
|
+
def brighten_hex(hex_color: str, factor: float) -> str:
|
|
657
|
+
"""
|
|
658
|
+
Darken a hex color by multiplying each RGB channel by `factor`.
|
|
659
|
+
factor=1.0 -> no change
|
|
660
|
+
factor=0.0 -> black
|
|
661
|
+
factor=0.18 -> good for diff backgrounds (recommended)
|
|
662
|
+
"""
|
|
663
|
+
hex_color = hex_color.lstrip("#")
|
|
664
|
+
if len(hex_color) != 6:
|
|
665
|
+
raise ValueError(f"Expected #RRGGBB, got {hex_color!r}")
|
|
666
|
+
|
|
667
|
+
r = int(hex_color[0:2], 16)
|
|
668
|
+
g = int(hex_color[2:4], 16)
|
|
669
|
+
b = int(hex_color[4:6], 16)
|
|
670
|
+
|
|
671
|
+
r = max(0, min(255, int(r * (1 + factor))))
|
|
672
|
+
g = max(0, min(255, int(g * (1 + factor))))
|
|
673
|
+
b = max(0, min(255, int(b * (1 + factor))))
|
|
674
|
+
|
|
675
|
+
return f"#{r:02x}{g:02x}{b:02x}"
|
|
676
|
+
|
|
677
|
+
|
|
678
|
+
def _format_diff_with_syntax_highlighting(
|
|
679
|
+
diff_text: str,
|
|
680
|
+
addition_color: str | None = None,
|
|
681
|
+
deletion_color: str | None = None,
|
|
682
|
+
) -> Text:
|
|
683
|
+
"""Format diff with full syntax highlighting using Pygments.
|
|
684
|
+
|
|
685
|
+
This renders diffs with:
|
|
686
|
+
- Syntax highlighting for code tokens
|
|
687
|
+
- Colored backgrounds for context/added/removed lines
|
|
688
|
+
- Monokai color scheme
|
|
689
|
+
- Optional custom colors for additions/deletions
|
|
690
|
+
|
|
691
|
+
Args:
|
|
692
|
+
diff_text: Raw unified diff text
|
|
693
|
+
addition_color: Optional custom color for added lines (default: green)
|
|
694
|
+
deletion_color: Optional custom color for deleted lines (default: red)
|
|
695
|
+
|
|
696
|
+
Returns:
|
|
697
|
+
Rich Text object with syntax highlighting (can be passed to emit_info)
|
|
698
|
+
"""
|
|
699
|
+
if not PYGMENTS_AVAILABLE:
|
|
700
|
+
return Text(diff_text)
|
|
701
|
+
|
|
702
|
+
# Extract file extension from diff headers
|
|
703
|
+
extension = _extract_file_extension_from_diff(diff_text)
|
|
704
|
+
lexer = _get_lexer_for_extension(extension)
|
|
705
|
+
|
|
706
|
+
# Generate background colors from foreground colors
|
|
707
|
+
add_fg = brighten_hex(addition_color, 0.6)
|
|
708
|
+
del_fg = brighten_hex(deletion_color, 0.6)
|
|
709
|
+
|
|
710
|
+
# Background colors for different line types
|
|
711
|
+
# Context lines have no background (None) for clean, minimal diffs
|
|
712
|
+
bg_colors = {
|
|
713
|
+
"removed": deletion_color,
|
|
714
|
+
"added": addition_color,
|
|
715
|
+
"context": None, # No background for unchanged lines
|
|
716
|
+
}
|
|
717
|
+
|
|
718
|
+
lines = diff_text.split("\n")
|
|
719
|
+
# Remove trailing empty line if it exists (from trailing \n in diff)
|
|
720
|
+
if lines and lines[-1] == "":
|
|
721
|
+
lines = lines[:-1]
|
|
722
|
+
result = Text()
|
|
723
|
+
|
|
724
|
+
for i, line in enumerate(lines):
|
|
725
|
+
if not line:
|
|
726
|
+
# Empty line - just add a newline if not the last line
|
|
727
|
+
if i < len(lines) - 1:
|
|
728
|
+
result.append("\n")
|
|
729
|
+
continue
|
|
730
|
+
|
|
731
|
+
# Skip diff headers - they're redundant noise since we show the filename in the banner
|
|
732
|
+
if line.startswith(("---", "+++", "@@", "diff ", "index ")):
|
|
733
|
+
continue
|
|
734
|
+
else:
|
|
735
|
+
# Determine line type and extract code content
|
|
736
|
+
if line.startswith("-"):
|
|
737
|
+
line_type = "removed"
|
|
738
|
+
code = line[1:] # Remove the '-' prefix
|
|
739
|
+
marker_style = f"bold {del_fg} on {bg_colors[line_type]}"
|
|
740
|
+
prefix = "- "
|
|
741
|
+
elif line.startswith("+"):
|
|
742
|
+
line_type = "added"
|
|
743
|
+
code = line[1:] # Remove the '+' prefix
|
|
744
|
+
marker_style = f"bold {add_fg} on {bg_colors[line_type]}"
|
|
745
|
+
prefix = "+ "
|
|
746
|
+
else:
|
|
747
|
+
line_type = "context"
|
|
748
|
+
code = line[1:] if line.startswith(" ") else line
|
|
749
|
+
# Context lines have no background - clean and minimal
|
|
750
|
+
marker_style = "" # No special styling for context markers
|
|
751
|
+
prefix = " "
|
|
752
|
+
|
|
753
|
+
# Add the marker prefix
|
|
754
|
+
if marker_style: # Only apply style if we have one
|
|
755
|
+
result.append(prefix, style=marker_style)
|
|
756
|
+
else:
|
|
757
|
+
result.append(prefix)
|
|
758
|
+
|
|
759
|
+
# Add syntax-highlighted code
|
|
760
|
+
highlighted = _highlight_code_line(code, bg_colors[line_type], lexer)
|
|
761
|
+
result.append_text(highlighted)
|
|
762
|
+
|
|
763
|
+
# Add newline after each line except the last
|
|
764
|
+
if i < len(lines) - 1:
|
|
765
|
+
result.append("\n")
|
|
766
|
+
|
|
767
|
+
return result
|
|
768
|
+
|
|
769
|
+
|
|
770
|
+
def format_diff_with_colors(diff_text: str) -> Text:
|
|
771
|
+
"""Format diff text with beautiful syntax highlighting.
|
|
772
|
+
|
|
773
|
+
This is the canonical diff formatting function used across the codebase.
|
|
774
|
+
It applies user-configurable color coding with full syntax highlighting using Pygments.
|
|
775
|
+
|
|
776
|
+
The function respects user preferences from config:
|
|
777
|
+
- get_diff_addition_color(): Color for added lines (markers and backgrounds)
|
|
778
|
+
- get_diff_deletion_color(): Color for deleted lines (markers and backgrounds)
|
|
779
|
+
|
|
780
|
+
Args:
|
|
781
|
+
diff_text: Raw diff text to format
|
|
782
|
+
|
|
783
|
+
Returns:
|
|
784
|
+
Rich Text object with syntax highlighting
|
|
785
|
+
"""
|
|
786
|
+
from code_puppy.config import (
|
|
787
|
+
get_diff_addition_color,
|
|
788
|
+
get_diff_deletion_color,
|
|
789
|
+
)
|
|
790
|
+
|
|
791
|
+
if not diff_text or not diff_text.strip():
|
|
792
|
+
return Text("-- no diff available --", style="dim")
|
|
793
|
+
|
|
794
|
+
addition_base_color = get_diff_addition_color()
|
|
795
|
+
deletion_base_color = get_diff_deletion_color()
|
|
796
|
+
|
|
797
|
+
# Always use beautiful syntax highlighting!
|
|
798
|
+
if not PYGMENTS_AVAILABLE:
|
|
799
|
+
emit_warning("Pygments not available, diffs will look plain")
|
|
800
|
+
# Return plain text as fallback
|
|
801
|
+
return Text(diff_text)
|
|
802
|
+
|
|
803
|
+
# Return Text object with custom colors - emit_info handles this correctly
|
|
804
|
+
return _format_diff_with_syntax_highlighting(
|
|
805
|
+
diff_text,
|
|
806
|
+
addition_color=addition_base_color,
|
|
807
|
+
deletion_color=deletion_base_color,
|
|
808
|
+
)
|
|
809
|
+
|
|
810
|
+
|
|
811
|
+
async def arrow_select_async(
|
|
812
|
+
message: str,
|
|
813
|
+
choices: list[str],
|
|
814
|
+
preview_callback: Optional[Callable[[int], str]] = None,
|
|
815
|
+
) -> str:
|
|
816
|
+
"""Async version: Show an arrow-key navigable selector with optional preview.
|
|
817
|
+
|
|
818
|
+
Args:
|
|
819
|
+
message: The prompt message to display
|
|
820
|
+
choices: List of choice strings
|
|
821
|
+
preview_callback: Optional callback that takes the selected index and returns
|
|
822
|
+
preview text to display below the choices
|
|
823
|
+
|
|
824
|
+
Returns:
|
|
825
|
+
The selected choice string
|
|
826
|
+
|
|
827
|
+
Raises:
|
|
828
|
+
KeyboardInterrupt: If user cancels with Ctrl-C
|
|
829
|
+
"""
|
|
830
|
+
import html
|
|
831
|
+
|
|
832
|
+
selected_index = [0] # Mutable container for selected index
|
|
833
|
+
result = [None] # Mutable container for result
|
|
834
|
+
|
|
835
|
+
def get_formatted_text():
|
|
836
|
+
"""Generate the formatted text for display."""
|
|
837
|
+
# Escape XML special characters to prevent parsing errors
|
|
838
|
+
safe_message = html.escape(message)
|
|
839
|
+
lines = [f"<b>{safe_message}</b>", ""]
|
|
840
|
+
for i, choice in enumerate(choices):
|
|
841
|
+
safe_choice = html.escape(choice)
|
|
842
|
+
if i == selected_index[0]:
|
|
843
|
+
lines.append(f"<ansigreen>❯ {safe_choice}</ansigreen>")
|
|
844
|
+
else:
|
|
845
|
+
lines.append(f" {safe_choice}")
|
|
846
|
+
lines.append("")
|
|
847
|
+
|
|
848
|
+
# Add preview section if callback provided
|
|
849
|
+
if preview_callback is not None:
|
|
850
|
+
preview_text = preview_callback(selected_index[0])
|
|
851
|
+
if preview_text:
|
|
852
|
+
import textwrap
|
|
853
|
+
|
|
854
|
+
# Box width (excluding borders and padding)
|
|
855
|
+
box_width = 60
|
|
856
|
+
border_top = (
|
|
857
|
+
"<ansiyellow>┌─ Preview "
|
|
858
|
+
+ "─" * (box_width - 10)
|
|
859
|
+
+ "┐</ansiyellow>"
|
|
860
|
+
)
|
|
861
|
+
border_bottom = "<ansiyellow>└" + "─" * box_width + "┘</ansiyellow>"
|
|
862
|
+
|
|
863
|
+
lines.append(border_top)
|
|
864
|
+
|
|
865
|
+
# Wrap text to fit within box width (minus padding)
|
|
866
|
+
wrapped_lines = textwrap.wrap(preview_text, width=box_width - 2)
|
|
867
|
+
|
|
868
|
+
# If no wrapped lines (empty text), add empty line
|
|
869
|
+
if not wrapped_lines:
|
|
870
|
+
wrapped_lines = [""]
|
|
871
|
+
|
|
872
|
+
for wrapped_line in wrapped_lines:
|
|
873
|
+
safe_preview = html.escape(wrapped_line)
|
|
874
|
+
# Pad line to box width for consistent appearance
|
|
875
|
+
padded_line = safe_preview.ljust(box_width - 2)
|
|
876
|
+
lines.append(f"<dim>│ {padded_line} │</dim>")
|
|
877
|
+
|
|
878
|
+
lines.append(border_bottom)
|
|
879
|
+
lines.append("")
|
|
880
|
+
|
|
881
|
+
lines.append(
|
|
882
|
+
"<ansicyan>(Use ↑↓ or Ctrl+P/N to select, Enter to confirm)</ansicyan>"
|
|
883
|
+
)
|
|
884
|
+
return HTML("\n".join(lines))
|
|
885
|
+
|
|
886
|
+
# Key bindings
|
|
887
|
+
kb = KeyBindings()
|
|
888
|
+
|
|
889
|
+
@kb.add("up")
|
|
890
|
+
@kb.add("c-p") # Ctrl+P = previous (Emacs-style)
|
|
891
|
+
def move_up(event):
|
|
892
|
+
selected_index[0] = (selected_index[0] - 1) % len(choices)
|
|
893
|
+
event.app.invalidate() # Force redraw to update preview
|
|
894
|
+
|
|
895
|
+
@kb.add("down")
|
|
896
|
+
@kb.add("c-n") # Ctrl+N = next (Emacs-style)
|
|
897
|
+
def move_down(event):
|
|
898
|
+
selected_index[0] = (selected_index[0] + 1) % len(choices)
|
|
899
|
+
event.app.invalidate() # Force redraw to update preview
|
|
900
|
+
|
|
901
|
+
@kb.add("enter")
|
|
902
|
+
def accept(event):
|
|
903
|
+
result[0] = choices[selected_index[0]]
|
|
904
|
+
event.app.exit()
|
|
905
|
+
|
|
906
|
+
@kb.add("c-c") # Ctrl-C
|
|
907
|
+
def cancel(event):
|
|
908
|
+
result[0] = None
|
|
909
|
+
event.app.exit()
|
|
910
|
+
|
|
911
|
+
# Layout
|
|
912
|
+
control = FormattedTextControl(get_formatted_text)
|
|
913
|
+
layout = Layout(Window(content=control))
|
|
914
|
+
|
|
915
|
+
# Application
|
|
916
|
+
app = Application(
|
|
917
|
+
layout=layout,
|
|
918
|
+
key_bindings=kb,
|
|
919
|
+
full_screen=False,
|
|
920
|
+
)
|
|
921
|
+
|
|
922
|
+
# Flush output before prompt_toolkit takes control
|
|
923
|
+
sys.stdout.flush()
|
|
924
|
+
sys.stderr.flush()
|
|
925
|
+
|
|
926
|
+
# Run the app asynchronously
|
|
927
|
+
await app.run_async()
|
|
928
|
+
|
|
929
|
+
if result[0] is None:
|
|
930
|
+
raise KeyboardInterrupt()
|
|
931
|
+
|
|
932
|
+
return result[0]
|
|
933
|
+
|
|
934
|
+
|
|
935
|
+
def arrow_select(message: str, choices: list[str]) -> str:
|
|
936
|
+
"""Show an arrow-key navigable selector (synchronous version).
|
|
937
|
+
|
|
938
|
+
Args:
|
|
939
|
+
message: The prompt message to display
|
|
940
|
+
choices: List of choice strings
|
|
941
|
+
|
|
942
|
+
Returns:
|
|
943
|
+
The selected choice string
|
|
944
|
+
|
|
945
|
+
Raises:
|
|
946
|
+
KeyboardInterrupt: If user cancels with Ctrl-C
|
|
947
|
+
"""
|
|
948
|
+
|
|
949
|
+
selected_index = [0] # Mutable container for selected index
|
|
950
|
+
result = [None] # Mutable container for result
|
|
951
|
+
|
|
952
|
+
def get_formatted_text():
|
|
953
|
+
"""Generate the formatted text for display."""
|
|
954
|
+
lines = [f"<b>{message}</b>", ""]
|
|
955
|
+
for i, choice in enumerate(choices):
|
|
956
|
+
if i == selected_index[0]:
|
|
957
|
+
lines.append(f"<ansigreen>❯ {choice}</ansigreen>")
|
|
958
|
+
else:
|
|
959
|
+
lines.append(f" {choice}")
|
|
960
|
+
lines.append("")
|
|
961
|
+
lines.append(
|
|
962
|
+
"<ansicyan>(Use ↑↓ or Ctrl+P/N to select, Enter to confirm)</ansicyan>"
|
|
963
|
+
)
|
|
964
|
+
return HTML("\n".join(lines))
|
|
965
|
+
|
|
966
|
+
# Key bindings
|
|
967
|
+
kb = KeyBindings()
|
|
968
|
+
|
|
969
|
+
@kb.add("up")
|
|
970
|
+
@kb.add("c-p") # Ctrl+P = previous (Emacs-style)
|
|
971
|
+
def move_up(event):
|
|
972
|
+
selected_index[0] = (selected_index[0] - 1) % len(choices)
|
|
973
|
+
event.app.invalidate() # Force redraw to update preview
|
|
974
|
+
|
|
975
|
+
@kb.add("down")
|
|
976
|
+
@kb.add("c-n") # Ctrl+N = next (Emacs-style)
|
|
977
|
+
def move_down(event):
|
|
978
|
+
selected_index[0] = (selected_index[0] + 1) % len(choices)
|
|
979
|
+
event.app.invalidate() # Force redraw to update preview
|
|
980
|
+
|
|
981
|
+
@kb.add("enter")
|
|
982
|
+
def accept(event):
|
|
983
|
+
result[0] = choices[selected_index[0]]
|
|
984
|
+
event.app.exit()
|
|
985
|
+
|
|
986
|
+
@kb.add("c-c") # Ctrl-C
|
|
987
|
+
def cancel(event):
|
|
988
|
+
result[0] = None
|
|
989
|
+
event.app.exit()
|
|
990
|
+
|
|
991
|
+
# Layout
|
|
992
|
+
control = FormattedTextControl(get_formatted_text)
|
|
993
|
+
layout = Layout(Window(content=control))
|
|
994
|
+
|
|
995
|
+
# Application
|
|
996
|
+
app = Application(
|
|
997
|
+
layout=layout,
|
|
998
|
+
key_bindings=kb,
|
|
999
|
+
full_screen=False,
|
|
1000
|
+
)
|
|
1001
|
+
|
|
1002
|
+
# Flush output before prompt_toolkit takes control
|
|
1003
|
+
sys.stdout.flush()
|
|
1004
|
+
sys.stderr.flush()
|
|
1005
|
+
|
|
1006
|
+
# Check if we're already in an async context
|
|
1007
|
+
try:
|
|
1008
|
+
asyncio.get_running_loop()
|
|
1009
|
+
# We're in an async context - can't use app.run()
|
|
1010
|
+
# Caller should use arrow_select_async instead
|
|
1011
|
+
raise RuntimeError(
|
|
1012
|
+
"arrow_select() called from async context. Use arrow_select_async() instead."
|
|
1013
|
+
)
|
|
1014
|
+
except RuntimeError as e:
|
|
1015
|
+
if "no running event loop" in str(e).lower():
|
|
1016
|
+
# No event loop, safe to use app.run()
|
|
1017
|
+
app.run()
|
|
1018
|
+
else:
|
|
1019
|
+
# Re-raise if it's our error message
|
|
1020
|
+
raise
|
|
1021
|
+
|
|
1022
|
+
if result[0] is None:
|
|
1023
|
+
raise KeyboardInterrupt()
|
|
1024
|
+
|
|
1025
|
+
return result[0]
|
|
1026
|
+
|
|
1027
|
+
|
|
1028
|
+
def get_user_approval(
|
|
1029
|
+
title: str,
|
|
1030
|
+
content: Text | str,
|
|
1031
|
+
preview: str | None = None,
|
|
1032
|
+
border_style: str = "dim white",
|
|
1033
|
+
puppy_name: str | None = None,
|
|
1034
|
+
) -> tuple[bool, str | None]:
|
|
1035
|
+
"""Show a beautiful approval panel with arrow-key selector.
|
|
1036
|
+
|
|
1037
|
+
Args:
|
|
1038
|
+
title: Title for the panel (e.g., "File Operation", "Shell Command")
|
|
1039
|
+
content: Main content to display (Rich Text object or string)
|
|
1040
|
+
preview: Optional preview content (like a diff)
|
|
1041
|
+
border_style: Border color/style for the panel
|
|
1042
|
+
puppy_name: Name of the assistant (defaults to config value)
|
|
1043
|
+
|
|
1044
|
+
Returns:
|
|
1045
|
+
Tuple of (confirmed: bool, user_feedback: str | None)
|
|
1046
|
+
- confirmed: True if approved, False if rejected
|
|
1047
|
+
- user_feedback: Optional feedback text if user provided it
|
|
1048
|
+
"""
|
|
1049
|
+
import time
|
|
1050
|
+
|
|
1051
|
+
from code_puppy.tools.command_runner import set_awaiting_user_input
|
|
1052
|
+
|
|
1053
|
+
if puppy_name is None:
|
|
1054
|
+
from code_puppy.config import get_puppy_name
|
|
1055
|
+
|
|
1056
|
+
puppy_name = get_puppy_name().title()
|
|
1057
|
+
|
|
1058
|
+
# Build panel content
|
|
1059
|
+
if isinstance(content, str):
|
|
1060
|
+
panel_content = Text(content)
|
|
1061
|
+
else:
|
|
1062
|
+
panel_content = content
|
|
1063
|
+
|
|
1064
|
+
# Add preview if provided
|
|
1065
|
+
if preview:
|
|
1066
|
+
panel_content.append("\n\n", style="")
|
|
1067
|
+
panel_content.append("Preview of changes:", style="bold underline")
|
|
1068
|
+
panel_content.append("\n", style="")
|
|
1069
|
+
formatted_preview = format_diff_with_colors(preview)
|
|
1070
|
+
|
|
1071
|
+
# Handle both string (text mode) and Text object (highlight mode)
|
|
1072
|
+
if isinstance(formatted_preview, Text):
|
|
1073
|
+
preview_text = formatted_preview
|
|
1074
|
+
else:
|
|
1075
|
+
preview_text = Text.from_markup(formatted_preview)
|
|
1076
|
+
|
|
1077
|
+
panel_content.append(preview_text)
|
|
1078
|
+
|
|
1079
|
+
# Mark that we showed a diff preview
|
|
1080
|
+
try:
|
|
1081
|
+
from code_puppy.plugins.file_permission_handler.register_callbacks import (
|
|
1082
|
+
set_diff_already_shown,
|
|
1083
|
+
)
|
|
1084
|
+
|
|
1085
|
+
set_diff_already_shown(True)
|
|
1086
|
+
except ImportError:
|
|
1087
|
+
pass
|
|
1088
|
+
|
|
1089
|
+
# Create panel
|
|
1090
|
+
panel = Panel(
|
|
1091
|
+
panel_content,
|
|
1092
|
+
title=f"[bold white]{title}[/bold white]",
|
|
1093
|
+
border_style=border_style,
|
|
1094
|
+
padding=(1, 2),
|
|
1095
|
+
)
|
|
1096
|
+
|
|
1097
|
+
# Pause spinners BEFORE showing panel
|
|
1098
|
+
set_awaiting_user_input(True)
|
|
1099
|
+
# Also explicitly pause spinners to ensure they're fully stopped
|
|
1100
|
+
try:
|
|
1101
|
+
from code_puppy.messaging.spinner import pause_all_spinners
|
|
1102
|
+
|
|
1103
|
+
pause_all_spinners()
|
|
1104
|
+
except (ImportError, Exception):
|
|
1105
|
+
pass
|
|
1106
|
+
|
|
1107
|
+
time.sleep(0.3) # Let spinners fully stop
|
|
1108
|
+
|
|
1109
|
+
# Display panel
|
|
1110
|
+
local_console = Console()
|
|
1111
|
+
emit_info("")
|
|
1112
|
+
local_console.print(panel)
|
|
1113
|
+
emit_info("")
|
|
1114
|
+
|
|
1115
|
+
# Flush and buffer before selector
|
|
1116
|
+
sys.stdout.flush()
|
|
1117
|
+
sys.stderr.flush()
|
|
1118
|
+
time.sleep(0.1)
|
|
1119
|
+
|
|
1120
|
+
user_feedback = None
|
|
1121
|
+
confirmed = False
|
|
1122
|
+
|
|
1123
|
+
try:
|
|
1124
|
+
# Final flush
|
|
1125
|
+
sys.stdout.flush()
|
|
1126
|
+
|
|
1127
|
+
# Show arrow-key selector
|
|
1128
|
+
choice = arrow_select(
|
|
1129
|
+
"💭 What would you like to do?",
|
|
1130
|
+
[
|
|
1131
|
+
"✓ Approve",
|
|
1132
|
+
"✗ Reject",
|
|
1133
|
+
f"💬 Reject with feedback (tell {puppy_name} what to change)",
|
|
1134
|
+
],
|
|
1135
|
+
)
|
|
1136
|
+
|
|
1137
|
+
if choice == "✓ Approve":
|
|
1138
|
+
confirmed = True
|
|
1139
|
+
elif choice == "✗ Reject":
|
|
1140
|
+
confirmed = False
|
|
1141
|
+
else:
|
|
1142
|
+
# User wants to provide feedback
|
|
1143
|
+
confirmed = False
|
|
1144
|
+
emit_info("")
|
|
1145
|
+
emit_info(f"Tell {puppy_name} what to change:")
|
|
1146
|
+
user_feedback = Prompt.ask(
|
|
1147
|
+
"[bold green]➤[/bold green]",
|
|
1148
|
+
default="",
|
|
1149
|
+
).strip()
|
|
1150
|
+
|
|
1151
|
+
if not user_feedback:
|
|
1152
|
+
user_feedback = None
|
|
1153
|
+
|
|
1154
|
+
except (KeyboardInterrupt, EOFError):
|
|
1155
|
+
emit_error("Cancelled by user")
|
|
1156
|
+
confirmed = False
|
|
1157
|
+
|
|
1158
|
+
finally:
|
|
1159
|
+
set_awaiting_user_input(False)
|
|
1160
|
+
|
|
1161
|
+
# Force Rich console to reset display state to prevent artifacts
|
|
1162
|
+
try:
|
|
1163
|
+
# Clear Rich's internal display state to prevent artifacts
|
|
1164
|
+
local_console.file.write("\r") # Return to start of line
|
|
1165
|
+
local_console.file.write("\x1b[K") # Clear current line
|
|
1166
|
+
local_console.file.flush()
|
|
1167
|
+
except Exception:
|
|
1168
|
+
pass
|
|
1169
|
+
|
|
1170
|
+
# Ensure streams are flushed
|
|
1171
|
+
sys.stdout.flush()
|
|
1172
|
+
sys.stderr.flush()
|
|
1173
|
+
|
|
1174
|
+
# Show result BEFORE resuming spinners (no puppy litter!)
|
|
1175
|
+
emit_info("")
|
|
1176
|
+
if not confirmed:
|
|
1177
|
+
if user_feedback:
|
|
1178
|
+
emit_error("Rejected with feedback!")
|
|
1179
|
+
emit_warning(f'Telling {puppy_name}: "{user_feedback}"')
|
|
1180
|
+
else:
|
|
1181
|
+
emit_error("Rejected.")
|
|
1182
|
+
else:
|
|
1183
|
+
emit_success("Approved!")
|
|
1184
|
+
|
|
1185
|
+
# NOW resume spinners after showing the result
|
|
1186
|
+
try:
|
|
1187
|
+
from code_puppy.messaging.spinner import resume_all_spinners
|
|
1188
|
+
|
|
1189
|
+
resume_all_spinners()
|
|
1190
|
+
except (ImportError, Exception):
|
|
1191
|
+
pass
|
|
1192
|
+
|
|
1193
|
+
return confirmed, user_feedback
|
|
1194
|
+
|
|
1195
|
+
|
|
1196
|
+
async def get_user_approval_async(
|
|
1197
|
+
title: str,
|
|
1198
|
+
content: Text | str,
|
|
1199
|
+
preview: str | None = None,
|
|
1200
|
+
border_style: str = "dim white",
|
|
1201
|
+
puppy_name: str | None = None,
|
|
1202
|
+
) -> tuple[bool, str | None]:
|
|
1203
|
+
"""Async version of get_user_approval - show a beautiful approval panel with arrow-key selector.
|
|
1204
|
+
|
|
1205
|
+
Args:
|
|
1206
|
+
title: Title for the panel (e.g., "File Operation", "Shell Command")
|
|
1207
|
+
content: Main content to display (Rich Text object or string)
|
|
1208
|
+
preview: Optional preview content (like a diff)
|
|
1209
|
+
border_style: Border color/style for the panel
|
|
1210
|
+
puppy_name: Name of the assistant (defaults to config value)
|
|
1211
|
+
|
|
1212
|
+
Returns:
|
|
1213
|
+
Tuple of (confirmed: bool, user_feedback: str | None)
|
|
1214
|
+
- confirmed: True if approved, False if rejected
|
|
1215
|
+
- user_feedback: Optional feedback text if user provided it
|
|
1216
|
+
"""
|
|
1217
|
+
|
|
1218
|
+
from code_puppy.tools.command_runner import set_awaiting_user_input
|
|
1219
|
+
|
|
1220
|
+
if puppy_name is None:
|
|
1221
|
+
from code_puppy.config import get_puppy_name
|
|
1222
|
+
|
|
1223
|
+
puppy_name = get_puppy_name().title()
|
|
1224
|
+
|
|
1225
|
+
# Build panel content
|
|
1226
|
+
if isinstance(content, str):
|
|
1227
|
+
panel_content = Text(content)
|
|
1228
|
+
else:
|
|
1229
|
+
panel_content = content
|
|
1230
|
+
|
|
1231
|
+
# Add preview if provided
|
|
1232
|
+
if preview:
|
|
1233
|
+
panel_content.append("\n\n", style="")
|
|
1234
|
+
panel_content.append("Preview of changes:", style="bold underline")
|
|
1235
|
+
panel_content.append("\n", style="")
|
|
1236
|
+
formatted_preview = format_diff_with_colors(preview)
|
|
1237
|
+
|
|
1238
|
+
# Handle both string (text mode) and Text object (highlight mode)
|
|
1239
|
+
if isinstance(formatted_preview, Text):
|
|
1240
|
+
preview_text = formatted_preview
|
|
1241
|
+
else:
|
|
1242
|
+
preview_text = Text.from_markup(formatted_preview)
|
|
1243
|
+
|
|
1244
|
+
panel_content.append(preview_text)
|
|
1245
|
+
|
|
1246
|
+
# Mark that we showed a diff preview
|
|
1247
|
+
try:
|
|
1248
|
+
from code_puppy.plugins.file_permission_handler.register_callbacks import (
|
|
1249
|
+
set_diff_already_shown,
|
|
1250
|
+
)
|
|
1251
|
+
|
|
1252
|
+
set_diff_already_shown(True)
|
|
1253
|
+
except ImportError:
|
|
1254
|
+
pass
|
|
1255
|
+
|
|
1256
|
+
# Create panel
|
|
1257
|
+
panel = Panel(
|
|
1258
|
+
panel_content,
|
|
1259
|
+
title=f"[bold white]{title}[/bold white]",
|
|
1260
|
+
border_style=border_style,
|
|
1261
|
+
padding=(1, 2),
|
|
1262
|
+
)
|
|
1263
|
+
|
|
1264
|
+
# Pause spinners BEFORE showing panel
|
|
1265
|
+
set_awaiting_user_input(True)
|
|
1266
|
+
# Also explicitly pause spinners to ensure they're fully stopped
|
|
1267
|
+
try:
|
|
1268
|
+
from code_puppy.messaging.spinner import pause_all_spinners
|
|
1269
|
+
|
|
1270
|
+
pause_all_spinners()
|
|
1271
|
+
except (ImportError, Exception):
|
|
1272
|
+
pass
|
|
1273
|
+
|
|
1274
|
+
await asyncio.sleep(0.3) # Let spinners fully stop
|
|
1275
|
+
|
|
1276
|
+
# Display panel
|
|
1277
|
+
local_console = Console()
|
|
1278
|
+
emit_info("")
|
|
1279
|
+
local_console.print(panel)
|
|
1280
|
+
emit_info("")
|
|
1281
|
+
|
|
1282
|
+
# Flush and buffer before selector
|
|
1283
|
+
sys.stdout.flush()
|
|
1284
|
+
sys.stderr.flush()
|
|
1285
|
+
await asyncio.sleep(0.1)
|
|
1286
|
+
|
|
1287
|
+
user_feedback = None
|
|
1288
|
+
confirmed = False
|
|
1289
|
+
|
|
1290
|
+
try:
|
|
1291
|
+
# Final flush
|
|
1292
|
+
sys.stdout.flush()
|
|
1293
|
+
|
|
1294
|
+
# Show arrow-key selector (ASYNC VERSION)
|
|
1295
|
+
choice = await arrow_select_async(
|
|
1296
|
+
"💭 What would you like to do?",
|
|
1297
|
+
[
|
|
1298
|
+
"✓ Approve",
|
|
1299
|
+
"✗ Reject",
|
|
1300
|
+
f"💬 Reject with feedback (tell {puppy_name} what to change)",
|
|
1301
|
+
],
|
|
1302
|
+
)
|
|
1303
|
+
|
|
1304
|
+
if choice == "✓ Approve":
|
|
1305
|
+
confirmed = True
|
|
1306
|
+
elif choice == "✗ Reject":
|
|
1307
|
+
confirmed = False
|
|
1308
|
+
else:
|
|
1309
|
+
# User wants to provide feedback
|
|
1310
|
+
confirmed = False
|
|
1311
|
+
emit_info("")
|
|
1312
|
+
emit_info(f"Tell {puppy_name} what to change:")
|
|
1313
|
+
user_feedback = Prompt.ask(
|
|
1314
|
+
"[bold green]➤[/bold green]",
|
|
1315
|
+
default="",
|
|
1316
|
+
).strip()
|
|
1317
|
+
|
|
1318
|
+
if not user_feedback:
|
|
1319
|
+
user_feedback = None
|
|
1320
|
+
|
|
1321
|
+
except (KeyboardInterrupt, EOFError):
|
|
1322
|
+
emit_error("Cancelled by user")
|
|
1323
|
+
confirmed = False
|
|
1324
|
+
|
|
1325
|
+
finally:
|
|
1326
|
+
set_awaiting_user_input(False)
|
|
1327
|
+
|
|
1328
|
+
# Force Rich console to reset display state to prevent artifacts
|
|
1329
|
+
try:
|
|
1330
|
+
# Clear Rich's internal display state to prevent artifacts
|
|
1331
|
+
local_console.file.write("\r") # Return to start of line
|
|
1332
|
+
local_console.file.write("\x1b[K") # Clear current line
|
|
1333
|
+
local_console.file.flush()
|
|
1334
|
+
except Exception:
|
|
1335
|
+
pass
|
|
1336
|
+
|
|
1337
|
+
# Ensure streams are flushed
|
|
1338
|
+
sys.stdout.flush()
|
|
1339
|
+
sys.stderr.flush()
|
|
1340
|
+
|
|
1341
|
+
# Show result BEFORE resuming spinners (no puppy litter!)
|
|
1342
|
+
emit_info("")
|
|
1343
|
+
if not confirmed:
|
|
1344
|
+
if user_feedback:
|
|
1345
|
+
emit_error("Rejected with feedback!")
|
|
1346
|
+
emit_warning(f'Telling {puppy_name}: "{user_feedback}"')
|
|
1347
|
+
else:
|
|
1348
|
+
emit_error("Rejected.")
|
|
1349
|
+
else:
|
|
1350
|
+
emit_success("Approved!")
|
|
1351
|
+
|
|
1352
|
+
# NOW resume spinners after showing the result
|
|
1353
|
+
try:
|
|
1354
|
+
from code_puppy.messaging.spinner import resume_all_spinners
|
|
1355
|
+
|
|
1356
|
+
resume_all_spinners()
|
|
1357
|
+
except (ImportError, Exception):
|
|
1358
|
+
pass
|
|
1359
|
+
|
|
1360
|
+
return confirmed, user_feedback
|
|
1361
|
+
|
|
1362
|
+
|
|
1363
|
+
def _find_best_window(
|
|
1364
|
+
haystack_lines: list[str],
|
|
1365
|
+
needle: str,
|
|
1366
|
+
) -> Tuple[Optional[Tuple[int, int]], float]:
|
|
1367
|
+
"""
|
|
1368
|
+
Return (start, end) indices of the window with the highest
|
|
1369
|
+
Jaro-Winkler similarity to `needle`, along with that score.
|
|
1370
|
+
If nothing clears JW_THRESHOLD, return (None, score).
|
|
1371
|
+
"""
|
|
1372
|
+
needle = needle.rstrip("\n")
|
|
1373
|
+
needle_lines = needle.splitlines()
|
|
1374
|
+
win_size = len(needle_lines)
|
|
1375
|
+
best_score = 0.0
|
|
1376
|
+
best_span: Optional[Tuple[int, int]] = None
|
|
1377
|
+
# Pre-join the needle once; join windows on the fly
|
|
1378
|
+
for i in range(len(haystack_lines) - win_size + 1):
|
|
1379
|
+
window = "\n".join(haystack_lines[i : i + win_size])
|
|
1380
|
+
score = JaroWinkler.normalized_similarity(window, needle)
|
|
1381
|
+
if score > best_score:
|
|
1382
|
+
best_score = score
|
|
1383
|
+
best_span = (i, i + win_size)
|
|
1384
|
+
|
|
1385
|
+
return best_span, best_score
|
|
1386
|
+
|
|
1387
|
+
|
|
1388
|
+
def generate_group_id(tool_name: str, extra_context: str = "") -> str:
|
|
1389
|
+
"""Generate a unique group_id for tool output grouping.
|
|
1390
|
+
|
|
1391
|
+
Args:
|
|
1392
|
+
tool_name: Name of the tool (e.g., 'list_files', 'edit_file')
|
|
1393
|
+
extra_context: Optional extra context to make group_id more unique
|
|
1394
|
+
|
|
1395
|
+
Returns:
|
|
1396
|
+
A string in format: tool_name_hash
|
|
1397
|
+
"""
|
|
1398
|
+
# Create a unique identifier using timestamp, context, and a random component
|
|
1399
|
+
import random
|
|
1400
|
+
|
|
1401
|
+
timestamp = str(int(time.time() * 1000000)) # microseconds for more uniqueness
|
|
1402
|
+
random_component = random.randint(1000, 9999) # Add randomness
|
|
1403
|
+
context_string = f"{tool_name}_{timestamp}_{random_component}_{extra_context}"
|
|
1404
|
+
|
|
1405
|
+
# Generate a short hash
|
|
1406
|
+
hash_obj = hashlib.md5(context_string.encode())
|
|
1407
|
+
short_hash = hash_obj.hexdigest()[:8]
|
|
1408
|
+
|
|
1409
|
+
return f"{tool_name}_{short_hash}"
|