vibecore 0.2.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.

Potentially problematic release.


This version of vibecore might be problematic. Click here for more details.

Files changed (63) hide show
  1. vibecore/__init__.py +0 -0
  2. vibecore/agents/default.py +79 -0
  3. vibecore/agents/prompts.py +12 -0
  4. vibecore/agents/task_agent.py +66 -0
  5. vibecore/cli.py +150 -0
  6. vibecore/context.py +24 -0
  7. vibecore/handlers/__init__.py +5 -0
  8. vibecore/handlers/stream_handler.py +231 -0
  9. vibecore/main.py +506 -0
  10. vibecore/main.tcss +0 -0
  11. vibecore/mcp/__init__.py +6 -0
  12. vibecore/mcp/manager.py +167 -0
  13. vibecore/mcp/server_wrapper.py +109 -0
  14. vibecore/models/__init__.py +5 -0
  15. vibecore/models/anthropic.py +239 -0
  16. vibecore/prompts/common_system_prompt.txt +64 -0
  17. vibecore/py.typed +0 -0
  18. vibecore/session/__init__.py +5 -0
  19. vibecore/session/file_lock.py +127 -0
  20. vibecore/session/jsonl_session.py +236 -0
  21. vibecore/session/loader.py +193 -0
  22. vibecore/session/path_utils.py +81 -0
  23. vibecore/settings.py +161 -0
  24. vibecore/tools/__init__.py +1 -0
  25. vibecore/tools/base.py +27 -0
  26. vibecore/tools/file/__init__.py +5 -0
  27. vibecore/tools/file/executor.py +282 -0
  28. vibecore/tools/file/tools.py +184 -0
  29. vibecore/tools/file/utils.py +78 -0
  30. vibecore/tools/python/__init__.py +1 -0
  31. vibecore/tools/python/backends/__init__.py +1 -0
  32. vibecore/tools/python/backends/terminal_backend.py +58 -0
  33. vibecore/tools/python/helpers.py +80 -0
  34. vibecore/tools/python/manager.py +208 -0
  35. vibecore/tools/python/tools.py +27 -0
  36. vibecore/tools/shell/__init__.py +5 -0
  37. vibecore/tools/shell/executor.py +223 -0
  38. vibecore/tools/shell/tools.py +156 -0
  39. vibecore/tools/task/__init__.py +5 -0
  40. vibecore/tools/task/executor.py +51 -0
  41. vibecore/tools/task/tools.py +51 -0
  42. vibecore/tools/todo/__init__.py +1 -0
  43. vibecore/tools/todo/manager.py +31 -0
  44. vibecore/tools/todo/models.py +36 -0
  45. vibecore/tools/todo/tools.py +111 -0
  46. vibecore/utils/__init__.py +5 -0
  47. vibecore/utils/text.py +28 -0
  48. vibecore/widgets/core.py +332 -0
  49. vibecore/widgets/core.tcss +63 -0
  50. vibecore/widgets/expandable.py +121 -0
  51. vibecore/widgets/expandable.tcss +69 -0
  52. vibecore/widgets/info.py +25 -0
  53. vibecore/widgets/info.tcss +17 -0
  54. vibecore/widgets/messages.py +232 -0
  55. vibecore/widgets/messages.tcss +85 -0
  56. vibecore/widgets/tool_message_factory.py +121 -0
  57. vibecore/widgets/tool_messages.py +483 -0
  58. vibecore/widgets/tool_messages.tcss +289 -0
  59. vibecore-0.2.0.dist-info/METADATA +407 -0
  60. vibecore-0.2.0.dist-info/RECORD +63 -0
  61. vibecore-0.2.0.dist-info/WHEEL +4 -0
  62. vibecore-0.2.0.dist-info/entry_points.txt +2 -0
  63. vibecore-0.2.0.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
+ )