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

74 statements  

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

1"""Status rows that mirror the active slash command's signature, and the analyze tip for /add.""" 

2 

3from __future__ import annotations 

4 

5import textwrap 

6from pathlib import Path 

7from typing import ClassVar 

8 

9from textual.content import Content 

10from textual.widgets import Static 

11 

12from lilbee.app.analyze import tip_shows 

13from lilbee.cli.tui import messages as msg 

14from lilbee.cli.tui.command_registry import COMMANDS 

15from lilbee.cli.tui.thread_safe import call_from_thread 

16from lilbee.core.config import cfg 

17 

18_CSS_FILE = Path(__file__).parent / "arg_hint.tcss" 

19 

20_REGISTRY = {cmd.name: cmd for cmd in COMMANDS} 

21for _cmd in COMMANDS: 

22 for _alias in _cmd.aliases: 

23 _REGISTRY[_alias] = _cmd 

24 

25TIP_COMMAND = "/add" 

26_TIP_GROUP = "analyze-tip" 

27_TIP_GAP = 2 # cells between the tip label and its text 

28_TIP_MIN_WIDTH = 20 # before the first layout the width reads 0 

29 

30 

31class ArgHintLine(Static): 

32 """Reactive hint that mirrors the active slash command's signature. 

33 

34 Under ``/add`` it adds the analyze tip while the project should see it, before ingest starts. 

35 """ 

36 

37 DEFAULT_CSS: ClassVar[str] = _CSS_FILE.read_text(encoding="utf-8") 

38 

39 def __init__(self, *, id: str | None = None) -> None: 

40 super().__init__("", id=id) 

41 self.display = False 

42 self._text = "" 

43 self._tip = False 

44 

45 def update_for_input(self, text: str) -> None: 

46 """Render the appropriate hint for the current chat input contents.""" 

47 if _command(text) == TIP_COMMAND and _command(self._text) != TIP_COMMAND: 

48 self.refresh_tip() 

49 self._text = text 

50 self._render_hint() 

51 

52 def refresh_tip(self) -> None: 

53 """Re-read off the loop whether the project should see the analyze tip.""" 

54 self.run_worker(self._read_tip, thread=True, group=_TIP_GROUP, exclusive=True) 

55 

56 def _read_tip(self) -> None: 

57 call_from_thread(self, self._set_tip, tip_shows(cfg.data_root)) 

58 

59 def _set_tip(self, shows: bool) -> None: 

60 self._tip = shows 

61 self._render_hint() 

62 

63 def on_resize(self) -> None: 

64 """Wrap the tip again for the new width.""" 

65 if self._tip: 

66 self._render_hint() 

67 

68 def _render_hint(self) -> None: 

69 rendered = _hint_for(self._text) 

70 if rendered is None: 

71 self.update("") 

72 self.display = False 

73 return 

74 if self._tip and _command(self._text) == TIP_COMMAND: 

75 rendered = Content("\n").join([rendered, _tip_lines(self.content_size.width)]) 

76 self.update(rendered) 

77 self.display = True 

78 

79 

80def _command(text: str) -> str | None: 

81 """The slash command *text* starts with, once a space ends the command name.""" 

82 command, space, _argument = text.partition(" ") 

83 if not command.startswith("/") or not space: 

84 return None 

85 return command.lower() 

86 

87 

88def _tip_lines(width: int) -> Content: 

89 """The tip in *width* cells, each wrapped line indented under the first line's text.""" 

90 label = f" {msg.ANALYZE_TIP_LABEL}" 

91 indent = " " * (len(label) + _TIP_GAP) 

92 wrapped = textwrap.wrap(msg.ANALYZE_TIP, width=max(width - len(indent), _TIP_MIN_WIDTH)) 

93 first, *rest = wrapped 

94 lines = [ 

95 Content.assemble( 

96 Content.styled(label, "$accent"), Content.styled(" " * _TIP_GAP + first, "$text-muted") 

97 ), 

98 *(Content.styled(indent + line, "$text-muted") for line in rest), 

99 ] 

100 return Content("\n").join(lines) 

101 

102 

103def _hint_for(text: str) -> Content | None: 

104 """Build the hint content for *text*, or ``None`` when nothing should show.""" 

105 command = _command(text) 

106 cmd = None if command is None else _REGISTRY.get(command) 

107 if cmd is None: 

108 return None 

109 

110 name_part = Content.styled(f" {cmd.name}", "$success") 

111 args_part = Content.styled(f" {cmd.args_hint}", "$text-muted") if cmd.args_hint else Content("") 

112 sep = Content.styled(" · ", "$text-muted") 

113 help_part = Content.styled(cmd.help_text, "$text-muted") 

114 return Content.assemble(name_part, args_part, sep, help_part)