from __future__ import annotations from typing import cast from textual.app import App, ComposeResult from textual.containers import VerticalScroll from textual.pilot import Pilot from textual.widget import Widget from tests.snapshots.snap_compare import SnapCompare from vibe.cli.textual_ui.handlers.event_handler import EventHandler from vibe.cli.textual_ui.widgets.tools import ToolCallMessage from vibe.core.tools.builtins.read import Read, ReadArgs, ReadResult from vibe.core.types import ToolCallEvent, ToolResultEvent class ParallelToolCallsApp(App): CSS_PATH = "../../vibe/cli/textual_ui/app.tcss" def __init__(self) -> None: super().__init__() self._scroll: VerticalScroll | None = None self._handler: EventHandler | None = None def compose(self) -> ComposeResult: self._scroll = VerticalScroll(id="messages") yield self._scroll def on_mount(self) -> None: async def mount_callback( widget: Widget, *, after: Widget | None = None ) -> None: if self._scroll is None: return if after is not None and after.parent is self._scroll: await self._scroll.mount(widget, after=after) else: await self._scroll.mount(widget) self._handler = EventHandler( mount_callback=mount_callback, get_tools_collapsed=lambda: False ) async def emit_all_tool_calls(self) -> None: if self._handler is None: return for i in range(3): await self._handler.handle_event( ToolCallEvent( tool_call_id=f"tc_{i}", tool_call_index=i, tool_name="read", tool_class=Read, args=ReadArgs(file_path=f"/src/file_{i}.py"), ) ) def freeze_spinners(self) -> None: for widget in self.query(ToolCallMessage): widget._is_spinning = False if widget._spinner_timer: widget._spinner_timer.stop() widget._spinner_timer = None widget._spinner.reset() if widget._indicator_widget: widget._indicator_widget.update(widget._spinner.current_frame()) async def resolve_all_results(self) -> None: if self._handler is None: return for i in range(3): await self._handler.handle_event( ToolResultEvent( tool_name="read", tool_class=Read, result=ReadResult( file_path=f"/src/file_{i}.py", content=f" 1→# content of file_{i}.py", num_lines=1, start_line=1, total_lines=1, ), tool_call_id=f"tc_{i}", ) ) def test_snapshot_parallel_tool_calls_pending(snap_compare: SnapCompare) -> None: async def run_before(pilot: Pilot) -> None: app = cast(ParallelToolCallsApp, pilot.app) await app.emit_all_tool_calls() await pilot.pause(0.3) app.freeze_spinners() await pilot.pause(0.1) assert snap_compare( "test_ui_snapshot_parallel_tool_calls.py:ParallelToolCallsApp", terminal_size=(80, 15), run_before=run_before, ) def test_snapshot_parallel_tool_calls_resolved(snap_compare: SnapCompare) -> None: async def run_before(pilot: Pilot) -> None: app = cast(ParallelToolCallsApp, pilot.app) await app.emit_all_tool_calls() await pilot.pause(0.3) await app.resolve_all_results() await pilot.pause(0.3) assert snap_compare( "test_ui_snapshot_parallel_tool_calls.py:ParallelToolCallsApp", terminal_size=(80, 20), run_before=run_before, )