minion-code 0.1.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (59) hide show
  1. examples/advance_tui.py +508 -0
  2. examples/agent_with_todos.py +165 -0
  3. examples/file_freshness_example.py +97 -0
  4. examples/file_watching_example.py +110 -0
  5. examples/interruptible_tui.py +5 -0
  6. examples/message_response_children_demo.py +226 -0
  7. examples/rich_example.py +4 -0
  8. examples/simple_file_watching.py +57 -0
  9. examples/simple_tui.py +267 -0
  10. examples/simple_usage.py +69 -0
  11. minion_code/__init__.py +16 -0
  12. minion_code/agents/__init__.py +11 -0
  13. minion_code/agents/code_agent.py +320 -0
  14. minion_code/cli.py +502 -0
  15. minion_code/commands/__init__.py +90 -0
  16. minion_code/commands/clear_command.py +70 -0
  17. minion_code/commands/help_command.py +90 -0
  18. minion_code/commands/history_command.py +104 -0
  19. minion_code/commands/quit_command.py +32 -0
  20. minion_code/commands/status_command.py +115 -0
  21. minion_code/commands/tools_command.py +86 -0
  22. minion_code/commands/version_command.py +104 -0
  23. minion_code/components/Message.py +304 -0
  24. minion_code/components/MessageResponse.py +188 -0
  25. minion_code/components/PromptInput.py +534 -0
  26. minion_code/components/__init__.py +29 -0
  27. minion_code/screens/REPL.py +925 -0
  28. minion_code/screens/__init__.py +4 -0
  29. minion_code/services/__init__.py +50 -0
  30. minion_code/services/event_system.py +108 -0
  31. minion_code/services/file_freshness_service.py +582 -0
  32. minion_code/tools/__init__.py +69 -0
  33. minion_code/tools/bash_tool.py +58 -0
  34. minion_code/tools/file_edit_tool.py +238 -0
  35. minion_code/tools/file_read_tool.py +73 -0
  36. minion_code/tools/file_write_tool.py +36 -0
  37. minion_code/tools/glob_tool.py +58 -0
  38. minion_code/tools/grep_tool.py +105 -0
  39. minion_code/tools/ls_tool.py +65 -0
  40. minion_code/tools/multi_edit_tool.py +271 -0
  41. minion_code/tools/python_interpreter_tool.py +105 -0
  42. minion_code/tools/todo_read_tool.py +100 -0
  43. minion_code/tools/todo_write_tool.py +234 -0
  44. minion_code/tools/user_input_tool.py +53 -0
  45. minion_code/types.py +88 -0
  46. minion_code/utils/__init__.py +44 -0
  47. minion_code/utils/mcp_loader.py +211 -0
  48. minion_code/utils/todo_file_utils.py +110 -0
  49. minion_code/utils/todo_storage.py +149 -0
  50. minion_code-0.1.0.dist-info/METADATA +350 -0
  51. minion_code-0.1.0.dist-info/RECORD +59 -0
  52. minion_code-0.1.0.dist-info/WHEEL +5 -0
  53. minion_code-0.1.0.dist-info/entry_points.txt +4 -0
  54. minion_code-0.1.0.dist-info/licenses/LICENSE +661 -0
  55. minion_code-0.1.0.dist-info/top_level.txt +3 -0
  56. tests/__init__.py +1 -0
  57. tests/test_basic.py +20 -0
  58. tests/test_readonly_tools.py +102 -0
  59. tests/test_tools.py +83 -0
@@ -0,0 +1,304 @@
1
+ """
2
+ Message Component - Python equivalent of React Message component
3
+ Handles rendering of user and assistant messages with different content types
4
+ """
5
+
6
+ from textual.containers import Container, Vertical, Horizontal
7
+ from textual.widgets import Static, RichLog
8
+ from textual.reactive import reactive
9
+ from rich.text import Text
10
+ from rich.console import Console
11
+ from rich.markdown import Markdown
12
+ from rich.syntax import Syntax
13
+ from typing import List, Dict, Any, Optional, Set
14
+ import json
15
+ import logging
16
+
17
+ # Import shared types
18
+ from ..types import Message as MessageType, MessageContent, InputMode
19
+
20
+ logger = logging.getLogger(__name__)
21
+
22
+
23
+ class Message(Container):
24
+ """
25
+ Main message component equivalent to React Message
26
+ Handles rendering of both user and assistant messages
27
+ """
28
+
29
+ DEFAULT_CSS = """
30
+ Message {
31
+ width: 80%;
32
+ height: auto;
33
+ margin-bottom: 1;
34
+ }
35
+
36
+ .user-message {
37
+ border-left: thick blue;
38
+ padding-left: 1;
39
+ height: auto;
40
+ }
41
+
42
+ .assistant-message {
43
+ border-left: thick green;
44
+ padding-left: 1;
45
+ height: auto;
46
+ }
47
+
48
+ .tool-use-message {
49
+ border-left: thick yellow;
50
+ padding-left: 1;
51
+ background: $surface-lighten-1;
52
+ height: auto;
53
+ }
54
+
55
+ .error-message {
56
+ border-left: thick red;
57
+ padding-left: 1;
58
+ background: $error 10%;
59
+ height: auto;
60
+ }
61
+
62
+ .message-content {
63
+ width: 100%;
64
+ height: auto;
65
+ padding: 1;
66
+ }
67
+
68
+ .message-meta {
69
+ color: $text-muted;
70
+ text-style: dim;
71
+ height: 1;
72
+ }
73
+ """
74
+
75
+ def __init__(self,
76
+ message: MessageType,
77
+ messages: List[MessageType] = None,
78
+ add_margin: bool = True,
79
+ tools: List[Any] = None,
80
+ verbose: bool = False,
81
+ debug: bool = False,
82
+ errored_tool_use_ids: Set[str] = None,
83
+ in_progress_tool_use_ids: Set[str] = None,
84
+ unresolved_tool_use_ids: Set[str] = None,
85
+ should_animate: bool = False,
86
+ should_show_dot: bool = False,
87
+ width: Optional[int] = None,
88
+ **kwargs):
89
+ super().__init__(**kwargs)
90
+
91
+ self.message = message
92
+ self.messages = messages or []
93
+ self.add_margin = add_margin
94
+ self.tools = tools or []
95
+ self.verbose = verbose
96
+ self.debug = debug
97
+ self.errored_tool_use_ids = errored_tool_use_ids or set()
98
+ self.in_progress_tool_use_ids = in_progress_tool_use_ids or set()
99
+ self.unresolved_tool_use_ids = unresolved_tool_use_ids or set()
100
+ self.should_animate = should_animate
101
+ self.should_show_dot = should_show_dot
102
+ self.width = width
103
+
104
+ def compose(self):
105
+ """Compose the message interface"""
106
+ if self.message.type.value == "assistant":
107
+ yield from self._render_assistant_message()
108
+ else:
109
+ yield from self._render_user_message()
110
+
111
+ def _render_user_message(self):
112
+ """Render user message - equivalent to UserMessage component"""
113
+ with Vertical(classes="user-message"):
114
+ # Message metadata
115
+ if self.verbose or self.debug:
116
+ yield Static(
117
+ f"User • {self._format_timestamp()}",
118
+ classes="message-meta"
119
+ )
120
+
121
+ # Message content
122
+ content = self.message.message.content
123
+ if isinstance(content, str):
124
+ yield from self._render_text_content(content)
125
+ elif isinstance(content, list):
126
+ for item in content:
127
+ yield from self._render_content_block(item)
128
+
129
+ def _render_assistant_message(self):
130
+ """Render assistant message - equivalent to AssistantMessage component"""
131
+ with Vertical(classes="assistant-message"):
132
+ # Message metadata
133
+ if self.verbose or self.debug:
134
+ meta_text = f"Assistant • {self._format_timestamp()}"
135
+ if hasattr(self.message, 'cost_usd') and self.message.cost_usd:
136
+ meta_text += f" • ${self.message.cost_usd:.4f}"
137
+ if hasattr(self.message, 'duration_ms') and self.message.duration_ms:
138
+ meta_text += f" • {self.message.duration_ms}ms"
139
+
140
+ yield Static(meta_text, classes="message-meta")
141
+
142
+ # Message content
143
+ content = self.message.message.content
144
+ if isinstance(content, str):
145
+ yield from self._render_text_content(content)
146
+ elif isinstance(content, list):
147
+ for item in content:
148
+ yield from self._render_content_block(item)
149
+
150
+ def _render_content_block(self, block: Dict[str, Any]):
151
+ """Render individual content blocks based on type"""
152
+ block_type = block.get('type', 'text')
153
+
154
+ if block_type == 'text':
155
+ yield from self._render_text_content(block.get('text', ''))
156
+ elif block_type == 'tool_use':
157
+ yield from self._render_tool_use_block(block)
158
+ elif block_type == 'tool_result':
159
+ yield from self._render_tool_result_block(block)
160
+ elif block_type == 'thinking':
161
+ yield from self._render_thinking_block(block)
162
+ elif block_type == 'redacted_thinking':
163
+ yield from self._render_redacted_thinking_block()
164
+ else:
165
+ # Unknown block type
166
+ yield Static(f"[Unknown content type: {block_type}]", classes="error-message")
167
+
168
+ def _render_text_content(self, text: str):
169
+ """Render text content with markdown support"""
170
+ if not text.strip():
171
+ return
172
+
173
+ try:
174
+ # For now, just render as plain text to avoid compose-time issues
175
+ # TODO: Implement proper markdown rendering with RichLog after mount
176
+ yield Static(text, classes="message-content")
177
+ except Exception as e:
178
+ logger.warning(f"Error rendering text: {e}")
179
+ yield Static(text, classes="message-content")
180
+
181
+ def _render_tool_use_block(self, block: Dict[str, Any]):
182
+ """Render tool use block - equivalent to AssistantToolUseMessage"""
183
+ tool_name = block.get('name', 'unknown')
184
+ tool_id = block.get('id', '')
185
+ parameters = block.get('input', {})
186
+
187
+ # Determine status
188
+ status = "completed"
189
+ if tool_id in self.in_progress_tool_use_ids:
190
+ status = "in_progress"
191
+ elif tool_id in self.errored_tool_use_ids:
192
+ status = "error"
193
+ elif tool_id in self.unresolved_tool_use_ids:
194
+ status = "unresolved"
195
+
196
+ with Vertical(classes="tool-use-message"):
197
+ # Tool header
198
+ status_icon = {
199
+ "completed": "✅",
200
+ "in_progress": "⏳",
201
+ "error": "❌",
202
+ "unresolved": "⏸️"
203
+ }.get(status, "🔧")
204
+
205
+ yield Static(
206
+ f"{status_icon} Tool: {tool_name}",
207
+ classes="message-meta"
208
+ )
209
+
210
+ # Tool parameters (if verbose)
211
+ if self.verbose and parameters:
212
+ try:
213
+ params_json = json.dumps(parameters, indent=2)
214
+ # For now, render as plain text to avoid compose-time issues
215
+ yield Static(params_json, classes="message-content")
216
+ except Exception:
217
+ yield Static(str(parameters), classes="message-content")
218
+
219
+ def _render_tool_result_block(self, block: Dict[str, Any]):
220
+ """Render tool result block - equivalent to UserToolResultMessage"""
221
+ tool_use_id = block.get('tool_use_id', '')
222
+ content = block.get('content', '')
223
+ is_error = block.get('is_error', False)
224
+
225
+ classes = "error-message" if is_error else "message-content"
226
+
227
+ with Vertical():
228
+ # Result header
229
+ icon = "❌" if is_error else "📤"
230
+ yield Static(
231
+ f"{icon} Tool Result",
232
+ classes="message-meta"
233
+ )
234
+
235
+ # Result content
236
+ if isinstance(content, str):
237
+ yield Static(content, classes=classes)
238
+ elif isinstance(content, list):
239
+ for item in content:
240
+ if isinstance(item, dict) and item.get('type') == 'text':
241
+ yield Static(item.get('text', ''), classes=classes)
242
+ else:
243
+ yield Static(str(item), classes=classes)
244
+
245
+ def _render_thinking_block(self, block: Dict[str, Any]):
246
+ """Render thinking block - equivalent to AssistantThinkingMessage"""
247
+ if not self.debug:
248
+ return # Only show in debug mode
249
+
250
+ thinking_content = block.get('content', '')
251
+
252
+ with Vertical():
253
+ yield Static("🤔 Thinking...", classes="message-meta")
254
+ yield Static(thinking_content, classes="message-content")
255
+
256
+ def _render_redacted_thinking_block(self):
257
+ """Render redacted thinking block"""
258
+ if not self.debug:
259
+ return
260
+
261
+ yield Static("🤔 [Thinking content redacted]", classes="message-meta")
262
+
263
+ def _format_timestamp(self) -> str:
264
+ """Format message timestamp"""
265
+ import datetime
266
+ dt = datetime.datetime.fromtimestamp(self.message.timestamp)
267
+ return dt.strftime("%H:%M:%S")
268
+
269
+
270
+ class UserMessage(Message):
271
+ """Specialized component for user messages"""
272
+
273
+ def __init__(self, message: MessageType, **kwargs):
274
+ super().__init__(message, **kwargs)
275
+
276
+ def compose(self):
277
+ """Compose user message with specific styling"""
278
+ yield from self._render_user_message()
279
+
280
+
281
+ class AssistantMessage(Message):
282
+ """Specialized component for assistant messages"""
283
+
284
+ def __init__(self, message: MessageType, **kwargs):
285
+ super().__init__(message, **kwargs)
286
+
287
+ def compose(self):
288
+ """Compose assistant message with specific styling"""
289
+ yield from self._render_assistant_message()
290
+
291
+
292
+ class ToolUseMessage(Message):
293
+ """Specialized component for tool use messages"""
294
+
295
+ def __init__(self, message: MessageType, **kwargs):
296
+ super().__init__(message, **kwargs)
297
+
298
+ def compose(self):
299
+ """Compose tool use message with specific styling"""
300
+ content = self.message.message.content
301
+ if isinstance(content, list):
302
+ for item in content:
303
+ if item.get('type') == 'tool_use':
304
+ yield from self._render_tool_use_block(item)
@@ -0,0 +1,188 @@
1
+ """
2
+ MessageResponse Component - Python equivalent of React MessageResponse component
3
+ Provides visual indentation for tool execution progress messages
4
+ """
5
+
6
+ from textual.containers import Container, Horizontal
7
+ from textual.widgets import Static
8
+ from textual.widget import Widget
9
+ from rich.text import Text
10
+ from typing import Any, Optional, List, Union
11
+
12
+
13
+ class MessageResponse(Container):
14
+ """
15
+ MessageResponse component equivalent to React MessageResponse
16
+ Provides visual indentation with "⎿" indicator for children widgets
17
+
18
+ Usage:
19
+ # With direct content
20
+ response = MessageResponse(content="Operation completed")
21
+
22
+ # With child widgets (like React children)
23
+ response = MessageResponse()
24
+ response.mount(Message(...)) # Mount child widgets
25
+
26
+ # Or pass children during initialization
27
+ response = MessageResponse(children=[message_widget, status_widget])
28
+ """
29
+
30
+ DEFAULT_CSS = """
31
+ MessageResponse {
32
+ height: auto;
33
+ width: 100%;
34
+ }
35
+
36
+ .message-response-container {
37
+ height: auto;
38
+ width: 100%;
39
+ margin: 0;
40
+ padding: 0;
41
+ layers: first second;
42
+ }
43
+
44
+ .response-indicator {
45
+ height: 1;
46
+ width: auto;
47
+ color: $text-muted;
48
+ text-style: dim;
49
+ margin: 0;
50
+ padding: 0;
51
+ }
52
+
53
+ .response-content {
54
+ width: 100%;
55
+ height: auto;
56
+ margin-left: 4;
57
+ margin-top: 0;
58
+ margin-bottom: 0;
59
+ padding: 0;
60
+ }
61
+
62
+ .response-content > * {
63
+ margin: 0;
64
+ padding: 0;
65
+ height: auto;
66
+ }
67
+
68
+ /* 移除Vertical容器的默认间距 */
69
+ .response-content Vertical {
70
+ margin: 0;
71
+ padding: 0;
72
+ }
73
+ """
74
+
75
+ def __init__(self,
76
+ children: Optional[Union[Widget, List[Widget]]] = None,
77
+ content: Optional[str] = None,
78
+ **kwargs):
79
+ super().__init__(**kwargs)
80
+ self.children_widgets = []
81
+ self.content = content
82
+
83
+ # Handle children parameter
84
+ if children:
85
+ if isinstance(children, list):
86
+ self.children_widgets = children
87
+ else:
88
+ self.children_widgets = [children]
89
+
90
+ def compose(self):
91
+ """Compose the MessageResponse interface"""
92
+ from textual.containers import Vertical
93
+
94
+ # 使用Vertical多层布局
95
+ with Horizontal(classes="message-response-container"):
96
+ #yield (Static(" ⎿", classes="response-indicator"))
97
+ if self.children_widgets:
98
+
99
+ for child in self.children_widgets:
100
+ yield child
101
+
102
+
103
+ # def on_mount(self):
104
+ # print("mounting")
105
+ # pass
106
+ def mount_child(self, widget: Widget):
107
+ """
108
+ Mount a child widget to the content area
109
+ Equivalent to React's children mounting
110
+ """
111
+ content_area = self.query_one("#content-area", Container)
112
+ content_area.mount(widget)
113
+
114
+ def mount_children(self, widgets: List[Widget]):
115
+ """Mount multiple child widgets"""
116
+ for widget in widgets:
117
+ self.mount_child(widget)
118
+
119
+ def clear_children(self):
120
+ """Clear all child widgets from content area"""
121
+ content_area = self.query_one("#content-area", Container)
122
+ for child in list(content_area.children):
123
+ child.remove()
124
+
125
+
126
+ class MessageResponseText(MessageResponse):
127
+ """Specialized MessageResponse for text content"""
128
+
129
+ def __init__(self, text: str, **kwargs):
130
+ super().__init__(content=text, **kwargs)
131
+
132
+
133
+ class MessageResponseStatus(MessageResponse):
134
+ """Specialized MessageResponse for status messages"""
135
+
136
+ def __init__(self, status: str, message: str = "", **kwargs):
137
+ status_icons = {
138
+ "loading": "⏳",
139
+ "success": "✅",
140
+ "error": "❌",
141
+ "warning": "⚠️",
142
+ "info": "ℹ️",
143
+ "thinking": "🤔"
144
+ }
145
+
146
+ icon = status_icons.get(status, "•")
147
+ content = f"{icon} {message}" if message else icon
148
+
149
+ super().__init__(content=content, **kwargs)
150
+
151
+
152
+ class MessageResponseProgress(MessageResponse):
153
+ """Specialized MessageResponse for progress indicators"""
154
+
155
+ def __init__(self, current: int, total: int, message: str = "", **kwargs):
156
+ progress_text = f"[{current}/{total}]"
157
+ if message:
158
+ progress_text += f" {message}"
159
+
160
+ super().__init__(content=progress_text, **kwargs)
161
+
162
+
163
+ class MessageResponseTyping(MessageResponse):
164
+ """Specialized MessageResponse for typing indicators"""
165
+
166
+ def __init__(self, **kwargs):
167
+ super().__init__(content="typing...", **kwargs)
168
+
169
+ def on_mount(self):
170
+ """Start typing animation when mounted"""
171
+ self._animate_typing()
172
+
173
+ def _animate_typing(self):
174
+ """Simple typing animation"""
175
+ # This could be enhanced with actual animation
176
+ # For now, just show static typing indicator
177
+ pass
178
+
179
+
180
+ class MessageResponseWithChildren(MessageResponse):
181
+ """
182
+ Specialized MessageResponse that demonstrates children usage
183
+ Equivalent to React's <MessageResponse><Message /></MessageResponse>
184
+ """
185
+
186
+ def __init__(self, message_widget: Widget, **kwargs):
187
+ # Pass the message widget as a child
188
+ super().__init__(children=[message_widget], **kwargs)