code-puppy 0.0.336__py3-none-any.whl → 0.0.348__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- code_puppy/agents/base_agent.py +41 -224
- code_puppy/agents/event_stream_handler.py +257 -0
- code_puppy/claude_cache_client.py +208 -2
- code_puppy/cli_runner.py +53 -35
- code_puppy/command_line/add_model_menu.py +8 -9
- code_puppy/command_line/autosave_menu.py +18 -24
- code_puppy/command_line/clipboard.py +527 -0
- code_puppy/command_line/core_commands.py +34 -0
- code_puppy/command_line/mcp/catalog_server_installer.py +5 -6
- code_puppy/command_line/mcp/custom_server_form.py +54 -19
- code_puppy/command_line/mcp/custom_server_installer.py +8 -9
- code_puppy/command_line/mcp/handler.py +0 -2
- code_puppy/command_line/mcp/help_command.py +1 -5
- code_puppy/command_line/mcp/start_command.py +36 -18
- code_puppy/command_line/onboarding_slides.py +0 -1
- code_puppy/command_line/prompt_toolkit_completion.py +124 -0
- code_puppy/command_line/utils.py +54 -0
- code_puppy/http_utils.py +93 -130
- code_puppy/mcp_/async_lifecycle.py +35 -4
- code_puppy/mcp_/managed_server.py +49 -24
- code_puppy/mcp_/manager.py +81 -52
- code_puppy/messaging/message_queue.py +11 -23
- code_puppy/messaging/messages.py +3 -0
- code_puppy/messaging/rich_renderer.py +13 -3
- code_puppy/model_factory.py +16 -0
- code_puppy/models.json +2 -2
- code_puppy/plugins/antigravity_oauth/antigravity_model.py +17 -2
- code_puppy/plugins/claude_code_oauth/utils.py +126 -7
- code_puppy/terminal_utils.py +128 -1
- code_puppy/tools/agent_tools.py +66 -13
- code_puppy/tools/command_runner.py +1 -0
- code_puppy/tools/common.py +3 -9
- {code_puppy-0.0.336.data → code_puppy-0.0.348.data}/data/code_puppy/models.json +2 -2
- {code_puppy-0.0.336.dist-info → code_puppy-0.0.348.dist-info}/METADATA +19 -71
- {code_puppy-0.0.336.dist-info → code_puppy-0.0.348.dist-info}/RECORD +39 -38
- code_puppy/command_line/mcp/add_command.py +0 -170
- {code_puppy-0.0.336.data → code_puppy-0.0.348.data}/data/code_puppy/models_dev_api.json +0 -0
- {code_puppy-0.0.336.dist-info → code_puppy-0.0.348.dist-info}/WHEEL +0 -0
- {code_puppy-0.0.336.dist-info → code_puppy-0.0.348.dist-info}/entry_points.txt +0 -0
- {code_puppy-0.0.336.dist-info → code_puppy-0.0.348.dist-info}/licenses/LICENSE +0 -0
|
@@ -43,7 +43,7 @@ CUSTOM_SERVER_EXAMPLES = {
|
|
|
43
43
|
"type": "http",
|
|
44
44
|
"url": "http://localhost:8080/mcp",
|
|
45
45
|
"headers": {
|
|
46
|
-
"Authorization": "Bearer
|
|
46
|
+
"Authorization": "Bearer $MY_API_KEY",
|
|
47
47
|
"Content-Type": "application/json"
|
|
48
48
|
},
|
|
49
49
|
"timeout": 30
|
|
@@ -52,7 +52,7 @@ CUSTOM_SERVER_EXAMPLES = {
|
|
|
52
52
|
"type": "sse",
|
|
53
53
|
"url": "http://localhost:8080/sse",
|
|
54
54
|
"headers": {
|
|
55
|
-
"Authorization": "Bearer
|
|
55
|
+
"Authorization": "Bearer $MY_API_KEY"
|
|
56
56
|
}
|
|
57
57
|
}""",
|
|
58
58
|
}
|
|
@@ -367,24 +367,59 @@ class CustomServerForm:
|
|
|
367
367
|
config_dict = json.loads(self.json_config)
|
|
368
368
|
|
|
369
369
|
try:
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
370
|
+
# In edit mode, find the existing server and update it
|
|
371
|
+
if self.edit_mode and self.original_name:
|
|
372
|
+
existing_config = self.manager.get_server_by_name(self.original_name)
|
|
373
|
+
if existing_config:
|
|
374
|
+
# Use the existing server's ID for the update
|
|
375
|
+
server_config = ServerConfig(
|
|
376
|
+
id=existing_config.id,
|
|
377
|
+
name=server_name,
|
|
378
|
+
type=server_type,
|
|
379
|
+
enabled=True,
|
|
380
|
+
config=config_dict,
|
|
381
|
+
)
|
|
382
|
+
|
|
383
|
+
# Update the server in the manager
|
|
384
|
+
success = self.manager.update_server(
|
|
385
|
+
existing_config.id, server_config
|
|
386
|
+
)
|
|
387
|
+
|
|
388
|
+
if not success:
|
|
389
|
+
self.validation_error = "Failed to update server"
|
|
390
|
+
self.status_message = "Save failed: Could not update server"
|
|
391
|
+
self.status_is_error = True
|
|
392
|
+
return False
|
|
393
|
+
|
|
394
|
+
server_id = existing_config.id
|
|
395
|
+
else:
|
|
396
|
+
# Original server not found, treat as new registration
|
|
397
|
+
server_config = ServerConfig(
|
|
398
|
+
id=server_name,
|
|
399
|
+
name=server_name,
|
|
400
|
+
type=server_type,
|
|
401
|
+
enabled=True,
|
|
402
|
+
config=config_dict,
|
|
403
|
+
)
|
|
404
|
+
server_id = self.manager.register_server(server_config)
|
|
405
|
+
else:
|
|
406
|
+
# New server - register it
|
|
407
|
+
server_config = ServerConfig(
|
|
408
|
+
id=server_name,
|
|
409
|
+
name=server_name,
|
|
410
|
+
type=server_type,
|
|
411
|
+
enabled=True,
|
|
412
|
+
config=config_dict,
|
|
385
413
|
)
|
|
386
|
-
|
|
387
|
-
|
|
414
|
+
|
|
415
|
+
# Register with manager
|
|
416
|
+
server_id = self.manager.register_server(server_config)
|
|
417
|
+
|
|
418
|
+
if not server_id:
|
|
419
|
+
self.validation_error = "Failed to register server"
|
|
420
|
+
self.status_message = "Save failed: Could not register server (name may already exist)"
|
|
421
|
+
self.status_is_error = True
|
|
422
|
+
return False
|
|
388
423
|
|
|
389
424
|
# Save to mcp_servers.json for persistence
|
|
390
425
|
if os.path.exists(MCP_SERVERS_FILE):
|
|
@@ -7,6 +7,7 @@ custom MCP servers with JSON configuration.
|
|
|
7
7
|
import json
|
|
8
8
|
import os
|
|
9
9
|
|
|
10
|
+
from code_puppy.command_line.utils import safe_input
|
|
10
11
|
from code_puppy.messaging import emit_error, emit_info, emit_success, emit_warning
|
|
11
12
|
|
|
12
13
|
# Example configurations for each server type
|
|
@@ -24,7 +25,7 @@ CUSTOM_SERVER_EXAMPLES = {
|
|
|
24
25
|
"type": "http",
|
|
25
26
|
"url": "http://localhost:8080/mcp",
|
|
26
27
|
"headers": {
|
|
27
|
-
"Authorization": "Bearer
|
|
28
|
+
"Authorization": "Bearer $MY_API_KEY",
|
|
28
29
|
"Content-Type": "application/json"
|
|
29
30
|
},
|
|
30
31
|
"timeout": 30
|
|
@@ -33,7 +34,7 @@ CUSTOM_SERVER_EXAMPLES = {
|
|
|
33
34
|
"type": "sse",
|
|
34
35
|
"url": "http://localhost:8080/sse",
|
|
35
36
|
"headers": {
|
|
36
|
-
"Authorization": "Bearer
|
|
37
|
+
"Authorization": "Bearer $MY_API_KEY"
|
|
37
38
|
}
|
|
38
39
|
}""",
|
|
39
40
|
}
|
|
@@ -58,7 +59,7 @@ def prompt_and_install_custom_server(manager) -> bool:
|
|
|
58
59
|
|
|
59
60
|
# Get server name
|
|
60
61
|
try:
|
|
61
|
-
server_name =
|
|
62
|
+
server_name = safe_input(" Server name: ")
|
|
62
63
|
if not server_name:
|
|
63
64
|
emit_warning("Server name is required")
|
|
64
65
|
return False
|
|
@@ -71,9 +72,7 @@ def prompt_and_install_custom_server(manager) -> bool:
|
|
|
71
72
|
existing = find_server_id_by_name(manager, server_name)
|
|
72
73
|
if existing:
|
|
73
74
|
try:
|
|
74
|
-
override =
|
|
75
|
-
f" Server '{server_name}' exists. Override? [y/N]: "
|
|
76
|
-
).strip()
|
|
75
|
+
override = safe_input(f" Server '{server_name}' exists. Override? [y/N]: ")
|
|
77
76
|
if not override.lower().startswith("y"):
|
|
78
77
|
emit_warning("Cancelled")
|
|
79
78
|
return False
|
|
@@ -89,7 +88,7 @@ def prompt_and_install_custom_server(manager) -> bool:
|
|
|
89
88
|
emit_info(" 3. 📡 sse - Server-Sent Events\n")
|
|
90
89
|
|
|
91
90
|
try:
|
|
92
|
-
type_choice =
|
|
91
|
+
type_choice = safe_input(" Enter choice [1-3]: ")
|
|
93
92
|
except (KeyboardInterrupt, EOFError):
|
|
94
93
|
emit_info("")
|
|
95
94
|
emit_warning("Cancelled")
|
|
@@ -115,8 +114,8 @@ def prompt_and_install_custom_server(manager) -> bool:
|
|
|
115
114
|
empty_count = 0
|
|
116
115
|
try:
|
|
117
116
|
while True:
|
|
118
|
-
line =
|
|
119
|
-
if line
|
|
117
|
+
line = safe_input("")
|
|
118
|
+
if line == "":
|
|
120
119
|
empty_count += 1
|
|
121
120
|
if empty_count >= 2:
|
|
122
121
|
break
|
|
@@ -12,7 +12,6 @@ from rich.text import Text
|
|
|
12
12
|
|
|
13
13
|
from code_puppy.messaging import emit_info
|
|
14
14
|
|
|
15
|
-
from .add_command import AddCommand
|
|
16
15
|
from .base import MCPCommandBase
|
|
17
16
|
from .edit_command import EditCommand
|
|
18
17
|
from .help_command import HelpCommand
|
|
@@ -63,7 +62,6 @@ class MCPCommandHandler(MCPCommandBase):
|
|
|
63
62
|
"restart": RestartCommand(),
|
|
64
63
|
"status": StatusCommand(),
|
|
65
64
|
"test": TestCommand(),
|
|
66
|
-
"add": AddCommand(),
|
|
67
65
|
"edit": EditCommand(),
|
|
68
66
|
"remove": RemoveCommand(),
|
|
69
67
|
"logs": LogsCommand(),
|
|
@@ -101,10 +101,6 @@ class HelpCommand(MCPCommandBase):
|
|
|
101
101
|
Text("/mcp logs", style="cyan")
|
|
102
102
|
+ Text(" <name> [limit] Show recent events (default limit: 10)")
|
|
103
103
|
)
|
|
104
|
-
help_lines.append(
|
|
105
|
-
Text("/mcp add", style="cyan")
|
|
106
|
-
+ Text(" [json] Add new server (JSON or wizard)")
|
|
107
|
-
)
|
|
108
104
|
help_lines.append(
|
|
109
105
|
Text("/mcp edit", style="cyan")
|
|
110
106
|
+ Text(" <name> Edit existing server config")
|
|
@@ -134,7 +130,7 @@ class HelpCommand(MCPCommandBase):
|
|
|
134
130
|
/mcp start-all # Start all servers at once
|
|
135
131
|
/mcp stop-all # Stop all running servers
|
|
136
132
|
/mcp edit filesystem # Edit an existing server config
|
|
137
|
-
/mcp
|
|
133
|
+
/mcp remove filesystem # Remove a server"""
|
|
138
134
|
help_lines.append(Text(examples_text, style="dim"))
|
|
139
135
|
|
|
140
136
|
# Combine all lines
|
|
@@ -3,7 +3,6 @@ MCP Start Command - Starts a specific MCP server.
|
|
|
3
3
|
"""
|
|
4
4
|
|
|
5
5
|
import logging
|
|
6
|
-
import time
|
|
7
6
|
from typing import List, Optional
|
|
8
7
|
|
|
9
8
|
from rich.text import Text
|
|
@@ -23,6 +22,7 @@ class StartCommand(MCPCommandBase):
|
|
|
23
22
|
Command handler for starting MCP servers.
|
|
24
23
|
|
|
25
24
|
Starts a specific MCP server by name and reloads the agent.
|
|
25
|
+
The server subprocess starts asynchronously in the background.
|
|
26
26
|
"""
|
|
27
27
|
|
|
28
28
|
def execute(self, args: List[str], group_id: Optional[str] = None) -> None:
|
|
@@ -56,31 +56,49 @@ class StartCommand(MCPCommandBase):
|
|
|
56
56
|
suggest_similar_servers(self.manager, server_name, group_id=group_id)
|
|
57
57
|
return
|
|
58
58
|
|
|
59
|
-
#
|
|
59
|
+
# Get server info for better messaging (safely handle missing method)
|
|
60
|
+
server_type = "unknown"
|
|
61
|
+
try:
|
|
62
|
+
if hasattr(self.manager, "get_server_by_name"):
|
|
63
|
+
server_config = self.manager.get_server_by_name(server_name)
|
|
64
|
+
server_type = (
|
|
65
|
+
getattr(server_config, "type", "unknown")
|
|
66
|
+
if server_config
|
|
67
|
+
else "unknown"
|
|
68
|
+
)
|
|
69
|
+
except Exception:
|
|
70
|
+
pass # Default to unknown type if we can't determine it
|
|
71
|
+
|
|
72
|
+
# Start the server (schedules async start in background)
|
|
60
73
|
success = self.manager.start_server_sync(server_id)
|
|
61
74
|
|
|
62
75
|
if success:
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
76
|
+
if server_type == "stdio":
|
|
77
|
+
# Stdio servers start subprocess asynchronously
|
|
78
|
+
emit_success(
|
|
79
|
+
f"🚀 Starting server: {server_name} (subprocess starting in background)",
|
|
80
|
+
message_group=group_id,
|
|
81
|
+
)
|
|
82
|
+
emit_info(
|
|
83
|
+
Text.from_markup(
|
|
84
|
+
"[dim]Tip: Use /mcp status to check if the server is fully initialized[/dim]"
|
|
85
|
+
),
|
|
86
|
+
message_group=group_id,
|
|
87
|
+
)
|
|
88
|
+
else:
|
|
89
|
+
# SSE/HTTP servers connect on first use
|
|
90
|
+
emit_success(
|
|
91
|
+
f"✅ Enabled server: {server_name}",
|
|
92
|
+
message_group=group_id,
|
|
93
|
+
)
|
|
78
94
|
|
|
79
95
|
# Reload the agent to pick up the newly enabled server
|
|
96
|
+
# NOTE: We don't block or wait - the server will be ready
|
|
97
|
+
# when the next prompt runs (pydantic-ai handles connection)
|
|
80
98
|
try:
|
|
81
99
|
agent = get_current_agent()
|
|
82
100
|
agent.reload_code_generation_agent()
|
|
83
|
-
#
|
|
101
|
+
# Clear MCP tool cache - it will be repopulated on next run
|
|
84
102
|
agent.update_mcp_tool_cache_sync()
|
|
85
103
|
emit_info(
|
|
86
104
|
"Agent reloaded with updated servers",
|
|
@@ -122,7 +122,6 @@ def slide_mcp() -> str:
|
|
|
122
122
|
content += "[white]Supercharge with external tools![/white]\n\n"
|
|
123
123
|
content += "[green]Commands:[/green]\n"
|
|
124
124
|
content += " [cyan]/mcp install[/cyan] Browse catalog\n"
|
|
125
|
-
content += " [cyan]/mcp add[/cyan] Add custom server\n"
|
|
126
125
|
content += " [cyan]/mcp list[/cyan] See your servers\n\n"
|
|
127
126
|
content += "[yellow]🌟 Popular picks:[/yellow]\n"
|
|
128
127
|
content += " • GitHub integration\n"
|
|
@@ -27,6 +27,10 @@ from code_puppy.command_line.attachments import (
|
|
|
27
27
|
_detect_path_tokens,
|
|
28
28
|
_tokenise,
|
|
29
29
|
)
|
|
30
|
+
from code_puppy.command_line.clipboard import (
|
|
31
|
+
capture_clipboard_image_to_pending,
|
|
32
|
+
has_image_in_clipboard,
|
|
33
|
+
)
|
|
30
34
|
from code_puppy.command_line.command_registry import get_unique_commands
|
|
31
35
|
from code_puppy.command_line.file_path_completion import FilePathCompleter
|
|
32
36
|
from code_puppy.command_line.load_context_completion import LoadContextCompleter
|
|
@@ -644,6 +648,126 @@ async def get_input_with_combined_completion(
|
|
|
644
648
|
else:
|
|
645
649
|
event.current_buffer.validate_and_handle()
|
|
646
650
|
|
|
651
|
+
# Handle bracketed paste - smart detection for text vs images.
|
|
652
|
+
# Most terminals (Windows included!) send Ctrl+V through bracketed paste.
|
|
653
|
+
# - If there's meaningful text content → paste as text (drag-and-drop file paths, copied text)
|
|
654
|
+
# - If text is empty/whitespace → check for clipboard image (image paste on Windows)
|
|
655
|
+
@bindings.add(Keys.BracketedPaste)
|
|
656
|
+
def handle_bracketed_paste(event):
|
|
657
|
+
"""Handle bracketed paste - smart text vs image detection."""
|
|
658
|
+
pasted_data = event.data
|
|
659
|
+
|
|
660
|
+
# If we have meaningful text content, paste it (don't check for images)
|
|
661
|
+
# This handles drag-and-drop file paths and normal text paste
|
|
662
|
+
if pasted_data and pasted_data.strip():
|
|
663
|
+
# Normalize Windows line endings to Unix style
|
|
664
|
+
sanitized_data = pasted_data.replace("\r\n", "\n").replace("\r", "\n")
|
|
665
|
+
event.app.current_buffer.insert_text(sanitized_data)
|
|
666
|
+
return
|
|
667
|
+
|
|
668
|
+
# No meaningful text - check if clipboard has an image (Windows image paste!)
|
|
669
|
+
try:
|
|
670
|
+
if has_image_in_clipboard():
|
|
671
|
+
placeholder = capture_clipboard_image_to_pending()
|
|
672
|
+
if placeholder:
|
|
673
|
+
event.app.current_buffer.insert_text(placeholder + " ")
|
|
674
|
+
event.app.output.bell()
|
|
675
|
+
return
|
|
676
|
+
except Exception:
|
|
677
|
+
pass
|
|
678
|
+
|
|
679
|
+
# Fallback: if there was whitespace-only data, paste it
|
|
680
|
+
if pasted_data:
|
|
681
|
+
sanitized_data = pasted_data.replace("\r\n", "\n").replace("\r", "\n")
|
|
682
|
+
event.app.current_buffer.insert_text(sanitized_data)
|
|
683
|
+
|
|
684
|
+
# Fallback Ctrl+V for terminals without bracketed paste support
|
|
685
|
+
@bindings.add("c-v", eager=True)
|
|
686
|
+
def handle_smart_paste(event):
|
|
687
|
+
"""Handle Ctrl+V - auto-detect image vs text in clipboard."""
|
|
688
|
+
try:
|
|
689
|
+
# Check for image first
|
|
690
|
+
if has_image_in_clipboard():
|
|
691
|
+
placeholder = capture_clipboard_image_to_pending()
|
|
692
|
+
if placeholder:
|
|
693
|
+
event.app.current_buffer.insert_text(placeholder + " ")
|
|
694
|
+
# The placeholder itself is visible feedback - no need for extra output
|
|
695
|
+
# Use bell for audible feedback (works in most terminals)
|
|
696
|
+
event.app.output.bell()
|
|
697
|
+
return # Don't also paste text
|
|
698
|
+
except Exception:
|
|
699
|
+
pass # Fall through to text paste on any error
|
|
700
|
+
|
|
701
|
+
# No image (or error) - do normal text paste
|
|
702
|
+
# prompt_toolkit doesn't have built-in paste, so we handle it manually
|
|
703
|
+
try:
|
|
704
|
+
import platform
|
|
705
|
+
import subprocess
|
|
706
|
+
|
|
707
|
+
text = None
|
|
708
|
+
system = platform.system()
|
|
709
|
+
|
|
710
|
+
if system == "Darwin": # macOS
|
|
711
|
+
result = subprocess.run(
|
|
712
|
+
["pbpaste"], capture_output=True, text=True, timeout=2
|
|
713
|
+
)
|
|
714
|
+
if result.returncode == 0:
|
|
715
|
+
text = result.stdout
|
|
716
|
+
elif system == "Windows":
|
|
717
|
+
# Windows - use powershell
|
|
718
|
+
result = subprocess.run(
|
|
719
|
+
["powershell", "-command", "Get-Clipboard"],
|
|
720
|
+
capture_output=True,
|
|
721
|
+
text=True,
|
|
722
|
+
timeout=2,
|
|
723
|
+
)
|
|
724
|
+
if result.returncode == 0:
|
|
725
|
+
text = result.stdout
|
|
726
|
+
else: # Linux
|
|
727
|
+
# Try xclip first, then xsel
|
|
728
|
+
for cmd in [
|
|
729
|
+
["xclip", "-selection", "clipboard", "-o"],
|
|
730
|
+
["xsel", "--clipboard", "--output"],
|
|
731
|
+
]:
|
|
732
|
+
try:
|
|
733
|
+
result = subprocess.run(
|
|
734
|
+
cmd, capture_output=True, text=True, timeout=2
|
|
735
|
+
)
|
|
736
|
+
if result.returncode == 0:
|
|
737
|
+
text = result.stdout
|
|
738
|
+
break
|
|
739
|
+
except FileNotFoundError:
|
|
740
|
+
continue
|
|
741
|
+
|
|
742
|
+
if text:
|
|
743
|
+
# Normalize Windows line endings to Unix style
|
|
744
|
+
text = text.replace("\r\n", "\n").replace("\r", "\n")
|
|
745
|
+
# Strip trailing newline that clipboard tools often add
|
|
746
|
+
text = text.rstrip("\n")
|
|
747
|
+
event.app.current_buffer.insert_text(text)
|
|
748
|
+
except Exception:
|
|
749
|
+
pass # Silently fail if text paste doesn't work
|
|
750
|
+
|
|
751
|
+
# F3 - dedicated image paste (shows error if no image)
|
|
752
|
+
@bindings.add("f3")
|
|
753
|
+
def handle_image_paste_f3(event):
|
|
754
|
+
"""Handle F3 - paste image from clipboard (image-only, shows error if none)."""
|
|
755
|
+
try:
|
|
756
|
+
if has_image_in_clipboard():
|
|
757
|
+
placeholder = capture_clipboard_image_to_pending()
|
|
758
|
+
if placeholder:
|
|
759
|
+
event.app.current_buffer.insert_text(placeholder + " ")
|
|
760
|
+
# The placeholder itself is visible feedback
|
|
761
|
+
# Use bell for audible feedback (works in most terminals)
|
|
762
|
+
event.app.output.bell()
|
|
763
|
+
else:
|
|
764
|
+
# Insert a transient message that user can delete
|
|
765
|
+
event.app.current_buffer.insert_text("[⚠️ no image in clipboard] ")
|
|
766
|
+
event.app.output.bell()
|
|
767
|
+
except Exception:
|
|
768
|
+
event.app.current_buffer.insert_text("[❌ clipboard error] ")
|
|
769
|
+
event.app.output.bell()
|
|
770
|
+
|
|
647
771
|
session = PromptSession(
|
|
648
772
|
completer=completer,
|
|
649
773
|
history=history,
|
code_puppy/command_line/utils.py
CHANGED
|
@@ -37,3 +37,57 @@ def make_directory_table(path: str = None) -> Table:
|
|
|
37
37
|
for f in sorted(files):
|
|
38
38
|
table.add_row("[yellow]file[/yellow]", f"{f}")
|
|
39
39
|
return table
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def _reset_windows_console() -> None:
|
|
43
|
+
"""Reset Windows console to normal input mode.
|
|
44
|
+
|
|
45
|
+
After a prompt_toolkit Application exits on Windows, the console can be
|
|
46
|
+
left in a weird state where Enter doesn't work properly. This resets it.
|
|
47
|
+
"""
|
|
48
|
+
import sys
|
|
49
|
+
|
|
50
|
+
if sys.platform != "win32":
|
|
51
|
+
return
|
|
52
|
+
|
|
53
|
+
try:
|
|
54
|
+
import ctypes
|
|
55
|
+
|
|
56
|
+
kernel32 = ctypes.windll.kernel32
|
|
57
|
+
# Get handle to stdin
|
|
58
|
+
STD_INPUT_HANDLE = -10
|
|
59
|
+
handle = kernel32.GetStdHandle(STD_INPUT_HANDLE)
|
|
60
|
+
|
|
61
|
+
# Enable line input and echo (normal console mode)
|
|
62
|
+
# ENABLE_LINE_INPUT = 0x0002
|
|
63
|
+
# ENABLE_ECHO_INPUT = 0x0004
|
|
64
|
+
# ENABLE_PROCESSED_INPUT = 0x0001
|
|
65
|
+
NORMAL_MODE = 0x0007 # Line input + echo + processed
|
|
66
|
+
kernel32.SetConsoleMode(handle, NORMAL_MODE)
|
|
67
|
+
except Exception:
|
|
68
|
+
pass # Silently ignore errors - this is best-effort
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
def safe_input(prompt_text: str = "") -> str:
|
|
72
|
+
"""Cross-platform safe input that works after prompt_toolkit Applications.
|
|
73
|
+
|
|
74
|
+
On Windows, raw input() can fail after a prompt_toolkit Application exits
|
|
75
|
+
because the terminal can be left in a weird state. This function resets
|
|
76
|
+
the Windows console mode before calling input().
|
|
77
|
+
|
|
78
|
+
Args:
|
|
79
|
+
prompt_text: The prompt to display to the user
|
|
80
|
+
|
|
81
|
+
Returns:
|
|
82
|
+
The user's input string (stripped)
|
|
83
|
+
|
|
84
|
+
Raises:
|
|
85
|
+
KeyboardInterrupt: If user presses Ctrl+C
|
|
86
|
+
EOFError: If user presses Ctrl+D/Ctrl+Z
|
|
87
|
+
"""
|
|
88
|
+
# Reset Windows console to normal mode before reading input
|
|
89
|
+
_reset_windows_console()
|
|
90
|
+
|
|
91
|
+
# Use standard input() - now that console is reset, it should work
|
|
92
|
+
result = input(prompt_text)
|
|
93
|
+
return result.strip() if result else ""
|