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

36 statements  

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

1"""Single source of truth for TUI slash commands. 

2 

3Every slash command is defined once here. All other modules (chat dispatch, 

4suggester, help modal, autocomplete) read from this registry. 

5""" 

6 

7from __future__ import annotations 

8 

9from dataclasses import dataclass 

10 

11 

12@dataclass(frozen=True) 

13class SlashCommand: 

14 """Definition of a single slash command.""" 

15 

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 

25 

26 

27ANALYZE_OFF_ARG = "off" 

28ANALYZE_REPORT_ARG = "report" 

29 

30COMMANDS: tuple[SlashCommand, ...] = ( 

31 SlashCommand( 

32 "/model", 

33 "_cmd_model", 

34 aliases=(), 

35 args_hint="[name]", 

36 help_text="Switch chat model (no arg opens the catalog)", 

37 # The chat screen queues a mid-answer switch, so this never cuts a turn. 

38 allowed_while_streaming=True, 

39 ), 

40 SlashCommand( 

41 "/add", 

42 "_cmd_add", 

43 aliases=(), 

44 args_hint="<path>", 

45 help_text="Add file or folder to the knowledge base", 

46 ), 

47 SlashCommand( 

48 "/crawl", 

49 "_cmd_crawl", 

50 aliases=(), 

51 args_hint="[url]", 

52 help_text="Crawl a URL (no arg opens the dialog)", 

53 ), 

54 SlashCommand( 

55 "/delete", 

56 "_cmd_delete", 

57 aliases=(), 

58 args_hint="<name>", 

59 help_text="Remove a document from the index", 

60 ), 

61 SlashCommand( 

62 "/prune-ignored", 

63 "_cmd_prune_ignored", 

64 aliases=(), 

65 args_hint="", 

66 help_text="Drop indexed documents a .lilbeeignore now excludes", 

67 ), 

68 SlashCommand( 

69 "/set", 

70 "_cmd_set", 

71 aliases=(), 

72 args_hint="<key> <value>", 

73 help_text="Change a setting; a list setting takes one item here, so use Settings for more", 

74 ), 

75 SlashCommand( 

76 "/theme", 

77 "_cmd_theme", 

78 aliases=(), 

79 args_hint="[name]", 

80 help_text="Switch theme (no arg opens the theme list)", 

81 ), 

82 SlashCommand( 

83 "/profile", 

84 "_cmd_profile", 

85 args_hint="[name]", 

86 help_text="Switch profile (no arg opens the Profile tab in Settings)", 

87 ), 

88 SlashCommand( 

89 "/analyze", 

90 "_cmd_analyze", 

91 args_hint=f"[dir|{ANALYZE_REPORT_ARG}|{ANALYZE_OFF_ARG}]", 

92 help_text=( 

93 f"Recommend a profile for your documents ({ANALYZE_REPORT_ARG} opens the last " 

94 f"report, {ANALYZE_OFF_ARG} hides the tip)" 

95 ), 

96 ), 

97 SlashCommand( 

98 "/reset", 

99 "_cmd_reset", 

100 help_text="Factory reset (asks for confirmation)", 

101 ), 

102 SlashCommand( 

103 "/rebuild", 

104 "_cmd_rebuild", 

105 help_text="Re-index the documents directory from scratch (asks for confirmation)", 

106 ), 

107 SlashCommand( 

108 "/export", 

109 "_cmd_export", 

110 aliases=(), 

111 args_hint="<path>", 

112 help_text="Export a per-page text dataset (parquet or jsonl)", 

113 ), 

114 SlashCommand( 

115 "/import", 

116 "_cmd_import", 

117 aliases=(), 

118 args_hint="<path>", 

119 help_text="Import a per-page text dataset, re-embedding it", 

120 ), 

121 SlashCommand("/status", "_cmd_status", help_text="Show knowledge-base status"), 

122 SlashCommand("/settings", "_cmd_settings", help_text="Open settings"), 

123 SlashCommand( 

124 "/models", 

125 "_cmd_catalog", 

126 aliases=("/m", "/catalog"), 

127 help_text="Browse the model catalog", 

128 ), 

129 SlashCommand( 

130 "/remember", 

131 "_cmd_remember", 

132 args_hint="<text>", 

133 help_text="Save a memory (prefix with 'pref:' for a preference)", 

134 ), 

135 SlashCommand( 

136 "/memories", 

137 "_cmd_memories", 

138 help_text="Browse and manage your saved memories", 

139 ), 

140 SlashCommand( 

141 "/wiki", 

142 "_cmd_wiki", 

143 help_text="Open the wiki view", 

144 ), 

145 SlashCommand( 

146 "/remove", 

147 "_cmd_remove", 

148 aliases=(), 

149 args_hint="<name>", 

150 help_text="Uninstall a downloaded model", 

151 ), 

152 SlashCommand( 

153 "/login", 

154 "_cmd_login", 

155 args_hint="[token]", 

156 help_text="Log in to Hugging Face (no arg opens the token page)", 

157 ), 

158 SlashCommand("/help", "_cmd_help", aliases=("/h",), help_text="Show the slash-command catalog"), 

159 SlashCommand("/version", "_cmd_version", help_text="Show the lilbee version"), 

160 SlashCommand( 

161 "/cancel", 

162 "_cmd_cancel", 

163 help_text="Cancel any in-flight operations", 

164 # Stopping the live turn is the point, so it must reach a live stream. 

165 allowed_while_streaming=True, 

166 ), 

167 SlashCommand("/clear", "_cmd_clear", help_text="Clear the conversation"), 

168 SlashCommand("/sessions", "_cmd_sessions", help_text="Toggle the sessions drawer"), 

169 SlashCommand( 

170 "/fork", 

171 "_cmd_fork", 

172 help_text="Branch this conversation into a new session after any answer", 

173 ), 

174 SlashCommand( 

175 "/export-chat", 

176 "_cmd_export_chat", 

177 args_hint="[path]", 

178 help_text="Export this conversation as markdown (default: the current directory)", 

179 ), 

180 SlashCommand("/quit", "_cmd_quit", aliases=("/q", "/exit"), help_text="Exit lilbee"), 

181) 

182 

183 

184def build_dispatch_dict() -> dict[str, str]: 

185 """Build a mapping from command name (and aliases) to handler method name.""" 

186 dispatch: dict[str, str] = {} 

187 for cmd in COMMANDS: 

188 dispatch[cmd.name] = cmd.handler 

189 for alias in cmd.aliases: 

190 dispatch[alias] = cmd.handler 

191 return dispatch 

192 

193 

194def get_command(name: str) -> SlashCommand: 

195 """Return the registry entry whose name is exactly *name* (aliases excluded).""" 

196 for cmd in COMMANDS: 

197 if cmd.name == name: 

198 return cmd 

199 raise KeyError(name) 

200 

201 

202def runs_while_streaming(name: str) -> bool: 

203 """Whether *name* (command or alias) may be submitted mid-answer. 

204 

205 Unknown names are False so the streaming gate keeps rejecting them; the 

206 unknown-command toast is the caller's job. 

207 """ 

208 for cmd in COMMANDS: 

209 if name == cmd.name or name in cmd.aliases: 

210 return cmd.allowed_while_streaming 

211 return False 

212 

213 

214def completion_names() -> tuple[str, ...]: 

215 """All command names including aliases, for tab completion.""" 

216 names: list[str] = [] 

217 for cmd in COMMANDS: 

218 names.append(cmd.name) 

219 names.extend(cmd.aliases) 

220 return tuple(names)