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,534 @@
1
+ """
2
+ PromptInput Component - Python equivalent of React PromptInput
3
+ Handles user input with multiple modes (prompt, bash, koding)
4
+ """
5
+
6
+ from textual.containers import Container, Horizontal, Vertical
7
+ from textual.widgets import Input, Static, Button
8
+ from textual.reactive import reactive, var
9
+ from textual import on, work
10
+ from textual.events import Key
11
+ from rich.text import Text
12
+ from typing import List, Dict, Any, Optional, Callable, Union
13
+ from dataclasses import dataclass
14
+ from enum import Enum
15
+ import asyncio
16
+ import time
17
+
18
+ # Simple logging setup for TUI - disable to prevent screen interference
19
+ import logging
20
+ logger = logging.getLogger(__name__)
21
+ logger.disabled = True
22
+
23
+ # Import shared types
24
+ from ..types import (
25
+ InputMode, Message, MessageType, MessageContent, ModelInfo
26
+ )
27
+
28
+
29
+ class PromptInput(Container):
30
+ """
31
+ Main input component equivalent to React PromptInput
32
+ Handles user input with mode switching and command processing
33
+ """
34
+
35
+ # Working CSS
36
+ CSS = """
37
+ PromptInput {
38
+ dock: bottom;
39
+ height: 6;
40
+ margin: 1;
41
+ border: solid white;
42
+ padding: 1;
43
+ }
44
+
45
+ .mode-bash PromptInput {
46
+ border: solid yellow;
47
+ }
48
+
49
+ .mode-koding PromptInput {
50
+ border: solid cyan;
51
+ }
52
+
53
+ #mode_prefix {
54
+ width: 3;
55
+ content-align: center middle;
56
+ text-style: bold;
57
+ }
58
+
59
+ #main_input {
60
+ width: 1fr;
61
+ }
62
+
63
+ .help-text {
64
+ color: gray;
65
+ text-style: dim;
66
+ margin-bottom: 1;
67
+ }
68
+
69
+ .model-info {
70
+ height: 1;
71
+ content-align: right middle;
72
+ background: gray 10%;
73
+ color: white;
74
+ }
75
+ """
76
+
77
+ # Reactive properties equivalent to React useState
78
+ mode = reactive(InputMode.PROMPT)
79
+ input_value = reactive("")
80
+ is_disabled = reactive(False)
81
+ is_loading = reactive(False)
82
+ submit_count = reactive(0)
83
+ cursor_offset = reactive(0)
84
+
85
+ # State for messages and UI feedback
86
+ exit_message = var(dict) # {"show": bool, "key": str}
87
+ message = var(dict) # {"show": bool, "text": str}
88
+ model_switch_message = var(dict) # {"show": bool, "text": str}
89
+ pasted_image = var(None) # Optional[str]
90
+ pasted_text = var(None) # Optional[str]
91
+ 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):
109
+ super().__init__(**kwargs)
110
+
111
+ # Props equivalent to TypeScript Props interface
112
+ self.commands = commands or []
113
+ self.fork_number = fork_number
114
+ self.message_log_name = message_log_name
115
+ self.debug = debug
116
+ self.verbose = verbose
117
+ self.messages = messages or []
118
+ self.tools = tools or []
119
+ self.read_file_timestamps = read_file_timestamps or {}
120
+ self.abort_controller = abort_controller
121
+
122
+ # Initialize reactive state
123
+ self.mode = mode
124
+ self.input_value = input_value
125
+ self.is_disabled = is_disabled
126
+ self.is_loading = is_loading
127
+ self.submit_count = submit_count
128
+ self.cursor_offset = len(input_value)
129
+
130
+ # Initialize state variables
131
+ self.exit_message = {"show": False, "key": ""}
132
+ self.message = {"show": False, "text": ""}
133
+ self.model_switch_message = {"show": False, "text": ""}
134
+ self.pasted_image = None
135
+ self.pasted_text = None
136
+ self.placeholder = ""
137
+
138
+ # Callbacks (would be passed as props in React)
139
+ self.on_query: Optional[Callable] = None
140
+ self.on_input_change: Optional[Callable] = None
141
+ self.on_mode_change: Optional[Callable] = None
142
+ self.on_submit_count_change: Optional[Callable] = None
143
+ self.set_is_loading: Optional[Callable] = None
144
+ self.set_abort_controller: Optional[Callable] = None
145
+ self.on_show_message_selector: Optional[Callable] = None
146
+ self.set_fork_convo_with_messages: Optional[Callable] = None
147
+ self.on_model_change: Optional[Callable] = None
148
+ self.set_tool_jsx: Optional[Callable] = None
149
+
150
+ logger.info(f"PromptInput initialized in {mode.value} mode")
151
+
152
+ def on_mount(self):
153
+ """Set focus to input when component mounts"""
154
+ try:
155
+ input_widget = self.query_one("#main_input", expect_type=Input)
156
+ 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
+
161
+ 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():
171
+ yield Static(self._get_mode_prefix(), id="mode_prefix")
172
+ yield Input(
173
+ placeholder=self._get_placeholder(),
174
+ value=self.input_value,
175
+ id="main_input",
176
+ disabled=self.is_disabled or self.is_loading
177
+ )
178
+
179
+ def _render_model_info(self) -> Static:
180
+ """Render model information - equivalent to model info display"""
181
+ model_info = self._get_model_info()
182
+ if model_info:
183
+ info_text = f"[{model_info.provider}] {model_info.name}: {model_info.current_tokens//1000}k / {model_info.context_length//1000}k"
184
+ return Static(info_text, id="model_info", classes="model-info")
185
+ return Static("", id="model_info")
186
+
187
+ # _render_status_area method removed - content moved to compose method
188
+
189
+ def _get_mode_prefix(self) -> str:
190
+ """Get the mode prefix character"""
191
+ if self.mode == InputMode.BASH:
192
+ return " ! "
193
+ elif self.mode == InputMode.KODING:
194
+ return " # "
195
+ else:
196
+ return " > "
197
+
198
+ def _get_placeholder(self) -> str:
199
+ """Get placeholder text based on current mode"""
200
+ if self.placeholder:
201
+ return self.placeholder
202
+
203
+ if self.mode == InputMode.BASH:
204
+ return "Enter bash command..."
205
+ elif self.mode == InputMode.KODING:
206
+ return "Enter note for AGENTS.md..."
207
+ else:
208
+ return "Enter your message..."
209
+
210
+ def _get_model_info(self) -> Optional[ModelInfo]:
211
+ """Get current model information - equivalent to modelInfo useMemo"""
212
+ # This would integrate with the actual model manager
213
+ # For now, return mock data
214
+ return ModelInfo(
215
+ name="claude-3-5-sonnet-20241022",
216
+ provider="anthropic",
217
+ context_length=200000,
218
+ current_tokens=len(str(self.messages)) * 4, # Rough token estimate
219
+ id="claude-3-5-sonnet"
220
+ )
221
+
222
+ # 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
+
228
+ # Handle mode switching based on input prefix
229
+ if value.startswith('!'):
230
+ if self.mode != InputMode.BASH:
231
+ self.mode = InputMode.BASH
232
+ if self.on_mode_change:
233
+ self.on_mode_change(InputMode.BASH)
234
+ elif value.startswith('#'):
235
+ if self.mode != InputMode.KODING:
236
+ self.mode = InputMode.KODING
237
+ if self.on_mode_change:
238
+ self.on_mode_change(InputMode.KODING)
239
+
240
+ self.input_value = value
241
+ if self.on_input_change:
242
+ 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
+
249
+ if not input_text:
250
+ return
251
+
252
+ if self.is_disabled or self.is_loading:
253
+ return
254
+
255
+ logger.info(f"Submitting input: {input_text[:50]}... (mode: {self.mode.value})")
256
+
257
+ # Handle exit commands
258
+ if input_text.lower() in ['exit', 'quit', ':q', ':q!', ':wq', ':wq!']:
259
+ self._handle_exit()
260
+ 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
271
+ self.input_value = ""
272
+ self.mode = InputMode.PROMPT
273
+ if self.on_mode_change:
274
+ self.on_mode_change(InputMode.PROMPT)
275
+
276
+ # Update submit count
277
+ self.submit_count += 1
278
+ if self.on_submit_count_change:
279
+ self.on_submit_count_change(lambda x: x + 1)
280
+
281
+ async def _handle_koding_input(self, input_text: str):
282
+ """Handle koding mode input - equivalent to koding mode handling"""
283
+ logger.info(f"Processing koding input: {input_text}")
284
+
285
+ # Strip # prefix if present
286
+ content = input_text[1:].strip() if input_text.startswith('#') else input_text
287
+
288
+ # 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']):
290
+ # Handle as AI request for AGENTS.md content
291
+ await self._handle_koding_ai_request(content)
292
+ else:
293
+ # Handle as direct note to AGENTS.md
294
+ await self._handle_koding_note(content)
295
+
296
+ # Add to history
297
+ self._add_to_history(f"#{input_text}" if not input_text.startswith('#') else input_text)
298
+
299
+ async def _handle_bash_input(self, input_text: str):
300
+ """Handle bash mode input - equivalent to bash command processing"""
301
+ logger.info(f"Processing bash input: {input_text}")
302
+
303
+ # Strip ! prefix if present
304
+ command = input_text[1:].strip() if input_text.startswith('!') else input_text
305
+
306
+ try:
307
+ # Execute bash command (simplified version)
308
+ import subprocess
309
+ result = subprocess.run(
310
+ command,
311
+ shell=True,
312
+ capture_output=True,
313
+ text=True,
314
+ timeout=30
315
+ )
316
+
317
+ # Create assistant message with result
318
+ if result.returncode == 0:
319
+ response = result.stdout or "Command executed successfully"
320
+ else:
321
+ response = f"Error: {result.stderr}"
322
+
323
+ # 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
+
329
+ # 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
+
336
+ if self.set_is_loading:
337
+ self.set_is_loading(True)
338
+
339
+ # Create new abort controller
340
+ if self.set_abort_controller:
341
+ new_controller = asyncio.create_task(asyncio.sleep(0)) # Mock controller
342
+ 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
+
353
+ async def _handle_koding_ai_request(self, content: str):
354
+ """Handle AI request for koding mode"""
355
+ logger.info(f"Processing koding AI request: {content}")
356
+
357
+ # This would integrate with the AI system to generate content for AGENTS.md
358
+ # For now, just log the request
359
+ koding_context = (
360
+ "The user is using Koding mode. Format your response as a comprehensive, "
361
+ "well-structured document suitable for adding to AGENTS.md. Use proper "
362
+ "markdown formatting with headings, lists, code blocks, etc."
363
+ )
364
+
365
+ # This would be processed by the main query system
366
+ if self.on_query:
367
+ user_message = Message(
368
+ type=MessageType.USER,
369
+ message=MessageContent(content),
370
+ options={"isKodingRequest": True, "kodingContext": koding_context}
371
+ )
372
+ await self.on_query([user_message])
373
+
374
+ async def _handle_koding_note(self, content: str):
375
+ """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)
379
+ try:
380
+ interpreted_content = await self._interpret_hash_command(content)
381
+ self._handle_hash_command(interpreted_content)
382
+ 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
+
388
+ 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
+
394
+ def _handle_hash_command(self, content: str):
395
+ """Handle hash command - equivalent to handleHashCommand"""
396
+ try:
397
+ from pathlib import Path
398
+ 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")
406
+ 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]:
410
+ """Process user input - equivalent to processUserInput"""
411
+ user_message = Message(
412
+ type=MessageType.USER,
413
+ message=MessageContent(input_text),
414
+ options={"mode": mode.value}
415
+ )
416
+ return [user_message]
417
+
418
+ 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
+
422
+ def _handle_exit(self):
423
+ """Handle exit command"""
424
+ logger.info("Exit command received")
425
+ # This would typically exit the application
426
+ # For now, just show exit message
427
+ 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
+
470
+ def _handle_quick_model_switch(self):
471
+ """Handle quick model switching - equivalent to handleQuickModelSwitch"""
472
+ logger.info("Model switch requested")
473
+
474
+ # This would integrate with the model manager
475
+ # For now, show a mock message
476
+ self.model_switch_message = {
477
+ "show": True,
478
+ "text": "✅ Model switching would happen here"
479
+ }
480
+
481
+ # Clear message after 3 seconds
482
+ self.set_timer(3.0, lambda: setattr(self, 'model_switch_message', {"show": False, "text": ""}))
483
+
484
+ if self.on_model_change:
485
+ self.on_model_change()
486
+
487
+ def _cycle_mode(self):
488
+ """Cycle through input modes"""
489
+ modes = list(InputMode)
490
+ current_index = modes.index(self.mode)
491
+ new_mode = modes[(current_index + 1) % len(modes)]
492
+ self.mode = new_mode
493
+
494
+ if self.on_mode_change:
495
+ self.on_mode_change(new_mode)
496
+
497
+ logger.info(f"Cycled to mode: {new_mode.value}")
498
+
499
+ # Reactive property watchers
500
+ def watch_mode(self, mode: InputMode):
501
+ """Watch mode changes and update UI"""
502
+ logger.info(f"Mode changed to: {mode.value}")
503
+ try:
504
+ # Update mode prefix
505
+ prefix_widget = self.query_one("#mode_prefix", expect_type=Static)
506
+ prefix_widget.update(self._get_mode_prefix())
507
+
508
+ # Update input placeholder
509
+ input_widget = self.query_one("#main_input", expect_type=Input)
510
+ input_widget.placeholder = self._get_placeholder()
511
+
512
+ # Update container classes
513
+ container = self.query_one("#input_container")
514
+ container.remove_class("mode-prompt", "mode-bash", "mode-koding")
515
+ container.add_class(f"mode-{mode.value}")
516
+ except:
517
+ pass # Widgets might not be mounted yet
518
+
519
+ def watch_is_loading(self, is_loading: bool):
520
+ """Watch loading state changes"""
521
+ try:
522
+ input_widget = self.query_one("#main_input", expect_type=Input)
523
+ input_widget.disabled = self.is_disabled or is_loading
524
+ except:
525
+ pass
526
+
527
+ def watch_input_value(self, value: str):
528
+ """Watch input value changes"""
529
+ try:
530
+ input_widget = self.query_one("#main_input", expect_type=Input)
531
+ if input_widget.value != value:
532
+ input_widget.value = value
533
+ except:
534
+ pass
@@ -0,0 +1,29 @@
1
+ """
2
+ Components module for minion_code
3
+ Contains reusable UI components using Textual
4
+ """
5
+
6
+ from .PromptInput import PromptInput
7
+ from .Message import Message, UserMessage, AssistantMessage, ToolUseMessage
8
+ from .MessageResponse import (
9
+ MessageResponse,
10
+ MessageResponseText,
11
+ MessageResponseStatus,
12
+ MessageResponseProgress,
13
+ MessageResponseTyping,
14
+ MessageResponseWithChildren
15
+ )
16
+
17
+ __all__ = [
18
+ 'PromptInput',
19
+ 'Message',
20
+ 'UserMessage',
21
+ 'AssistantMessage',
22
+ 'ToolUseMessage',
23
+ 'MessageResponse',
24
+ 'MessageResponseText',
25
+ 'MessageResponseStatus',
26
+ 'MessageResponseProgress',
27
+ 'MessageResponseTyping',
28
+ 'MessageResponseWithChildren'
29
+ ]