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

76 statements  

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

1"""Command palette provider for lilbee TUI.""" 

2 

3from __future__ import annotations 

4 

5import logging 

6from typing import TYPE_CHECKING, Any 

7 

8from textual.command import Hit, Hits, Provider 

9 

10from lilbee import settings 

11from lilbee.config import cfg 

12from lilbee.services import get_services 

13 

14log = logging.getLogger(__name__) 

15 

16if TYPE_CHECKING: 

17 from lilbee.cli.tui.app import LilbeeApp 

18 

19 

20class LilbeeCommandProvider(Provider): 

21 """Provides searchable commands for the Textual command palette (Ctrl+P).""" 

22 

23 @property 

24 def _app(self) -> LilbeeApp: 

25 from lilbee.cli.tui.app import LilbeeApp 

26 

27 if not isinstance(self.screen.app, LilbeeApp): # test apps aren't LilbeeApp 

28 raise TypeError(f"Expected LilbeeApp, got {type(self.screen.app).__name__}") 

29 return self.screen.app 

30 

31 async def search(self, query: str) -> Hits: 

32 matcher = self.matcher(query) 

33 for cmd_text, help_text, action in self._get_commands(): 

34 score = matcher.match(cmd_text) 

35 if score > 0: 

36 yield Hit(score, matcher.highlight(cmd_text), action, help=help_text) 

37 

38 async def discover(self) -> Hits: 

39 for cmd_text, help_text, action in self._get_commands(): 

40 yield Hit(1.0, cmd_text, action, help=help_text) 

41 

42 def _get_commands(self) -> list[tuple[str, str, Any]]: 

43 app = self._app 

44 commands: list[tuple[str, str, Any]] = [ 

45 ("Open catalog", "Browse and install models", lambda: app.switch_view("Catalog")), 

46 ("Run setup wizard", "Configure chat and embedding models", self._action_setup), 

47 ("Open status", "Knowledge base status", lambda: app.switch_view("Status")), 

48 ("Open settings", "View and change settings", lambda: app.switch_view("Settings")), 

49 ("Open task center", "Monitor background tasks", lambda: app.switch_view("Tasks")), 

50 ("Help", "Show keybinding reference", app.action_push_help), 

51 ("Cycle theme", "Switch to next color theme", app.action_cycle_theme), 

52 ("Sync documents", "Sync knowledge base", self._action_sync), 

53 ("Open wiki", "Browse and generate wiki pages", self._action_open_wiki), 

54 ("Show version", "Display lilbee version", self._action_version), 

55 ( 

56 "Reset knowledge base", 

57 "Delete all data (requires /reset confirm)", 

58 self._action_noop, 

59 ), 

60 ("Quit", "Exit lilbee", app.action_quit), 

61 ] 

62 

63 commands.extend(self._model_commands()) 

64 commands.extend(self._document_commands()) 

65 return commands 

66 

67 def _model_commands(self) -> list[tuple[str, str, Any]]: 

68 """Generate commands for installed models.""" 

69 commands: list[tuple[str, str, Any]] = [] 

70 try: 

71 from lilbee.models import list_installed_models 

72 

73 for name in list_installed_models(): 

74 commands.append( 

75 ( 

76 f"Set chat model → {name}", 

77 "Switch chat model", 

78 lambda n=name: self._set_model("chat_model", n), 

79 ) 

80 ) 

81 except Exception: 

82 log.debug("Failed to list installed models", exc_info=True) 

83 

84 return commands 

85 

86 def _document_commands(self) -> list[tuple[str, str, Any]]: 

87 """Generate commands for indexed documents.""" 

88 commands: list[tuple[str, str, Any]] = [] 

89 try: 

90 for src in get_services().store.get_sources(): 

91 name = src.get("filename", src.get("source", "")) 

92 if name: 

93 commands.append( 

94 ( 

95 f"Delete document → {name}", 

96 f"Remove {name} from index", 

97 lambda n=name: self._delete_doc(n), 

98 ) 

99 ) 

100 except Exception: 

101 log.debug("Failed to list documents", exc_info=True) 

102 return commands 

103 

104 def _set_model(self, attr: str, value: str) -> None: 

105 setattr(cfg, attr, value) 

106 settings.set_value(cfg.data_root, attr, value) 

107 display = value or "off" 

108 self.screen.app.notify(f"{attr}: {display}") 

109 if attr == "chat_model": 

110 self.screen.app.title = f"lilbee — {value}" 

111 

112 def _delete_doc(self, name: str) -> None: 

113 store = get_services().store 

114 store.delete_by_source(name) 

115 store.delete_source(name) 

116 self.screen.app.notify(f"Deleted {name}") 

117 

118 def _action_sync(self) -> None: 

119 self.screen.app.notify("Use /add <path> or auto-sync on launch") 

120 

121 def _action_version(self) -> None: 

122 from lilbee.cli.helpers import get_version 

123 

124 self.screen.app.notify(f"lilbee {get_version()}") 

125 

126 def _action_setup(self) -> None: 

127 from lilbee.cli.tui.screens.setup import SetupWizard 

128 

129 self.screen.app.push_screen(SetupWizard()) 

130 

131 def _action_open_wiki(self) -> None: 

132 from lilbee.cli.tui.app import LilbeeApp 

133 

134 app = self.screen.app 

135 if isinstance(app, LilbeeApp): # test apps aren't LilbeeApp 

136 app.switch_view("Wiki") 

137 

138 def _action_noop(self) -> None: 

139 self.screen.app.notify("Type '/reset confirm' in chat to reset")