minion-code 0.1.0__py3-none-any.whl → 0.1.1__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.
- examples/cli_entrypoint.py +60 -0
- examples/{agent_with_todos.py → components/agent_with_todos.py} +58 -47
- examples/{message_response_children_demo.py → components/message_response_children_demo.py} +61 -55
- examples/components/messages_component.py +199 -0
- examples/file_freshness_example.py +22 -22
- examples/file_watching_example.py +32 -26
- examples/interruptible_tui.py +921 -3
- examples/repl_tui.py +129 -0
- examples/skills/example_usage.py +57 -0
- examples/start.py +173 -0
- minion_code/__init__.py +1 -1
- minion_code/acp_server/__init__.py +34 -0
- minion_code/acp_server/agent.py +539 -0
- minion_code/acp_server/hooks.py +354 -0
- minion_code/acp_server/main.py +194 -0
- minion_code/acp_server/permissions.py +142 -0
- minion_code/acp_server/test_client.py +104 -0
- minion_code/adapters/__init__.py +22 -0
- minion_code/adapters/output_adapter.py +207 -0
- minion_code/adapters/rich_adapter.py +169 -0
- minion_code/adapters/textual_adapter.py +254 -0
- minion_code/agents/__init__.py +2 -2
- minion_code/agents/code_agent.py +517 -104
- minion_code/agents/hooks.py +378 -0
- minion_code/cli.py +538 -429
- minion_code/cli_simple.py +665 -0
- minion_code/commands/__init__.py +136 -29
- minion_code/commands/clear_command.py +19 -46
- minion_code/commands/help_command.py +33 -49
- minion_code/commands/history_command.py +37 -55
- minion_code/commands/model_command.py +194 -0
- minion_code/commands/quit_command.py +9 -12
- minion_code/commands/resume_command.py +181 -0
- minion_code/commands/skill_command.py +89 -0
- minion_code/commands/status_command.py +48 -73
- minion_code/commands/tools_command.py +54 -52
- minion_code/commands/version_command.py +34 -69
- minion_code/components/ConfirmDialog.py +430 -0
- minion_code/components/Message.py +318 -97
- minion_code/components/MessageResponse.py +30 -29
- minion_code/components/Messages.py +351 -0
- minion_code/components/PromptInput.py +499 -245
- minion_code/components/__init__.py +24 -17
- minion_code/const.py +7 -0
- minion_code/screens/REPL.py +1453 -469
- minion_code/screens/__init__.py +1 -1
- minion_code/services/__init__.py +20 -20
- minion_code/services/event_system.py +19 -14
- minion_code/services/file_freshness_service.py +223 -170
- minion_code/skills/__init__.py +25 -0
- minion_code/skills/skill.py +128 -0
- minion_code/skills/skill_loader.py +198 -0
- minion_code/skills/skill_registry.py +177 -0
- minion_code/subagents/__init__.py +31 -0
- minion_code/subagents/builtin/__init__.py +30 -0
- minion_code/subagents/builtin/claude_code_guide.py +32 -0
- minion_code/subagents/builtin/explore.py +36 -0
- minion_code/subagents/builtin/general_purpose.py +19 -0
- minion_code/subagents/builtin/plan.py +61 -0
- minion_code/subagents/subagent.py +116 -0
- minion_code/subagents/subagent_loader.py +147 -0
- minion_code/subagents/subagent_registry.py +151 -0
- minion_code/tools/__init__.py +8 -2
- minion_code/tools/bash_tool.py +16 -3
- minion_code/tools/file_edit_tool.py +201 -104
- minion_code/tools/file_read_tool.py +183 -26
- minion_code/tools/file_write_tool.py +17 -3
- minion_code/tools/glob_tool.py +23 -2
- minion_code/tools/grep_tool.py +229 -21
- minion_code/tools/ls_tool.py +28 -3
- minion_code/tools/multi_edit_tool.py +89 -84
- minion_code/tools/python_interpreter_tool.py +9 -1
- minion_code/tools/skill_tool.py +210 -0
- minion_code/tools/task_tool.py +287 -0
- minion_code/tools/todo_read_tool.py +28 -24
- minion_code/tools/todo_write_tool.py +82 -65
- minion_code/{types.py → type_defs.py} +15 -2
- minion_code/utils/__init__.py +45 -17
- minion_code/utils/config.py +610 -0
- minion_code/utils/history.py +114 -0
- minion_code/utils/logs.py +53 -0
- minion_code/utils/mcp_loader.py +153 -55
- minion_code/utils/output_truncator.py +233 -0
- minion_code/utils/session_storage.py +369 -0
- minion_code/utils/todo_file_utils.py +26 -22
- minion_code/utils/todo_storage.py +43 -33
- minion_code/web/__init__.py +9 -0
- minion_code/web/adapters/__init__.py +5 -0
- minion_code/web/adapters/web_adapter.py +524 -0
- minion_code/web/api/__init__.py +7 -0
- minion_code/web/api/chat.py +277 -0
- minion_code/web/api/interactions.py +136 -0
- minion_code/web/api/sessions.py +135 -0
- minion_code/web/server.py +149 -0
- minion_code/web/services/__init__.py +5 -0
- minion_code/web/services/session_manager.py +420 -0
- minion_code-0.1.1.dist-info/METADATA +475 -0
- minion_code-0.1.1.dist-info/RECORD +111 -0
- {minion_code-0.1.0.dist-info → minion_code-0.1.1.dist-info}/WHEEL +1 -1
- minion_code-0.1.1.dist-info/entry_points.txt +6 -0
- tests/test_adapter.py +67 -0
- tests/test_adapter_simple.py +79 -0
- tests/test_file_read_tool.py +144 -0
- tests/test_readonly_tools.py +0 -2
- tests/test_skills.py +441 -0
- examples/advance_tui.py +0 -508
- examples/rich_example.py +0 -4
- examples/simple_file_watching.py +0 -57
- examples/simple_tui.py +0 -267
- examples/simple_usage.py +0 -69
- minion_code-0.1.0.dist-info/METADATA +0 -350
- minion_code-0.1.0.dist-info/RECORD +0 -59
- minion_code-0.1.0.dist-info/entry_points.txt +0 -4
- {minion_code-0.1.0.dist-info → minion_code-0.1.1.dist-info}/licenses/LICENSE +0 -0
- {minion_code-0.1.0.dist-info → minion_code-0.1.1.dist-info}/top_level.txt +0 -0
minion_code/screens/REPL.py
CHANGED
|
@@ -6,7 +6,16 @@ Simulates React-like component structure as documented in AGENTS.md
|
|
|
6
6
|
|
|
7
7
|
from textual.app import App, ComposeResult
|
|
8
8
|
from textual.containers import Container, Horizontal, Vertical, ScrollableContainer
|
|
9
|
-
from textual.widgets import
|
|
9
|
+
from textual.widgets import (
|
|
10
|
+
Input,
|
|
11
|
+
RichLog,
|
|
12
|
+
Button,
|
|
13
|
+
Static,
|
|
14
|
+
Header,
|
|
15
|
+
Footer,
|
|
16
|
+
Label,
|
|
17
|
+
TextArea,
|
|
18
|
+
)
|
|
10
19
|
from textual.reactive import reactive, var
|
|
11
20
|
from textual import on, work
|
|
12
21
|
from textual.screen import Screen
|
|
@@ -14,6 +23,7 @@ from rich.text import Text
|
|
|
14
23
|
from rich.syntax import Syntax
|
|
15
24
|
from rich.console import Console
|
|
16
25
|
import asyncio
|
|
26
|
+
import os
|
|
17
27
|
from typing import List, Dict, Any, Optional, Callable, Union, Set
|
|
18
28
|
from dataclasses import dataclass, field
|
|
19
29
|
from enum import Enum
|
|
@@ -21,78 +31,147 @@ import uuid
|
|
|
21
31
|
import time
|
|
22
32
|
from pathlib import Path
|
|
23
33
|
|
|
24
|
-
#
|
|
25
|
-
import
|
|
26
|
-
|
|
27
|
-
|
|
34
|
+
# Session storage imports
|
|
35
|
+
from ..utils.session_storage import (
|
|
36
|
+
Session,
|
|
37
|
+
create_session,
|
|
38
|
+
save_session,
|
|
39
|
+
load_session,
|
|
40
|
+
get_latest_session_id,
|
|
41
|
+
add_message as session_add_message,
|
|
42
|
+
restore_agent_history,
|
|
43
|
+
)
|
|
44
|
+
|
|
45
|
+
# No logging in UI components to reduce noise
|
|
28
46
|
|
|
29
47
|
|
|
30
48
|
# Import shared types
|
|
31
|
-
from ..
|
|
32
|
-
MessageType,
|
|
33
|
-
|
|
34
|
-
|
|
49
|
+
from ..type_defs import (
|
|
50
|
+
MessageType,
|
|
51
|
+
InputMode,
|
|
52
|
+
MessageContent,
|
|
53
|
+
Message as MessageData,
|
|
54
|
+
ToolUseConfirm,
|
|
55
|
+
BinaryFeedbackContext,
|
|
56
|
+
ToolJSXContext,
|
|
57
|
+
REPLConfig,
|
|
58
|
+
ModelInfo,
|
|
35
59
|
)
|
|
36
60
|
|
|
37
61
|
|
|
38
62
|
class Logo(Static):
|
|
39
63
|
"""Logo component equivalent to React Logo component"""
|
|
40
|
-
|
|
41
|
-
def __init__(
|
|
64
|
+
|
|
65
|
+
def __init__(
|
|
66
|
+
self,
|
|
67
|
+
mcp_clients=None,
|
|
68
|
+
is_default_model=True,
|
|
69
|
+
update_banner_version=None,
|
|
70
|
+
**kwargs,
|
|
71
|
+
):
|
|
42
72
|
super().__init__(**kwargs)
|
|
43
73
|
self.mcp_clients = mcp_clients or []
|
|
44
74
|
self.is_default_model = is_default_model
|
|
45
75
|
self.update_banner_version = update_banner_version
|
|
46
|
-
|
|
76
|
+
|
|
47
77
|
def render(self) -> str:
|
|
48
78
|
logo_text = "🤖 Minion Code Assistant"
|
|
49
79
|
if self.update_banner_version:
|
|
50
80
|
logo_text += f" (Update available: {self.update_banner_version})"
|
|
51
81
|
return logo_text
|
|
52
82
|
|
|
83
|
+
|
|
53
84
|
class ModeIndicator(Static):
|
|
54
85
|
"""Mode indicator component"""
|
|
55
|
-
|
|
86
|
+
|
|
56
87
|
def __init__(self, mode: InputMode = InputMode.PROMPT, **kwargs):
|
|
57
88
|
super().__init__(**kwargs)
|
|
58
89
|
self.mode = mode
|
|
59
|
-
|
|
90
|
+
|
|
60
91
|
def render(self) -> str:
|
|
61
92
|
return f"Mode: {self.mode.value.upper()}"
|
|
62
93
|
|
|
94
|
+
|
|
63
95
|
class Spinner(Static):
|
|
64
|
-
"""
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
96
|
+
"""Simple loading spinner - just one line of animated text"""
|
|
97
|
+
|
|
98
|
+
DEFAULT_CSS = """
|
|
99
|
+
Spinner {
|
|
100
|
+
color: $primary;
|
|
101
|
+
text-style: italic;
|
|
102
|
+
height: 1;
|
|
103
|
+
margin: 1 0;
|
|
104
|
+
padding: 0 1;
|
|
105
|
+
}
|
|
106
|
+
"""
|
|
107
|
+
|
|
108
|
+
def __init__(self, message: str = "Processing", **kwargs):
|
|
109
|
+
super().__init__("⠋ Processing...", **kwargs)
|
|
110
|
+
self.base_message = message
|
|
69
111
|
self.spinner_chars = "⠋⠙⠹⠸⠼⠴⠦⠧⠇⠏"
|
|
70
112
|
self.spinner_index = 0
|
|
71
|
-
|
|
113
|
+
self._timer = None
|
|
114
|
+
|
|
72
115
|
def on_mount(self):
|
|
73
|
-
self.set_interval(0.1, self.update_spinner)
|
|
74
|
-
|
|
116
|
+
self._timer = self.set_interval(0.1, self.update_spinner)
|
|
117
|
+
|
|
118
|
+
def on_unmount(self):
|
|
119
|
+
if self._timer:
|
|
120
|
+
self._timer.stop()
|
|
121
|
+
|
|
75
122
|
def update_spinner(self):
|
|
76
123
|
self.spinner_index = (self.spinner_index + 1) % len(self.spinner_chars)
|
|
77
|
-
self.update(f"{self.spinner_chars[self.spinner_index]}
|
|
124
|
+
self.update(f"{self.spinner_chars[self.spinner_index]} {self.base_message}...")
|
|
125
|
+
|
|
126
|
+
def set_message(self, message: str):
|
|
127
|
+
"""Update the spinner message"""
|
|
128
|
+
self.base_message = message
|
|
129
|
+
|
|
78
130
|
|
|
79
131
|
class MessageWidget(Container):
|
|
80
|
-
"""Individual message display widget"""
|
|
81
|
-
|
|
82
|
-
def __init__(
|
|
132
|
+
"""Individual message display widget with streaming support"""
|
|
133
|
+
|
|
134
|
+
def __init__(
|
|
135
|
+
self, message: MessageData, verbose: bool = False, debug: bool = False, **kwargs
|
|
136
|
+
):
|
|
83
137
|
super().__init__(**kwargs)
|
|
84
138
|
self.message = message
|
|
85
139
|
self.verbose = verbose
|
|
86
140
|
self.debug = debug
|
|
87
|
-
|
|
141
|
+
self.is_streaming = (
|
|
142
|
+
message.options.get("streaming", False) if message.options else False
|
|
143
|
+
)
|
|
144
|
+
self.is_error = (
|
|
145
|
+
message.options.get("error", False) if message.options else False
|
|
146
|
+
)
|
|
147
|
+
|
|
88
148
|
def compose(self) -> ComposeResult:
|
|
149
|
+
content_text = self._get_content_text()
|
|
150
|
+
|
|
89
151
|
if self.message.type == MessageType.USER:
|
|
90
|
-
yield Static(f"👤 User:
|
|
152
|
+
yield Static(f"👤 User:", classes="user-label")
|
|
153
|
+
yield Static(content_text, classes="user-message")
|
|
91
154
|
elif self.message.type == MessageType.ASSISTANT:
|
|
92
|
-
|
|
155
|
+
if self.is_streaming:
|
|
156
|
+
yield Static(
|
|
157
|
+
f"🤖 Assistant: ⠋ Thinking...", classes="assistant-streaming"
|
|
158
|
+
)
|
|
159
|
+
elif self.is_error:
|
|
160
|
+
yield Static(f"❌ Assistant:", classes="assistant-error-label")
|
|
161
|
+
yield Static(content_text, classes="assistant-error")
|
|
162
|
+
else:
|
|
163
|
+
yield Static(f"🤖 Assistant:", classes="assistant-label")
|
|
164
|
+
# Handle markdown content
|
|
165
|
+
if "```" in content_text or content_text.startswith("#"):
|
|
166
|
+
from rich.markdown import Markdown
|
|
167
|
+
|
|
168
|
+
yield Static(Markdown(content_text), classes="assistant-message")
|
|
169
|
+
else:
|
|
170
|
+
yield Static(content_text, classes="assistant-message")
|
|
93
171
|
elif self.message.type == MessageType.PROGRESS:
|
|
94
|
-
yield Static(f"⚙️ Progress:
|
|
95
|
-
|
|
172
|
+
yield Static(f"⚙️ Progress:", classes="progress-label")
|
|
173
|
+
yield Static(content_text, classes="progress-message")
|
|
174
|
+
|
|
96
175
|
def _get_content_text(self) -> str:
|
|
97
176
|
if isinstance(self.message.message.content, str):
|
|
98
177
|
return self.message.message.content
|
|
@@ -105,25 +184,54 @@ class MessageWidget(Container):
|
|
|
105
184
|
return "\n".join(text_parts)
|
|
106
185
|
return str(self.message.message.content)
|
|
107
186
|
|
|
187
|
+
def update_streaming_content(self, new_content: str):
|
|
188
|
+
"""Update streaming message content"""
|
|
189
|
+
if self.is_streaming:
|
|
190
|
+
try:
|
|
191
|
+
# Update the message content
|
|
192
|
+
self.message.message.content = new_content
|
|
193
|
+
# Find and update the static widget
|
|
194
|
+
static_widgets = self.query("Static")
|
|
195
|
+
if len(static_widgets) > 1:
|
|
196
|
+
static_widgets[1].update(new_content)
|
|
197
|
+
except Exception:
|
|
198
|
+
pass # Silently handle streaming update errors
|
|
199
|
+
|
|
200
|
+
def finalize_streaming(self, final_content: str):
|
|
201
|
+
"""Finalize streaming message with final content"""
|
|
202
|
+
if self.is_streaming:
|
|
203
|
+
self.is_streaming = False
|
|
204
|
+
self.message.options["streaming"] = False
|
|
205
|
+
self.message.message.content = final_content
|
|
206
|
+
# Refresh the entire widget
|
|
207
|
+
self.refresh()
|
|
208
|
+
|
|
108
209
|
|
|
109
|
-
#
|
|
210
|
+
# Import components
|
|
110
211
|
from ..components.PromptInput import PromptInput
|
|
212
|
+
from ..components.Messages import Messages
|
|
213
|
+
from ..components.ConfirmDialog import ConfirmDialog, ChoiceDialog, InputDialog
|
|
214
|
+
|
|
215
|
+
# Import adapters
|
|
216
|
+
from ..adapters.textual_adapter import TextualOutputAdapter
|
|
217
|
+
|
|
111
218
|
|
|
112
219
|
class CostThresholdDialog(Container):
|
|
113
220
|
"""Cost threshold warning dialog"""
|
|
114
|
-
|
|
221
|
+
|
|
115
222
|
def compose(self) -> ComposeResult:
|
|
116
223
|
yield Static("⚠️ Cost Threshold Warning", classes="dialog-title")
|
|
117
224
|
yield Static("You have exceeded $5 in API costs. Please be mindful of usage.")
|
|
118
225
|
yield Button("Acknowledge", id="acknowledge_btn", variant="primary")
|
|
119
226
|
|
|
227
|
+
|
|
120
228
|
class PermissionRequest(Container):
|
|
121
229
|
"""Permission request dialog for tool usage"""
|
|
122
|
-
|
|
230
|
+
|
|
123
231
|
def __init__(self, tool_use_confirm: ToolUseConfirm, **kwargs):
|
|
124
232
|
super().__init__(**kwargs)
|
|
125
233
|
self.tool_use_confirm = tool_use_confirm
|
|
126
|
-
|
|
234
|
+
|
|
127
235
|
def compose(self) -> ComposeResult:
|
|
128
236
|
yield Static(f"🔧 Tool Permission Request", classes="dialog-title")
|
|
129
237
|
yield Static(f"Tool: {self.tool_use_confirm.tool_name}")
|
|
@@ -131,11 +239,11 @@ class PermissionRequest(Container):
|
|
|
131
239
|
with Horizontal():
|
|
132
240
|
yield Button("Allow", id="allow_btn", variant="success")
|
|
133
241
|
yield Button("Deny", id="deny_btn", variant="error")
|
|
134
|
-
|
|
242
|
+
|
|
135
243
|
@on(Button.Pressed, "#allow_btn")
|
|
136
244
|
def allow_tool(self):
|
|
137
245
|
self.tool_use_confirm.on_confirm()
|
|
138
|
-
|
|
246
|
+
|
|
139
247
|
@on(Button.Pressed, "#deny_btn")
|
|
140
248
|
def deny_tool(self):
|
|
141
249
|
self.tool_use_confirm.on_abort()
|
|
@@ -143,11 +251,11 @@ class PermissionRequest(Container):
|
|
|
143
251
|
|
|
144
252
|
class MessageSelector(Container):
|
|
145
253
|
"""Message selector for conversation forking"""
|
|
146
|
-
|
|
147
|
-
def __init__(self, messages: List[
|
|
254
|
+
|
|
255
|
+
def __init__(self, messages: List[MessageData], **kwargs):
|
|
148
256
|
super().__init__(**kwargs)
|
|
149
257
|
self.messages = messages
|
|
150
|
-
|
|
258
|
+
|
|
151
259
|
def compose(self) -> ComposeResult:
|
|
152
260
|
yield Static("📝 Select Message to Fork From", classes="dialog-title")
|
|
153
261
|
with ScrollableContainer():
|
|
@@ -155,8 +263,8 @@ class MessageSelector(Container):
|
|
|
155
263
|
content = self._get_message_preview(message)
|
|
156
264
|
yield Button(f"{i}: {content[:50]}...", id=f"msg_{i}")
|
|
157
265
|
yield Button("Cancel", id="cancel_selector", variant="error")
|
|
158
|
-
|
|
159
|
-
def _get_message_preview(self, message:
|
|
266
|
+
|
|
267
|
+
def _get_message_preview(self, message: MessageData) -> str:
|
|
160
268
|
if isinstance(message.message.content, str):
|
|
161
269
|
return message.message.content
|
|
162
270
|
return str(message.message.content)[:50]
|
|
@@ -167,55 +275,241 @@ class REPL(Container):
|
|
|
167
275
|
Main REPL Component - Python equivalent of React REPL component
|
|
168
276
|
Manages the entire conversation interface with AI assistant
|
|
169
277
|
"""
|
|
278
|
+
|
|
279
|
+
DEFAULT_CSS = """
|
|
280
|
+
/* Message styling */
|
|
281
|
+
.user-label {
|
|
282
|
+
text-style: bold;
|
|
283
|
+
color: blue;
|
|
284
|
+
margin-top: 1;
|
|
285
|
+
margin-bottom: 0;
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
.user-message {
|
|
289
|
+
background: blue 20%;
|
|
290
|
+
color: white;
|
|
291
|
+
margin: 1;
|
|
292
|
+
margin-top: 0;
|
|
293
|
+
padding: 1;
|
|
294
|
+
border-left: solid blue;
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
.assistant-label {
|
|
298
|
+
text-style: bold;
|
|
299
|
+
color: green;
|
|
300
|
+
margin-top: 1;
|
|
301
|
+
margin-bottom: 0;
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
.assistant-message {
|
|
305
|
+
background: green 20%;
|
|
306
|
+
color: white;
|
|
307
|
+
margin: 1;
|
|
308
|
+
margin-top: 0;
|
|
309
|
+
padding: 1;
|
|
310
|
+
border-left: solid green;
|
|
311
|
+
}
|
|
170
312
|
|
|
313
|
+
.assistant-streaming {
|
|
314
|
+
background: yellow 20%;
|
|
315
|
+
color: black;
|
|
316
|
+
margin: 1;
|
|
317
|
+
padding: 1;
|
|
318
|
+
border-left: solid yellow;
|
|
319
|
+
text-style: italic;
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
.assistant-error-label {
|
|
323
|
+
text-style: bold;
|
|
324
|
+
color: red;
|
|
325
|
+
margin-top: 1;
|
|
326
|
+
margin-bottom: 0;
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
.assistant-error {
|
|
330
|
+
background: red 20%;
|
|
331
|
+
color: white;
|
|
332
|
+
margin: 1;
|
|
333
|
+
margin-top: 0;
|
|
334
|
+
padding: 1;
|
|
335
|
+
border-left: solid red;
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
.progress-label {
|
|
339
|
+
text-style: bold;
|
|
340
|
+
color: yellow;
|
|
341
|
+
margin-top: 1;
|
|
342
|
+
margin-bottom: 0;
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
.progress-message {
|
|
346
|
+
background: yellow 20%;
|
|
347
|
+
color: black;
|
|
348
|
+
margin: 1;
|
|
349
|
+
margin-top: 0;
|
|
350
|
+
padding: 1;
|
|
351
|
+
border-left: solid yellow;
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
.dialog-title {
|
|
355
|
+
text-style: bold;
|
|
356
|
+
content-align: center middle;
|
|
357
|
+
margin: 1;
|
|
358
|
+
background: cyan 30%;
|
|
359
|
+
color: black;
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
#messages_container {
|
|
363
|
+
height: 1fr;
|
|
364
|
+
margin: 0;
|
|
365
|
+
scrollbar-background: gray 50%;
|
|
366
|
+
scrollbar-color: white;
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
#main_input {
|
|
370
|
+
width: 1fr;
|
|
371
|
+
margin-right: 1;
|
|
372
|
+
border: solid white;
|
|
373
|
+
dock: bottom;
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
/* PromptInput component styles */
|
|
377
|
+
.model-info {
|
|
378
|
+
|
|
379
|
+
height: 1;
|
|
380
|
+
content-align: right middle;
|
|
381
|
+
color: white;
|
|
382
|
+
margin-bottom: 1;
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
#input_container {
|
|
386
|
+
margin: 1;
|
|
387
|
+
padding: 1;
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
#mode_prefix {
|
|
391
|
+
width: 3;
|
|
392
|
+
content-align: center middle;
|
|
393
|
+
text-style: bold;
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
.mode-bash #mode_prefix {
|
|
397
|
+
color: yellow;
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
.mode-koding #mode_prefix {
|
|
401
|
+
color: cyan;
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
#status_area {
|
|
405
|
+
dock: bottom;
|
|
406
|
+
height: 2;
|
|
407
|
+
margin: 1;
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
.status-message {
|
|
411
|
+
color: white;
|
|
412
|
+
text-style: dim;
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
.model-switch-message {
|
|
416
|
+
color: green;
|
|
417
|
+
text-style: bold;
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
.help-text {
|
|
421
|
+
margin-right: 2;
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
.help-text.active {
|
|
425
|
+
color: white;
|
|
426
|
+
text-style: bold;
|
|
427
|
+
}
|
|
428
|
+
|
|
429
|
+
.help-text.inactive {
|
|
430
|
+
color: gray;
|
|
431
|
+
text-style: dim;
|
|
432
|
+
}
|
|
433
|
+
|
|
434
|
+
Button {
|
|
435
|
+
margin: 1;
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
Input {
|
|
439
|
+
border: solid white;
|
|
440
|
+
}
|
|
441
|
+
|
|
442
|
+
|
|
443
|
+
"""
|
|
444
|
+
|
|
171
445
|
# Reactive properties equivalent to React useState
|
|
172
446
|
fork_number = reactive(0)
|
|
173
|
-
is_loading = reactive(False)
|
|
174
|
-
messages = var(list) # List[
|
|
447
|
+
is_loading = reactive(False) # Recompose when loading state changes
|
|
448
|
+
messages = var(list) # List[MessageData]
|
|
175
449
|
input_value = reactive("")
|
|
176
450
|
input_mode = reactive(InputMode.PROMPT)
|
|
177
451
|
submit_count = reactive(0)
|
|
178
|
-
is_message_selector_visible = reactive(
|
|
179
|
-
|
|
452
|
+
is_message_selector_visible = reactive(
|
|
453
|
+
False, recompose=True
|
|
454
|
+
) # Recompose when selector visibility changes
|
|
455
|
+
show_cost_dialog = reactive(
|
|
456
|
+
False, recompose=True
|
|
457
|
+
) # Recompose when dialog visibility changes
|
|
180
458
|
have_shown_cost_dialog = reactive(False)
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
459
|
+
should_show_prompt_input = reactive(True, recompose=True)
|
|
460
|
+
|
|
461
|
+
def __init__(
|
|
462
|
+
self,
|
|
463
|
+
commands=None,
|
|
464
|
+
safe_mode=False,
|
|
465
|
+
debug=False,
|
|
466
|
+
initial_fork_number=0,
|
|
467
|
+
initial_prompt=None,
|
|
468
|
+
message_log_name="default",
|
|
469
|
+
should_show_prompt_input=True,
|
|
470
|
+
tools=None,
|
|
471
|
+
verbose=False,
|
|
472
|
+
initial_messages=None,
|
|
473
|
+
mcp_clients=None,
|
|
474
|
+
is_default_model=True,
|
|
475
|
+
initial_update_version=None,
|
|
476
|
+
initial_update_commands=None,
|
|
477
|
+
agent=None, # Agent passed from app level
|
|
478
|
+
resume_session_id=None, # Session ID to resume
|
|
479
|
+
continue_last=False, # Continue most recent session
|
|
480
|
+
**kwargs,
|
|
481
|
+
):
|
|
198
482
|
super().__init__(**kwargs)
|
|
199
|
-
|
|
483
|
+
|
|
200
484
|
# Props equivalent to TypeScript Props interface
|
|
201
485
|
self.commands = commands or []
|
|
202
486
|
self.safe_mode = safe_mode
|
|
203
487
|
self.debug = debug
|
|
204
488
|
self.initial_fork_number = initial_fork_number
|
|
205
489
|
self.initial_prompt = initial_prompt
|
|
490
|
+
print(f"DEBUG REPL.__init__: initial_prompt={initial_prompt}")
|
|
206
491
|
self.message_log_name = message_log_name
|
|
207
|
-
self.should_show_prompt_input = should_show_prompt_input
|
|
208
492
|
self.tools = tools or []
|
|
209
493
|
self.verbose = verbose
|
|
210
494
|
self.mcp_clients = mcp_clients or []
|
|
211
495
|
self.is_default_model = is_default_model
|
|
212
496
|
self.initial_update_version = initial_update_version
|
|
213
497
|
self.initial_update_commands = initial_update_commands
|
|
214
|
-
|
|
498
|
+
|
|
499
|
+
# Session management
|
|
500
|
+
self.resume_session_id = resume_session_id
|
|
501
|
+
self.continue_last = continue_last
|
|
502
|
+
self.session: Optional[Session] = None
|
|
503
|
+
|
|
215
504
|
# Initialize state
|
|
216
505
|
self.messages = initial_messages or []
|
|
506
|
+
print(f"DEBUG: REPL initialized with {len(self.messages)} messages")
|
|
217
507
|
self.fork_number = initial_fork_number
|
|
218
|
-
|
|
508
|
+
self.should_show_prompt_input = should_show_prompt_input
|
|
509
|
+
|
|
510
|
+
# Agent from app level
|
|
511
|
+
self.agent = agent
|
|
512
|
+
|
|
219
513
|
# Internal state
|
|
220
514
|
self.config = REPLConfig()
|
|
221
515
|
self.abort_controller: Optional[asyncio.Task] = None
|
|
@@ -223,165 +517,346 @@ class REPL(Container):
|
|
|
223
517
|
self.tool_use_confirm: Optional[ToolUseConfirm] = None
|
|
224
518
|
self.binary_feedback_context: Optional[BinaryFeedbackContext] = None
|
|
225
519
|
self.read_file_timestamps: Dict[str, float] = {}
|
|
226
|
-
self.fork_convo_with_messages_on_next_render: Optional[List[
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
520
|
+
self.fork_convo_with_messages_on_next_render: Optional[List[MessageData]] = None
|
|
521
|
+
|
|
522
|
+
# Output adapter for command execution
|
|
523
|
+
self.output_adapter = TextualOutputAdapter(on_output=self.handle_command_output)
|
|
524
|
+
self.active_dialog: Optional[Container] = None
|
|
525
|
+
|
|
526
|
+
def _create_test_messages(self) -> List[MessageData]:
|
|
527
|
+
"""Create some test messages for development/testing"""
|
|
528
|
+
import time
|
|
529
|
+
|
|
530
|
+
test_messages = []
|
|
531
|
+
|
|
532
|
+
# Welcome message from assistant
|
|
533
|
+
test_messages.append(
|
|
534
|
+
MessageData(
|
|
535
|
+
type=MessageType.ASSISTANT,
|
|
536
|
+
message=MessageContent(
|
|
537
|
+
"👋 Welcome to Minion Code Assistant! I'm here to help you with coding tasks, file operations, and more. What would you like to work on today?"
|
|
538
|
+
),
|
|
539
|
+
timestamp=time.time() - 120,
|
|
540
|
+
options={},
|
|
541
|
+
)
|
|
542
|
+
)
|
|
543
|
+
|
|
544
|
+
# Example user message
|
|
545
|
+
test_messages.append(
|
|
546
|
+
MessageData(
|
|
547
|
+
type=MessageType.USER,
|
|
548
|
+
message=MessageContent(
|
|
549
|
+
"Can you help me understand how to use this REPL interface?"
|
|
550
|
+
),
|
|
551
|
+
timestamp=time.time() - 100,
|
|
552
|
+
options={},
|
|
553
|
+
)
|
|
554
|
+
)
|
|
555
|
+
|
|
556
|
+
# Example assistant response with code
|
|
557
|
+
test_messages.append(
|
|
558
|
+
MessageData(
|
|
559
|
+
type=MessageType.ASSISTANT,
|
|
560
|
+
message=MessageContent(
|
|
561
|
+
"""Absolutely! Here's how to use the REPL interface:
|
|
562
|
+
|
|
563
|
+
## Input Modes
|
|
564
|
+
- **Prompt mode** (`>`): Regular conversation with the AI assistant
|
|
565
|
+
- **Bash mode** (`!`): Execute shell commands directly
|
|
566
|
+
- **Koding mode** (`#`): Add notes or generate content for AGENTS.md
|
|
567
|
+
|
|
568
|
+
## Keyboard Shortcuts
|
|
569
|
+
- `Enter`: Submit your message
|
|
570
|
+
- `Ctrl+Enter`, `Tab`, or `Ctrl+J`: Add a new line
|
|
571
|
+
- `Escape`: Switch modes or show message selector
|
|
572
|
+
- `Shift+M`: Quick model switching
|
|
573
|
+
|
|
574
|
+
## Examples
|
|
575
|
+
```bash
|
|
576
|
+
# Bash mode - execute commands
|
|
577
|
+
!ls -la
|
|
578
|
+
|
|
579
|
+
# Koding mode - add to AGENTS.md
|
|
580
|
+
#Create a new Python function for data processing
|
|
581
|
+
|
|
582
|
+
# Regular prompt
|
|
583
|
+
How do I implement error handling in Python?
|
|
584
|
+
```
|
|
585
|
+
|
|
586
|
+
Try typing something to get started!"""
|
|
587
|
+
),
|
|
588
|
+
timestamp=time.time() - 80,
|
|
589
|
+
options={},
|
|
590
|
+
)
|
|
591
|
+
)
|
|
592
|
+
|
|
593
|
+
return test_messages
|
|
594
|
+
|
|
230
595
|
def compose(self) -> ComposeResult:
|
|
231
596
|
"""Compose the REPL interface - equivalent to React render method"""
|
|
232
|
-
# Static header section (equivalent to Static items in React)
|
|
233
597
|
with Vertical():
|
|
598
|
+
# Logo at the top
|
|
234
599
|
yield Logo(
|
|
235
600
|
mcp_clients=self.mcp_clients,
|
|
236
601
|
is_default_model=self.is_default_model,
|
|
237
|
-
update_banner_version=self.initial_update_version
|
|
602
|
+
update_banner_version=self.initial_update_version,
|
|
238
603
|
)
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
message_log_name=self.message_log_name,
|
|
278
|
-
is_disabled=False,
|
|
279
|
-
is_loading=self.is_loading,
|
|
280
|
-
debug=self.debug,
|
|
281
|
-
verbose=self.verbose,
|
|
282
|
-
messages=self.messages,
|
|
283
|
-
tools=self.tools,
|
|
284
|
-
input_value=self.input_value,
|
|
285
|
-
mode=self.input_mode,
|
|
286
|
-
submit_count=self.submit_count,
|
|
287
|
-
read_file_timestamps=self.read_file_timestamps,
|
|
288
|
-
abort_controller=self.abort_controller
|
|
289
|
-
)
|
|
290
|
-
|
|
291
|
-
# Set up callbacks
|
|
292
|
-
prompt_input.on_query = self.on_query_from_prompt
|
|
293
|
-
prompt_input.on_input_change = self.on_input_change_from_prompt
|
|
294
|
-
prompt_input.on_mode_change = self.on_mode_change_from_prompt
|
|
295
|
-
prompt_input.on_submit_count_change = self.on_submit_count_change_from_prompt
|
|
296
|
-
prompt_input.set_is_loading = self.set_loading_from_prompt
|
|
297
|
-
prompt_input.set_abort_controller = self.set_abort_controller_from_prompt
|
|
298
|
-
prompt_input.on_show_message_selector = self.show_message_selector
|
|
299
|
-
prompt_input.set_fork_convo_with_messages = self.set_fork_convo_messages
|
|
300
|
-
prompt_input.on_model_change = self.on_model_change_from_prompt
|
|
301
|
-
prompt_input.set_tool_jsx = self.set_tool_jsx_from_prompt
|
|
302
|
-
|
|
303
|
-
yield prompt_input
|
|
304
|
-
|
|
604
|
+
|
|
605
|
+
# Messages container (main content area) - takes remaining space
|
|
606
|
+
print(
|
|
607
|
+
f"DEBUG: REPL.compose() creating Messages component with {len(self.messages)} messages"
|
|
608
|
+
)
|
|
609
|
+
yield Messages(
|
|
610
|
+
messages=self.messages,
|
|
611
|
+
tools=self.tools,
|
|
612
|
+
verbose=self.verbose,
|
|
613
|
+
debug=self.debug,
|
|
614
|
+
id="messages_container",
|
|
615
|
+
)
|
|
616
|
+
|
|
617
|
+
# Loading indicator - simple one-line text with animation
|
|
618
|
+
if self.is_loading:
|
|
619
|
+
yield Spinner(message="Assistant is thinking")
|
|
620
|
+
|
|
621
|
+
# Other dynamic content (dialogs, etc.) - also between Messages and PromptInput
|
|
622
|
+
# Tool JSX (equivalent to {toolJSX ? toolJSX.jsx : null})
|
|
623
|
+
if self.tool_jsx and self.tool_jsx.jsx:
|
|
624
|
+
yield self.tool_jsx.jsx
|
|
625
|
+
|
|
626
|
+
# Binary feedback (equivalent to BinaryFeedback component)
|
|
627
|
+
if self.binary_feedback_context and not self.is_message_selector_visible:
|
|
628
|
+
yield Static("🔄 Binary feedback component would render here")
|
|
629
|
+
|
|
630
|
+
# Permission request (equivalent to PermissionRequest component)
|
|
631
|
+
if (
|
|
632
|
+
self.tool_use_confirm
|
|
633
|
+
and not self.is_message_selector_visible
|
|
634
|
+
and not self.binary_feedback_context
|
|
635
|
+
):
|
|
636
|
+
yield PermissionRequest(self.tool_use_confirm)
|
|
637
|
+
|
|
638
|
+
# Cost dialog (equivalent to CostThresholdDialog component)
|
|
639
|
+
if self.show_cost_dialog and not self.is_loading:
|
|
640
|
+
yield CostThresholdDialog()
|
|
641
|
+
|
|
305
642
|
# Message selector (equivalent to {isMessageSelectorVisible && <MessageSelector />})
|
|
306
643
|
if self.is_message_selector_visible:
|
|
307
644
|
yield MessageSelector(messages=self.messages)
|
|
308
|
-
|
|
645
|
+
|
|
646
|
+
# PromptInput component at the bottom (dock: bottom)
|
|
647
|
+
if self.should_show_prompt_input:
|
|
648
|
+
prompt_input = PromptInput(
|
|
649
|
+
commands=self.commands,
|
|
650
|
+
fork_number=self.fork_number,
|
|
651
|
+
message_log_name=self.message_log_name,
|
|
652
|
+
is_disabled=False,
|
|
653
|
+
is_loading=self.is_loading,
|
|
654
|
+
debug=self.debug,
|
|
655
|
+
verbose=self.verbose,
|
|
656
|
+
messages=self.messages,
|
|
657
|
+
tools=self.tools,
|
|
658
|
+
input_value=self.input_value,
|
|
659
|
+
mode=self.input_mode,
|
|
660
|
+
submit_count=self.submit_count,
|
|
661
|
+
read_file_timestamps=self.read_file_timestamps,
|
|
662
|
+
abort_controller=self.abort_controller,
|
|
663
|
+
)
|
|
664
|
+
|
|
665
|
+
# Set up callbacks
|
|
666
|
+
prompt_input.on_query = self.on_query_from_prompt
|
|
667
|
+
prompt_input.on_add_user_message = (
|
|
668
|
+
self.on_add_user_message_from_prompt
|
|
669
|
+
) # New immediate display callback
|
|
670
|
+
prompt_input.on_input_change = self.on_input_change_from_prompt
|
|
671
|
+
prompt_input.on_mode_change = self.on_mode_change_from_prompt
|
|
672
|
+
prompt_input.on_submit_count_change = (
|
|
673
|
+
self.on_submit_count_change_from_prompt
|
|
674
|
+
)
|
|
675
|
+
prompt_input.set_is_loading = self.set_loading_from_prompt
|
|
676
|
+
prompt_input.set_abort_controller = (
|
|
677
|
+
self.set_abort_controller_from_prompt
|
|
678
|
+
)
|
|
679
|
+
prompt_input.on_show_message_selector = self.show_message_selector
|
|
680
|
+
prompt_input.set_fork_convo_with_messages = self.set_fork_convo_messages
|
|
681
|
+
prompt_input.on_model_change = self.on_model_change_from_prompt
|
|
682
|
+
prompt_input.set_tool_jsx = self.set_tool_jsx_from_prompt
|
|
683
|
+
prompt_input.on_execute_command = (
|
|
684
|
+
self.on_execute_command_from_prompt
|
|
685
|
+
) # Command execution callback
|
|
686
|
+
|
|
687
|
+
yield prompt_input
|
|
688
|
+
|
|
309
689
|
def on_mount(self):
|
|
310
690
|
"""Component lifecycle - equivalent to React useEffect(() => { onInit() }, [])"""
|
|
311
|
-
|
|
691
|
+
# Initialize session
|
|
692
|
+
self._init_session()
|
|
312
693
|
self.call_later(self.on_init)
|
|
313
694
|
# Set focus to the input after a short delay to ensure it's mounted
|
|
314
695
|
self.set_timer(0.1, self._set_focus_to_input)
|
|
315
|
-
|
|
696
|
+
|
|
697
|
+
def _init_session(self):
|
|
698
|
+
"""Initialize or restore session."""
|
|
699
|
+
current_project = os.getcwd()
|
|
700
|
+
|
|
701
|
+
# Try to restore session if requested
|
|
702
|
+
if self.resume_session_id:
|
|
703
|
+
self.session = load_session(self.resume_session_id)
|
|
704
|
+
if self.session:
|
|
705
|
+
print(
|
|
706
|
+
f"DEBUG: Restored session {self.resume_session_id} with {len(self.session.messages)} messages"
|
|
707
|
+
)
|
|
708
|
+
# Restore messages to UI
|
|
709
|
+
self._restore_ui_messages_from_session()
|
|
710
|
+
else:
|
|
711
|
+
print(
|
|
712
|
+
f"DEBUG: Session {self.resume_session_id} not found, creating new"
|
|
713
|
+
)
|
|
714
|
+
self.session = create_session(current_project)
|
|
715
|
+
elif self.continue_last:
|
|
716
|
+
latest_id = get_latest_session_id(project_path=current_project)
|
|
717
|
+
if latest_id:
|
|
718
|
+
self.session = load_session(latest_id)
|
|
719
|
+
if self.session:
|
|
720
|
+
print(
|
|
721
|
+
f"DEBUG: Continuing session {latest_id} with {len(self.session.messages)} messages"
|
|
722
|
+
)
|
|
723
|
+
# Restore messages to UI
|
|
724
|
+
self._restore_ui_messages_from_session()
|
|
725
|
+
else:
|
|
726
|
+
self.session = create_session(current_project)
|
|
727
|
+
else:
|
|
728
|
+
self.session = create_session(current_project)
|
|
729
|
+
else:
|
|
730
|
+
# Create new session
|
|
731
|
+
self.session = create_session(current_project)
|
|
732
|
+
print(f"DEBUG: Created new session {self.session.metadata.session_id}")
|
|
733
|
+
|
|
734
|
+
def _restore_ui_messages_from_session(self):
|
|
735
|
+
"""Restore UI messages from session."""
|
|
736
|
+
if not self.session or not self.session.messages:
|
|
737
|
+
return
|
|
738
|
+
|
|
739
|
+
# Clear existing UI messages first
|
|
740
|
+
self.messages.clear()
|
|
741
|
+
|
|
742
|
+
# Convert session messages to MessageData for UI display
|
|
743
|
+
for msg in self.session.messages:
|
|
744
|
+
msg_type = MessageType.USER if msg.role == "user" else MessageType.ASSISTANT
|
|
745
|
+
ui_message = MessageData(
|
|
746
|
+
type=msg_type, message=MessageContent(msg.content), options={}
|
|
747
|
+
)
|
|
748
|
+
self.messages.append(ui_message)
|
|
749
|
+
|
|
750
|
+
print(f"DEBUG: Restored {len(self.session.messages)} messages to UI")
|
|
751
|
+
|
|
752
|
+
def _save_message_to_session(self, role: str, content: str):
|
|
753
|
+
"""Save a message to the current session."""
|
|
754
|
+
if self.session:
|
|
755
|
+
session_add_message(self.session, role, content, auto_save=True)
|
|
756
|
+
if self.verbose:
|
|
757
|
+
print(
|
|
758
|
+
f"DEBUG: Saved {role} message to session {self.session.metadata.session_id}"
|
|
759
|
+
)
|
|
760
|
+
|
|
316
761
|
def _set_focus_to_input(self):
|
|
317
762
|
"""Set focus to the main input widget"""
|
|
318
763
|
try:
|
|
319
|
-
# Try to find the
|
|
320
|
-
input_widget = self.query_one("#
|
|
764
|
+
# Try to find the main TextArea input
|
|
765
|
+
input_widget = self.query_one("#main_input", expect_type=TextArea)
|
|
321
766
|
input_widget.focus()
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
logger.warning(f"Could not set focus to simple input: {e}")
|
|
325
|
-
# If that fails, try to focus any Input widget
|
|
767
|
+
except Exception:
|
|
768
|
+
# If that fails, try to focus any TextArea or Input widget
|
|
326
769
|
try:
|
|
327
|
-
|
|
328
|
-
if
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
770
|
+
text_areas = self.query("TextArea")
|
|
771
|
+
if text_areas:
|
|
772
|
+
text_areas[0].focus()
|
|
773
|
+
else:
|
|
774
|
+
inputs = self.query("Input")
|
|
775
|
+
if inputs:
|
|
776
|
+
inputs[0].focus()
|
|
777
|
+
except Exception:
|
|
778
|
+
pass # Silently handle focus errors
|
|
779
|
+
|
|
334
780
|
async def on_init(self):
|
|
335
781
|
"""Initialize REPL - equivalent to React onInit function"""
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
782
|
+
# Initial prompt processing moved to set_agent to ensure agent is ready
|
|
783
|
+
pass
|
|
784
|
+
|
|
785
|
+
@work(exclusive=False)
|
|
786
|
+
async def _start_initial_prompt_worker(self):
|
|
787
|
+
"""Worker to process initial prompt."""
|
|
788
|
+
print("DEBUG: _start_initial_prompt_worker started")
|
|
789
|
+
await self._process_initial_prompt()
|
|
790
|
+
|
|
791
|
+
async def _process_initial_prompt(self):
|
|
792
|
+
"""Process the initial prompt after agent is ready."""
|
|
793
|
+
print(
|
|
794
|
+
f"DEBUG _process_initial_prompt called: prompt={self.initial_prompt}, agent={self.agent}"
|
|
795
|
+
)
|
|
796
|
+
if not self.initial_prompt or not self.agent:
|
|
797
|
+
print("DEBUG: Skipping - no prompt or no agent")
|
|
340
798
|
return
|
|
341
|
-
|
|
799
|
+
|
|
342
800
|
self.is_loading = True
|
|
343
|
-
|
|
801
|
+
prompt_to_process = self.initial_prompt
|
|
802
|
+
# Clear immediately to prevent re-processing
|
|
803
|
+
self.initial_prompt = None
|
|
804
|
+
|
|
344
805
|
try:
|
|
806
|
+
print(f"DEBUG: Processing prompt: {prompt_to_process}")
|
|
345
807
|
# Process initial prompt (equivalent to processUserInput)
|
|
346
808
|
new_messages = await self.process_user_input(
|
|
347
|
-
self.
|
|
348
|
-
self.input_mode
|
|
809
|
+
prompt_to_process, self.input_mode
|
|
349
810
|
)
|
|
350
|
-
|
|
811
|
+
print(f"DEBUG: Got {len(new_messages) if new_messages else 0} new messages")
|
|
812
|
+
|
|
351
813
|
if new_messages:
|
|
352
814
|
# Add to history (equivalent to addToHistory)
|
|
353
|
-
self.add_to_history(
|
|
354
|
-
|
|
815
|
+
self.add_to_history(prompt_to_process)
|
|
816
|
+
|
|
355
817
|
# Update messages (equivalent to setMessages)
|
|
356
818
|
self.messages = [*self.messages, *new_messages]
|
|
357
|
-
|
|
358
|
-
#
|
|
819
|
+
|
|
820
|
+
# Update UI
|
|
821
|
+
try:
|
|
822
|
+
messages_component = self.query_one(
|
|
823
|
+
"#messages_container", expect_type=Messages
|
|
824
|
+
)
|
|
825
|
+
messages_component.update_messages(self.messages)
|
|
826
|
+
except Exception:
|
|
827
|
+
self.refresh()
|
|
828
|
+
|
|
829
|
+
# Query API (equivalent to query function)
|
|
830
|
+
print("DEBUG: Calling query_api")
|
|
359
831
|
await self.query_api(new_messages)
|
|
360
|
-
|
|
832
|
+
print("DEBUG: query_api completed")
|
|
833
|
+
|
|
361
834
|
except Exception as e:
|
|
362
|
-
|
|
835
|
+
print(f"DEBUG: Error processing initial prompt: {e}")
|
|
836
|
+
import traceback
|
|
837
|
+
|
|
838
|
+
traceback.print_exc()
|
|
363
839
|
finally:
|
|
364
840
|
self.is_loading = False
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
841
|
+
|
|
842
|
+
async def process_user_input(
|
|
843
|
+
self, input_text: str, mode: InputMode
|
|
844
|
+
) -> List[MessageData]:
|
|
368
845
|
"""Process user input - equivalent to processUserInput function"""
|
|
369
|
-
|
|
370
|
-
|
|
846
|
+
|
|
371
847
|
# Create user message
|
|
372
|
-
user_message =
|
|
848
|
+
user_message = MessageData(
|
|
373
849
|
type=MessageType.USER,
|
|
374
850
|
message=MessageContent(input_text),
|
|
375
|
-
options={"isKodingRequest": mode == InputMode.KODING}
|
|
851
|
+
options={"isKodingRequest": mode == InputMode.KODING},
|
|
376
852
|
)
|
|
377
|
-
|
|
853
|
+
|
|
378
854
|
# Handle different input modes
|
|
379
855
|
if mode == InputMode.BASH:
|
|
380
856
|
# Handle bash command
|
|
381
857
|
result = await self.execute_bash_command(input_text)
|
|
382
|
-
assistant_message =
|
|
383
|
-
type=MessageType.ASSISTANT,
|
|
384
|
-
message=MessageContent(result)
|
|
858
|
+
assistant_message = MessageData(
|
|
859
|
+
type=MessageType.ASSISTANT, message=MessageContent(result)
|
|
385
860
|
)
|
|
386
861
|
return [user_message, assistant_message]
|
|
387
862
|
elif mode == InputMode.KODING:
|
|
@@ -390,17 +865,14 @@ class REPL(Container):
|
|
|
390
865
|
else:
|
|
391
866
|
# Handle regular prompt
|
|
392
867
|
return [user_message]
|
|
393
|
-
|
|
868
|
+
|
|
394
869
|
async def execute_bash_command(self, command: str) -> str:
|
|
395
870
|
"""Execute bash command - simplified version"""
|
|
396
871
|
try:
|
|
397
872
|
import subprocess
|
|
873
|
+
|
|
398
874
|
result = subprocess.run(
|
|
399
|
-
command,
|
|
400
|
-
shell=True,
|
|
401
|
-
capture_output=True,
|
|
402
|
-
text=True,
|
|
403
|
-
timeout=30
|
|
875
|
+
command, shell=True, capture_output=True, text=True, timeout=30
|
|
404
876
|
)
|
|
405
877
|
if result.returncode == 0:
|
|
406
878
|
return result.stdout or "Command executed successfully"
|
|
@@ -408,36 +880,408 @@ class REPL(Container):
|
|
|
408
880
|
return f"Error: {result.stderr}"
|
|
409
881
|
except Exception as e:
|
|
410
882
|
return f"Error executing command: {str(e)}"
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
"""
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
#
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
883
|
+
|
|
884
|
+
def set_agent(self, agent):
|
|
885
|
+
"""Set agent from app level and bind output adapter"""
|
|
886
|
+
print(f"DEBUG set_agent: agent={agent}, initial_prompt={self.initial_prompt}")
|
|
887
|
+
self.agent = agent
|
|
888
|
+
# Bind output adapter to agent if it supports confirmation
|
|
889
|
+
if hasattr(agent, "set_output_adapter"):
|
|
890
|
+
agent.set_output_adapter(self.output_adapter)
|
|
891
|
+
|
|
892
|
+
# Restore agent history from session if resuming
|
|
893
|
+
if self.session and self.session.messages:
|
|
894
|
+
restore_agent_history(agent, self.session, self.verbose)
|
|
895
|
+
|
|
896
|
+
# Process initial prompt now that agent is ready
|
|
897
|
+
if self.initial_prompt:
|
|
898
|
+
print(f"DEBUG: Triggering initial prompt processing: {self.initial_prompt}")
|
|
899
|
+
# Start the worker to process initial prompt
|
|
900
|
+
self._start_initial_prompt_worker()
|
|
901
|
+
|
|
902
|
+
def handle_command_output(self, output_type: str, data: dict):
|
|
903
|
+
"""Handle output from command execution via adapter"""
|
|
904
|
+
if output_type == "panel":
|
|
905
|
+
self.display_panel_output(data)
|
|
906
|
+
elif output_type == "table":
|
|
907
|
+
self.display_table_output(data)
|
|
908
|
+
elif output_type == "text":
|
|
909
|
+
self.display_text_output(data)
|
|
910
|
+
elif output_type == "confirm":
|
|
911
|
+
self.show_confirm_dialog(data)
|
|
912
|
+
elif output_type == "choice":
|
|
913
|
+
self.show_choice_dialog(data)
|
|
914
|
+
elif output_type == "input":
|
|
915
|
+
self.show_input_dialog(data)
|
|
916
|
+
|
|
917
|
+
def display_panel_output(self, data: dict):
|
|
918
|
+
"""Display panel output as a message"""
|
|
919
|
+
content = (
|
|
920
|
+
f"{data.get('title', '')}\n\n{data['content']}"
|
|
921
|
+
if data.get("title")
|
|
922
|
+
else data["content"]
|
|
923
|
+
)
|
|
924
|
+
message = MessageData(
|
|
925
|
+
type=MessageType.ASSISTANT,
|
|
926
|
+
message=MessageContent(content),
|
|
927
|
+
options={"border_style": data.get("border_style", "blue")},
|
|
928
|
+
)
|
|
929
|
+
self.messages = [*self.messages, message]
|
|
930
|
+
self._refresh_messages()
|
|
931
|
+
|
|
932
|
+
def display_table_output(self, data: dict):
|
|
933
|
+
"""Display table output as formatted text"""
|
|
934
|
+
# Format table as text
|
|
935
|
+
headers = data.get("headers", [])
|
|
936
|
+
rows = data.get("rows", [])
|
|
937
|
+
title = data.get("title", "")
|
|
938
|
+
|
|
939
|
+
lines = []
|
|
940
|
+
if title:
|
|
941
|
+
lines.append(f"=== {title} ===\n")
|
|
942
|
+
|
|
943
|
+
if headers:
|
|
944
|
+
lines.append(" | ".join(headers))
|
|
945
|
+
lines.append("-" * (len(" | ".join(headers))))
|
|
946
|
+
|
|
947
|
+
for row in rows:
|
|
948
|
+
lines.append(" | ".join(str(cell) for cell in row))
|
|
949
|
+
|
|
950
|
+
content = "\n".join(lines)
|
|
951
|
+
message = MessageData(
|
|
952
|
+
type=MessageType.ASSISTANT, message=MessageContent(content), options={}
|
|
953
|
+
)
|
|
954
|
+
self.messages = [*self.messages, message]
|
|
955
|
+
self._refresh_messages()
|
|
956
|
+
|
|
957
|
+
def display_text_output(self, data: dict):
|
|
958
|
+
"""Display plain text output"""
|
|
959
|
+
message = MessageData(
|
|
960
|
+
type=MessageType.ASSISTANT,
|
|
961
|
+
message=MessageContent(data["content"]),
|
|
962
|
+
options={"style": data.get("style", "")},
|
|
963
|
+
)
|
|
964
|
+
self.messages = [*self.messages, message]
|
|
965
|
+
self._refresh_messages()
|
|
966
|
+
|
|
967
|
+
def show_confirm_dialog(self, data: dict):
|
|
968
|
+
"""Show confirmation dialog"""
|
|
969
|
+
if self.active_dialog:
|
|
970
|
+
self.active_dialog.remove()
|
|
971
|
+
|
|
972
|
+
self.active_dialog = ConfirmDialog(
|
|
973
|
+
interaction_id=data["interaction_id"],
|
|
974
|
+
message=data["message"],
|
|
975
|
+
title=data.get("title", "Confirm"),
|
|
976
|
+
ok_text=data.get("ok_text", "Yes"),
|
|
977
|
+
cancel_text=data.get("cancel_text", "No"),
|
|
978
|
+
on_result=self.handle_confirm_result,
|
|
979
|
+
)
|
|
980
|
+
self.mount(self.active_dialog)
|
|
981
|
+
|
|
982
|
+
def show_choice_dialog(self, data: dict):
|
|
983
|
+
"""Show choice selection dialog"""
|
|
984
|
+
if self.active_dialog:
|
|
985
|
+
self.active_dialog.remove()
|
|
986
|
+
|
|
987
|
+
self.active_dialog = ChoiceDialog(
|
|
988
|
+
interaction_id=data["interaction_id"],
|
|
989
|
+
message=data["message"],
|
|
990
|
+
choices=data["choices"],
|
|
991
|
+
title=data.get("title", "Select"),
|
|
992
|
+
on_result=self.handle_choice_result,
|
|
993
|
+
)
|
|
994
|
+
self.mount(self.active_dialog)
|
|
995
|
+
|
|
996
|
+
def show_input_dialog(self, data: dict):
|
|
997
|
+
"""Show text input dialog"""
|
|
998
|
+
if self.active_dialog:
|
|
999
|
+
self.active_dialog.remove()
|
|
1000
|
+
|
|
1001
|
+
self.active_dialog = InputDialog(
|
|
1002
|
+
interaction_id=data["interaction_id"],
|
|
1003
|
+
message=data["message"],
|
|
1004
|
+
title=data.get("title", "Input"),
|
|
1005
|
+
default=data.get("default", ""),
|
|
1006
|
+
placeholder=data.get("placeholder", ""),
|
|
1007
|
+
on_result=self.handle_input_result,
|
|
1008
|
+
)
|
|
1009
|
+
self.mount(self.active_dialog)
|
|
1010
|
+
|
|
1011
|
+
def handle_confirm_result(self, interaction_id: str, result: bool):
|
|
1012
|
+
"""Handle confirmation dialog result"""
|
|
1013
|
+
self.output_adapter.resolve_interaction(interaction_id, result)
|
|
1014
|
+
self.active_dialog = None
|
|
1015
|
+
|
|
1016
|
+
def handle_choice_result(self, interaction_id: str, result: int):
|
|
1017
|
+
"""Handle choice dialog result"""
|
|
1018
|
+
self.output_adapter.resolve_interaction(interaction_id, result)
|
|
1019
|
+
self.active_dialog = None
|
|
1020
|
+
|
|
1021
|
+
def handle_input_result(self, interaction_id: str, result: Optional[str]):
|
|
1022
|
+
"""Handle input dialog result"""
|
|
1023
|
+
self.output_adapter.resolve_interaction(interaction_id, result)
|
|
1024
|
+
self.active_dialog = None
|
|
1025
|
+
|
|
1026
|
+
def _refresh_messages(self):
|
|
1027
|
+
"""Helper to refresh messages component"""
|
|
1028
|
+
try:
|
|
1029
|
+
messages_component = self.query_one("#messages_container", Messages)
|
|
1030
|
+
messages_component.update_messages(self.messages)
|
|
1031
|
+
except Exception:
|
|
1032
|
+
self.refresh()
|
|
1033
|
+
|
|
1034
|
+
async def query_api(self, new_messages: List[MessageData]):
|
|
1035
|
+
"""Query the AI API with streaming support - equivalent to query function"""
|
|
1036
|
+
|
|
1037
|
+
if not new_messages or new_messages[-1].type != MessageType.USER:
|
|
1038
|
+
return
|
|
1039
|
+
|
|
1040
|
+
user_content = new_messages[-1].message.content
|
|
1041
|
+
|
|
1042
|
+
# Check if agent is available
|
|
1043
|
+
if not self.agent:
|
|
1044
|
+
error_message = MessageData(
|
|
425
1045
|
type=MessageType.ASSISTANT,
|
|
426
|
-
message=MessageContent(
|
|
1046
|
+
message=MessageContent("❌ Agent not initialized yet. Please wait..."),
|
|
1047
|
+
options={"error": True},
|
|
427
1048
|
)
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
#
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
1049
|
+
self.messages = [*self.messages, error_message]
|
|
1050
|
+
return
|
|
1051
|
+
|
|
1052
|
+
try:
|
|
1053
|
+
# Set loading state with immediate UI feedback
|
|
1054
|
+
self.is_loading = True
|
|
1055
|
+
|
|
1056
|
+
# Add a temporary "thinking" message to show immediate feedback
|
|
1057
|
+
thinking_message = MessageData(
|
|
1058
|
+
type=MessageType.ASSISTANT,
|
|
1059
|
+
message=MessageContent("🤔 Thinking..."),
|
|
1060
|
+
options={"streaming": True, "temporary": True},
|
|
1061
|
+
)
|
|
1062
|
+
self.messages = [*self.messages, thinking_message]
|
|
1063
|
+
|
|
1064
|
+
# Update Messages component immediately
|
|
1065
|
+
try:
|
|
1066
|
+
messages_component = self.query_one(
|
|
1067
|
+
"#messages_container", expect_type=Messages
|
|
1068
|
+
)
|
|
1069
|
+
messages_component.update_messages(self.messages)
|
|
1070
|
+
except Exception:
|
|
1071
|
+
self.refresh() # Fallback to full refresh
|
|
1072
|
+
|
|
1073
|
+
# Process with agent - check if it supports streaming
|
|
1074
|
+
try:
|
|
1075
|
+
if hasattr(self.agent, "run_async"):
|
|
1076
|
+
# Try to use streaming if supported
|
|
1077
|
+
try:
|
|
1078
|
+
# Attempt streaming response with granular event handling
|
|
1079
|
+
response_content = ""
|
|
1080
|
+
current_status = ""
|
|
1081
|
+
|
|
1082
|
+
async for chunk in await self.agent.run_async(
|
|
1083
|
+
user_content, stream=True
|
|
1084
|
+
):
|
|
1085
|
+
chunk_type = getattr(chunk, "chunk_type", "text")
|
|
1086
|
+
chunk_content = getattr(chunk, "content", str(chunk))
|
|
1087
|
+
chunk_metadata = getattr(chunk, "metadata", {})
|
|
1088
|
+
|
|
1089
|
+
# Handle different chunk types
|
|
1090
|
+
if chunk_type == "step_start":
|
|
1091
|
+
# Show step indicator
|
|
1092
|
+
current_status = f"🔄 {chunk_content}"
|
|
1093
|
+
status_message = MessageData(
|
|
1094
|
+
type=MessageType.PROGRESS,
|
|
1095
|
+
message=MessageContent(current_status),
|
|
1096
|
+
options={"streaming": True, "step_start": True},
|
|
1097
|
+
)
|
|
1098
|
+
self.messages = [*self.messages[:-1], status_message]
|
|
1099
|
+
|
|
1100
|
+
elif chunk_type == "thinking":
|
|
1101
|
+
# Accumulate thinking content (LLM response)
|
|
1102
|
+
response_content += chunk_content
|
|
1103
|
+
streaming_message = MessageData(
|
|
1104
|
+
type=MessageType.ASSISTANT,
|
|
1105
|
+
message=MessageContent(
|
|
1106
|
+
f"{current_status}\n\n{response_content}"
|
|
1107
|
+
if current_status
|
|
1108
|
+
else response_content
|
|
1109
|
+
),
|
|
1110
|
+
options={"streaming": True},
|
|
1111
|
+
)
|
|
1112
|
+
self.messages = [*self.messages[:-1], streaming_message]
|
|
1113
|
+
|
|
1114
|
+
elif chunk_type == "code_start":
|
|
1115
|
+
# Show code execution indicator
|
|
1116
|
+
code_preview = chunk_metadata.get(
|
|
1117
|
+
"code_preview", chunk_content[:100]
|
|
1118
|
+
)
|
|
1119
|
+
exec_status = f"⚙️ Executing code...\n```python\n{code_preview}\n```"
|
|
1120
|
+
code_message = MessageData(
|
|
1121
|
+
type=MessageType.PROGRESS,
|
|
1122
|
+
message=MessageContent(
|
|
1123
|
+
f"{response_content}\n\n{exec_status}"
|
|
1124
|
+
),
|
|
1125
|
+
options={"streaming": True, "code_executing": True},
|
|
1126
|
+
)
|
|
1127
|
+
self.messages = [*self.messages[:-1], code_message]
|
|
1128
|
+
|
|
1129
|
+
elif chunk_type == "code_result":
|
|
1130
|
+
# Show code execution result
|
|
1131
|
+
success = chunk_metadata.get("success", True)
|
|
1132
|
+
if success:
|
|
1133
|
+
result_status = (
|
|
1134
|
+
f"✅ Code executed:\n{chunk_content}"
|
|
1135
|
+
)
|
|
1136
|
+
else:
|
|
1137
|
+
result_status = (
|
|
1138
|
+
f"❌ Execution error:\n{chunk_content}"
|
|
1139
|
+
)
|
|
1140
|
+
|
|
1141
|
+
result_message = MessageData(
|
|
1142
|
+
type=MessageType.ASSISTANT,
|
|
1143
|
+
message=MessageContent(
|
|
1144
|
+
f"{response_content}\n\n{result_status}"
|
|
1145
|
+
),
|
|
1146
|
+
options={"streaming": True, "code_result": True},
|
|
1147
|
+
)
|
|
1148
|
+
self.messages = [*self.messages[:-1], result_message]
|
|
1149
|
+
|
|
1150
|
+
elif chunk_type in (
|
|
1151
|
+
"agent_response",
|
|
1152
|
+
"final_answer",
|
|
1153
|
+
"completion",
|
|
1154
|
+
):
|
|
1155
|
+
# Final response - extract answer
|
|
1156
|
+
final_content = (
|
|
1157
|
+
getattr(chunk, "answer", chunk_content)
|
|
1158
|
+
or chunk_content
|
|
1159
|
+
)
|
|
1160
|
+
response_content = str(final_content)
|
|
1161
|
+
final_message = MessageData(
|
|
1162
|
+
type=MessageType.ASSISTANT,
|
|
1163
|
+
message=MessageContent(response_content),
|
|
1164
|
+
options={"streaming": True},
|
|
1165
|
+
)
|
|
1166
|
+
self.messages = [*self.messages[:-1], final_message]
|
|
1167
|
+
|
|
1168
|
+
else:
|
|
1169
|
+
# Default: accumulate as text
|
|
1170
|
+
response_content += chunk_content
|
|
1171
|
+
streaming_message = MessageData(
|
|
1172
|
+
type=MessageType.ASSISTANT,
|
|
1173
|
+
message=MessageContent(response_content),
|
|
1174
|
+
options={"streaming": True},
|
|
1175
|
+
)
|
|
1176
|
+
self.messages = [*self.messages[:-1], streaming_message]
|
|
1177
|
+
|
|
1178
|
+
# Update UI
|
|
1179
|
+
try:
|
|
1180
|
+
messages_component = self.query_one(
|
|
1181
|
+
"#messages_container", expect_type=Messages
|
|
1182
|
+
)
|
|
1183
|
+
messages_component.update_messages(self.messages)
|
|
1184
|
+
except Exception:
|
|
1185
|
+
self.refresh() # Fallback to full refresh
|
|
1186
|
+
|
|
1187
|
+
# Finalize the streaming message
|
|
1188
|
+
final_message = MessageData(
|
|
1189
|
+
type=MessageType.ASSISTANT,
|
|
1190
|
+
message=MessageContent(response_content),
|
|
1191
|
+
options={}, # Remove streaming flag
|
|
1192
|
+
)
|
|
1193
|
+
self.messages = [*self.messages[:-1], final_message]
|
|
1194
|
+
messages_component = self.query_one(
|
|
1195
|
+
"#messages_container", expect_type=Messages
|
|
1196
|
+
)
|
|
1197
|
+
messages_component.update_messages(self.messages)
|
|
1198
|
+
|
|
1199
|
+
# Save assistant response to session
|
|
1200
|
+
if response_content:
|
|
1201
|
+
self._save_message_to_session("assistant", response_content)
|
|
1202
|
+
|
|
1203
|
+
except Exception as e:
|
|
1204
|
+
raise
|
|
1205
|
+
else:
|
|
1206
|
+
# Agent doesn't support async, show error
|
|
1207
|
+
error_message = MessageData(
|
|
1208
|
+
type=MessageType.ASSISTANT,
|
|
1209
|
+
message=MessageContent(
|
|
1210
|
+
"❌ Agent does not support async operations"
|
|
1211
|
+
),
|
|
1212
|
+
options={"error": True},
|
|
1213
|
+
)
|
|
1214
|
+
self.messages = [*self.messages[:-1], error_message]
|
|
1215
|
+
|
|
1216
|
+
# Handle Koding mode special case
|
|
1217
|
+
if new_messages[-1].options and new_messages[-1].options.get(
|
|
1218
|
+
"isKodingRequest"
|
|
1219
|
+
):
|
|
1220
|
+
await self.handle_koding_response(self.messages[-1])
|
|
1221
|
+
|
|
1222
|
+
except Exception as e:
|
|
1223
|
+
# Format error message for UI display
|
|
1224
|
+
error_text = self._format_error_for_ui(e)
|
|
1225
|
+
|
|
1226
|
+
error_message = MessageData(
|
|
1227
|
+
type=MessageType.ASSISTANT,
|
|
1228
|
+
message=MessageContent(error_text),
|
|
1229
|
+
options={"error": True},
|
|
1230
|
+
)
|
|
1231
|
+
# Replace thinking message with error
|
|
1232
|
+
self.messages = [*self.messages[:-1], error_message]
|
|
1233
|
+
|
|
1234
|
+
# Update Messages component
|
|
1235
|
+
try:
|
|
1236
|
+
messages_component = self.query_one(
|
|
1237
|
+
"#messages_container", expect_type=Messages
|
|
1238
|
+
)
|
|
1239
|
+
messages_component.update_messages(self.messages)
|
|
1240
|
+
except Exception:
|
|
1241
|
+
self.refresh() # Fallback to full refresh
|
|
1242
|
+
|
|
1243
|
+
except Exception as e:
|
|
1244
|
+
# Show error message to user
|
|
1245
|
+
error_text = self._format_error_for_ui(e)
|
|
1246
|
+
error_message = MessageData(
|
|
1247
|
+
type=MessageType.ASSISTANT,
|
|
1248
|
+
message=MessageContent(error_text),
|
|
1249
|
+
options={"error": True},
|
|
1250
|
+
)
|
|
1251
|
+
|
|
1252
|
+
# Replace thinking/streaming message with error
|
|
1253
|
+
if self.messages and (
|
|
1254
|
+
self.messages[-1].options.get("streaming")
|
|
1255
|
+
or self.messages[-1].options.get("temporary")
|
|
1256
|
+
):
|
|
1257
|
+
self.messages = [*self.messages[:-1], error_message]
|
|
1258
|
+
else:
|
|
1259
|
+
self.messages = [*self.messages, error_message]
|
|
1260
|
+
|
|
1261
|
+
# Update Messages component
|
|
1262
|
+
try:
|
|
1263
|
+
messages_component = self.query_one(
|
|
1264
|
+
"#messages_container", expect_type=Messages
|
|
1265
|
+
)
|
|
1266
|
+
messages_component.update_messages(self.messages)
|
|
1267
|
+
except Exception:
|
|
1268
|
+
self.refresh() # Fallback to full refresh
|
|
1269
|
+
|
|
1270
|
+
finally:
|
|
1271
|
+
self.is_loading = False
|
|
1272
|
+
|
|
1273
|
+
# Final UI update to remove loading indicators
|
|
1274
|
+
try:
|
|
1275
|
+
messages_component = self.query_one(
|
|
1276
|
+
"#messages_container", expect_type=Messages
|
|
1277
|
+
)
|
|
1278
|
+
messages_component.update_messages(self.messages)
|
|
1279
|
+
except Exception:
|
|
1280
|
+
self.refresh() # Fallback to full refresh
|
|
1281
|
+
|
|
1282
|
+
async def handle_koding_response(self, assistant_message: MessageData):
|
|
438
1283
|
"""Handle Koding mode response - equivalent to handleHashCommand"""
|
|
439
|
-
|
|
440
|
-
|
|
1284
|
+
|
|
441
1285
|
content = assistant_message.message.content
|
|
442
1286
|
if isinstance(content, str) and content.strip():
|
|
443
1287
|
# Save to AGENTS.md (equivalent to handleHashCommand)
|
|
@@ -445,147 +1289,343 @@ class REPL(Container):
|
|
|
445
1289
|
agents_md_path = Path("AGENTS.md")
|
|
446
1290
|
if agents_md_path.exists():
|
|
447
1291
|
with open(agents_md_path, "a") as f:
|
|
448
|
-
f.write(
|
|
1292
|
+
f.write(
|
|
1293
|
+
f"\n\n## Response - {time.strftime('%Y-%m-%d %H:%M:%S')}\n\n"
|
|
1294
|
+
)
|
|
449
1295
|
f.write(content)
|
|
450
1296
|
f.write("\n")
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
1297
|
+
except Exception:
|
|
1298
|
+
pass # Silently handle file write errors
|
|
1299
|
+
|
|
455
1300
|
def add_to_history(self, command: str):
|
|
456
1301
|
"""Add command to history - equivalent to addToHistory"""
|
|
457
1302
|
# This would integrate with the history system
|
|
458
|
-
|
|
459
|
-
|
|
1303
|
+
pass
|
|
1304
|
+
|
|
460
1305
|
def on_cancel(self):
|
|
461
1306
|
"""Cancel current operation - equivalent to onCancel function"""
|
|
462
1307
|
if not self.is_loading:
|
|
463
1308
|
return
|
|
464
|
-
|
|
1309
|
+
|
|
465
1310
|
self.is_loading = False
|
|
466
|
-
|
|
1311
|
+
self.loading = False
|
|
1312
|
+
|
|
467
1313
|
if self.tool_use_confirm:
|
|
468
1314
|
self.tool_use_confirm.on_abort()
|
|
469
1315
|
elif self.abort_controller:
|
|
470
1316
|
self.abort_controller.cancel()
|
|
471
|
-
|
|
472
|
-
logger.info("Operation cancelled")
|
|
473
|
-
|
|
1317
|
+
|
|
474
1318
|
# Callback methods for PromptInput component
|
|
475
|
-
|
|
476
|
-
"""Handle
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
1319
|
+
def on_add_user_message_from_prompt(self, user_message: MessageData):
|
|
1320
|
+
"""Handle immediate user message display (synchronous)"""
|
|
1321
|
+
# 立即显示用户消息 - 同步操作,不等待任何异步处理
|
|
1322
|
+
self.messages = [*self.messages, user_message]
|
|
1323
|
+
|
|
1324
|
+
# Save user message to session
|
|
1325
|
+
user_content = user_message.message.content
|
|
1326
|
+
if isinstance(user_content, str):
|
|
1327
|
+
self._save_message_to_session("user", user_content)
|
|
1328
|
+
|
|
1329
|
+
# 立即更新UI显示用户消息
|
|
1330
|
+
try:
|
|
1331
|
+
messages_component = self.query_one(
|
|
1332
|
+
"#messages_container", expect_type=Messages
|
|
1333
|
+
)
|
|
1334
|
+
messages_component.update_messages(self.messages)
|
|
1335
|
+
except Exception:
|
|
1336
|
+
self.refresh() # Fallback to full refresh
|
|
1337
|
+
|
|
1338
|
+
async def on_query_from_prompt(
|
|
1339
|
+
self, messages: List[MessageData], abort_controller=None
|
|
1340
|
+
):
|
|
1341
|
+
"""Handle AI query processing (user message already displayed)"""
|
|
1342
|
+
# 用户消息已经通过 on_add_user_message_from_prompt 显示了
|
|
1343
|
+
# 这里只处理AI响应
|
|
1344
|
+
self.run_worker(
|
|
1345
|
+
self._process_ai_response(messages, abort_controller), exclusive=False
|
|
1346
|
+
)
|
|
1347
|
+
|
|
1348
|
+
async def _process_ai_response(
|
|
1349
|
+
self, user_messages: List[MessageData], abort_controller=None
|
|
1350
|
+
):
|
|
1351
|
+
"""Process AI response in background worker"""
|
|
1352
|
+
try:
|
|
1353
|
+
# Use passed AbortController or create new one
|
|
1354
|
+
controller_to_use = abort_controller or asyncio.create_task(
|
|
1355
|
+
asyncio.sleep(0)
|
|
1356
|
+
)
|
|
1357
|
+
if not abort_controller:
|
|
1358
|
+
self.abort_controller = controller_to_use
|
|
1359
|
+
|
|
1360
|
+
# Query API for AI response (query_api handles its own loading state)
|
|
1361
|
+
await self.query_api(user_messages)
|
|
1362
|
+
|
|
1363
|
+
except Exception as e:
|
|
1364
|
+
# Handle errors in background processing
|
|
1365
|
+
error_message = MessageData(
|
|
1366
|
+
type=MessageType.ASSISTANT,
|
|
1367
|
+
message=MessageContent(f"❌ Error processing request: {str(e)}"),
|
|
1368
|
+
options={"error": True},
|
|
1369
|
+
)
|
|
1370
|
+
self.messages = [*self.messages, error_message]
|
|
1371
|
+
|
|
1372
|
+
# Update UI with error
|
|
1373
|
+
try:
|
|
1374
|
+
messages_component = self.query_one(
|
|
1375
|
+
"#messages_container", expect_type=Messages
|
|
1376
|
+
)
|
|
1377
|
+
messages_component.update_messages(self.messages)
|
|
1378
|
+
except Exception:
|
|
1379
|
+
self.refresh()
|
|
1380
|
+
|
|
1381
|
+
# Clear loading state on error
|
|
1382
|
+
self.is_loading = False
|
|
1383
|
+
|
|
490
1384
|
def on_input_change_from_prompt(self, value: str):
|
|
491
1385
|
"""Handle input change from PromptInput"""
|
|
492
1386
|
self.input_value = value
|
|
493
|
-
|
|
1387
|
+
|
|
494
1388
|
def on_mode_change_from_prompt(self, mode: InputMode):
|
|
495
1389
|
"""Handle mode change from PromptInput"""
|
|
496
1390
|
self.input_mode = mode
|
|
497
|
-
|
|
1391
|
+
|
|
498
1392
|
def on_submit_count_change_from_prompt(self, updater):
|
|
499
1393
|
"""Handle submit count change from PromptInput"""
|
|
500
1394
|
if callable(updater):
|
|
501
1395
|
self.submit_count = updater(self.submit_count)
|
|
502
1396
|
else:
|
|
503
1397
|
self.submit_count = updater
|
|
504
|
-
|
|
1398
|
+
|
|
505
1399
|
def set_loading_from_prompt(self, is_loading: bool):
|
|
506
1400
|
"""Set loading state from PromptInput"""
|
|
507
1401
|
self.is_loading = is_loading
|
|
508
|
-
|
|
1402
|
+
|
|
509
1403
|
def set_abort_controller_from_prompt(self, controller):
|
|
510
1404
|
"""Set abort controller from PromptInput"""
|
|
511
1405
|
self.abort_controller = controller
|
|
512
|
-
|
|
1406
|
+
|
|
513
1407
|
def show_message_selector(self):
|
|
514
1408
|
"""Show message selector from PromptInput"""
|
|
515
1409
|
self.is_message_selector_visible = True
|
|
516
|
-
|
|
517
|
-
def set_fork_convo_messages(self, messages: List[
|
|
1410
|
+
|
|
1411
|
+
def set_fork_convo_messages(self, messages: List[MessageData]):
|
|
518
1412
|
"""Set fork conversation messages from PromptInput"""
|
|
519
1413
|
self.fork_convo_with_messages_on_next_render = messages
|
|
520
|
-
|
|
1414
|
+
|
|
521
1415
|
def on_model_change_from_prompt(self):
|
|
522
1416
|
"""Handle model change from PromptInput"""
|
|
523
1417
|
self.fork_number += 1
|
|
524
|
-
|
|
525
|
-
|
|
1418
|
+
|
|
526
1419
|
def set_tool_jsx_from_prompt(self, tool_jsx):
|
|
527
1420
|
"""Set tool JSX from PromptInput"""
|
|
528
1421
|
self.tool_jsx = tool_jsx
|
|
529
|
-
|
|
1422
|
+
|
|
1423
|
+
async def on_execute_command_from_prompt(self, command_name: str, args: str):
|
|
1424
|
+
"""
|
|
1425
|
+
Execute a slash command (e.g., /clear, /help, /tools).
|
|
1426
|
+
Handles different command types:
|
|
1427
|
+
- LOCAL: Direct execution, returns result immediately
|
|
1428
|
+
- LOCAL_JSX: Requires UI interaction (dialogs, confirmations)
|
|
1429
|
+
- PROMPT: Replaces user input and sends to LLM for processing
|
|
1430
|
+
"""
|
|
1431
|
+
from minion_code.commands import command_registry, CommandType
|
|
1432
|
+
|
|
1433
|
+
# Get command class from registry
|
|
1434
|
+
command_class = command_registry.get_command(command_name)
|
|
1435
|
+
|
|
1436
|
+
if not command_class:
|
|
1437
|
+
# Unknown command - show error
|
|
1438
|
+
error_message = MessageData(
|
|
1439
|
+
type=MessageType.ASSISTANT,
|
|
1440
|
+
message=MessageContent(
|
|
1441
|
+
f"❌ Unknown command: /{command_name}\n💡 Use '/help' to see available commands"
|
|
1442
|
+
),
|
|
1443
|
+
options={"error": True},
|
|
1444
|
+
)
|
|
1445
|
+
self.messages = [*self.messages, error_message]
|
|
1446
|
+
self._refresh_messages()
|
|
1447
|
+
return
|
|
1448
|
+
|
|
1449
|
+
# Handle different command types
|
|
1450
|
+
command_type = getattr(command_class, "command_type", CommandType.LOCAL)
|
|
1451
|
+
is_skill = getattr(command_class, "is_skill", False)
|
|
1452
|
+
|
|
1453
|
+
if command_type == CommandType.PROMPT:
|
|
1454
|
+
# PROMPT type: Replace user input and send to LLM
|
|
1455
|
+
# Create command instance to get the expanded prompt
|
|
1456
|
+
command_instance = command_class(self.output_adapter, self.agent)
|
|
1457
|
+
try:
|
|
1458
|
+
expanded_prompt = await command_instance.get_prompt(args)
|
|
1459
|
+
|
|
1460
|
+
# Add as user message and send to LLM
|
|
1461
|
+
user_message = MessageData(
|
|
1462
|
+
type=MessageType.USER,
|
|
1463
|
+
message=MessageContent(expanded_prompt),
|
|
1464
|
+
options={"from_command": command_name},
|
|
1465
|
+
)
|
|
1466
|
+
self.messages = [*self.messages, user_message]
|
|
1467
|
+
self._refresh_messages()
|
|
1468
|
+
|
|
1469
|
+
# Process through AI (this will show "Thinking..." as expected)
|
|
1470
|
+
await self.query_api([user_message])
|
|
1471
|
+
|
|
1472
|
+
except Exception as e:
|
|
1473
|
+
error_message = MessageData(
|
|
1474
|
+
type=MessageType.ASSISTANT,
|
|
1475
|
+
message=MessageContent(
|
|
1476
|
+
f"❌ Error expanding /{command_name}: {str(e)}"
|
|
1477
|
+
),
|
|
1478
|
+
options={"error": True},
|
|
1479
|
+
)
|
|
1480
|
+
self.messages = [*self.messages, error_message]
|
|
1481
|
+
self._refresh_messages()
|
|
1482
|
+
return
|
|
1483
|
+
|
|
1484
|
+
# LOCAL and LOCAL_JSX types: Direct execution
|
|
1485
|
+
# Determine status message based on is_skill
|
|
1486
|
+
if is_skill:
|
|
1487
|
+
status_text = f"⚙️ /{command_name} skill is executing..."
|
|
1488
|
+
else:
|
|
1489
|
+
status_text = f"⚙️ /{command_name} is executing..."
|
|
1490
|
+
|
|
1491
|
+
status_message = MessageData(
|
|
1492
|
+
type=MessageType.PROGRESS,
|
|
1493
|
+
message=MessageContent(status_text),
|
|
1494
|
+
options={"command": True},
|
|
1495
|
+
)
|
|
1496
|
+
self.messages = [*self.messages, status_message]
|
|
1497
|
+
self._refresh_messages()
|
|
1498
|
+
|
|
1499
|
+
try:
|
|
1500
|
+
# Create command instance with TextualOutputAdapter
|
|
1501
|
+
command_instance = command_class(self.output_adapter, self.agent)
|
|
1502
|
+
|
|
1503
|
+
# Special handling for quit command
|
|
1504
|
+
if command_name in ["quit", "exit", "q", "bye"]:
|
|
1505
|
+
command_instance._tui_instance = self
|
|
1506
|
+
|
|
1507
|
+
# Execute the command
|
|
1508
|
+
await command_instance.execute(args)
|
|
1509
|
+
|
|
1510
|
+
# Remove the status message after successful execution
|
|
1511
|
+
# (command output is handled by output_adapter callbacks)
|
|
1512
|
+
if self.messages and self.messages[-1].options.get("command"):
|
|
1513
|
+
self.messages = self.messages[:-1]
|
|
1514
|
+
self._refresh_messages()
|
|
1515
|
+
|
|
1516
|
+
except Exception as e:
|
|
1517
|
+
# Show error message
|
|
1518
|
+
error_message = MessageData(
|
|
1519
|
+
type=MessageType.ASSISTANT,
|
|
1520
|
+
message=MessageContent(f"❌ Error executing /{command_name}: {str(e)}"),
|
|
1521
|
+
options={"error": True},
|
|
1522
|
+
)
|
|
1523
|
+
# Replace status message with error
|
|
1524
|
+
self.messages = [*self.messages[:-1], error_message]
|
|
1525
|
+
self._refresh_messages()
|
|
1526
|
+
|
|
1527
|
+
def show_prompt_input(self):
|
|
1528
|
+
"""Show the prompt input component"""
|
|
1529
|
+
self.should_show_prompt_input = True
|
|
1530
|
+
|
|
1531
|
+
def hide_prompt_input(self):
|
|
1532
|
+
"""Hide the prompt input component"""
|
|
1533
|
+
self.should_show_prompt_input = False
|
|
1534
|
+
|
|
1535
|
+
def toggle_prompt_input(self):
|
|
1536
|
+
"""Toggle the prompt input component visibility"""
|
|
1537
|
+
self.should_show_prompt_input = not self.should_show_prompt_input
|
|
1538
|
+
|
|
530
1539
|
@on(Button.Pressed, "#acknowledge_btn")
|
|
531
1540
|
def acknowledge_cost_dialog(self):
|
|
532
1541
|
"""Acknowledge cost threshold dialog"""
|
|
533
1542
|
self.show_cost_dialog = False
|
|
534
1543
|
self.have_shown_cost_dialog = True
|
|
535
1544
|
self.config.has_acknowledged_cost_threshold = True
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
def normalize_messages(self) -> List[Message]:
|
|
1545
|
+
|
|
1546
|
+
def normalize_messages(self) -> List[MessageData]:
|
|
539
1547
|
"""Normalize messages - equivalent to normalizeMessages function"""
|
|
540
1548
|
# Filter out empty messages and normalize structure
|
|
541
1549
|
return [msg for msg in self.messages if self.is_not_empty_message(msg)]
|
|
542
|
-
|
|
543
|
-
def is_not_empty_message(self, message:
|
|
1550
|
+
|
|
1551
|
+
def is_not_empty_message(self, message: MessageData) -> bool:
|
|
544
1552
|
"""Check if message is not empty - equivalent to isNotEmptyMessage"""
|
|
545
1553
|
if isinstance(message.message.content, str):
|
|
546
1554
|
return bool(message.message.content.strip())
|
|
547
1555
|
return bool(message.message.content)
|
|
548
|
-
|
|
1556
|
+
|
|
549
1557
|
def get_unresolved_tool_use_ids(self) -> Set[str]:
|
|
550
1558
|
"""Get unresolved tool use IDs - equivalent to getUnresolvedToolUseIDs"""
|
|
551
1559
|
# This would analyze messages for unresolved tool uses
|
|
552
1560
|
return set()
|
|
553
|
-
|
|
1561
|
+
|
|
554
1562
|
def get_in_progress_tool_use_ids(self) -> Set[str]:
|
|
555
1563
|
"""Get in-progress tool use IDs - equivalent to getInProgressToolUseIDs"""
|
|
556
1564
|
# This would analyze messages for in-progress tool uses
|
|
557
1565
|
return set()
|
|
558
|
-
|
|
1566
|
+
|
|
559
1567
|
def get_errored_tool_use_ids(self) -> Set[str]:
|
|
560
1568
|
"""Get errored tool use IDs - equivalent to getErroredToolUseMessages"""
|
|
561
1569
|
# This would analyze messages for errored tool uses
|
|
562
1570
|
return set()
|
|
563
|
-
|
|
1571
|
+
|
|
1572
|
+
def _format_error_for_ui(self, error: Exception) -> str:
|
|
1573
|
+
"""Format error message for UI display with appropriate context"""
|
|
1574
|
+
error_type = type(error).__name__
|
|
1575
|
+
error_msg = str(error)
|
|
1576
|
+
|
|
1577
|
+
# Handle common error types with user-friendly messages
|
|
1578
|
+
if "ImportError" in error_type or "ModuleNotFoundError" in error_type:
|
|
1579
|
+
return f"❌ Module Error: {error_msg}\n💡 Try installing missing dependencies or check your environment setup."
|
|
1580
|
+
|
|
1581
|
+
elif "ConnectionError" in error_type or "TimeoutError" in error_type:
|
|
1582
|
+
return f"❌ Connection Error: {error_msg}\n💡 Check your internet connection or API configuration."
|
|
1583
|
+
|
|
1584
|
+
elif "PermissionError" in error_type:
|
|
1585
|
+
return f"❌ Permission Error: {error_msg}\n💡 Check file permissions or run with appropriate privileges."
|
|
1586
|
+
|
|
1587
|
+
elif "FileNotFoundError" in error_type:
|
|
1588
|
+
return f"❌ File Not Found: {error_msg}\n💡 Verify the file path exists and is accessible."
|
|
1589
|
+
|
|
1590
|
+
elif "ValueError" in error_type or "TypeError" in error_type:
|
|
1591
|
+
return f"❌ Input Error: {error_msg}\n💡 Please check your input format and try again."
|
|
1592
|
+
|
|
1593
|
+
else:
|
|
1594
|
+
# Generic error with helpful context
|
|
1595
|
+
return f"❌ {error_type}: {error_msg}\n💡 If this error persists, please check the logs for more details."
|
|
1596
|
+
|
|
564
1597
|
# Reactive property watchers (equivalent to React useEffect)
|
|
565
1598
|
def watch_fork_number(self, fork_number: int):
|
|
566
1599
|
"""Watch fork number changes"""
|
|
567
|
-
|
|
568
|
-
|
|
1600
|
+
pass
|
|
1601
|
+
|
|
569
1602
|
def watch_is_loading(self, is_loading: bool):
|
|
570
1603
|
"""Watch loading state changes"""
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
def
|
|
1604
|
+
pass
|
|
1605
|
+
|
|
1606
|
+
def watch_should_show_prompt_input(self, should_show: bool):
|
|
1607
|
+
"""Watch prompt input visibility changes"""
|
|
1608
|
+
# This will trigger recomposition when the property changes
|
|
1609
|
+
pass
|
|
1610
|
+
|
|
1611
|
+
def watch_messages(self, messages: List[MessageData]):
|
|
574
1612
|
"""Watch messages changes - equivalent to useEffect([messages], ...)"""
|
|
575
|
-
|
|
576
|
-
|
|
1613
|
+
pass
|
|
1614
|
+
|
|
577
1615
|
# Check cost threshold (equivalent to cost threshold useEffect)
|
|
578
1616
|
total_cost = self.get_total_cost()
|
|
579
|
-
if (
|
|
580
|
-
|
|
581
|
-
not self.
|
|
1617
|
+
if (
|
|
1618
|
+
total_cost >= 5.0
|
|
1619
|
+
and not self.show_cost_dialog
|
|
1620
|
+
and not self.have_shown_cost_dialog
|
|
1621
|
+
):
|
|
582
1622
|
self.show_cost_dialog = True
|
|
583
|
-
|
|
1623
|
+
|
|
584
1624
|
def get_total_cost(self) -> float:
|
|
585
1625
|
"""Get total API cost - equivalent to getTotalCost"""
|
|
586
1626
|
# This would calculate actual API costs
|
|
587
1627
|
return len(self.messages) * 0.01 # Mock cost calculation
|
|
588
|
-
|
|
1628
|
+
|
|
589
1629
|
def _get_mode_prefix(self) -> str:
|
|
590
1630
|
"""Get the mode prefix character"""
|
|
591
1631
|
if self.input_mode == InputMode.BASH:
|
|
@@ -594,226 +1634,70 @@ class REPL(Container):
|
|
|
594
1634
|
return "#"
|
|
595
1635
|
else:
|
|
596
1636
|
return ">"
|
|
597
|
-
|
|
1637
|
+
|
|
598
1638
|
# Simplified event handlers for debugging
|
|
599
1639
|
@on(Input.Changed, "#simple_input")
|
|
600
1640
|
def on_simple_input_changed(self, event):
|
|
601
1641
|
"""Handle simple input changes"""
|
|
602
1642
|
self.input_value = event.value
|
|
603
|
-
|
|
604
|
-
|
|
1643
|
+
|
|
605
1644
|
@on(Input.Submitted, "#simple_input")
|
|
606
1645
|
@on(Button.Pressed, "#simple_send")
|
|
607
1646
|
async def on_simple_submit(self, event):
|
|
608
1647
|
"""Handle simple input submission"""
|
|
609
1648
|
input_widget = self.query_one("#simple_input", expect_type=Input)
|
|
610
1649
|
input_text = input_widget.value.strip()
|
|
611
|
-
|
|
1650
|
+
|
|
612
1651
|
if not input_text:
|
|
613
1652
|
return
|
|
614
|
-
|
|
615
|
-
logger.info(f"Submitting: {input_text}")
|
|
616
|
-
|
|
1653
|
+
|
|
617
1654
|
# Add user message to display
|
|
618
|
-
user_message =
|
|
1655
|
+
user_message = MessageData(
|
|
619
1656
|
type=MessageType.USER,
|
|
620
1657
|
message=MessageContent(input_text),
|
|
621
|
-
options={"mode": self.input_mode.value}
|
|
1658
|
+
options={"mode": self.input_mode.value},
|
|
622
1659
|
)
|
|
623
1660
|
self.messages = [*self.messages, user_message]
|
|
624
|
-
|
|
1661
|
+
|
|
625
1662
|
# Create simple response
|
|
626
1663
|
response_text = f"Received: {input_text} (mode: {self.input_mode.value})"
|
|
627
|
-
assistant_message =
|
|
628
|
-
type=MessageType.ASSISTANT,
|
|
629
|
-
message=MessageContent(response_text)
|
|
1664
|
+
assistant_message = MessageData(
|
|
1665
|
+
type=MessageType.ASSISTANT, message=MessageContent(response_text)
|
|
630
1666
|
)
|
|
631
1667
|
self.messages = [*self.messages, assistant_message]
|
|
632
|
-
|
|
1668
|
+
|
|
633
1669
|
# Clear input
|
|
634
1670
|
input_widget.value = ""
|
|
635
1671
|
self.input_value = ""
|
|
636
|
-
|
|
1672
|
+
|
|
637
1673
|
# Keep focus
|
|
638
1674
|
input_widget.focus()
|
|
639
|
-
|
|
1675
|
+
|
|
640
1676
|
@on(Button.Pressed, "#simple_mode")
|
|
641
1677
|
def on_simple_mode_change(self):
|
|
642
1678
|
"""Handle mode change"""
|
|
643
1679
|
modes = list(InputMode)
|
|
644
1680
|
current_index = modes.index(self.input_mode)
|
|
645
1681
|
self.input_mode = modes[(current_index + 1) % len(modes)]
|
|
646
|
-
|
|
1682
|
+
|
|
647
1683
|
# Update mode indicator
|
|
648
1684
|
try:
|
|
649
1685
|
mode_indicator = self.query_one("#mode_indicator", expect_type=Static)
|
|
650
1686
|
mode_indicator.update(f" {self._get_mode_prefix()} ")
|
|
651
|
-
|
|
1687
|
+
|
|
652
1688
|
# Update input placeholder
|
|
653
1689
|
input_widget = self.query_one("#simple_input", expect_type=Input)
|
|
654
1690
|
input_widget.placeholder = f"Enter {self.input_mode.value} command..."
|
|
655
1691
|
except:
|
|
656
1692
|
pass
|
|
657
|
-
|
|
658
|
-
logger.info(f"Mode changed to: {self.input_mode.value}")
|
|
659
1693
|
|
|
660
1694
|
|
|
661
1695
|
class REPLApp(App):
|
|
662
1696
|
"""
|
|
663
1697
|
Main REPL Application - equivalent to the main App wrapper in React
|
|
664
|
-
Provides the application context and styling
|
|
665
|
-
"""
|
|
666
|
-
|
|
667
|
-
CSS = """
|
|
668
|
-
/* Equivalent to CSS styling in React component */
|
|
669
|
-
.user-message {
|
|
670
|
-
background: blue 20%;
|
|
671
|
-
color: white;
|
|
672
|
-
margin: 1;
|
|
673
|
-
padding: 1;
|
|
674
|
-
border: solid blue;
|
|
675
|
-
}
|
|
676
|
-
|
|
677
|
-
.assistant-message {
|
|
678
|
-
background: green 20%;
|
|
679
|
-
color: white;
|
|
680
|
-
margin: 1;
|
|
681
|
-
padding: 1;
|
|
682
|
-
border: solid green;
|
|
683
|
-
}
|
|
684
|
-
|
|
685
|
-
.progress-message {
|
|
686
|
-
background: yellow 20%;
|
|
687
|
-
color: black;
|
|
688
|
-
margin: 1;
|
|
689
|
-
padding: 1;
|
|
690
|
-
border: solid yellow;
|
|
691
|
-
}
|
|
692
|
-
|
|
693
|
-
.dialog-title {
|
|
694
|
-
text-style: bold;
|
|
695
|
-
content-align: center middle;
|
|
696
|
-
margin: 1;
|
|
697
|
-
background: cyan 30%;
|
|
698
|
-
color: black;
|
|
699
|
-
}
|
|
700
|
-
|
|
701
|
-
#messages_container {
|
|
702
|
-
height: 1fr;
|
|
703
|
-
border: solid cyan;
|
|
704
|
-
margin: 1;
|
|
705
|
-
scrollbar-background: gray 50%;
|
|
706
|
-
scrollbar-color: white;
|
|
707
|
-
}
|
|
708
|
-
|
|
709
|
-
#dynamic_content {
|
|
710
|
-
dock: bottom;
|
|
711
|
-
height: auto;
|
|
712
|
-
margin: 1;
|
|
713
|
-
background: gray 10%;
|
|
714
|
-
}
|
|
715
|
-
|
|
716
|
-
#main_input {
|
|
717
|
-
width: 1fr;
|
|
718
|
-
margin-right: 1;
|
|
719
|
-
border: solid white;
|
|
720
|
-
}
|
|
721
|
-
|
|
722
|
-
#simple_input_area {
|
|
723
|
-
dock: bottom;
|
|
724
|
-
height: 3;
|
|
725
|
-
margin: 1;
|
|
726
|
-
}
|
|
727
|
-
|
|
728
|
-
#mode_indicator {
|
|
729
|
-
width: 3;
|
|
730
|
-
content-align: center middle;
|
|
731
|
-
text-style: bold;
|
|
732
|
-
background: gray 20%;
|
|
733
|
-
}
|
|
734
|
-
|
|
735
|
-
#simple_input {
|
|
736
|
-
width: 1fr;
|
|
737
|
-
margin-right: 1;
|
|
738
|
-
}
|
|
739
|
-
|
|
740
|
-
/* PromptInput component styles */
|
|
741
|
-
.model-info {
|
|
742
|
-
dock: top;
|
|
743
|
-
height: 1;
|
|
744
|
-
content-align: right middle;
|
|
745
|
-
background: gray 10%;
|
|
746
|
-
color: white;
|
|
747
|
-
margin-bottom: 1;
|
|
748
|
-
}
|
|
749
|
-
|
|
750
|
-
#input_container {
|
|
751
|
-
border: solid white;
|
|
752
|
-
margin: 1;
|
|
753
|
-
padding: 1;
|
|
754
|
-
}
|
|
755
|
-
|
|
756
|
-
.mode-bash #input_container {
|
|
757
|
-
border: solid yellow;
|
|
758
|
-
}
|
|
759
|
-
|
|
760
|
-
.mode-koding #input_container {
|
|
761
|
-
border: solid cyan;
|
|
762
|
-
}
|
|
763
|
-
|
|
764
|
-
#mode_prefix {
|
|
765
|
-
width: 3;
|
|
766
|
-
content-align: center middle;
|
|
767
|
-
text-style: bold;
|
|
768
|
-
}
|
|
769
|
-
|
|
770
|
-
.mode-bash #mode_prefix {
|
|
771
|
-
color: yellow;
|
|
772
|
-
}
|
|
773
|
-
|
|
774
|
-
.mode-koding #mode_prefix {
|
|
775
|
-
color: cyan;
|
|
776
|
-
}
|
|
777
|
-
|
|
778
|
-
#status_area {
|
|
779
|
-
dock: bottom;
|
|
780
|
-
height: 2;
|
|
781
|
-
margin: 1;
|
|
782
|
-
}
|
|
783
|
-
|
|
784
|
-
.status-message {
|
|
785
|
-
color: white;
|
|
786
|
-
text-style: dim;
|
|
787
|
-
}
|
|
788
|
-
|
|
789
|
-
.model-switch-message {
|
|
790
|
-
color: green;
|
|
791
|
-
text-style: bold;
|
|
792
|
-
}
|
|
793
|
-
|
|
794
|
-
.help-text {
|
|
795
|
-
margin-right: 2;
|
|
796
|
-
}
|
|
797
|
-
|
|
798
|
-
.help-text.active {
|
|
799
|
-
color: white;
|
|
800
|
-
text-style: bold;
|
|
801
|
-
}
|
|
802
|
-
|
|
803
|
-
.help-text.inactive {
|
|
804
|
-
color: gray;
|
|
805
|
-
text-style: dim;
|
|
806
|
-
}
|
|
807
|
-
|
|
808
|
-
Button {
|
|
809
|
-
margin: 1;
|
|
810
|
-
}
|
|
811
|
-
|
|
812
|
-
Input {
|
|
813
|
-
border: solid white;
|
|
814
|
-
}
|
|
1698
|
+
Provides the application context, agent management, and styling
|
|
815
1699
|
"""
|
|
816
|
-
|
|
1700
|
+
|
|
817
1701
|
def __init__(self, **kwargs):
|
|
818
1702
|
super().__init__(**kwargs)
|
|
819
1703
|
# Initialize with default props (equivalent to React props)
|
|
@@ -831,26 +1715,90 @@ class REPLApp(App):
|
|
|
831
1715
|
"mcp_clients": [],
|
|
832
1716
|
"is_default_model": True,
|
|
833
1717
|
"initial_update_version": None,
|
|
834
|
-
"initial_update_commands": None
|
|
1718
|
+
"initial_update_commands": None,
|
|
1719
|
+
"resume_session_id": None,
|
|
1720
|
+
"continue_last": False,
|
|
835
1721
|
}
|
|
836
|
-
|
|
1722
|
+
|
|
1723
|
+
# App-level agent management
|
|
1724
|
+
self.agent = None
|
|
1725
|
+
self.agent_ready = False
|
|
1726
|
+
|
|
837
1727
|
def compose(self) -> ComposeResult:
|
|
838
1728
|
"""Compose the main application - equivalent to React App render"""
|
|
839
|
-
yield Header(show_clock=
|
|
840
|
-
|
|
1729
|
+
yield Header(show_clock=False)
|
|
1730
|
+
# Pass agent to REPL component (filter out app-level props like 'model')
|
|
1731
|
+
repl_props_filtered = {k: v for k, v in self.repl_props.items() if k != "model"}
|
|
1732
|
+
repl_props_with_agent = {**repl_props_filtered, "agent": self.agent}
|
|
1733
|
+
yield REPL(**repl_props_with_agent)
|
|
841
1734
|
yield Footer()
|
|
842
|
-
|
|
1735
|
+
|
|
843
1736
|
def on_mount(self):
|
|
844
1737
|
"""Application mount lifecycle"""
|
|
845
1738
|
self.title = "Minion Code Assistant"
|
|
846
|
-
|
|
1739
|
+
# Initialize agent at app level
|
|
1740
|
+
self.run_worker(self._initialize_agent())
|
|
1741
|
+
|
|
1742
|
+
async def _initialize_agent(self):
|
|
1743
|
+
"""Initialize the MinionCodeAgent at app level"""
|
|
1744
|
+
try:
|
|
1745
|
+
from minion_code import MinionCodeAgent
|
|
1746
|
+
from minion_code.utils.logs import logger
|
|
1747
|
+
from minion_code.agents.hooks import create_default_hooks
|
|
1748
|
+
|
|
1749
|
+
# Check for model from CLI or use default
|
|
1750
|
+
# Users can override with --model flag or config
|
|
1751
|
+
model_from_props = self.repl_props.get("model")
|
|
1752
|
+
default_llm = model_from_props if model_from_props else "claude-sonnet-4-5"
|
|
1753
|
+
|
|
1754
|
+
# Get REPL component's output adapter for permission dialogs
|
|
1755
|
+
try:
|
|
1756
|
+
repl_component = self.query_one(REPL)
|
|
1757
|
+
output_adapter = repl_component.output_adapter
|
|
1758
|
+
hooks = create_default_hooks(output_adapter)
|
|
1759
|
+
logger.info(
|
|
1760
|
+
"Created hooks with TextualOutputAdapter for permission dialogs"
|
|
1761
|
+
)
|
|
1762
|
+
except Exception as e:
|
|
1763
|
+
logger.warning(
|
|
1764
|
+
f"Could not get output adapter, using autonomous hooks: {e}"
|
|
1765
|
+
)
|
|
1766
|
+
from minion_code.agents.hooks import create_autonomous_hooks
|
|
1767
|
+
|
|
1768
|
+
hooks = create_autonomous_hooks()
|
|
1769
|
+
|
|
1770
|
+
logger.info(f"Initializing agent with LLM: {default_llm}")
|
|
1771
|
+
self.agent = await MinionCodeAgent.create(
|
|
1772
|
+
name="REPL Assistant",
|
|
1773
|
+
llm=default_llm,
|
|
1774
|
+
hooks=hooks,
|
|
1775
|
+
# History decay: save large outputs to file after N steps
|
|
1776
|
+
decay_enabled=True,
|
|
1777
|
+
decay_ttl_steps=3,
|
|
1778
|
+
decay_min_size=100_000, # 100KB
|
|
1779
|
+
)
|
|
1780
|
+
self.agent_ready = True
|
|
1781
|
+
|
|
1782
|
+
logger.info(f"Agent initialized with {len(self.agent.tools)} tools")
|
|
1783
|
+
|
|
1784
|
+
# Update REPL component with agent
|
|
1785
|
+
try:
|
|
1786
|
+
repl_component = self.query_one(REPL)
|
|
1787
|
+
repl_component.set_agent(self.agent)
|
|
1788
|
+
logger.info("Agent set on REPL component")
|
|
1789
|
+
except Exception as e:
|
|
1790
|
+
logger.warning(f"Could not set agent on REPL: {e}")
|
|
1791
|
+
|
|
1792
|
+
except Exception as e:
|
|
1793
|
+
from minion_code.utils.logs import logger
|
|
1794
|
+
|
|
1795
|
+
logger.error(f"Failed to initialize agent: {e}")
|
|
1796
|
+
self.agent_ready = False
|
|
847
1797
|
|
|
848
1798
|
|
|
849
1799
|
# Utility functions equivalent to TypeScript utility functions
|
|
850
1800
|
def should_render_statically(
|
|
851
|
-
message:
|
|
852
|
-
messages: List[Message],
|
|
853
|
-
unresolved_tool_use_ids: Set[str]
|
|
1801
|
+
message: MessageData, messages: List[MessageData], unresolved_tool_use_ids: Set[str]
|
|
854
1802
|
) -> bool:
|
|
855
1803
|
"""
|
|
856
1804
|
Determine if message should render statically
|
|
@@ -864,6 +1812,7 @@ def should_render_statically(
|
|
|
864
1812
|
return len(unresolved_tool_use_ids) == 0
|
|
865
1813
|
return True
|
|
866
1814
|
|
|
1815
|
+
|
|
867
1816
|
def intersects(set_a: Set[str], set_b: Set[str]) -> bool:
|
|
868
1817
|
"""Check if two sets intersect - equivalent to intersects function"""
|
|
869
1818
|
return len(set_a & set_b) > 0
|
|
@@ -876,42 +1825,77 @@ def create_repl(
|
|
|
876
1825
|
debug=False,
|
|
877
1826
|
initial_prompt=None,
|
|
878
1827
|
verbose=False,
|
|
879
|
-
|
|
1828
|
+
resume_session_id=None,
|
|
1829
|
+
continue_last=False,
|
|
1830
|
+
**kwargs,
|
|
880
1831
|
) -> REPLApp:
|
|
881
1832
|
"""
|
|
882
1833
|
Create a configured REPL application
|
|
883
1834
|
Equivalent to calling REPL component with props in React
|
|
884
1835
|
"""
|
|
1836
|
+
print(f"DEBUG create_repl: initial_prompt={initial_prompt}")
|
|
885
1837
|
app = REPLApp()
|
|
886
|
-
app.repl_props.update(
|
|
887
|
-
|
|
888
|
-
|
|
889
|
-
|
|
890
|
-
|
|
891
|
-
|
|
892
|
-
|
|
893
|
-
|
|
1838
|
+
app.repl_props.update(
|
|
1839
|
+
{
|
|
1840
|
+
"commands": commands or [],
|
|
1841
|
+
"safe_mode": safe_mode,
|
|
1842
|
+
"debug": debug,
|
|
1843
|
+
"initial_prompt": initial_prompt,
|
|
1844
|
+
"verbose": verbose,
|
|
1845
|
+
"resume_session_id": resume_session_id,
|
|
1846
|
+
"continue_last": continue_last,
|
|
1847
|
+
**kwargs,
|
|
1848
|
+
}
|
|
1849
|
+
)
|
|
1850
|
+
print(f"DEBUG create_repl: repl_props={app.repl_props}")
|
|
894
1851
|
return app
|
|
895
1852
|
|
|
896
1853
|
|
|
897
|
-
def run(
|
|
1854
|
+
def run(
|
|
1855
|
+
initial_prompt=None,
|
|
1856
|
+
debug=False,
|
|
1857
|
+
verbose=False,
|
|
1858
|
+
resume_session_id=None,
|
|
1859
|
+
continue_last=False,
|
|
1860
|
+
model=None,
|
|
1861
|
+
):
|
|
898
1862
|
"""Run the REPL application with optional configuration"""
|
|
1863
|
+
# File-based logging for TUI debugging
|
|
1864
|
+
import logging
|
|
1865
|
+
|
|
1866
|
+
logging.basicConfig(
|
|
1867
|
+
filename="/tmp/minion_repl_debug.log",
|
|
1868
|
+
level=logging.DEBUG,
|
|
1869
|
+
format="%(asctime)s - %(levelname)s - %(message)s",
|
|
1870
|
+
force=True, # Override any existing config
|
|
1871
|
+
)
|
|
1872
|
+
logging.debug(f"=== REPL run() called ===")
|
|
1873
|
+
logging.debug(f"initial_prompt: {repr(initial_prompt)}")
|
|
1874
|
+
logging.debug(f"debug: {debug}, verbose: {verbose}, model: {model}")
|
|
1875
|
+
logging.debug(
|
|
1876
|
+
f"resume_session_id: {resume_session_id}, continue_last: {continue_last}"
|
|
1877
|
+
)
|
|
1878
|
+
|
|
899
1879
|
app = create_repl(
|
|
900
1880
|
initial_prompt=initial_prompt,
|
|
901
1881
|
debug=debug,
|
|
902
|
-
verbose=verbose
|
|
1882
|
+
verbose=verbose,
|
|
1883
|
+
resume_session_id=resume_session_id,
|
|
1884
|
+
continue_last=continue_last,
|
|
1885
|
+
model=model,
|
|
903
1886
|
)
|
|
1887
|
+
logging.debug(f"app.repl_props: {app.repl_props}")
|
|
904
1888
|
app.run()
|
|
905
1889
|
|
|
906
1890
|
|
|
907
1891
|
if __name__ == "__main__":
|
|
908
1892
|
import sys
|
|
909
|
-
|
|
1893
|
+
|
|
910
1894
|
# Parse command line arguments (basic implementation)
|
|
911
1895
|
initial_prompt = None
|
|
912
1896
|
debug = False
|
|
913
1897
|
verbose = False
|
|
914
|
-
|
|
1898
|
+
|
|
915
1899
|
if len(sys.argv) > 1:
|
|
916
1900
|
if "--debug" in sys.argv:
|
|
917
1901
|
debug = True
|
|
@@ -921,5 +1905,5 @@ if __name__ == "__main__":
|
|
|
921
1905
|
prompt_index = sys.argv.index("--prompt")
|
|
922
1906
|
if prompt_index + 1 < len(sys.argv):
|
|
923
1907
|
initial_prompt = sys.argv[prompt_index + 1]
|
|
924
|
-
|
|
925
|
-
run(initial_prompt=initial_prompt, debug=debug, verbose=verbose)
|
|
1908
|
+
|
|
1909
|
+
run(initial_prompt=initial_prompt, debug=debug, verbose=verbose)
|