Files
mistral-vibe/vibe/cli/textual_ui/widgets/messages.py
Quentin d8dbeeb31e v1.2.0
Co-Authored-By: Quentin Torroba <quentin.torroba@mistral.ai>
Co-Authored-By: Michel Thomazo <michel.thomazo@mistral.ai>
Co-Authored-By: Kracekumar <kracethekingmaker@gmail.com>
2025-12-18 17:49:14 +01:00

201 lines
6.5 KiB
Python

from __future__ import annotations
from typing import Any
from textual.app import ComposeResult
from textual.containers import Horizontal, Vertical
from textual.widgets import Markdown, Static
from textual.widgets._markdown import MarkdownStream
class NonSelectableStatic(Static):
@property
def text_selection(self) -> None:
return None
@text_selection.setter
def text_selection(self, value: Any) -> None:
pass
def get_selection(self, selection: Any) -> None:
return None
class ExpandingBorder(NonSelectableStatic):
def render(self) -> str:
height = self.size.height
return "\n".join([""] * (height - 1) + [""])
def on_resize(self) -> None:
self.refresh()
class UserMessage(Static):
def __init__(self, content: str, pending: bool = False) -> None:
super().__init__()
self.add_class("user-message")
self._content = content
self._pending = pending
def compose(self) -> ComposeResult:
with Horizontal(classes="user-message-container"):
yield NonSelectableStatic("> ", classes="user-message-prompt")
yield Static(self._content, markup=False, classes="user-message-content")
if self._pending:
self.add_class("pending")
async def set_pending(self, pending: bool) -> None:
if pending == self._pending:
return
self._pending = pending
if pending:
self.add_class("pending")
return
self.remove_class("pending")
class AssistantMessage(Static):
def __init__(self, content: str) -> None:
super().__init__()
self.add_class("assistant-message")
self._content = content
self._markdown: Markdown | None = None
self._stream: MarkdownStream | None = None
def compose(self) -> ComposeResult:
with Horizontal(classes="assistant-message-container"):
yield NonSelectableStatic("", classes="assistant-message-dot")
with Vertical(classes="assistant-message-content"):
markdown = Markdown("")
self._markdown = markdown
yield markdown
def _get_markdown(self) -> Markdown:
if self._markdown is None:
self._markdown = self.query_one(Markdown)
return self._markdown
def _ensure_stream(self) -> MarkdownStream:
if self._stream is None:
self._stream = Markdown.get_stream(self._get_markdown())
return self._stream
async def append_content(self, content: str) -> None:
if not content:
return
self._content += content
stream = self._ensure_stream()
await stream.write(content)
async def write_initial_content(self) -> None:
if self._content:
stream = self._ensure_stream()
await stream.write(self._content)
async def stop_stream(self) -> None:
if self._stream is None:
return
await self._stream.stop()
self._stream = None
class UserCommandMessage(Static):
def __init__(self, content: str) -> None:
super().__init__()
self.add_class("user-command-message")
self._content = content
def compose(self) -> ComposeResult:
with Horizontal(classes="user-command-container"):
yield ExpandingBorder(classes="user-command-border")
with Vertical(classes="user-command-content"):
yield Markdown(self._content)
class InterruptMessage(Static):
def __init__(self) -> None:
super().__init__()
self.add_class("interrupt-message")
def compose(self) -> ComposeResult:
with Horizontal(classes="interrupt-container"):
yield ExpandingBorder(classes="interrupt-border")
yield Static(
"Interrupted · What should Vibe do instead?",
markup=False,
classes="interrupt-content",
)
class BashOutputMessage(Static):
def __init__(self, command: str, cwd: str, output: str, exit_code: int) -> None:
super().__init__()
self.add_class("bash-output-message")
self._command = command
self._cwd = cwd
self._output = output
self._exit_code = exit_code
def compose(self) -> ComposeResult:
with Vertical(classes="bash-output-container"):
with Horizontal(classes="bash-cwd-line"):
yield Static(self._cwd, markup=False, classes="bash-cwd")
yield Static("", classes="bash-cwd-spacer")
if self._exit_code == 0:
yield Static("", classes="bash-exit-success")
else:
yield Static("", classes="bash-exit-failure")
yield Static(f" ({self._exit_code})", classes="bash-exit-code")
with Horizontal(classes="bash-command-line"):
yield Static("> ", classes="bash-chevron")
yield Static(self._command, markup=False, classes="bash-command")
yield Static("", classes="bash-command-spacer")
yield Static(self._output, markup=False, classes="bash-output")
class ErrorMessage(Static):
def __init__(self, error: str, collapsed: bool = True) -> None:
super().__init__()
self.add_class("error-message")
self._error = error
self.collapsed = collapsed
self._content_widget: Static | None = None
def compose(self) -> ComposeResult:
with Horizontal(classes="error-container"):
yield ExpandingBorder(classes="error-border")
self._content_widget = Static(
self._get_text(), markup=False, classes="error-content"
)
yield self._content_widget
def _get_text(self) -> str:
if self.collapsed:
return "Error. (ctrl+o to expand)"
return f"Error: {self._error}"
def set_collapsed(self, collapsed: bool) -> None:
if self.collapsed == collapsed:
return
self.collapsed = collapsed
if self._content_widget:
self._content_widget.update(self._get_text())
class WarningMessage(Static):
def __init__(self, message: str) -> None:
super().__init__()
self.add_class("warning-message")
self._message = message
def compose(self) -> ComposeResult:
with Horizontal(classes="warning-container"):
yield ExpandingBorder(classes="warning-border")
yield Static(self._message, markup=False, classes="warning-content")