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

53 statements  

« prev     ^ index     » next       coverage.py v7.13.4, created at 2026-04-29 19:16 +0000

1"""Tab completion for the chat input via Textual's Suggester API.""" 

2 

3from __future__ import annotations 

4 

5from textual.suggester import Suggester 

6 

7from lilbee.cli.settings_map import SETTINGS_MAP 

8from lilbee.cli.tui.app import DARK_THEMES 

9from lilbee.cli.tui.command_registry import completion_names 

10from lilbee.services import get_services 

11 

12_SLASH_COMMANDS = completion_names() 

13 

14 

15class SlashSuggester(Suggester): 

16 """Context-aware suggestions for the chat input. 

17 Suggests slash command names when input starts with '/'. 

18 Suggests argument values for commands that take them. 

19 """ 

20 

21 async def get_suggestion(self, value: str) -> str | None: 

22 if not value: 

23 return None 

24 

25 if value.startswith("/") and " " not in value: 

26 return self._suggest_command(value) 

27 

28 if " " in value: 

29 return self._suggest_argument(value) 

30 

31 return None 

32 

33 def _suggest_command(self, prefix: str) -> str | None: 

34 for cmd in _SLASH_COMMANDS: 

35 if cmd.startswith(prefix) and cmd != prefix: 

36 return cmd 

37 return None 

38 

39 def _suggest_argument(self, value: str) -> str | None: 

40 cmd, _, partial = value.partition(" ") 

41 cmd = cmd.lower() 

42 

43 if cmd == "/model": 

44 return self._suggest_from_list(value, partial, self._get_model_names()) 

45 if cmd == "/set": 

46 return self._suggest_from_list(value, partial, self._get_setting_names()) 

47 if cmd == "/delete": 

48 return self._suggest_from_list(value, partial, self._get_document_names()) 

49 if cmd == "/theme": 

50 return self._suggest_from_list(value, partial, self._get_theme_names()) 

51 return None 

52 

53 def _suggest_from_list(self, full: str, partial: str, options: list[str]) -> str | None: 

54 for opt in options: 

55 if opt.startswith(partial) and opt != partial: 

56 return full[: len(full) - len(partial)] + opt 

57 return None 

58 

59 def _get_model_names(self) -> list[str]: 

60 try: 

61 from lilbee.models import list_installed_models 

62 

63 return list_installed_models() 

64 except Exception: 

65 return [] 

66 

67 def _get_setting_names(self) -> list[str]: 

68 return list(SETTINGS_MAP.keys()) 

69 

70 def _get_document_names(self) -> list[str]: 

71 try: 

72 sources = get_services().store.get_sources() 

73 return [s.get("filename", s.get("source", "")) for s in sources] # pragma: no cover 

74 except Exception: 

75 return [] 

76 

77 def _get_theme_names(self) -> list[str]: 

78 return list(DARK_THEMES)