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.
Files changed (115) hide show
  1. examples/cli_entrypoint.py +60 -0
  2. examples/{agent_with_todos.py → components/agent_with_todos.py} +58 -47
  3. examples/{message_response_children_demo.py → components/message_response_children_demo.py} +61 -55
  4. examples/components/messages_component.py +199 -0
  5. examples/file_freshness_example.py +22 -22
  6. examples/file_watching_example.py +32 -26
  7. examples/interruptible_tui.py +921 -3
  8. examples/repl_tui.py +129 -0
  9. examples/skills/example_usage.py +57 -0
  10. examples/start.py +173 -0
  11. minion_code/__init__.py +1 -1
  12. minion_code/acp_server/__init__.py +34 -0
  13. minion_code/acp_server/agent.py +539 -0
  14. minion_code/acp_server/hooks.py +354 -0
  15. minion_code/acp_server/main.py +194 -0
  16. minion_code/acp_server/permissions.py +142 -0
  17. minion_code/acp_server/test_client.py +104 -0
  18. minion_code/adapters/__init__.py +22 -0
  19. minion_code/adapters/output_adapter.py +207 -0
  20. minion_code/adapters/rich_adapter.py +169 -0
  21. minion_code/adapters/textual_adapter.py +254 -0
  22. minion_code/agents/__init__.py +2 -2
  23. minion_code/agents/code_agent.py +517 -104
  24. minion_code/agents/hooks.py +378 -0
  25. minion_code/cli.py +538 -429
  26. minion_code/cli_simple.py +665 -0
  27. minion_code/commands/__init__.py +136 -29
  28. minion_code/commands/clear_command.py +19 -46
  29. minion_code/commands/help_command.py +33 -49
  30. minion_code/commands/history_command.py +37 -55
  31. minion_code/commands/model_command.py +194 -0
  32. minion_code/commands/quit_command.py +9 -12
  33. minion_code/commands/resume_command.py +181 -0
  34. minion_code/commands/skill_command.py +89 -0
  35. minion_code/commands/status_command.py +48 -73
  36. minion_code/commands/tools_command.py +54 -52
  37. minion_code/commands/version_command.py +34 -69
  38. minion_code/components/ConfirmDialog.py +430 -0
  39. minion_code/components/Message.py +318 -97
  40. minion_code/components/MessageResponse.py +30 -29
  41. minion_code/components/Messages.py +351 -0
  42. minion_code/components/PromptInput.py +499 -245
  43. minion_code/components/__init__.py +24 -17
  44. minion_code/const.py +7 -0
  45. minion_code/screens/REPL.py +1453 -469
  46. minion_code/screens/__init__.py +1 -1
  47. minion_code/services/__init__.py +20 -20
  48. minion_code/services/event_system.py +19 -14
  49. minion_code/services/file_freshness_service.py +223 -170
  50. minion_code/skills/__init__.py +25 -0
  51. minion_code/skills/skill.py +128 -0
  52. minion_code/skills/skill_loader.py +198 -0
  53. minion_code/skills/skill_registry.py +177 -0
  54. minion_code/subagents/__init__.py +31 -0
  55. minion_code/subagents/builtin/__init__.py +30 -0
  56. minion_code/subagents/builtin/claude_code_guide.py +32 -0
  57. minion_code/subagents/builtin/explore.py +36 -0
  58. minion_code/subagents/builtin/general_purpose.py +19 -0
  59. minion_code/subagents/builtin/plan.py +61 -0
  60. minion_code/subagents/subagent.py +116 -0
  61. minion_code/subagents/subagent_loader.py +147 -0
  62. minion_code/subagents/subagent_registry.py +151 -0
  63. minion_code/tools/__init__.py +8 -2
  64. minion_code/tools/bash_tool.py +16 -3
  65. minion_code/tools/file_edit_tool.py +201 -104
  66. minion_code/tools/file_read_tool.py +183 -26
  67. minion_code/tools/file_write_tool.py +17 -3
  68. minion_code/tools/glob_tool.py +23 -2
  69. minion_code/tools/grep_tool.py +229 -21
  70. minion_code/tools/ls_tool.py +28 -3
  71. minion_code/tools/multi_edit_tool.py +89 -84
  72. minion_code/tools/python_interpreter_tool.py +9 -1
  73. minion_code/tools/skill_tool.py +210 -0
  74. minion_code/tools/task_tool.py +287 -0
  75. minion_code/tools/todo_read_tool.py +28 -24
  76. minion_code/tools/todo_write_tool.py +82 -65
  77. minion_code/{types.py → type_defs.py} +15 -2
  78. minion_code/utils/__init__.py +45 -17
  79. minion_code/utils/config.py +610 -0
  80. minion_code/utils/history.py +114 -0
  81. minion_code/utils/logs.py +53 -0
  82. minion_code/utils/mcp_loader.py +153 -55
  83. minion_code/utils/output_truncator.py +233 -0
  84. minion_code/utils/session_storage.py +369 -0
  85. minion_code/utils/todo_file_utils.py +26 -22
  86. minion_code/utils/todo_storage.py +43 -33
  87. minion_code/web/__init__.py +9 -0
  88. minion_code/web/adapters/__init__.py +5 -0
  89. minion_code/web/adapters/web_adapter.py +524 -0
  90. minion_code/web/api/__init__.py +7 -0
  91. minion_code/web/api/chat.py +277 -0
  92. minion_code/web/api/interactions.py +136 -0
  93. minion_code/web/api/sessions.py +135 -0
  94. minion_code/web/server.py +149 -0
  95. minion_code/web/services/__init__.py +5 -0
  96. minion_code/web/services/session_manager.py +420 -0
  97. minion_code-0.1.2.dist-info/METADATA +476 -0
  98. minion_code-0.1.2.dist-info/RECORD +111 -0
  99. {minion_code-0.1.0.dist-info → minion_code-0.1.2.dist-info}/WHEEL +1 -1
  100. minion_code-0.1.2.dist-info/entry_points.txt +6 -0
  101. tests/test_adapter.py +67 -0
  102. tests/test_adapter_simple.py +79 -0
  103. tests/test_file_read_tool.py +144 -0
  104. tests/test_readonly_tools.py +0 -2
  105. tests/test_skills.py +441 -0
  106. examples/advance_tui.py +0 -508
  107. examples/rich_example.py +0 -4
  108. examples/simple_file_watching.py +0 -57
  109. examples/simple_tui.py +0 -267
  110. examples/simple_usage.py +0 -69
  111. minion_code-0.1.0.dist-info/METADATA +0 -350
  112. minion_code-0.1.0.dist-info/RECORD +0 -59
  113. minion_code-0.1.0.dist-info/entry_points.txt +0 -4
  114. {minion_code-0.1.0.dist-info → minion_code-0.1.2.dist-info}/licenses/LICENSE +0 -0
  115. {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
- # Simple logging setup for TUI - disable to prevent screen interference
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 ..types import (
25
- InputMode, Message, MessageType, MessageContent, ModelInfo
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
- # Working CSS
36
- CSS = """
82
+
83
+ DEFAULT_CSS = """
37
84
  PromptInput {
38
85
  dock: bottom;
39
- height: 6;
40
- margin: 1;
41
- border: solid white;
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: 3;
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__(self,
94
- commands=None,
95
- fork_number=0,
96
- message_log_name="default",
97
- is_disabled=False,
98
- is_loading=False,
99
- debug=False,
100
- verbose=False,
101
- messages=None,
102
- tools=None,
103
- input_value="",
104
- mode=InputMode.PROMPT,
105
- submit_count=0,
106
- read_file_timestamps=None,
107
- abort_controller=None,
108
- **kwargs):
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
- logger.info(f"PromptInput initialized in {mode.value} mode")
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=Input)
219
+ input_widget = self.query_one("#main_input", expect_type=CustomTextArea)
156
220
  input_widget.focus()
157
- logger.info("Focus set to main input")
158
- except Exception as e:
159
- logger.warning(f"Could not set focus to input: {e}")
160
-
221
+ except Exception:
222
+ pass # Silently handle focus errors
223
+
161
224
  def compose(self):
162
- """Compose the PromptInput interface - working version"""
163
- # Model info at top
164
- yield self._render_model_info()
165
-
166
- # Help text
167
- yield Static("! for bash mode · # for AGENTS.md · / for commands", classes="help-text")
168
-
169
- # Input area with mode prefix
170
- with Horizontal():
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 Input(
173
- placeholder=self._get_placeholder(),
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(Input.Changed, "#main_input")
224
- def on_input_changed(self, event):
225
- """Handle input value changes - equivalent to onChange callback"""
226
- value = event.value
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(Input.Submitted, "#main_input")
245
- async def on_submit(self, event):
246
- """Handle input submission - equivalent to onSubmit function"""
247
- input_text = event.value.strip()
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 ['exit', 'quit', ':q', ':q!', ':wq', ':wq!']:
366
+ if input_text.lower() in ["exit", "quit", ":q", ":q!", ":wq", ":wq!"]:
259
367
  self._handle_exit()
260
368
  return
261
-
262
- # Handle different modes
263
- if self.mode == InputMode.KODING or input_text.startswith('#'):
264
- await self._handle_koding_input(input_text)
265
- elif self.mode == InputMode.BASH or input_text.startswith('!'):
266
- await self._handle_bash_input(input_text)
267
- else:
268
- await self._handle_prompt_input(input_text)
269
-
270
- # Clear input and reset mode
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
- # Update submit count
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
- logger.info(f"Processing koding input: {input_text}")
284
-
445
+
285
446
  # Strip # prefix if present
286
- content = input_text[1:].strip() if input_text.startswith('#') else input_text
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(word in content.lower() for word in ['put', 'create', 'generate', 'write', 'give', 'provide']):
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(f"#{input_text}" if not input_text.startswith('#') else input_text)
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
- logger.info(f"Processing bash input: {input_text}")
302
-
467
+
303
468
  # Strip ! prefix if present
304
- command = input_text[1:].strip() if input_text.startswith('!') else input_text
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
- logger.info(f"Bash command result: {response[:100]}...")
325
-
326
- except Exception as e:
327
- logger.error(f"Error executing bash command: {e}")
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(f"!{input_text}" if not input_text.startswith('!') else input_text)
331
-
332
- async def _handle_prompt_input(self, input_text: str):
333
- """Handle regular prompt input - equivalent to normal message processing"""
334
- logger.info(f"Processing prompt input: {input_text}")
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
- # Process user input (this would integrate with the actual message processing)
345
- messages = await self._process_user_input(input_text, self.mode)
346
-
347
- if messages and self.on_query:
348
- await self.on_query(messages)
349
-
350
- # Add to history
351
- self._add_to_history(input_text)
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
- logger.info(f"Processing koding AI request: {content}")
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 = 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
- logger.info(f"Adding note to AGENTS.md: {content}")
377
-
378
- # Interpret and format the note using AI (simplified version)
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
- interpreted_content = await self._interpret_hash_command(content)
381
- self._handle_hash_command(interpreted_content)
553
+ self._handle_hash_command(content)
382
554
  except Exception as e:
383
- logger.error(f"Error interpreting hash command: {e}")
384
- # Fallback to simple formatting
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
- """Interpret hash command using AI - equivalent to interpretHashCommand"""
390
- # This would integrate with the AI system
391
- # For now, return simple formatting
392
- return f"# {content}\n\n_Added on {time.strftime('%Y-%m-%d %H:%M:%S')}_"
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 agents_md.exists():
401
- with open(agents_md, "a", encoding="utf-8") as f:
402
- f.write(f"\n\n{content}\n")
403
- logger.info("Added content to AGENTS.md")
404
- else:
405
- logger.warning("AGENTS.md not found")
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
- logger.error(f"Error writing to AGENTS.md: {e}")
408
-
409
- async def _process_user_input(self, input_text: str, mode: InputMode) -> List[Message]:
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 = 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
- """Add input to history - equivalent to addToHistory"""
420
- logger.info(f"Added to history: {input_text[:50]}...")
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(3.0, lambda: setattr(self, 'exit_message', {"show": False, "key": ""}))
429
-
430
- # Key event handling
431
- def on_key(self, event: Key) -> bool:
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
- logger.info("Model switch requested")
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(3.0, lambda: setattr(self, 'model_switch_message', {"show": False, "text": ""}))
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=Input)
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=Input)
531
- if input_widget.value != value:
532
- input_widget.value = value
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