from __future__ import annotations from rich.style import Style from textual.widgets.text_area import TextAreaTheme from tests.cli.plan_offer.adapters.fake_whoami_gateway import FakeWhoAmIGateway from tests.conftest import build_test_agent_loop, build_test_vibe_config from tests.stubs.fake_backend import FakeBackend from vibe.cli.plan_offer.ports.whoami_gateway import WhoAmIPlanType, WhoAmIResponse from vibe.cli.textual_ui.app import VibeApp from vibe.cli.textual_ui.widgets.chat_input import ChatTextArea from vibe.core.agents.models import BuiltinAgentName from vibe.core.config import VibeConfig def default_config() -> VibeConfig: """Default configuration for snapshot testing. Remove as much interference as possible from the snapshot comparison, in order to get a clean pixel-to-pixel comparison. - Injects a fake backend to prevent (or stub) LLM calls. - Disables the banner animation. - Forces a value for the displayed workdir - Hides the chat input cursor (as the blinking animation is not deterministic). """ return build_test_vibe_config( disable_welcome_banner_animation=True, displayed_workdir="/test/workdir" ) class BaseSnapshotTestApp(VibeApp): CSS_PATH = "../../vibe/cli/textual_ui/app.tcss" _current_agent_name: str = BuiltinAgentName.DEFAULT def __init__( self, config: VibeConfig | None = None, backend: FakeBackend | None = None, **kwargs, ): agent_loop_kwargs: dict = {} if "mcp_registry" in kwargs: agent_loop_kwargs["mcp_registry"] = kwargs.pop("mcp_registry") agent_loop = build_test_agent_loop( config=config or default_config(), agent_name=self._current_agent_name, enable_streaming=bool(kwargs.get("enable_streaming", False)), backend=backend or FakeBackend(), **agent_loop_kwargs, ) plan_offer_gateway = kwargs.pop( "plan_offer_gateway", FakeWhoAmIGateway( WhoAmIResponse( plan_type=WhoAmIPlanType.CHAT, plan_name="INDIVIDUAL", prompt_switching_to_pro_plan=False, ) ), ) super().__init__( agent_loop=agent_loop, plan_offer_gateway=plan_offer_gateway, **kwargs ) async def on_ready(self): # on_ready is called once all the on_mount in the MRO chain have been called # https://textual.textualize.io/api/events/#textual.events.Ready self._hide_chat_input_cursor() def _hide_chat_input_cursor(self) -> None: text_area = self.query_one(ChatTextArea) hidden_cursor_theme = TextAreaTheme(name="hidden_cursor", cursor_style=Style()) text_area.register_theme(hidden_cursor_theme) text_area.theme = "hidden_cursor"