Coverage for src/lilbee/cli/tui/command_registry.py: 100%
34 statements
« prev ^ index » next coverage.py v7.15.2, created at 2026-09-28 17:20 +0000
« prev ^ index » next coverage.py v7.15.2, created at 2026-09-28 17:20 +0000
1"""Single source of truth for TUI slash commands.
3Every slash command is defined once here. All other modules (chat dispatch,
4suggester, help modal, autocomplete) read from this registry.
5"""
7from __future__ import annotations
9from dataclasses import dataclass
12@dataclass(frozen=True)
13class SlashCommand:
14 """Definition of a single slash command."""
16 name: str
17 handler: str
18 aliases: tuple[str, ...] = ()
19 args_hint: str = ""
20 help_text: str = ""
21 # Reachable from the command bar while an answer is streaming. Off by
22 # default: a command that touches the index, the fleet or the transcript
23 # would race the live turn.
24 allowed_while_streaming: bool = False
27COMMANDS: tuple[SlashCommand, ...] = (
28 SlashCommand(
29 "/model",
30 "_cmd_model",
31 aliases=(),
32 args_hint="[name]",
33 help_text="Switch chat model (no arg opens the catalog)",
34 # The chat screen queues a mid-answer switch, so this never cuts a turn.
35 allowed_while_streaming=True,
36 ),
37 SlashCommand(
38 "/add",
39 "_cmd_add",
40 aliases=(),
41 args_hint="<path>",
42 help_text="Add file or folder to the knowledge base",
43 ),
44 SlashCommand(
45 "/crawl",
46 "_cmd_crawl",
47 aliases=(),
48 args_hint="[url]",
49 help_text="Crawl a URL (no arg opens the dialog)",
50 ),
51 SlashCommand(
52 "/delete",
53 "_cmd_delete",
54 aliases=(),
55 args_hint="<name>",
56 help_text="Remove a document from the index",
57 ),
58 SlashCommand(
59 "/prune-ignored",
60 "_cmd_prune_ignored",
61 aliases=(),
62 args_hint="",
63 help_text="Drop indexed documents a .lilbeeignore now excludes",
64 ),
65 SlashCommand(
66 "/set",
67 "_cmd_set",
68 aliases=(),
69 args_hint="<key> <value>",
70 help_text="Change a setting",
71 ),
72 SlashCommand(
73 "/theme",
74 "_cmd_theme",
75 aliases=(),
76 args_hint="[name]",
77 help_text="Switch theme (no arg opens the theme list)",
78 ),
79 SlashCommand(
80 "/reset",
81 "_cmd_reset",
82 help_text="Factory reset (asks for confirmation)",
83 ),
84 SlashCommand(
85 "/rebuild",
86 "_cmd_rebuild",
87 help_text="Re-index the documents directory from scratch (asks for confirmation)",
88 ),
89 SlashCommand(
90 "/export",
91 "_cmd_export",
92 aliases=(),
93 args_hint="<path>",
94 help_text="Export a per-page text dataset (parquet or jsonl)",
95 ),
96 SlashCommand(
97 "/import",
98 "_cmd_import",
99 aliases=(),
100 args_hint="<path>",
101 help_text="Import a per-page text dataset, re-embedding it",
102 ),
103 SlashCommand("/status", "_cmd_status", help_text="Show knowledge-base status"),
104 SlashCommand("/settings", "_cmd_settings", help_text="Open settings"),
105 SlashCommand(
106 "/models",
107 "_cmd_catalog",
108 aliases=("/m", "/catalog"),
109 help_text="Browse the model catalog",
110 ),
111 SlashCommand(
112 "/remember",
113 "_cmd_remember",
114 args_hint="<text>",
115 help_text="Save a memory (prefix with 'pref:' for a preference)",
116 ),
117 SlashCommand(
118 "/memories",
119 "_cmd_memories",
120 help_text="Browse and manage your saved memories",
121 ),
122 SlashCommand(
123 "/wiki",
124 "_cmd_wiki",
125 help_text="Open the wiki view",
126 ),
127 SlashCommand(
128 "/remove",
129 "_cmd_remove",
130 aliases=(),
131 args_hint="<name>",
132 help_text="Uninstall a downloaded model",
133 ),
134 SlashCommand(
135 "/login",
136 "_cmd_login",
137 args_hint="[token]",
138 help_text="Log in to Hugging Face (no arg opens the token page)",
139 ),
140 SlashCommand("/help", "_cmd_help", aliases=("/h",), help_text="Show the slash-command catalog"),
141 SlashCommand("/version", "_cmd_version", help_text="Show the lilbee version"),
142 SlashCommand(
143 "/cancel",
144 "_cmd_cancel",
145 help_text="Cancel any in-flight operations",
146 # Stopping the live turn is the point, so it must reach a live stream.
147 allowed_while_streaming=True,
148 ),
149 SlashCommand("/clear", "_cmd_clear", help_text="Clear the conversation"),
150 SlashCommand("/sessions", "_cmd_sessions", help_text="Toggle the sessions drawer"),
151 SlashCommand(
152 "/fork",
153 "_cmd_fork",
154 help_text="Branch this conversation into a new session after any answer",
155 ),
156 SlashCommand(
157 "/export-chat",
158 "_cmd_export_chat",
159 args_hint="[path]",
160 help_text="Export this conversation as markdown (default: the current directory)",
161 ),
162 SlashCommand("/quit", "_cmd_quit", aliases=("/q", "/exit"), help_text="Exit lilbee"),
163)
166def build_dispatch_dict() -> dict[str, str]:
167 """Build a mapping from command name (and aliases) to handler method name."""
168 dispatch: dict[str, str] = {}
169 for cmd in COMMANDS:
170 dispatch[cmd.name] = cmd.handler
171 for alias in cmd.aliases:
172 dispatch[alias] = cmd.handler
173 return dispatch
176def get_command(name: str) -> SlashCommand:
177 """Return the registry entry whose name is exactly *name* (aliases excluded)."""
178 for cmd in COMMANDS:
179 if cmd.name == name:
180 return cmd
181 raise KeyError(name)
184def runs_while_streaming(name: str) -> bool:
185 """Whether *name* (command or alias) may be submitted mid-answer.
187 Unknown names are False so the streaming gate keeps rejecting them; the
188 unknown-command toast is the caller's job.
189 """
190 for cmd in COMMANDS:
191 if name == cmd.name or name in cmd.aliases:
192 return cmd.allowed_while_streaming
193 return False
196def completion_names() -> tuple[str, ...]:
197 """All command names including aliases, for tab completion."""
198 names: list[str] = []
199 for cmd in COMMANDS:
200 names.append(cmd.name)
201 names.extend(cmd.aliases)
202 return tuple(names)