Coverage for src/lilbee/cli/tui/widgets/fleet_drawer.py: 100%

33 statements  

« prev     ^ index     » next       coverage.py v7.15.2, created at 2026-08-14 11:46 +0000

1"""Fleet side drawer: FleetBody docked right so chat stays live on the left.""" 

2 

3from __future__ import annotations 

4 

5from pathlib import Path 

6from typing import ClassVar 

7 

8from textual.app import ComposeResult 

9from textual.binding import Binding, BindingType 

10from textual.screen import Screen 

11 

12from lilbee.cli.tui.widgets.drawer import Drawer 

13from lilbee.cli.tui.widgets.fleet_body import FleetBody 

14 

15_CSS_FILE = Path(__file__).parent / "fleet_drawer.tcss" 

16_DRAWER_CSS = _CSS_FILE.read_text(encoding="utf-8") 

17 

18# CSS class the drawer sets on its host screen so app.tcss insets the docked 

19# top/bottom bars to the space left of the drawer. 

20_HOST_OPEN_CLASS = "fleet-open" 

21 

22 

23class FleetDrawer(Drawer): 

24 """GPU fleet panel as a non-modal right-side drawer; closed with esc or ctrl+g. 

25 

26 Docked to the right so the screen underneath reflows to the left and stays 

27 interactive -- the chat prompt keeps working while the live GPU bars move in 

28 the drawer. The preview/apply/clear keys are scoped here, so they fire only 

29 while focus is inside the drawer, never from the chat prompt. 

30 """ 

31 

32 DEFAULT_CSS: ClassVar[str] = _DRAWER_CSS 

33 

34 BINDINGS: ClassVar[list[BindingType]] = [ 

35 Binding("escape", "close", "Close", show=True), 

36 # priority so a focused toggle child doesn't swallow ctrl+r/s/x 

37 Binding("ctrl+r", "preview", "Preview", show=True, priority=True), 

38 Binding("ctrl+s", "apply", "Apply", show=True, priority=True), 

39 Binding("ctrl+x", "clear", "Auto", show=True, priority=True), 

40 ] 

41 

42 def __init__(self) -> None: 

43 super().__init__(id="fleet-drawer") 

44 self._host: Screen[object] | None = None 

45 

46 def compose(self) -> ComposeResult: 

47 yield FleetBody() 

48 

49 # No on_mount focus grab: opening the drawer must NOT steal focus from the 

50 # chat prompt, so you can keep typing while it stays open. Click a toggle (or 

51 # tab) to focus the drawer; ctrl+g closes it from anywhere, esc when focused. 

52 

53 def on_mount(self) -> None: 

54 """Inset the host screen's docked bars so the drawer sits beside them.""" 

55 self._host = self.screen 

56 self._host.add_class(_HOST_OPEN_CLASS) 

57 

58 def on_unmount(self) -> None: 

59 """Restore the bars to full width when the drawer closes by any path.""" 

60 if self._host is not None: 

61 self._host.remove_class(_HOST_OPEN_CLASS) 

62 

63 def action_close(self) -> None: 

64 """Remove the drawer, returning full width to the screen underneath.""" 

65 self.remove() 

66 

67 def action_preview(self) -> None: 

68 """Delegate preview (ctrl+r) to the hosted FleetBody.""" 

69 self.query_one(FleetBody).action_preview() 

70 

71 def action_apply(self) -> None: 

72 """Delegate apply (ctrl+s) to the hosted FleetBody.""" 

73 self.query_one(FleetBody).action_apply() 

74 

75 def action_clear(self) -> None: 

76 """Delegate clear/auto (ctrl+x) to the hosted FleetBody.""" 

77 self.query_one(FleetBody).action_clear()