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
@@ -14,19 +14,19 @@ class MessageResponse(Container):
14
14
  """
15
15
  MessageResponse component equivalent to React MessageResponse
16
16
  Provides visual indentation with "⎿" indicator for children widgets
17
-
17
+
18
18
  Usage:
19
19
  # With direct content
20
20
  response = MessageResponse(content="Operation completed")
21
-
21
+
22
22
  # With child widgets (like React children)
23
23
  response = MessageResponse()
24
24
  response.mount(Message(...)) # Mount child widgets
25
-
25
+
26
26
  # Or pass children during initialization
27
27
  response = MessageResponse(children=[message_widget, status_widget])
28
28
  """
29
-
29
+
30
30
  DEFAULT_CSS = """
31
31
  MessageResponse {
32
32
  height: auto;
@@ -71,35 +71,36 @@ class MessageResponse(Container):
71
71
  padding: 0;
72
72
  }
73
73
  """
74
-
75
- def __init__(self,
76
- children: Optional[Union[Widget, List[Widget]]] = None,
77
- content: Optional[str] = None,
78
- **kwargs):
74
+
75
+ def __init__(
76
+ self,
77
+ children: Optional[Union[Widget, List[Widget]]] = None,
78
+ content: Optional[str] = None,
79
+ **kwargs,
80
+ ):
79
81
  super().__init__(**kwargs)
80
82
  self.children_widgets = []
81
83
  self.content = content
82
-
84
+
83
85
  # Handle children parameter
84
86
  if children:
85
87
  if isinstance(children, list):
86
88
  self.children_widgets = children
87
89
  else:
88
90
  self.children_widgets = [children]
89
-
91
+
90
92
  def compose(self):
91
93
  """Compose the MessageResponse interface"""
92
94
  from textual.containers import Vertical
93
-
95
+
94
96
  # 使用Vertical多层布局
95
97
  with Horizontal(classes="message-response-container"):
96
- #yield (Static(" ⎿", classes="response-indicator"))
98
+ # yield (Static(" ⎿", classes="response-indicator"))
97
99
  if self.children_widgets:
98
100
 
99
101
  for child in self.children_widgets:
100
102
  yield child
101
103
 
102
-
103
104
  # def on_mount(self):
104
105
  # print("mounting")
105
106
  # pass
@@ -110,12 +111,12 @@ class MessageResponse(Container):
110
111
  """
111
112
  content_area = self.query_one("#content-area", Container)
112
113
  content_area.mount(widget)
113
-
114
+
114
115
  def mount_children(self, widgets: List[Widget]):
115
116
  """Mount multiple child widgets"""
116
117
  for widget in widgets:
117
118
  self.mount_child(widget)
118
-
119
+
119
120
  def clear_children(self):
120
121
  """Clear all child widgets from content area"""
121
122
  content_area = self.query_one("#content-area", Container)
@@ -125,51 +126,51 @@ class MessageResponse(Container):
125
126
 
126
127
  class MessageResponseText(MessageResponse):
127
128
  """Specialized MessageResponse for text content"""
128
-
129
+
129
130
  def __init__(self, text: str, **kwargs):
130
131
  super().__init__(content=text, **kwargs)
131
132
 
132
133
 
133
134
  class MessageResponseStatus(MessageResponse):
134
135
  """Specialized MessageResponse for status messages"""
135
-
136
+
136
137
  def __init__(self, status: str, message: str = "", **kwargs):
137
138
  status_icons = {
138
139
  "loading": "⏳",
139
- "success": "✅",
140
+ "success": "✅",
140
141
  "error": "❌",
141
142
  "warning": "⚠️",
142
143
  "info": "ℹ️",
143
- "thinking": "🤔"
144
+ "thinking": "🤔",
144
145
  }
145
-
146
+
146
147
  icon = status_icons.get(status, "•")
147
148
  content = f"{icon} {message}" if message else icon
148
-
149
+
149
150
  super().__init__(content=content, **kwargs)
150
151
 
151
152
 
152
153
  class MessageResponseProgress(MessageResponse):
153
154
  """Specialized MessageResponse for progress indicators"""
154
-
155
+
155
156
  def __init__(self, current: int, total: int, message: str = "", **kwargs):
156
157
  progress_text = f"[{current}/{total}]"
157
158
  if message:
158
159
  progress_text += f" {message}"
159
-
160
+
160
161
  super().__init__(content=progress_text, **kwargs)
161
162
 
162
163
 
163
164
  class MessageResponseTyping(MessageResponse):
164
165
  """Specialized MessageResponse for typing indicators"""
165
-
166
+
166
167
  def __init__(self, **kwargs):
167
168
  super().__init__(content="typing...", **kwargs)
168
-
169
+
169
170
  def on_mount(self):
170
171
  """Start typing animation when mounted"""
171
172
  self._animate_typing()
172
-
173
+
173
174
  def _animate_typing(self):
174
175
  """Simple typing animation"""
175
176
  # This could be enhanced with actual animation
@@ -182,7 +183,7 @@ class MessageResponseWithChildren(MessageResponse):
182
183
  Specialized MessageResponse that demonstrates children usage
183
184
  Equivalent to React's <MessageResponse><Message /></MessageResponse>
184
185
  """
185
-
186
+
186
187
  def __init__(self, message_widget: Widget, **kwargs):
187
188
  # Pass the message widget as a child
188
- super().__init__(children=[message_widget], **kwargs)
189
+ super().__init__(children=[message_widget], **kwargs)
@@ -0,0 +1,351 @@
1
+ """
2
+ Messages Component - Python equivalent of React Messages component
3
+ Renders a list of messages in the REPL interface
4
+ """
5
+
6
+ from textual.containers import Container, ScrollableContainer, Vertical
7
+ from textual.widgets import Static
8
+ from textual.reactive import reactive, var
9
+ from typing import List, Dict, Any, Optional, Set
10
+ from dataclasses import dataclass
11
+
12
+ # Import shared types and components
13
+ from ..type_defs import Message as MessageType, MessageContent, InputMode
14
+ from .Message import Message, UserMessage, AssistantMessage, ToolUseMessage
15
+
16
+
17
+ class Messages(ScrollableContainer):
18
+ """
19
+ Messages container component equivalent to React Messages component
20
+ Renders a list of messages with proper scrolling and layout
21
+ """
22
+
23
+ DEFAULT_CSS = """
24
+ Messages {
25
+ height: 1fr;
26
+ width: 100%;
27
+ margin: 1;
28
+ padding: 1;
29
+ scrollbar-background: $surface-lighten-1;
30
+ scrollbar-color: $primary;
31
+ }
32
+
33
+ .messages-container {
34
+ width: 100%;
35
+ height: auto;
36
+ }
37
+
38
+ .empty-state {
39
+ width: 100%;
40
+ height: 100%;
41
+ content-align: center middle;
42
+ color: $text-muted;
43
+ text-style: dim;
44
+ }
45
+
46
+ .message-item {
47
+ width: 100%;
48
+ margin-bottom: 1;
49
+ }
50
+ """
51
+
52
+ # Reactive properties
53
+ messages = reactive(list, recompose=True) # List[MessageType]
54
+ # messages = vars(list) # List[MessageType]
55
+
56
+ def __init__(
57
+ self,
58
+ messages: List[MessageType] = None,
59
+ tools: List[Any] = None,
60
+ verbose: bool = False,
61
+ debug: bool = False,
62
+ errored_tool_use_ids: Set[str] = None,
63
+ in_progress_tool_use_ids: Set[str] = None,
64
+ unresolved_tool_use_ids: Set[str] = None,
65
+ should_animate: bool = False,
66
+ auto_scroll: bool = True,
67
+ **kwargs,
68
+ ):
69
+ super().__init__(**kwargs)
70
+
71
+ # Props equivalent to TypeScript Props interface
72
+ self._initial_messages = messages or []
73
+ print(
74
+ f"DEBUG: Messages component initialized with {len(self._initial_messages)} initial messages"
75
+ )
76
+ self.tools = tools or []
77
+ self.verbose = verbose
78
+ self.debug = debug
79
+ self.errored_tool_use_ids = errored_tool_use_ids or set()
80
+ self.in_progress_tool_use_ids = in_progress_tool_use_ids or set()
81
+ self.unresolved_tool_use_ids = unresolved_tool_use_ids or set()
82
+ self.should_animate = should_animate
83
+ self.auto_scroll = auto_scroll
84
+
85
+ # Internal state
86
+ self._last_message_count = 0
87
+ self._is_mounted = False
88
+
89
+ # Set messages after initialization to avoid watch_messages being called too early
90
+ if self._initial_messages:
91
+ self.messages = self._initial_messages.copy()
92
+
93
+ def compose(self):
94
+ """Compose the messages interface - equivalent to React render method"""
95
+ print(f"DEBUG: Messages.compose() called with {len(self.messages)} messages")
96
+ if not self.messages:
97
+ # Empty state - equivalent to showing placeholder when no messages
98
+ print("DEBUG: Showing empty state")
99
+ yield Static(
100
+ "💬 Start a conversation by typing a message below...",
101
+ classes="empty-state",
102
+ )
103
+ else:
104
+ # Messages container
105
+ print(f"DEBUG: Rendering {len(self.messages)} messages")
106
+ with Vertical(classes="messages-container"):
107
+ for i, message in enumerate(self.messages):
108
+ print(f"DEBUG: Creating message widget {i}: {message.type}")
109
+ yield self._create_message_widget(message, i)
110
+
111
+ def on_mount(self):
112
+ """Called when the widget is mounted"""
113
+ self._is_mounted = True
114
+ # Now it's safe to update the display if needed
115
+ if self.messages != self._initial_messages:
116
+ self._update_display()
117
+
118
+ def _create_message_widget(self, message: MessageType, index: int) -> Message:
119
+ """Create a message widget based on message type"""
120
+
121
+ # Common props for all message types
122
+ message_props = {
123
+ "message": message,
124
+ "messages": self.messages,
125
+ "tools": self.tools,
126
+ "verbose": self.verbose,
127
+ "debug": self.debug,
128
+ "errored_tool_use_ids": self.errored_tool_use_ids,
129
+ "in_progress_tool_use_ids": self.in_progress_tool_use_ids,
130
+ "unresolved_tool_use_ids": self.unresolved_tool_use_ids,
131
+ "should_animate": self.should_animate,
132
+ "classes": "message-item",
133
+ "id": f"message_{index}",
134
+ }
135
+
136
+ # Create appropriate message component based on type
137
+ if message.type.value == "user":
138
+ return UserMessage(**message_props)
139
+ elif message.type.value == "assistant":
140
+ # Check if this is a tool use message
141
+ if self._is_tool_use_message(message):
142
+ return ToolUseMessage(**message_props)
143
+ else:
144
+ return AssistantMessage(**message_props)
145
+ else:
146
+ # Default to generic Message component
147
+ return Message(**message_props)
148
+
149
+ def _is_tool_use_message(self, message: MessageType) -> bool:
150
+ """Check if message contains tool use content"""
151
+ content = message.message.content
152
+ if isinstance(content, list):
153
+ return any(
154
+ isinstance(item, dict) and item.get("type") == "tool_use"
155
+ for item in content
156
+ )
157
+ return False
158
+
159
+ def add_message(self, message: MessageType):
160
+ """Add a new message to the list"""
161
+ self.messages.append(message)
162
+ self.mutate_reactive(Messages.messages)
163
+
164
+ if self.auto_scroll:
165
+ self.call_later(self._scroll_to_bottom)
166
+
167
+ def update_messages(self, messages: List[MessageType]):
168
+ """Update the entire messages list"""
169
+ # Clear and replace all messages
170
+ self.messages = messages
171
+ self.mutate_reactive(Messages.messages)
172
+
173
+ # Auto-scroll if new messages were added
174
+ if len(messages) > self._last_message_count and self.auto_scroll:
175
+ self.call_later(self._scroll_to_bottom)
176
+
177
+ self._last_message_count = len(messages)
178
+
179
+ def update_streaming_message(self, message_index: int, new_content: str):
180
+ """Update a streaming message at specific index"""
181
+ if 0 <= message_index < len(self.messages):
182
+ # Update the message content
183
+ self.messages[message_index].message.content = new_content
184
+
185
+ # Find and update the corresponding widget
186
+ try:
187
+ message_widget = self.query_one(f"#message_{message_index}")
188
+ if hasattr(message_widget, "update_streaming_content"):
189
+ message_widget.update_streaming_content(new_content)
190
+ except Exception:
191
+ pass # Widget might not exist yet
192
+
193
+ def finalize_streaming_message(self, message_index: int, final_content: str):
194
+ """Finalize a streaming message with final content"""
195
+ if 0 <= message_index < len(self.messages):
196
+ # Update the message content and remove streaming flag
197
+ message = self.messages[message_index]
198
+ message.message.content = final_content
199
+ if message.options:
200
+ message.options.pop("streaming", None)
201
+
202
+ # Find and update the corresponding widget
203
+ try:
204
+ message_widget = self.query_one(f"#message_{message_index}")
205
+ if hasattr(message_widget, "finalize_streaming"):
206
+ message_widget.finalize_streaming(final_content)
207
+ except Exception:
208
+ pass # Widget might not exist yet
209
+
210
+ def clear_messages(self):
211
+ """Clear all messages"""
212
+ self.messages = []
213
+ self.mutate_reactive(Messages.messages)
214
+
215
+ def _update_display(self):
216
+ """Update the display when messages change"""
217
+ # Use recompose to rebuild the entire widget tree
218
+ self.recompose()
219
+
220
+ def _scroll_to_bottom(self):
221
+ """Scroll to the bottom of the messages container"""
222
+ try:
223
+ self.scroll_end(animate=True)
224
+ except Exception:
225
+ pass # Silently handle scroll errors
226
+
227
+ def get_message_count(self) -> int:
228
+ """Get the current number of messages"""
229
+ return len(self.messages)
230
+
231
+ def get_last_message(self) -> Optional[MessageType]:
232
+ """Get the last message in the list"""
233
+ return self.messages[-1] if self.messages else None
234
+
235
+ def get_messages_by_type(self, message_type: str) -> List[MessageType]:
236
+ """Get all messages of a specific type"""
237
+ return [msg for msg in self.messages if msg.type.value == message_type]
238
+
239
+ def find_message_by_id(self, message_id: str) -> Optional[MessageType]:
240
+ """Find a message by its ID"""
241
+ for message in self.messages:
242
+ if hasattr(message, "id") and message.id == message_id:
243
+ return message
244
+ return None
245
+
246
+ # Reactive property watchers
247
+ def watch_messages(self, messages: List[MessageType]):
248
+ """Watch for changes to the messages list"""
249
+ # Only update display if the widget is mounted
250
+ if self._is_mounted:
251
+ self._update_display()
252
+
253
+ # Auto-scroll if new messages were added
254
+ if len(messages) > self._last_message_count and self.auto_scroll:
255
+ self.call_later(self._scroll_to_bottom)
256
+
257
+ self._last_message_count = len(messages)
258
+
259
+
260
+ class MessagesWithStatus(Container):
261
+ """
262
+ Messages container with status indicators
263
+ Equivalent to a more advanced Messages component with loading states
264
+ """
265
+
266
+ DEFAULT_CSS = """
267
+ MessagesWithStatus {
268
+ height: 1fr;
269
+ width: 100%;
270
+ }
271
+
272
+ .status-bar {
273
+ dock: bottom;
274
+ height: 1;
275
+ background: $surface-lighten-1;
276
+ content-align: center middle;
277
+ color: $text-muted;
278
+ }
279
+
280
+ .typing-indicator {
281
+ color: $primary;
282
+ text-style: italic;
283
+ }
284
+
285
+ .error-indicator {
286
+ color: $error;
287
+ text-style: bold;
288
+ }
289
+ """
290
+
291
+ def __init__(
292
+ self,
293
+ messages: List[MessageType] = None,
294
+ is_loading: bool = False,
295
+ error_message: Optional[str] = None,
296
+ typing_indicator: Optional[str] = None,
297
+ **kwargs,
298
+ ):
299
+ super().__init__(**kwargs)
300
+
301
+ self.messages = messages or []
302
+ self.is_loading = is_loading
303
+ self.error_message = error_message
304
+ self.typing_indicator = typing_indicator
305
+
306
+ def compose(self):
307
+ """Compose messages with status bar"""
308
+ # Main messages component
309
+ yield Messages(messages=self.messages, id="main_messages")
310
+
311
+ # Status bar
312
+ yield self._render_status_bar()
313
+
314
+ def _render_status_bar(self) -> Static:
315
+ """Render the status bar based on current state"""
316
+ if self.error_message:
317
+ return Static(
318
+ f"❌ {self.error_message}", classes="status-bar error-indicator"
319
+ )
320
+ elif self.is_loading:
321
+ return Static(
322
+ "⠋ Assistant is thinking...", classes="status-bar typing-indicator"
323
+ )
324
+ elif self.typing_indicator:
325
+ return Static(
326
+ f"⌨️ {self.typing_indicator}", classes="status-bar typing-indicator"
327
+ )
328
+ else:
329
+ return Static("", classes="status-bar")
330
+
331
+ def update_status(
332
+ self,
333
+ is_loading: bool = None,
334
+ error_message: str = None,
335
+ typing_indicator: str = None,
336
+ ):
337
+ """Update the status indicators"""
338
+ if is_loading is not None:
339
+ self.is_loading = is_loading
340
+ if error_message is not None:
341
+ self.error_message = error_message
342
+ if typing_indicator is not None:
343
+ self.typing_indicator = typing_indicator
344
+
345
+ # Update status bar
346
+ try:
347
+ status_bar = self.query_one(".status-bar")
348
+ new_status_bar = self._render_status_bar()
349
+ status_bar.update(new_status_bar.renderable)
350
+ except Exception:
351
+ pass # Status bar might not be mounted yet