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
@@ -0,0 +1,430 @@
1
+ #!/usr/bin/env python3
2
+ # -*- coding: utf-8 -*-
3
+ """
4
+ Dialog Components for Textual TUI
5
+
6
+ These components provide user interaction dialogs for confirmation,
7
+ choice selection, and text input in the Textual TUI.
8
+ """
9
+
10
+ from textual.containers import Container, Vertical, Horizontal, VerticalScroll
11
+ from textual.widgets import Button, Static, Label
12
+ from textual.app import ComposeResult
13
+ from textual import on
14
+ from typing import Callable, Optional, List
15
+
16
+
17
+ class ConfirmDialog(Container):
18
+ """
19
+ Confirmation dialog component.
20
+
21
+ Displays a message with Yes/No buttons and calls a callback
22
+ with the user's choice.
23
+ """
24
+
25
+ DEFAULT_CSS = """
26
+ ConfirmDialog {
27
+ width: 60;
28
+ height: auto;
29
+ background: $panel;
30
+ border: thick $primary;
31
+ padding: 1 2;
32
+ layer: overlay;
33
+ }
34
+
35
+ ConfirmDialog .dialog-title {
36
+ text-style: bold;
37
+ text-align: center;
38
+ color: $warning;
39
+ margin-bottom: 1;
40
+ }
41
+
42
+ ConfirmDialog .dialog-message {
43
+ text-align: left;
44
+ color: $text;
45
+ margin-bottom: 2;
46
+ padding: 1;
47
+ }
48
+
49
+ ConfirmDialog .dialog-buttons {
50
+ height: 3;
51
+ align: center middle;
52
+ }
53
+
54
+ ConfirmDialog Button {
55
+ margin: 0 1;
56
+ }
57
+ """
58
+
59
+ def __init__(
60
+ self,
61
+ interaction_id: str,
62
+ message: str,
63
+ title: str = "Confirm",
64
+ ok_text: str = "Yes",
65
+ cancel_text: str = "No",
66
+ on_result: Optional[Callable[[str, bool], None]] = None,
67
+ **kwargs,
68
+ ):
69
+ """
70
+ Initialize confirmation dialog.
71
+
72
+ Args:
73
+ interaction_id: Unique ID for this interaction
74
+ message: Message to display
75
+ title: Dialog title
76
+ ok_text: Text for confirmation button
77
+ cancel_text: Text for cancel button
78
+ on_result: Callback function (interaction_id, result)
79
+ """
80
+ super().__init__(**kwargs)
81
+ self.interaction_id = interaction_id
82
+ self.message = message
83
+ self.title = title
84
+ self.ok_text = ok_text
85
+ self.cancel_text = cancel_text
86
+ self.on_result = on_result
87
+
88
+ def compose(self) -> ComposeResult:
89
+ """Compose the dialog UI."""
90
+ with Vertical():
91
+ yield Static(self.title, classes="dialog-title")
92
+ yield Static(self.message, classes="dialog-message")
93
+ with Horizontal(classes="dialog-buttons"):
94
+ yield Button(self.ok_text, id="confirm_ok", variant="success")
95
+ yield Button(self.cancel_text, id="confirm_cancel", variant="error")
96
+
97
+ @on(Button.Pressed, "#confirm_ok")
98
+ def on_ok(self):
99
+ """Handle confirmation button click."""
100
+ if self.on_result:
101
+ self.on_result(self.interaction_id, True)
102
+ self.remove()
103
+
104
+ @on(Button.Pressed, "#confirm_cancel")
105
+ def on_cancel(self):
106
+ """Handle cancel button click."""
107
+ if self.on_result:
108
+ self.on_result(self.interaction_id, False)
109
+ self.remove()
110
+
111
+
112
+ class ChoiceDialog(Container):
113
+ """
114
+ Choice selection dialog component.
115
+
116
+ Displays a list of choices as buttons and calls a callback
117
+ with the selected index. Supports keyboard navigation with
118
+ arrow keys and number keys.
119
+ """
120
+
121
+ BINDINGS = [
122
+ ("up", "move_up", "Previous"),
123
+ ("down", "move_down", "Next"),
124
+ ("k", "move_up", "Previous"),
125
+ ("j", "move_down", "Next"),
126
+ ("escape", "cancel", "Cancel"),
127
+ ("enter", "select", "Select"),
128
+ ]
129
+
130
+ DEFAULT_CSS = """
131
+ ChoiceDialog {
132
+ width: 60;
133
+ max-height: 80%;
134
+ background: $panel;
135
+ border: thick $primary;
136
+ padding: 1 2;
137
+ layer: overlay;
138
+ }
139
+
140
+ ChoiceDialog .dialog-title {
141
+ text-style: bold;
142
+ text-align: center;
143
+ color: $accent;
144
+ margin-bottom: 1;
145
+ }
146
+
147
+ ChoiceDialog .dialog-message {
148
+ text-align: left;
149
+ color: $text;
150
+ margin-bottom: 1;
151
+ padding: 1;
152
+ }
153
+
154
+ ChoiceDialog .dialog-hint {
155
+ text-align: center;
156
+ color: $text-muted;
157
+ margin-bottom: 1;
158
+ }
159
+
160
+ ChoiceDialog .choice-scroll {
161
+ max-height: 50vh;
162
+ min-height: 10;
163
+ padding: 1;
164
+ scrollbar-gutter: stable;
165
+ }
166
+
167
+ ChoiceDialog Button {
168
+ width: 100%;
169
+ margin: 0 0 1 0;
170
+ }
171
+
172
+ ChoiceDialog Button:focus {
173
+ background: $accent;
174
+ }
175
+
176
+ ChoiceDialog .cancel-button {
177
+ margin-top: 1;
178
+ }
179
+ """
180
+
181
+ def __init__(
182
+ self,
183
+ interaction_id: str,
184
+ message: str,
185
+ choices: List[str],
186
+ title: str = "Select",
187
+ on_result: Optional[Callable[[str, int], None]] = None,
188
+ **kwargs,
189
+ ):
190
+ """
191
+ Initialize choice dialog.
192
+
193
+ Args:
194
+ interaction_id: Unique ID for this interaction
195
+ message: Message to display
196
+ choices: List of choice strings
197
+ title: Dialog title
198
+ on_result: Callback function (interaction_id, selected_index)
199
+ """
200
+ super().__init__(**kwargs)
201
+ self.interaction_id = interaction_id
202
+ self.message = message
203
+ self.choices = choices
204
+ self.title = title
205
+ self.on_result = on_result
206
+ self.can_focus = True
207
+
208
+ def compose(self) -> ComposeResult:
209
+ """Compose the dialog UI."""
210
+ with Vertical():
211
+ yield Static(self.title, classes="dialog-title")
212
+ if self.message:
213
+ yield Static(self.message, classes="dialog-message")
214
+ yield Static(
215
+ "Use ↑↓/jk to navigate, Enter to select, Esc to cancel",
216
+ classes="dialog-hint",
217
+ )
218
+ with VerticalScroll(classes="choice-scroll"):
219
+ for i, choice in enumerate(self.choices):
220
+ yield Button(
221
+ f"{i+1}. {choice}", id=f"choice_{i}", variant="primary"
222
+ )
223
+ yield Button(
224
+ "Cancel", id="choice_cancel", variant="error", classes="cancel-button"
225
+ )
226
+
227
+ def on_mount(self):
228
+ """Focus first button when dialog appears."""
229
+ try:
230
+ first_button = self.query_one("#choice_0", Button)
231
+ first_button.focus()
232
+ except Exception:
233
+ pass
234
+
235
+ def action_move_up(self):
236
+ """Move focus to previous choice."""
237
+ try:
238
+ focused = self.app.focused
239
+ if focused and focused.id and focused.id.startswith("choice_"):
240
+ if focused.id == "choice_cancel":
241
+ # Move from cancel to last choice
242
+ last_btn = self.query_one(f"#choice_{len(self.choices)-1}", Button)
243
+ last_btn.focus()
244
+ self._scroll_to_button(last_btn)
245
+ else:
246
+ idx = int(focused.id.split("_")[1])
247
+ if idx > 0:
248
+ prev_btn = self.query_one(f"#choice_{idx-1}", Button)
249
+ prev_btn.focus()
250
+ self._scroll_to_button(prev_btn)
251
+ except Exception:
252
+ pass
253
+
254
+ def action_move_down(self):
255
+ """Move focus to next choice."""
256
+ try:
257
+ focused = self.app.focused
258
+ if focused and focused.id and focused.id.startswith("choice_"):
259
+ idx = int(focused.id.split("_")[1])
260
+ if idx < len(self.choices) - 1:
261
+ next_btn = self.query_one(f"#choice_{idx+1}", Button)
262
+ next_btn.focus()
263
+ self._scroll_to_button(next_btn)
264
+ else:
265
+ # Move to cancel button
266
+ cancel_btn = self.query_one("#choice_cancel", Button)
267
+ cancel_btn.focus()
268
+ except Exception:
269
+ pass
270
+
271
+ def _scroll_to_button(self, button: Button):
272
+ """Scroll to make the button visible."""
273
+ try:
274
+ scroll_container = self.query_one(".choice-scroll", VerticalScroll)
275
+ scroll_container.scroll_to_widget(button)
276
+ except Exception:
277
+ pass
278
+
279
+ def action_select(self):
280
+ """Select the currently focused choice."""
281
+ try:
282
+ focused = self.app.focused
283
+ if focused and focused.id:
284
+ focused.press()
285
+ except Exception:
286
+ pass
287
+
288
+ def action_cancel(self):
289
+ """Cancel the dialog."""
290
+ if self.on_result:
291
+ self.on_result(self.interaction_id, -1)
292
+ self.remove()
293
+
294
+ @on(Button.Pressed)
295
+ def on_button_pressed(self, event: Button.Pressed):
296
+ """Handle button click."""
297
+ button_id = event.button.id
298
+
299
+ if button_id == "choice_cancel":
300
+ if self.on_result:
301
+ self.on_result(self.interaction_id, -1)
302
+ elif button_id and button_id.startswith("choice_"):
303
+ try:
304
+ choice_index = int(button_id.split("_")[1])
305
+ if self.on_result:
306
+ self.on_result(self.interaction_id, choice_index)
307
+ except (ValueError, IndexError):
308
+ pass
309
+
310
+ self.remove()
311
+
312
+
313
+ class InputDialog(Container):
314
+ """
315
+ Text input dialog component.
316
+
317
+ Displays an input field and calls a callback with the entered text.
318
+ """
319
+
320
+ DEFAULT_CSS = """
321
+ InputDialog {
322
+ width: 60;
323
+ height: auto;
324
+ background: $panel;
325
+ border: thick $primary;
326
+ padding: 1 2;
327
+ layer: overlay;
328
+ }
329
+
330
+ InputDialog .dialog-title {
331
+ text-style: bold;
332
+ text-align: center;
333
+ color: $accent;
334
+ margin-bottom: 1;
335
+ }
336
+
337
+ InputDialog .dialog-message {
338
+ text-align: left;
339
+ color: $text;
340
+ margin-bottom: 1;
341
+ padding: 1;
342
+ }
343
+
344
+ InputDialog Input {
345
+ width: 100%;
346
+ margin: 1 0;
347
+ }
348
+
349
+ InputDialog .dialog-buttons {
350
+ height: 3;
351
+ align: center middle;
352
+ margin-top: 1;
353
+ }
354
+
355
+ InputDialog Button {
356
+ margin: 0 1;
357
+ }
358
+ """
359
+
360
+ def __init__(
361
+ self,
362
+ interaction_id: str,
363
+ message: str,
364
+ title: str = "Input",
365
+ default: str = "",
366
+ placeholder: str = "",
367
+ on_result: Optional[Callable[[str, Optional[str]], None]] = None,
368
+ **kwargs,
369
+ ):
370
+ """
371
+ Initialize input dialog.
372
+
373
+ Args:
374
+ interaction_id: Unique ID for this interaction
375
+ message: Message to display
376
+ title: Dialog title
377
+ default: Default input value
378
+ placeholder: Placeholder text
379
+ on_result: Callback function (interaction_id, input_text or None)
380
+ """
381
+ super().__init__(**kwargs)
382
+ self.interaction_id = interaction_id
383
+ self.message = message
384
+ self.title = title
385
+ self.default = default
386
+ self.placeholder = placeholder
387
+ self.on_result = on_result
388
+
389
+ def compose(self) -> ComposeResult:
390
+ """Compose the dialog UI."""
391
+ from textual.widgets import Input
392
+
393
+ with Vertical():
394
+ yield Static(self.title, classes="dialog-title")
395
+ if self.message:
396
+ yield Static(self.message, classes="dialog-message")
397
+ yield Input(
398
+ value=self.default, placeholder=self.placeholder, id="input_field"
399
+ )
400
+ with Horizontal(classes="dialog-buttons"):
401
+ yield Button("OK", id="input_ok", variant="success")
402
+ yield Button("Cancel", id="input_cancel", variant="error")
403
+
404
+ def on_mount(self):
405
+ """Focus the input field when dialog appears."""
406
+ try:
407
+ input_field = self.query_one("#input_field")
408
+ input_field.focus()
409
+ except:
410
+ pass
411
+
412
+ @on(Button.Pressed, "#input_ok")
413
+ def on_ok(self):
414
+ """Handle OK button click."""
415
+ try:
416
+ input_field = self.query_one("#input_field")
417
+ value = input_field.value
418
+ if self.on_result:
419
+ self.on_result(self.interaction_id, value)
420
+ except:
421
+ if self.on_result:
422
+ self.on_result(self.interaction_id, None)
423
+ self.remove()
424
+
425
+ @on(Button.Pressed, "#input_cancel")
426
+ def on_cancel(self):
427
+ """Handle cancel button click."""
428
+ if self.on_result:
429
+ self.on_result(self.interaction_id, None)
430
+ self.remove()