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
« 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."""
3from __future__ import annotations
5from pathlib import Path
6from typing import ClassVar
8from textual.app import ComposeResult
9from textual.binding import Binding, BindingType
10from textual.screen import Screen
12from lilbee.cli.tui.widgets.drawer import Drawer
13from lilbee.cli.tui.widgets.fleet_body import FleetBody
15_CSS_FILE = Path(__file__).parent / "fleet_drawer.tcss"
16_DRAWER_CSS = _CSS_FILE.read_text(encoding="utf-8")
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"
23class FleetDrawer(Drawer):
24 """GPU fleet panel as a non-modal right-side drawer; closed with esc or ctrl+g.
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 """
32 DEFAULT_CSS: ClassVar[str] = _DRAWER_CSS
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 ]
42 def __init__(self) -> None:
43 super().__init__(id="fleet-drawer")
44 self._host: Screen[object] | None = None
46 def compose(self) -> ComposeResult:
47 yield FleetBody()
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.
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)
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)
63 def action_close(self) -> None:
64 """Remove the drawer, returning full width to the screen underneath."""
65 self.remove()
67 def action_preview(self) -> None:
68 """Delegate preview (ctrl+r) to the hosted FleetBody."""
69 self.query_one(FleetBody).action_preview()
71 def action_apply(self) -> None:
72 """Delegate apply (ctrl+s) to the hosted FleetBody."""
73 self.query_one(FleetBody).action_apply()
75 def action_clear(self) -> None:
76 """Delegate clear/auto (ctrl+x) to the hosted FleetBody."""
77 self.query_one(FleetBody).action_clear()