vibecore 0.2.0a1__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.
Potentially problematic release.
This version of vibecore might be problematic. Click here for more details.
- vibecore/__init__.py +0 -0
- vibecore/agents/default.py +79 -0
- vibecore/agents/prompts.py +12 -0
- vibecore/agents/task_agent.py +66 -0
- vibecore/cli.py +131 -0
- vibecore/context.py +24 -0
- vibecore/handlers/__init__.py +5 -0
- vibecore/handlers/stream_handler.py +231 -0
- vibecore/main.py +506 -0
- vibecore/main.tcss +0 -0
- vibecore/mcp/__init__.py +6 -0
- vibecore/mcp/manager.py +167 -0
- vibecore/mcp/server_wrapper.py +109 -0
- vibecore/models/__init__.py +5 -0
- vibecore/models/anthropic.py +239 -0
- vibecore/prompts/common_system_prompt.txt +64 -0
- vibecore/py.typed +0 -0
- vibecore/session/__init__.py +5 -0
- vibecore/session/file_lock.py +127 -0
- vibecore/session/jsonl_session.py +236 -0
- vibecore/session/loader.py +193 -0
- vibecore/session/path_utils.py +81 -0
- vibecore/settings.py +161 -0
- vibecore/tools/__init__.py +1 -0
- vibecore/tools/base.py +27 -0
- vibecore/tools/file/__init__.py +5 -0
- vibecore/tools/file/executor.py +282 -0
- vibecore/tools/file/tools.py +184 -0
- vibecore/tools/file/utils.py +78 -0
- vibecore/tools/python/__init__.py +1 -0
- vibecore/tools/python/backends/__init__.py +1 -0
- vibecore/tools/python/backends/terminal_backend.py +58 -0
- vibecore/tools/python/helpers.py +80 -0
- vibecore/tools/python/manager.py +208 -0
- vibecore/tools/python/tools.py +27 -0
- vibecore/tools/shell/__init__.py +5 -0
- vibecore/tools/shell/executor.py +223 -0
- vibecore/tools/shell/tools.py +156 -0
- vibecore/tools/task/__init__.py +5 -0
- vibecore/tools/task/executor.py +51 -0
- vibecore/tools/task/tools.py +51 -0
- vibecore/tools/todo/__init__.py +1 -0
- vibecore/tools/todo/manager.py +31 -0
- vibecore/tools/todo/models.py +36 -0
- vibecore/tools/todo/tools.py +111 -0
- vibecore/utils/__init__.py +5 -0
- vibecore/utils/text.py +28 -0
- vibecore/widgets/core.py +332 -0
- vibecore/widgets/core.tcss +63 -0
- vibecore/widgets/expandable.py +121 -0
- vibecore/widgets/expandable.tcss +69 -0
- vibecore/widgets/info.py +25 -0
- vibecore/widgets/info.tcss +17 -0
- vibecore/widgets/messages.py +232 -0
- vibecore/widgets/messages.tcss +85 -0
- vibecore/widgets/tool_message_factory.py +121 -0
- vibecore/widgets/tool_messages.py +483 -0
- vibecore/widgets/tool_messages.tcss +289 -0
- vibecore-0.2.0a1.dist-info/METADATA +407 -0
- vibecore-0.2.0a1.dist-info/RECORD +63 -0
- vibecore-0.2.0a1.dist-info/WHEEL +4 -0
- vibecore-0.2.0a1.dist-info/entry_points.txt +2 -0
- vibecore-0.2.0a1.dist-info/licenses/LICENSE +21 -0
|
@@ -0,0 +1,483 @@
|
|
|
1
|
+
"""Tool-specific message widgets for vibecore.
|
|
2
|
+
|
|
3
|
+
This module contains specialized message widgets for displaying
|
|
4
|
+
the execution and results of various tools.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
import json
|
|
8
|
+
import re
|
|
9
|
+
from typing import TYPE_CHECKING
|
|
10
|
+
|
|
11
|
+
from agents import Agent, StreamEvent
|
|
12
|
+
from textual import log
|
|
13
|
+
from textual.app import ComposeResult
|
|
14
|
+
from textual.containers import Horizontal, Vertical
|
|
15
|
+
from textual.content import Content
|
|
16
|
+
from textual.reactive import reactive
|
|
17
|
+
from textual.widgets import Button, Static
|
|
18
|
+
|
|
19
|
+
from vibecore.widgets.core import MainScroll
|
|
20
|
+
|
|
21
|
+
from .expandable import ExpandableContent, ExpandableMarkdown
|
|
22
|
+
from .messages import AgentMessage, BaseMessage, MessageHeader, MessageStatus
|
|
23
|
+
|
|
24
|
+
if TYPE_CHECKING:
|
|
25
|
+
from vibecore.handlers.stream_handler import AgentStreamHandler
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
class BaseToolMessage(BaseMessage):
|
|
29
|
+
"""Base class for all tool execution messages."""
|
|
30
|
+
|
|
31
|
+
output: reactive[str] = reactive("", recompose=True)
|
|
32
|
+
|
|
33
|
+
def update(self, status: MessageStatus, output: str | None = None) -> None:
|
|
34
|
+
"""Update the status and optionally the output of the tool message."""
|
|
35
|
+
self.status = status
|
|
36
|
+
if output is not None:
|
|
37
|
+
self.output = output
|
|
38
|
+
|
|
39
|
+
def _render_output(
|
|
40
|
+
self, output, truncated_lines: int = 3, collapsed_text: str | Content | None = None
|
|
41
|
+
) -> ComposeResult:
|
|
42
|
+
"""Render the output section if output exists."""
|
|
43
|
+
if output:
|
|
44
|
+
with Horizontal(classes="tool-output"):
|
|
45
|
+
yield Static("└─", classes="tool-output-prefix")
|
|
46
|
+
with Vertical(classes="tool-output-content"):
|
|
47
|
+
yield ExpandableContent(
|
|
48
|
+
Content(output),
|
|
49
|
+
truncated_lines=truncated_lines,
|
|
50
|
+
classes="tool-output-expandable",
|
|
51
|
+
collapsed_text=collapsed_text,
|
|
52
|
+
)
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
class ToolMessage(BaseToolMessage):
|
|
56
|
+
"""A widget to display generic tool execution messages."""
|
|
57
|
+
|
|
58
|
+
tool_name: reactive[str] = reactive("")
|
|
59
|
+
command: reactive[str] = reactive("")
|
|
60
|
+
|
|
61
|
+
def __init__(
|
|
62
|
+
self, tool_name: str, command: str, output: str = "", status: MessageStatus = MessageStatus.EXECUTING, **kwargs
|
|
63
|
+
) -> None:
|
|
64
|
+
"""
|
|
65
|
+
Construct a ToolMessage.
|
|
66
|
+
|
|
67
|
+
Args:
|
|
68
|
+
tool_name: The name of the tool (e.g., "Bash").
|
|
69
|
+
command: The command being executed.
|
|
70
|
+
output: The output from the tool (optional, can be set later).
|
|
71
|
+
status: The status of execution.
|
|
72
|
+
**kwargs: Additional keyword arguments for Widget.
|
|
73
|
+
"""
|
|
74
|
+
super().__init__(status=status, **kwargs)
|
|
75
|
+
self.tool_name = tool_name
|
|
76
|
+
self.command = command
|
|
77
|
+
self.output = output
|
|
78
|
+
|
|
79
|
+
def compose(self) -> ComposeResult:
|
|
80
|
+
"""Create child widgets for the tool message."""
|
|
81
|
+
# Truncate command if too long
|
|
82
|
+
max_command_length = 60
|
|
83
|
+
display_command = (
|
|
84
|
+
self.command[:max_command_length] + "…" if len(self.command) > max_command_length else self.command
|
|
85
|
+
)
|
|
86
|
+
|
|
87
|
+
# Header line
|
|
88
|
+
header = f"{self.tool_name}({display_command})"
|
|
89
|
+
yield MessageHeader("⏺", header, status=self.status)
|
|
90
|
+
|
|
91
|
+
# Output lines
|
|
92
|
+
yield from self._render_output(self.output, truncated_lines=3)
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
class PythonToolMessage(BaseToolMessage):
|
|
96
|
+
"""A widget to display Python code execution messages."""
|
|
97
|
+
|
|
98
|
+
code: reactive[str] = reactive("")
|
|
99
|
+
|
|
100
|
+
def __init__(self, code: str, output: str = "", status: MessageStatus = MessageStatus.EXECUTING, **kwargs) -> None:
|
|
101
|
+
"""
|
|
102
|
+
Construct a PythonToolMessage.
|
|
103
|
+
|
|
104
|
+
Args:
|
|
105
|
+
code: The Python code being executed.
|
|
106
|
+
output: The output from the execution (optional, can be set later).
|
|
107
|
+
status: The status of execution.
|
|
108
|
+
**kwargs: Additional keyword arguments for Widget.
|
|
109
|
+
"""
|
|
110
|
+
super().__init__(status=status, **kwargs)
|
|
111
|
+
self.code = code
|
|
112
|
+
self.output = output
|
|
113
|
+
|
|
114
|
+
def on_button_pressed(self, event: Button.Pressed) -> None:
|
|
115
|
+
"""Handle button press events."""
|
|
116
|
+
if event.button.has_class("copy-button"):
|
|
117
|
+
# Copy the Python code to clipboard
|
|
118
|
+
self.app.copy_to_clipboard(self.code)
|
|
119
|
+
|
|
120
|
+
def compose(self) -> ComposeResult:
|
|
121
|
+
"""Create child widgets for the Python execution message."""
|
|
122
|
+
# Header line
|
|
123
|
+
yield MessageHeader("⏺", "Python", status=self.status)
|
|
124
|
+
|
|
125
|
+
# Python code display
|
|
126
|
+
with Horizontal(classes="python-code"):
|
|
127
|
+
yield Static("└─", classes="python-code-prefix")
|
|
128
|
+
yield Button("Copy", classes="copy-button", variant="primary")
|
|
129
|
+
with Vertical(classes="python-code-content code-container"):
|
|
130
|
+
# Use ExpandableMarkdown for code display
|
|
131
|
+
yield ExpandableMarkdown(
|
|
132
|
+
self.code, language="python", truncated_lines=8, classes="python-code-expandable"
|
|
133
|
+
)
|
|
134
|
+
|
|
135
|
+
# Output
|
|
136
|
+
yield from self._render_output(self.output, truncated_lines=5)
|
|
137
|
+
|
|
138
|
+
|
|
139
|
+
class ReadToolMessage(BaseToolMessage):
|
|
140
|
+
"""A widget to display file read operations with collapsible content."""
|
|
141
|
+
|
|
142
|
+
file_path: reactive[str] = reactive("")
|
|
143
|
+
content: reactive[str] = reactive("", recompose=True)
|
|
144
|
+
|
|
145
|
+
_LINE_NUMBER_PATTERN = re.compile(r"^\s*\d+\t", re.MULTILINE)
|
|
146
|
+
|
|
147
|
+
def __init__(
|
|
148
|
+
self, file_path: str, output: str = "", status: MessageStatus = MessageStatus.EXECUTING, **kwargs
|
|
149
|
+
) -> None:
|
|
150
|
+
"""
|
|
151
|
+
Construct a ReadToolMessage.
|
|
152
|
+
|
|
153
|
+
Args:
|
|
154
|
+
file_path: The file path being read.
|
|
155
|
+
output: The output from the read operation (can be set later).
|
|
156
|
+
status: The status of execution.
|
|
157
|
+
**kwargs: Additional keyword arguments for Widget.
|
|
158
|
+
"""
|
|
159
|
+
super().__init__(status=status, **kwargs)
|
|
160
|
+
self.file_path = file_path
|
|
161
|
+
self.output = output
|
|
162
|
+
|
|
163
|
+
def compose(self) -> ComposeResult:
|
|
164
|
+
"""Create child widgets for the read message."""
|
|
165
|
+
# Truncate file path if too long
|
|
166
|
+
max_path_length = 60
|
|
167
|
+
display_path = (
|
|
168
|
+
self.file_path[:max_path_length] + "…" if len(self.file_path) > max_path_length else self.file_path
|
|
169
|
+
)
|
|
170
|
+
|
|
171
|
+
# Header line
|
|
172
|
+
header = f"Read({display_path})"
|
|
173
|
+
yield MessageHeader("⏺", header, status=self.status)
|
|
174
|
+
|
|
175
|
+
clean_output = self._LINE_NUMBER_PATTERN.sub("", self.output)
|
|
176
|
+
line_count = len(self.output.splitlines()) if self.output else 0
|
|
177
|
+
collapsed_text = f"Read [b]{line_count}[/b] lines (view)"
|
|
178
|
+
|
|
179
|
+
yield from self._render_output(clean_output, truncated_lines=0, collapsed_text=collapsed_text)
|
|
180
|
+
|
|
181
|
+
|
|
182
|
+
class TaskToolMessage(BaseToolMessage):
|
|
183
|
+
"""A widget to display task execution messages."""
|
|
184
|
+
|
|
185
|
+
description: reactive[str] = reactive("", recompose=True)
|
|
186
|
+
prompt: reactive[str] = reactive("", recompose=True)
|
|
187
|
+
|
|
188
|
+
def __init__(
|
|
189
|
+
self, description: str, prompt: str, output: str = "", status: MessageStatus = MessageStatus.EXECUTING, **kwargs
|
|
190
|
+
) -> None:
|
|
191
|
+
"""
|
|
192
|
+
Construct a TaskToolMessage.
|
|
193
|
+
|
|
194
|
+
Args:
|
|
195
|
+
description: Short task description.
|
|
196
|
+
prompt: Full task instructions.
|
|
197
|
+
output: The output from the task execution (optional, can be set later).
|
|
198
|
+
status: The status of execution.
|
|
199
|
+
**kwargs: Additional keyword arguments for Widget.
|
|
200
|
+
"""
|
|
201
|
+
super().__init__(status=status, **kwargs)
|
|
202
|
+
self.description = description
|
|
203
|
+
self.prompt = prompt
|
|
204
|
+
self.output = output
|
|
205
|
+
self._agent_stream_handler: AgentStreamHandler | None = None
|
|
206
|
+
self.main_scroll = MainScroll(id="messages")
|
|
207
|
+
|
|
208
|
+
def compose(self) -> ComposeResult:
|
|
209
|
+
"""Create child widgets for the task message."""
|
|
210
|
+
# Header line
|
|
211
|
+
header = f"Task({self.description})"
|
|
212
|
+
yield MessageHeader("⏺", header, status=self.status)
|
|
213
|
+
|
|
214
|
+
# Show prompt if available and status is executing
|
|
215
|
+
if self.prompt and self.status == MessageStatus.EXECUTING:
|
|
216
|
+
with Horizontal(classes="task-prompt"):
|
|
217
|
+
yield Static("└─", classes="task-prompt-prefix")
|
|
218
|
+
with Vertical(classes="task-prompt-content"):
|
|
219
|
+
yield ExpandableContent(
|
|
220
|
+
self.prompt,
|
|
221
|
+
truncated_lines=5,
|
|
222
|
+
classes="task-prompt-expandable",
|
|
223
|
+
)
|
|
224
|
+
|
|
225
|
+
# XXX(serialx): self.output being a recompose=True field means whenever self.output changes, main_scroll will be
|
|
226
|
+
# emptied. So let's just hide it for now.
|
|
227
|
+
# TODO(serialx): Turn all recompose=True fields into TCSS display: none toggle to avoid this issue.
|
|
228
|
+
if not self.output:
|
|
229
|
+
with Horizontal(classes="message-content"):
|
|
230
|
+
yield Static("└─", classes="message-content-prefix")
|
|
231
|
+
with Vertical(classes="message-content-body"):
|
|
232
|
+
log(f"self id: {id(self)}")
|
|
233
|
+
log(f"self.main_scroll(id: {id(self.main_scroll)}): {self.main_scroll}")
|
|
234
|
+
yield self.main_scroll
|
|
235
|
+
|
|
236
|
+
# Output lines
|
|
237
|
+
yield from self._render_output(self.output, truncated_lines=5)
|
|
238
|
+
|
|
239
|
+
async def handle_task_tool_event(self, event: StreamEvent) -> None:
|
|
240
|
+
"""Handle task tool events from the agent.
|
|
241
|
+
Note: This is called by the main app's AgentStreamHandler to process tool events.
|
|
242
|
+
"""
|
|
243
|
+
# Create handler lazily to avoid circular import
|
|
244
|
+
if self._agent_stream_handler is None:
|
|
245
|
+
from vibecore.handlers.stream_handler import AgentStreamHandler
|
|
246
|
+
|
|
247
|
+
self._agent_stream_handler = AgentStreamHandler(self)
|
|
248
|
+
|
|
249
|
+
await self._agent_stream_handler.handle_event(event)
|
|
250
|
+
|
|
251
|
+
async def add_message(self, message: BaseMessage) -> None:
|
|
252
|
+
"""Add a message widget to the main scroll area.
|
|
253
|
+
|
|
254
|
+
Args:
|
|
255
|
+
message: The message to add
|
|
256
|
+
"""
|
|
257
|
+
await self.main_scroll.mount(message)
|
|
258
|
+
|
|
259
|
+
async def handle_agent_message(self, message: BaseMessage) -> None:
|
|
260
|
+
"""Add a message widget to the main scroll area."""
|
|
261
|
+
await self.add_message(message)
|
|
262
|
+
|
|
263
|
+
async def handle_agent_update(self, new_agent: Agent) -> None:
|
|
264
|
+
"""Handle agent updates."""
|
|
265
|
+
pass
|
|
266
|
+
|
|
267
|
+
async def handle_agent_error(self, error: Exception) -> None:
|
|
268
|
+
"""Handle errors during streaming."""
|
|
269
|
+
log(f"Error during task agent response: {type(error).__name__}: {error!s}")
|
|
270
|
+
|
|
271
|
+
# Create an error message for the user
|
|
272
|
+
error_msg = f"❌ Error: {type(error).__name__}"
|
|
273
|
+
if str(error):
|
|
274
|
+
error_msg += f"\n\n{error!s}"
|
|
275
|
+
|
|
276
|
+
# Display the error to the user
|
|
277
|
+
# TODO(serialx): Use a dedicated error message widget
|
|
278
|
+
error_agent_msg = AgentMessage(error_msg, status=MessageStatus.ERROR)
|
|
279
|
+
await self.add_message(error_agent_msg)
|
|
280
|
+
|
|
281
|
+
async def handle_agent_finished(self) -> None:
|
|
282
|
+
"""Handle when the agent has finished processing."""
|
|
283
|
+
# Remove the last agent message if it is still executing (which means the agent run was cancelled)
|
|
284
|
+
main_scroll = self.query_one("#messages", MainScroll)
|
|
285
|
+
try:
|
|
286
|
+
last_message = main_scroll.query_one("BaseMessage:last-child", BaseMessage)
|
|
287
|
+
if last_message.status == MessageStatus.EXECUTING:
|
|
288
|
+
last_message.remove()
|
|
289
|
+
except Exception:
|
|
290
|
+
# No messages to clean up
|
|
291
|
+
pass
|
|
292
|
+
|
|
293
|
+
|
|
294
|
+
class TodoWriteToolMessage(BaseToolMessage):
|
|
295
|
+
"""A widget to display todo list updates."""
|
|
296
|
+
|
|
297
|
+
todos: reactive[list[dict[str, str]]] = reactive([], recompose=True)
|
|
298
|
+
|
|
299
|
+
def __init__(
|
|
300
|
+
self, todos: list[dict[str, str]], output: str = "", status: MessageStatus = MessageStatus.EXECUTING, **kwargs
|
|
301
|
+
) -> None:
|
|
302
|
+
"""
|
|
303
|
+
Construct a TodoWriteToolMessage.
|
|
304
|
+
|
|
305
|
+
Args:
|
|
306
|
+
todos: The list of todos being written.
|
|
307
|
+
output: The output from the tool (optional, can be set later).
|
|
308
|
+
status: The status of execution.
|
|
309
|
+
**kwargs: Additional keyword arguments for Widget.
|
|
310
|
+
"""
|
|
311
|
+
super().__init__(status=status, **kwargs)
|
|
312
|
+
self.todos = todos
|
|
313
|
+
self.output = output
|
|
314
|
+
|
|
315
|
+
def compose(self) -> ComposeResult:
|
|
316
|
+
"""Create child widgets for the todo write message."""
|
|
317
|
+
# Header line
|
|
318
|
+
yield MessageHeader("⏺", "TodoWrite", status=self.status)
|
|
319
|
+
|
|
320
|
+
# Todo list display
|
|
321
|
+
if self.todos:
|
|
322
|
+
with Horizontal(classes="todo-list"):
|
|
323
|
+
yield Static("└─", classes="todo-list-prefix")
|
|
324
|
+
with Vertical(classes="todo-list-content"):
|
|
325
|
+
# Display all todos in a single list
|
|
326
|
+
for todo in self.todos:
|
|
327
|
+
status = todo.get("status", "pending")
|
|
328
|
+
icon = "☒" if status == "completed" else "☐"
|
|
329
|
+
yield Static(f"{icon} {todo.get('content', '')}", classes=f"todo-item {status}")
|
|
330
|
+
|
|
331
|
+
|
|
332
|
+
class WriteToolMessage(BaseToolMessage):
|
|
333
|
+
"""A widget to display file write operations with markdown content viewer."""
|
|
334
|
+
|
|
335
|
+
file_path: reactive[str] = reactive("")
|
|
336
|
+
content: reactive[str] = reactive("", recompose=True)
|
|
337
|
+
|
|
338
|
+
def __init__(
|
|
339
|
+
self, file_path: str, content: str, output: str = "", status: MessageStatus = MessageStatus.EXECUTING, **kwargs
|
|
340
|
+
) -> None:
|
|
341
|
+
"""
|
|
342
|
+
Construct a WriteToolMessage.
|
|
343
|
+
|
|
344
|
+
Args:
|
|
345
|
+
file_path: The file path being written to.
|
|
346
|
+
content: The content being written.
|
|
347
|
+
output: The output from the write operation (can be set later).
|
|
348
|
+
status: The status of execution.
|
|
349
|
+
**kwargs: Additional keyword arguments for Widget.
|
|
350
|
+
"""
|
|
351
|
+
super().__init__(status=status, **kwargs)
|
|
352
|
+
self.file_path = file_path
|
|
353
|
+
self.content = content
|
|
354
|
+
self.output = output
|
|
355
|
+
|
|
356
|
+
def compose(self) -> ComposeResult:
|
|
357
|
+
"""Create child widgets for the write message."""
|
|
358
|
+
# Truncate file path if too long
|
|
359
|
+
max_path_length = 60
|
|
360
|
+
display_path = (
|
|
361
|
+
self.file_path[:max_path_length] + "…" if len(self.file_path) > max_path_length else self.file_path
|
|
362
|
+
)
|
|
363
|
+
|
|
364
|
+
# Header line
|
|
365
|
+
header = f"Write({display_path})"
|
|
366
|
+
yield MessageHeader("⏺", header, status=self.status)
|
|
367
|
+
|
|
368
|
+
# Content display with markdown support
|
|
369
|
+
if self.content:
|
|
370
|
+
with Horizontal(classes="write-content"):
|
|
371
|
+
yield Static("└─", classes="write-content-prefix")
|
|
372
|
+
with Vertical(classes="write-content-body"):
|
|
373
|
+
yield ExpandableContent(
|
|
374
|
+
Content(self.content), truncated_lines=10, classes="write-content-expandable"
|
|
375
|
+
)
|
|
376
|
+
|
|
377
|
+
# Output (success/error message)
|
|
378
|
+
if self.output:
|
|
379
|
+
with Horizontal(classes="tool-output"):
|
|
380
|
+
yield Static("└─", classes="tool-output-prefix")
|
|
381
|
+
with Vertical(classes="tool-output-content"):
|
|
382
|
+
yield Static(self.output, classes="write-output-message")
|
|
383
|
+
|
|
384
|
+
|
|
385
|
+
class MCPToolMessage(BaseToolMessage):
|
|
386
|
+
"""A widget to display MCP tool execution messages."""
|
|
387
|
+
|
|
388
|
+
server_name: reactive[str] = reactive("")
|
|
389
|
+
tool_name: reactive[str] = reactive("")
|
|
390
|
+
arguments: reactive[str] = reactive("")
|
|
391
|
+
|
|
392
|
+
def __init__(
|
|
393
|
+
self,
|
|
394
|
+
server_name: str,
|
|
395
|
+
tool_name: str,
|
|
396
|
+
arguments: str,
|
|
397
|
+
output: str = "",
|
|
398
|
+
status: MessageStatus = MessageStatus.EXECUTING,
|
|
399
|
+
**kwargs,
|
|
400
|
+
) -> None:
|
|
401
|
+
"""
|
|
402
|
+
Construct an MCPToolMessage.
|
|
403
|
+
|
|
404
|
+
Args:
|
|
405
|
+
server_name: The name of the MCP server.
|
|
406
|
+
tool_name: The name of the tool being called.
|
|
407
|
+
arguments: JSON string of tool arguments.
|
|
408
|
+
output: The output from the tool (optional, can be set later).
|
|
409
|
+
status: The status of execution.
|
|
410
|
+
**kwargs: Additional keyword arguments for Widget.
|
|
411
|
+
"""
|
|
412
|
+
super().__init__(status=status, **kwargs)
|
|
413
|
+
self.server_name = server_name
|
|
414
|
+
self.tool_name = tool_name
|
|
415
|
+
self.arguments = arguments
|
|
416
|
+
self.output = output
|
|
417
|
+
|
|
418
|
+
def _prettify_json_output(self, output: str) -> tuple[bool, str]:
|
|
419
|
+
"""Try to prettify JSON output.
|
|
420
|
+
|
|
421
|
+
Args:
|
|
422
|
+
output: The raw output string.
|
|
423
|
+
|
|
424
|
+
Returns:
|
|
425
|
+
A tuple of (is_json, formatted_output).
|
|
426
|
+
"""
|
|
427
|
+
if not output or not output.strip():
|
|
428
|
+
return False, output
|
|
429
|
+
|
|
430
|
+
try:
|
|
431
|
+
# Try to parse as JSON
|
|
432
|
+
json_obj = json.loads(output)
|
|
433
|
+
# Pretty print with 2-space indentation
|
|
434
|
+
formatted = json.dumps(json_obj, indent=2, ensure_ascii=False)
|
|
435
|
+
return True, formatted
|
|
436
|
+
except (json.JSONDecodeError, TypeError, ValueError):
|
|
437
|
+
# Not valid JSON, return as-is
|
|
438
|
+
return False, output
|
|
439
|
+
|
|
440
|
+
def compose(self) -> ComposeResult:
|
|
441
|
+
"""Create child widgets for the MCP tool message."""
|
|
442
|
+
# Header line showing MCP server and tool
|
|
443
|
+
# Access the actual values, not the reactive descriptors
|
|
444
|
+
server_name = self.server_name
|
|
445
|
+
tool_name = self.tool_name
|
|
446
|
+
header = f"MCP[{server_name}]::{tool_name}"
|
|
447
|
+
yield MessageHeader("⏺", header, status=self.status)
|
|
448
|
+
|
|
449
|
+
# Arguments display (if any)
|
|
450
|
+
if self.arguments and self.arguments != "{}":
|
|
451
|
+
with Horizontal(classes="mcp-arguments"):
|
|
452
|
+
yield Static("└─", classes="mcp-arguments-prefix")
|
|
453
|
+
with Vertical(classes="mcp-arguments-content"):
|
|
454
|
+
# Truncate arguments if too long
|
|
455
|
+
max_args_length = 100
|
|
456
|
+
display_args = (
|
|
457
|
+
self.arguments[:max_args_length] + "…"
|
|
458
|
+
if len(self.arguments) > max_args_length
|
|
459
|
+
else self.arguments
|
|
460
|
+
)
|
|
461
|
+
yield Static(f"Args: {display_args}", classes="mcp-arguments-text")
|
|
462
|
+
|
|
463
|
+
# Output - check if it's JSON and prettify if so
|
|
464
|
+
if self.output:
|
|
465
|
+
if json_output := json.loads(self.output):
|
|
466
|
+
assert json_output.get("type") == "text", "Expected JSON output type to be 'text'"
|
|
467
|
+
is_json, processed_output = self._prettify_json_output(json_output.get("text", ""))
|
|
468
|
+
else:
|
|
469
|
+
# output should always be a JSON string, but if not, treat it as plain text
|
|
470
|
+
is_json, processed_output = False, self.output
|
|
471
|
+
with Horizontal(classes="tool-output"):
|
|
472
|
+
yield Static("└─", classes="tool-output-prefix")
|
|
473
|
+
with Vertical(classes="tool-output-content"):
|
|
474
|
+
if is_json:
|
|
475
|
+
# Use ExpandableMarkdown for JSON with syntax highlighting
|
|
476
|
+
yield ExpandableMarkdown(
|
|
477
|
+
processed_output, language="json", truncated_lines=8, classes="mcp-output-json"
|
|
478
|
+
)
|
|
479
|
+
else:
|
|
480
|
+
# Use ExpandableMarkdown for non-JSON content (renders as markdown without code block)
|
|
481
|
+
yield ExpandableMarkdown(
|
|
482
|
+
processed_output, language="", truncated_lines=5, classes="mcp-output-markdown"
|
|
483
|
+
)
|