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.
- examples/cli_entrypoint.py +60 -0
- examples/{agent_with_todos.py → components/agent_with_todos.py} +58 -47
- examples/{message_response_children_demo.py → components/message_response_children_demo.py} +61 -55
- examples/components/messages_component.py +199 -0
- examples/file_freshness_example.py +22 -22
- examples/file_watching_example.py +32 -26
- examples/interruptible_tui.py +921 -3
- examples/repl_tui.py +129 -0
- examples/skills/example_usage.py +57 -0
- examples/start.py +173 -0
- minion_code/__init__.py +1 -1
- minion_code/acp_server/__init__.py +34 -0
- minion_code/acp_server/agent.py +539 -0
- minion_code/acp_server/hooks.py +354 -0
- minion_code/acp_server/main.py +194 -0
- minion_code/acp_server/permissions.py +142 -0
- minion_code/acp_server/test_client.py +104 -0
- minion_code/adapters/__init__.py +22 -0
- minion_code/adapters/output_adapter.py +207 -0
- minion_code/adapters/rich_adapter.py +169 -0
- minion_code/adapters/textual_adapter.py +254 -0
- minion_code/agents/__init__.py +2 -2
- minion_code/agents/code_agent.py +517 -104
- minion_code/agents/hooks.py +378 -0
- minion_code/cli.py +538 -429
- minion_code/cli_simple.py +665 -0
- minion_code/commands/__init__.py +136 -29
- minion_code/commands/clear_command.py +19 -46
- minion_code/commands/help_command.py +33 -49
- minion_code/commands/history_command.py +37 -55
- minion_code/commands/model_command.py +194 -0
- minion_code/commands/quit_command.py +9 -12
- minion_code/commands/resume_command.py +181 -0
- minion_code/commands/skill_command.py +89 -0
- minion_code/commands/status_command.py +48 -73
- minion_code/commands/tools_command.py +54 -52
- minion_code/commands/version_command.py +34 -69
- minion_code/components/ConfirmDialog.py +430 -0
- minion_code/components/Message.py +318 -97
- minion_code/components/MessageResponse.py +30 -29
- minion_code/components/Messages.py +351 -0
- minion_code/components/PromptInput.py +499 -245
- minion_code/components/__init__.py +24 -17
- minion_code/const.py +7 -0
- minion_code/screens/REPL.py +1453 -469
- minion_code/screens/__init__.py +1 -1
- minion_code/services/__init__.py +20 -20
- minion_code/services/event_system.py +19 -14
- minion_code/services/file_freshness_service.py +223 -170
- minion_code/skills/__init__.py +25 -0
- minion_code/skills/skill.py +128 -0
- minion_code/skills/skill_loader.py +198 -0
- minion_code/skills/skill_registry.py +177 -0
- minion_code/subagents/__init__.py +31 -0
- minion_code/subagents/builtin/__init__.py +30 -0
- minion_code/subagents/builtin/claude_code_guide.py +32 -0
- minion_code/subagents/builtin/explore.py +36 -0
- minion_code/subagents/builtin/general_purpose.py +19 -0
- minion_code/subagents/builtin/plan.py +61 -0
- minion_code/subagents/subagent.py +116 -0
- minion_code/subagents/subagent_loader.py +147 -0
- minion_code/subagents/subagent_registry.py +151 -0
- minion_code/tools/__init__.py +8 -2
- minion_code/tools/bash_tool.py +16 -3
- minion_code/tools/file_edit_tool.py +201 -104
- minion_code/tools/file_read_tool.py +183 -26
- minion_code/tools/file_write_tool.py +17 -3
- minion_code/tools/glob_tool.py +23 -2
- minion_code/tools/grep_tool.py +229 -21
- minion_code/tools/ls_tool.py +28 -3
- minion_code/tools/multi_edit_tool.py +89 -84
- minion_code/tools/python_interpreter_tool.py +9 -1
- minion_code/tools/skill_tool.py +210 -0
- minion_code/tools/task_tool.py +287 -0
- minion_code/tools/todo_read_tool.py +28 -24
- minion_code/tools/todo_write_tool.py +82 -65
- minion_code/{types.py → type_defs.py} +15 -2
- minion_code/utils/__init__.py +45 -17
- minion_code/utils/config.py +610 -0
- minion_code/utils/history.py +114 -0
- minion_code/utils/logs.py +53 -0
- minion_code/utils/mcp_loader.py +153 -55
- minion_code/utils/output_truncator.py +233 -0
- minion_code/utils/session_storage.py +369 -0
- minion_code/utils/todo_file_utils.py +26 -22
- minion_code/utils/todo_storage.py +43 -33
- minion_code/web/__init__.py +9 -0
- minion_code/web/adapters/__init__.py +5 -0
- minion_code/web/adapters/web_adapter.py +524 -0
- minion_code/web/api/__init__.py +7 -0
- minion_code/web/api/chat.py +277 -0
- minion_code/web/api/interactions.py +136 -0
- minion_code/web/api/sessions.py +135 -0
- minion_code/web/server.py +149 -0
- minion_code/web/services/__init__.py +5 -0
- minion_code/web/services/session_manager.py +420 -0
- minion_code-0.1.2.dist-info/METADATA +476 -0
- minion_code-0.1.2.dist-info/RECORD +111 -0
- {minion_code-0.1.0.dist-info → minion_code-0.1.2.dist-info}/WHEEL +1 -1
- minion_code-0.1.2.dist-info/entry_points.txt +6 -0
- tests/test_adapter.py +67 -0
- tests/test_adapter_simple.py +79 -0
- tests/test_file_read_tool.py +144 -0
- tests/test_readonly_tools.py +0 -2
- tests/test_skills.py +441 -0
- examples/advance_tui.py +0 -508
- examples/rich_example.py +0 -4
- examples/simple_file_watching.py +0 -57
- examples/simple_tui.py +0 -267
- examples/simple_usage.py +0 -69
- minion_code-0.1.0.dist-info/METADATA +0 -350
- minion_code-0.1.0.dist-info/RECORD +0 -59
- minion_code-0.1.0.dist-info/entry_points.txt +0 -4
- {minion_code-0.1.0.dist-info → minion_code-0.1.2.dist-info}/licenses/LICENSE +0 -0
- {minion_code-0.1.0.dist-info → minion_code-0.1.2.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
|
|
13
|
+
from rich.panel import Panel
|
|
14
|
+
from typing import List, Dict, Any, Optional, Set, Tuple
|
|
14
15
|
import json
|
|
15
|
-
import
|
|
16
|
+
import re
|
|
16
17
|
|
|
17
18
|
# Import shared types
|
|
18
|
-
from ..
|
|
19
|
+
from ..type_defs import Message as MessageType, MessageContent, InputMode
|
|
19
20
|
|
|
20
|
-
|
|
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__(
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
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,
|
|
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,
|
|
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(
|
|
153
|
-
|
|
154
|
-
if block_type ==
|
|
155
|
-
yield from self._render_text_content(block.get(
|
|
156
|
-
elif block_type ==
|
|
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 ==
|
|
309
|
+
elif block_type == "tool_result":
|
|
159
310
|
yield from self._render_tool_result_block(block)
|
|
160
|
-
elif block_type ==
|
|
311
|
+
elif block_type == "thinking":
|
|
161
312
|
yield from self._render_thinking_block(block)
|
|
162
|
-
elif block_type ==
|
|
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(
|
|
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
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
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(
|
|
184
|
-
tool_id = block.get(
|
|
185
|
-
parameters = block.get(
|
|
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
|
-
|
|
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(
|
|
222
|
-
content = block.get(
|
|
223
|
-
is_error = block.get(
|
|
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
|
-
|
|
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(
|
|
241
|
-
yield Static(item.get(
|
|
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(
|
|
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(
|
|
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)
|