Coverage for src / lilbee / cli / tui / commands.py: 100%
80 statements
« prev ^ index » next coverage.py v7.13.4, created at 2026-06-28 01:01 +0000
« prev ^ index » next coverage.py v7.13.4, created at 2026-06-28 01:01 +0000
1"""Command palette provider for lilbee TUI."""
3from __future__ import annotations
5import logging
6from typing import TYPE_CHECKING, Any, cast
8from textual.command import Hit, Hits, Provider
10from lilbee.app.services import get_services
11from lilbee.cli.tui import messages as msg
12from lilbee.core.config import cfg
14log = logging.getLogger(__name__)
16if TYPE_CHECKING:
17 from lilbee.cli.tui.app import LilbeeApp
20class LilbeeCommandProvider(Provider):
21 """Provides searchable commands for the Textual command palette (Ctrl+P)."""
23 @property
24 def _app(self) -> LilbeeApp:
25 return cast("LilbeeApp", self.screen.app)
27 async def search(self, query: str) -> Hits:
28 matcher = self.matcher(query)
29 for cmd_text, help_text, action in self._get_commands():
30 score = matcher.match(cmd_text)
31 if score > 0:
32 yield Hit(score, matcher.highlight(cmd_text), action, help=help_text)
34 async def discover(self) -> Hits:
35 for cmd_text, help_text, action in self._get_commands():
36 yield Hit(1.0, cmd_text, action, help=help_text)
38 def _get_commands(self) -> list[tuple[str, str, Any]]:
39 app = self._app
40 commands: list[tuple[str, str, Any]] = [
41 ("Open catalog", "Browse and install models", lambda: app.switch_view("Catalog")),
42 ("Run setup wizard", "Configure chat and embedding models", self._action_setup),
43 ("Open status", "Knowledge base status", lambda: app.switch_view("Status")),
44 ("Open settings", "View and change settings", lambda: app.switch_view("Settings")),
45 ("Open task center", "Monitor background tasks", lambda: app.switch_view("Tasks")),
46 ("Help", "Show keybinding reference", app.action_push_help),
47 ("Cycle theme", "Switch to next color theme", app.action_cycle_theme),
48 ("Sync documents", "Sync knowledge base", self._action_sync),
49 (
50 "Retry skipped documents",
51 "Re-attempt files that failed a previous sync",
52 self._action_retry_skipped,
53 ),
54 ("Open wiki", "Browse and generate wiki pages", self._action_open_wiki),
55 ("Show version", "Display lilbee version", self._action_version),
56 (
57 "Reset knowledge base",
58 "Delete all data (asks for confirmation)",
59 self._action_reset,
60 ),
61 ("Quit", "Exit lilbee", app.action_quit),
62 ]
64 commands.extend(self._model_commands())
65 commands.extend(self._document_commands())
66 return commands
68 def _model_commands(self) -> list[tuple[str, str, Any]]:
69 """Generate commands for installed models."""
70 commands: list[tuple[str, str, Any]] = []
71 try:
72 from lilbee.modelhub.models import list_installed_models
74 for name in list_installed_models():
75 commands.append(
76 (
77 f"Set chat model → {name}",
78 "Switch chat model",
79 lambda n=name: self._set_model("chat_model", n),
80 )
81 )
82 except Exception:
83 log.debug("Failed to list installed models", exc_info=True)
85 return commands
87 def _document_commands(self) -> list[tuple[str, str, Any]]:
88 """Generate commands for indexed documents."""
89 commands: list[tuple[str, str, Any]] = []
90 try:
91 for src in get_services().store.get_sources():
92 name = src.get("filename", src.get("source", ""))
93 if name:
94 commands.append(
95 (
96 f"Delete document → {name}",
97 f"Remove {name} from index",
98 lambda n=name: self._delete_doc(n),
99 )
100 )
101 except Exception:
102 log.debug("Failed to list documents", exc_info=True)
103 return commands
105 def _set_model(self, attr: str, value: str) -> None:
106 # Route through LilbeeApp.set_active_model so model-bar / scope chip
107 # / status bar subscribers (settings_changed_signal) refresh.
108 app = self._app
109 app.set_active_model(attr, value)
110 display = value or "off"
111 app.notify(f"{attr}: {display}")
112 if attr == "chat_model":
113 app.title = f"lilbee: {value}"
115 def _delete_doc(self, name: str) -> None:
116 get_services().store.remove_documents([name])
117 self.screen.app.notify(f"Deleted {name}")
119 def _action_sync(self) -> None:
120 self._app.action_run_sync()
122 def _action_retry_skipped(self) -> None:
123 """Clear the failed-file markers and kick off a sync to retry them.
125 Clearing the marker cache and then running a normal sync is
126 equivalent to ``lilbee sync --retry-skipped`` / ``POST /api/sync``
127 with ``retry_skipped=true``.
128 """
129 from lilbee.data.ingest.skip_marker import clear_skip_markers, load_skip_markers
131 cleared = len(load_skip_markers(cfg.data_root))
132 clear_skip_markers(cfg.data_root)
133 self.screen.app.notify(msg.retry_skipped_message(cleared))
134 self._app.action_run_sync()
136 def _action_version(self) -> None:
137 from lilbee.app.version import get_version
139 self.screen.app.notify(f"lilbee {get_version()}")
141 def _action_setup(self) -> None:
142 from lilbee.cli.tui.screens.setup import SetupWizard
144 self.screen.app.push_screen(SetupWizard())
146 def _action_open_wiki(self) -> None:
147 self._app.switch_view("Wiki")
149 def _action_reset(self) -> None:
150 """Trigger /reset from the palette so the ConfirmDialog flow fires."""
151 from lilbee.cli.tui.screens.chat import ChatScreen
153 app = self._app
154 chat = next((s for s in app.screen_stack if isinstance(s, ChatScreen)), None)
155 if chat is None:
156 app.notify("Open Chat to run /reset")
157 return
158 chat._cmd_reset("")