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.

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 +131 -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.0a1.dist-info/METADATA +407 -0
  60. vibecore-0.2.0a1.dist-info/RECORD +63 -0
  61. vibecore-0.2.0a1.dist-info/WHEEL +4 -0
  62. vibecore-0.2.0a1.dist-info/entry_points.txt +2 -0
  63. vibecore-0.2.0a1.dist-info/licenses/LICENSE +21 -0
@@ -0,0 +1,332 @@
1
+ import os
2
+ import time
3
+ from typing import ClassVar
4
+
5
+ from textual import events
6
+ from textual.app import ComposeResult
7
+ from textual.containers import Horizontal, ScrollableContainer, Vertical
8
+ from textual.geometry import Size
9
+ from textual.message import Message
10
+ from textual.widget import Widget
11
+ from textual.widgets import Footer, ProgressBar, Static, TextArea
12
+
13
+
14
+ class InputBox(Widget):
15
+ """A simple input box widget."""
16
+
17
+ def compose(self) -> ComposeResult:
18
+ """Create child widgets for the input box."""
19
+ text_area = MyTextArea(compact=True, id="input-textarea")
20
+ yield Static(">", id="input-label")
21
+ yield text_area
22
+
23
+
24
+ class AppFooter(Widget):
25
+ def get_current_working_directory(self) -> str:
26
+ """Get the current working directory for display.
27
+
28
+ Returns:
29
+ The current working directory path, with home directory replaced by ~
30
+ """
31
+ cwd = os.getcwd()
32
+ if cwd.startswith(os.path.expanduser("~")):
33
+ cwd = cwd.replace(os.path.expanduser("~"), "~", 1)
34
+ return cwd
35
+
36
+ def compose(self) -> ComposeResult:
37
+ yield LoadingWidget(status="Generating…", id="loading-widget")
38
+ yield InputBox()
39
+ # Wrap ProgressBar in vertical container to dock it right
40
+ with Vertical(id="context-info"):
41
+ cwd = self.get_current_working_directory()
42
+ yield Static(f"{cwd}", id="context-cwd")
43
+ with Horizontal(id="context-progress-container"):
44
+ yield Static("Context: ", id="context-progress-label")
45
+ yield ProgressBar(total=100, id="context-progress", show_eta=False)
46
+ yield Footer()
47
+
48
+ def set_context_progress(self, percent: float) -> None:
49
+ bar = self.query_one("#context-progress", ProgressBar)
50
+ value = max(0, min(100, int(percent * 100)))
51
+ bar.update(total=100, progress=value)
52
+
53
+ def on_mount(self) -> None:
54
+ """Hide loading widget on mount."""
55
+ self.query_one("#loading-widget").display = False
56
+
57
+ def show_loading(self, status: str = "Generating…", metadata: str = "") -> None:
58
+ """Show the loading widget with given status and metadata."""
59
+ loading = self.query_one("#loading-widget", LoadingWidget)
60
+ loading._start_time = time.monotonic() # Reset the timer
61
+ loading.update_status(status)
62
+ if metadata:
63
+ loading.update_metadata(metadata)
64
+ loading.display = True
65
+
66
+ def hide_loading(self) -> None:
67
+ """Hide the loading widget."""
68
+ self.query_one("#loading-widget").display = False
69
+
70
+
71
+ class MyTextArea(TextArea):
72
+ class UserMessage(Message):
73
+ """A user message input."""
74
+
75
+ def __init__(self, text: str) -> None:
76
+ self.text = text
77
+ super().__init__()
78
+
79
+ def __repr__(self) -> str:
80
+ return f"UserMessage(text={self.text!r})"
81
+
82
+ def __init__(self, **kwargs) -> None:
83
+ """Initialize MyTextArea with history tracking."""
84
+ super().__init__(**kwargs)
85
+ self.history_index = -1 # -1 means we're typing a new message
86
+ self.draft_text = "" # Store the draft when navigating history
87
+
88
+ async def get_user_history(self) -> list[str]:
89
+ """Get the list of user messages from the session and current input_items."""
90
+ from vibecore.main import VibecoreApp
91
+
92
+ app = self.app
93
+ if isinstance(app, VibecoreApp):
94
+ history = []
95
+
96
+ # First, load history from the session
97
+ if app.session:
98
+ try:
99
+ # Get all items from the session
100
+ session_items = await app.session.get_items()
101
+ for item in session_items:
102
+ # Filter for user messages
103
+ if isinstance(item, dict):
104
+ # Check both "role" and "type" fields for compatibility
105
+ role = item.get("role") or item.get("type")
106
+ if role == "user":
107
+ content = item.get("content")
108
+ if isinstance(content, str):
109
+ history.append(content)
110
+ except Exception as e:
111
+ # Log error but don't crash
112
+ from textual import log
113
+
114
+ log(f"Error loading session history: {e}")
115
+
116
+ # Then add current session's messages (in memory, not yet persisted)
117
+ for item in app.input_items:
118
+ if isinstance(item, dict) and item.get("role") == "user":
119
+ content = item.get("content")
120
+ if isinstance(content, str) and content not in history:
121
+ history.append(content)
122
+
123
+ return history
124
+ return []
125
+
126
+ def check_action(self, action: str, parameters: tuple[object, ...]) -> bool | None:
127
+ """Check if an action may run."""
128
+ # If text is empty, allow app to handle ctrl+d, not as delete_right
129
+ return not (action == "delete_right" and not self.text)
130
+
131
+ async def _on_key(self, event: events.Key) -> None:
132
+ if event.key == "enter":
133
+ self.post_message(self.UserMessage(self.text))
134
+ self.text = ""
135
+ self.history_index = -1 # Reset history navigation
136
+ self.draft_text = ""
137
+ event.prevent_default()
138
+ return
139
+
140
+ # Handle up arrow for history navigation
141
+ if event.key == "up":
142
+ if not self.cursor_at_start_of_text:
143
+ # Move cursor to start of text first
144
+ await super()._on_key(event)
145
+ return
146
+ else:
147
+ # Navigate to previous history item
148
+ history = await self.get_user_history()
149
+ if history:
150
+ # Save current draft if starting history navigation
151
+ if self.history_index == -1:
152
+ self.draft_text = self.text
153
+
154
+ # Move to previous item
155
+ if self.history_index < len(history) - 1:
156
+ self.history_index += 1
157
+ self.text = history[-(self.history_index + 1)]
158
+ self.move_cursor((0, 0)) # Move cursor to start
159
+ event.prevent_default()
160
+ return
161
+
162
+ # Handle down arrow for history navigation
163
+ elif event.key == "down":
164
+ if not self.cursor_at_end_of_text:
165
+ # Move cursor to end of text first
166
+ await super()._on_key(event)
167
+ return
168
+ else:
169
+ # Navigate to next history item
170
+ if self.history_index >= 0:
171
+ self.history_index -= 1
172
+ if self.history_index == -1:
173
+ # Return to draft
174
+ self.text = self.draft_text
175
+ else:
176
+ history = await self.get_user_history()
177
+ self.text = history[-(self.history_index + 1)]
178
+ # Move cursor to end of text
179
+ last_line = self.document.line_count - 1
180
+ last_column = len(self.document[last_line]) if last_line >= 0 else 0
181
+ self.move_cursor((last_line, last_column))
182
+ event.prevent_default()
183
+ return
184
+
185
+ self._restart_blink()
186
+
187
+ if self.read_only:
188
+ return
189
+
190
+ key = event.key
191
+ insert_values = {
192
+ "shift+enter": "\n",
193
+ # Ghostty with config: keybind = shift+enter=text:\n
194
+ "ctrl+j": "\n",
195
+ }
196
+ if self.tab_behavior == "indent":
197
+ if key == "escape":
198
+ event.stop()
199
+ event.prevent_default()
200
+ self.screen.focus_next()
201
+ return
202
+ if self.indent_type == "tabs":
203
+ insert_values["tab"] = "\t"
204
+ else:
205
+ insert_values["tab"] = " " * self._find_columns_to_next_tab_stop()
206
+
207
+ if event.is_printable or key in insert_values:
208
+ event.stop()
209
+ event.prevent_default()
210
+ insert = insert_values.get(key, event.character)
211
+ # `insert` is not None because event.character cannot be
212
+ # None because we've checked that it's printable.
213
+ assert insert is not None
214
+ start, end = self.selection
215
+ self._replace_via_keyboard(insert, start, end)
216
+
217
+
218
+ class MainScroll(ScrollableContainer):
219
+ """A container with vertical layout and an automatic scrollbar on the Y axis."""
220
+
221
+ def watch_virtual_size(self, size: Size) -> None:
222
+ """Scroll to the bottom when resized = when new content is added."""
223
+ # If the scroll is near the end, keep the scroll sticky to the end
224
+ epsilon = 30
225
+ in_the_end = (size.height - (self.scroll_target_y + self.scrollable_size.height)) < epsilon
226
+ if size.height > self.scrollable_size.height and in_the_end:
227
+ self.scroll_end(animate=False)
228
+
229
+
230
+ class LoadingWidget(Widget):
231
+ """A loading indicator with spinner, status text, and metadata."""
232
+
233
+ DEFAULT_CSS = """
234
+ LoadingWidget {
235
+ width: 1fr;
236
+ height: 1;
237
+ padding: 0 1;
238
+ }
239
+
240
+ LoadingWidget .loading-spinner {
241
+ color: $primary;
242
+ }
243
+
244
+ LoadingWidget .loading-status {
245
+ color: $text;
246
+ margin: 0 1;
247
+ }
248
+
249
+ LoadingWidget .loading-metadata {
250
+ color: $text-muted;
251
+ }
252
+ """
253
+
254
+ SPINNERS: ClassVar[list[str]] = ["⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"]
255
+
256
+ def __init__(
257
+ self,
258
+ status: str = "Loading…",
259
+ show_time: bool = True,
260
+ show_metadata: bool = True,
261
+ metadata: str = "",
262
+ escape_message: str = "esc to interrupt",
263
+ **kwargs,
264
+ ) -> None:
265
+ super().__init__(**kwargs)
266
+ self.status = status
267
+ self.show_time = show_time
268
+ self.show_metadata = show_metadata
269
+ self.metadata = metadata
270
+ self.escape_message = escape_message
271
+ self._spinner_index = 0
272
+ self._start_time = time.monotonic()
273
+ self._spinner_timer = None
274
+
275
+ def compose(self) -> ComposeResult:
276
+ yield Static("", id="loading-content")
277
+
278
+ def on_mount(self) -> None:
279
+ """Start the spinner animation when mounted."""
280
+ self._spinner_timer = self.set_interval(0.1, self._update_spinner)
281
+ self._update_display()
282
+
283
+ def on_unmount(self) -> None:
284
+ """Stop the spinner animation when unmounted."""
285
+ if self._spinner_timer:
286
+ self._spinner_timer.stop()
287
+
288
+ def _update_spinner(self) -> None:
289
+ """Update the spinner character and elapsed time."""
290
+ self._spinner_index = (self._spinner_index + 1) % len(self.SPINNERS)
291
+ self._update_display()
292
+
293
+ def _update_display(self) -> None:
294
+ """Update the entire loading display."""
295
+ parts = []
296
+
297
+ # Spinner
298
+ spinner = self.SPINNERS[self._spinner_index]
299
+ parts.append(f"[bold]{spinner}[/bold]")
300
+
301
+ # Status text
302
+ parts.append(self.status)
303
+
304
+ # Metadata section
305
+ metadata_parts = []
306
+
307
+ if self.show_time:
308
+ elapsed = int(time.monotonic() - self._start_time)
309
+ metadata_parts.append(f"{elapsed}s")
310
+
311
+ if self.show_metadata and self.metadata:
312
+ metadata_parts.append(self.metadata)
313
+
314
+ if self.escape_message:
315
+ metadata_parts.append(self.escape_message)
316
+
317
+ if metadata_parts:
318
+ metadata_str = " · ".join(metadata_parts)
319
+ parts.append(f"[dim]({metadata_str})[/dim]")
320
+
321
+ content = " ".join(parts)
322
+ self.query_one("#loading-content", Static).update(content)
323
+
324
+ def update_status(self, status: str) -> None:
325
+ """Update the status text."""
326
+ self.status = status
327
+ self._update_display()
328
+
329
+ def update_metadata(self, metadata: str) -> None:
330
+ """Update the metadata text."""
331
+ self.metadata = metadata
332
+ self._update_display()
@@ -0,0 +1,63 @@
1
+ AppFooter {
2
+ dock: bottom;
3
+ height: auto;
4
+ }
5
+
6
+ Footer {
7
+ dock: none;
8
+ }
9
+
10
+ #context-info {
11
+ height: 2;
12
+ color: $text-primary;
13
+
14
+ #context-cwd {
15
+ margin-left: 1;
16
+ height: 1;
17
+ width: auto;
18
+ dock: left;
19
+ }
20
+
21
+ #context-progress-container {
22
+ margin-right: 1;
23
+ height: auto;
24
+ width: auto;
25
+ dock: right;
26
+ layout: horizontal;
27
+
28
+ #context-progress-label {
29
+ width: auto;
30
+ }
31
+ }
32
+ }
33
+
34
+ InputBox {
35
+ height: auto;
36
+ layout: horizontal;
37
+
38
+ #input-label {
39
+ width: 3;
40
+ height: 1;
41
+ text-align: center;
42
+ }
43
+
44
+ TextArea {
45
+ width: 1fr;
46
+ height: auto;
47
+ max-height: 10;
48
+ min-height: 1;
49
+ border: none;
50
+ padding: 0 0;
51
+ background: $background;
52
+ & .text-area--cursor-line {
53
+ background: $background;
54
+ }
55
+ }
56
+
57
+ border: round $border-blurred;
58
+ &:focus-within {
59
+ border: round $border;
60
+ }
61
+
62
+ # background: $surface;
63
+ }
@@ -0,0 +1,121 @@
1
+ """Expandable content widgets for Textual applications."""
2
+
3
+ from textual.app import ComposeResult
4
+ from textual.content import Content
5
+ from textual.events import Click
6
+ from textual.reactive import reactive
7
+ from textual.widget import Widget
8
+ from textual.widgets import Markdown, Static
9
+
10
+
11
+ class ExpandableContent(Widget):
12
+ """A widget that shows truncated content with an expandable button."""
13
+
14
+ expanded: reactive[bool] = reactive(False, recompose=True)
15
+
16
+ def __init__(
17
+ self, content: str | Content, truncated_lines: int = 3, collapsed_text: str | Content | None = None, **kwargs
18
+ ) -> None:
19
+ """
20
+ Initialize the ExpandableContent widget.
21
+
22
+ Args:
23
+ content: The full content to display (str or Content for safe rendering)
24
+ truncated_lines: Number of lines to show when collapsed (ignored if collapsed_text is provided)
25
+ collapsed_text: Custom text to show when collapsed (overrides truncated content)
26
+ **kwargs: Additional keyword arguments for Widget
27
+ """
28
+ super().__init__(**kwargs)
29
+ self.content = content
30
+ self.truncated_lines = truncated_lines
31
+ self.collapsed_text = collapsed_text
32
+ # Extract plain text for line counting
33
+ self.content_str = str(content) if isinstance(content, Content) else content
34
+ self.lines = self.content_str.splitlines()
35
+ self.total_lines = len(self.lines)
36
+
37
+ def compose(self) -> ComposeResult:
38
+ """Create child widgets based on expanded state."""
39
+ if self.expanded:
40
+ # Show all content
41
+ yield Static(self.content, classes="expandable-content-full")
42
+ yield Static("▲ collapse", classes="expandable-toggle expanded")
43
+ else:
44
+ # Show custom collapsed text if provided
45
+ if self.collapsed_text is not None:
46
+ yield Static(self.collapsed_text, classes="expandable-toggle collapsed")
47
+ # Show truncated content
48
+ elif self.total_lines > self.truncated_lines:
49
+ truncated_text = "\n".join(self.lines[: self.truncated_lines])
50
+ # Preserve Content safety if original was Content
51
+ truncated_content = Content(truncated_text) if isinstance(self.content, Content) else truncated_text
52
+ yield Static(truncated_content, classes="expandable-content-truncated")
53
+ remaining_lines = self.total_lines - self.truncated_lines
54
+ yield Static(f"… +{remaining_lines} more lines (view)", classes="expandable-toggle collapsed")
55
+ else:
56
+ # If content fits, just show it all
57
+ yield Static(self.content, classes="expandable-content-full")
58
+
59
+ def on_click(self, event: Click) -> None:
60
+ """Handle click events to toggle expansion."""
61
+ # Only toggle if we clicked on the toggle element
62
+ if event.widget and event.widget.has_class("expandable-toggle"):
63
+ self.expanded = not self.expanded
64
+ event.stop()
65
+
66
+
67
+ class ExpandableMarkdown(Widget):
68
+ """A widget that shows truncated Markdown content with an expandable button."""
69
+
70
+ expanded: reactive[bool] = reactive(False, recompose=True)
71
+
72
+ def __init__(self, code: str, language: str = "python", truncated_lines: int = 8, **kwargs) -> None:
73
+ """
74
+ Initialize the ExpandableMarkdown widget.
75
+
76
+ Args:
77
+ code: The full code to display
78
+ language: Programming language for syntax highlighting (empty string for plain markdown)
79
+ truncated_lines: Number of lines to show when collapsed
80
+ **kwargs: Additional keyword arguments for Widget
81
+ """
82
+ super().__init__(**kwargs)
83
+ self.code = code
84
+ self.language = language
85
+ self.truncated_lines = truncated_lines
86
+ self.lines = code.splitlines()
87
+ self.total_lines = len(self.lines)
88
+
89
+ def compose(self) -> ComposeResult:
90
+ """Create child widgets based on expanded state."""
91
+ # Determine content format based on language
92
+ if self.language:
93
+ # Render as code block with syntax highlighting
94
+ full_content = f"```{self.language}\n{self.code}\n```"
95
+ truncated_lines = "\n".join(self.lines[: self.truncated_lines])
96
+ truncated_content = f"```{self.language}\n{truncated_lines}\n```"
97
+ else:
98
+ # Render as plain markdown (no code block)
99
+ full_content = self.code
100
+ truncated_content = "\n".join(self.lines[: self.truncated_lines])
101
+
102
+ if self.expanded:
103
+ # Show all content
104
+ yield Markdown(full_content, classes="expandable-markdown-full")
105
+ yield Static("▲ collapse", classes="expandable-toggle expanded")
106
+ else:
107
+ # Show truncated content
108
+ if self.total_lines > self.truncated_lines:
109
+ yield Markdown(truncated_content, classes="expandable-markdown-truncated")
110
+ remaining_lines = self.total_lines - self.truncated_lines
111
+ yield Static(f"… +{remaining_lines} more lines (view)", classes="expandable-toggle collapsed")
112
+ else:
113
+ # If content fits, just show it all
114
+ yield Markdown(full_content, classes="expandable-markdown-full")
115
+
116
+ def on_click(self, event: Click) -> None:
117
+ """Handle click events to toggle expansion."""
118
+ # Only toggle if we clicked on the toggle element
119
+ if event.widget and event.widget.has_class("expandable-toggle"):
120
+ self.expanded = not self.expanded
121
+ event.stop()
@@ -0,0 +1,69 @@
1
+ /* Expandable Content Widget Styles */
2
+ ExpandableContent {
3
+ height: auto;
4
+
5
+ .expandable-content-truncated, .expandable-content-full {
6
+ padding: 0;
7
+ margin: 0;
8
+ height: auto;
9
+ }
10
+
11
+ .expandable-toggle {
12
+ padding: 0 0 0 0;
13
+ margin: 0 0 0 0;
14
+ width: auto;
15
+
16
+ &:hover {
17
+ color: $primary;
18
+ }
19
+
20
+ &.collapsed {
21
+ margin-top: 0;
22
+ }
23
+
24
+ &.expanded {
25
+ margin-top: 1;
26
+ }
27
+ }
28
+ }
29
+
30
+ /* Expandable Markdown Widget Styles */
31
+ ExpandableMarkdown {
32
+ height: auto;
33
+ width: 1fr;
34
+
35
+ Markdown {
36
+ background: $background;
37
+ padding: 0;
38
+
39
+ MarkdownFence {
40
+ margin: 0;
41
+ padding: 0;
42
+ max-height: 100%;
43
+
44
+ &> Static {
45
+ # Override MarkdownFence > Static (Syntax) padding/margin
46
+ width: 1fr; # Trigger word-wrap instead of overflow
47
+ padding: 0 2 0 0;
48
+ }
49
+ }
50
+ }
51
+
52
+ .expandable-toggle {
53
+ padding: 0 0 0 2;
54
+ margin: 0 0 0 0;
55
+ width: auto;
56
+
57
+ &:hover {
58
+ color: $primary;
59
+ }
60
+
61
+ &.collapsed {
62
+ margin-top: 0;
63
+ }
64
+
65
+ &.expanded {
66
+ margin-top: 1;
67
+ }
68
+ }
69
+ }
@@ -0,0 +1,25 @@
1
+ from textual.app import ComposeResult
2
+ from textual.widget import Widget
3
+ from textual.widgets import Static
4
+
5
+ VIBECORE_LOGO = """
6
+ ██╗ ██╗██╗██████╗ ███████╗ ██████╗ ██████╗ ██████╗ ███████╗
7
+ ██║ ██║██║██╔══██╗██╔════╝██╔════╝██╔═══██╗██╔══██╗██╔════╝
8
+ ██║ ██║██║██████╔╝█████╗ ██║ ██║ ██║██████╔╝█████╗
9
+ ╚██╗ ██╔╝██║██╔══██╗██╔══╝ ██║ ██║ ██║██╔══██╗██╔══╝
10
+ ╚████╔╝ ██║██████╔╝███████╗╚██████╗╚██████╔╝██║ ██║███████╗
11
+ ╚═══╝ ╚═╝╚═════╝ ╚══════╝ ╚═════╝ ╚═════╝ ╚═╝ ╚═╝╚══════╝
12
+ """.strip()
13
+
14
+
15
+ class Welcome(Widget):
16
+ """A simple input box widget."""
17
+
18
+ def compose(self) -> ComposeResult:
19
+ """Create child widgets for the input box."""
20
+ yield Static(f"[$primary]{VIBECORE_LOGO}[/]", classes="logo")
21
+ yield Static("Welcome to [$text-primary][b]Vibecore[/b][/]!", classes="title")
22
+ yield Static(
23
+ "Type '/help' to see available commands.",
24
+ classes="subtitle",
25
+ )
@@ -0,0 +1,17 @@
1
+
2
+ Welcome {
3
+ height: auto;
4
+ width: auto;
5
+ padding: 1 2;
6
+ margin-bottom: 1;
7
+
8
+ border: round $border;
9
+
10
+ .logo {
11
+ margin-bottom: 1;
12
+ }
13
+
14
+ Static {
15
+ width: auto;
16
+ }
17
+ }