minion-code 0.1.0__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/advance_tui.py +508 -0
- examples/agent_with_todos.py +165 -0
- examples/file_freshness_example.py +97 -0
- examples/file_watching_example.py +110 -0
- examples/interruptible_tui.py +5 -0
- examples/message_response_children_demo.py +226 -0
- examples/rich_example.py +4 -0
- examples/simple_file_watching.py +57 -0
- examples/simple_tui.py +267 -0
- examples/simple_usage.py +69 -0
- minion_code/__init__.py +16 -0
- minion_code/agents/__init__.py +11 -0
- minion_code/agents/code_agent.py +320 -0
- minion_code/cli.py +502 -0
- minion_code/commands/__init__.py +90 -0
- minion_code/commands/clear_command.py +70 -0
- minion_code/commands/help_command.py +90 -0
- minion_code/commands/history_command.py +104 -0
- minion_code/commands/quit_command.py +32 -0
- minion_code/commands/status_command.py +115 -0
- minion_code/commands/tools_command.py +86 -0
- minion_code/commands/version_command.py +104 -0
- minion_code/components/Message.py +304 -0
- minion_code/components/MessageResponse.py +188 -0
- minion_code/components/PromptInput.py +534 -0
- minion_code/components/__init__.py +29 -0
- minion_code/screens/REPL.py +925 -0
- minion_code/screens/__init__.py +4 -0
- minion_code/services/__init__.py +50 -0
- minion_code/services/event_system.py +108 -0
- minion_code/services/file_freshness_service.py +582 -0
- minion_code/tools/__init__.py +69 -0
- minion_code/tools/bash_tool.py +58 -0
- minion_code/tools/file_edit_tool.py +238 -0
- minion_code/tools/file_read_tool.py +73 -0
- minion_code/tools/file_write_tool.py +36 -0
- minion_code/tools/glob_tool.py +58 -0
- minion_code/tools/grep_tool.py +105 -0
- minion_code/tools/ls_tool.py +65 -0
- minion_code/tools/multi_edit_tool.py +271 -0
- minion_code/tools/python_interpreter_tool.py +105 -0
- minion_code/tools/todo_read_tool.py +100 -0
- minion_code/tools/todo_write_tool.py +234 -0
- minion_code/tools/user_input_tool.py +53 -0
- minion_code/types.py +88 -0
- minion_code/utils/__init__.py +44 -0
- minion_code/utils/mcp_loader.py +211 -0
- minion_code/utils/todo_file_utils.py +110 -0
- minion_code/utils/todo_storage.py +149 -0
- minion_code-0.1.0.dist-info/METADATA +350 -0
- minion_code-0.1.0.dist-info/RECORD +59 -0
- minion_code-0.1.0.dist-info/WHEEL +5 -0
- minion_code-0.1.0.dist-info/entry_points.txt +4 -0
- minion_code-0.1.0.dist-info/licenses/LICENSE +661 -0
- minion_code-0.1.0.dist-info/top_level.txt +3 -0
- tests/__init__.py +1 -0
- tests/test_basic.py +20 -0
- tests/test_readonly_tools.py +102 -0
- tests/test_tools.py +83 -0
|
@@ -0,0 +1,925 @@
|
|
|
1
|
+
"""
|
|
2
|
+
REPL Screen Implementation using Textual UI Components
|
|
3
|
+
Python equivalent of /Users/femtozheng/web-project/Kode/src/screens/REPL.tsx
|
|
4
|
+
Simulates React-like component structure as documented in AGENTS.md
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from textual.app import App, ComposeResult
|
|
8
|
+
from textual.containers import Container, Horizontal, Vertical, ScrollableContainer
|
|
9
|
+
from textual.widgets import Input, RichLog, Button, Static, Header, Footer, Label
|
|
10
|
+
from textual.reactive import reactive, var
|
|
11
|
+
from textual import on, work
|
|
12
|
+
from textual.screen import Screen
|
|
13
|
+
from rich.text import Text
|
|
14
|
+
from rich.syntax import Syntax
|
|
15
|
+
from rich.console import Console
|
|
16
|
+
import asyncio
|
|
17
|
+
from typing import List, Dict, Any, Optional, Callable, Union, Set
|
|
18
|
+
from dataclasses import dataclass, field
|
|
19
|
+
from enum import Enum
|
|
20
|
+
import uuid
|
|
21
|
+
import time
|
|
22
|
+
from pathlib import Path
|
|
23
|
+
|
|
24
|
+
# Simple logging setup for TUI - disable to prevent screen interference
|
|
25
|
+
import logging
|
|
26
|
+
logger = logging.getLogger(__name__)
|
|
27
|
+
logger.disabled = True
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
# Import shared types
|
|
31
|
+
from ..types import (
|
|
32
|
+
MessageType, InputMode, MessageContent, Message,
|
|
33
|
+
ToolUseConfirm, BinaryFeedbackContext, ToolJSXContext,
|
|
34
|
+
REPLConfig, ModelInfo
|
|
35
|
+
)
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
class Logo(Static):
|
|
39
|
+
"""Logo component equivalent to React Logo component"""
|
|
40
|
+
|
|
41
|
+
def __init__(self, mcp_clients=None, is_default_model=True, update_banner_version=None, **kwargs):
|
|
42
|
+
super().__init__(**kwargs)
|
|
43
|
+
self.mcp_clients = mcp_clients or []
|
|
44
|
+
self.is_default_model = is_default_model
|
|
45
|
+
self.update_banner_version = update_banner_version
|
|
46
|
+
|
|
47
|
+
def render(self) -> str:
|
|
48
|
+
logo_text = "🤖 Minion Code Assistant"
|
|
49
|
+
if self.update_banner_version:
|
|
50
|
+
logo_text += f" (Update available: {self.update_banner_version})"
|
|
51
|
+
return logo_text
|
|
52
|
+
|
|
53
|
+
class ModeIndicator(Static):
|
|
54
|
+
"""Mode indicator component"""
|
|
55
|
+
|
|
56
|
+
def __init__(self, mode: InputMode = InputMode.PROMPT, **kwargs):
|
|
57
|
+
super().__init__(**kwargs)
|
|
58
|
+
self.mode = mode
|
|
59
|
+
|
|
60
|
+
def render(self) -> str:
|
|
61
|
+
return f"Mode: {self.mode.value.upper()}"
|
|
62
|
+
|
|
63
|
+
class Spinner(Static):
|
|
64
|
+
"""Loading spinner component"""
|
|
65
|
+
|
|
66
|
+
def __init__(self, **kwargs):
|
|
67
|
+
super().__init__("⠋ Loading...", **kwargs)
|
|
68
|
+
self.auto_refresh = 0.1
|
|
69
|
+
self.spinner_chars = "⠋⠙⠹⠸⠼⠴⠦⠧⠇⠏"
|
|
70
|
+
self.spinner_index = 0
|
|
71
|
+
|
|
72
|
+
def on_mount(self):
|
|
73
|
+
self.set_interval(0.1, self.update_spinner)
|
|
74
|
+
|
|
75
|
+
def update_spinner(self):
|
|
76
|
+
self.spinner_index = (self.spinner_index + 1) % len(self.spinner_chars)
|
|
77
|
+
self.update(f"{self.spinner_chars[self.spinner_index]} Loading...")
|
|
78
|
+
|
|
79
|
+
class MessageWidget(Container):
|
|
80
|
+
"""Individual message display widget"""
|
|
81
|
+
|
|
82
|
+
def __init__(self, message: Message, verbose: bool = False, debug: bool = False, **kwargs):
|
|
83
|
+
super().__init__(**kwargs)
|
|
84
|
+
self.message = message
|
|
85
|
+
self.verbose = verbose
|
|
86
|
+
self.debug = debug
|
|
87
|
+
|
|
88
|
+
def compose(self) -> ComposeResult:
|
|
89
|
+
if self.message.type == MessageType.USER:
|
|
90
|
+
yield Static(f"👤 User: {self._get_content_text()}", classes="user-message")
|
|
91
|
+
elif self.message.type == MessageType.ASSISTANT:
|
|
92
|
+
yield Static(f"🤖 Assistant: {self._get_content_text()}", classes="assistant-message")
|
|
93
|
+
elif self.message.type == MessageType.PROGRESS:
|
|
94
|
+
yield Static(f"⚙️ Progress: {self._get_content_text()}", classes="progress-message")
|
|
95
|
+
|
|
96
|
+
def _get_content_text(self) -> str:
|
|
97
|
+
if isinstance(self.message.message.content, str):
|
|
98
|
+
return self.message.message.content
|
|
99
|
+
elif isinstance(self.message.message.content, list):
|
|
100
|
+
# Extract text from structured content
|
|
101
|
+
text_parts = []
|
|
102
|
+
for block in self.message.message.content:
|
|
103
|
+
if isinstance(block, dict) and block.get("type") == "text":
|
|
104
|
+
text_parts.append(block.get("text", ""))
|
|
105
|
+
return "\n".join(text_parts)
|
|
106
|
+
return str(self.message.message.content)
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
# PromptInput moved to components/PromptInput.py
|
|
110
|
+
from ..components.PromptInput import PromptInput
|
|
111
|
+
|
|
112
|
+
class CostThresholdDialog(Container):
|
|
113
|
+
"""Cost threshold warning dialog"""
|
|
114
|
+
|
|
115
|
+
def compose(self) -> ComposeResult:
|
|
116
|
+
yield Static("⚠️ Cost Threshold Warning", classes="dialog-title")
|
|
117
|
+
yield Static("You have exceeded $5 in API costs. Please be mindful of usage.")
|
|
118
|
+
yield Button("Acknowledge", id="acknowledge_btn", variant="primary")
|
|
119
|
+
|
|
120
|
+
class PermissionRequest(Container):
|
|
121
|
+
"""Permission request dialog for tool usage"""
|
|
122
|
+
|
|
123
|
+
def __init__(self, tool_use_confirm: ToolUseConfirm, **kwargs):
|
|
124
|
+
super().__init__(**kwargs)
|
|
125
|
+
self.tool_use_confirm = tool_use_confirm
|
|
126
|
+
|
|
127
|
+
def compose(self) -> ComposeResult:
|
|
128
|
+
yield Static(f"🔧 Tool Permission Request", classes="dialog-title")
|
|
129
|
+
yield Static(f"Tool: {self.tool_use_confirm.tool_name}")
|
|
130
|
+
yield Static(f"Parameters: {self.tool_use_confirm.parameters}")
|
|
131
|
+
with Horizontal():
|
|
132
|
+
yield Button("Allow", id="allow_btn", variant="success")
|
|
133
|
+
yield Button("Deny", id="deny_btn", variant="error")
|
|
134
|
+
|
|
135
|
+
@on(Button.Pressed, "#allow_btn")
|
|
136
|
+
def allow_tool(self):
|
|
137
|
+
self.tool_use_confirm.on_confirm()
|
|
138
|
+
|
|
139
|
+
@on(Button.Pressed, "#deny_btn")
|
|
140
|
+
def deny_tool(self):
|
|
141
|
+
self.tool_use_confirm.on_abort()
|
|
142
|
+
|
|
143
|
+
|
|
144
|
+
class MessageSelector(Container):
|
|
145
|
+
"""Message selector for conversation forking"""
|
|
146
|
+
|
|
147
|
+
def __init__(self, messages: List[Message], **kwargs):
|
|
148
|
+
super().__init__(**kwargs)
|
|
149
|
+
self.messages = messages
|
|
150
|
+
|
|
151
|
+
def compose(self) -> ComposeResult:
|
|
152
|
+
yield Static("📝 Select Message to Fork From", classes="dialog-title")
|
|
153
|
+
with ScrollableContainer():
|
|
154
|
+
for i, message in enumerate(self.messages[-10:]): # Show last 10 messages
|
|
155
|
+
content = self._get_message_preview(message)
|
|
156
|
+
yield Button(f"{i}: {content[:50]}...", id=f"msg_{i}")
|
|
157
|
+
yield Button("Cancel", id="cancel_selector", variant="error")
|
|
158
|
+
|
|
159
|
+
def _get_message_preview(self, message: Message) -> str:
|
|
160
|
+
if isinstance(message.message.content, str):
|
|
161
|
+
return message.message.content
|
|
162
|
+
return str(message.message.content)[:50]
|
|
163
|
+
|
|
164
|
+
|
|
165
|
+
class REPL(Container):
|
|
166
|
+
"""
|
|
167
|
+
Main REPL Component - Python equivalent of React REPL component
|
|
168
|
+
Manages the entire conversation interface with AI assistant
|
|
169
|
+
"""
|
|
170
|
+
|
|
171
|
+
# Reactive properties equivalent to React useState
|
|
172
|
+
fork_number = reactive(0)
|
|
173
|
+
is_loading = reactive(False)
|
|
174
|
+
messages = var(list) # List[Message]
|
|
175
|
+
input_value = reactive("")
|
|
176
|
+
input_mode = reactive(InputMode.PROMPT)
|
|
177
|
+
submit_count = reactive(0)
|
|
178
|
+
is_message_selector_visible = reactive(False)
|
|
179
|
+
show_cost_dialog = reactive(False)
|
|
180
|
+
have_shown_cost_dialog = reactive(False)
|
|
181
|
+
|
|
182
|
+
def __init__(self,
|
|
183
|
+
commands=None,
|
|
184
|
+
safe_mode=False,
|
|
185
|
+
debug=False,
|
|
186
|
+
initial_fork_number=0,
|
|
187
|
+
initial_prompt=None,
|
|
188
|
+
message_log_name="default",
|
|
189
|
+
should_show_prompt_input=True,
|
|
190
|
+
tools=None,
|
|
191
|
+
verbose=False,
|
|
192
|
+
initial_messages=None,
|
|
193
|
+
mcp_clients=None,
|
|
194
|
+
is_default_model=True,
|
|
195
|
+
initial_update_version=None,
|
|
196
|
+
initial_update_commands=None,
|
|
197
|
+
**kwargs):
|
|
198
|
+
super().__init__(**kwargs)
|
|
199
|
+
|
|
200
|
+
# Props equivalent to TypeScript Props interface
|
|
201
|
+
self.commands = commands or []
|
|
202
|
+
self.safe_mode = safe_mode
|
|
203
|
+
self.debug = debug
|
|
204
|
+
self.initial_fork_number = initial_fork_number
|
|
205
|
+
self.initial_prompt = initial_prompt
|
|
206
|
+
self.message_log_name = message_log_name
|
|
207
|
+
self.should_show_prompt_input = should_show_prompt_input
|
|
208
|
+
self.tools = tools or []
|
|
209
|
+
self.verbose = verbose
|
|
210
|
+
self.mcp_clients = mcp_clients or []
|
|
211
|
+
self.is_default_model = is_default_model
|
|
212
|
+
self.initial_update_version = initial_update_version
|
|
213
|
+
self.initial_update_commands = initial_update_commands
|
|
214
|
+
|
|
215
|
+
# Initialize state
|
|
216
|
+
self.messages = initial_messages or []
|
|
217
|
+
self.fork_number = initial_fork_number
|
|
218
|
+
|
|
219
|
+
# Internal state
|
|
220
|
+
self.config = REPLConfig()
|
|
221
|
+
self.abort_controller: Optional[asyncio.Task] = None
|
|
222
|
+
self.tool_jsx: Optional[ToolJSXContext] = None
|
|
223
|
+
self.tool_use_confirm: Optional[ToolUseConfirm] = None
|
|
224
|
+
self.binary_feedback_context: Optional[BinaryFeedbackContext] = None
|
|
225
|
+
self.read_file_timestamps: Dict[str, float] = {}
|
|
226
|
+
self.fork_convo_with_messages_on_next_render: Optional[List[Message]] = None
|
|
227
|
+
|
|
228
|
+
logger.info(f"REPL initialized with {len(self.messages)} initial messages")
|
|
229
|
+
|
|
230
|
+
def compose(self) -> ComposeResult:
|
|
231
|
+
"""Compose the REPL interface - equivalent to React render method"""
|
|
232
|
+
# Static header section (equivalent to Static items in React)
|
|
233
|
+
with Vertical():
|
|
234
|
+
yield Logo(
|
|
235
|
+
mcp_clients=self.mcp_clients,
|
|
236
|
+
is_default_model=self.is_default_model,
|
|
237
|
+
update_banner_version=self.initial_update_version
|
|
238
|
+
)
|
|
239
|
+
yield ModeIndicator(mode=self.input_mode)
|
|
240
|
+
|
|
241
|
+
# Messages container (equivalent to messagesJSX mapping)
|
|
242
|
+
with ScrollableContainer(id="messages_container"):
|
|
243
|
+
for message in self.messages:
|
|
244
|
+
yield MessageWidget(
|
|
245
|
+
message=message,
|
|
246
|
+
verbose=self.verbose,
|
|
247
|
+
debug=self.debug
|
|
248
|
+
)
|
|
249
|
+
|
|
250
|
+
# Dynamic content area (equivalent to conditional rendering in React)
|
|
251
|
+
with Container(id="dynamic_content"):
|
|
252
|
+
# Spinner (equivalent to {!toolJSX && !toolUseConfirm && !binaryFeedbackContext && isLoading && <Spinner />})
|
|
253
|
+
if self.is_loading and not self.tool_jsx and not self.tool_use_confirm and not self.binary_feedback_context:
|
|
254
|
+
yield Spinner()
|
|
255
|
+
|
|
256
|
+
# Tool JSX (equivalent to {toolJSX ? toolJSX.jsx : null})
|
|
257
|
+
if self.tool_jsx and self.tool_jsx.jsx:
|
|
258
|
+
yield self.tool_jsx.jsx
|
|
259
|
+
|
|
260
|
+
# Binary feedback (equivalent to BinaryFeedback component)
|
|
261
|
+
if self.binary_feedback_context and not self.is_message_selector_visible:
|
|
262
|
+
yield Static("🔄 Binary feedback component would render here")
|
|
263
|
+
|
|
264
|
+
# Permission request (equivalent to PermissionRequest component)
|
|
265
|
+
if self.tool_use_confirm and not self.is_message_selector_visible and not self.binary_feedback_context:
|
|
266
|
+
yield PermissionRequest(self.tool_use_confirm)
|
|
267
|
+
|
|
268
|
+
# Cost dialog (equivalent to CostThresholdDialog component)
|
|
269
|
+
if self.show_cost_dialog and not self.is_loading:
|
|
270
|
+
yield CostThresholdDialog()
|
|
271
|
+
|
|
272
|
+
# PromptInput component (now working)
|
|
273
|
+
if True: # Always show for now
|
|
274
|
+
prompt_input = PromptInput(
|
|
275
|
+
commands=self.commands,
|
|
276
|
+
fork_number=self.fork_number,
|
|
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
|
+
|
|
305
|
+
# Message selector (equivalent to {isMessageSelectorVisible && <MessageSelector />})
|
|
306
|
+
if self.is_message_selector_visible:
|
|
307
|
+
yield MessageSelector(messages=self.messages)
|
|
308
|
+
|
|
309
|
+
def on_mount(self):
|
|
310
|
+
"""Component lifecycle - equivalent to React useEffect(() => { onInit() }, [])"""
|
|
311
|
+
logger.info("REPL mounted, starting initialization")
|
|
312
|
+
self.call_later(self.on_init)
|
|
313
|
+
# Set focus to the input after a short delay to ensure it's mounted
|
|
314
|
+
self.set_timer(0.1, self._set_focus_to_input)
|
|
315
|
+
|
|
316
|
+
def _set_focus_to_input(self):
|
|
317
|
+
"""Set focus to the main input widget"""
|
|
318
|
+
try:
|
|
319
|
+
# Try to find the simplified input
|
|
320
|
+
input_widget = self.query_one("#simple_input", expect_type=Input)
|
|
321
|
+
input_widget.focus()
|
|
322
|
+
logger.info("Focus set to simple input from REPL")
|
|
323
|
+
except Exception as e:
|
|
324
|
+
logger.warning(f"Could not set focus to simple input: {e}")
|
|
325
|
+
# If that fails, try to focus any Input widget
|
|
326
|
+
try:
|
|
327
|
+
inputs = self.query("Input")
|
|
328
|
+
if inputs:
|
|
329
|
+
inputs[0].focus()
|
|
330
|
+
logger.info("Focus set to first available input")
|
|
331
|
+
except Exception as e2:
|
|
332
|
+
logger.warning(f"Could not set focus to any input: {e2}")
|
|
333
|
+
|
|
334
|
+
async def on_init(self):
|
|
335
|
+
"""Initialize REPL - equivalent to React onInit function"""
|
|
336
|
+
logger.info("REPL initialization started")
|
|
337
|
+
|
|
338
|
+
if not self.initial_prompt:
|
|
339
|
+
logger.info("No initial prompt provided")
|
|
340
|
+
return
|
|
341
|
+
|
|
342
|
+
self.is_loading = True
|
|
343
|
+
|
|
344
|
+
try:
|
|
345
|
+
# Process initial prompt (equivalent to processUserInput)
|
|
346
|
+
new_messages = await self.process_user_input(
|
|
347
|
+
self.initial_prompt,
|
|
348
|
+
self.input_mode
|
|
349
|
+
)
|
|
350
|
+
|
|
351
|
+
if new_messages:
|
|
352
|
+
# Add to history (equivalent to addToHistory)
|
|
353
|
+
self.add_to_history(self.initial_prompt)
|
|
354
|
+
|
|
355
|
+
# Update messages (equivalent to setMessages)
|
|
356
|
+
self.messages = [*self.messages, *new_messages]
|
|
357
|
+
|
|
358
|
+
# Query API if needed (equivalent to query function)
|
|
359
|
+
await self.query_api(new_messages)
|
|
360
|
+
|
|
361
|
+
except Exception as e:
|
|
362
|
+
logger.error(f"Initialization failed: {e}")
|
|
363
|
+
finally:
|
|
364
|
+
self.is_loading = False
|
|
365
|
+
logger.info("REPL initialization completed")
|
|
366
|
+
|
|
367
|
+
async def process_user_input(self, input_text: str, mode: InputMode) -> List[Message]:
|
|
368
|
+
"""Process user input - equivalent to processUserInput function"""
|
|
369
|
+
logger.info(f"Processing user input: {input_text[:50]}... (mode: {mode.value})")
|
|
370
|
+
|
|
371
|
+
# Create user message
|
|
372
|
+
user_message = Message(
|
|
373
|
+
type=MessageType.USER,
|
|
374
|
+
message=MessageContent(input_text),
|
|
375
|
+
options={"isKodingRequest": mode == InputMode.KODING}
|
|
376
|
+
)
|
|
377
|
+
|
|
378
|
+
# Handle different input modes
|
|
379
|
+
if mode == InputMode.BASH:
|
|
380
|
+
# Handle bash command
|
|
381
|
+
result = await self.execute_bash_command(input_text)
|
|
382
|
+
assistant_message = Message(
|
|
383
|
+
type=MessageType.ASSISTANT,
|
|
384
|
+
message=MessageContent(result)
|
|
385
|
+
)
|
|
386
|
+
return [user_message, assistant_message]
|
|
387
|
+
elif mode == InputMode.KODING:
|
|
388
|
+
# Handle koding request
|
|
389
|
+
return [user_message]
|
|
390
|
+
else:
|
|
391
|
+
# Handle regular prompt
|
|
392
|
+
return [user_message]
|
|
393
|
+
|
|
394
|
+
async def execute_bash_command(self, command: str) -> str:
|
|
395
|
+
"""Execute bash command - simplified version"""
|
|
396
|
+
try:
|
|
397
|
+
import subprocess
|
|
398
|
+
result = subprocess.run(
|
|
399
|
+
command,
|
|
400
|
+
shell=True,
|
|
401
|
+
capture_output=True,
|
|
402
|
+
text=True,
|
|
403
|
+
timeout=30
|
|
404
|
+
)
|
|
405
|
+
if result.returncode == 0:
|
|
406
|
+
return result.stdout or "Command executed successfully"
|
|
407
|
+
else:
|
|
408
|
+
return f"Error: {result.stderr}"
|
|
409
|
+
except Exception as e:
|
|
410
|
+
return f"Error executing command: {str(e)}"
|
|
411
|
+
|
|
412
|
+
async def query_api(self, new_messages: List[Message]):
|
|
413
|
+
"""Query the AI API - equivalent to query function"""
|
|
414
|
+
logger.info("Querying AI API")
|
|
415
|
+
|
|
416
|
+
# This would integrate with the actual AI API
|
|
417
|
+
# For now, create a mock response
|
|
418
|
+
if new_messages and new_messages[-1].type == MessageType.USER:
|
|
419
|
+
user_content = new_messages[-1].message.content
|
|
420
|
+
|
|
421
|
+
# Mock AI response
|
|
422
|
+
response_content = f"I received your message: {user_content}"
|
|
423
|
+
|
|
424
|
+
assistant_message = Message(
|
|
425
|
+
type=MessageType.ASSISTANT,
|
|
426
|
+
message=MessageContent(response_content)
|
|
427
|
+
)
|
|
428
|
+
|
|
429
|
+
# Update messages
|
|
430
|
+
self.messages = [*self.messages, assistant_message]
|
|
431
|
+
|
|
432
|
+
# Handle Koding mode special case
|
|
433
|
+
if (new_messages[-1].options and
|
|
434
|
+
new_messages[-1].options.get("isKodingRequest")):
|
|
435
|
+
await self.handle_koding_response(assistant_message)
|
|
436
|
+
|
|
437
|
+
async def handle_koding_response(self, assistant_message: Message):
|
|
438
|
+
"""Handle Koding mode response - equivalent to handleHashCommand"""
|
|
439
|
+
logger.info("Handling Koding response")
|
|
440
|
+
|
|
441
|
+
content = assistant_message.message.content
|
|
442
|
+
if isinstance(content, str) and content.strip():
|
|
443
|
+
# Save to AGENTS.md (equivalent to handleHashCommand)
|
|
444
|
+
try:
|
|
445
|
+
agents_md_path = Path("AGENTS.md")
|
|
446
|
+
if agents_md_path.exists():
|
|
447
|
+
with open(agents_md_path, "a") as f:
|
|
448
|
+
f.write(f"\n\n## Koding Response - {time.strftime('%Y-%m-%d %H:%M:%S')}\n\n")
|
|
449
|
+
f.write(content)
|
|
450
|
+
f.write("\n")
|
|
451
|
+
logger.info("Saved Koding response to AGENTS.md")
|
|
452
|
+
except Exception as e:
|
|
453
|
+
logger.error(f"Error saving to AGENTS.md: {e}")
|
|
454
|
+
|
|
455
|
+
def add_to_history(self, command: str):
|
|
456
|
+
"""Add command to history - equivalent to addToHistory"""
|
|
457
|
+
# This would integrate with the history system
|
|
458
|
+
logger.info(f"Added to history: {command[:50]}...")
|
|
459
|
+
|
|
460
|
+
def on_cancel(self):
|
|
461
|
+
"""Cancel current operation - equivalent to onCancel function"""
|
|
462
|
+
if not self.is_loading:
|
|
463
|
+
return
|
|
464
|
+
|
|
465
|
+
self.is_loading = False
|
|
466
|
+
|
|
467
|
+
if self.tool_use_confirm:
|
|
468
|
+
self.tool_use_confirm.on_abort()
|
|
469
|
+
elif self.abort_controller:
|
|
470
|
+
self.abort_controller.cancel()
|
|
471
|
+
|
|
472
|
+
logger.info("Operation cancelled")
|
|
473
|
+
|
|
474
|
+
# Callback methods for PromptInput component
|
|
475
|
+
async def on_query_from_prompt(self, messages: List[Message], abort_controller=None):
|
|
476
|
+
"""Handle query from PromptInput - equivalent to onQuery prop"""
|
|
477
|
+
logger.info(f"Received query from PromptInput with {len(messages)} messages")
|
|
478
|
+
|
|
479
|
+
# Use passed AbortController or create new one
|
|
480
|
+
controller_to_use = abort_controller or asyncio.create_task(asyncio.sleep(0))
|
|
481
|
+
if not abort_controller:
|
|
482
|
+
self.abort_controller = controller_to_use
|
|
483
|
+
|
|
484
|
+
# Update messages
|
|
485
|
+
self.messages = [*self.messages, *messages]
|
|
486
|
+
|
|
487
|
+
# Query API
|
|
488
|
+
await self.query_api(messages)
|
|
489
|
+
|
|
490
|
+
def on_input_change_from_prompt(self, value: str):
|
|
491
|
+
"""Handle input change from PromptInput"""
|
|
492
|
+
self.input_value = value
|
|
493
|
+
|
|
494
|
+
def on_mode_change_from_prompt(self, mode: InputMode):
|
|
495
|
+
"""Handle mode change from PromptInput"""
|
|
496
|
+
self.input_mode = mode
|
|
497
|
+
|
|
498
|
+
def on_submit_count_change_from_prompt(self, updater):
|
|
499
|
+
"""Handle submit count change from PromptInput"""
|
|
500
|
+
if callable(updater):
|
|
501
|
+
self.submit_count = updater(self.submit_count)
|
|
502
|
+
else:
|
|
503
|
+
self.submit_count = updater
|
|
504
|
+
|
|
505
|
+
def set_loading_from_prompt(self, is_loading: bool):
|
|
506
|
+
"""Set loading state from PromptInput"""
|
|
507
|
+
self.is_loading = is_loading
|
|
508
|
+
|
|
509
|
+
def set_abort_controller_from_prompt(self, controller):
|
|
510
|
+
"""Set abort controller from PromptInput"""
|
|
511
|
+
self.abort_controller = controller
|
|
512
|
+
|
|
513
|
+
def show_message_selector(self):
|
|
514
|
+
"""Show message selector from PromptInput"""
|
|
515
|
+
self.is_message_selector_visible = True
|
|
516
|
+
|
|
517
|
+
def set_fork_convo_messages(self, messages: List[Message]):
|
|
518
|
+
"""Set fork conversation messages from PromptInput"""
|
|
519
|
+
self.fork_convo_with_messages_on_next_render = messages
|
|
520
|
+
|
|
521
|
+
def on_model_change_from_prompt(self):
|
|
522
|
+
"""Handle model change from PromptInput"""
|
|
523
|
+
self.fork_number += 1
|
|
524
|
+
logger.info("Model changed, incrementing fork number")
|
|
525
|
+
|
|
526
|
+
def set_tool_jsx_from_prompt(self, tool_jsx):
|
|
527
|
+
"""Set tool JSX from PromptInput"""
|
|
528
|
+
self.tool_jsx = tool_jsx
|
|
529
|
+
|
|
530
|
+
@on(Button.Pressed, "#acknowledge_btn")
|
|
531
|
+
def acknowledge_cost_dialog(self):
|
|
532
|
+
"""Acknowledge cost threshold dialog"""
|
|
533
|
+
self.show_cost_dialog = False
|
|
534
|
+
self.have_shown_cost_dialog = True
|
|
535
|
+
self.config.has_acknowledged_cost_threshold = True
|
|
536
|
+
logger.info("Cost threshold acknowledged")
|
|
537
|
+
|
|
538
|
+
def normalize_messages(self) -> List[Message]:
|
|
539
|
+
"""Normalize messages - equivalent to normalizeMessages function"""
|
|
540
|
+
# Filter out empty messages and normalize structure
|
|
541
|
+
return [msg for msg in self.messages if self.is_not_empty_message(msg)]
|
|
542
|
+
|
|
543
|
+
def is_not_empty_message(self, message: Message) -> bool:
|
|
544
|
+
"""Check if message is not empty - equivalent to isNotEmptyMessage"""
|
|
545
|
+
if isinstance(message.message.content, str):
|
|
546
|
+
return bool(message.message.content.strip())
|
|
547
|
+
return bool(message.message.content)
|
|
548
|
+
|
|
549
|
+
def get_unresolved_tool_use_ids(self) -> Set[str]:
|
|
550
|
+
"""Get unresolved tool use IDs - equivalent to getUnresolvedToolUseIDs"""
|
|
551
|
+
# This would analyze messages for unresolved tool uses
|
|
552
|
+
return set()
|
|
553
|
+
|
|
554
|
+
def get_in_progress_tool_use_ids(self) -> Set[str]:
|
|
555
|
+
"""Get in-progress tool use IDs - equivalent to getInProgressToolUseIDs"""
|
|
556
|
+
# This would analyze messages for in-progress tool uses
|
|
557
|
+
return set()
|
|
558
|
+
|
|
559
|
+
def get_errored_tool_use_ids(self) -> Set[str]:
|
|
560
|
+
"""Get errored tool use IDs - equivalent to getErroredToolUseMessages"""
|
|
561
|
+
# This would analyze messages for errored tool uses
|
|
562
|
+
return set()
|
|
563
|
+
|
|
564
|
+
# Reactive property watchers (equivalent to React useEffect)
|
|
565
|
+
def watch_fork_number(self, fork_number: int):
|
|
566
|
+
"""Watch fork number changes"""
|
|
567
|
+
logger.info(f"Fork number changed to: {fork_number}")
|
|
568
|
+
|
|
569
|
+
def watch_is_loading(self, is_loading: bool):
|
|
570
|
+
"""Watch loading state changes"""
|
|
571
|
+
logger.info(f"Loading state changed to: {is_loading}")
|
|
572
|
+
|
|
573
|
+
def watch_messages(self, messages: List[Message]):
|
|
574
|
+
"""Watch messages changes - equivalent to useEffect([messages], ...)"""
|
|
575
|
+
logger.info(f"Messages updated, count: {len(messages)}")
|
|
576
|
+
|
|
577
|
+
# Check cost threshold (equivalent to cost threshold useEffect)
|
|
578
|
+
total_cost = self.get_total_cost()
|
|
579
|
+
if (total_cost >= 5.0 and
|
|
580
|
+
not self.show_cost_dialog and
|
|
581
|
+
not self.have_shown_cost_dialog):
|
|
582
|
+
self.show_cost_dialog = True
|
|
583
|
+
|
|
584
|
+
def get_total_cost(self) -> float:
|
|
585
|
+
"""Get total API cost - equivalent to getTotalCost"""
|
|
586
|
+
# This would calculate actual API costs
|
|
587
|
+
return len(self.messages) * 0.01 # Mock cost calculation
|
|
588
|
+
|
|
589
|
+
def _get_mode_prefix(self) -> str:
|
|
590
|
+
"""Get the mode prefix character"""
|
|
591
|
+
if self.input_mode == InputMode.BASH:
|
|
592
|
+
return "!"
|
|
593
|
+
elif self.input_mode == InputMode.KODING:
|
|
594
|
+
return "#"
|
|
595
|
+
else:
|
|
596
|
+
return ">"
|
|
597
|
+
|
|
598
|
+
# Simplified event handlers for debugging
|
|
599
|
+
@on(Input.Changed, "#simple_input")
|
|
600
|
+
def on_simple_input_changed(self, event):
|
|
601
|
+
"""Handle simple input changes"""
|
|
602
|
+
self.input_value = event.value
|
|
603
|
+
logger.info(f"Input changed: {event.value}")
|
|
604
|
+
|
|
605
|
+
@on(Input.Submitted, "#simple_input")
|
|
606
|
+
@on(Button.Pressed, "#simple_send")
|
|
607
|
+
async def on_simple_submit(self, event):
|
|
608
|
+
"""Handle simple input submission"""
|
|
609
|
+
input_widget = self.query_one("#simple_input", expect_type=Input)
|
|
610
|
+
input_text = input_widget.value.strip()
|
|
611
|
+
|
|
612
|
+
if not input_text:
|
|
613
|
+
return
|
|
614
|
+
|
|
615
|
+
logger.info(f"Submitting: {input_text}")
|
|
616
|
+
|
|
617
|
+
# Add user message to display
|
|
618
|
+
user_message = Message(
|
|
619
|
+
type=MessageType.USER,
|
|
620
|
+
message=MessageContent(input_text),
|
|
621
|
+
options={"mode": self.input_mode.value}
|
|
622
|
+
)
|
|
623
|
+
self.messages = [*self.messages, user_message]
|
|
624
|
+
|
|
625
|
+
# Create simple response
|
|
626
|
+
response_text = f"Received: {input_text} (mode: {self.input_mode.value})"
|
|
627
|
+
assistant_message = Message(
|
|
628
|
+
type=MessageType.ASSISTANT,
|
|
629
|
+
message=MessageContent(response_text)
|
|
630
|
+
)
|
|
631
|
+
self.messages = [*self.messages, assistant_message]
|
|
632
|
+
|
|
633
|
+
# Clear input
|
|
634
|
+
input_widget.value = ""
|
|
635
|
+
self.input_value = ""
|
|
636
|
+
|
|
637
|
+
# Keep focus
|
|
638
|
+
input_widget.focus()
|
|
639
|
+
|
|
640
|
+
@on(Button.Pressed, "#simple_mode")
|
|
641
|
+
def on_simple_mode_change(self):
|
|
642
|
+
"""Handle mode change"""
|
|
643
|
+
modes = list(InputMode)
|
|
644
|
+
current_index = modes.index(self.input_mode)
|
|
645
|
+
self.input_mode = modes[(current_index + 1) % len(modes)]
|
|
646
|
+
|
|
647
|
+
# Update mode indicator
|
|
648
|
+
try:
|
|
649
|
+
mode_indicator = self.query_one("#mode_indicator", expect_type=Static)
|
|
650
|
+
mode_indicator.update(f" {self._get_mode_prefix()} ")
|
|
651
|
+
|
|
652
|
+
# Update input placeholder
|
|
653
|
+
input_widget = self.query_one("#simple_input", expect_type=Input)
|
|
654
|
+
input_widget.placeholder = f"Enter {self.input_mode.value} command..."
|
|
655
|
+
except:
|
|
656
|
+
pass
|
|
657
|
+
|
|
658
|
+
logger.info(f"Mode changed to: {self.input_mode.value}")
|
|
659
|
+
|
|
660
|
+
|
|
661
|
+
class REPLApp(App):
|
|
662
|
+
"""
|
|
663
|
+
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
|
+
}
|
|
815
|
+
"""
|
|
816
|
+
|
|
817
|
+
def __init__(self, **kwargs):
|
|
818
|
+
super().__init__(**kwargs)
|
|
819
|
+
# Initialize with default props (equivalent to React props)
|
|
820
|
+
self.repl_props = {
|
|
821
|
+
"commands": [],
|
|
822
|
+
"safe_mode": False,
|
|
823
|
+
"debug": False,
|
|
824
|
+
"initial_fork_number": 0,
|
|
825
|
+
"initial_prompt": None,
|
|
826
|
+
"message_log_name": "default",
|
|
827
|
+
"should_show_prompt_input": True,
|
|
828
|
+
"tools": [],
|
|
829
|
+
"verbose": False,
|
|
830
|
+
"initial_messages": [],
|
|
831
|
+
"mcp_clients": [],
|
|
832
|
+
"is_default_model": True,
|
|
833
|
+
"initial_update_version": None,
|
|
834
|
+
"initial_update_commands": None
|
|
835
|
+
}
|
|
836
|
+
|
|
837
|
+
def compose(self) -> ComposeResult:
|
|
838
|
+
"""Compose the main application - equivalent to React App render"""
|
|
839
|
+
yield Header(show_clock=True)
|
|
840
|
+
yield REPL(**self.repl_props)
|
|
841
|
+
yield Footer()
|
|
842
|
+
|
|
843
|
+
def on_mount(self):
|
|
844
|
+
"""Application mount lifecycle"""
|
|
845
|
+
self.title = "Minion Code Assistant"
|
|
846
|
+
logger.info("REPL Application started")
|
|
847
|
+
|
|
848
|
+
|
|
849
|
+
# Utility functions equivalent to TypeScript utility functions
|
|
850
|
+
def should_render_statically(
|
|
851
|
+
message: Message,
|
|
852
|
+
messages: List[Message],
|
|
853
|
+
unresolved_tool_use_ids: Set[str]
|
|
854
|
+
) -> bool:
|
|
855
|
+
"""
|
|
856
|
+
Determine if message should render statically
|
|
857
|
+
Equivalent to shouldRenderStatically function in TypeScript
|
|
858
|
+
"""
|
|
859
|
+
if message.type in [MessageType.USER, MessageType.ASSISTANT]:
|
|
860
|
+
# For now, render all user and assistant messages statically
|
|
861
|
+
return True
|
|
862
|
+
elif message.type == MessageType.PROGRESS:
|
|
863
|
+
# Progress messages depend on tool use resolution
|
|
864
|
+
return len(unresolved_tool_use_ids) == 0
|
|
865
|
+
return True
|
|
866
|
+
|
|
867
|
+
def intersects(set_a: Set[str], set_b: Set[str]) -> bool:
|
|
868
|
+
"""Check if two sets intersect - equivalent to intersects function"""
|
|
869
|
+
return len(set_a & set_b) > 0
|
|
870
|
+
|
|
871
|
+
|
|
872
|
+
# Factory function to create REPL with specific configuration
|
|
873
|
+
def create_repl(
|
|
874
|
+
commands=None,
|
|
875
|
+
safe_mode=False,
|
|
876
|
+
debug=False,
|
|
877
|
+
initial_prompt=None,
|
|
878
|
+
verbose=False,
|
|
879
|
+
**kwargs
|
|
880
|
+
) -> REPLApp:
|
|
881
|
+
"""
|
|
882
|
+
Create a configured REPL application
|
|
883
|
+
Equivalent to calling REPL component with props in React
|
|
884
|
+
"""
|
|
885
|
+
app = REPLApp()
|
|
886
|
+
app.repl_props.update({
|
|
887
|
+
"commands": commands or [],
|
|
888
|
+
"safe_mode": safe_mode,
|
|
889
|
+
"debug": debug,
|
|
890
|
+
"initial_prompt": initial_prompt,
|
|
891
|
+
"verbose": verbose,
|
|
892
|
+
**kwargs
|
|
893
|
+
})
|
|
894
|
+
return app
|
|
895
|
+
|
|
896
|
+
|
|
897
|
+
def run(initial_prompt=None, debug=False, verbose=False):
|
|
898
|
+
"""Run the REPL application with optional configuration"""
|
|
899
|
+
app = create_repl(
|
|
900
|
+
initial_prompt=initial_prompt,
|
|
901
|
+
debug=debug,
|
|
902
|
+
verbose=verbose
|
|
903
|
+
)
|
|
904
|
+
app.run()
|
|
905
|
+
|
|
906
|
+
|
|
907
|
+
if __name__ == "__main__":
|
|
908
|
+
import sys
|
|
909
|
+
|
|
910
|
+
# Parse command line arguments (basic implementation)
|
|
911
|
+
initial_prompt = None
|
|
912
|
+
debug = False
|
|
913
|
+
verbose = False
|
|
914
|
+
|
|
915
|
+
if len(sys.argv) > 1:
|
|
916
|
+
if "--debug" in sys.argv:
|
|
917
|
+
debug = True
|
|
918
|
+
if "--verbose" in sys.argv:
|
|
919
|
+
verbose = True
|
|
920
|
+
if "--prompt" in sys.argv:
|
|
921
|
+
prompt_index = sys.argv.index("--prompt")
|
|
922
|
+
if prompt_index + 1 < len(sys.argv):
|
|
923
|
+
initial_prompt = sys.argv[prompt_index + 1]
|
|
924
|
+
|
|
925
|
+
run(initial_prompt=initial_prompt, debug=debug, verbose=verbose)
|