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
|
@@ -8,6 +8,7 @@
|
|
|
8
8
|
# BOLD = '\033[1m'
|
|
9
9
|
import asyncio
|
|
10
10
|
import os
|
|
11
|
+
import sys
|
|
11
12
|
from typing import Optional
|
|
12
13
|
|
|
13
14
|
from prompt_toolkit import PromptSession
|
|
@@ -26,13 +27,19 @@ from code_puppy.command_line.attachments import (
|
|
|
26
27
|
_detect_path_tokens,
|
|
27
28
|
_tokenise,
|
|
28
29
|
)
|
|
30
|
+
from code_puppy.command_line.clipboard import (
|
|
31
|
+
capture_clipboard_image_to_pending,
|
|
32
|
+
has_image_in_clipboard,
|
|
33
|
+
)
|
|
34
|
+
from code_puppy.command_line.command_registry import get_unique_commands
|
|
29
35
|
from code_puppy.command_line.file_path_completion import FilePathCompleter
|
|
30
36
|
from code_puppy.command_line.load_context_completion import LoadContextCompleter
|
|
37
|
+
from code_puppy.command_line.mcp_completion import MCPCompleter
|
|
31
38
|
from code_puppy.command_line.model_picker_completion import (
|
|
32
39
|
ModelNameCompleter,
|
|
33
40
|
get_active_model,
|
|
34
|
-
update_model_in_input,
|
|
35
41
|
)
|
|
42
|
+
from code_puppy.command_line.pin_command_completion import PinCompleter, UnpinCompleter
|
|
36
43
|
from code_puppy.command_line.utils import list_directory
|
|
37
44
|
from code_puppy.config import (
|
|
38
45
|
COMMAND_HISTORY_FILE,
|
|
@@ -42,65 +49,119 @@ from code_puppy.config import (
|
|
|
42
49
|
)
|
|
43
50
|
|
|
44
51
|
|
|
52
|
+
def _sanitize_for_encoding(text: str) -> str:
|
|
53
|
+
"""Remove or replace characters that can't be safely encoded.
|
|
54
|
+
|
|
55
|
+
This handles:
|
|
56
|
+
- Lone surrogate characters (U+D800-U+DFFF) which are invalid in UTF-8
|
|
57
|
+
- Other problematic Unicode sequences from Windows copy-paste
|
|
58
|
+
|
|
59
|
+
Args:
|
|
60
|
+
text: The string to sanitize
|
|
61
|
+
|
|
62
|
+
Returns:
|
|
63
|
+
A cleaned string safe for UTF-8 encoding
|
|
64
|
+
"""
|
|
65
|
+
# First, try to encode as UTF-8 to catch any problematic characters
|
|
66
|
+
try:
|
|
67
|
+
text.encode("utf-8")
|
|
68
|
+
return text # String is already valid UTF-8
|
|
69
|
+
except UnicodeEncodeError:
|
|
70
|
+
pass
|
|
71
|
+
|
|
72
|
+
# Replace surrogates and other problematic characters
|
|
73
|
+
# Use 'surrogatepass' to encode surrogates, then decode with 'replace' to clean them
|
|
74
|
+
try:
|
|
75
|
+
# Encode allowing surrogates, then decode replacing invalid sequences
|
|
76
|
+
cleaned = text.encode("utf-8", errors="surrogatepass").decode(
|
|
77
|
+
"utf-8", errors="replace"
|
|
78
|
+
)
|
|
79
|
+
return cleaned
|
|
80
|
+
except (UnicodeEncodeError, UnicodeDecodeError):
|
|
81
|
+
# Last resort: filter out all non-BMP and surrogate characters
|
|
82
|
+
return "".join(
|
|
83
|
+
char
|
|
84
|
+
for char in text
|
|
85
|
+
if ord(char) < 0xD800 or (ord(char) > 0xDFFF and ord(char) < 0x10000)
|
|
86
|
+
)
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
class SafeFileHistory(FileHistory):
|
|
90
|
+
"""A FileHistory that handles encoding errors gracefully on Windows.
|
|
91
|
+
|
|
92
|
+
Windows terminals and copy-paste operations can introduce invalid
|
|
93
|
+
Unicode surrogate characters that cause UTF-8 encoding to fail.
|
|
94
|
+
This class sanitizes history entries before writing them to disk.
|
|
95
|
+
"""
|
|
96
|
+
|
|
97
|
+
def store_string(self, string: str) -> None:
|
|
98
|
+
"""Store a string in the history, sanitizing it first."""
|
|
99
|
+
sanitized = _sanitize_for_encoding(string)
|
|
100
|
+
try:
|
|
101
|
+
super().store_string(sanitized)
|
|
102
|
+
except (UnicodeEncodeError, UnicodeDecodeError, OSError) as e:
|
|
103
|
+
# If we still can't write, log the error but don't crash
|
|
104
|
+
# This can happen with particularly malformed input
|
|
105
|
+
# Note: Using sys.stderr here intentionally - this is a low-level
|
|
106
|
+
# warning that shouldn't use the messaging system
|
|
107
|
+
sys.stderr.write(f"Warning: Could not save to command history: {e}\n")
|
|
108
|
+
|
|
109
|
+
|
|
45
110
|
class SetCompleter(Completer):
|
|
46
111
|
def __init__(self, trigger: str = "/set"):
|
|
47
112
|
self.trigger = trigger
|
|
48
113
|
|
|
49
114
|
def get_completions(self, document, complete_event):
|
|
115
|
+
cursor_position = document.cursor_position
|
|
50
116
|
text_before_cursor = document.text_before_cursor
|
|
51
117
|
stripped_text_for_trigger_check = text_before_cursor.lstrip()
|
|
52
118
|
|
|
53
|
-
|
|
119
|
+
# If user types just /set (no space), suggest adding a space
|
|
120
|
+
if stripped_text_for_trigger_check == self.trigger:
|
|
121
|
+
from prompt_toolkit.formatted_text import FormattedText
|
|
122
|
+
|
|
123
|
+
yield Completion(
|
|
124
|
+
self.trigger + " ",
|
|
125
|
+
start_position=-len(self.trigger),
|
|
126
|
+
display=self.trigger + " ",
|
|
127
|
+
display_meta=FormattedText(
|
|
128
|
+
[("class:set-completer-meta", "set config key")]
|
|
129
|
+
),
|
|
130
|
+
)
|
|
131
|
+
return
|
|
132
|
+
|
|
133
|
+
# Require a space after /set before showing completions
|
|
134
|
+
if not stripped_text_for_trigger_check.startswith(self.trigger + " "):
|
|
54
135
|
return
|
|
55
136
|
|
|
56
137
|
# Determine the part of the text that is relevant for this completer
|
|
57
138
|
# This handles cases like " /set foo" where the trigger isn't at the start of the string
|
|
58
139
|
actual_trigger_pos = text_before_cursor.find(self.trigger)
|
|
59
|
-
effective_input = text_before_cursor[
|
|
60
|
-
actual_trigger_pos:
|
|
61
|
-
] # e.g., "/set keypart" or "/set "
|
|
62
|
-
|
|
63
|
-
tokens = effective_input.split()
|
|
64
|
-
|
|
65
|
-
# Case 1: Input is exactly the trigger (e.g., "/set") and nothing more (not even a trailing space on effective_input).
|
|
66
|
-
# Suggest adding a space.
|
|
67
|
-
if (
|
|
68
|
-
len(tokens) == 1
|
|
69
|
-
and tokens[0] == self.trigger
|
|
70
|
-
and not effective_input.endswith(" ")
|
|
71
|
-
):
|
|
72
|
-
yield Completion(
|
|
73
|
-
text=self.trigger + " ", # Text to insert
|
|
74
|
-
start_position=-len(tokens[0]), # Replace the trigger itself
|
|
75
|
-
display=self.trigger + " ", # Visual display
|
|
76
|
-
display_meta="set config key",
|
|
77
|
-
)
|
|
78
|
-
return
|
|
79
140
|
|
|
80
|
-
#
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
# If len(tokens) == 1, it implies effective_input was like "/set ", so base_to_complete remains ""
|
|
85
|
-
# This means we list all keys.
|
|
141
|
+
# Extract the input after /set and space (up to cursor)
|
|
142
|
+
trigger_end = actual_trigger_pos + len(self.trigger) + 1 # +1 for the space
|
|
143
|
+
text_after_trigger = text_before_cursor[trigger_end:cursor_position].lstrip()
|
|
144
|
+
start_position = -(len(text_after_trigger))
|
|
86
145
|
|
|
87
146
|
# --- SPECIAL HANDLING FOR 'model' KEY ---
|
|
88
|
-
if
|
|
147
|
+
if text_after_trigger == "model":
|
|
89
148
|
# Don't return any completions -- let ModelNameCompleter handle it
|
|
90
149
|
return
|
|
91
|
-
|
|
150
|
+
|
|
151
|
+
# Get config keys and sort them alphabetically for consistent display
|
|
152
|
+
config_keys = sorted(get_config_keys())
|
|
153
|
+
|
|
154
|
+
for key in config_keys:
|
|
92
155
|
if key == "model" or key == "puppy_token":
|
|
93
156
|
continue # exclude 'model' and 'puppy_token' from regular /set completions
|
|
94
|
-
if key.startswith(
|
|
157
|
+
if key.startswith(text_after_trigger):
|
|
95
158
|
prev_value = get_value(key)
|
|
96
159
|
value_part = f" = {prev_value}" if prev_value is not None else " = "
|
|
97
160
|
completion_text = f"{key}{value_part}"
|
|
98
161
|
|
|
99
162
|
yield Completion(
|
|
100
163
|
completion_text,
|
|
101
|
-
start_position
|
|
102
|
-
base_to_complete
|
|
103
|
-
), # Correctly replace only the typed part of the key
|
|
164
|
+
start_position=start_position,
|
|
104
165
|
display_meta="",
|
|
105
166
|
)
|
|
106
167
|
|
|
@@ -229,23 +290,43 @@ class CDCompleter(Completer):
|
|
|
229
290
|
self.trigger = trigger
|
|
230
291
|
|
|
231
292
|
def get_completions(self, document, complete_event):
|
|
232
|
-
|
|
233
|
-
|
|
293
|
+
text_before_cursor = document.text_before_cursor
|
|
294
|
+
stripped_text = text_before_cursor.lstrip()
|
|
295
|
+
|
|
296
|
+
# Require a space after /cd before showing completions (consistency with other completers)
|
|
297
|
+
if not stripped_text.startswith(self.trigger + " "):
|
|
234
298
|
return
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
299
|
+
|
|
300
|
+
# Extract the directory path after /cd and space (up to cursor)
|
|
301
|
+
trigger_pos = text_before_cursor.find(self.trigger)
|
|
302
|
+
trigger_end = trigger_pos + len(self.trigger) + 1 # +1 for the space
|
|
303
|
+
dir_path = text_before_cursor[trigger_end:].lstrip()
|
|
304
|
+
start_position = -(len(dir_path))
|
|
305
|
+
|
|
240
306
|
try:
|
|
241
|
-
prefix = os.path.expanduser(
|
|
307
|
+
prefix = os.path.expanduser(dir_path)
|
|
242
308
|
part = os.path.dirname(prefix) if os.path.dirname(prefix) else "."
|
|
243
309
|
dirs, _ = list_directory(part)
|
|
244
|
-
dirnames = [d for d in dirs if d.startswith(os.path.basename(
|
|
245
|
-
base_dir = os.path.dirname(
|
|
310
|
+
dirnames = [d for d in dirs if d.startswith(os.path.basename(prefix))]
|
|
311
|
+
base_dir = os.path.dirname(prefix)
|
|
312
|
+
|
|
313
|
+
# Preserve the user's original prefix (e.g., ~/ or relative paths)
|
|
314
|
+
# Extract what the user originally typed (with ~ or ./ preserved)
|
|
315
|
+
if dir_path.startswith("~"):
|
|
316
|
+
# User typed something with ~, preserve it
|
|
317
|
+
user_prefix = "~" + os.sep
|
|
318
|
+
# For suggestion, we replace the expanded base_dir back with ~/
|
|
319
|
+
original_prefix = dir_path.rstrip(os.sep)
|
|
320
|
+
else:
|
|
321
|
+
user_prefix = None
|
|
322
|
+
original_prefix = None
|
|
323
|
+
|
|
246
324
|
for d in dirnames:
|
|
247
325
|
# Build the completion text so we keep the already-typed directory parts.
|
|
248
|
-
if
|
|
326
|
+
if user_prefix and original_prefix:
|
|
327
|
+
# Restore ~ prefix
|
|
328
|
+
suggestion = user_prefix + d + os.sep
|
|
329
|
+
elif base_dir and base_dir != ".":
|
|
249
330
|
suggestion = os.path.join(base_dir, d)
|
|
250
331
|
else:
|
|
251
332
|
suggestion = d
|
|
@@ -253,7 +334,7 @@ class CDCompleter(Completer):
|
|
|
253
334
|
suggestion = suggestion.rstrip(os.sep) + os.sep
|
|
254
335
|
yield Completion(
|
|
255
336
|
suggestion,
|
|
256
|
-
start_position
|
|
337
|
+
start_position=start_position,
|
|
257
338
|
display=d + os.sep,
|
|
258
339
|
display_meta="Directory",
|
|
259
340
|
)
|
|
@@ -262,6 +343,174 @@ class CDCompleter(Completer):
|
|
|
262
343
|
pass
|
|
263
344
|
|
|
264
345
|
|
|
346
|
+
class AgentCompleter(Completer):
|
|
347
|
+
"""
|
|
348
|
+
A completer that triggers on '/agent' to show available agents.
|
|
349
|
+
|
|
350
|
+
Usage: /agent <agent-name>
|
|
351
|
+
"""
|
|
352
|
+
|
|
353
|
+
def __init__(self, trigger: str = "/agent"):
|
|
354
|
+
self.trigger = trigger
|
|
355
|
+
|
|
356
|
+
def get_completions(self, document, complete_event):
|
|
357
|
+
cursor_position = document.cursor_position
|
|
358
|
+
text_before_cursor = document.text_before_cursor
|
|
359
|
+
stripped_text = text_before_cursor.lstrip()
|
|
360
|
+
|
|
361
|
+
# Require a space after /agent before showing completions
|
|
362
|
+
if not stripped_text.startswith(self.trigger + " "):
|
|
363
|
+
return
|
|
364
|
+
|
|
365
|
+
# Extract the input after /agent and space (up to cursor)
|
|
366
|
+
trigger_pos = text_before_cursor.find(self.trigger)
|
|
367
|
+
trigger_end = trigger_pos + len(self.trigger) + 1 # +1 for the space
|
|
368
|
+
text_after_trigger = text_before_cursor[trigger_end:cursor_position].lstrip()
|
|
369
|
+
start_position = -(len(text_after_trigger))
|
|
370
|
+
|
|
371
|
+
# Load all available agent names
|
|
372
|
+
try:
|
|
373
|
+
from code_puppy.command_line.pin_command_completion import load_agent_names
|
|
374
|
+
|
|
375
|
+
agent_names = load_agent_names()
|
|
376
|
+
except Exception:
|
|
377
|
+
# If agent loading fails, return no completions
|
|
378
|
+
return
|
|
379
|
+
|
|
380
|
+
# Filter and yield agent completions
|
|
381
|
+
try:
|
|
382
|
+
from code_puppy.command_line.pin_command_completion import (
|
|
383
|
+
_get_agent_display_meta,
|
|
384
|
+
)
|
|
385
|
+
except ImportError:
|
|
386
|
+
_get_agent_display_meta = lambda x: "default" # noqa: E731
|
|
387
|
+
|
|
388
|
+
for agent_name in agent_names:
|
|
389
|
+
if agent_name.lower().startswith(text_after_trigger.lower()):
|
|
390
|
+
yield Completion(
|
|
391
|
+
agent_name,
|
|
392
|
+
start_position=start_position,
|
|
393
|
+
display=agent_name,
|
|
394
|
+
display_meta=_get_agent_display_meta(agent_name),
|
|
395
|
+
)
|
|
396
|
+
|
|
397
|
+
|
|
398
|
+
class SlashCompleter(Completer):
|
|
399
|
+
"""
|
|
400
|
+
A completer that triggers on '/' at the beginning of the line
|
|
401
|
+
to show all available slash commands.
|
|
402
|
+
"""
|
|
403
|
+
|
|
404
|
+
def get_completions(self, document, complete_event):
|
|
405
|
+
text_before_cursor = document.text_before_cursor
|
|
406
|
+
stripped_text = text_before_cursor.lstrip()
|
|
407
|
+
|
|
408
|
+
# Only trigger if '/' is the first non-whitespace character
|
|
409
|
+
if not stripped_text.startswith("/"):
|
|
410
|
+
return
|
|
411
|
+
|
|
412
|
+
# Get the text after the initial slash
|
|
413
|
+
if len(stripped_text) == 1:
|
|
414
|
+
# User just typed '/', show all commands
|
|
415
|
+
partial = ""
|
|
416
|
+
start_position = 0 # Don't replace anything, just insert at cursor
|
|
417
|
+
else:
|
|
418
|
+
# User is typing a command after the slash
|
|
419
|
+
partial = stripped_text[1:] # text after '/'
|
|
420
|
+
start_position = -(len(partial)) # Replace what was typed after '/'
|
|
421
|
+
|
|
422
|
+
# Load all available commands
|
|
423
|
+
try:
|
|
424
|
+
commands = get_unique_commands()
|
|
425
|
+
except Exception:
|
|
426
|
+
# If command loading fails, return no completions
|
|
427
|
+
return
|
|
428
|
+
|
|
429
|
+
# Collect all primary commands and their aliases for proper alphabetical sorting
|
|
430
|
+
all_completions = []
|
|
431
|
+
|
|
432
|
+
# Convert partial to lowercase for case-insensitive matching
|
|
433
|
+
partial_lower = partial.lower()
|
|
434
|
+
|
|
435
|
+
for cmd in commands:
|
|
436
|
+
# Add primary command (case-insensitive matching)
|
|
437
|
+
if cmd.name.lower().startswith(partial_lower):
|
|
438
|
+
all_completions.append(
|
|
439
|
+
{
|
|
440
|
+
"text": cmd.name,
|
|
441
|
+
"display": f"/{cmd.name}",
|
|
442
|
+
"meta": cmd.description,
|
|
443
|
+
"sort_key": cmd.name.lower(), # Case-insensitive sort
|
|
444
|
+
}
|
|
445
|
+
)
|
|
446
|
+
|
|
447
|
+
# Add all aliases (case-insensitive matching)
|
|
448
|
+
for alias in cmd.aliases:
|
|
449
|
+
if alias.lower().startswith(partial_lower):
|
|
450
|
+
all_completions.append(
|
|
451
|
+
{
|
|
452
|
+
"text": alias,
|
|
453
|
+
"display": f"/{alias} (alias for /{cmd.name})",
|
|
454
|
+
"meta": cmd.description,
|
|
455
|
+
"sort_key": alias.lower(), # Sort by alias name, not primary command
|
|
456
|
+
}
|
|
457
|
+
)
|
|
458
|
+
|
|
459
|
+
# Also include custom commands from plugins (like claude-code-auth)
|
|
460
|
+
try:
|
|
461
|
+
from code_puppy import callbacks, plugins
|
|
462
|
+
|
|
463
|
+
# Ensure plugins are loaded so custom commands are registered
|
|
464
|
+
plugins.load_plugin_callbacks()
|
|
465
|
+
custom_help_results = callbacks.on_custom_command_help()
|
|
466
|
+
for res in custom_help_results:
|
|
467
|
+
if not res:
|
|
468
|
+
continue
|
|
469
|
+
# Format 1: List of tuples (command_name, description)
|
|
470
|
+
if isinstance(res, list):
|
|
471
|
+
for item in res:
|
|
472
|
+
if isinstance(item, tuple) and len(item) == 2:
|
|
473
|
+
cmd_name = str(item[0])
|
|
474
|
+
description = str(item[1])
|
|
475
|
+
if cmd_name.lower().startswith(partial_lower):
|
|
476
|
+
all_completions.append(
|
|
477
|
+
{
|
|
478
|
+
"text": cmd_name,
|
|
479
|
+
"display": f"/{cmd_name}",
|
|
480
|
+
"meta": description,
|
|
481
|
+
"sort_key": cmd_name.lower(),
|
|
482
|
+
}
|
|
483
|
+
)
|
|
484
|
+
# Format 2: Single tuple (command_name, description)
|
|
485
|
+
elif isinstance(res, tuple) and len(res) == 2:
|
|
486
|
+
cmd_name = str(res[0])
|
|
487
|
+
description = str(res[1])
|
|
488
|
+
if cmd_name.lower().startswith(partial_lower):
|
|
489
|
+
all_completions.append(
|
|
490
|
+
{
|
|
491
|
+
"text": cmd_name,
|
|
492
|
+
"display": f"/{cmd_name}",
|
|
493
|
+
"meta": description,
|
|
494
|
+
"sort_key": cmd_name.lower(),
|
|
495
|
+
}
|
|
496
|
+
)
|
|
497
|
+
except Exception:
|
|
498
|
+
# If custom command loading fails, continue with registered commands only
|
|
499
|
+
pass
|
|
500
|
+
|
|
501
|
+
# Sort all completions alphabetically
|
|
502
|
+
all_completions.sort(key=lambda x: x["sort_key"])
|
|
503
|
+
|
|
504
|
+
# Yield the sorted completions
|
|
505
|
+
for completion in all_completions:
|
|
506
|
+
yield Completion(
|
|
507
|
+
completion["text"],
|
|
508
|
+
start_position=start_position,
|
|
509
|
+
display=completion["display"],
|
|
510
|
+
display_meta=completion["meta"],
|
|
511
|
+
)
|
|
512
|
+
|
|
513
|
+
|
|
265
514
|
def get_prompt_with_active_model(base: str = ">>> "):
|
|
266
515
|
from code_puppy.agents.agent_manager import get_current_agent
|
|
267
516
|
|
|
@@ -310,14 +559,22 @@ def get_prompt_with_active_model(base: str = ">>> "):
|
|
|
310
559
|
async def get_input_with_combined_completion(
|
|
311
560
|
prompt_str=">>> ", history_file: Optional[str] = None
|
|
312
561
|
) -> str:
|
|
313
|
-
|
|
562
|
+
# Use SafeFileHistory to handle encoding errors gracefully on Windows
|
|
563
|
+
history = SafeFileHistory(history_file) if history_file else None
|
|
314
564
|
completer = merge_completers(
|
|
315
565
|
[
|
|
316
566
|
FilePathCompleter(symbol="@"),
|
|
317
567
|
ModelNameCompleter(trigger="/model"),
|
|
568
|
+
ModelNameCompleter(trigger="/m"),
|
|
318
569
|
CDCompleter(trigger="/cd"),
|
|
319
570
|
SetCompleter(trigger="/set"),
|
|
320
571
|
LoadContextCompleter(trigger="/load_context"),
|
|
572
|
+
PinCompleter(trigger="/pin_model"),
|
|
573
|
+
UnpinCompleter(trigger="/unpin"),
|
|
574
|
+
AgentCompleter(trigger="/agent"),
|
|
575
|
+
AgentCompleter(trigger="/a"),
|
|
576
|
+
MCPCompleter(trigger="/mcp"),
|
|
577
|
+
SlashCompleter(),
|
|
321
578
|
]
|
|
322
579
|
)
|
|
323
580
|
# Add custom key bindings and multiline toggle
|
|
@@ -326,20 +583,47 @@ async def get_input_with_combined_completion(
|
|
|
326
583
|
# Multiline mode state
|
|
327
584
|
multiline = {"enabled": False}
|
|
328
585
|
|
|
586
|
+
# Ctrl+X keybinding - exit with KeyboardInterrupt for shell command cancellation
|
|
587
|
+
@bindings.add(Keys.ControlX)
|
|
588
|
+
def _(event):
|
|
589
|
+
try:
|
|
590
|
+
event.app.exit(exception=KeyboardInterrupt)
|
|
591
|
+
except Exception:
|
|
592
|
+
# Ignore "Return value already set" errors when exit was already called
|
|
593
|
+
# This happens when user presses multiple exit keys in quick succession
|
|
594
|
+
pass
|
|
595
|
+
|
|
596
|
+
# Escape keybinding - exit with KeyboardInterrupt
|
|
597
|
+
@bindings.add(Keys.Escape)
|
|
598
|
+
def _(event):
|
|
599
|
+
try:
|
|
600
|
+
event.app.exit(exception=KeyboardInterrupt)
|
|
601
|
+
except Exception:
|
|
602
|
+
# Ignore "Return value already set" errors when exit was already called
|
|
603
|
+
pass
|
|
604
|
+
|
|
605
|
+
# NOTE: We intentionally do NOT override Ctrl+C here.
|
|
606
|
+
# prompt_toolkit's default Ctrl+C handler properly resets the terminal state on Windows.
|
|
607
|
+
# Overriding it with event.app.exit(exception=KeyboardInterrupt) can leave the terminal
|
|
608
|
+
# in a bad state where characters cannot be typed. Let prompt_toolkit handle Ctrl+C natively.
|
|
609
|
+
|
|
329
610
|
# Toggle multiline with Alt+M
|
|
330
611
|
@bindings.add(Keys.Escape, "m")
|
|
331
612
|
def _(event):
|
|
332
613
|
multiline["enabled"] = not multiline["enabled"]
|
|
333
614
|
status = "ON" if multiline["enabled"] else "OFF"
|
|
334
615
|
# Print status for user feedback (version-agnostic)
|
|
335
|
-
|
|
616
|
+
# Note: Using sys.stdout here for immediate feedback during input
|
|
617
|
+
sys.stdout.write(f"[multiline] {status}\n")
|
|
618
|
+
sys.stdout.flush()
|
|
336
619
|
|
|
337
620
|
# Also toggle multiline with F2 (more reliable across platforms)
|
|
338
621
|
@bindings.add("f2")
|
|
339
622
|
def _(event):
|
|
340
623
|
multiline["enabled"] = not multiline["enabled"]
|
|
341
624
|
status = "ON" if multiline["enabled"] else "OFF"
|
|
342
|
-
|
|
625
|
+
sys.stdout.write(f"[multiline] {status}\n")
|
|
626
|
+
sys.stdout.flush()
|
|
343
627
|
|
|
344
628
|
# Newline insert bindings — robust and explicit
|
|
345
629
|
# Ctrl+J (line feed) works in virtually all terminals; mark eager so it wins
|
|
@@ -364,10 +648,125 @@ async def get_input_with_combined_completion(
|
|
|
364
648
|
else:
|
|
365
649
|
event.current_buffer.validate_and_handle()
|
|
366
650
|
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
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()
|
|
371
770
|
|
|
372
771
|
session = PromptSession(
|
|
373
772
|
completer=completer,
|
|
@@ -385,19 +784,20 @@ async def get_input_with_combined_completion(
|
|
|
385
784
|
{
|
|
386
785
|
# Keys must AVOID the 'class:' prefix – that prefix is used only when
|
|
387
786
|
# tagging tokens in `FormattedText`. See prompt_toolkit docs.
|
|
388
|
-
"puppy": "bold
|
|
389
|
-
"owner": "bold
|
|
390
|
-
"agent": "bold
|
|
391
|
-
"model": "bold
|
|
392
|
-
"cwd": "bold
|
|
393
|
-
"arrow": "bold
|
|
394
|
-
"attachment-placeholder": "italic
|
|
787
|
+
"puppy": "bold ansibrightcyan",
|
|
788
|
+
"owner": "bold ansibrightblue",
|
|
789
|
+
"agent": "bold ansibrightblue",
|
|
790
|
+
"model": "bold ansibrightcyan",
|
|
791
|
+
"cwd": "bold ansibrightgreen",
|
|
792
|
+
"arrow": "bold ansibrightblue",
|
|
793
|
+
"attachment-placeholder": "italic ansicyan",
|
|
395
794
|
}
|
|
396
795
|
)
|
|
397
796
|
text = await session.prompt_async(prompt_str, style=style)
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
797
|
+
# NOTE: We used to call update_model_in_input(text) here to handle /model and /m
|
|
798
|
+
# commands at the prompt level, but that prevented the command handler from running
|
|
799
|
+
# and emitting success messages. Now we let all /model commands fall through to
|
|
800
|
+
# the command handler in main.py for consistent handling.
|
|
401
801
|
return text
|
|
402
802
|
|
|
403
803
|
|