minion-code 0.1.0__py3-none-any.whl → 0.1.2__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.2.dist-info/METADATA +476 -0
- minion_code-0.1.2.dist-info/RECORD +111 -0
- {minion_code-0.1.0.dist-info → minion_code-0.1.2.dist-info}/WHEEL +1 -1
- minion_code-0.1.2.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.2.dist-info}/licenses/LICENSE +0 -0
- {minion_code-0.1.0.dist-info → minion_code-0.1.2.dist-info}/top_level.txt +0 -0
|
@@ -4,10 +4,11 @@ Handles user input with multiple modes (prompt, bash, koding)
|
|
|
4
4
|
"""
|
|
5
5
|
|
|
6
6
|
from textual.containers import Container, Horizontal, Vertical
|
|
7
|
-
from textual.widgets import Input, Static, Button
|
|
7
|
+
from textual.widgets import Input, Static, Button, TextArea
|
|
8
8
|
from textual.reactive import reactive, var
|
|
9
9
|
from textual import on, work
|
|
10
10
|
from textual.events import Key
|
|
11
|
+
from textual.message import Message
|
|
11
12
|
from rich.text import Text
|
|
12
13
|
from typing import List, Dict, Any, Optional, Callable, Union
|
|
13
14
|
from dataclasses import dataclass
|
|
@@ -15,30 +16,76 @@ from enum import Enum
|
|
|
15
16
|
import asyncio
|
|
16
17
|
import time
|
|
17
18
|
|
|
18
|
-
#
|
|
19
|
-
import logging
|
|
20
|
-
logger = logging.getLogger(__name__)
|
|
21
|
-
logger.disabled = True
|
|
19
|
+
# No logging in UI components to reduce noise
|
|
22
20
|
|
|
23
21
|
# Import shared types
|
|
24
|
-
from ..
|
|
25
|
-
InputMode,
|
|
22
|
+
from ..type_defs import (
|
|
23
|
+
InputMode,
|
|
24
|
+
Message as MinionMessage,
|
|
25
|
+
MessageType,
|
|
26
|
+
MessageContent,
|
|
27
|
+
ModelInfo,
|
|
26
28
|
)
|
|
27
29
|
|
|
28
30
|
|
|
31
|
+
class CustomTextArea(TextArea):
|
|
32
|
+
"""Custom TextArea with adaptive height and key event posting"""
|
|
33
|
+
|
|
34
|
+
DEFAULT_CSS = """
|
|
35
|
+
CustomTextArea {
|
|
36
|
+
height: auto;
|
|
37
|
+
min-height: 1;
|
|
38
|
+
max-height: 10;
|
|
39
|
+
width: 1fr;
|
|
40
|
+
border: solid white;
|
|
41
|
+
}
|
|
42
|
+
"""
|
|
43
|
+
|
|
44
|
+
# Inherit COMPONENT_CLASSES from TextArea
|
|
45
|
+
COMPONENT_CLASSES = TextArea.COMPONENT_CLASSES
|
|
46
|
+
|
|
47
|
+
class KeyPressed(Message):
|
|
48
|
+
"""Message posted when a key is pressed"""
|
|
49
|
+
|
|
50
|
+
def __init__(self, key: str) -> None:
|
|
51
|
+
super().__init__()
|
|
52
|
+
self.key = key
|
|
53
|
+
|
|
54
|
+
def on_key(self, event: Key) -> bool:
|
|
55
|
+
"""Handle key events and post to parent"""
|
|
56
|
+
# Post key event to parent for handling
|
|
57
|
+
self.post_message(self.KeyPressed(event.key))
|
|
58
|
+
|
|
59
|
+
# Handle Ctrl+Enter, Tab, and Ctrl+J - prevent default, let parent add newline manually
|
|
60
|
+
if event.key in ["tab"]:
|
|
61
|
+
event.prevent_default()
|
|
62
|
+
event.stop()
|
|
63
|
+
return True
|
|
64
|
+
if event.key in ["ctrl+enter", "ctrl+j"]:
|
|
65
|
+
return True # Prevent TextArea from handling, parent will add newline
|
|
66
|
+
|
|
67
|
+
# Handle Enter - prevent default and let parent handle
|
|
68
|
+
if event.key == "enter":
|
|
69
|
+
event.prevent_default()
|
|
70
|
+
event.stop()
|
|
71
|
+
return True # Prevent TextArea from handling
|
|
72
|
+
|
|
73
|
+
# Let TextArea handle all other keys normally
|
|
74
|
+
return False
|
|
75
|
+
|
|
76
|
+
|
|
29
77
|
class PromptInput(Container):
|
|
30
78
|
"""
|
|
31
79
|
Main input component equivalent to React PromptInput
|
|
32
80
|
Handles user input with mode switching and command processing
|
|
33
81
|
"""
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
CSS = """
|
|
82
|
+
|
|
83
|
+
DEFAULT_CSS = """
|
|
37
84
|
PromptInput {
|
|
38
85
|
dock: bottom;
|
|
39
|
-
height:
|
|
40
|
-
|
|
41
|
-
|
|
86
|
+
height: auto;
|
|
87
|
+
max-height: 15;
|
|
88
|
+
margin: 0 1 1 1;
|
|
42
89
|
padding: 1;
|
|
43
90
|
}
|
|
44
91
|
|
|
@@ -50,14 +97,18 @@ class PromptInput(Container):
|
|
|
50
97
|
border: solid cyan;
|
|
51
98
|
}
|
|
52
99
|
|
|
100
|
+
.input-row {
|
|
101
|
+
height: auto;
|
|
102
|
+
width: 1fr;
|
|
103
|
+
}
|
|
104
|
+
|
|
53
105
|
#mode_prefix {
|
|
54
|
-
width:
|
|
106
|
+
width: 4;
|
|
107
|
+
min-width: 4;
|
|
108
|
+
max-width: 4;
|
|
55
109
|
content-align: center middle;
|
|
56
110
|
text-style: bold;
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
#main_input {
|
|
60
|
-
width: 1fr;
|
|
111
|
+
margin-right: 1;
|
|
61
112
|
}
|
|
62
113
|
|
|
63
114
|
.help-text {
|
|
@@ -72,8 +123,15 @@ class PromptInput(Container):
|
|
|
72
123
|
background: gray 10%;
|
|
73
124
|
color: white;
|
|
74
125
|
}
|
|
75
|
-
"""
|
|
76
126
|
|
|
127
|
+
CustomTextArea {
|
|
128
|
+
width: 1fr;
|
|
129
|
+
height: auto;
|
|
130
|
+
min-height: 1;
|
|
131
|
+
max-height: 10;
|
|
132
|
+
}
|
|
133
|
+
"""
|
|
134
|
+
|
|
77
135
|
# Reactive properties equivalent to React useState
|
|
78
136
|
mode = reactive(InputMode.PROMPT)
|
|
79
137
|
input_value = reactive("")
|
|
@@ -81,7 +139,7 @@ class PromptInput(Container):
|
|
|
81
139
|
is_loading = reactive(False)
|
|
82
140
|
submit_count = reactive(0)
|
|
83
141
|
cursor_offset = reactive(0)
|
|
84
|
-
|
|
142
|
+
|
|
85
143
|
# State for messages and UI feedback
|
|
86
144
|
exit_message = var(dict) # {"show": bool, "key": str}
|
|
87
145
|
message = var(dict) # {"show": bool, "text": str}
|
|
@@ -89,25 +147,27 @@ class PromptInput(Container):
|
|
|
89
147
|
pasted_image = var(None) # Optional[str]
|
|
90
148
|
pasted_text = var(None) # Optional[str]
|
|
91
149
|
placeholder = reactive("")
|
|
92
|
-
|
|
93
|
-
def __init__(
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
150
|
+
|
|
151
|
+
def __init__(
|
|
152
|
+
self,
|
|
153
|
+
commands=None,
|
|
154
|
+
fork_number=0,
|
|
155
|
+
message_log_name="default",
|
|
156
|
+
is_disabled=False,
|
|
157
|
+
is_loading=False,
|
|
158
|
+
debug=False,
|
|
159
|
+
verbose=False,
|
|
160
|
+
messages=None,
|
|
161
|
+
tools=None,
|
|
162
|
+
input_value="",
|
|
163
|
+
mode=InputMode.PROMPT,
|
|
164
|
+
submit_count=0,
|
|
165
|
+
read_file_timestamps=None,
|
|
166
|
+
abort_controller=None,
|
|
167
|
+
**kwargs,
|
|
168
|
+
):
|
|
109
169
|
super().__init__(**kwargs)
|
|
110
|
-
|
|
170
|
+
|
|
111
171
|
# Props equivalent to TypeScript Props interface
|
|
112
172
|
self.commands = commands or []
|
|
113
173
|
self.fork_number = fork_number
|
|
@@ -118,7 +178,7 @@ class PromptInput(Container):
|
|
|
118
178
|
self.tools = tools or []
|
|
119
179
|
self.read_file_timestamps = read_file_timestamps or {}
|
|
120
180
|
self.abort_controller = abort_controller
|
|
121
|
-
|
|
181
|
+
|
|
122
182
|
# Initialize reactive state
|
|
123
183
|
self.mode = mode
|
|
124
184
|
self.input_value = input_value
|
|
@@ -126,7 +186,7 @@ class PromptInput(Container):
|
|
|
126
186
|
self.is_loading = is_loading
|
|
127
187
|
self.submit_count = submit_count
|
|
128
188
|
self.cursor_offset = len(input_value)
|
|
129
|
-
|
|
189
|
+
|
|
130
190
|
# Initialize state variables
|
|
131
191
|
self.exit_message = {"show": False, "key": ""}
|
|
132
192
|
self.message = {"show": False, "text": ""}
|
|
@@ -134,9 +194,12 @@ class PromptInput(Container):
|
|
|
134
194
|
self.pasted_image = None
|
|
135
195
|
self.pasted_text = None
|
|
136
196
|
self.placeholder = ""
|
|
137
|
-
|
|
197
|
+
|
|
138
198
|
# Callbacks (would be passed as props in React)
|
|
139
199
|
self.on_query: Optional[Callable] = None
|
|
200
|
+
self.on_add_user_message: Optional[Callable] = (
|
|
201
|
+
None # New callback for immediate message display
|
|
202
|
+
)
|
|
140
203
|
self.on_input_change: Optional[Callable] = None
|
|
141
204
|
self.on_mode_change: Optional[Callable] = None
|
|
142
205
|
self.on_submit_count_change: Optional[Callable] = None
|
|
@@ -146,36 +209,43 @@ class PromptInput(Container):
|
|
|
146
209
|
self.set_fork_convo_with_messages: Optional[Callable] = None
|
|
147
210
|
self.on_model_change: Optional[Callable] = None
|
|
148
211
|
self.set_tool_jsx: Optional[Callable] = None
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
212
|
+
self.on_execute_command: Optional[Callable] = (
|
|
213
|
+
None # Callback for executing / commands
|
|
214
|
+
)
|
|
215
|
+
|
|
152
216
|
def on_mount(self):
|
|
153
217
|
"""Set focus to input when component mounts"""
|
|
154
218
|
try:
|
|
155
|
-
input_widget = self.query_one("#main_input", expect_type=
|
|
219
|
+
input_widget = self.query_one("#main_input", expect_type=CustomTextArea)
|
|
156
220
|
input_widget.focus()
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
221
|
+
except Exception:
|
|
222
|
+
pass # Silently handle focus errors
|
|
223
|
+
|
|
161
224
|
def compose(self):
|
|
162
|
-
"""Compose the PromptInput interface
|
|
163
|
-
#
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
225
|
+
"""Compose the PromptInput interface with loading indicator"""
|
|
226
|
+
# Loading indicator - replaces "BEFORE TextArea - Ready"
|
|
227
|
+
if self.is_loading:
|
|
228
|
+
yield Static(
|
|
229
|
+
"⠋ Assistant is thinking...",
|
|
230
|
+
id="loading_status",
|
|
231
|
+
classes="loading-status",
|
|
232
|
+
)
|
|
233
|
+
|
|
234
|
+
# Input area with mode prefix in horizontal layout
|
|
235
|
+
with Horizontal(classes="input-row"):
|
|
171
236
|
yield Static(self._get_mode_prefix(), id="mode_prefix")
|
|
172
|
-
yield
|
|
173
|
-
|
|
174
|
-
value=self.input_value,
|
|
237
|
+
yield CustomTextArea(
|
|
238
|
+
text=self.input_value,
|
|
175
239
|
id="main_input",
|
|
176
|
-
disabled=self.is_disabled or self.is_loading
|
|
240
|
+
disabled=self.is_disabled or self.is_loading,
|
|
241
|
+
show_line_numbers=False,
|
|
177
242
|
)
|
|
178
|
-
|
|
243
|
+
|
|
244
|
+
yield Static(
|
|
245
|
+
"Enter to submit · Ctrl+Enter/Ctrl+J/Tab for new line · ! for bash · # for AGENTS.md",
|
|
246
|
+
classes="help-text",
|
|
247
|
+
)
|
|
248
|
+
|
|
179
249
|
def _render_model_info(self) -> Static:
|
|
180
250
|
"""Render model information - equivalent to model info display"""
|
|
181
251
|
model_info = self._get_model_info()
|
|
@@ -183,9 +253,9 @@ class PromptInput(Container):
|
|
|
183
253
|
info_text = f"[{model_info.provider}] {model_info.name}: {model_info.current_tokens//1000}k / {model_info.context_length//1000}k"
|
|
184
254
|
return Static(info_text, id="model_info", classes="model-info")
|
|
185
255
|
return Static("", id="model_info")
|
|
186
|
-
|
|
187
|
-
# _render_status_area method removed - content moved to compose method
|
|
188
|
-
|
|
256
|
+
|
|
257
|
+
# _render_status_area method removed - content moved to compose method
|
|
258
|
+
|
|
189
259
|
def _get_mode_prefix(self) -> str:
|
|
190
260
|
"""Get the mode prefix character"""
|
|
191
261
|
if self.mode == InputMode.BASH:
|
|
@@ -194,19 +264,19 @@ class PromptInput(Container):
|
|
|
194
264
|
return " # "
|
|
195
265
|
else:
|
|
196
266
|
return " > "
|
|
197
|
-
|
|
267
|
+
|
|
198
268
|
def _get_placeholder(self) -> str:
|
|
199
269
|
"""Get placeholder text based on current mode"""
|
|
200
270
|
if self.placeholder:
|
|
201
271
|
return self.placeholder
|
|
202
|
-
|
|
272
|
+
|
|
203
273
|
if self.mode == InputMode.BASH:
|
|
204
274
|
return "Enter bash command..."
|
|
205
275
|
elif self.mode == InputMode.KODING:
|
|
206
276
|
return "Enter note for AGENTS.md..."
|
|
207
277
|
else:
|
|
208
278
|
return "Enter your message..."
|
|
209
|
-
|
|
279
|
+
|
|
210
280
|
def _get_model_info(self) -> Optional[ModelInfo]:
|
|
211
281
|
"""Get current model information - equivalent to modelInfo useMemo"""
|
|
212
282
|
# This would integrate with the actual model manager
|
|
@@ -216,144 +286,245 @@ class PromptInput(Container):
|
|
|
216
286
|
provider="anthropic",
|
|
217
287
|
context_length=200000,
|
|
218
288
|
current_tokens=len(str(self.messages)) * 4, # Rough token estimate
|
|
219
|
-
id="claude-3-5-sonnet"
|
|
289
|
+
id="claude-3-5-sonnet",
|
|
220
290
|
)
|
|
221
|
-
|
|
291
|
+
|
|
222
292
|
# Event handlers
|
|
223
|
-
@on(
|
|
224
|
-
def
|
|
225
|
-
"""Handle
|
|
226
|
-
value = event.
|
|
227
|
-
|
|
293
|
+
@on(TextArea.Changed, "#main_input")
|
|
294
|
+
def on_textarea_changed(self, event: TextArea.Changed):
|
|
295
|
+
"""Handle TextArea content changes"""
|
|
296
|
+
value = event.text_area.text
|
|
297
|
+
|
|
228
298
|
# Handle mode switching based on input prefix
|
|
229
|
-
if value.startswith(
|
|
299
|
+
if value.startswith("!"):
|
|
230
300
|
if self.mode != InputMode.BASH:
|
|
231
301
|
self.mode = InputMode.BASH
|
|
232
302
|
if self.on_mode_change:
|
|
233
303
|
self.on_mode_change(InputMode.BASH)
|
|
234
|
-
elif value.startswith(
|
|
304
|
+
elif value.startswith("#"):
|
|
235
305
|
if self.mode != InputMode.KODING:
|
|
236
306
|
self.mode = InputMode.KODING
|
|
237
307
|
if self.on_mode_change:
|
|
238
308
|
self.on_mode_change(InputMode.KODING)
|
|
239
|
-
|
|
309
|
+
|
|
240
310
|
self.input_value = value
|
|
241
311
|
if self.on_input_change:
|
|
242
312
|
self.on_input_change(value)
|
|
243
|
-
|
|
244
|
-
@on(
|
|
245
|
-
|
|
246
|
-
"""Handle
|
|
247
|
-
|
|
248
|
-
|
|
313
|
+
|
|
314
|
+
@on(CustomTextArea.KeyPressed)
|
|
315
|
+
def on_custom_textarea_key(self, event: CustomTextArea.KeyPressed):
|
|
316
|
+
"""Handle key events from CustomTextArea"""
|
|
317
|
+
key = event.key
|
|
318
|
+
|
|
319
|
+
if key == "enter":
|
|
320
|
+
# Regular Enter - submit
|
|
321
|
+
self.run_worker(self._handle_submit(), exclusive=True)
|
|
322
|
+
elif key in ["ctrl+enter", "ctrl+j"]:
|
|
323
|
+
# Ctrl+Enter, Tab, or Ctrl+J - manually add newline
|
|
324
|
+
self._insert_newline()
|
|
325
|
+
elif key in ["backspace", "delete"]:
|
|
326
|
+
# Handle mode reset on empty input
|
|
327
|
+
if self.mode == InputMode.BASH and not self.input_value:
|
|
328
|
+
self.mode = InputMode.PROMPT
|
|
329
|
+
if self.on_mode_change:
|
|
330
|
+
self.on_mode_change(InputMode.PROMPT)
|
|
331
|
+
elif self.mode == InputMode.KODING and not self.input_value:
|
|
332
|
+
self.mode = InputMode.PROMPT
|
|
333
|
+
if self.on_mode_change:
|
|
334
|
+
self.on_mode_change(InputMode.PROMPT)
|
|
335
|
+
elif key == "escape":
|
|
336
|
+
# Handle escape key
|
|
337
|
+
if not self.input_value and not self.is_loading and len(self.messages) > 0:
|
|
338
|
+
if self.on_show_message_selector:
|
|
339
|
+
self.on_show_message_selector()
|
|
340
|
+
else:
|
|
341
|
+
self.mode = InputMode.PROMPT
|
|
342
|
+
if self.on_mode_change:
|
|
343
|
+
self.on_mode_change(InputMode.PROMPT)
|
|
344
|
+
elif key == "shift+m":
|
|
345
|
+
# Handle model switching
|
|
346
|
+
self._handle_quick_model_switch()
|
|
347
|
+
elif key == "shift+tab":
|
|
348
|
+
# Handle mode cycling
|
|
349
|
+
self._cycle_mode()
|
|
350
|
+
|
|
351
|
+
async def _handle_submit(self):
|
|
352
|
+
"""Handle input submission with immediate UI feedback"""
|
|
353
|
+
try:
|
|
354
|
+
text_area = self.query_one("#main_input", expect_type=CustomTextArea)
|
|
355
|
+
input_text = text_area.text.strip()
|
|
356
|
+
except:
|
|
357
|
+
return
|
|
358
|
+
|
|
249
359
|
if not input_text:
|
|
250
360
|
return
|
|
251
|
-
|
|
361
|
+
|
|
252
362
|
if self.is_disabled or self.is_loading:
|
|
253
363
|
return
|
|
254
|
-
|
|
255
|
-
logger.info(f"Submitting input: {input_text[:50]}... (mode: {self.mode.value})")
|
|
256
|
-
|
|
364
|
+
|
|
257
365
|
# Handle exit commands
|
|
258
|
-
if input_text.lower() in [
|
|
366
|
+
if input_text.lower() in ["exit", "quit", ":q", ":q!", ":wq", ":wq!"]:
|
|
259
367
|
self._handle_exit()
|
|
260
368
|
return
|
|
261
|
-
|
|
262
|
-
# Handle
|
|
263
|
-
if
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
369
|
+
|
|
370
|
+
# Handle slash commands (e.g., /clear, /help, /tools)
|
|
371
|
+
if input_text.startswith("/"):
|
|
372
|
+
# Clear input immediately
|
|
373
|
+
with text_area.prevent(TextArea.Changed):
|
|
374
|
+
text_area.text = ""
|
|
375
|
+
self.input_value = ""
|
|
376
|
+
|
|
377
|
+
# Execute command (no AI processing, no "Thinking..." message)
|
|
378
|
+
await self._handle_command_input(input_text)
|
|
379
|
+
|
|
380
|
+
# Update history
|
|
381
|
+
self._add_to_history(input_text)
|
|
382
|
+
return
|
|
383
|
+
|
|
384
|
+
# 1. 立即清空输入框并重置模式 - 提供即时反馈
|
|
385
|
+
original_mode = self.mode
|
|
386
|
+
|
|
387
|
+
with text_area.prevent(TextArea.Changed):
|
|
388
|
+
text_area.text = ""
|
|
271
389
|
self.input_value = ""
|
|
272
390
|
self.mode = InputMode.PROMPT
|
|
391
|
+
|
|
273
392
|
if self.on_mode_change:
|
|
274
393
|
self.on_mode_change(InputMode.PROMPT)
|
|
275
|
-
|
|
276
|
-
#
|
|
394
|
+
|
|
395
|
+
# 2. 根据模式处理输入
|
|
396
|
+
if original_mode == InputMode.KODING:
|
|
397
|
+
# Koding 模式:直接处理笔记,不走 AI query
|
|
398
|
+
await self._handle_koding_input(input_text)
|
|
399
|
+
elif original_mode == InputMode.BASH:
|
|
400
|
+
# Bash 模式:执行命令
|
|
401
|
+
await self._handle_bash_input(input_text)
|
|
402
|
+
else:
|
|
403
|
+
# Prompt 模式:正常的 AI 对话
|
|
404
|
+
# 2a. 立即创建用户消息
|
|
405
|
+
user_message = self._create_user_message(input_text, original_mode)
|
|
406
|
+
|
|
407
|
+
# 2b. 立即显示用户消息(同步操作,不等待网络)
|
|
408
|
+
if self.on_add_user_message:
|
|
409
|
+
self.on_add_user_message(user_message)
|
|
410
|
+
|
|
411
|
+
# 2c. 启动后台AI处理 - 让父组件管理 worker
|
|
412
|
+
if self.on_query:
|
|
413
|
+
# 直接调用父组件的回调,让父组件管理 worker
|
|
414
|
+
await self.on_query([user_message])
|
|
415
|
+
|
|
416
|
+
# 3. 更新提交计数和历史记录
|
|
277
417
|
self.submit_count += 1
|
|
278
418
|
if self.on_submit_count_change:
|
|
279
419
|
self.on_submit_count_change(lambda x: x + 1)
|
|
280
|
-
|
|
420
|
+
|
|
421
|
+
self._add_to_history(input_text)
|
|
422
|
+
|
|
423
|
+
async def _handle_command_input(self, input_text: str):
|
|
424
|
+
"""
|
|
425
|
+
Handle slash command input (e.g., /clear, /help, /tools).
|
|
426
|
+
Commands are executed directly without AI processing.
|
|
427
|
+
"""
|
|
428
|
+
# Parse command: /command_name args
|
|
429
|
+
command_input = input_text[1:] if input_text.startswith("/") else input_text
|
|
430
|
+
parts = command_input.split(" ", 1)
|
|
431
|
+
command_name = parts[0].lower()
|
|
432
|
+
args = parts[1] if len(parts) > 1 else ""
|
|
433
|
+
|
|
434
|
+
# Delegate to REPL for command execution
|
|
435
|
+
if self.on_execute_command:
|
|
436
|
+
await self.on_execute_command(command_name, args)
|
|
437
|
+
else:
|
|
438
|
+
# Fallback: show error if no handler is set
|
|
439
|
+
self._show_temporary_message(
|
|
440
|
+
f"❌ Command handler not available for /{command_name}", duration=3.0
|
|
441
|
+
)
|
|
442
|
+
|
|
281
443
|
async def _handle_koding_input(self, input_text: str):
|
|
282
444
|
"""Handle koding mode input - equivalent to koding mode handling"""
|
|
283
|
-
|
|
284
|
-
|
|
445
|
+
|
|
285
446
|
# Strip # prefix if present
|
|
286
|
-
content = input_text[1:].strip() if input_text.startswith(
|
|
287
|
-
|
|
447
|
+
content = input_text[1:].strip() if input_text.startswith("#") else input_text
|
|
448
|
+
|
|
288
449
|
# Check if this is an action prompt (put, create, generate, etc.)
|
|
289
|
-
if any(
|
|
450
|
+
if any(
|
|
451
|
+
word in content.lower()
|
|
452
|
+
for word in ["put", "create", "generate", "write", "give", "provide"]
|
|
453
|
+
):
|
|
290
454
|
# Handle as AI request for AGENTS.md content
|
|
291
455
|
await self._handle_koding_ai_request(content)
|
|
292
456
|
else:
|
|
293
457
|
# Handle as direct note to AGENTS.md
|
|
294
458
|
await self._handle_koding_note(content)
|
|
295
|
-
|
|
459
|
+
|
|
296
460
|
# Add to history
|
|
297
|
-
self._add_to_history(
|
|
298
|
-
|
|
461
|
+
self._add_to_history(
|
|
462
|
+
f"#{input_text}" if not input_text.startswith("#") else input_text
|
|
463
|
+
)
|
|
464
|
+
|
|
299
465
|
async def _handle_bash_input(self, input_text: str):
|
|
300
466
|
"""Handle bash mode input - equivalent to bash command processing"""
|
|
301
|
-
|
|
302
|
-
|
|
467
|
+
|
|
303
468
|
# Strip ! prefix if present
|
|
304
|
-
command = input_text[1:].strip() if input_text.startswith(
|
|
305
|
-
|
|
469
|
+
command = input_text[1:].strip() if input_text.startswith("!") else input_text
|
|
470
|
+
|
|
306
471
|
try:
|
|
307
472
|
# Execute bash command (simplified version)
|
|
308
473
|
import subprocess
|
|
474
|
+
|
|
309
475
|
result = subprocess.run(
|
|
310
|
-
command,
|
|
311
|
-
shell=True,
|
|
312
|
-
capture_output=True,
|
|
313
|
-
text=True,
|
|
314
|
-
timeout=30
|
|
476
|
+
command, shell=True, capture_output=True, text=True, timeout=30
|
|
315
477
|
)
|
|
316
|
-
|
|
478
|
+
|
|
317
479
|
# Create assistant message with result
|
|
318
480
|
if result.returncode == 0:
|
|
319
481
|
response = result.stdout or "Command executed successfully"
|
|
320
482
|
else:
|
|
321
483
|
response = f"Error: {result.stderr}"
|
|
322
|
-
|
|
484
|
+
|
|
323
485
|
# This would typically be handled by the parent REPL component
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
except Exception
|
|
327
|
-
|
|
328
|
-
|
|
486
|
+
pass
|
|
487
|
+
|
|
488
|
+
except Exception:
|
|
489
|
+
pass # Silently handle bash command errors
|
|
490
|
+
|
|
329
491
|
# Add to history
|
|
330
|
-
self._add_to_history(
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
492
|
+
self._add_to_history(
|
|
493
|
+
f"!{input_text}" if not input_text.startswith("!") else input_text
|
|
494
|
+
)
|
|
495
|
+
|
|
496
|
+
def _create_user_message(self, input_text: str, mode: InputMode):
|
|
497
|
+
"""Create user message for immediate display"""
|
|
498
|
+
from ..type_defs import Message as MinionMessage, MessageType, MessageContent
|
|
499
|
+
|
|
500
|
+
return MinionMessage(
|
|
501
|
+
type=MessageType.USER,
|
|
502
|
+
message=MessageContent(input_text),
|
|
503
|
+
options={"mode": mode.value},
|
|
504
|
+
)
|
|
505
|
+
|
|
506
|
+
async def _handle_prompt_response(self, input_text: str):
|
|
507
|
+
"""Handle AI response for regular prompt input"""
|
|
508
|
+
|
|
336
509
|
if self.set_is_loading:
|
|
337
510
|
self.set_is_loading(True)
|
|
338
|
-
|
|
511
|
+
|
|
339
512
|
# Create new abort controller
|
|
340
513
|
if self.set_abort_controller:
|
|
341
514
|
new_controller = asyncio.create_task(asyncio.sleep(0)) # Mock controller
|
|
342
515
|
self.set_abort_controller(new_controller)
|
|
343
|
-
|
|
344
|
-
#
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
516
|
+
|
|
517
|
+
# 这里不再创建用户消息,因为已经在 _handle_submit 中创建并显示了
|
|
518
|
+
# 直接触发 AI 响应处理
|
|
519
|
+
|
|
520
|
+
async def _handle_prompt_input(self, input_text: str):
|
|
521
|
+
"""Handle regular prompt input - equivalent to normal message processing (deprecated)"""
|
|
522
|
+
# 这个方法现在被 _handle_prompt_response 替代
|
|
523
|
+
await self._handle_prompt_response(input_text)
|
|
524
|
+
|
|
353
525
|
async def _handle_koding_ai_request(self, content: str):
|
|
354
526
|
"""Handle AI request for koding mode"""
|
|
355
|
-
|
|
356
|
-
|
|
527
|
+
|
|
357
528
|
# This would integrate with the AI system to generate content for AGENTS.md
|
|
358
529
|
# For now, just log the request
|
|
359
530
|
koding_context = (
|
|
@@ -361,174 +532,257 @@ class PromptInput(Container):
|
|
|
361
532
|
"well-structured document suitable for adding to AGENTS.md. Use proper "
|
|
362
533
|
"markdown formatting with headings, lists, code blocks, etc."
|
|
363
534
|
)
|
|
364
|
-
|
|
535
|
+
|
|
365
536
|
# This would be processed by the main query system
|
|
366
537
|
if self.on_query:
|
|
367
|
-
user_message =
|
|
538
|
+
user_message = MinionMessage(
|
|
368
539
|
type=MessageType.USER,
|
|
369
540
|
message=MessageContent(content),
|
|
370
|
-
options={"isKodingRequest": True, "kodingContext": koding_context}
|
|
541
|
+
options={"isKodingRequest": True, "kodingContext": koding_context},
|
|
371
542
|
)
|
|
372
543
|
await self.on_query([user_message])
|
|
373
|
-
|
|
544
|
+
|
|
374
545
|
async def _handle_koding_note(self, content: str):
|
|
375
546
|
"""Handle direct note to AGENTS.md"""
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
547
|
+
|
|
548
|
+
# Show processing message
|
|
549
|
+
self._show_temporary_message("🤔 Formatting note with AI...", duration=30.0)
|
|
550
|
+
|
|
551
|
+
# Interpret and format the note using AI
|
|
379
552
|
try:
|
|
380
|
-
|
|
381
|
-
self._handle_hash_command(interpreted_content)
|
|
553
|
+
self._handle_hash_command(content)
|
|
382
554
|
except Exception as e:
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
formatted_content = f"# {content}\n\n_Added on {time.strftime('%Y-%m-%d %H:%M:%S')}_"
|
|
386
|
-
self._handle_hash_command(formatted_content)
|
|
387
|
-
|
|
555
|
+
pass
|
|
556
|
+
|
|
388
557
|
async def _interpret_hash_command(self, content: str) -> str:
|
|
389
|
-
"""
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
558
|
+
"""
|
|
559
|
+
Interpret hash command using AI - equivalent to interpretHashCommand.
|
|
560
|
+
|
|
561
|
+
Uses the AI to transform raw notes into well-structured content for AGENTS.md.
|
|
562
|
+
Adds appropriate markdown formatting, headings, bullet points, etc.
|
|
563
|
+
|
|
564
|
+
Args:
|
|
565
|
+
content: Raw note content from user
|
|
566
|
+
|
|
567
|
+
Returns:
|
|
568
|
+
Formatted markdown content ready for AGENTS.md
|
|
569
|
+
"""
|
|
570
|
+
try:
|
|
571
|
+
# Import query_quick for AI interpretation
|
|
572
|
+
from ..agents.code_agent import query_quick
|
|
573
|
+
|
|
574
|
+
# Get agent from parent REPL component if available
|
|
575
|
+
agent = None
|
|
576
|
+
try:
|
|
577
|
+
# Try to get agent from parent
|
|
578
|
+
parent = self.parent
|
|
579
|
+
while parent and not hasattr(parent, "agent"):
|
|
580
|
+
parent = parent.parent
|
|
581
|
+
if parent and hasattr(parent, "agent"):
|
|
582
|
+
agent = parent.agent
|
|
583
|
+
except:
|
|
584
|
+
pass
|
|
585
|
+
|
|
586
|
+
# If no agent available, fall back to simple formatting
|
|
587
|
+
if not agent:
|
|
588
|
+
return f"# {content}\n\n_Added on {time.strftime('%m/%d/%Y, %I:%M:%S %p')}_"
|
|
589
|
+
|
|
590
|
+
# Create system prompt for note interpretation
|
|
591
|
+
system_prompt = [
|
|
592
|
+
"You're helping the user structure notes that will be added to their AGENTS.md file.",
|
|
593
|
+
"Format the user's input into a well-structured note that will be useful for later reference.",
|
|
594
|
+
"Add appropriate markdown formatting, headings, bullet points, or other structural elements as needed.",
|
|
595
|
+
"The goal is to transform the raw note into something that will be more useful when reviewed later.",
|
|
596
|
+
"You should keep the original meaning but make the structure clear.",
|
|
597
|
+
]
|
|
598
|
+
|
|
599
|
+
# Send request to AI using query_quick
|
|
600
|
+
result = await query_quick(
|
|
601
|
+
agent=agent,
|
|
602
|
+
user_prompt=f"Transform this note for AGENTS.md: {content}",
|
|
603
|
+
system_prompt=system_prompt,
|
|
604
|
+
)
|
|
605
|
+
|
|
606
|
+
# Extract content from response
|
|
607
|
+
if isinstance(result, str):
|
|
608
|
+
formatted_content = result
|
|
609
|
+
else:
|
|
610
|
+
# Handle other response formats
|
|
611
|
+
formatted_content = str(result)
|
|
612
|
+
|
|
613
|
+
# Add timestamp
|
|
614
|
+
timestamp = time.strftime("%m/%d/%Y, %I:%M:%S %p")
|
|
615
|
+
if "_Added on" not in formatted_content:
|
|
616
|
+
formatted_content += f"\n\n_Added on {timestamp}_"
|
|
617
|
+
|
|
618
|
+
return formatted_content
|
|
619
|
+
|
|
620
|
+
except Exception as e:
|
|
621
|
+
# If interpretation fails, return input with minimal formatting
|
|
622
|
+
timestamp = time.strftime("%m/%d/%Y, %I:%M:%S %p")
|
|
623
|
+
return f"# {content}\n\n_Added on {timestamp}_"
|
|
624
|
+
|
|
394
625
|
def _handle_hash_command(self, content: str):
|
|
395
626
|
"""Handle hash command - equivalent to handleHashCommand"""
|
|
396
627
|
try:
|
|
397
628
|
from pathlib import Path
|
|
629
|
+
|
|
398
630
|
agents_md = Path("AGENTS.md")
|
|
399
|
-
|
|
400
|
-
if
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
631
|
+
|
|
632
|
+
# Create file if it doesn't exist
|
|
633
|
+
if not agents_md.exists():
|
|
634
|
+
with open(agents_md, "w", encoding="utf-8") as f:
|
|
635
|
+
f.write("# Agent Development Guidelines\n\n")
|
|
636
|
+
|
|
637
|
+
# Append the formatted content
|
|
638
|
+
with open(agents_md, "a", encoding="utf-8") as f:
|
|
639
|
+
f.write(f"\n\n{content}\n")
|
|
640
|
+
|
|
641
|
+
# Show success message to user
|
|
642
|
+
self._show_temporary_message(f"✅ Note added to AGENTS.md", duration=3.0)
|
|
643
|
+
|
|
406
644
|
except Exception as e:
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
645
|
+
# Show error message to user
|
|
646
|
+
self._show_temporary_message(
|
|
647
|
+
f"❌ Failed to write to AGENTS.md: {e}", duration=5.0
|
|
648
|
+
)
|
|
649
|
+
|
|
650
|
+
def _show_temporary_message(self, text: str, duration: float = 3.0):
|
|
651
|
+
"""Show a temporary message to the user"""
|
|
652
|
+
self.message = {"show": True, "text": text}
|
|
653
|
+
self.set_timer(
|
|
654
|
+
duration, lambda: setattr(self, "message", {"show": False, "text": ""})
|
|
655
|
+
)
|
|
656
|
+
|
|
657
|
+
async def _process_user_input(
|
|
658
|
+
self, input_text: str, mode: InputMode
|
|
659
|
+
) -> List[MinionMessage]:
|
|
410
660
|
"""Process user input - equivalent to processUserInput"""
|
|
411
|
-
user_message =
|
|
661
|
+
user_message = MinionMessage(
|
|
412
662
|
type=MessageType.USER,
|
|
413
663
|
message=MessageContent(input_text),
|
|
414
|
-
options={"mode": mode.value}
|
|
664
|
+
options={"mode": mode.value},
|
|
415
665
|
)
|
|
416
666
|
return [user_message]
|
|
417
|
-
|
|
667
|
+
|
|
418
668
|
def _add_to_history(self, input_text: str):
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
669
|
+
from minion_code.utils.history import add_to_history
|
|
670
|
+
|
|
671
|
+
add_to_history(input_text)
|
|
672
|
+
|
|
422
673
|
def _handle_exit(self):
|
|
423
674
|
"""Handle exit command"""
|
|
424
|
-
logger.info("Exit command received")
|
|
425
675
|
# This would typically exit the application
|
|
426
676
|
# For now, just show exit message
|
|
427
677
|
self.exit_message = {"show": True, "key": "Ctrl+C"}
|
|
428
|
-
self.set_timer(
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
"""Handle special key combinations - equivalent to useInput hook"""
|
|
433
|
-
# Handle mode switching with backspace/delete
|
|
434
|
-
if event.key in ["backspace", "delete"]:
|
|
435
|
-
if self.mode == InputMode.BASH and not self.input_value:
|
|
436
|
-
self.mode = InputMode.PROMPT
|
|
437
|
-
if self.on_mode_change:
|
|
438
|
-
self.on_mode_change(InputMode.PROMPT)
|
|
439
|
-
return True
|
|
440
|
-
elif self.mode == InputMode.KODING and not self.input_value:
|
|
441
|
-
self.mode = InputMode.PROMPT
|
|
442
|
-
if self.on_mode_change:
|
|
443
|
-
self.on_mode_change(InputMode.PROMPT)
|
|
444
|
-
return True
|
|
445
|
-
|
|
446
|
-
# Handle escape key
|
|
447
|
-
if event.key == "escape":
|
|
448
|
-
if not self.input_value and not self.is_loading and len(self.messages) > 0:
|
|
449
|
-
if self.on_show_message_selector:
|
|
450
|
-
self.on_show_message_selector()
|
|
451
|
-
return True
|
|
452
|
-
else:
|
|
453
|
-
self.mode = InputMode.PROMPT
|
|
454
|
-
if self.on_mode_change:
|
|
455
|
-
self.on_mode_change(InputMode.PROMPT)
|
|
456
|
-
return True
|
|
457
|
-
|
|
458
|
-
# Handle Shift+M for model switching
|
|
459
|
-
if event.key == "shift+m":
|
|
460
|
-
self._handle_quick_model_switch()
|
|
461
|
-
return True
|
|
462
|
-
|
|
463
|
-
# Handle Shift+Tab for mode cycling
|
|
464
|
-
if event.key == "shift+tab":
|
|
465
|
-
self._cycle_mode()
|
|
466
|
-
return True
|
|
467
|
-
|
|
468
|
-
return False
|
|
469
|
-
|
|
678
|
+
self.set_timer(
|
|
679
|
+
3.0, lambda: setattr(self, "exit_message", {"show": False, "key": ""})
|
|
680
|
+
)
|
|
681
|
+
|
|
470
682
|
def _handle_quick_model_switch(self):
|
|
471
683
|
"""Handle quick model switching - equivalent to handleQuickModelSwitch"""
|
|
472
|
-
|
|
473
|
-
|
|
684
|
+
|
|
474
685
|
# This would integrate with the model manager
|
|
475
686
|
# For now, show a mock message
|
|
476
687
|
self.model_switch_message = {
|
|
477
688
|
"show": True,
|
|
478
|
-
"text": "✅ Model switching would happen here"
|
|
689
|
+
"text": "✅ Model switching would happen here",
|
|
479
690
|
}
|
|
480
|
-
|
|
691
|
+
|
|
481
692
|
# Clear message after 3 seconds
|
|
482
|
-
self.set_timer(
|
|
483
|
-
|
|
693
|
+
self.set_timer(
|
|
694
|
+
3.0,
|
|
695
|
+
lambda: setattr(self, "model_switch_message", {"show": False, "text": ""}),
|
|
696
|
+
)
|
|
697
|
+
|
|
484
698
|
if self.on_model_change:
|
|
485
699
|
self.on_model_change()
|
|
486
|
-
|
|
700
|
+
|
|
701
|
+
def _insert_newline(self):
|
|
702
|
+
"""Insert newline at current cursor position"""
|
|
703
|
+
try:
|
|
704
|
+
text_area = self.query_one("#main_input", expect_type=CustomTextArea)
|
|
705
|
+
|
|
706
|
+
# Get current cursor position
|
|
707
|
+
cursor_row, cursor_col = text_area.cursor_location
|
|
708
|
+
|
|
709
|
+
# Get current text
|
|
710
|
+
current_text = text_area.text
|
|
711
|
+
|
|
712
|
+
# Split text into lines
|
|
713
|
+
lines = current_text.split("\n")
|
|
714
|
+
|
|
715
|
+
# Insert newline at cursor position
|
|
716
|
+
if cursor_row < len(lines):
|
|
717
|
+
line = lines[cursor_row]
|
|
718
|
+
# Split the current line at cursor position
|
|
719
|
+
before_cursor = line[:cursor_col]
|
|
720
|
+
after_cursor = line[cursor_col:]
|
|
721
|
+
|
|
722
|
+
# Replace current line with split lines
|
|
723
|
+
lines[cursor_row] = before_cursor
|
|
724
|
+
lines.insert(cursor_row + 1, after_cursor)
|
|
725
|
+
else:
|
|
726
|
+
# Cursor is beyond existing lines, just add a new line
|
|
727
|
+
lines.append("")
|
|
728
|
+
|
|
729
|
+
# Update text area with new content
|
|
730
|
+
new_text = "\n".join(lines)
|
|
731
|
+
text_area.text = new_text
|
|
732
|
+
|
|
733
|
+
# Move cursor to next line
|
|
734
|
+
text_area.cursor_location = (cursor_row + 1, 0)
|
|
735
|
+
|
|
736
|
+
# Update input value
|
|
737
|
+
self.input_value = new_text
|
|
738
|
+
if self.on_input_change:
|
|
739
|
+
self.on_input_change(new_text)
|
|
740
|
+
|
|
741
|
+
except Exception:
|
|
742
|
+
pass # Silently handle newline insertion errors
|
|
743
|
+
|
|
487
744
|
def _cycle_mode(self):
|
|
488
745
|
"""Cycle through input modes"""
|
|
489
746
|
modes = list(InputMode)
|
|
490
747
|
current_index = modes.index(self.mode)
|
|
491
748
|
new_mode = modes[(current_index + 1) % len(modes)]
|
|
492
749
|
self.mode = new_mode
|
|
493
|
-
|
|
750
|
+
|
|
494
751
|
if self.on_mode_change:
|
|
495
752
|
self.on_mode_change(new_mode)
|
|
496
|
-
|
|
497
|
-
logger.info(f"Cycled to mode: {new_mode.value}")
|
|
498
|
-
|
|
753
|
+
|
|
499
754
|
# Reactive property watchers
|
|
500
755
|
def watch_mode(self, mode: InputMode):
|
|
501
756
|
"""Watch mode changes and update UI"""
|
|
502
|
-
logger.info(f"Mode changed to: {mode.value}")
|
|
503
757
|
try:
|
|
504
758
|
# Update mode prefix
|
|
505
759
|
prefix_widget = self.query_one("#mode_prefix", expect_type=Static)
|
|
506
760
|
prefix_widget.update(self._get_mode_prefix())
|
|
507
|
-
|
|
761
|
+
|
|
508
762
|
# Update input placeholder
|
|
509
763
|
input_widget = self.query_one("#main_input", expect_type=Input)
|
|
510
764
|
input_widget.placeholder = self._get_placeholder()
|
|
511
|
-
|
|
765
|
+
|
|
512
766
|
# Update container classes
|
|
513
767
|
container = self.query_one("#input_container")
|
|
514
768
|
container.remove_class("mode-prompt", "mode-bash", "mode-koding")
|
|
515
769
|
container.add_class(f"mode-{mode.value}")
|
|
516
770
|
except:
|
|
517
771
|
pass # Widgets might not be mounted yet
|
|
518
|
-
|
|
772
|
+
|
|
519
773
|
def watch_is_loading(self, is_loading: bool):
|
|
520
774
|
"""Watch loading state changes"""
|
|
521
775
|
try:
|
|
522
|
-
input_widget = self.query_one("#main_input", expect_type=
|
|
776
|
+
input_widget = self.query_one("#main_input", expect_type=CustomTextArea)
|
|
523
777
|
input_widget.disabled = self.is_disabled or is_loading
|
|
524
778
|
except:
|
|
525
779
|
pass
|
|
526
|
-
|
|
780
|
+
|
|
527
781
|
def watch_input_value(self, value: str):
|
|
528
782
|
"""Watch input value changes"""
|
|
529
783
|
try:
|
|
530
|
-
input_widget = self.query_one("#main_input", expect_type=
|
|
531
|
-
if input_widget.
|
|
532
|
-
input_widget.
|
|
784
|
+
input_widget = self.query_one("#main_input", expect_type=CustomTextArea)
|
|
785
|
+
if input_widget.text != value:
|
|
786
|
+
input_widget.text = value
|
|
533
787
|
except:
|
|
534
|
-
pass
|
|
788
|
+
pass
|