code-puppy 0.0.214__py3-none-any.whl → 0.0.366__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- code_puppy/__init__.py +7 -1
- code_puppy/agents/__init__.py +2 -0
- code_puppy/agents/agent_c_reviewer.py +59 -6
- code_puppy/agents/agent_code_puppy.py +7 -1
- code_puppy/agents/agent_code_reviewer.py +12 -2
- code_puppy/agents/agent_cpp_reviewer.py +73 -6
- code_puppy/agents/agent_creator_agent.py +45 -4
- code_puppy/agents/agent_golang_reviewer.py +92 -3
- code_puppy/agents/agent_javascript_reviewer.py +101 -8
- code_puppy/agents/agent_manager.py +81 -4
- code_puppy/agents/agent_pack_leader.py +383 -0
- code_puppy/agents/agent_planning.py +163 -0
- code_puppy/agents/agent_python_programmer.py +165 -0
- code_puppy/agents/agent_python_reviewer.py +28 -6
- code_puppy/agents/agent_qa_expert.py +98 -6
- code_puppy/agents/agent_qa_kitten.py +12 -7
- code_puppy/agents/agent_security_auditor.py +113 -3
- code_puppy/agents/agent_terminal_qa.py +323 -0
- code_puppy/agents/agent_typescript_reviewer.py +106 -7
- code_puppy/agents/base_agent.py +802 -176
- code_puppy/agents/event_stream_handler.py +350 -0
- code_puppy/agents/pack/__init__.py +34 -0
- code_puppy/agents/pack/bloodhound.py +304 -0
- code_puppy/agents/pack/husky.py +321 -0
- code_puppy/agents/pack/retriever.py +393 -0
- code_puppy/agents/pack/shepherd.py +348 -0
- code_puppy/agents/pack/terrier.py +287 -0
- code_puppy/agents/pack/watchdog.py +367 -0
- code_puppy/agents/prompt_reviewer.py +145 -0
- code_puppy/agents/subagent_stream_handler.py +276 -0
- code_puppy/api/__init__.py +13 -0
- code_puppy/api/app.py +169 -0
- code_puppy/api/main.py +21 -0
- code_puppy/api/pty_manager.py +446 -0
- code_puppy/api/routers/__init__.py +12 -0
- code_puppy/api/routers/agents.py +36 -0
- code_puppy/api/routers/commands.py +217 -0
- code_puppy/api/routers/config.py +74 -0
- code_puppy/api/routers/sessions.py +232 -0
- code_puppy/api/templates/terminal.html +361 -0
- code_puppy/api/websocket.py +154 -0
- code_puppy/callbacks.py +142 -4
- code_puppy/chatgpt_codex_client.py +283 -0
- code_puppy/claude_cache_client.py +586 -0
- code_puppy/cli_runner.py +916 -0
- code_puppy/command_line/add_model_menu.py +1079 -0
- code_puppy/command_line/agent_menu.py +395 -0
- code_puppy/command_line/attachments.py +10 -5
- code_puppy/command_line/autosave_menu.py +605 -0
- code_puppy/command_line/clipboard.py +527 -0
- code_puppy/command_line/colors_menu.py +520 -0
- code_puppy/command_line/command_handler.py +176 -738
- code_puppy/command_line/command_registry.py +150 -0
- code_puppy/command_line/config_commands.py +715 -0
- code_puppy/command_line/core_commands.py +792 -0
- code_puppy/command_line/diff_menu.py +863 -0
- code_puppy/command_line/load_context_completion.py +15 -22
- code_puppy/command_line/mcp/base.py +0 -3
- code_puppy/command_line/mcp/catalog_server_installer.py +175 -0
- code_puppy/command_line/mcp/custom_server_form.py +688 -0
- code_puppy/command_line/mcp/custom_server_installer.py +195 -0
- code_puppy/command_line/mcp/edit_command.py +148 -0
- code_puppy/command_line/mcp/handler.py +9 -4
- code_puppy/command_line/mcp/help_command.py +6 -5
- code_puppy/command_line/mcp/install_command.py +15 -26
- code_puppy/command_line/mcp/install_menu.py +685 -0
- code_puppy/command_line/mcp/list_command.py +2 -2
- code_puppy/command_line/mcp/logs_command.py +174 -65
- code_puppy/command_line/mcp/remove_command.py +2 -2
- code_puppy/command_line/mcp/restart_command.py +12 -4
- code_puppy/command_line/mcp/search_command.py +16 -10
- code_puppy/command_line/mcp/start_all_command.py +18 -6
- code_puppy/command_line/mcp/start_command.py +47 -25
- code_puppy/command_line/mcp/status_command.py +4 -5
- code_puppy/command_line/mcp/stop_all_command.py +7 -1
- code_puppy/command_line/mcp/stop_command.py +8 -4
- code_puppy/command_line/mcp/test_command.py +2 -2
- code_puppy/command_line/mcp/wizard_utils.py +20 -16
- code_puppy/command_line/mcp_completion.py +174 -0
- code_puppy/command_line/model_picker_completion.py +75 -25
- code_puppy/command_line/model_settings_menu.py +884 -0
- code_puppy/command_line/motd.py +14 -8
- code_puppy/command_line/onboarding_slides.py +179 -0
- code_puppy/command_line/onboarding_wizard.py +340 -0
- code_puppy/command_line/pin_command_completion.py +329 -0
- code_puppy/command_line/prompt_toolkit_completion.py +463 -63
- code_puppy/command_line/session_commands.py +296 -0
- code_puppy/command_line/utils.py +54 -0
- code_puppy/config.py +898 -112
- code_puppy/error_logging.py +118 -0
- code_puppy/gemini_code_assist.py +385 -0
- code_puppy/gemini_model.py +602 -0
- code_puppy/http_utils.py +210 -148
- code_puppy/keymap.py +128 -0
- code_puppy/main.py +5 -698
- code_puppy/mcp_/__init__.py +17 -0
- code_puppy/mcp_/async_lifecycle.py +35 -4
- code_puppy/mcp_/blocking_startup.py +70 -43
- code_puppy/mcp_/captured_stdio_server.py +2 -2
- code_puppy/mcp_/config_wizard.py +4 -4
- code_puppy/mcp_/dashboard.py +15 -6
- code_puppy/mcp_/managed_server.py +65 -38
- code_puppy/mcp_/manager.py +146 -52
- code_puppy/mcp_/mcp_logs.py +224 -0
- code_puppy/mcp_/registry.py +6 -6
- code_puppy/mcp_/server_registry_catalog.py +24 -5
- code_puppy/messaging/__init__.py +199 -2
- code_puppy/messaging/bus.py +610 -0
- code_puppy/messaging/commands.py +167 -0
- code_puppy/messaging/markdown_patches.py +57 -0
- code_puppy/messaging/message_queue.py +17 -48
- code_puppy/messaging/messages.py +500 -0
- code_puppy/messaging/queue_console.py +1 -24
- code_puppy/messaging/renderers.py +43 -146
- code_puppy/messaging/rich_renderer.py +1027 -0
- code_puppy/messaging/spinner/__init__.py +21 -5
- code_puppy/messaging/spinner/console_spinner.py +86 -51
- code_puppy/messaging/subagent_console.py +461 -0
- code_puppy/model_factory.py +634 -83
- code_puppy/model_utils.py +167 -0
- code_puppy/models.json +66 -68
- code_puppy/models_dev_api.json +1 -0
- code_puppy/models_dev_parser.py +592 -0
- code_puppy/plugins/__init__.py +164 -10
- code_puppy/plugins/antigravity_oauth/__init__.py +10 -0
- code_puppy/plugins/antigravity_oauth/accounts.py +406 -0
- code_puppy/plugins/antigravity_oauth/antigravity_model.py +704 -0
- code_puppy/plugins/antigravity_oauth/config.py +42 -0
- code_puppy/plugins/antigravity_oauth/constants.py +136 -0
- code_puppy/plugins/antigravity_oauth/oauth.py +478 -0
- code_puppy/plugins/antigravity_oauth/register_callbacks.py +406 -0
- code_puppy/plugins/antigravity_oauth/storage.py +271 -0
- code_puppy/plugins/antigravity_oauth/test_plugin.py +319 -0
- code_puppy/plugins/antigravity_oauth/token.py +167 -0
- code_puppy/plugins/antigravity_oauth/transport.py +767 -0
- code_puppy/plugins/antigravity_oauth/utils.py +169 -0
- code_puppy/plugins/chatgpt_oauth/__init__.py +8 -0
- code_puppy/plugins/chatgpt_oauth/config.py +52 -0
- code_puppy/plugins/chatgpt_oauth/oauth_flow.py +328 -0
- code_puppy/plugins/chatgpt_oauth/register_callbacks.py +94 -0
- code_puppy/plugins/chatgpt_oauth/test_plugin.py +293 -0
- code_puppy/plugins/chatgpt_oauth/utils.py +489 -0
- code_puppy/plugins/claude_code_oauth/README.md +167 -0
- code_puppy/plugins/claude_code_oauth/SETUP.md +93 -0
- code_puppy/plugins/claude_code_oauth/__init__.py +6 -0
- code_puppy/plugins/claude_code_oauth/config.py +50 -0
- code_puppy/plugins/claude_code_oauth/register_callbacks.py +308 -0
- code_puppy/plugins/claude_code_oauth/test_plugin.py +283 -0
- code_puppy/plugins/claude_code_oauth/utils.py +518 -0
- code_puppy/plugins/customizable_commands/__init__.py +0 -0
- code_puppy/plugins/customizable_commands/register_callbacks.py +169 -0
- code_puppy/plugins/example_custom_command/README.md +280 -0
- code_puppy/plugins/example_custom_command/register_callbacks.py +2 -2
- code_puppy/plugins/file_permission_handler/__init__.py +4 -0
- code_puppy/plugins/file_permission_handler/register_callbacks.py +523 -0
- code_puppy/plugins/frontend_emitter/__init__.py +25 -0
- code_puppy/plugins/frontend_emitter/emitter.py +121 -0
- code_puppy/plugins/frontend_emitter/register_callbacks.py +261 -0
- code_puppy/plugins/oauth_puppy_html.py +228 -0
- code_puppy/plugins/shell_safety/__init__.py +6 -0
- code_puppy/plugins/shell_safety/agent_shell_safety.py +69 -0
- code_puppy/plugins/shell_safety/command_cache.py +156 -0
- code_puppy/plugins/shell_safety/register_callbacks.py +202 -0
- code_puppy/prompts/antigravity_system_prompt.md +1 -0
- code_puppy/prompts/codex_system_prompt.md +310 -0
- code_puppy/pydantic_patches.py +131 -0
- code_puppy/reopenable_async_client.py +8 -8
- code_puppy/round_robin_model.py +9 -12
- code_puppy/session_storage.py +2 -1
- code_puppy/status_display.py +21 -4
- code_puppy/summarization_agent.py +41 -13
- code_puppy/terminal_utils.py +418 -0
- code_puppy/tools/__init__.py +37 -1
- code_puppy/tools/agent_tools.py +536 -52
- code_puppy/tools/browser/__init__.py +37 -0
- code_puppy/tools/browser/browser_control.py +19 -23
- code_puppy/tools/browser/browser_interactions.py +41 -48
- code_puppy/tools/browser/browser_locators.py +36 -38
- code_puppy/tools/browser/browser_manager.py +316 -0
- code_puppy/tools/browser/browser_navigation.py +16 -16
- code_puppy/tools/browser/browser_screenshot.py +79 -143
- code_puppy/tools/browser/browser_scripts.py +32 -42
- code_puppy/tools/browser/browser_workflows.py +44 -27
- code_puppy/tools/browser/chromium_terminal_manager.py +259 -0
- code_puppy/tools/browser/terminal_command_tools.py +521 -0
- code_puppy/tools/browser/terminal_screenshot_tools.py +556 -0
- code_puppy/tools/browser/terminal_tools.py +525 -0
- code_puppy/tools/command_runner.py +930 -147
- code_puppy/tools/common.py +1113 -5
- code_puppy/tools/display.py +84 -0
- code_puppy/tools/file_modifications.py +288 -89
- code_puppy/tools/file_operations.py +226 -154
- code_puppy/tools/subagent_context.py +158 -0
- code_puppy/uvx_detection.py +242 -0
- code_puppy/version_checker.py +30 -11
- code_puppy-0.0.366.data/data/code_puppy/models.json +110 -0
- code_puppy-0.0.366.data/data/code_puppy/models_dev_api.json +1 -0
- {code_puppy-0.0.214.dist-info → code_puppy-0.0.366.dist-info}/METADATA +149 -75
- code_puppy-0.0.366.dist-info/RECORD +217 -0
- {code_puppy-0.0.214.dist-info → code_puppy-0.0.366.dist-info}/WHEEL +1 -1
- code_puppy/command_line/mcp/add_command.py +0 -183
- code_puppy/messaging/spinner/textual_spinner.py +0 -106
- code_puppy/tools/browser/camoufox_manager.py +0 -216
- code_puppy/tools/browser/vqa_agent.py +0 -70
- code_puppy/tui/__init__.py +0 -10
- code_puppy/tui/app.py +0 -1105
- code_puppy/tui/components/__init__.py +0 -21
- code_puppy/tui/components/chat_view.py +0 -551
- code_puppy/tui/components/command_history_modal.py +0 -218
- code_puppy/tui/components/copy_button.py +0 -139
- code_puppy/tui/components/custom_widgets.py +0 -63
- code_puppy/tui/components/human_input_modal.py +0 -175
- code_puppy/tui/components/input_area.py +0 -167
- code_puppy/tui/components/sidebar.py +0 -309
- code_puppy/tui/components/status_bar.py +0 -185
- code_puppy/tui/messages.py +0 -27
- code_puppy/tui/models/__init__.py +0 -8
- code_puppy/tui/models/chat_message.py +0 -25
- code_puppy/tui/models/command_history.py +0 -89
- code_puppy/tui/models/enums.py +0 -24
- code_puppy/tui/screens/__init__.py +0 -17
- code_puppy/tui/screens/autosave_picker.py +0 -175
- code_puppy/tui/screens/help.py +0 -130
- code_puppy/tui/screens/mcp_install_wizard.py +0 -803
- code_puppy/tui/screens/settings.py +0 -306
- code_puppy/tui/screens/tools.py +0 -74
- code_puppy/tui_state.py +0 -55
- code_puppy-0.0.214.data/data/code_puppy/models.json +0 -112
- code_puppy-0.0.214.dist-info/RECORD +0 -131
- {code_puppy-0.0.214.dist-info → code_puppy-0.0.366.dist-info}/entry_points.txt +0 -0
- {code_puppy-0.0.214.dist-info → code_puppy-0.0.366.dist-info}/licenses/LICENSE +0 -0
code_puppy/tui/app.py
DELETED
|
@@ -1,1105 +0,0 @@
|
|
|
1
|
-
"""
|
|
2
|
-
Main TUI application class.
|
|
3
|
-
"""
|
|
4
|
-
|
|
5
|
-
from datetime import datetime, timezone
|
|
6
|
-
|
|
7
|
-
from textual import on
|
|
8
|
-
from textual.app import App, ComposeResult
|
|
9
|
-
from textual.binding import Binding
|
|
10
|
-
from textual.containers import Container
|
|
11
|
-
from textual.events import Resize
|
|
12
|
-
from textual.reactive import reactive
|
|
13
|
-
from textual.widgets import Footer, ListView
|
|
14
|
-
|
|
15
|
-
# message_history_accumulator and prune_interrupted_tool_calls have been moved to BaseAgent class
|
|
16
|
-
from code_puppy.agents.agent_manager import get_current_agent
|
|
17
|
-
from code_puppy.command_line.command_handler import handle_command
|
|
18
|
-
from code_puppy.config import (
|
|
19
|
-
get_global_model_name,
|
|
20
|
-
get_puppy_name,
|
|
21
|
-
initialize_command_history_file,
|
|
22
|
-
save_command_to_history,
|
|
23
|
-
)
|
|
24
|
-
|
|
25
|
-
# Import our message queue system
|
|
26
|
-
from code_puppy.messaging import TUIRenderer, get_global_queue
|
|
27
|
-
from code_puppy.tui.components import (
|
|
28
|
-
ChatView,
|
|
29
|
-
CustomTextArea,
|
|
30
|
-
InputArea,
|
|
31
|
-
Sidebar,
|
|
32
|
-
StatusBar,
|
|
33
|
-
)
|
|
34
|
-
|
|
35
|
-
# Import shared message classes
|
|
36
|
-
from .messages import CommandSelected, HistoryEntrySelected
|
|
37
|
-
from .models import ChatMessage, MessageType
|
|
38
|
-
from .screens import HelpScreen, MCPInstallWizardScreen, SettingsScreen, ToolsScreen
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
class CodePuppyTUI(App):
|
|
42
|
-
"""Main Code Puppy TUI application."""
|
|
43
|
-
|
|
44
|
-
TITLE = "Code Puppy - AI Code Assistant"
|
|
45
|
-
SUB_TITLE = "TUI Mode"
|
|
46
|
-
|
|
47
|
-
CSS = """
|
|
48
|
-
Screen {
|
|
49
|
-
layout: horizontal;
|
|
50
|
-
}
|
|
51
|
-
|
|
52
|
-
#main-area {
|
|
53
|
-
layout: vertical;
|
|
54
|
-
width: 1fr;
|
|
55
|
-
min-width: 40;
|
|
56
|
-
}
|
|
57
|
-
|
|
58
|
-
#chat-container {
|
|
59
|
-
height: 1fr;
|
|
60
|
-
min-height: 10;
|
|
61
|
-
}
|
|
62
|
-
"""
|
|
63
|
-
|
|
64
|
-
BINDINGS = [
|
|
65
|
-
Binding("ctrl+q", "quit", "Quit"),
|
|
66
|
-
Binding("ctrl+c", "quit", "Quit"),
|
|
67
|
-
Binding("ctrl+l", "clear_chat", "Clear Chat"),
|
|
68
|
-
Binding("ctrl+1", "show_help", "Help"),
|
|
69
|
-
Binding("ctrl+2", "toggle_sidebar", "History"),
|
|
70
|
-
Binding("ctrl+3", "open_settings", "Settings"),
|
|
71
|
-
Binding("ctrl+4", "show_tools", "Tools"),
|
|
72
|
-
Binding("ctrl+5", "focus_input", "Focus Prompt"),
|
|
73
|
-
Binding("ctrl+6", "focus_chat", "Focus Response"),
|
|
74
|
-
Binding("ctrl+t", "open_mcp_wizard", "MCP Install Wizard"),
|
|
75
|
-
]
|
|
76
|
-
|
|
77
|
-
# Reactive variables for app state
|
|
78
|
-
current_model = reactive("")
|
|
79
|
-
puppy_name = reactive("")
|
|
80
|
-
current_agent = reactive("")
|
|
81
|
-
agent_busy = reactive(False)
|
|
82
|
-
|
|
83
|
-
def watch_agent_busy(self) -> None:
|
|
84
|
-
"""Watch for changes to agent_busy state."""
|
|
85
|
-
# Update the submit/cancel button state when agent_busy changes
|
|
86
|
-
self._update_submit_cancel_button(self.agent_busy)
|
|
87
|
-
|
|
88
|
-
def watch_current_agent(self) -> None:
|
|
89
|
-
"""Watch for changes to current_agent and update title."""
|
|
90
|
-
self._update_title()
|
|
91
|
-
|
|
92
|
-
def _update_title(self) -> None:
|
|
93
|
-
"""Update the application title to include current agent."""
|
|
94
|
-
if self.current_agent:
|
|
95
|
-
self.title = f"Code Puppy - {self.current_agent}"
|
|
96
|
-
self.sub_title = "TUI Mode"
|
|
97
|
-
else:
|
|
98
|
-
self.title = "Code Puppy - AI Code Assistant"
|
|
99
|
-
self.sub_title = "TUI Mode"
|
|
100
|
-
|
|
101
|
-
def _on_agent_reload(self, agent_id: str, agent_name: str) -> None:
|
|
102
|
-
"""Callback for when agent is reloaded/changed."""
|
|
103
|
-
# Get the updated agent configuration
|
|
104
|
-
from code_puppy.agents.agent_manager import get_current_agent
|
|
105
|
-
|
|
106
|
-
current_agent_config = get_current_agent()
|
|
107
|
-
new_agent_display = (
|
|
108
|
-
current_agent_config.display_name if current_agent_config else "code-puppy"
|
|
109
|
-
)
|
|
110
|
-
|
|
111
|
-
# Update the reactive variable (this will trigger watch_current_agent)
|
|
112
|
-
self.current_agent = new_agent_display
|
|
113
|
-
|
|
114
|
-
# Add a system message to notify the user
|
|
115
|
-
self.add_system_message(f"🔄 Switched to agent: {new_agent_display}")
|
|
116
|
-
|
|
117
|
-
def __init__(self, initial_command: str = None, **kwargs):
|
|
118
|
-
super().__init__(**kwargs)
|
|
119
|
-
self._current_worker = None
|
|
120
|
-
self.initial_command = initial_command
|
|
121
|
-
|
|
122
|
-
# Initialize message queue renderer
|
|
123
|
-
self.message_queue = get_global_queue()
|
|
124
|
-
self.message_renderer = TUIRenderer(self.message_queue, self)
|
|
125
|
-
self._renderer_started = False
|
|
126
|
-
|
|
127
|
-
def compose(self) -> ComposeResult:
|
|
128
|
-
"""Create the UI layout."""
|
|
129
|
-
yield StatusBar()
|
|
130
|
-
yield Sidebar()
|
|
131
|
-
with Container(id="main-area"):
|
|
132
|
-
with Container(id="chat-container"):
|
|
133
|
-
yield ChatView(id="chat-view")
|
|
134
|
-
yield InputArea()
|
|
135
|
-
yield Footer()
|
|
136
|
-
|
|
137
|
-
def on_mount(self) -> None:
|
|
138
|
-
"""Initialize the application when mounted."""
|
|
139
|
-
# Register this app instance for global access
|
|
140
|
-
from code_puppy.tui_state import set_tui_app_instance
|
|
141
|
-
|
|
142
|
-
set_tui_app_instance(self)
|
|
143
|
-
|
|
144
|
-
# Register callback for agent reload events
|
|
145
|
-
from code_puppy.callbacks import register_callback
|
|
146
|
-
|
|
147
|
-
register_callback("agent_reload", self._on_agent_reload)
|
|
148
|
-
|
|
149
|
-
# Load configuration
|
|
150
|
-
self.current_model = get_global_model_name()
|
|
151
|
-
self.puppy_name = get_puppy_name()
|
|
152
|
-
|
|
153
|
-
# Get current agent information
|
|
154
|
-
from code_puppy.agents.agent_manager import get_current_agent
|
|
155
|
-
|
|
156
|
-
current_agent_config = get_current_agent()
|
|
157
|
-
self.current_agent = (
|
|
158
|
-
current_agent_config.display_name if current_agent_config else "code-puppy"
|
|
159
|
-
)
|
|
160
|
-
|
|
161
|
-
# Initial title update
|
|
162
|
-
self._update_title()
|
|
163
|
-
|
|
164
|
-
# Use runtime manager to ensure we always have the current agent
|
|
165
|
-
# Update status bar
|
|
166
|
-
status_bar = self.query_one(StatusBar)
|
|
167
|
-
status_bar.current_model = self.current_model
|
|
168
|
-
status_bar.puppy_name = self.puppy_name
|
|
169
|
-
status_bar.agent_status = "Ready"
|
|
170
|
-
|
|
171
|
-
# Add welcome message with YOLO mode notification
|
|
172
|
-
self.add_system_message(
|
|
173
|
-
"Welcome to Code Puppy 🐶!\n💨 YOLO mode is enabled in TUI: commands will execute without confirmation."
|
|
174
|
-
)
|
|
175
|
-
|
|
176
|
-
# Start the message renderer EARLY to catch startup messages
|
|
177
|
-
# Using call_after_refresh to start it as soon as possible after mount
|
|
178
|
-
self.call_after_refresh(self.start_message_renderer_sync)
|
|
179
|
-
|
|
180
|
-
# Kick off a non-blocking preload of the agent/model so the
|
|
181
|
-
# status bar shows loading before first prompt
|
|
182
|
-
self.call_after_refresh(self.preload_agent_on_startup)
|
|
183
|
-
|
|
184
|
-
# After preload, offer to restore an autosave session (like interactive mode)
|
|
185
|
-
self.call_after_refresh(self.maybe_prompt_restore_autosave)
|
|
186
|
-
|
|
187
|
-
# Apply responsive design adjustments
|
|
188
|
-
self.apply_responsive_layout()
|
|
189
|
-
|
|
190
|
-
# Auto-focus the input field so user can start typing immediately
|
|
191
|
-
self.call_after_refresh(self.focus_input_field)
|
|
192
|
-
|
|
193
|
-
# Process initial command if provided
|
|
194
|
-
if self.initial_command:
|
|
195
|
-
self.call_after_refresh(self.process_initial_command)
|
|
196
|
-
|
|
197
|
-
def _tighten_text(self, text: str) -> str:
|
|
198
|
-
"""Aggressively tighten whitespace: trim lines, collapse multiples, drop extra blanks."""
|
|
199
|
-
try:
|
|
200
|
-
import re
|
|
201
|
-
|
|
202
|
-
# Split into lines, strip each, drop empty runs
|
|
203
|
-
lines = [re.sub(r"\s+", " ", ln.strip()) for ln in text.splitlines()]
|
|
204
|
-
# Remove consecutive blank lines
|
|
205
|
-
tight_lines = []
|
|
206
|
-
last_blank = False
|
|
207
|
-
for ln in lines:
|
|
208
|
-
is_blank = ln == ""
|
|
209
|
-
if is_blank and last_blank:
|
|
210
|
-
continue
|
|
211
|
-
tight_lines.append(ln)
|
|
212
|
-
last_blank = is_blank
|
|
213
|
-
return "\n".join(tight_lines).strip()
|
|
214
|
-
except Exception:
|
|
215
|
-
return text.strip()
|
|
216
|
-
|
|
217
|
-
def add_system_message(
|
|
218
|
-
self, content: str, message_group: str = None, group_id: str = None
|
|
219
|
-
) -> None:
|
|
220
|
-
"""Add a system message to the chat."""
|
|
221
|
-
# Support both parameter names for backward compatibility
|
|
222
|
-
final_group_id = message_group or group_id
|
|
223
|
-
# Tighten only plain strings
|
|
224
|
-
content_to_use = (
|
|
225
|
-
self._tighten_text(content) if isinstance(content, str) else content
|
|
226
|
-
)
|
|
227
|
-
message = ChatMessage(
|
|
228
|
-
id=f"sys_{datetime.now(timezone.utc).timestamp()}",
|
|
229
|
-
type=MessageType.SYSTEM,
|
|
230
|
-
content=content_to_use,
|
|
231
|
-
timestamp=datetime.now(timezone.utc),
|
|
232
|
-
group_id=final_group_id,
|
|
233
|
-
)
|
|
234
|
-
chat_view = self.query_one("#chat-view", ChatView)
|
|
235
|
-
chat_view.add_message(message)
|
|
236
|
-
|
|
237
|
-
def add_system_message_rich(
|
|
238
|
-
self, rich_content, message_group: str = None, group_id: str = None
|
|
239
|
-
) -> None:
|
|
240
|
-
"""Add a system message with Rich content (like Markdown) to the chat."""
|
|
241
|
-
# Support both parameter names for backward compatibility
|
|
242
|
-
final_group_id = message_group or group_id
|
|
243
|
-
message = ChatMessage(
|
|
244
|
-
id=f"sys_rich_{datetime.now(timezone.utc).timestamp()}",
|
|
245
|
-
type=MessageType.SYSTEM,
|
|
246
|
-
content=rich_content, # Store the Rich object directly
|
|
247
|
-
timestamp=datetime.now(timezone.utc),
|
|
248
|
-
group_id=final_group_id,
|
|
249
|
-
)
|
|
250
|
-
chat_view = self.query_one("#chat-view", ChatView)
|
|
251
|
-
chat_view.add_message(message)
|
|
252
|
-
|
|
253
|
-
def add_user_message(self, content: str, message_group: str = None) -> None:
|
|
254
|
-
"""Add a user message to the chat."""
|
|
255
|
-
message = ChatMessage(
|
|
256
|
-
id=f"user_{datetime.now(timezone.utc).timestamp()}",
|
|
257
|
-
type=MessageType.USER,
|
|
258
|
-
content=content,
|
|
259
|
-
timestamp=datetime.now(timezone.utc),
|
|
260
|
-
group_id=message_group,
|
|
261
|
-
)
|
|
262
|
-
chat_view = self.query_one("#chat-view", ChatView)
|
|
263
|
-
chat_view.add_message(message)
|
|
264
|
-
|
|
265
|
-
def add_agent_message(self, content: str, message_group: str = None) -> None:
|
|
266
|
-
"""Add an agent message to the chat."""
|
|
267
|
-
message = ChatMessage(
|
|
268
|
-
id=f"agent_{datetime.now(timezone.utc).timestamp()}",
|
|
269
|
-
type=MessageType.AGENT_RESPONSE,
|
|
270
|
-
content=content,
|
|
271
|
-
timestamp=datetime.now(timezone.utc),
|
|
272
|
-
group_id=message_group,
|
|
273
|
-
)
|
|
274
|
-
chat_view = self.query_one("#chat-view", ChatView)
|
|
275
|
-
chat_view.add_message(message)
|
|
276
|
-
|
|
277
|
-
def add_error_message(self, content: str, message_group: str = None) -> None:
|
|
278
|
-
"""Add an error message to the chat."""
|
|
279
|
-
content_to_use = (
|
|
280
|
-
self._tighten_text(content) if isinstance(content, str) else content
|
|
281
|
-
)
|
|
282
|
-
message = ChatMessage(
|
|
283
|
-
id=f"error_{datetime.now(timezone.utc).timestamp()}",
|
|
284
|
-
type=MessageType.ERROR,
|
|
285
|
-
content=content_to_use,
|
|
286
|
-
timestamp=datetime.now(timezone.utc),
|
|
287
|
-
group_id=message_group,
|
|
288
|
-
)
|
|
289
|
-
chat_view = self.query_one("#chat-view", ChatView)
|
|
290
|
-
chat_view.add_message(message)
|
|
291
|
-
|
|
292
|
-
def add_agent_reasoning_message(
|
|
293
|
-
self, content: str, message_group: str = None
|
|
294
|
-
) -> None:
|
|
295
|
-
"""Add an agent reasoning message to the chat."""
|
|
296
|
-
message = ChatMessage(
|
|
297
|
-
id=f"agent_reasoning_{datetime.now(timezone.utc).timestamp()}",
|
|
298
|
-
type=MessageType.AGENT_REASONING,
|
|
299
|
-
content=content,
|
|
300
|
-
timestamp=datetime.now(timezone.utc),
|
|
301
|
-
group_id=message_group,
|
|
302
|
-
)
|
|
303
|
-
chat_view = self.query_one("#chat-view", ChatView)
|
|
304
|
-
chat_view.add_message(message)
|
|
305
|
-
|
|
306
|
-
def add_planned_next_steps_message(
|
|
307
|
-
self, content: str, message_group: str = None
|
|
308
|
-
) -> None:
|
|
309
|
-
"""Add an planned next steps to the chat."""
|
|
310
|
-
message = ChatMessage(
|
|
311
|
-
id=f"planned_next_steps_{datetime.now(timezone.utc).timestamp()}",
|
|
312
|
-
type=MessageType.PLANNED_NEXT_STEPS,
|
|
313
|
-
content=content,
|
|
314
|
-
timestamp=datetime.now(timezone.utc),
|
|
315
|
-
group_id=message_group,
|
|
316
|
-
)
|
|
317
|
-
chat_view = self.query_one("#chat-view", ChatView)
|
|
318
|
-
chat_view.add_message(message)
|
|
319
|
-
|
|
320
|
-
def on_custom_text_area_message_sent(
|
|
321
|
-
self, event: CustomTextArea.MessageSent
|
|
322
|
-
) -> None:
|
|
323
|
-
"""Handle message sent from custom text area."""
|
|
324
|
-
self.action_send_message()
|
|
325
|
-
|
|
326
|
-
def on_input_area_submit_requested(self, event) -> None:
|
|
327
|
-
"""Handle submit button clicked."""
|
|
328
|
-
self.action_send_message()
|
|
329
|
-
|
|
330
|
-
def on_input_area_cancel_requested(self, event) -> None:
|
|
331
|
-
"""Handle cancel button clicked."""
|
|
332
|
-
self.action_cancel_processing()
|
|
333
|
-
|
|
334
|
-
async def on_key(self, event) -> None:
|
|
335
|
-
"""Handle app-level key events."""
|
|
336
|
-
input_field = self.query_one("#input-field", CustomTextArea)
|
|
337
|
-
|
|
338
|
-
# Only handle keys when input field is focused
|
|
339
|
-
if input_field.has_focus:
|
|
340
|
-
# Handle Ctrl+Enter or Shift+Enter for a new line
|
|
341
|
-
if event.key in ("ctrl+enter", "shift+enter"):
|
|
342
|
-
input_field.insert("\n")
|
|
343
|
-
event.prevent_default()
|
|
344
|
-
return
|
|
345
|
-
|
|
346
|
-
# Check if a modal is currently active - if so, let the modal handle keys
|
|
347
|
-
if hasattr(self, "_active_screen") and self._active_screen:
|
|
348
|
-
# Don't handle keys at the app level when a modal is active
|
|
349
|
-
return
|
|
350
|
-
|
|
351
|
-
# Handle arrow keys for sidebar navigation when sidebar is visible
|
|
352
|
-
if not input_field.has_focus:
|
|
353
|
-
try:
|
|
354
|
-
sidebar = self.query_one(Sidebar)
|
|
355
|
-
if sidebar.display:
|
|
356
|
-
# Handle navigation for the currently active tab
|
|
357
|
-
tabs = self.query_one("#sidebar-tabs")
|
|
358
|
-
active_tab = tabs.active
|
|
359
|
-
|
|
360
|
-
if active_tab == "history-tab":
|
|
361
|
-
history_list = self.query_one("#history-list", ListView)
|
|
362
|
-
if event.key == "enter":
|
|
363
|
-
if history_list.highlighted_child and hasattr(
|
|
364
|
-
history_list.highlighted_child, "command_entry"
|
|
365
|
-
):
|
|
366
|
-
# Show command history modal
|
|
367
|
-
from .components.command_history_modal import (
|
|
368
|
-
CommandHistoryModal,
|
|
369
|
-
)
|
|
370
|
-
|
|
371
|
-
# Make sure sidebar's current_history_index is synced with the ListView
|
|
372
|
-
sidebar.current_history_index = history_list.index
|
|
373
|
-
|
|
374
|
-
# Push the modal screen
|
|
375
|
-
# The modal will get the command entries from the sidebar
|
|
376
|
-
self.push_screen(CommandHistoryModal())
|
|
377
|
-
event.prevent_default()
|
|
378
|
-
return
|
|
379
|
-
except Exception:
|
|
380
|
-
pass
|
|
381
|
-
|
|
382
|
-
def refresh_history_display(self) -> None:
|
|
383
|
-
"""Refresh the history display with the command history file."""
|
|
384
|
-
try:
|
|
385
|
-
sidebar = self.query_one(Sidebar)
|
|
386
|
-
sidebar.load_command_history()
|
|
387
|
-
except Exception:
|
|
388
|
-
pass # Silently fail if history list not available
|
|
389
|
-
|
|
390
|
-
def action_send_message(self) -> None:
|
|
391
|
-
"""Send the current message."""
|
|
392
|
-
input_field = self.query_one("#input-field", CustomTextArea)
|
|
393
|
-
message = input_field.text.strip()
|
|
394
|
-
|
|
395
|
-
if message:
|
|
396
|
-
# Clear input
|
|
397
|
-
input_field.text = ""
|
|
398
|
-
|
|
399
|
-
# Add user message to chat
|
|
400
|
-
self.add_user_message(message)
|
|
401
|
-
|
|
402
|
-
# Save command to history file with timestamp
|
|
403
|
-
try:
|
|
404
|
-
save_command_to_history(message)
|
|
405
|
-
except Exception as e:
|
|
406
|
-
self.add_error_message(f"Failed to save command history: {str(e)}")
|
|
407
|
-
|
|
408
|
-
# Update button state
|
|
409
|
-
self._update_submit_cancel_button(True)
|
|
410
|
-
|
|
411
|
-
# Process the message asynchronously using Textual's worker system
|
|
412
|
-
# Using exclusive=False to avoid TaskGroup conflicts with MCP servers
|
|
413
|
-
self._current_worker = self.run_worker(
|
|
414
|
-
self.process_message(message), exclusive=False
|
|
415
|
-
)
|
|
416
|
-
|
|
417
|
-
def _update_submit_cancel_button(self, is_cancel_mode: bool) -> None:
|
|
418
|
-
"""Update the submit/cancel button state."""
|
|
419
|
-
try:
|
|
420
|
-
from .components.input_area import SubmitCancelButton
|
|
421
|
-
|
|
422
|
-
button = self.query_one(SubmitCancelButton)
|
|
423
|
-
button.is_cancel_mode = is_cancel_mode
|
|
424
|
-
except Exception:
|
|
425
|
-
pass # Silently fail if button not found
|
|
426
|
-
|
|
427
|
-
def action_cancel_processing(self) -> None:
|
|
428
|
-
"""Cancel the current message processing."""
|
|
429
|
-
if hasattr(self, "_current_worker") and self._current_worker is not None:
|
|
430
|
-
try:
|
|
431
|
-
# First, kill any running shell processes (same as interactive mode Ctrl+C)
|
|
432
|
-
from code_puppy.tools.command_runner import (
|
|
433
|
-
kill_all_running_shell_processes,
|
|
434
|
-
)
|
|
435
|
-
|
|
436
|
-
killed = kill_all_running_shell_processes()
|
|
437
|
-
if killed:
|
|
438
|
-
self.add_system_message(
|
|
439
|
-
f"🔥 Cancelled {killed} running shell process(es)"
|
|
440
|
-
)
|
|
441
|
-
# Don't stop spinner/agent - let the agent continue processing
|
|
442
|
-
# Shell processes killed, but agent worker continues running
|
|
443
|
-
|
|
444
|
-
else:
|
|
445
|
-
# Only cancel the agent task if NO processes were killed
|
|
446
|
-
self._current_worker.cancel()
|
|
447
|
-
self.add_system_message("⚠️ Processing cancelled by user")
|
|
448
|
-
# Stop spinner and clear state only when agent is actually cancelled
|
|
449
|
-
self._current_worker = None
|
|
450
|
-
self.agent_busy = False
|
|
451
|
-
self.stop_agent_progress()
|
|
452
|
-
except Exception as e:
|
|
453
|
-
self.add_error_message(f"Failed to cancel processing: {str(e)}")
|
|
454
|
-
# Only clear state on exception if we haven't already done so
|
|
455
|
-
if (
|
|
456
|
-
hasattr(self, "_current_worker")
|
|
457
|
-
and self._current_worker is not None
|
|
458
|
-
):
|
|
459
|
-
self._current_worker = None
|
|
460
|
-
self.agent_busy = False
|
|
461
|
-
self.stop_agent_progress()
|
|
462
|
-
|
|
463
|
-
async def process_message(self, message: str) -> None:
|
|
464
|
-
"""Process a user message asynchronously."""
|
|
465
|
-
try:
|
|
466
|
-
self.agent_busy = True
|
|
467
|
-
self._update_submit_cancel_button(True)
|
|
468
|
-
self.start_agent_progress("Thinking")
|
|
469
|
-
|
|
470
|
-
# Handle commands
|
|
471
|
-
if message.strip().startswith("/"):
|
|
472
|
-
# Handle special commands directly
|
|
473
|
-
if message.strip().lower() in ("clear", "/clear"):
|
|
474
|
-
self.action_clear_chat()
|
|
475
|
-
return
|
|
476
|
-
|
|
477
|
-
# Let the command handler process all /agent commands
|
|
478
|
-
# result will be handled by the command handler directly through messaging system
|
|
479
|
-
if message.strip().startswith("/agent"):
|
|
480
|
-
# The command handler will emit messages directly to our messaging system
|
|
481
|
-
handle_command(message.strip())
|
|
482
|
-
# Agent manager will automatically use the latest agent
|
|
483
|
-
return
|
|
484
|
-
|
|
485
|
-
# Handle exit commands
|
|
486
|
-
if message.strip().lower() in ("/exit", "/quit"):
|
|
487
|
-
self.add_system_message("Goodbye!")
|
|
488
|
-
# Exit the application
|
|
489
|
-
self.app.exit()
|
|
490
|
-
return
|
|
491
|
-
|
|
492
|
-
# Use the existing command handler
|
|
493
|
-
# The command handler directly uses the messaging system, so we don't need to capture stdout
|
|
494
|
-
try:
|
|
495
|
-
result = handle_command(message.strip())
|
|
496
|
-
if not result:
|
|
497
|
-
self.add_system_message(f"Unknown command: {message}")
|
|
498
|
-
except Exception as e:
|
|
499
|
-
self.add_error_message(f"Error executing command: {str(e)}")
|
|
500
|
-
return
|
|
501
|
-
|
|
502
|
-
# Process with agent
|
|
503
|
-
try:
|
|
504
|
-
self.update_agent_progress("Processing", 25)
|
|
505
|
-
|
|
506
|
-
# Use agent_manager's run_with_mcp to handle MCP servers properly
|
|
507
|
-
try:
|
|
508
|
-
agent = get_current_agent()
|
|
509
|
-
self.update_agent_progress("Processing", 50)
|
|
510
|
-
result = await agent.run_with_mcp(
|
|
511
|
-
message,
|
|
512
|
-
)
|
|
513
|
-
|
|
514
|
-
if not result or not hasattr(result, "output"):
|
|
515
|
-
self.add_error_message("Invalid response format from agent")
|
|
516
|
-
return
|
|
517
|
-
|
|
518
|
-
self.update_agent_progress("Processing", 75)
|
|
519
|
-
agent_response = result.output
|
|
520
|
-
self.add_agent_message(agent_response)
|
|
521
|
-
|
|
522
|
-
# Auto-save session if enabled (mirror --interactive)
|
|
523
|
-
try:
|
|
524
|
-
from code_puppy.config import auto_save_session_if_enabled
|
|
525
|
-
|
|
526
|
-
auto_save_session_if_enabled()
|
|
527
|
-
except Exception:
|
|
528
|
-
pass
|
|
529
|
-
|
|
530
|
-
# Refresh history display to show new interaction
|
|
531
|
-
self.refresh_history_display()
|
|
532
|
-
|
|
533
|
-
except Exception as eg:
|
|
534
|
-
# Handle TaskGroup and other exceptions
|
|
535
|
-
# BaseExceptionGroup is only available in Python 3.11+
|
|
536
|
-
if hasattr(eg, "exceptions"):
|
|
537
|
-
# Handle TaskGroup exceptions specifically (Python 3.11+)
|
|
538
|
-
for e in eg.exceptions:
|
|
539
|
-
self.add_error_message(f"MCP/Agent error: {str(e)}")
|
|
540
|
-
else:
|
|
541
|
-
# Handle regular exceptions
|
|
542
|
-
self.add_error_message(f"MCP/Agent error: {str(eg)}")
|
|
543
|
-
finally:
|
|
544
|
-
pass
|
|
545
|
-
except Exception as agent_error:
|
|
546
|
-
# Handle any other errors in agent processing
|
|
547
|
-
self.add_error_message(f"Agent processing failed: {str(agent_error)}")
|
|
548
|
-
|
|
549
|
-
except Exception as e:
|
|
550
|
-
self.add_error_message(f"Error processing message: {str(e)}")
|
|
551
|
-
finally:
|
|
552
|
-
self.agent_busy = False
|
|
553
|
-
self._update_submit_cancel_button(False)
|
|
554
|
-
self.stop_agent_progress()
|
|
555
|
-
|
|
556
|
-
# Action methods
|
|
557
|
-
def action_clear_chat(self) -> None:
|
|
558
|
-
"""Clear the chat history."""
|
|
559
|
-
chat_view = self.query_one("#chat-view", ChatView)
|
|
560
|
-
chat_view.clear_messages()
|
|
561
|
-
agent = get_current_agent()
|
|
562
|
-
agent.clear_message_history()
|
|
563
|
-
self.add_system_message("Chat history cleared")
|
|
564
|
-
|
|
565
|
-
def action_show_help(self) -> None:
|
|
566
|
-
"""Show help information in a modal."""
|
|
567
|
-
self.push_screen(HelpScreen())
|
|
568
|
-
|
|
569
|
-
def action_toggle_sidebar(self) -> None:
|
|
570
|
-
"""Toggle sidebar visibility."""
|
|
571
|
-
sidebar = self.query_one(Sidebar)
|
|
572
|
-
sidebar.display = not sidebar.display
|
|
573
|
-
|
|
574
|
-
# If sidebar is now visible, focus the history list to enable immediate keyboard navigation
|
|
575
|
-
if sidebar.display:
|
|
576
|
-
try:
|
|
577
|
-
# Ensure history tab is active
|
|
578
|
-
tabs = self.query_one("#sidebar-tabs")
|
|
579
|
-
tabs.active = "history-tab"
|
|
580
|
-
|
|
581
|
-
# Refresh the command history
|
|
582
|
-
sidebar.load_command_history()
|
|
583
|
-
|
|
584
|
-
# Focus the history list
|
|
585
|
-
history_list = self.query_one("#history-list", ListView)
|
|
586
|
-
history_list.focus()
|
|
587
|
-
|
|
588
|
-
# If the list has items, get the first item for the modal
|
|
589
|
-
if len(history_list.children) > 0:
|
|
590
|
-
# Reset sidebar's internal index tracker to 0
|
|
591
|
-
sidebar.current_history_index = 0
|
|
592
|
-
|
|
593
|
-
# Set ListView index to match
|
|
594
|
-
history_list.index = 0
|
|
595
|
-
|
|
596
|
-
# Get the first item and show the command history modal
|
|
597
|
-
first_item = history_list.children[0]
|
|
598
|
-
if hasattr(first_item, "command_entry"):
|
|
599
|
-
# command_entry = first_item.command_entry
|
|
600
|
-
|
|
601
|
-
# Use call_after_refresh to allow UI to update first
|
|
602
|
-
def show_modal():
|
|
603
|
-
from .components.command_history_modal import (
|
|
604
|
-
CommandHistoryModal,
|
|
605
|
-
)
|
|
606
|
-
|
|
607
|
-
# Get all command entries from the history list
|
|
608
|
-
command_entries = []
|
|
609
|
-
for i, child in enumerate(history_list.children):
|
|
610
|
-
if hasattr(child, "command_entry"):
|
|
611
|
-
command_entries.append(child.command_entry)
|
|
612
|
-
|
|
613
|
-
# Push the modal screen
|
|
614
|
-
# The modal will get the command entries from the sidebar
|
|
615
|
-
self.push_screen(CommandHistoryModal())
|
|
616
|
-
|
|
617
|
-
# Schedule modal to appear after UI refresh
|
|
618
|
-
self.call_after_refresh(show_modal)
|
|
619
|
-
except Exception as e:
|
|
620
|
-
# Log the exception in debug mode but silently fail for end users
|
|
621
|
-
import logging
|
|
622
|
-
|
|
623
|
-
logging.debug(f"Error focusing history item: {str(e)}")
|
|
624
|
-
pass
|
|
625
|
-
else:
|
|
626
|
-
# If sidebar is now hidden, focus the input field for a smooth workflow
|
|
627
|
-
try:
|
|
628
|
-
self.action_focus_input()
|
|
629
|
-
except Exception:
|
|
630
|
-
# Silently fail if there's an issue with focusing
|
|
631
|
-
pass
|
|
632
|
-
|
|
633
|
-
def action_focus_input(self) -> None:
|
|
634
|
-
"""Focus the input field."""
|
|
635
|
-
input_field = self.query_one("#input-field", CustomTextArea)
|
|
636
|
-
input_field.focus()
|
|
637
|
-
|
|
638
|
-
def focus_input_field(self) -> None:
|
|
639
|
-
"""Focus the input field (used for auto-focus on startup)."""
|
|
640
|
-
try:
|
|
641
|
-
input_field = self.query_one("#input-field", CustomTextArea)
|
|
642
|
-
input_field.focus()
|
|
643
|
-
except Exception:
|
|
644
|
-
pass # Silently handle if widget not ready yet
|
|
645
|
-
|
|
646
|
-
def action_focus_chat(self) -> None:
|
|
647
|
-
"""Focus the chat area."""
|
|
648
|
-
chat_view = self.query_one("#chat-view", ChatView)
|
|
649
|
-
chat_view.focus()
|
|
650
|
-
|
|
651
|
-
def action_show_tools(self) -> None:
|
|
652
|
-
"""Show the tools modal."""
|
|
653
|
-
self.push_screen(ToolsScreen())
|
|
654
|
-
|
|
655
|
-
def action_open_settings(self) -> None:
|
|
656
|
-
"""Open the settings configuration screen."""
|
|
657
|
-
|
|
658
|
-
def handle_settings_result(result):
|
|
659
|
-
if result and result.get("success"):
|
|
660
|
-
# Update reactive variables
|
|
661
|
-
from code_puppy.config import get_global_model_name, get_puppy_name
|
|
662
|
-
|
|
663
|
-
self.puppy_name = get_puppy_name()
|
|
664
|
-
|
|
665
|
-
# Handle model change if needed
|
|
666
|
-
if result.get("model_changed"):
|
|
667
|
-
new_model = get_global_model_name()
|
|
668
|
-
self.current_model = new_model
|
|
669
|
-
try:
|
|
670
|
-
current_agent = get_current_agent()
|
|
671
|
-
current_agent.reload_code_generation_agent()
|
|
672
|
-
except Exception as reload_error:
|
|
673
|
-
self.add_error_message(
|
|
674
|
-
f"Failed to reload agent after model change: {reload_error}"
|
|
675
|
-
)
|
|
676
|
-
|
|
677
|
-
# Update status bar
|
|
678
|
-
status_bar = self.query_one(StatusBar)
|
|
679
|
-
status_bar.puppy_name = self.puppy_name
|
|
680
|
-
status_bar.current_model = self.current_model
|
|
681
|
-
|
|
682
|
-
# Show success message
|
|
683
|
-
self.add_system_message(result.get("message", "Settings updated"))
|
|
684
|
-
elif (
|
|
685
|
-
result
|
|
686
|
-
and not result.get("success")
|
|
687
|
-
and "cancelled" not in result.get("message", "").lower()
|
|
688
|
-
):
|
|
689
|
-
# Show error message (but not for cancellation)
|
|
690
|
-
self.add_error_message(result.get("message", "Settings update failed"))
|
|
691
|
-
|
|
692
|
-
self.push_screen(SettingsScreen(), handle_settings_result)
|
|
693
|
-
|
|
694
|
-
def action_open_mcp_wizard(self) -> None:
|
|
695
|
-
"""Open the MCP Install Wizard."""
|
|
696
|
-
|
|
697
|
-
def handle_wizard_result(result):
|
|
698
|
-
if result and result.get("success"):
|
|
699
|
-
# Show success message
|
|
700
|
-
self.add_system_message(
|
|
701
|
-
result.get("message", "MCP server installed successfully")
|
|
702
|
-
)
|
|
703
|
-
|
|
704
|
-
# If a server was installed, suggest starting it
|
|
705
|
-
if result.get("server_name"):
|
|
706
|
-
server_name = result["server_name"]
|
|
707
|
-
self.add_system_message(
|
|
708
|
-
f"💡 Use '/mcp start {server_name}' to start the server"
|
|
709
|
-
)
|
|
710
|
-
elif (
|
|
711
|
-
result
|
|
712
|
-
and not result.get("success")
|
|
713
|
-
and "cancelled" not in result.get("message", "").lower()
|
|
714
|
-
):
|
|
715
|
-
# Show error message (but not for cancellation)
|
|
716
|
-
self.add_error_message(result.get("message", "MCP installation failed"))
|
|
717
|
-
|
|
718
|
-
self.push_screen(MCPInstallWizardScreen(), handle_wizard_result)
|
|
719
|
-
|
|
720
|
-
def process_initial_command(self) -> None:
|
|
721
|
-
"""Process the initial command provided when starting the TUI."""
|
|
722
|
-
if self.initial_command:
|
|
723
|
-
# Add the initial command to the input field
|
|
724
|
-
input_field = self.query_one("#input-field", CustomTextArea)
|
|
725
|
-
input_field.text = self.initial_command
|
|
726
|
-
|
|
727
|
-
# Show that we're auto-executing the initial command
|
|
728
|
-
self.add_system_message(
|
|
729
|
-
f"🚀 Auto-executing initial command: {self.initial_command}"
|
|
730
|
-
)
|
|
731
|
-
|
|
732
|
-
# Automatically submit the message
|
|
733
|
-
self.action_send_message()
|
|
734
|
-
|
|
735
|
-
def show_history_details(self, history_entry: dict) -> None:
|
|
736
|
-
"""Show detailed information about a selected history entry."""
|
|
737
|
-
try:
|
|
738
|
-
timestamp = history_entry.get("timestamp", "Unknown time")
|
|
739
|
-
description = history_entry.get("description", "No description")
|
|
740
|
-
output = history_entry.get("output", "")
|
|
741
|
-
awaiting_input = history_entry.get("awaiting_user_input", False)
|
|
742
|
-
|
|
743
|
-
# Parse timestamp for better display with safe parsing
|
|
744
|
-
def parse_timestamp_safely_for_details(timestamp_str: str) -> str:
|
|
745
|
-
"""Parse timestamp string safely for detailed display."""
|
|
746
|
-
try:
|
|
747
|
-
# Handle 'Z' suffix (common UTC format)
|
|
748
|
-
cleaned_timestamp = timestamp_str.replace("Z", "+00:00")
|
|
749
|
-
parsed_dt = datetime.fromisoformat(cleaned_timestamp)
|
|
750
|
-
|
|
751
|
-
# If the datetime is naive (no timezone), assume UTC
|
|
752
|
-
if parsed_dt.tzinfo is None:
|
|
753
|
-
parsed_dt = parsed_dt.replace(tzinfo=timezone.utc)
|
|
754
|
-
|
|
755
|
-
return parsed_dt.strftime("%Y-%m-%d %H:%M:%S")
|
|
756
|
-
except (ValueError, AttributeError, TypeError):
|
|
757
|
-
# Handle invalid timestamp formats gracefully
|
|
758
|
-
return timestamp_str
|
|
759
|
-
|
|
760
|
-
formatted_time = parse_timestamp_safely_for_details(timestamp)
|
|
761
|
-
|
|
762
|
-
# Create detailed view content
|
|
763
|
-
details = [
|
|
764
|
-
f"Timestamp: {formatted_time}",
|
|
765
|
-
f"Description: {description}",
|
|
766
|
-
"",
|
|
767
|
-
]
|
|
768
|
-
|
|
769
|
-
if output:
|
|
770
|
-
details.extend(
|
|
771
|
-
[
|
|
772
|
-
"Output:",
|
|
773
|
-
"─" * 40,
|
|
774
|
-
output,
|
|
775
|
-
"",
|
|
776
|
-
]
|
|
777
|
-
)
|
|
778
|
-
|
|
779
|
-
if awaiting_input:
|
|
780
|
-
details.append("⚠️ Was awaiting user input")
|
|
781
|
-
|
|
782
|
-
# Display details as a system message in the chat
|
|
783
|
-
detail_text = "\\n".join(details)
|
|
784
|
-
self.add_system_message(f"History Details:\\n{detail_text}")
|
|
785
|
-
|
|
786
|
-
except Exception as e:
|
|
787
|
-
self.add_error_message(f"Failed to show history details: {e}")
|
|
788
|
-
|
|
789
|
-
# Progress and status methods
|
|
790
|
-
def set_agent_status(self, status: str, show_progress: bool = False) -> None:
|
|
791
|
-
"""Update agent status and optionally show/hide progress bar."""
|
|
792
|
-
try:
|
|
793
|
-
# Update status bar
|
|
794
|
-
status_bar = self.query_one(StatusBar)
|
|
795
|
-
status_bar.agent_status = status
|
|
796
|
-
|
|
797
|
-
# Update spinner visibility
|
|
798
|
-
from .components.input_area import SimpleSpinnerWidget
|
|
799
|
-
|
|
800
|
-
spinner = self.query_one("#spinner", SimpleSpinnerWidget)
|
|
801
|
-
if show_progress:
|
|
802
|
-
spinner.add_class("visible")
|
|
803
|
-
spinner.display = True
|
|
804
|
-
spinner.start_spinning()
|
|
805
|
-
else:
|
|
806
|
-
spinner.remove_class("visible")
|
|
807
|
-
spinner.display = False
|
|
808
|
-
spinner.stop_spinning()
|
|
809
|
-
|
|
810
|
-
except Exception:
|
|
811
|
-
pass # Silently fail if widgets not available
|
|
812
|
-
|
|
813
|
-
def start_agent_progress(self, initial_status: str = "Thinking") -> None:
|
|
814
|
-
"""Start showing agent progress indicators."""
|
|
815
|
-
self.set_agent_status(initial_status, show_progress=True)
|
|
816
|
-
|
|
817
|
-
def update_agent_progress(self, status: str, progress: int = None) -> None:
|
|
818
|
-
"""Update agent progress during processing."""
|
|
819
|
-
try:
|
|
820
|
-
status_bar = self.query_one(StatusBar)
|
|
821
|
-
status_bar.agent_status = status
|
|
822
|
-
# Note: LoadingIndicator doesn't use progress values, it just spins
|
|
823
|
-
except Exception:
|
|
824
|
-
pass
|
|
825
|
-
|
|
826
|
-
def stop_agent_progress(self) -> None:
|
|
827
|
-
"""Stop showing agent progress indicators."""
|
|
828
|
-
self.set_agent_status("Ready", show_progress=False)
|
|
829
|
-
|
|
830
|
-
def on_resize(self, event: Resize) -> None:
|
|
831
|
-
"""Handle terminal resize events to update responsive elements."""
|
|
832
|
-
try:
|
|
833
|
-
# Apply responsive layout adjustments
|
|
834
|
-
self.apply_responsive_layout()
|
|
835
|
-
|
|
836
|
-
# Update status bar to reflect new width
|
|
837
|
-
status_bar = self.query_one(StatusBar)
|
|
838
|
-
status_bar.update_status()
|
|
839
|
-
|
|
840
|
-
# Refresh history display with new responsive truncation
|
|
841
|
-
self.refresh_history_display()
|
|
842
|
-
|
|
843
|
-
except Exception:
|
|
844
|
-
pass # Silently handle resize errors
|
|
845
|
-
|
|
846
|
-
def apply_responsive_layout(self) -> None:
|
|
847
|
-
"""Apply responsive layout adjustments based on terminal size."""
|
|
848
|
-
try:
|
|
849
|
-
terminal_width = self.size.width if hasattr(self, "size") else 80
|
|
850
|
-
terminal_height = self.size.height if hasattr(self, "size") else 24
|
|
851
|
-
sidebar = self.query_one(Sidebar)
|
|
852
|
-
|
|
853
|
-
# Responsive sidebar width based on terminal width
|
|
854
|
-
if terminal_width >= 120:
|
|
855
|
-
sidebar.styles.width = 35
|
|
856
|
-
elif terminal_width >= 100:
|
|
857
|
-
sidebar.styles.width = 30
|
|
858
|
-
elif terminal_width >= 80:
|
|
859
|
-
sidebar.styles.width = 25
|
|
860
|
-
elif terminal_width >= 60:
|
|
861
|
-
sidebar.styles.width = 20
|
|
862
|
-
else:
|
|
863
|
-
sidebar.styles.width = 15
|
|
864
|
-
|
|
865
|
-
# Auto-hide sidebar on very narrow terminals
|
|
866
|
-
if terminal_width < 50:
|
|
867
|
-
if sidebar.display:
|
|
868
|
-
sidebar.display = False
|
|
869
|
-
self.add_system_message(
|
|
870
|
-
"💡 Sidebar auto-hidden for narrow terminal. Press Ctrl+2 to toggle."
|
|
871
|
-
)
|
|
872
|
-
|
|
873
|
-
# Adjust input area height for very short terminals
|
|
874
|
-
if terminal_height < 20:
|
|
875
|
-
input_area = self.query_one(InputArea)
|
|
876
|
-
input_area.styles.height = 7
|
|
877
|
-
else:
|
|
878
|
-
input_area = self.query_one(InputArea)
|
|
879
|
-
input_area.styles.height = 9
|
|
880
|
-
|
|
881
|
-
except Exception:
|
|
882
|
-
pass
|
|
883
|
-
|
|
884
|
-
def start_message_renderer_sync(self):
|
|
885
|
-
"""Synchronous wrapper to start message renderer via run_worker."""
|
|
886
|
-
self.run_worker(self.start_message_renderer(), exclusive=False)
|
|
887
|
-
|
|
888
|
-
async def preload_agent_on_startup(self) -> None:
|
|
889
|
-
"""Preload the agent/model at startup so loading status is visible."""
|
|
890
|
-
try:
|
|
891
|
-
# Show loading in status bar and spinner
|
|
892
|
-
self.start_agent_progress("Loading")
|
|
893
|
-
|
|
894
|
-
# Warm up agent/model without blocking UI
|
|
895
|
-
import asyncio
|
|
896
|
-
|
|
897
|
-
from code_puppy.agents.agent_manager import get_current_agent
|
|
898
|
-
|
|
899
|
-
agent = get_current_agent()
|
|
900
|
-
|
|
901
|
-
# Run the synchronous reload in a worker thread
|
|
902
|
-
await asyncio.to_thread(agent.reload_code_generation_agent)
|
|
903
|
-
|
|
904
|
-
# After load, refresh current model (in case of fallback or changes)
|
|
905
|
-
from code_puppy.config import get_global_model_name
|
|
906
|
-
|
|
907
|
-
self.current_model = get_global_model_name()
|
|
908
|
-
|
|
909
|
-
# Let the user know model/agent are ready
|
|
910
|
-
self.add_system_message("Model and agent preloaded. Ready to roll 🛼")
|
|
911
|
-
except Exception as e:
|
|
912
|
-
# Surface any preload issues but keep app usable
|
|
913
|
-
self.add_error_message(f"Startup preload failed: {e}")
|
|
914
|
-
finally:
|
|
915
|
-
# Always stop spinner and set ready state
|
|
916
|
-
self.stop_agent_progress()
|
|
917
|
-
|
|
918
|
-
async def start_message_renderer(self):
|
|
919
|
-
"""Start the message renderer to consume messages from the queue."""
|
|
920
|
-
if not self._renderer_started:
|
|
921
|
-
self._renderer_started = True
|
|
922
|
-
|
|
923
|
-
# Process any buffered startup messages first
|
|
924
|
-
from io import StringIO
|
|
925
|
-
|
|
926
|
-
from rich.console import Console
|
|
927
|
-
|
|
928
|
-
from code_puppy.messaging import get_buffered_startup_messages
|
|
929
|
-
|
|
930
|
-
buffered_messages = get_buffered_startup_messages()
|
|
931
|
-
|
|
932
|
-
if buffered_messages:
|
|
933
|
-
# Group startup messages into a single display
|
|
934
|
-
startup_content_lines = []
|
|
935
|
-
|
|
936
|
-
for message in buffered_messages:
|
|
937
|
-
try:
|
|
938
|
-
# Convert message content to string for grouping
|
|
939
|
-
if hasattr(message.content, "__rich_console__"):
|
|
940
|
-
# For Rich objects, render to plain text
|
|
941
|
-
string_io = StringIO()
|
|
942
|
-
# Use markup=False to prevent interpretation of square brackets as markup
|
|
943
|
-
temp_console = Console(
|
|
944
|
-
file=string_io,
|
|
945
|
-
width=80,
|
|
946
|
-
legacy_windows=False,
|
|
947
|
-
markup=False,
|
|
948
|
-
)
|
|
949
|
-
temp_console.print(message.content)
|
|
950
|
-
content_str = string_io.getvalue().rstrip("\n")
|
|
951
|
-
else:
|
|
952
|
-
content_str = str(message.content)
|
|
953
|
-
|
|
954
|
-
startup_content_lines.append(content_str)
|
|
955
|
-
except Exception as e:
|
|
956
|
-
startup_content_lines.append(
|
|
957
|
-
f"Error processing startup message: {e}"
|
|
958
|
-
)
|
|
959
|
-
|
|
960
|
-
# Create a single grouped startup message (tightened)
|
|
961
|
-
grouped_content = "\n".join(startup_content_lines)
|
|
962
|
-
self.add_system_message(self._tighten_text(grouped_content))
|
|
963
|
-
|
|
964
|
-
# Clear the startup buffer after processing
|
|
965
|
-
self.message_queue.clear_startup_buffer()
|
|
966
|
-
|
|
967
|
-
# Now start the regular message renderer
|
|
968
|
-
await self.message_renderer.start()
|
|
969
|
-
|
|
970
|
-
async def maybe_prompt_restore_autosave(self) -> None:
|
|
971
|
-
"""Offer to restore an autosave session at startup (TUI version)."""
|
|
972
|
-
try:
|
|
973
|
-
from pathlib import Path
|
|
974
|
-
|
|
975
|
-
from code_puppy.config import (
|
|
976
|
-
AUTOSAVE_DIR,
|
|
977
|
-
set_current_autosave_from_session_name,
|
|
978
|
-
)
|
|
979
|
-
from code_puppy.session_storage import list_sessions, load_session
|
|
980
|
-
|
|
981
|
-
base_dir = Path(AUTOSAVE_DIR)
|
|
982
|
-
sessions = list_sessions(base_dir)
|
|
983
|
-
if not sessions:
|
|
984
|
-
return
|
|
985
|
-
|
|
986
|
-
# Show modal picker for selection
|
|
987
|
-
from .screens.autosave_picker import AutosavePicker
|
|
988
|
-
|
|
989
|
-
async def handle_result(result_name: str | None):
|
|
990
|
-
if not result_name:
|
|
991
|
-
return
|
|
992
|
-
try:
|
|
993
|
-
# Load history and set into agent
|
|
994
|
-
from code_puppy.agents.agent_manager import get_current_agent
|
|
995
|
-
|
|
996
|
-
history = load_session(result_name, base_dir)
|
|
997
|
-
agent = get_current_agent()
|
|
998
|
-
agent.set_message_history(history)
|
|
999
|
-
|
|
1000
|
-
# Set current autosave session id so subsequent autosaves overwrite this session
|
|
1001
|
-
try:
|
|
1002
|
-
set_current_autosave_from_session_name(result_name)
|
|
1003
|
-
except Exception:
|
|
1004
|
-
pass
|
|
1005
|
-
|
|
1006
|
-
# Update token info/status bar
|
|
1007
|
-
total_tokens = sum(
|
|
1008
|
-
agent.estimate_tokens_for_message(msg) for msg in history
|
|
1009
|
-
)
|
|
1010
|
-
try:
|
|
1011
|
-
status_bar = self.query_one(StatusBar)
|
|
1012
|
-
status_bar.update_token_info(
|
|
1013
|
-
total_tokens,
|
|
1014
|
-
agent.get_model_context_length(),
|
|
1015
|
-
total_tokens / max(1, agent.get_model_context_length()),
|
|
1016
|
-
)
|
|
1017
|
-
except Exception:
|
|
1018
|
-
pass
|
|
1019
|
-
|
|
1020
|
-
# Notify
|
|
1021
|
-
session_path = base_dir / f"{result_name}.pkl"
|
|
1022
|
-
self.add_system_message(
|
|
1023
|
-
f"✅ Autosave loaded: {len(history)} messages ({total_tokens} tokens)\n"
|
|
1024
|
-
f"📁 From: {session_path}"
|
|
1025
|
-
)
|
|
1026
|
-
|
|
1027
|
-
# Refresh history sidebar
|
|
1028
|
-
self.refresh_history_display()
|
|
1029
|
-
except Exception as e:
|
|
1030
|
-
self.add_error_message(f"Failed to load autosave: {e}")
|
|
1031
|
-
|
|
1032
|
-
# Push modal and await result
|
|
1033
|
-
picker = AutosavePicker(base_dir)
|
|
1034
|
-
|
|
1035
|
-
# Use Textual's push_screen with a result callback
|
|
1036
|
-
def on_picker_result(result_name=None):
|
|
1037
|
-
# Schedule async handler to avoid blocking UI
|
|
1038
|
-
|
|
1039
|
-
self.run_worker(handle_result(result_name), exclusive=False)
|
|
1040
|
-
|
|
1041
|
-
self.push_screen(picker, on_picker_result)
|
|
1042
|
-
except Exception as e:
|
|
1043
|
-
# Fail silently but show debug in chat
|
|
1044
|
-
self.add_system_message(f"[dim]Autosave prompt error: {e}[/dim]")
|
|
1045
|
-
|
|
1046
|
-
async def stop_message_renderer(self):
|
|
1047
|
-
"""Stop the message renderer."""
|
|
1048
|
-
if self._renderer_started:
|
|
1049
|
-
self._renderer_started = False
|
|
1050
|
-
try:
|
|
1051
|
-
await self.message_renderer.stop()
|
|
1052
|
-
except Exception as e:
|
|
1053
|
-
# Log renderer stop errors but don't crash
|
|
1054
|
-
self.add_system_message(f"Renderer stop error: {e}")
|
|
1055
|
-
|
|
1056
|
-
@on(HistoryEntrySelected)
|
|
1057
|
-
def on_history_entry_selected(self, event: HistoryEntrySelected) -> None:
|
|
1058
|
-
"""Handle selection of a history entry from the sidebar."""
|
|
1059
|
-
# Display the history entry details
|
|
1060
|
-
self.show_history_details(event.history_entry)
|
|
1061
|
-
|
|
1062
|
-
@on(CommandSelected)
|
|
1063
|
-
def on_command_selected(self, event: CommandSelected) -> None:
|
|
1064
|
-
"""Handle selection of a command from the history modal."""
|
|
1065
|
-
# Set the command in the input field
|
|
1066
|
-
input_field = self.query_one("#input-field", CustomTextArea)
|
|
1067
|
-
input_field.text = event.command
|
|
1068
|
-
|
|
1069
|
-
# Focus the input field for immediate editing
|
|
1070
|
-
input_field.focus()
|
|
1071
|
-
|
|
1072
|
-
# Close the sidebar automatically for a smoother workflow
|
|
1073
|
-
sidebar = self.query_one(Sidebar)
|
|
1074
|
-
sidebar.display = False
|
|
1075
|
-
|
|
1076
|
-
async def on_unmount(self):
|
|
1077
|
-
"""Clean up when the app is unmounted."""
|
|
1078
|
-
try:
|
|
1079
|
-
# Unregister the agent reload callback
|
|
1080
|
-
from code_puppy.callbacks import unregister_callback
|
|
1081
|
-
|
|
1082
|
-
unregister_callback("agent_reload", self._on_agent_reload)
|
|
1083
|
-
|
|
1084
|
-
await self.stop_message_renderer()
|
|
1085
|
-
except Exception as e:
|
|
1086
|
-
# Log unmount errors but don't crash during cleanup
|
|
1087
|
-
try:
|
|
1088
|
-
self.add_system_message(f"Unmount cleanup error: {e}")
|
|
1089
|
-
except Exception:
|
|
1090
|
-
# If we can't even add a message, just ignore
|
|
1091
|
-
pass
|
|
1092
|
-
|
|
1093
|
-
|
|
1094
|
-
async def run_textual_ui(initial_command: str = None):
|
|
1095
|
-
"""Run the Textual UI interface."""
|
|
1096
|
-
# Always enable YOLO mode in TUI mode for a smoother experience
|
|
1097
|
-
from code_puppy.config import set_config_value
|
|
1098
|
-
|
|
1099
|
-
# Initialize the command history file
|
|
1100
|
-
initialize_command_history_file()
|
|
1101
|
-
|
|
1102
|
-
set_config_value("yolo_mode", "true")
|
|
1103
|
-
|
|
1104
|
-
app = CodePuppyTUI(initial_command=initial_command)
|
|
1105
|
-
await app.run_async()
|