minion-code 0.1.0__py3-none-any.whl → 0.1.1__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.1.dist-info/METADATA +475 -0
  98. minion_code-0.1.1.dist-info/RECORD +111 -0
  99. {minion_code-0.1.0.dist-info → minion_code-0.1.1.dist-info}/WHEEL +1 -1
  100. minion_code-0.1.1.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.1.dist-info}/licenses/LICENSE +0 -0
  115. {minion_code-0.1.0.dist-info → minion_code-0.1.1.dist-info}/top_level.txt +0 -0
@@ -10,14 +10,112 @@ from rich.text import Text
10
10
  from rich.console import Console
11
11
  from rich.markdown import Markdown
12
12
  from rich.syntax import Syntax
13
- from typing import List, Dict, Any, Optional, Set
13
+ from rich.panel import Panel
14
+ from typing import List, Dict, Any, Optional, Set, Tuple
14
15
  import json
15
- import logging
16
+ import re
16
17
 
17
18
  # Import shared types
18
- from ..types import Message as MessageType, MessageContent, InputMode
19
+ from ..type_defs import Message as MessageType, MessageContent, InputMode
19
20
 
20
- logger = logging.getLogger(__name__)
21
+
22
+ def parse_agent_response(text: str) -> List[Tuple[str, str]]:
23
+ """
24
+ Parse agent response to extract different sections.
25
+ Returns a list of (section_type, content) tuples.
26
+
27
+ Section types: 'thought', 'code', 'output', 'text'
28
+ """
29
+ sections = []
30
+
31
+ # Pattern to match **Thought:** or **Code:** sections
32
+ thought_pattern = r"\*\*Thought:\*\*\s*(.*?)(?=\*\*Code:\*\*|\*\*Output:\*\*|```|$)"
33
+ code_block_pattern = r"```(\w*)\n(.*?)```"
34
+
35
+ remaining = text
36
+ last_end = 0
37
+
38
+ # First, find all **Thought:** sections
39
+ thought_match = re.search(r"\*\*Thought:\*\*\s*", text)
40
+ code_marker_match = re.search(r"\*\*Code:\*\*\s*", text)
41
+
42
+ if thought_match or code_marker_match:
43
+ # This looks like a structured agent response
44
+
45
+ # Extract thought section
46
+ if thought_match:
47
+ thought_start = thought_match.end()
48
+ # Find where thought ends (at **Code:** or code block)
49
+ thought_end = len(text)
50
+ if code_marker_match and code_marker_match.start() > thought_match.start():
51
+ thought_end = code_marker_match.start()
52
+ else:
53
+ # Look for code block
54
+ code_block = re.search(r"```", text[thought_start:])
55
+ if code_block:
56
+ thought_end = thought_start + code_block.start()
57
+
58
+ thought_content = text[thought_start:thought_end].strip()
59
+ if thought_content:
60
+ sections.append(("thought", thought_content))
61
+ last_end = thought_end
62
+
63
+ # Extract code section
64
+ if code_marker_match:
65
+ code_start = code_marker_match.end()
66
+ # Find the code block after **Code:**
67
+ code_block_match = re.search(
68
+ r"```(\w*)\n(.*?)```", text[code_start:], re.DOTALL
69
+ )
70
+ if code_block_match:
71
+ lang = code_block_match.group(1) or "python"
72
+ code_content = code_block_match.group(2).strip()
73
+ sections.append(("code", f"{lang}:{code_content}"))
74
+ last_end = code_start + code_block_match.end()
75
+ elif not code_marker_match and thought_match:
76
+ # No **Code:** marker, look for code block directly
77
+ code_block_match = re.search(
78
+ r"```(\w*)\n(.*?)```", text[last_end:], re.DOTALL
79
+ )
80
+ if code_block_match:
81
+ lang = code_block_match.group(1) or "python"
82
+ code_content = code_block_match.group(2).strip()
83
+ sections.append(("code", f"{lang}:{code_content}"))
84
+ last_end = last_end + code_block_match.end()
85
+
86
+ # Everything after the code block is output
87
+ output_content = text[last_end:].strip()
88
+ if output_content:
89
+ sections.append(("output", output_content))
90
+
91
+ else:
92
+ # Not a structured response, check for just code blocks
93
+ code_blocks = list(re.finditer(r"```(\w*)\n(.*?)```", text, re.DOTALL))
94
+
95
+ if code_blocks:
96
+ current_pos = 0
97
+ for match in code_blocks:
98
+ # Text before code block
99
+ before_text = text[current_pos : match.start()].strip()
100
+ if before_text:
101
+ sections.append(("text", before_text))
102
+
103
+ # Code block
104
+ lang = match.group(1) or "python"
105
+ code_content = match.group(2).strip()
106
+ sections.append(("code", f"{lang}:{code_content}"))
107
+
108
+ current_pos = match.end()
109
+
110
+ # Text after last code block
111
+ after_text = text[current_pos:].strip()
112
+ if after_text:
113
+ sections.append(("output", after_text))
114
+ else:
115
+ # Plain text
116
+ sections.append(("text", text))
117
+
118
+ return sections if sections else [("text", text)]
21
119
 
22
120
 
23
121
  class Message(Container):
@@ -25,69 +123,123 @@ class Message(Container):
25
123
  Main message component equivalent to React Message
26
124
  Handles rendering of both user and assistant messages
27
125
  """
28
-
126
+
29
127
  DEFAULT_CSS = """
30
128
  Message {
31
129
  width: 80%;
32
130
  height: auto;
33
131
  margin-bottom: 1;
34
132
  }
35
-
133
+
36
134
  .user-message {
37
135
  border-left: thick blue;
38
136
  padding-left: 1;
39
137
  height: auto;
40
138
  }
41
-
139
+
42
140
  .assistant-message {
43
141
  border-left: thick green;
44
142
  padding-left: 1;
45
143
  height: auto;
46
144
  }
47
-
145
+
48
146
  .tool-use-message {
49
147
  border-left: thick yellow;
50
148
  padding-left: 1;
51
149
  background: $surface-lighten-1;
52
150
  height: auto;
53
151
  }
54
-
152
+
55
153
  .error-message {
56
154
  border-left: thick red;
57
155
  padding-left: 1;
58
156
  background: $error 10%;
59
157
  height: auto;
60
158
  }
61
-
159
+
62
160
  .message-content {
63
161
  width: 100%;
64
162
  height: auto;
65
163
  padding: 1;
66
164
  }
67
-
165
+
68
166
  .message-meta {
69
167
  color: $text-muted;
70
168
  text-style: dim;
71
169
  height: 1;
72
170
  }
171
+
172
+ .streaming-message {
173
+ color: $primary;
174
+ text-style: italic;
175
+ background: $primary 10%;
176
+ }
177
+
178
+ /* Section-specific styles */
179
+ .thought-section {
180
+ background: $primary 15%;
181
+ border-left: thick $primary;
182
+ padding: 1;
183
+ margin-bottom: 1;
184
+ }
185
+
186
+ .thought-label {
187
+ color: $primary;
188
+ text-style: bold;
189
+ margin-bottom: 0;
190
+ }
191
+
192
+ .code-section {
193
+ background: $surface-darken-1;
194
+ border-left: thick yellow;
195
+ padding: 1;
196
+ margin-bottom: 1;
197
+ }
198
+
199
+ .code-label {
200
+ color: yellow;
201
+ text-style: bold;
202
+ margin-bottom: 0;
203
+ }
204
+
205
+ .output-section {
206
+ background: green 20%;
207
+ border-left: thick green;
208
+ padding: 1;
209
+ margin-top: 1;
210
+ }
211
+
212
+ .output-label {
213
+ color: green;
214
+ text-style: bold;
215
+ margin-bottom: 0;
216
+ }
217
+
218
+ .section-divider {
219
+ color: $text-muted;
220
+ text-style: dim;
221
+ margin: 1 0;
222
+ }
73
223
  """
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):
224
+
225
+ def __init__(
226
+ self,
227
+ message: MessageType,
228
+ messages: List[MessageType] = None,
229
+ add_margin: bool = True,
230
+ tools: List[Any] = None,
231
+ verbose: bool = False,
232
+ debug: bool = False,
233
+ errored_tool_use_ids: Set[str] = None,
234
+ in_progress_tool_use_ids: Set[str] = None,
235
+ unresolved_tool_use_ids: Set[str] = None,
236
+ should_animate: bool = False,
237
+ should_show_dot: bool = False,
238
+ width: Optional[int] = None,
239
+ **kwargs,
240
+ ):
89
241
  super().__init__(**kwargs)
90
-
242
+
91
243
  self.message = message
92
244
  self.messages = messages or []
93
245
  self.add_margin = add_margin
@@ -100,24 +252,23 @@ class Message(Container):
100
252
  self.should_animate = should_animate
101
253
  self.should_show_dot = should_show_dot
102
254
  self.width = width
103
-
255
+
104
256
  def compose(self):
105
257
  """Compose the message interface"""
106
258
  if self.message.type.value == "assistant":
107
259
  yield from self._render_assistant_message()
108
260
  else:
109
261
  yield from self._render_user_message()
110
-
262
+
111
263
  def _render_user_message(self):
112
264
  """Render user message - equivalent to UserMessage component"""
113
265
  with Vertical(classes="user-message"):
114
266
  # Message metadata
115
267
  if self.verbose or self.debug:
116
268
  yield Static(
117
- f"User • {self._format_timestamp()}",
118
- classes="message-meta"
269
+ f"User • {self._format_timestamp()}", classes="message-meta"
119
270
  )
120
-
271
+
121
272
  # Message content
122
273
  content = self.message.message.content
123
274
  if isinstance(content, str):
@@ -125,20 +276,20 @@ class Message(Container):
125
276
  elif isinstance(content, list):
126
277
  for item in content:
127
278
  yield from self._render_content_block(item)
128
-
279
+
129
280
  def _render_assistant_message(self):
130
281
  """Render assistant message - equivalent to AssistantMessage component"""
131
282
  with Vertical(classes="assistant-message"):
132
283
  # Message metadata
133
284
  if self.verbose or self.debug:
134
285
  meta_text = f"Assistant • {self._format_timestamp()}"
135
- if hasattr(self.message, 'cost_usd') and self.message.cost_usd:
286
+ if hasattr(self.message, "cost_usd") and self.message.cost_usd:
136
287
  meta_text += f" • ${self.message.cost_usd:.4f}"
137
- if hasattr(self.message, 'duration_ms') and self.message.duration_ms:
288
+ if hasattr(self.message, "duration_ms") and self.message.duration_ms:
138
289
  meta_text += f" • {self.message.duration_ms}ms"
139
-
290
+
140
291
  yield Static(meta_text, classes="message-meta")
141
-
292
+
142
293
  # Message content
143
294
  content = self.message.message.content
144
295
  if isinstance(content, str):
@@ -146,44 +297,119 @@ class Message(Container):
146
297
  elif isinstance(content, list):
147
298
  for item in content:
148
299
  yield from self._render_content_block(item)
149
-
300
+
150
301
  def _render_content_block(self, block: Dict[str, Any]):
151
302
  """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':
303
+ block_type = block.get("type", "text")
304
+
305
+ if block_type == "text":
306
+ yield from self._render_text_content(block.get("text", ""))
307
+ elif block_type == "tool_use":
157
308
  yield from self._render_tool_use_block(block)
158
- elif block_type == 'tool_result':
309
+ elif block_type == "tool_result":
159
310
  yield from self._render_tool_result_block(block)
160
- elif block_type == 'thinking':
311
+ elif block_type == "thinking":
161
312
  yield from self._render_thinking_block(block)
162
- elif block_type == 'redacted_thinking':
313
+ elif block_type == "redacted_thinking":
163
314
  yield from self._render_redacted_thinking_block()
164
315
  else:
165
316
  # Unknown block type
166
- yield Static(f"[Unknown content type: {block_type}]", classes="error-message")
167
-
317
+ yield Static(
318
+ f"[Unknown content type: {block_type}]", classes="error-message"
319
+ )
320
+
168
321
  def _render_text_content(self, text: str):
169
- """Render text content with markdown support"""
322
+ """Render text content with markdown support and streaming indicators"""
170
323
  if not text.strip():
171
324
  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}")
325
+
326
+ # Check if this is a streaming or temporary message
327
+ is_streaming = (
328
+ self.message.options.get("streaming", False)
329
+ if self.message.options
330
+ else False
331
+ )
332
+ is_temporary = (
333
+ self.message.options.get("temporary", False)
334
+ if self.message.options
335
+ else False
336
+ )
337
+ is_error = (
338
+ self.message.options.get("error", False) if self.message.options else False
339
+ )
340
+
341
+ # For streaming/temporary/error messages, render simply
342
+ if is_streaming or is_temporary or is_error:
343
+ prefix = ""
344
+ if is_streaming:
345
+ prefix = "⠋ "
346
+ elif is_temporary:
347
+ prefix = "🤔 "
348
+ elif is_error:
349
+ prefix = "❌ "
350
+
351
+ classes = "message-content"
352
+ if is_error:
353
+ classes += " error-message"
354
+ elif is_streaming or is_temporary:
355
+ classes += " streaming-message"
356
+
357
+ yield Static(f"{prefix}{text}", classes=classes)
358
+ return
359
+
360
+ # Parse the response into sections for non-streaming messages
361
+ sections = parse_agent_response(text)
362
+
363
+ # If only one plain text section, render normally
364
+ if len(sections) == 1 and sections[0][0] == "text":
179
365
  yield Static(text, classes="message-content")
180
-
366
+ return
367
+
368
+ # Render each section with appropriate styling
369
+ for section_type, content in sections:
370
+ yield from self._render_section(section_type, content)
371
+
372
+ def _render_section(self, section_type: str, content: str):
373
+ """Render a specific section with appropriate styling"""
374
+ if section_type == "thought":
375
+ with Vertical(classes="thought-section"):
376
+ yield Static("💭 Thought:", classes="thought-label")
377
+ yield Static(content, classes="message-content")
378
+
379
+ elif section_type == "code":
380
+ # Parse language and code content
381
+ if ":" in content:
382
+ lang, code_content = content.split(":", 1)
383
+ else:
384
+ lang, code_content = "python", content
385
+
386
+ with Vertical(classes="code-section"):
387
+ yield Static(f"📝 Code ({lang}):", classes="code-label")
388
+ # Try to use syntax highlighting
389
+ try:
390
+ syntax = Syntax(
391
+ code_content, lang, theme="monokai", line_numbers=True
392
+ )
393
+ yield Static(syntax, classes="message-content")
394
+ except Exception:
395
+ yield Static(
396
+ f"```{lang}\n{code_content}\n```", classes="message-content"
397
+ )
398
+
399
+ elif section_type == "output":
400
+ with Vertical(classes="output-section"):
401
+ yield Static("📤 Output:", classes="output-label")
402
+ yield Static(content, classes="message-content")
403
+
404
+ else: # 'text' or unknown
405
+ yield Static(content, classes="message-content")
406
+
181
407
  def _render_tool_use_block(self, block: Dict[str, Any]):
182
408
  """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
-
409
+ tool_name = block.get("name", "unknown")
410
+ tool_id = block.get("id", "")
411
+ parameters = block.get("input", {})
412
+
187
413
  # Determine status
188
414
  status = "completed"
189
415
  if tool_id in self.in_progress_tool_use_ids:
@@ -192,21 +418,18 @@ class Message(Container):
192
418
  status = "error"
193
419
  elif tool_id in self.unresolved_tool_use_ids:
194
420
  status = "unresolved"
195
-
421
+
196
422
  with Vertical(classes="tool-use-message"):
197
423
  # Tool header
198
424
  status_icon = {
199
425
  "completed": "✅",
200
426
  "in_progress": "⏳",
201
427
  "error": "❌",
202
- "unresolved": "⏸️"
428
+ "unresolved": "⏸️",
203
429
  }.get(status, "🔧")
204
-
205
- yield Static(
206
- f"{status_icon} Tool: {tool_name}",
207
- classes="message-meta"
208
- )
209
-
430
+
431
+ yield Static(f"{status_icon} Tool: {tool_name}", classes="message-meta")
432
+
210
433
  # Tool parameters (if verbose)
211
434
  if self.verbose and parameters:
212
435
  try:
@@ -215,64 +438,62 @@ class Message(Container):
215
438
  yield Static(params_json, classes="message-content")
216
439
  except Exception:
217
440
  yield Static(str(parameters), classes="message-content")
218
-
441
+
219
442
  def _render_tool_result_block(self, block: Dict[str, Any]):
220
443
  """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
-
444
+ tool_use_id = block.get("tool_use_id", "")
445
+ content = block.get("content", "")
446
+ is_error = block.get("is_error", False)
447
+
225
448
  classes = "error-message" if is_error else "message-content"
226
-
449
+
227
450
  with Vertical():
228
451
  # Result header
229
452
  icon = "❌" if is_error else "📤"
230
- yield Static(
231
- f"{icon} Tool Result",
232
- classes="message-meta"
233
- )
234
-
453
+ yield Static(f"{icon} Tool Result", classes="message-meta")
454
+
235
455
  # Result content
236
456
  if isinstance(content, str):
237
457
  yield Static(content, classes=classes)
238
458
  elif isinstance(content, list):
239
459
  for item in content:
240
- if isinstance(item, dict) and item.get('type') == 'text':
241
- yield Static(item.get('text', ''), classes=classes)
460
+ if isinstance(item, dict) and item.get("type") == "text":
461
+ yield Static(item.get("text", ""), classes=classes)
242
462
  else:
243
463
  yield Static(str(item), classes=classes)
244
-
464
+
245
465
  def _render_thinking_block(self, block: Dict[str, Any]):
246
466
  """Render thinking block - equivalent to AssistantThinkingMessage"""
247
467
  if not self.debug:
248
468
  return # Only show in debug mode
249
-
250
- thinking_content = block.get('content', '')
251
-
469
+
470
+ thinking_content = block.get("content", "")
471
+
252
472
  with Vertical():
253
473
  yield Static("🤔 Thinking...", classes="message-meta")
254
474
  yield Static(thinking_content, classes="message-content")
255
-
475
+
256
476
  def _render_redacted_thinking_block(self):
257
477
  """Render redacted thinking block"""
258
478
  if not self.debug:
259
479
  return
260
-
480
+
261
481
  yield Static("🤔 [Thinking content redacted]", classes="message-meta")
262
-
482
+
263
483
  def _format_timestamp(self) -> str:
264
484
  """Format message timestamp"""
265
485
  import datetime
486
+
266
487
  dt = datetime.datetime.fromtimestamp(self.message.timestamp)
267
488
  return dt.strftime("%H:%M:%S")
268
489
 
269
490
 
270
491
  class UserMessage(Message):
271
492
  """Specialized component for user messages"""
272
-
493
+
273
494
  def __init__(self, message: MessageType, **kwargs):
274
495
  super().__init__(message, **kwargs)
275
-
496
+
276
497
  def compose(self):
277
498
  """Compose user message with specific styling"""
278
499
  yield from self._render_user_message()
@@ -280,10 +501,10 @@ class UserMessage(Message):
280
501
 
281
502
  class AssistantMessage(Message):
282
503
  """Specialized component for assistant messages"""
283
-
504
+
284
505
  def __init__(self, message: MessageType, **kwargs):
285
506
  super().__init__(message, **kwargs)
286
-
507
+
287
508
  def compose(self):
288
509
  """Compose assistant message with specific styling"""
289
510
  yield from self._render_assistant_message()
@@ -291,14 +512,14 @@ class AssistantMessage(Message):
291
512
 
292
513
  class ToolUseMessage(Message):
293
514
  """Specialized component for tool use messages"""
294
-
515
+
295
516
  def __init__(self, message: MessageType, **kwargs):
296
517
  super().__init__(message, **kwargs)
297
-
518
+
298
519
  def compose(self):
299
520
  """Compose tool use message with specific styling"""
300
521
  content = self.message.message.content
301
522
  if isinstance(content, list):
302
523
  for item in content:
303
- if item.get('type') == 'tool_use':
304
- yield from self._render_tool_use_block(item)
524
+ if item.get("type") == "tool_use":
525
+ yield from self._render_tool_use_block(item)