v1.3.4
Co-Authored-By: Quentin Torroba <quentin.torroba@mistral.ai> Co-Authored-By: Vincent Guilloux <vincent.guilloux@mistral.ai> Co-Authored-By: Luis Cardoso <luis.cardoso@mistral.ai>
This commit is contained in:
parent
4d449be276
commit
add3ab5245
36 changed files with 399 additions and 259 deletions
|
|
@ -3,4 +3,4 @@ from __future__ import annotations
|
|||
from pathlib import Path
|
||||
|
||||
VIBE_ROOT = Path(__file__).parent
|
||||
__version__ = "1.3.3"
|
||||
__version__ = "1.3.4"
|
||||
|
|
|
|||
|
|
@ -8,7 +8,7 @@ from rich import print as rprint
|
|||
|
||||
from vibe import __version__
|
||||
from vibe.core.paths.config_paths import unlock_config_paths
|
||||
from vibe.core.trusted_folders import trusted_folders_manager
|
||||
from vibe.core.trusted_folders import has_trustable_content, trusted_folders_manager
|
||||
from vibe.setup.trusted_folders.trust_folder_dialog import (
|
||||
TrustDialogQuitException,
|
||||
ask_trust_folder,
|
||||
|
|
@ -105,7 +105,7 @@ def parse_arguments() -> argparse.Namespace:
|
|||
|
||||
def check_and_resolve_trusted_folder() -> None:
|
||||
cwd = Path.cwd()
|
||||
if not (cwd / ".vibe").exists() or cwd.resolve() == Path.home().resolve():
|
||||
if not has_trustable_content(cwd) or cwd.resolve() == Path.home().resolve():
|
||||
return
|
||||
|
||||
is_folder_trusted = trusted_folders_manager.is_trusted(cwd)
|
||||
|
|
|
|||
|
|
@ -41,6 +41,7 @@ from vibe.cli.textual_ui.widgets.messages import (
|
|||
WarningMessage,
|
||||
)
|
||||
from vibe.cli.textual_ui.widgets.mode_indicator import ModeIndicator
|
||||
from vibe.cli.textual_ui.widgets.no_markup_static import NoMarkupStatic
|
||||
from vibe.cli.textual_ui.widgets.path_display import PathDisplay
|
||||
from vibe.cli.textual_ui.widgets.tools import ToolCallMessage, ToolResultMessage
|
||||
from vibe.cli.textual_ui.widgets.welcome import WelcomeBanner
|
||||
|
|
@ -174,7 +175,7 @@ class VibeApp(App): # noqa: PLR0904
|
|||
yield PathDisplay(
|
||||
self.config.displayed_workdir or self.config.effective_workdir
|
||||
)
|
||||
yield Static(id="spacer")
|
||||
yield NoMarkupStatic(id="spacer")
|
||||
yield ContextProgress()
|
||||
|
||||
async def on_mount(self) -> None:
|
||||
|
|
@ -320,6 +321,24 @@ class VibeApp(App): # noqa: PLR0904
|
|||
|
||||
await self._switch_to_input_app()
|
||||
|
||||
async def on_compact_message_completed(
|
||||
self, message: CompactMessage.Completed
|
||||
) -> None:
|
||||
messages_area = self.query_one("#messages")
|
||||
children = list(messages_area.children)
|
||||
|
||||
try:
|
||||
compact_index = children.index(message.compact_widget)
|
||||
except ValueError:
|
||||
return
|
||||
|
||||
if compact_index == 0:
|
||||
return
|
||||
|
||||
with self.batch_update():
|
||||
for widget in children[:compact_index]:
|
||||
await widget.remove()
|
||||
|
||||
def _set_tool_permission_always(
|
||||
self, tool_name: str, save_permanently: bool = False
|
||||
) -> None:
|
||||
|
|
@ -502,31 +521,32 @@ class VibeApp(App): # noqa: PLR0904
|
|||
messages_area = self.query_one("#messages")
|
||||
tool_call_map: dict[str, str] = {}
|
||||
|
||||
for msg in self._loaded_messages:
|
||||
if msg.role == Role.system:
|
||||
continue
|
||||
with self.batch_update():
|
||||
for msg in self._loaded_messages:
|
||||
if msg.role == Role.system:
|
||||
continue
|
||||
|
||||
match msg.role:
|
||||
case Role.user:
|
||||
if msg.content:
|
||||
await messages_area.mount(UserMessage(msg.content))
|
||||
match msg.role:
|
||||
case Role.user:
|
||||
if msg.content:
|
||||
await messages_area.mount(UserMessage(msg.content))
|
||||
|
||||
case Role.assistant:
|
||||
await self._mount_history_assistant_message(
|
||||
msg, messages_area, tool_call_map
|
||||
)
|
||||
|
||||
case Role.tool:
|
||||
tool_name = msg.name or tool_call_map.get(
|
||||
msg.tool_call_id or "", "tool"
|
||||
)
|
||||
await messages_area.mount(
|
||||
ToolResultMessage(
|
||||
tool_name=tool_name,
|
||||
content=msg.content,
|
||||
collapsed=self._tools_collapsed,
|
||||
case Role.assistant:
|
||||
await self._mount_history_assistant_message(
|
||||
msg, messages_area, tool_call_map
|
||||
)
|
||||
|
||||
case Role.tool:
|
||||
tool_name = msg.name or tool_call_map.get(
|
||||
msg.tool_call_id or "", "tool"
|
||||
)
|
||||
await messages_area.mount(
|
||||
ToolResultMessage(
|
||||
tool_name=tool_name,
|
||||
content=msg.content,
|
||||
collapsed=self._tools_collapsed,
|
||||
)
|
||||
)
|
||||
)
|
||||
|
||||
async def _mount_history_assistant_message(
|
||||
self, msg: LLMMessage, messages_area: Widget, tool_call_map: dict[str, str]
|
||||
|
|
|
|||
|
|
@ -3,10 +3,9 @@ from __future__ import annotations
|
|||
from collections.abc import Callable
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
from textual.widgets import Static
|
||||
|
||||
from vibe.cli.textual_ui.widgets.compact import CompactMessage
|
||||
from vibe.cli.textual_ui.widgets.messages import AssistantMessage, ReasoningMessage
|
||||
from vibe.cli.textual_ui.widgets.no_markup_static import NoMarkupStatic
|
||||
from vibe.cli.textual_ui.widgets.tools import ToolCallMessage, ToolResultMessage
|
||||
from vibe.core.types import (
|
||||
AssistantEvent,
|
||||
|
|
@ -142,9 +141,7 @@ class EventHandler:
|
|||
self.current_compact = None
|
||||
|
||||
async def _handle_unknown_event(self, event: BaseEvent) -> None:
|
||||
await self.mount_callback(
|
||||
Static(str(event), markup=False, classes="unknown-event")
|
||||
)
|
||||
await self.mount_callback(NoMarkupStatic(str(event), classes="unknown-event"))
|
||||
|
||||
def stop_current_tool_call(self) -> None:
|
||||
if self.current_tool_call:
|
||||
|
|
|
|||
|
|
@ -10,6 +10,7 @@ from textual.containers import Container, Vertical, VerticalScroll
|
|||
from textual.message import Message
|
||||
from textual.widgets import Static
|
||||
|
||||
from vibe.cli.textual_ui.widgets.no_markup_static import NoMarkupStatic
|
||||
from vibe.cli.textual_ui.widgets.tool_widgets import get_approval_widget
|
||||
from vibe.core.config import VibeConfig
|
||||
|
||||
|
|
@ -67,7 +68,7 @@ class ApprovalApp(Container):
|
|||
|
||||
def compose(self) -> ComposeResult:
|
||||
with Vertical(id="approval-content"):
|
||||
self.title_widget = Static(
|
||||
self.title_widget = NoMarkupStatic(
|
||||
f"⚠ {self.tool_name} command", classes="approval-title"
|
||||
)
|
||||
yield self.title_widget
|
||||
|
|
@ -78,16 +79,16 @@ class ApprovalApp(Container):
|
|||
)
|
||||
yield self.tool_info_container
|
||||
|
||||
yield Static("")
|
||||
yield NoMarkupStatic("")
|
||||
|
||||
for _ in range(3):
|
||||
widget = Static("", classes="approval-option")
|
||||
widget = NoMarkupStatic("", classes="approval-option")
|
||||
self.option_widgets.append(widget)
|
||||
yield widget
|
||||
|
||||
yield Static("")
|
||||
yield NoMarkupStatic("")
|
||||
|
||||
self.help_widget = Static(
|
||||
self.help_widget = NoMarkupStatic(
|
||||
"↑↓ navigate Enter select ESC reject", classes="approval-help"
|
||||
)
|
||||
yield self.help_widget
|
||||
|
|
|
|||
|
|
@ -8,10 +8,10 @@ from textual.app import ComposeResult
|
|||
from textual.containers import Horizontal
|
||||
from textual.message import Message
|
||||
from textual.widget import Widget
|
||||
from textual.widgets import Static
|
||||
|
||||
from vibe.cli.history_manager import HistoryManager
|
||||
from vibe.cli.textual_ui.widgets.chat_input.text_area import ChatTextArea, InputMode
|
||||
from vibe.cli.textual_ui.widgets.no_markup_static import NoMarkupStatic
|
||||
|
||||
|
||||
class ChatInputBody(Widget):
|
||||
|
|
@ -23,7 +23,7 @@ class ChatInputBody(Widget):
|
|||
def __init__(self, history_file: Path | None = None, **kwargs: Any) -> None:
|
||||
super().__init__(**kwargs)
|
||||
self.input_widget: ChatTextArea | None = None
|
||||
self.prompt_widget: Static | None = None
|
||||
self.prompt_widget: NoMarkupStatic | None = None
|
||||
|
||||
if history_file:
|
||||
self.history = HistoryManager(history_file)
|
||||
|
|
@ -34,7 +34,7 @@ class ChatInputBody(Widget):
|
|||
|
||||
def compose(self) -> ComposeResult:
|
||||
with Horizontal():
|
||||
self.prompt_widget = Static(">", id="prompt")
|
||||
self.prompt_widget = NoMarkupStatic(">", id="prompt")
|
||||
yield self.prompt_widget
|
||||
|
||||
self.input_widget = ChatTextArea(placeholder="Ask anything...", id="input")
|
||||
|
|
|
|||
|
|
@ -1,9 +1,16 @@
|
|||
from __future__ import annotations
|
||||
|
||||
from textual.message import Message
|
||||
|
||||
from vibe.cli.textual_ui.widgets.status_message import StatusMessage
|
||||
|
||||
|
||||
class CompactMessage(StatusMessage):
|
||||
class Completed(Message):
|
||||
def __init__(self, compact_widget: CompactMessage) -> None:
|
||||
super().__init__()
|
||||
self.compact_widget = compact_widget
|
||||
|
||||
def __init__(self) -> None:
|
||||
super().__init__()
|
||||
self.add_class("compact-message")
|
||||
|
|
@ -36,6 +43,7 @@ class CompactMessage(StatusMessage):
|
|||
self.old_tokens = old_tokens
|
||||
self.new_tokens = new_tokens
|
||||
self.stop_spinning(success=True)
|
||||
self.post_message(self.Completed(self))
|
||||
|
||||
def set_error(self, error_message: str) -> None:
|
||||
self.error_message = error_message
|
||||
|
|
|
|||
|
|
@ -11,6 +11,7 @@ from textual.theme import BUILTIN_THEMES
|
|||
from textual.widgets import Static
|
||||
|
||||
from vibe.cli.textual_ui.terminal_theme import TERMINAL_THEME_NAME
|
||||
from vibe.cli.textual_ui.widgets.no_markup_static import NoMarkupStatic
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from vibe.core.config import VibeConfig
|
||||
|
|
@ -85,19 +86,19 @@ class ConfigApp(Container):
|
|||
|
||||
def compose(self) -> ComposeResult:
|
||||
with Vertical(id="config-content"):
|
||||
self.title_widget = Static("Settings", classes="settings-title")
|
||||
self.title_widget = NoMarkupStatic("Settings", classes="settings-title")
|
||||
yield self.title_widget
|
||||
|
||||
yield Static("")
|
||||
yield NoMarkupStatic("")
|
||||
|
||||
for _ in self.settings:
|
||||
widget = Static("", classes="settings-option")
|
||||
widget = NoMarkupStatic("", classes="settings-option")
|
||||
self.setting_widgets.append(widget)
|
||||
yield widget
|
||||
|
||||
yield Static("")
|
||||
yield NoMarkupStatic("")
|
||||
|
||||
self.help_widget = Static(
|
||||
self.help_widget = NoMarkupStatic(
|
||||
"↑↓ navigate Space/Enter toggle ESC exit", classes="settings-help"
|
||||
)
|
||||
yield self.help_widget
|
||||
|
|
@ -143,12 +144,13 @@ class ConfigApp(Container):
|
|||
current: str = self.changes.get(key, setting["value"])
|
||||
|
||||
options: list[str] = setting["options"]
|
||||
new_value = ""
|
||||
try:
|
||||
current_idx = options.index(current)
|
||||
next_idx = (current_idx + 1) % len(options)
|
||||
new_value: str = options[next_idx]
|
||||
new_value = options[next_idx]
|
||||
except (ValueError, IndexError):
|
||||
new_value: str = options[0] if options else current
|
||||
new_value = options[0] if options else current
|
||||
|
||||
self.changes[key] = new_value
|
||||
|
||||
|
|
|
|||
|
|
@ -4,7 +4,8 @@ from dataclasses import dataclass
|
|||
from typing import Any
|
||||
|
||||
from textual.reactive import reactive
|
||||
from textual.widgets import Static
|
||||
|
||||
from vibe.cli.textual_ui.widgets.no_markup_static import NoMarkupStatic
|
||||
|
||||
|
||||
@dataclass
|
||||
|
|
@ -13,7 +14,7 @@ class TokenState:
|
|||
current_tokens: int = 0
|
||||
|
||||
|
||||
class ContextProgress(Static):
|
||||
class ContextProgress(NoMarkupStatic):
|
||||
tokens = reactive(TokenState())
|
||||
|
||||
def __init__(self, **kwargs: Any) -> None:
|
||||
|
|
|
|||
|
|
@ -9,6 +9,7 @@ from textual.app import ComposeResult
|
|||
from textual.containers import Horizontal
|
||||
from textual.widgets import Static
|
||||
|
||||
from vibe.cli.textual_ui.widgets.no_markup_static import NoMarkupStatic
|
||||
from vibe.cli.textual_ui.widgets.spinner import SpinnerMixin, SpinnerType
|
||||
|
||||
|
||||
|
|
@ -55,10 +56,10 @@ class LoadingWidget(SpinnerMixin, Static):
|
|||
self.status = status or self._get_default_status()
|
||||
self.current_color_index = 0
|
||||
self.transition_progress = 0
|
||||
self.char_widgets: list[Static] = []
|
||||
self.ellipsis_widget: Static | None = None
|
||||
self._status_widget: Static | None = None
|
||||
self.hint_widget: Static | None = None
|
||||
self.start_time: float | None = None
|
||||
self._last_elapsed: int = -1
|
||||
|
||||
def _get_easter_egg(self) -> str | None:
|
||||
EASTER_EGG_PROBABILITY = 0.10
|
||||
|
|
@ -85,7 +86,7 @@ class LoadingWidget(SpinnerMixin, Static):
|
|||
|
||||
def set_status(self, status: str) -> None:
|
||||
self.status = self._apply_easter_egg(status)
|
||||
self._rebuild_chars()
|
||||
self._update_animation()
|
||||
|
||||
def compose(self) -> ComposeResult:
|
||||
with Horizontal(classes="loading-container"):
|
||||
|
|
@ -94,34 +95,14 @@ class LoadingWidget(SpinnerMixin, Static):
|
|||
)
|
||||
yield self._indicator_widget
|
||||
|
||||
with Horizontal(classes="loading-status"):
|
||||
for char in self.status:
|
||||
widget = Static(char, classes="loading-char")
|
||||
self.char_widgets.append(widget)
|
||||
yield widget
|
||||
self._status_widget = Static("", classes="loading-status")
|
||||
yield self._status_widget
|
||||
|
||||
self.ellipsis_widget = Static("… ", classes="loading-ellipsis")
|
||||
yield self.ellipsis_widget
|
||||
|
||||
self.hint_widget = Static("(0s esc to interrupt)", classes="loading-hint")
|
||||
self.hint_widget = NoMarkupStatic(
|
||||
"(0s esc to interrupt)", classes="loading-hint"
|
||||
)
|
||||
yield self.hint_widget
|
||||
|
||||
def _rebuild_chars(self) -> None:
|
||||
if not self.is_mounted:
|
||||
return
|
||||
|
||||
status_container = self.query_one(".loading-status", Horizontal)
|
||||
|
||||
status_container.remove_children()
|
||||
self.char_widgets.clear()
|
||||
|
||||
for char in self.status:
|
||||
widget = Static(char, classes="loading-char")
|
||||
self.char_widgets.append(widget)
|
||||
status_container.mount(widget)
|
||||
|
||||
self._update_animation()
|
||||
|
||||
def on_mount(self) -> None:
|
||||
self.start_time = time()
|
||||
self._update_animation()
|
||||
|
|
@ -144,24 +125,26 @@ class LoadingWidget(SpinnerMixin, Static):
|
|||
return next_color
|
||||
return current_color
|
||||
|
||||
def _build_status_text(self) -> str:
|
||||
parts = []
|
||||
for i, char in enumerate(self.status):
|
||||
color = self._get_color_for_position(1 + i)
|
||||
parts.append(f"[{color}]{char}[/]")
|
||||
ellipsis_start = 1 + len(self.status)
|
||||
color_ellipsis = self._get_color_for_position(ellipsis_start)
|
||||
parts.append(f"[{color_ellipsis}]… [/]")
|
||||
return "".join(parts)
|
||||
|
||||
def _update_animation(self) -> None:
|
||||
total_elements = 1 + len(self.char_widgets) + 2
|
||||
total_elements = 1 + len(self.status) + 1
|
||||
|
||||
if self._indicator_widget:
|
||||
spinner_char = self._spinner.next_frame()
|
||||
color = self._get_color_for_position(0)
|
||||
self._indicator_widget.update(f"[{color}]{spinner_char}[/]")
|
||||
|
||||
for i, widget in enumerate(self.char_widgets):
|
||||
position = 1 + i
|
||||
color = self._get_color_for_position(position)
|
||||
widget.update(f"[{color}]{self.status[i]}[/]")
|
||||
|
||||
if self.ellipsis_widget:
|
||||
ellipsis_start = 1 + len(self.status)
|
||||
color_ellipsis = self._get_color_for_position(ellipsis_start)
|
||||
color_space = self._get_color_for_position(ellipsis_start + 1)
|
||||
self.ellipsis_widget.update(f"[{color_ellipsis}]…[/][{color_space}] [/]")
|
||||
if self._status_widget:
|
||||
self._status_widget.update(self._build_status_text())
|
||||
|
||||
self.transition_progress += 1
|
||||
if self.transition_progress > total_elements:
|
||||
|
|
@ -172,4 +155,6 @@ class LoadingWidget(SpinnerMixin, Static):
|
|||
|
||||
if self.hint_widget and self.start_time is not None:
|
||||
elapsed = int(time() - self.start_time)
|
||||
self.hint_widget.update(f"({elapsed}s esc to interrupt)")
|
||||
if elapsed != self._last_elapsed:
|
||||
self._last_elapsed = elapsed
|
||||
self.hint_widget.update(f"({elapsed}s esc to interrupt)")
|
||||
|
|
|
|||
|
|
@ -7,10 +7,11 @@ from textual.containers import Horizontal, Vertical
|
|||
from textual.widgets import Markdown, Static
|
||||
from textual.widgets._markdown import MarkdownStream
|
||||
|
||||
from vibe.cli.textual_ui.widgets.no_markup_static import NoMarkupStatic
|
||||
from vibe.cli.textual_ui.widgets.spinner import SpinnerMixin, SpinnerType
|
||||
|
||||
|
||||
class NonSelectableStatic(Static):
|
||||
class NonSelectableStatic(NoMarkupStatic):
|
||||
@property
|
||||
def text_selection(self) -> None:
|
||||
return None
|
||||
|
|
@ -42,7 +43,7 @@ class UserMessage(Static):
|
|||
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")
|
||||
yield NoMarkupStatic(self._content, classes="user-message-content")
|
||||
if self._pending:
|
||||
self.add_class("pending")
|
||||
|
||||
|
|
@ -137,8 +138,8 @@ class ReasoningMessage(SpinnerMixin, StreamingMessageBase):
|
|||
self._spinner.current_frame(), classes="reasoning-indicator"
|
||||
)
|
||||
yield self._indicator_widget
|
||||
self._status_text_widget = Static(
|
||||
self.SPINNING_TEXT, markup=False, classes="reasoning-collapsed-text"
|
||||
self._status_text_widget = NoMarkupStatic(
|
||||
self.SPINNING_TEXT, classes="reasoning-collapsed-text"
|
||||
)
|
||||
yield self._status_text_widget
|
||||
self._triangle_widget = NonSelectableStatic(
|
||||
|
|
@ -204,9 +205,8 @@ class InterruptMessage(Static):
|
|||
def compose(self) -> ComposeResult:
|
||||
with Horizontal(classes="interrupt-container"):
|
||||
yield ExpandingBorder(classes="interrupt-border")
|
||||
yield Static(
|
||||
yield NoMarkupStatic(
|
||||
"Interrupted · What should Vibe do instead?",
|
||||
markup=False,
|
||||
classes="interrupt-content",
|
||||
)
|
||||
|
||||
|
|
@ -223,18 +223,20 @@ class BashOutputMessage(Static):
|
|||
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")
|
||||
yield NoMarkupStatic(self._cwd, classes="bash-cwd")
|
||||
yield NoMarkupStatic("", classes="bash-cwd-spacer")
|
||||
if self._exit_code == 0:
|
||||
yield Static("✓", classes="bash-exit-success")
|
||||
yield NoMarkupStatic("✓", classes="bash-exit-success")
|
||||
else:
|
||||
yield Static("✗", classes="bash-exit-failure")
|
||||
yield Static(f" ({self._exit_code})", classes="bash-exit-code")
|
||||
yield NoMarkupStatic("✗", classes="bash-exit-failure")
|
||||
yield NoMarkupStatic(
|
||||
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")
|
||||
yield NoMarkupStatic("> ", classes="bash-chevron")
|
||||
yield NoMarkupStatic(self._command, classes="bash-command")
|
||||
yield NoMarkupStatic("", classes="bash-command-spacer")
|
||||
yield NoMarkupStatic(self._output, classes="bash-output")
|
||||
|
||||
|
||||
class ErrorMessage(Static):
|
||||
|
|
@ -248,8 +250,8 @@ class ErrorMessage(Static):
|
|||
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"
|
||||
self._content_widget = NoMarkupStatic(
|
||||
self._get_text(), classes="error-content"
|
||||
)
|
||||
yield self._content_widget
|
||||
|
||||
|
|
@ -278,4 +280,4 @@ class WarningMessage(Static):
|
|||
with Horizontal(classes="warning-container"):
|
||||
if self._show_border:
|
||||
yield ExpandingBorder(classes="warning-border")
|
||||
yield Static(self._message, markup=False, classes="warning-content")
|
||||
yield NoMarkupStatic(self._message, classes="warning-content")
|
||||
|
|
|
|||
11
vibe/cli/textual_ui/widgets/no_markup_static.py
Normal file
11
vibe/cli/textual_ui/widgets/no_markup_static.py
Normal file
|
|
@ -0,0 +1,11 @@
|
|||
from __future__ import annotations
|
||||
|
||||
from typing import Any
|
||||
|
||||
from textual.visual import VisualType
|
||||
from textual.widgets import Static
|
||||
|
||||
|
||||
class NoMarkupStatic(Static):
|
||||
def __init__(self, content: VisualType = "", **kwargs: Any) -> None:
|
||||
super().__init__(content, markup=False, **kwargs)
|
||||
|
|
@ -2,10 +2,10 @@ from __future__ import annotations
|
|||
|
||||
from pathlib import Path
|
||||
|
||||
from textual.widgets import Static
|
||||
from vibe.cli.textual_ui.widgets.no_markup_static import NoMarkupStatic
|
||||
|
||||
|
||||
class PathDisplay(Static):
|
||||
class PathDisplay(NoMarkupStatic):
|
||||
def __init__(self, path: Path | str) -> None:
|
||||
super().__init__()
|
||||
self.can_focus = False
|
||||
|
|
|
|||
|
|
@ -147,3 +147,8 @@ class SpinnerMixin:
|
|||
self._indicator_widget.add_class("error")
|
||||
if self._status_text_widget and self.COMPLETED_TEXT:
|
||||
self._status_text_widget.update(self.COMPLETED_TEXT)
|
||||
|
||||
def on_unmount(self) -> None:
|
||||
if self._spinner_timer:
|
||||
self._spinner_timer.stop()
|
||||
self._spinner_timer = None
|
||||
|
|
|
|||
|
|
@ -7,16 +7,17 @@ from textual.containers import Horizontal
|
|||
from textual.widgets import Static
|
||||
|
||||
from vibe.cli.textual_ui.widgets.messages import NonSelectableStatic
|
||||
from vibe.cli.textual_ui.widgets.no_markup_static import NoMarkupStatic
|
||||
from vibe.cli.textual_ui.widgets.spinner import SpinnerMixin, SpinnerType
|
||||
|
||||
|
||||
class StatusMessage(SpinnerMixin, Static):
|
||||
class StatusMessage(SpinnerMixin, NoMarkupStatic):
|
||||
SPINNER_TYPE: ClassVar[SpinnerType] = SpinnerType.LINE
|
||||
|
||||
def __init__(self, initial_text: str = "", **kwargs: Any) -> None:
|
||||
self._initial_text = initial_text
|
||||
self._indicator_widget: Static | None = None
|
||||
self._text_widget: Static | None = None
|
||||
self._text_widget: NoMarkupStatic | None = None
|
||||
self.success = True
|
||||
self.init_spinner()
|
||||
super().__init__(**kwargs)
|
||||
|
|
@ -24,14 +25,10 @@ class StatusMessage(SpinnerMixin, Static):
|
|||
def compose(self) -> ComposeResult:
|
||||
with Horizontal():
|
||||
self._indicator_widget = NonSelectableStatic(
|
||||
self._spinner.current_frame(),
|
||||
markup=False,
|
||||
classes="status-indicator-icon",
|
||||
self._spinner.current_frame(), classes="status-indicator-icon"
|
||||
)
|
||||
yield self._indicator_widget
|
||||
self._text_widget = Static(
|
||||
"", markup=False, classes="status-indicator-text"
|
||||
)
|
||||
self._text_widget = NoMarkupStatic("", classes="status-indicator-text")
|
||||
yield self._text_widget
|
||||
|
||||
def on_mount(self) -> None:
|
||||
|
|
|
|||
|
|
@ -8,6 +8,7 @@ from textual.app import ComposeResult
|
|||
from textual.containers import Vertical
|
||||
from textual.widgets import Markdown, Static
|
||||
|
||||
from vibe.cli.textual_ui.widgets.no_markup_static import NoMarkupStatic
|
||||
from vibe.cli.textual_ui.widgets.utils import DEFAULT_TOOL_SHORTCUT, TOOL_SHORTCUTS
|
||||
from vibe.core.tools.builtins.bash import BashArgs, BashResult
|
||||
from vibe.core.tools.builtins.grep import GrepArgs, GrepResult
|
||||
|
|
@ -51,15 +52,15 @@ def parse_search_replace_to_diff(content: str) -> list[str]:
|
|||
def render_diff_line(line: str) -> Static:
|
||||
"""Render a single diff line with appropriate styling."""
|
||||
if line.startswith("---") or line.startswith("+++"):
|
||||
return Static(line, markup=False, classes="diff-header")
|
||||
return NoMarkupStatic(line, classes="diff-header")
|
||||
elif line.startswith("-"):
|
||||
return Static(line, markup=False, classes="diff-removed")
|
||||
return NoMarkupStatic(line, classes="diff-removed")
|
||||
elif line.startswith("+"):
|
||||
return Static(line, markup=False, classes="diff-added")
|
||||
return NoMarkupStatic(line, classes="diff-added")
|
||||
elif line.startswith("@@"):
|
||||
return Static(line, markup=False, classes="diff-range")
|
||||
return NoMarkupStatic(line, classes="diff-range")
|
||||
else:
|
||||
return Static(line, markup=False, classes="diff-context")
|
||||
return NoMarkupStatic(line, classes="diff-context")
|
||||
|
||||
|
||||
class ToolApprovalWidget[TArgs: BaseModel](Vertical):
|
||||
|
|
@ -80,10 +81,8 @@ class ToolApprovalWidget[TArgs: BaseModel](Vertical):
|
|||
if len(value_str) > MAX_MSG_SIZE:
|
||||
hidden = len(value_str) - MAX_MSG_SIZE
|
||||
value_str = value_str[:MAX_MSG_SIZE] + f"… ({hidden} more characters)"
|
||||
yield Static(
|
||||
f"{field_name}: {value_str}",
|
||||
markup=False,
|
||||
classes="approval-description",
|
||||
yield NoMarkupStatic(
|
||||
f"{field_name}: {value_str}", classes="approval-description"
|
||||
)
|
||||
|
||||
|
||||
|
|
@ -115,9 +114,9 @@ class ToolResultWidget[TResult: BaseModel](Static):
|
|||
def _header(self) -> ComposeResult:
|
||||
"""Yield the standard header. Subclasses can call this then add content."""
|
||||
if self.collapsed:
|
||||
yield Static(f"{self.message} {self._hint()}", markup=False)
|
||||
yield NoMarkupStatic(f"{self.message} {self._hint()}")
|
||||
else:
|
||||
yield Static(self.message, markup=False)
|
||||
yield NoMarkupStatic(self.message)
|
||||
|
||||
def compose(self) -> ComposeResult:
|
||||
"""Default: show message and optionally result fields."""
|
||||
|
|
@ -127,10 +126,8 @@ class ToolResultWidget[TResult: BaseModel](Static):
|
|||
for field_name in type(self.result).model_fields:
|
||||
value = getattr(self.result, field_name)
|
||||
if value is not None and value not in ("", []):
|
||||
yield Static(
|
||||
f"{field_name}: {value}",
|
||||
markup=False,
|
||||
classes="tool-result-detail",
|
||||
yield NoMarkupStatic(
|
||||
f"{field_name}: {value}", classes="tool-result-detail"
|
||||
)
|
||||
|
||||
|
||||
|
|
@ -144,24 +141,18 @@ class BashResultWidget(ToolResultWidget[BashResult]):
|
|||
yield from self._header()
|
||||
if self.collapsed or not self.result:
|
||||
return
|
||||
yield Static(
|
||||
f"returncode: {self.result.returncode}",
|
||||
markup=False,
|
||||
classes="tool-result-detail",
|
||||
yield NoMarkupStatic(
|
||||
f"returncode: {self.result.returncode}", classes="tool-result-detail"
|
||||
)
|
||||
if self.result.stdout:
|
||||
sep = "\n" if "\n" in self.result.stdout else " "
|
||||
yield Static(
|
||||
f"stdout:{sep}{self.result.stdout}",
|
||||
markup=False,
|
||||
classes="tool-result-detail",
|
||||
yield NoMarkupStatic(
|
||||
f"stdout:{sep}{self.result.stdout}", classes="tool-result-detail"
|
||||
)
|
||||
if self.result.stderr:
|
||||
sep = "\n" if "\n" in self.result.stderr else " "
|
||||
yield Static(
|
||||
f"stderr:{sep}{self.result.stderr}",
|
||||
markup=False,
|
||||
classes="tool-result-detail",
|
||||
yield NoMarkupStatic(
|
||||
f"stderr:{sep}{self.result.stderr}", classes="tool-result-detail"
|
||||
)
|
||||
|
||||
|
||||
|
|
@ -170,10 +161,8 @@ class WriteFileApprovalWidget(ToolApprovalWidget[WriteFileArgs]):
|
|||
path = Path(self.args.path)
|
||||
file_extension = path.suffix.lstrip(".") or "text"
|
||||
|
||||
yield Static(
|
||||
f"File: {self.args.path}", markup=False, classes="approval-description"
|
||||
)
|
||||
yield Static("")
|
||||
yield NoMarkupStatic(f"File: {self.args.path}", classes="approval-description")
|
||||
yield NoMarkupStatic("")
|
||||
yield Markdown(f"```{file_extension}\n{self.args.content}\n```")
|
||||
|
||||
|
||||
|
|
@ -182,26 +171,22 @@ class WriteFileResultWidget(ToolResultWidget[WriteFileResult]):
|
|||
yield from self._header()
|
||||
if self.collapsed or not self.result:
|
||||
return
|
||||
yield Static(
|
||||
f"Path: {self.result.path}", markup=False, classes="tool-result-detail"
|
||||
)
|
||||
yield Static(
|
||||
f"Bytes: {self.result.bytes_written}",
|
||||
markup=False,
|
||||
classes="tool-result-detail",
|
||||
yield NoMarkupStatic(f"Path: {self.result.path}", classes="tool-result-detail")
|
||||
yield NoMarkupStatic(
|
||||
f"Bytes: {self.result.bytes_written}", classes="tool-result-detail"
|
||||
)
|
||||
if self.result.content:
|
||||
yield Static("")
|
||||
yield NoMarkupStatic("")
|
||||
ext = Path(self.result.path).suffix.lstrip(".") or "text"
|
||||
yield Markdown(f"```{ext}\n{_truncate_lines(self.result.content, 10)}\n```")
|
||||
|
||||
|
||||
class SearchReplaceApprovalWidget(ToolApprovalWidget[SearchReplaceArgs]):
|
||||
def compose(self) -> ComposeResult:
|
||||
yield Static(
|
||||
f"File: {self.args.file_path}", markup=False, classes="approval-description"
|
||||
yield NoMarkupStatic(
|
||||
f"File: {self.args.file_path}", classes="approval-description"
|
||||
)
|
||||
yield Static("")
|
||||
yield NoMarkupStatic("")
|
||||
|
||||
diff_lines = parse_search_replace_to_diff(self.args.content)
|
||||
for line in diff_lines:
|
||||
|
|
@ -213,37 +198,30 @@ class SearchReplaceResultWidget(ToolResultWidget[SearchReplaceResult]):
|
|||
yield from self._header()
|
||||
if self.collapsed or not self.result:
|
||||
return
|
||||
yield Static(
|
||||
f"File: {self.result.file}", markup=False, classes="tool-result-detail"
|
||||
)
|
||||
yield Static(
|
||||
yield NoMarkupStatic(f"File: {self.result.file}", classes="tool-result-detail")
|
||||
yield NoMarkupStatic(
|
||||
f"Blocks applied: {self.result.blocks_applied}",
|
||||
markup=False,
|
||||
classes="tool-result-detail",
|
||||
)
|
||||
yield Static(
|
||||
f"Lines changed: {self.result.lines_changed}",
|
||||
markup=False,
|
||||
classes="tool-result-detail",
|
||||
yield NoMarkupStatic(
|
||||
f"Lines changed: {self.result.lines_changed}", classes="tool-result-detail"
|
||||
)
|
||||
for warning in self.result.warnings:
|
||||
yield Static(f"⚠ {warning}", markup=False, classes="tool-result-warning")
|
||||
yield NoMarkupStatic(f"⚠ {warning}", classes="tool-result-warning")
|
||||
if self.result.content:
|
||||
yield Static("")
|
||||
yield NoMarkupStatic("")
|
||||
for line in parse_search_replace_to_diff(self.result.content):
|
||||
yield render_diff_line(line)
|
||||
|
||||
|
||||
class TodoApprovalWidget(ToolApprovalWidget[TodoArgs]):
|
||||
def compose(self) -> ComposeResult:
|
||||
yield Static(
|
||||
f"Action: {self.args.action}", markup=False, classes="approval-description"
|
||||
yield NoMarkupStatic(
|
||||
f"Action: {self.args.action}", classes="approval-description"
|
||||
)
|
||||
if self.args.todos:
|
||||
yield Static(
|
||||
f"Todos: {len(self.args.todos)} items",
|
||||
markup=False,
|
||||
classes="approval-description",
|
||||
yield NoMarkupStatic(
|
||||
f"Todos: {len(self.args.todos)} items", classes="approval-description"
|
||||
)
|
||||
|
||||
|
||||
|
|
@ -252,13 +230,13 @@ class TodoResultWidget(ToolResultWidget[TodoResult]):
|
|||
|
||||
def compose(self) -> ComposeResult:
|
||||
if self.collapsed:
|
||||
yield Static(f"{self.message} {self._hint()}", markup=False)
|
||||
yield NoMarkupStatic(f"{self.message} {self._hint()}")
|
||||
else:
|
||||
yield Static(f"{self.message} {self._hint()}", markup=False)
|
||||
yield Static("")
|
||||
yield NoMarkupStatic(f"{self.message} {self._hint()}")
|
||||
yield NoMarkupStatic("")
|
||||
|
||||
if not self.result or not self.result.todos:
|
||||
yield Static("No todos", markup=False, classes="todo-empty")
|
||||
yield NoMarkupStatic("No todos", classes="todo-empty")
|
||||
return
|
||||
|
||||
# Group todos by status
|
||||
|
|
@ -280,8 +258,8 @@ class TodoResultWidget(ToolResultWidget[TodoResult]):
|
|||
for status in ["in_progress", "pending", "completed", "cancelled"]:
|
||||
for todo in by_status[status]:
|
||||
icon = self._get_status_icon(status)
|
||||
yield Static(
|
||||
f"{icon} {todo.content}", markup=False, classes=f"todo-{status}"
|
||||
yield NoMarkupStatic(
|
||||
f"{icon} {todo.content}", classes=f"todo-{status}"
|
||||
)
|
||||
|
||||
def _get_status_icon(self, status: str) -> str:
|
||||
|
|
@ -291,20 +269,14 @@ class TodoResultWidget(ToolResultWidget[TodoResult]):
|
|||
|
||||
class ReadFileApprovalWidget(ToolApprovalWidget[ReadFileArgs]):
|
||||
def compose(self) -> ComposeResult:
|
||||
yield Static(
|
||||
f"path: {self.args.path}", markup=False, classes="approval-description"
|
||||
)
|
||||
yield NoMarkupStatic(f"path: {self.args.path}", classes="approval-description")
|
||||
if self.args.offset > 0:
|
||||
yield Static(
|
||||
f"offset: {self.args.offset}",
|
||||
markup=False,
|
||||
classes="approval-description",
|
||||
yield NoMarkupStatic(
|
||||
f"offset: {self.args.offset}", classes="approval-description"
|
||||
)
|
||||
if self.args.limit is not None:
|
||||
yield Static(
|
||||
f"limit: {self.args.limit}",
|
||||
markup=False,
|
||||
classes="approval-description",
|
||||
yield NoMarkupStatic(
|
||||
f"limit: {self.args.limit}", classes="approval-description"
|
||||
)
|
||||
|
||||
|
||||
|
|
@ -314,32 +286,26 @@ class ReadFileResultWidget(ToolResultWidget[ReadFileResult]):
|
|||
if self.collapsed:
|
||||
return
|
||||
if self.result:
|
||||
yield Static(
|
||||
f"Path: {self.result.path}", markup=False, classes="tool-result-detail"
|
||||
yield NoMarkupStatic(
|
||||
f"Path: {self.result.path}", classes="tool-result-detail"
|
||||
)
|
||||
for warning in self.warnings:
|
||||
yield Static(f"⚠ {warning}", markup=False, classes="tool-result-warning")
|
||||
yield NoMarkupStatic(f"⚠ {warning}", classes="tool-result-warning")
|
||||
if self.result and self.result.content:
|
||||
yield Static("")
|
||||
yield NoMarkupStatic("")
|
||||
ext = Path(self.result.path).suffix.lstrip(".") or "text"
|
||||
yield Markdown(f"```{ext}\n{_truncate_lines(self.result.content, 10)}\n```")
|
||||
|
||||
|
||||
class GrepApprovalWidget(ToolApprovalWidget[GrepArgs]):
|
||||
def compose(self) -> ComposeResult:
|
||||
yield Static(
|
||||
f"pattern: {self.args.pattern}",
|
||||
markup=False,
|
||||
classes="approval-description",
|
||||
)
|
||||
yield Static(
|
||||
f"path: {self.args.path}", markup=False, classes="approval-description"
|
||||
yield NoMarkupStatic(
|
||||
f"pattern: {self.args.pattern}", classes="approval-description"
|
||||
)
|
||||
yield NoMarkupStatic(f"path: {self.args.path}", classes="approval-description")
|
||||
if self.args.max_matches is not None:
|
||||
yield Static(
|
||||
f"max_matches: {self.args.max_matches}",
|
||||
markup=False,
|
||||
classes="approval-description",
|
||||
yield NoMarkupStatic(
|
||||
f"max_matches: {self.args.max_matches}", classes="approval-description"
|
||||
)
|
||||
|
||||
|
||||
|
|
@ -349,9 +315,9 @@ class GrepResultWidget(ToolResultWidget[GrepResult]):
|
|||
if self.collapsed:
|
||||
return
|
||||
for warning in self.warnings:
|
||||
yield Static(f"⚠ {warning}", markup=False, classes="tool-result-warning")
|
||||
yield NoMarkupStatic(f"⚠ {warning}", classes="tool-result-warning")
|
||||
if self.result and self.result.matches:
|
||||
yield Static("")
|
||||
yield NoMarkupStatic("")
|
||||
yield Markdown(f"```\n{_truncate_lines(self.result.matches, 30)}\n```")
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -5,6 +5,7 @@ from textual.containers import Horizontal, Vertical
|
|||
from textual.widgets import Static
|
||||
|
||||
from vibe.cli.textual_ui.widgets.messages import ExpandingBorder
|
||||
from vibe.cli.textual_ui.widgets.no_markup_static import NoMarkupStatic
|
||||
from vibe.cli.textual_ui.widgets.status_message import StatusMessage
|
||||
from vibe.cli.textual_ui.widgets.tool_widgets import get_result_widget
|
||||
from vibe.cli.textual_ui.widgets.utils import DEFAULT_TOOL_SHORTCUT, TOOL_SHORTCUTS
|
||||
|
|
@ -99,11 +100,11 @@ class ToolResultMessage(Static):
|
|||
self.add_class("error-text")
|
||||
if self.collapsed:
|
||||
await self._content_container.mount(
|
||||
Static(f"Error. {self._hint()}", markup=False)
|
||||
NoMarkupStatic(f"Error. {self._hint()}")
|
||||
)
|
||||
else:
|
||||
await self._content_container.mount(
|
||||
Static(f"Error: {self._event.error}", markup=False)
|
||||
NoMarkupStatic(f"Error: {self._event.error}")
|
||||
)
|
||||
return
|
||||
|
||||
|
|
@ -112,11 +113,11 @@ class ToolResultMessage(Static):
|
|||
reason = self._event.skip_reason or "User skipped"
|
||||
if self.collapsed:
|
||||
await self._content_container.mount(
|
||||
Static(f"Skipped. {self._hint()}", markup=False)
|
||||
NoMarkupStatic(f"Skipped. {self._hint()}")
|
||||
)
|
||||
else:
|
||||
await self._content_container.mount(
|
||||
Static(f"Skipped: {reason}", markup=False)
|
||||
NoMarkupStatic(f"Skipped: {reason}")
|
||||
)
|
||||
return
|
||||
|
||||
|
|
@ -146,15 +147,15 @@ class ToolResultMessage(Static):
|
|||
|
||||
if self.collapsed:
|
||||
await self._content_container.mount(
|
||||
Static(f"{self._tool_name} completed {self._hint()}", markup=False)
|
||||
NoMarkupStatic(f"{self._tool_name} completed {self._hint()}")
|
||||
)
|
||||
return
|
||||
|
||||
if self._content:
|
||||
await self._content_container.mount(Static(self._content, markup=False))
|
||||
await self._content_container.mount(NoMarkupStatic(self._content))
|
||||
else:
|
||||
await self._content_container.mount(
|
||||
Static(f"{self._tool_name} completed.", markup=False)
|
||||
NoMarkupStatic(f"{self._tool_name} completed.")
|
||||
)
|
||||
|
||||
async def set_collapsed(self, collapsed: bool) -> None:
|
||||
|
|
|
|||
|
|
@ -24,8 +24,6 @@ from vibe.core.paths.global_paths import GLOBAL_ENV_FILE, SESSION_LOG_DIR
|
|||
from vibe.core.prompts import SystemPrompt
|
||||
from vibe.core.tools.base import BaseToolConfig
|
||||
|
||||
PROJECT_DOC_FILENAMES = ["AGENTS.md", "VIBE.md", ".vibe.md"]
|
||||
|
||||
|
||||
def load_api_keys_from_env() -> None:
|
||||
if GLOBAL_ENV_FILE.path.is_file():
|
||||
|
|
|
|||
|
|
@ -10,10 +10,10 @@ import sys
|
|||
import time
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
from vibe.core.config import PROJECT_DOC_FILENAMES
|
||||
from vibe.core.llm.format import get_active_tool_classes
|
||||
from vibe.core.paths.config_paths import INSTRUCTIONS_FILE
|
||||
from vibe.core.prompts import UtilityPrompt
|
||||
from vibe.core.trusted_folders import TRUSTABLE_FILENAMES, trusted_folders_manager
|
||||
from vibe.core.utils import is_dangerous_directory, is_windows
|
||||
|
||||
if TYPE_CHECKING:
|
||||
|
|
@ -30,7 +30,9 @@ def _load_user_instructions() -> str:
|
|||
|
||||
|
||||
def _load_project_doc(workdir: Path, max_bytes: int) -> str:
|
||||
for name in PROJECT_DOC_FILENAMES:
|
||||
if not trusted_folders_manager.is_trusted(workdir):
|
||||
return ""
|
||||
for name in TRUSTABLE_FILENAMES:
|
||||
path = workdir / name
|
||||
try:
|
||||
return path.read_text("utf-8", errors="ignore")[:max_bytes]
|
||||
|
|
|
|||
|
|
@ -1,13 +1,15 @@
|
|||
from __future__ import annotations
|
||||
|
||||
import asyncio
|
||||
from functools import lru_cache
|
||||
import os
|
||||
import re
|
||||
import signal
|
||||
import sys
|
||||
from typing import ClassVar, Literal, final
|
||||
|
||||
from pydantic import BaseModel, Field
|
||||
from tree_sitter import Language, Node, Parser
|
||||
import tree_sitter_bash as tsbash
|
||||
|
||||
from vibe.core.tools.base import (
|
||||
BaseTool,
|
||||
|
|
@ -19,6 +21,37 @@ from vibe.core.tools.base import (
|
|||
from vibe.core.utils import is_windows
|
||||
|
||||
|
||||
@lru_cache(maxsize=1)
|
||||
def _get_parser() -> Parser:
|
||||
return Parser(Language(tsbash.language()))
|
||||
|
||||
|
||||
def _extract_commands(command: str) -> list[str]:
|
||||
parser = _get_parser()
|
||||
tree = parser.parse(command.encode("utf-8"))
|
||||
|
||||
commands: list[str] = []
|
||||
|
||||
def find_commands(node: Node) -> None:
|
||||
if node.type == "command":
|
||||
parts = []
|
||||
for child in node.children:
|
||||
if (
|
||||
child.type
|
||||
in {"command_name", "word", "string", "raw_string", "concatenation"}
|
||||
and child.text is not None
|
||||
):
|
||||
parts.append(child.text.decode("utf-8"))
|
||||
if parts:
|
||||
commands.append(" ".join(parts))
|
||||
|
||||
for child in node.children:
|
||||
find_commands(child)
|
||||
|
||||
find_commands(tree.root_node)
|
||||
return commands
|
||||
|
||||
|
||||
def _get_subprocess_encoding() -> str:
|
||||
if sys.platform == "win32":
|
||||
# Windows console uses OEM code page (e.g., cp850, cp1252)
|
||||
|
|
@ -167,9 +200,10 @@ class Bash(BaseTool[BashArgs, BashResult, BashToolConfig, BaseToolState]):
|
|||
description: ClassVar[str] = "Run a one-off bash command and capture its output."
|
||||
|
||||
def check_allowlist_denylist(self, args: BashArgs) -> ToolPermission | None:
|
||||
command_parts = re.split(r"(?:&&|\|\||;|\|)", args.command)
|
||||
command_parts = [part.strip() for part in command_parts if part.strip()]
|
||||
if is_windows():
|
||||
return None
|
||||
|
||||
command_parts = _extract_commands(args.command)
|
||||
if not command_parts:
|
||||
return None
|
||||
|
||||
|
|
|
|||
|
|
@ -7,6 +7,17 @@ import tomli_w
|
|||
|
||||
from vibe.core.paths.global_paths import TRUSTED_FOLDERS_FILE
|
||||
|
||||
TRUSTABLE_FILENAMES = ["AGENTS.md", "VIBE.md", ".vibe.md"]
|
||||
|
||||
|
||||
def has_trustable_content(path: Path) -> bool:
|
||||
if (path / ".vibe").exists():
|
||||
return True
|
||||
for name in TRUSTABLE_FILENAMES:
|
||||
if (path / name).exists():
|
||||
return True
|
||||
return False
|
||||
|
||||
|
||||
class TrustedFoldersManager:
|
||||
def __init__(self) -> None:
|
||||
|
|
|
|||
|
|
@ -12,6 +12,7 @@ from textual.validation import Length
|
|||
from textual.widgets import Input, Link, Static
|
||||
|
||||
from vibe.cli.clipboard import copy_selection_to_clipboard
|
||||
from vibe.cli.textual_ui.widgets.no_markup_static import NoMarkupStatic
|
||||
from vibe.core.config import VibeConfig
|
||||
from vibe.core.paths.global_paths import GLOBAL_ENV_FILE
|
||||
from vibe.setup.onboarding.base import OnboardingScreen
|
||||
|
|
@ -48,10 +49,10 @@ class ApiKeyScreen(OnboardingScreen):
|
|||
return
|
||||
|
||||
help_url, help_name = PROVIDER_HELP[self.provider.name]
|
||||
yield Static(f"Grab your {provider_name} API key from the {help_name}:")
|
||||
yield NoMarkupStatic(f"Grab your {provider_name} API key from the {help_name}:")
|
||||
yield Center(
|
||||
Horizontal(
|
||||
Static("→ ", classes="link-chevron"),
|
||||
NoMarkupStatic("→ ", classes="link-chevron"),
|
||||
Link(help_url, url=help_url),
|
||||
classes="link-row",
|
||||
)
|
||||
|
|
@ -60,7 +61,7 @@ class ApiKeyScreen(OnboardingScreen):
|
|||
def _compose_config_docs(self) -> ComposeResult:
|
||||
yield Static("[dim]Learn more about Vibe configuration:[/]")
|
||||
yield Horizontal(
|
||||
Static("→ ", classes="link-chevron"),
|
||||
NoMarkupStatic("→ ", classes="link-chevron"),
|
||||
Link(CONFIG_DOCS_URL, url=CONFIG_DOCS_URL),
|
||||
classes="link-row",
|
||||
)
|
||||
|
|
@ -76,17 +77,17 @@ class ApiKeyScreen(OnboardingScreen):
|
|||
)
|
||||
|
||||
with Vertical(id="api-key-outer"):
|
||||
yield Static("", classes="spacer")
|
||||
yield Center(Static("One last thing...", id="api-key-title"))
|
||||
yield NoMarkupStatic("", classes="spacer")
|
||||
yield Center(NoMarkupStatic("One last thing...", id="api-key-title"))
|
||||
with Center():
|
||||
with Vertical(id="api-key-content"):
|
||||
yield from self._compose_provider_link(provider_name)
|
||||
yield Static(
|
||||
yield NoMarkupStatic(
|
||||
"...and paste it below to finish the setup:", id="paste-hint"
|
||||
)
|
||||
yield Center(Horizontal(self.input_widget, id="input-box"))
|
||||
yield Static("", id="feedback")
|
||||
yield Static("", classes="spacer")
|
||||
yield NoMarkupStatic("", id="feedback")
|
||||
yield NoMarkupStatic("", classes="spacer")
|
||||
yield Vertical(
|
||||
Vertical(*self._compose_config_docs(), id="config-docs-group"),
|
||||
id="config-docs-section",
|
||||
|
|
@ -96,7 +97,7 @@ class ApiKeyScreen(OnboardingScreen):
|
|||
self.input_widget.focus()
|
||||
|
||||
def on_input_changed(self, event: Input.Changed) -> None:
|
||||
feedback = self.query_one("#feedback", Static)
|
||||
feedback = self.query_one("#feedback", NoMarkupStatic)
|
||||
input_box = self.query_one("#input-box")
|
||||
|
||||
if event.validation_result is None:
|
||||
|
|
|
|||
|
|
@ -10,6 +10,7 @@ from textual.theme import BUILTIN_THEMES
|
|||
from textual.widgets import Markdown, Static
|
||||
|
||||
from vibe.cli.textual_ui.terminal_theme import TERMINAL_THEME_NAME
|
||||
from vibe.cli.textual_ui.widgets.no_markup_static import NoMarkupStatic
|
||||
from vibe.core.config import VibeConfig
|
||||
from vibe.setup.onboarding.base import OnboardingScreen
|
||||
|
||||
|
|
@ -67,19 +68,19 @@ class ThemeSelectionScreen(OnboardingScreen):
|
|||
|
||||
def _compose_theme_list(self) -> ComposeResult:
|
||||
for _ in range(VISIBLE_NEIGHBORS * 2 + 1):
|
||||
widget = Static("", classes="theme-item")
|
||||
widget = NoMarkupStatic("", classes="theme-item")
|
||||
self._theme_widgets.append(widget)
|
||||
yield widget
|
||||
|
||||
def compose(self) -> ComposeResult:
|
||||
with Center(id="theme-outer"):
|
||||
with Vertical(id="theme-content"):
|
||||
yield Static("Select your preferred theme", id="theme-title")
|
||||
yield NoMarkupStatic("Select your preferred theme", id="theme-title")
|
||||
yield Center(
|
||||
Horizontal(
|
||||
Static("Navigate ↑ ↓", id="nav-hint"),
|
||||
NoMarkupStatic("Navigate ↑ ↓", id="nav-hint"),
|
||||
Vertical(*self._compose_theme_list(), id="theme-list"),
|
||||
Static("Press Enter \u21b5", id="enter-hint"),
|
||||
NoMarkupStatic("Press Enter \u21b5", id="enter-hint"),
|
||||
id="theme-row",
|
||||
)
|
||||
)
|
||||
|
|
|
|||
|
|
@ -8,6 +8,7 @@ from textual.containers import Center, Vertical
|
|||
from textual.timer import Timer
|
||||
from textual.widgets import Static
|
||||
|
||||
from vibe.cli.textual_ui.widgets.no_markup_static import NoMarkupStatic
|
||||
from vibe.setup.onboarding.base import OnboardingScreen
|
||||
|
||||
WELCOME_PREFIX = "Welcome to "
|
||||
|
|
@ -68,7 +69,7 @@ class WelcomeScreen(OnboardingScreen):
|
|||
with Center():
|
||||
yield Static("", id="welcome-text")
|
||||
with Center():
|
||||
yield Static("", id="enter-hint", classes="hidden")
|
||||
yield NoMarkupStatic("", id="enter-hint", classes="hidden")
|
||||
|
||||
def on_mount(self) -> None:
|
||||
self._welcome_text = self.query_one("#welcome-text", Static)
|
||||
|
|
|
|||
|
|
@ -15,6 +15,7 @@ from vibe.cli.textual_ui.terminal_theme import (
|
|||
TERMINAL_THEME_NAME,
|
||||
capture_terminal_theme,
|
||||
)
|
||||
from vibe.cli.textual_ui.widgets.no_markup_static import NoMarkupStatic
|
||||
from vibe.core.paths.global_paths import GLOBAL_CONFIG_FILE, TRUSTED_FOLDERS_FILE
|
||||
|
||||
|
||||
|
|
@ -50,14 +51,14 @@ class TrustFolderDialog(CenterMiddle):
|
|||
|
||||
def compose(self) -> ComposeResult:
|
||||
with CenterMiddle(id="trust-dialog"):
|
||||
yield Static("⚠ Trust this folder?", id="trust-dialog-title")
|
||||
yield Static(
|
||||
yield NoMarkupStatic("⚠ Trust this folder?", id="trust-dialog-title")
|
||||
yield NoMarkupStatic(
|
||||
str(self.folder_path),
|
||||
id="trust-dialog-path",
|
||||
classes="trust-dialog-path",
|
||||
)
|
||||
yield Static(
|
||||
"A .vibe/ directory was found here. Should Vibe load custom configuration and tools from it?",
|
||||
yield NoMarkupStatic(
|
||||
"Files that can modify your Mistral Vibe setup were found here. Do you trust this folder?",
|
||||
id="trust-dialog-message",
|
||||
classes="trust-dialog-message",
|
||||
)
|
||||
|
|
@ -65,13 +66,17 @@ class TrustFolderDialog(CenterMiddle):
|
|||
with Horizontal(id="trust-options-container"):
|
||||
options = ["Yes", "No"]
|
||||
for idx, text in enumerate(options):
|
||||
widget = Static(f" {idx + 1}. {text}", classes="trust-option")
|
||||
widget = NoMarkupStatic(
|
||||
f" {idx + 1}. {text}", classes="trust-option"
|
||||
)
|
||||
self.option_widgets.append(widget)
|
||||
yield widget
|
||||
|
||||
yield Static("← → navigate Enter select", classes="trust-dialog-help")
|
||||
yield NoMarkupStatic(
|
||||
"← → navigate Enter select", classes="trust-dialog-help"
|
||||
)
|
||||
|
||||
yield Static(
|
||||
yield NoMarkupStatic(
|
||||
f"Setting will be saved in: {TRUSTED_FOLDERS_FILE.path}",
|
||||
id="trust-dialog-save-info",
|
||||
classes="trust-dialog-save-info",
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue