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

713 statements  

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

1"""Centralized user-facing messages for the TUI. 

2 

3ALL user-facing text MUST be defined here. Inline strings in 

4screens and widgets are forbidden -- this enables future i18n 

5and ensures consistent messaging. 

6""" 

7 

8from __future__ import annotations 

9 

10import logging 

11from functools import lru_cache 

12from importlib.util import find_spec 

13from pathlib import Path 

14 

15from lilbee.app.profiles import ProfileEffect, ProfileStatus, RebuildReason 

16from lilbee.core.config import cfg 

17from lilbee.core.profile_files import NameProblem, ProfileFolder 

18from lilbee.data.types import SyncResult 

19from lilbee.providers.fleet.gpu_backends import IntelHintKind, IntelUtilHint 

20from lilbee.runtime.progress import OcrBackendUsed 

21from lilbee.wiki.shared import WIKI_TYPE_HEADINGS as _WIKI_TYPE_HEADINGS 

22 

23log = logging.getLogger(__name__) 

24 

25 

26def app_title(model: str) -> str: 

27 """The window title showing the active chat model. Single source so every 

28 code path that sets the title uses the same format.""" 

29 return f"lilbee: {model}" if model else "lilbee" 

30 

31 

32CMD_UNKNOWN = "Unknown command: {cmd}" 

33CMD_ADD_NOT_FOUND = "Not found: {path}" 

34CMD_ADD_SUCCESS = "Added {count} file(s), syncing..." 

35CMD_ADD_RELOCATED = "{count} already indexed, location changed: relinked (no re-embed)." 

36CMD_ADD_DUPLICATE_TITLE = "File already in knowledge base" 

37CMD_ADD_DUPLICATE_MESSAGE = "{name} is already in the knowledge base. Overwrite and re-sync?" 

38CMD_ADD_SKIPPED_DUPLICATE = "Kept existing copy of {name}." 

39CMD_ADD_NAME_TAKEN = "The name {name} is taken by another source (use --force to overwrite)." 

40CMD_ADD_TRACKED = "Already tracked: {names}. Syncing." 

41CMD_ADD_OVERLAPPING = "Overlaps a registered source: {names}. Syncing." 

42CMD_ADD_CONTAINING = "Not added, it contains a source lilbee already indexes: {names}." 

43CMD_ADD_ABSORBED = "{parent} now includes the source {children}." 

44CMD_ADD_NOTHING = "Nothing to add: no path reached the knowledge base." 

45CMD_ADD_ERROR = "Error: {error}" 

46CMD_CRAWL_USAGE = "Usage: /crawl <url> [--depth N] [--max-pages N]" 

47CMD_CRAWL_FLAG_NOT_A_NUMBER = "{flag} must be a whole number" 

48CMD_CRAWL_STARTED = "Crawling {url}..." 

49CMD_CRAWL_PAGE = "Crawling [{current}/{total}]: {url}" 

50CMD_CRAWL_PAGE_INDETERMINATE = "Crawling... ({current} pages so far): {url}" 

51MODEL_REASON_DEFAULT = "it could not be resolved" 

52MODEL_FALLBACK_NOTICE = ( 

53 "{label} model {original!r} is unavailable ({reason}); using {effective!r} for this session. " 

54 "Pick a different model or restore the original to clear this notice." 

55) 

56MODEL_FALLBACK_FAILED = ( 

57 "{label} model {original!r} is unavailable ({reason}) and the fallback {effective!r} was " 

58 "rejected; keeping {original!r}. Pick a working {label} model in settings." 

59) 

60MODEL_ENV_PIN_UNUSABLE = ( 

61 "{label} model {original!r} is unavailable ({reason}). {env_var} sets it, so lilbee keeps it. " 

62 "Unset {env_var} or point it at an installed model." 

63) 

64MODEL_CLI_PIN_UNUSABLE = ( 

65 "{label} model {original!r} is unavailable ({reason}). The --model flag sets it, so lilbee " 

66 "keeps it. Pass a different model to --model or drop the flag." 

67) 

68MODEL_UNUSABLE_NO_FALLBACK = ( 

69 "{label} model {original!r} is unavailable ({reason}) and nothing is installed to fall back " 

70 "to. Pick one from the catalog." 

71) 

72MODEL_ADOPTED_LOG = "{label} model: using installed {effective!r}." 

73CMD_CRAWL_SUCCESS = "Crawled {count} page(s) from {url}" 

74CMD_CRAWL_PAGES_FAILED = "{count} page(s) failed; last: {reason}" 

75CMD_CRAWL_FAILED = "Crawl failed: {error}" 

76CMD_CRAWL_SYNCING = "Syncing crawled pages..." 

77SETUP_CHROMIUM_NAME = "Install Chromium browser" 

78SETUP_BROWSER_WARM_UP = "Start the browser" 

79SETUP_CHROMIUM_FAILED = "Chromium install failed: {error}" 

80SETUP_CHROMIUM_DETAIL = "chromium: {done}/{total} MB" 

81SETUP_CHROMIUM_DETAIL_UNKNOWN = "chromium: {done} MB" 

82SETUP_CHROMIUM_CLI_PROGRESS = " chromium: {pct}%" 

83SYNC_FAILED_FILES = "Sync failed for {files}" 

84SYNC_FAILED_TOAST = ( 

85 "Sync failed for {count} file(s). Press Esc to leave the prompt. " 

86 "Then press t to see the reason. Fix it, then press S to sync again." 

87) 

88SYNC_SKIPPED_NO_VISION = ( 

89 "Skipped (no text extracted): {files}. " 

90 "Configure a vision_model in Settings to OCR scanned PDFs." 

91) 

92SYNC_SKIPPED_OCR_OFF = ( 

93 "Skipped (no text extracted, OCR is off): {files}. " 

94 "Set ocr to auto in Settings to read scanned pages." 

95) 

96# Picker labels for settings whose raw values read poorly, keyed by setting then value. 

97SETTING_CHOICE_LABELS: dict[str, dict[str, str]] = { 

98 "ocr": {"auto": "Read", "all": "Read every page", "off": "Skip"}, 

99 "flash_attention": {"True": "On", "False": "Off"}, 

100} 

101# The blank row of a Select for a setting that can also be unset. 

102SETTING_AUTO_LABEL = "Auto" 

103SYNC_SKIPPED_VISION_FAILED = ( 

104 "Skipped (vision OCR returned no text): {files}. See {log_path} for the underlying error." 

105) 

106SYNC_HELD_OUT = ( 

107 '{count} file(s) stay held out of the index. Run "Retry skipped documents" to index them again.' 

108) 

109SYNC_UNREADABLE_FOLDER = "Could not open folder: {folder}" 

110STATUS_HELD_OUT_TITLE = "Held out of the index" 

111STATUS_HELD_OUT_EMPTY = "No files are held out." 

112STATUS_HELD_OUT_MORE = "{count} more held out" 

113CMD_RETRY_SKIPPED_NONE = "No failed files to retry; running a normal sync." 

114CMD_RETRY_SKIPPED_SOME = "Retrying {count} failed file(s); removed sources stay removed." 

115CMD_PRUNE_IGNORED_NONE = "Nothing indexed matches your ignore patterns." 

116CMD_PRUNE_IGNORED_SOME = "Dropped {count} document(s) your ignore patterns exclude." 

117 

118 

119def sync_skipped_message(result: SyncResult, log_path: Path) -> str: 

120 """The skipped-files message for the OCR the skipped files' extraction ran with. 

121 

122 OCR off names the setting; vision OCR that returned nothing points at 

123 *log_path* (the running process's own log file); otherwise (Tesseract, or 

124 no OCR involved) the advice is a vision model. 

125 """ 

126 files = ", ".join(result.skipped) 

127 backends = {report.backend for report in result.skipped_ocr.values()} 

128 if OcrBackendUsed.NONE in backends: 

129 return SYNC_SKIPPED_OCR_OFF.format(files=files) 

130 if OcrBackendUsed.VISION in backends: 

131 return SYNC_SKIPPED_VISION_FAILED.format(files=files, log_path=log_path) 

132 return SYNC_SKIPPED_NO_VISION.format(files=files) 

133 

134 

135def retry_skipped_message(count: int) -> str: 

136 """Toast for the 'Retry skipped documents' command.""" 

137 return CMD_RETRY_SKIPPED_NONE if count == 0 else CMD_RETRY_SKIPPED_SOME.format(count=count) 

138 

139 

140def prune_ignored_message(count: int) -> str: 

141 """Toast for the '/prune-ignored' command.""" 

142 return CMD_PRUNE_IGNORED_NONE if count == 0 else CMD_PRUNE_IGNORED_SOME.format(count=count) 

143 

144 

145CMD_DELETE_NO_DOCS = "No documents indexed" 

146CMD_DELETE_READ_FAILED = "Could not read the document list" 

147CMD_DELETE_USAGE = "Documents: {names}\nUsage: /delete <filename>" 

148CMD_DELETE_NOT_FOUND = "Not found: {name}" 

149CMD_DELETE_SUGGESTION = "Did you mean {name}?" 

150CMD_DELETE_SUCCESS = "Deleted {name}" 

151CMD_REMEMBER_USAGE = "Usage: /remember <text> (prefix with 'pref:' for a preference)" 

152CMD_REMEMBER_SUCCESS = "Remembered ({kind})." 

153CMD_REMEMBER_NO_EMBED = "No embedding model. Press m and install one to save memories." 

154MEMORY_AUTO_EXTRACTED = "Noted {count} memory(s) to review in /memories" 

155CMD_EXPORT_USAGE = "Usage: /export <path.parquet|path.jsonl>" 

156CMD_EXPORT_SUCCESS = "Exported {pages} page(s) to {output}" 

157CMD_IMPORT_USAGE = "Usage: /import <path.parquet|path.jsonl>" 

158CMD_IMPORT_SUCCESS = "Imported {sources} source(s) ({pages} pages, {chunks} chunks)" 

159CMD_RESET_SUCCESS = "Knowledge base reset" 

160CMD_RESET_PARTIAL = "Knowledge base reset ({skipped} item(s) could not be deleted)" 

161CMD_RESET_FAILED = "Reset failed: {error}" 

162CMD_RESET_CONFIRM_TITLE = "Reset the knowledge base?" 

163CMD_RESET_CONFIRM_MESSAGE = "This permanently deletes all indexed data." 

164CMD_REBUILD_CONFIRM_TITLE = "Rebuild the index?" 

165CMD_REBUILD_CONFIRM_MESSAGE = ( 

166 "Drops every chunk and re-embeds the documents directory from " 

167 "scratch. Takes minutes on large libraries. Source files on disk " 

168 "are left alone." 

169) 

170TASK_NAME_SYNC = "Sync documents" 

171TASK_NAME_WIKI = "Wikify" 

172TASK_NAME_WIKI_WIPE = "Delete wiki" 

173TASK_NAME_REBUILD = "Rebuild index" 

174TASK_NAME_IMPORT = "Import {file}" 

175TASK_NAME_EXPORT = "Export {file}" 

176TASK_NAME_ANALYZE = "Analyze {folder}" 

177IMPORT_STATUS_LOADING = "Loading dataset..." 

178EXPORT_STATUS_RUNNING = "Exporting..." 

179CMD_SET_UNKNOWN = "Unknown setting: {key}" 

180CMD_SET_SUCCESS = "{key} = {value}" 

181CMD_SET_AUTO = "auto" 

182CMD_SET_EMPTY_LIST = "(empty list)" 

183# Stands in for a credential's value wherever one would otherwise be echoed. 

184MASKED_VALUE = "************" 

185CMD_SET_INVALID = "Invalid value for {key}: {error}" 

186CMD_SET_TYPE_HINT = "{key} needs {kind}" 

187CMD_SET_CHOICES = "{key} must be one of {choices}" 

188CMD_SET_READONLY = "{key} is read-only; use the Models screen" 

189CMD_MODEL_SET = "Model set to {name}" 

190# Shown while a model swap's fleet reload runs off the event loop, so the swap 

191# reads as in-progress instead of a frozen TUI. Role-neutral: shared by the chat 

192# swap and the embed/vision/rerank swaps in model_pick, which name the model in 

193# their own surfaces (the chat input placeholder and warm footer for chat). 

194MODEL_SWAP_APPLYING = "Switching model, loading…" 

195MODEL_SWAP_QUEUED = "Switching to {name} when this answer finishes" 

196MODEL_SWAP_DONE = "Now using {name}" 

197MODEL_SWAP_FAILED = "Could not switch model: {error}" 

198MODEL_SWAP_STUCK = ( 

199 "Could not switch model: {error}. The old model could not be restored, so {name} is in use." 

200) 

201# Chat-input placeholder while a swap holds the input disabled: names the target 

202# model and says the input is waiting on it, so the disabled box is never a 

203# silent dead end. The warm progress itself shows in the task-bar footer. 

204CHAT_INPUT_SWITCHING = "Switching to {name} · chat unlocks when the model is ready…" 

205# Chat-input placeholder while a placement change reloads the whole fleet. 

206CHAT_INPUT_RELOADING = "Reloading the engine · one moment…" 

207# Shown when the user tries to send a prompt while the new chat model is still loading. 

208CHAT_MODEL_SWITCHING = "Still switching model. One moment, then send your prompt." 

209FLEET_RELOADING = "Applying placement, reloading the fleet. One moment, then send your prompt." 

210# Startup gate: shown from the moment the TUI paints until the app can serve. 

211STARTUP_PREPARING = "Preparing lilbee" 

212STARTUP_FAILED = "lilbee could not start: {error}" 

213CHAT_STACK_FAILED = "lilbee could not load its chat screen: {error}" 

214# Engine-load status painted into the pending answer while a prompt waits on a 

215# cold engine, and the failure that wait can end in. 

216ENGINE_READING_WEIGHTS = "Reading {name} weights" 

217ENGINE_WARMING = "Warming up the model" 

218# Names the phase, like its siblings above; "almost ready" promised a finish 

219# time the engine had not committed to (same rename as TASKBAR_WARM_LOADING). 

220ENGINE_ALMOST_READY = "Loading the engine" 

221ENGINE_LOAD_FAILED = "The engine failed to load: {error}" 

222ENGINE_FAILED_HINT = "Open the Catalog to install a model, or pick a different one in Settings." 

223ENGINE_NOT_READY = "The engine is not ready yet. Send your prompt again in a moment." 

224# Shown once when a prompt first waits on a cold engine and keep_engine_warm is off. 

225ENGINE_WARM_TIP = "Tip: Settings > Keep engine warm makes the next launch fast" 

226# Status painted into the pending answer when retrieval ran on a rewritten follow-up. 

227SEARCHING_FOR = "Searching for: {query}" 

228CMD_REMOVE_USAGE = "Usage: /remove <model_name>" 

229CMD_REMOVE_NOT_FOUND = "{name} is not installed" 

230CMD_REMOVE_SUCCESS = "Removed {name}" 

231CMD_REMOVE_FAILED = "Failed to remove {name}" 

232CMD_CANCEL = "Cancelled active operations" 

233CMD_CLEAR = "Conversation cleared" 

234SESSIONS_DISABLED_TITLE = "Sessions are turned off" 

235SESSIONS_DISABLED_MESSAGE = ( 

236 "Conversations are not being saved. Turn sessions on in Settings to list, " 

237 "resume, and manage past chats." 

238) 

239SESSIONS_COUNT = "{count} saved" 

240SESSIONS_EMPTY = "No saved conversations yet." 

241SESSIONS_FILTER_PLACEHOLDER = "Filter conversations…" 

242SESSIONS_RENAME_PLACEHOLDER = "New name… enter saves, esc cancels" 

243SESSIONS_ROW_META = "{count} msgs · {model}" 

244SESSIONS_RESUMED = "Resumed · {title}" 

245SESSIONS_MODEL_UNAVAILABLE = ( 

246 "This conversation used {model}, which isn't installed. Keeping {current}." 

247) 

248SESSIONS_NEW = "Started a new chat" 

249SESSIONS_DELETED = "Deleted · {title}" 

250SESSIONS_GONE = "This conversation was deleted, so it cannot be resumed." 

251SESSIONS_RENAME_GONE = "This conversation was deleted, so it cannot be renamed." 

252SESSIONS_DELETE_CONFIRM_TITLE = "Delete session" 

253SESSIONS_DELETE_CONFIRM = "Delete “{title}”? This cannot be undone." 

254SESSIONS_HINT = "↵ resume ^n new ^r rename ^d delete esc close" 

255SESSIONS_SAVE_FAILED = "Could not save the answer to this session: {error}" 

256FORK_PICKER_TITLE = "Fork after an answer" 

257FORK_PICKER_ANSWER_TO = "Answer to: {question}" 

258FORK_PICKER_HINT = "↑↓ select Enter fork Esc close" 

259FORK_DONE = "Forked · {title}" 

260FORK_FAILED = "Could not fork: {error}" 

261FORK_NO_SESSION = "Nothing to fork yet. Send a message first." 

262FORK_NO_ANSWER = "Nothing to fork yet. You can fork after the first answer." 

263FORK_SESSION_GONE = "This conversation was deleted, so it cannot be forked." 

264EXPORT_CHAT_DONE = "Exported to {path}" 

265EXPORT_CHAT_FAILED = "Could not export: {error}" 

266EXPORT_CHAT_NO_SESSION = "Nothing to export yet. Send a message first." 

267EXPORT_CHAT_SESSION_GONE = "This conversation was deleted, so it cannot be exported." 

268# The context chip: how much of this chat the model can still see. 

269# 

270# "context", not "memory": lilbee already has a Memory feature (/memories, the 

271# Memory settings group), so "memory 85%" reads as though those were 85% full. 

272# The jargon was never the problem; the collision was. 

273CONTEXT_CHIP_USAGE = "context {percent}%" 

274# Only when compaction is off and the window is nearly full: say what is about to 

275# happen, not what to do. The user can still act (turn compaction on, or ask the 

276# thing they care about now), which is what makes it worth saying at all. With 

277# compaction on there is nothing to decide, so the plain percentage stands. 

278CONTEXT_CHIP_USAGE_DROPPING = "context {percent}% · dropping soon" 

279CONTEXT_CHIP_COMPACTING = "condensing…" 

280CONTEXT_CHIP_TOOLTIP = ( 

281 "How much of this chat still fits in the model's context. Older turns drop out " 

282 "when it fills; they stay on screen but the model stops seeing them. Turn on " 

283 "chat_compaction in Settings to condense them into a summary instead." 

284) 

285 

286# Rules drawn in the log where the model's view of the chat changes. The 

287# transcript above them stays whole and scrollable, so every one of these is 

288# about what the model is *sent*, never about deleting anything. 

289# 

290# One shape for the family -- "N earlier messages <what happened>" -- so a reader 

291# learns it once instead of parsing three sentences about one idea. These are 

292# titles for a rich Rule, which draws the line out to the full width itself: no 

293# dashes belong in the strings. 

294CHAT_COMPACTED = "{count} earlier messages condensed to a summary" 

295CHAT_TRIMMED = "{count} earlier messages dropped from context" 

296# Turns that fell out with no summary standing in for them: this model's context 

297# is too small to carry that much conversation, however it is condensed. Say so, 

298# or the model just looks like it forgot for no reason. 

299CHAT_COMPACTION_STRANDED = "{count} more dropped · too much for this context" 

300 

301CHAT_COMPACTED_TOAST = "The context filled up, so earlier turns were condensed to keep them." 

302CHAT_TRIMMED_TOAST = ( 

303 "The chat outgrew this model's context, so earlier turns dropped out of what it " 

304 "can see. Turn on chat_compaction in Settings to condense them instead." 

305) 

306CHAT_COMPACTED_STRANDED_TOAST = ( 

307 "This model's context is too small for the whole conversation. Recent turns were " 

308 "condensed; older ones were dropped." 

309) 

310CMD_THEME_UNKNOWN = "No theme called {name}. Themes: {names}" 

311CMD_WIKI_DISABLED = "Wiki is disabled (set wiki = true in settings)" 

312CMD_WIKI_GENERATE_NO_EVIDENCE = ( 

313 "Nothing left in the index for {slug}; its sources are gone. " 

314 "Run `lilbee wiki index` to refresh." 

315) 

316CMD_WIKI_WIPE_NEEDS_YES = "Use --yes to confirm wiping the wiki in JSON mode" 

317CMD_WIKI_WIPE_WARNING = ( 

318 "This deletes every generated wiki page and its indexed rows.\n Pages: {path}" 

319) 

320TASK_NAME_CRAWL = "Crawl {url}" 

321STREAM_ERROR = "\n\n*Error: {error}*" 

322STREAM_DISCONNECTED = "\n\n*The engine connection dropped before the answer finished.*" 

323STREAM_CANCELLED = "\n\n*Response cancelled.*" 

324SYNC_STATUS_SYNCING = "Syncing..." 

325SYNC_STATUS_DONE = "Synced ({count} docs)" 

326SYNC_STATUS_FAILED = "Sync failed" 

327SYNC_FILE_PROGRESS = "Syncing [{current}/{total}]: {file}" 

328SYNC_TESSERACT_OCR = "Running Tesseract OCR on {file} ({total} pages in the file)" 

329SYNC_ALREADY_ACTIVE = "Sync in progress, please wait" 

330SYNC_CANCELLING = "A cancelled {kind} is still stopping, please wait" 

331TASK_STATUS_CANCELLING = "cancelling" 

332TASKBAR_CANCELLING = "Cancelling" 

333REBUILD_OWED = "The rebuild starts when the current work ends" 

334EMBEDDING_SET = "Embedding model: {name}" 

335CONFIRM_YES_LABEL = "Yes (y)" 

336CONFIRM_NO_LABEL = "No (n)" 

337CRAWL_DIALOG_TITLE = "Crawl a URL" 

338CRAWL_DIALOG_URL_PLACEHOLDER = "example.com (https:// added automatically)" 

339CRAWL_DIALOG_DEPTH_PLACEHOLDER = "blank = no limit" 

340CRAWL_DIALOG_MAX_PAGES_PLACEHOLDER = "clear for unlimited" 

341CRAWL_DIALOG_URL_LABEL = "URL" 

342CRAWL_DIALOG_RECURSIVE_LABEL = "Recursive (crawl whole site)" 

343CRAWL_DIALOG_BROWSER_LABEL = "Use browser (enables JavaScript, uses more memory)" 

344CRAWL_DIALOG_DEPTH_LABEL = "Depth cap" 

345CRAWL_DIALOG_MAX_PAGES_LABEL = "Max pages (clear for unlimited)" 

346CRAWL_DIALOG_SUBMIT = "Crawl" 

347CRAWL_DIALOG_CANCEL = "Cancel" 

348CRAWL_DIALOG_URL_REQUIRED = "URL is required" 

349CRAWL_DIALOG_INVALID_URL = "Invalid URL: {error}" 

350CRAWL_DIALOG_INVALID_NUMBER = "{field} must be a positive integer or blank" 

351CRAWL_DIALOG_LOGIN_LABEL = "Login for this crawl (optional, sent to this host only)" 

352CRAWL_DIALOG_AUTHORIZATION_LABEL = "Authorization header" 

353CRAWL_DIALOG_USERNAME_LABEL = "Username" 

354CRAWL_DIALOG_PASSWORD_LABEL = "Password" # noqa: S105 a field label, not a secret 

355CRAWL_DIALOG_COOKIE_LABEL = "Cookie" 

356CRAWL_DIALOG_LOGIN_PLACEHOLDER = "blank = use the stored login, if any" 

357CRAWL_DIALOG_INVALID_LOGIN = "Invalid login: {error}" 

358THEME_SET = "Theme: {name}" 

359HEADING_INSTALLED = "Installed" 

360HEADING_MATCHES = "Matches" 

361CATALOG_TAB_LOCAL = "Local" 

362CATALOG_TAB_FRONTIER = "Frontier" 

363CATALOG_TAB_DISCOVER = "Discover" 

364CATALOG_TAB_CHAT = "Chat" 

365CATALOG_TAB_EMBED = "Embed" 

366CATALOG_TAB_VISION = "Vision" 

367CATALOG_TAB_RERANK = "Rerank" 

368CATALOG_TAB_LIBRARY = "Library" 

369CATALOG_FRONTIER_SUMMARY = "{count} cloud models across {providers} providers" 

370CATALOG_GRID_OVERFLOW = "+{count} more on HF. Press v for the full list view" 

371CATALOG_GRID_LOAD_MORE = "{count} loaded · keep scrolling for more" 

372CATALOG_GRID_ALL_LOADED = "All {count} models loaded" 

373CATALOG_GRID_CUT_SHORT = "{count} models loaded · the listing stops here, search to narrow it" 

374CATALOG_LIST_COUNT = "{count} models" 

375CATALOG_LIST_CUT_SHORT = "{count} models · listing cut short, search to narrow it" 

376CATALOG_GRID_LOADING_MORE = "{frame} loading more models…" 

377CATALOG_USING_FRONTIER = "Using {name} via the {provider} API" 

378CATALOG_NEEDS_KEY = "{provider} needs an API key. Set {key_field} in Settings to enable this model." 

379CATALOG_KEY_REJECTED = ( 

380 "{provider} rejected the API key. Set a valid {key_field} in Settings to enable this model." 

381) 

382KEY_STATUS_READY = "ready" 

383KEY_STATUS_NEEDS_KEY = "needs key" 

384KEY_STATUS_KEY_REJECTED = "key rejected" 

385CATALOG_USING_REMOTE = "Using {name} (remote)" 

386CATALOG_ALREADY_INSTALLED = "{name} is already installed" 

387CATALOG_ALREADY_DOWNLOADING = "{name} is already downloading, press t to watch it" 

388CATALOG_QUEUED_DOWNLOAD = "Queued download: {name}" 

389CATALOG_WELCOME = "Pick a model to start chatting. The fit chip shows what runs on this machine." 

390CHAT_READY_TOAST = "Chat is ready. Press c." 

391CATALOG_INSTALLED_OK = "{name} installed" 

392CATALOG_GATED_REPO = "{name} requires login, run /login or lilbee login" 

393CATALOG_DOWNLOAD_FAILED = "{name}: download failed" 

394CATALOG_SELECT_FOR_INFO = "Select a model to view info" 

395CATALOG_FRONTIER_NO_INFO = "Info modal is for downloadable models only" 

396MODEL_INFO_HINT = "Esc / i / q to close" 

397MODEL_INFO_HF_LINK = "View on HuggingFace: https://huggingface.co/{repo}" 

398CATALOG_SELECT_TO_DELETE = "Select a model to delete" 

399CATALOG_NOT_INSTALLED = "{name} is not installed" 

400CATALOG_CONFIRM_DELETE = "Delete {name}? Press d again to confirm" 

401CATALOG_DELETED = "Deleted {name}" 

402CATALOG_DELETE_FAILED = "Delete failed: {error}" 

403CATALOG_NO_MATCH = "No models match your filters." 

404CATALOG_FILTER_PLACEHOLDER = "Filter models..." 

405CATALOG_VIEW_TOGGLE_GRID = "Press v for full list view · / to search" 

406CATALOG_VIEW_TOGGLE_LIST = "Press v for card view · s to sort" 

407CATALOG_VIEW_GRID = "Grid" 

408CATALOG_VIEW_LIST = "List" 

409CATALOG_SORT_LIST_ONLY = "Sort is available in list view (press v)" 

410CATALOG_SEARCHING_HF = "Searching HuggingFace…" 

411CATALOG_SEARCH_HF_CTA = '→ Search HuggingFace for "{query}"' 

412CHAT_INPUT_PLACEHOLDER_DEFAULT = "Ask… / commands ? keys F2 all commands" 

413# Replaces Textual's default magnifying-glass emoji, which the system emoji font 

414# draws in its own colors and at double width. Single cell, and distinct from the 

415# task list's ▶ so the two never read as the same mark. 

416COMMAND_PALETTE_ICON = "✦" 

417# Box-drawing, not block elements: shade blocks are dither patterns that draw as 

418# sparse dashes and full blocks seam per cell wherever the font is not cell-exact. 

419# Box-drawing pair: the safe default for terminals whose fonts do not tile 

420# block elements cell-exact (shade blocks render as sparse dashes there). 

421PROGRESS_BAR_FILL = "━" 

422PROGRESS_BAR_TRACK = "─" 

423# Block pair: the full-weight bars, used where the terminal tiles them. 

424PROGRESS_BAR_FILL_BLOCK = "█" 

425PROGRESS_BAR_TRACK_BLOCK = "░" 

426 

427 

428def progress_bar_glyphs() -> tuple[str, str]: 

429 """Bar fill/track pair: block elements where the terminal tiles them.""" 

430 from lilbee.cli.tui.color_compat import draws_block_bars 

431 

432 if draws_block_bars(): 

433 return PROGRESS_BAR_FILL_BLOCK, PROGRESS_BAR_TRACK_BLOCK 

434 return PROGRESS_BAR_FILL, PROGRESS_BAR_TRACK 

435 

436 

437SLASH_CATALOG_TITLE = "Slash Commands" 

438SLASH_CATALOG_FILTER_PLACEHOLDER = "Filter commands..." 

439SLASH_CATALOG_FOOTER_HINT = "↑↓ select Enter run Esc close" 

440SLASH_CATALOG_NO_MATCH = "No commands match" 

441HELP_HINT_COMMANDS = "type / for commands" 

442HELP_HINT_KEYS = "? for keys" 

443HELP_HINT_SEPARATOR = " · " 

444SCOPE_PILL_BOTH = "Both" 

445SCOPE_PILL_WIKI = "Wiki" 

446SCOPE_PILL_RAW = "Raw" 

447CHAT_BUSY = "Already answering. Press Ctrl+C to cancel, then submit your next prompt." 

448CHAT_STOPPING = "Stopping the answer. Submit again in a moment." 

449CHAT_MODEL_DOWNLOADING = "{name} is still downloading. Wait for it to finish, then submit." 

450MODEL_BEING_DOWNLOADED = ( 

451 "{name} is still downloading. Wait for it to finish before setting it active." 

452) 

453CHAT_WELCOME_TITLE = "lilbee" 

454CHAT_WELCOME_TAGLINE = "your local AI stack and personal encyclopedia." 

455CHAT_WELCOME_HINT = "Press / for commands, or just ask." 

456CHAT_WELCOME_NO_MODEL_HINT = "No chat model installed yet. Press m to pick one from the catalog." 

457CHAT_INPUT_NO_MODEL = "No chat model. Press m to pick one" 

458CHAT_LOGIN_PROMPT = "Paste your token with /login <token>" 

459CHAT_LOGGED_IN = "Logged in to HuggingFace" 

460CHAT_LOGIN_FAILED = "Login failed: {error}" 

461CHAT_VERSION = "lilbee {version}" 

462CHAT_RENDERING = "Rendering: {label}" 

463SETTINGS_READ_ONLY = "read-only" 

464SETTINGS_INVALID_VALUE = "Invalid value: {error}" 

465SETTINGS_RESET_TO_DEFAULT_TOOLTIP = "Reset to default" 

466SETTINGS_SOURCE_USER_PILL = "set by you" 

467SETTINGS_SOURCE_PROFILE_NAMED_PILL = "profile: {name}" 

468SETTINGS_SOURCE_PROFILE_PILL = "profile" 

469# The same word `lilbee settings list` prints for a value lilbee works out itself. 

470SETTINGS_SOURCE_AUTO_PILL = "auto" 

471 

472EMBED_SWAP_REBUILD_MESSAGE = ( 

473 "This embedding model needs a rebuild of the index. " 

474 "Switch now and rebuild? If you answer No, nothing changes." 

475) 

476MODEL_ASSIGN_REJECTED = "Model not set: {error}" 

477 

478EMBED_ADOPT_CONFIRM_TITLE = "Use this index's embedder?" 

479EMBED_ADOPT_CONFIRM_MESSAGE = ( 

480 "This index was built with embedding model '{model}'. Use it for this vault? " 

481 "lilbee will download it if needed and switch to it. No rebuild is required." 

482) 

483EMBED_ADOPT_NOTICE = "This index was built with a different embedder ('{model}')." 

484EMBED_ADOPT_REBUILD_NOTICE = ( 

485 "This index needs a {dim}-dim embedder. Rebuild it (press S to sync, or run " 

486 "`lilbee rebuild`) to use your current model." 

487) 

488EMBED_ADOPTING = "Switching to embedder '{model}'..." 

489EMBED_ADOPTED = "Now embedding with '{model}'." 

490EMBED_ADOPT_FAILED = "Could not adopt embedder: {error}" 

491EMBED_ADOPT_CANCELLED = "Kept the current embedder." 

492 

493SETTINGS_RESET_ALL_LABEL = "Reset all defaults" 

494SETTINGS_RESET_ALL_CONFIRM_TITLE = "Reset all settings?" 

495SETTINGS_RESET_ALL_CONFIRM_MESSAGE = ( 

496 "Your value for every writable setting is removed, so each one falls back to its default. " 

497 "Readonly fields (like installed models) are not affected." 

498) 

499SETTINGS_RESET_ALL_SUCCESS = "All settings reset to defaults" 

500SETTINGS_LIST_EDITOR_TITLE = "{key} ({count} lines)" 

501SETTINGS_LIST_EDITOR_INVALID_REGEX = "Invalid regex on line {n}: {error}" 

502SETTINGS_LIST_EDITOR_RESTORE_DEFAULTS = "Restore defaults" 

503SETTINGS_ADVANCED_TITLE = "Advanced ({count})" 

504SETTINGS_ADVANCED_TITLE_PROFILE = "Advanced ({count}, {profile_set} set by profile)" 

505SETTINGS_LEAVE_FIELD = "Leave field" 

506WIKI_EMPTY_STATE = "No wiki pages found" 

507# spaCy installs its NER model as an importable top-level package. 

508_SPACY_MODEL_PACKAGE = "en_core_web_sm" 

509WIKI_EMPTY_NEEDS_SPACY_LEAF = "spaCy not installed (see right pane)" 

510WIKI_EMPTY_NEEDS_SPACY_DETAIL = ( 

511 "## Wiki entity extraction needs spaCy\n\n" 

512 "Install it then re-ingest documents:\n\n" 

513 "```sh\n" 

514 "uv pip install spacy\n" 

515 "python -m spacy download en_core_web_sm\n" 

516 "```" 

517) 

518 

519 

520def wiki_empty_state_leaf() -> str: 

521 """Single-line sidebar tree leaf for the empty-wiki state.""" 

522 if not _spacy_available(): 

523 return WIKI_EMPTY_NEEDS_SPACY_LEAF 

524 return WIKI_EMPTY_STATE 

525 

526 

527def wiki_empty_state_detail() -> str: 

528 """Right-pane markdown body for the empty-wiki state.""" 

529 if not _spacy_available(): 

530 return WIKI_EMPTY_NEEDS_SPACY_DETAIL 

531 return WIKI_NO_CONTENT 

532 

533 

534@lru_cache(maxsize=1) 

535def _spacy_available() -> bool: 

536 """True when spaCy and its NER model are both importable. 

537 

538 Presence check only: the empty state repaints on every view switch and 

539 every filter keystroke, so loading the pipeline here would stall the UI 

540 thread for a second per paint. Cached because the answer cannot change 

541 within a session. 

542 """ 

543 return all(find_spec(name) is not None for name in ("spacy", _SPACY_MODEL_PACKAGE)) 

544 

545 

546WIKI_SEARCH_PLACEHOLDER = "Filter pages..." 

547WIKI_NO_CONTENT = "Select a page to view" 

548WIKI_NO_MATCHES = "No pages match '{filter}'" 

549WIKI_LOAD_FAILED_LEAF = "Failed to load pages (see right pane)" 

550WIKI_LOAD_FAILED = "## Failed to load wiki pages\n\n{error}" 

551WIKI_BUILD_STARTING = "Starting wiki run..." 

552WIKI_BUILD_PHASE = "{phase}..." 

553WIKI_BUILD_PAGE = "{label} ({current}/{total})" 

554WIKI_BUILD_DONE = "Wiki build finished: {count} pages" 

555WIKI_ALREADY_ACTIVE = "Wiki build in progress, please wait" 

556WIKI_STUBS_HEADING = "Not written yet" 

557WIKI_STUB_SUFFIX = "(not written)" 

558WIKI_STUB_DETAIL = ( 

559 "# {title}\n\n" 

560 "*This page has not been written yet.*\n\n" 

561 "{label} appears in {sources}. Opening it offers to write the page, which " 

562 "spends one LLM call and is GPU-heavy.\n\n" 

563 "If you would rather not be asked about pages like this, turn the wiki off " 

564 "in settings." 

565) 

566WIKI_STUB_CONFIRM_TITLE = "Write this page?" 

567WIKI_STUB_CONFIRM_MESSAGE = ( 

568 "Writing {label} spends one LLM call and is GPU-heavy. It draws on {sources}.\n\n" 

569 "You can turn the wiki off in settings if you would rather not be asked." 

570) 

571WIKI_STUB_TASK = "Write {label}" 

572WIKI_STUB_DONE = "Wrote {label}" 

573WIKI_STUB_FAILED = "Could not write {label}: {error}" 

574WIKI_STUB_STALE = "Nothing left to write {label} from; its sources are gone" 

575WIKI_WIPE_CONFIRM_TITLE = "Delete the wiki?" 

576WIKI_WIPE_CONFIRM_MESSAGE = ( 

577 "This deletes every generated page and its indexed rows. " 

578 "Your documents are not touched. This cannot be undone." 

579) 

580WIKI_WIPE_DISABLED_TITLE = "Wiki turned off. Delete what it generated?" 

581WIKI_WIPE_DISABLED_MESSAGE = ( 

582 "Turning the wiki off stops new pages being written, but the pages already " 

583 "generated stay on disk and in search. Delete them now?" 

584) 

585WIKI_WIPE_RUNNING = "Deleting wiki pages..." 

586WIKI_WIPE_DONE = "Wiki deleted: {count} pages removed" 

587WIKI_WIPE_NOTHING = "No wiki pages to delete" 

588WIKI_INDEX_LABEL = "Index" 

589WIKI_LOG_LABEL = "Log" 

590WIKI_DRAFTS_TITLE = "Wiki Drafts" 

591WIKI_DRAFTS_EMPTY = "No drafts pending review" 

592WIKI_DRAFTS_LOAD_FAILED = "Failed to load drafts: {error}" 

593WIKI_DRAFTS_COLUMN_SLUG = "Slug" 

594WIKI_DRAFTS_COLUMN_KIND = "Kind" 

595WIKI_DRAFTS_COLUMN_DRIFT = "Drift" 

596WIKI_DRAFTS_COLUMN_FAITHFULNESS = "Faithfulness" 

597WIKI_DRAFTS_COLUMN_PUBLISHED = "Published?" 

598WIKI_DRAFTS_KIND_DRIFT = "drift" 

599WIKI_DRAFTS_DIFF_EMPTY = "Select a draft to view its diff" 

600WIKI_DRAFTS_DIFF_NONE = "(no differences)" 

601WIKI_DRAFTS_DIFF_FAILED = "Failed to load diff: {error}" 

602WIKI_DRAFTS_ACCEPT_CONFIRM_TITLE = "Accept draft?" 

603WIKI_DRAFTS_ACCEPT_CONFIRM_MESSAGE = ( 

604 "Overwrite the published page with {slug} and re-index? This cannot be undone." 

605) 

606WIKI_DRAFTS_REJECT_CONFIRM_TITLE = "Reject draft?" 

607WIKI_DRAFTS_REJECT_CONFIRM_MESSAGE = "Delete draft {slug}? The published page will not change." 

608WIKI_DRAFTS_ACCEPTED = "Accepted {slug}" 

609WIKI_DRAFTS_REJECTED = "Rejected {slug}" 

610WIKI_DRAFTS_ACCEPT_FAILED = "Accept failed: {error}" 

611WIKI_DRAFTS_REJECT_FAILED = "Reject failed: {error}" 

612WIKI_DRAFTS_ACCEPT_TASK = "Accept draft {slug}" 

613WIKI_DRAFTS_REJECT_TASK = "Reject draft {slug}" 

614WIKI_DRAFTS_MISSING = "missing: {slug}" 

615WIKI_DRAFTS_NO_MATCHES = "No drafts match '{filter}'" 

616WIKI_DRAFTS_PUBLISHED_YES = "yes" 

617WIKI_DRAFTS_PUBLISHED_NO = "no" 

618WIKI_DRAFTS_SEARCH_PLACEHOLDER = "Filter drafts..." 

619MEMORIES_EMPTY = "No memories stored. Use /remember to add one." 

620MEMORIES_DISABLED = "Memory is off. Enable it with /set memory_enabled true." 

621MEMORIES_LOAD_FAILED = "Failed to load memories: {error}" 

622MEMORIES_COLUMN_KIND = "Kind" 

623MEMORIES_COLUMN_SHARED = "Shared" 

624MEMORIES_COLUMN_TEXT = "Memory" 

625MEMORIES_FLAG_YES = "yes" 

626MEMORIES_FLAG_NO = "no" 

627MEMORIES_SEARCH_PLACEHOLDER = "Filter memories..." 

628MEMORIES_EMPTY_STATE = ( 

629 "Memories are notes lilbee keeps about you between chats. Use /remember to save one." 

630) 

631MEMORIES_NO_MATCHES = "No memories match this filter." 

632MEMORIES_DELETE_CONFIRM_TITLE = "Delete memory?" 

633MEMORIES_DELETE_CONFIRM_MESSAGE = "Delete this memory? This cannot be undone." 

634MEMORIES_DELETED = "Deleted memory" 

635MEMORIES_DELETE_FAILED = "Delete failed: {error}" 

636MEMORIES_DELETE_NOT_FOUND = "Memory not found; it may already be gone." 

637MEMORIES_FLAG_NOT_FOUND = "Memory not found; it may already be gone." 

638MEMORIES_SHARED_ON = "Shared with agents" 

639MEMORIES_SHARED_OFF = "No longer shared with agents" 

640MEMORIES_FLAG_FAILED = "Update failed: {error}" 

641# Re-export the shared heading map with string keys so callers can 

642# look up by raw ``page_type`` string without coercion. 

643WIKI_TYPE_HEADINGS: dict[str, str] = { 

644 kind.value: label for kind, label in _WIKI_TYPE_HEADINGS.items() 

645} 

646APP_QUIT_AGAIN_HINT = "Answer cancelled. Press Ctrl+C again to quit." 

647SETUP_CARD_HINT = "↵ Enter to install" 

648INSTALLED_CARD_HINT = "D / ⌫ to delete" 

649 

650# Architecture compatibility pill labels (catalog row). 

651# SUPPORTED renders nothing to keep the row visually quiet for the common case. 

652COMPAT_PILL_UNSUPPORTED = "unsupported" 

653COMPAT_PILL_UNKNOWN = "untested" 

654 

655# Architecture compatibility copy for the catalog detail view + confirm modal. 

656COMPAT_DETAIL_SENTENCE_SUPPORTED = "Supported by your llama.cpp build." 

657COMPAT_DETAIL_SENTENCE_UNSUPPORTED = ( 

658 "Architecture {arch} is not in the supported set. Pull may fail at load." 

659) 

660COMPAT_DETAIL_SENTENCE_UNKNOWN = ( 

661 "Architecture unknown until download. Pull will probe the header first." 

662) 

663COMPAT_MODAL_TITLE = "Architecture not supported" 

664COMPAT_MODAL_BODY = ( 

665 "This model uses architecture {arch}. Your lilbee build doesn't support it, " 

666 "so loading after download will probably fail. Pull anyway?" 

667) 

668DEFAULT_VIEW = "Chat" 

669CATALOG_VIEW = "Catalog" 

670WIKI_VIEW = "Wiki" 

671FLEET_VIEW = "Fleet" 

672SESSIONS_VIEW = "Sessions" 

673FLEET_TITLE = "Placement" 

674FLEET_STATE_AUTO = "auto" 

675FLEET_STATE_MANUAL = "manual" 

676FLEET_STATE_EDITED = "edited · ctrl+s to apply" 

677FLEET_STATE_REBUILDING = "rebuilding fleet…" 

678FLEET_SINGLE_GPU_NOTE = "One graphics card: everything runs here." 

679FLEET_GPU_PROBING = "probing GPUs…" 

680FLEET_NO_GPUS = "(no GPUs detected)" 

681# Shown when the device probe failed outright (e.g. a wedged GPU driver), so the 

682# panel names the problem instead of sitting on the probing placeholder forever. 

683FLEET_GPU_PROBE_FAILED = "GPU probe failed: {reason}" 

684# Shown for a role that has no placement because its model isn't downloaded, so the 

685# empty slot reads as a fixable state instead of "GPU placement is broken". 

686FLEET_MODEL_NOT_DOWNLOADED = "{role}: {model} not downloaded, pull it to place it" 

687FLEET_SAVED_PLACEMENT_IGNORED = ( 

688 "A saved placement does not fit this hardware and is being ignored. " 

689 "Press the auto command to clear it, or set a new one." 

690) 

691# Shown when an Intel GPU's utilization is unreadable only because intel_gpu_top 

692# lacks the CAP_PERFMON grant, so the muted "--" reads as a fixable state. 

693FLEET_INTEL_UTIL_GRANT = ( 

694 "Intel GPU utilization needs a one-time grant: " 

695 "sudo setcap cap_perfmon+ep {binary} (or Linux 6.5+ reads it with no setup)" 

696) 

697# Shown when intel_gpu_top is not installed at all: the i915 PMU covers kernels 

698# too old to publish fdinfo engine counters, so installing igt-gpu-tools is the 

699# only path to utilization there. 

700FLEET_INTEL_UTIL_INSTALL = ( 

701 "Intel GPU utilization needs the igt-gpu-tools package " 

702 "(then: sudo setcap cap_perfmon+ep $(command -v intel_gpu_top))" 

703) 

704 

705 

706def intel_util_hint_text(hint: IntelUtilHint) -> str: 

707 """Localized fix instruction for an unreadable Intel util reading.""" 

708 if hint.kind is IntelHintKind.GRANT and hint.binary is not None: 

709 return FLEET_INTEL_UTIL_GRANT.format(binary=hint.binary) 

710 return FLEET_INTEL_UTIL_INSTALL 

711 

712 

713FLEET_CMD_PREVIEW = "Preview" 

714FLEET_CMD_APPLY = "Apply" 

715FLEET_CMD_AUTO = "Auto" 

716FLEET_TAG_SPLIT = "split" 

717FLEET_TAG_SINGLE = "one card" 

718FLEET_HELP_ICON = "?" 

719# Single hover explanation for the whole drawer (kept friendly, no jargon). 

720FLEET_HELP_TOOLTIP = ( 

721 "Top: how busy each GPU is right now.\n" 

722 "Grid: what runs on which GPU.\n" 

723 " • chat is one model split across the highlighted cards (they work as one).\n" 

724 " • embed/vision run as a full copy on each highlighted card.\n" 

725 " • rerank runs on one card; pick a single GPU.\n" 

726 "Click a cell to change it, then Apply. Auto lets lilbee choose." 

727) 

728# The full nav-view universe in order. Single source for the view set: the 

729# settings bar pre-creates a tab per entry (toggling Wiki visibility at 

730# runtime), get_nav_views() gates Wiki, and app.get_views() derives its 

731# factory map from get_nav_views(). 

732ALL_NAV_VIEWS: tuple[str, ...] = ( 

733 DEFAULT_VIEW, 

734 CATALOG_VIEW, 

735 "Status", 

736 "Settings", 

737 "Tasks", 

738 WIKI_VIEW, 

739 FLEET_VIEW, 

740 SESSIONS_VIEW, 

741) 

742 

743 

744def get_nav_views() -> list[str]: 

745 """Return the active nav view names, including Wiki when enabled.""" 

746 return [v for v in ALL_NAV_VIEWS if v != WIKI_VIEW or cfg.wiki] 

747 

748 

749MODE_NORMAL = "NORMAL" 

750MODE_INSERT = "INSERT" 

751TASKBAR_HINT = "Press t for Tasks" 

752TASKBAR_HINT_INPUT = "Esc then t for Tasks" 

753CHAT_REASONING_FINISHED = "reasoning · {tokens} tokens" 

754 

755STATUS_DOCS_LOAD_FAILED = "(unable to read store)" 

756STATUS_DOCS_EMPTY = "(no documents yet)" 

757STATUS_DOCS_TITLE = "Documents" 

758STATUS_DOCS_MORE = "{count} loaded · keep scrolling for more" 

759TASKBAR_STARTING_WORKER = "Starting {labels} worker..." 

760TASKBAR_STARTING_WORKERS = "Starting {labels} workers..." 

761# Cold-start chat warm line: a spinner, the model being loaded, and the phase 

762# (with byte % while paging weights) so the held input reads as "loading {model}", 

763# not "stuck". The name is the model's display label, or this fallback before the 

764# warm has stamped which model it is loading. 

765TASKBAR_WARM_LINE = "warming up {name} · {detail}" 

766TASKBAR_WARM_FALLBACK_NAME = "chat" 

767TASKBAR_WARM_STARTING = "starting engine" 

768TASKBAR_WARM_READING = "reading weights {pct}%" 

769# Names the phase, like its two siblings above. "almost ready" predicted a 

770# finish time the engine has not promised, and was the one phase saying nothing 

771# about what is happening. 

772TASKBAR_WARM_LOADING = "loading into VRAM" 

773 

774TASK_CENTER_TITLE = "Background Tasks" 

775TASK_CENTER_COUNTS = "{active} running · {queued} queued · {done} done" 

776TASK_CENTER_COUNTS_CANCELLING = " · {cancelling} cancelling" 

777TASK_CENTER_HINT = "r refresh c cancel C clear done q back j/k navigate" 

778TASK_CENTER_EMPTY_HEADLINE = "✓ all caught up" 

779TASK_CENTER_EMPTY_DETAIL = "no background tasks" 

780TASKBAR_SINGLE = "{name} [b]{pct:.1f}%[/b]" 

781TASKBAR_MULTIPLE = "[b]{count} tasks running[/b]" 

782TASKBAR_ONE = "[b]1 task running[/b]" 

783TASKBAR_QUEUED_COUNT = "{count} queued" 

784TASKBAR_ALL_DONE = "[b]Done[/b]" 

785TASKBAR_FAILED = "[b]{count} task failed[/b]" 

786TASKBAR_FAILED_PLURAL = "[b]{count} tasks failed[/b]" 

787TASKBAR_SYNC_PENDING_ONE = "[b]1 doc to sync[/b] · S to sync" 

788TASKBAR_SYNC_PENDING_PLURAL = "[b]{count} docs to sync[/b] · S to sync" 

789TASKBAR_SYNC_PENDING_ONE_INPUT = "[b]1 doc to sync[/b] · Esc then S to sync" 

790TASKBAR_SYNC_PENDING_PLURAL_INPUT = "[b]{count} docs to sync[/b] · Esc then S to sync" 

791SYNC_CANCELLED_RESUME = "Sync cancelled. Press S to resume." 

792SYNC_EMBEDDING = "Embedding {file}" 

793SYNC_FILE_DONE = "Done: {file}" 

794SYNC_WAITING = "Waiting for the running sync" 

795ADD_SYNCING_FILE = "Syncing {file}..." 

796ADD_PAGE_PROGRESS = "{status} page {current} of {total}" 

797ADD_FILE_DONE = "Done {file}" 

798 

799SETTINGS_API_KEYS_WARNING = ( 

800 "These keys are stored in plain text at {path}. " 

801 "Anything you send to these providers leaves your machine. " 

802 "Do not route sensitive documents from lilbee through them." 

803) 

804MODEL_BAR_CLOUD_PROVIDER_WARNING = ( 

805 "Chat prompts are being sent to {provider}. Do not share sensitive data." 

806) 

807CHAT_MODE_SEARCH_LABEL = "Search" 

808CHAT_MODE_CHAT_LABEL = "Chat" 

809CHAT_MODE_TOGGLE_TOOLTIP = ( 

810 "Search runs your question through document retrieval. " 

811 "Chat skips retrieval and answers directly. Click or press F3 to flip." 

812) 

813CHAT_MODE_TOGGLE_DISABLED_TOOLTIP = ( 

814 "Search needs an embedding model. Press the pill to pick one from the catalog." 

815) 

816SEARCH_NEEDS_EMBEDDER = "Search needs an embedding model. Pick one to enable it." 

817CHAT_MODE_SEARCH_NO_RESULTS = "Search returned 0 results, falling back to chat for this turn." 

818CHAT_MODE_SET = "Mode: {label}" 

819MODEL_PICKER_TITLE_CHAT = "Pick a chat model" 

820MODEL_PICKER_TITLE_EMBED = "Pick an embedding model" 

821MODEL_PICKER_TITLE_VISION = "Pick a vision model" 

822MODEL_PICKER_TITLE_RERANK = "Pick a reranker model" 

823MODEL_VALUE_NONE = "(none)" 

824MODEL_PICKER_DISABLE_LABEL = "(disabled, no model)" 

825MODEL_PICKER_CHAT_TOOLTIP = "Model used to answer your questions. Click to pick a different one." 

826MODEL_PICKER_EMBED_TOOLTIP = ( 

827 "Model used to vectorize search queries (Search mode). Click to pick a different one." 

828) 

829MODEL_PICKER_VISION_TOOLTIP = ( 

830 "Optional. Model used to read scanned PDFs and images. Click to pick one or browse the catalog." 

831) 

832MODEL_PICKER_RERANK_TOOLTIP = ( 

833 "Optional. Model used to sharpen search results. Click to pick one or browse the catalog." 

834) 

835MODEL_PICKER_BROWSE_CATALOG = "Browse catalog to download..." 

836MODEL_PICKER_SEARCH_PLACEHOLDER = "Search models..." 

837 

838# Model bar (chat-screen, below the input) 

839MODEL_BAR_CHAT_LABEL = "Chat" 

840MODEL_BAR_EMBED_LABEL = "Embed" 

841MODEL_BAR_VISION_LABEL = "Vision" 

842MODEL_BAR_RERANK_LABEL = "Rerank" 

843MODEL_BAR_DISABLED = "disabled" 

844MODEL_BAR_NONE = "none, pick one" 

845MODEL_BAR_NOT_INSTALLED = "{name} (not installed)" 

846MODEL_BAR_NOT_INSTALLED_TOOLTIP = ( 

847 "This model is not installed. Click to pick another, " 

848 "or press m to open the Catalog and install it." 

849) 

850MODEL_PICKER_TURN_OFF = "Turn off this model" 

851MODEL_PICKER_HINT = "Enter to pick · Esc to cancel · / to search" 

852 

853# Settings Profile tab, the profile line, and the Apply and Save-as dialogs 

854PROFILE_TAB_LABEL = "Profile" 

855PROFILE_LINE_LABEL = PROFILE_TAB_LABEL 

856PROFILE_LINE_COUNT_ONE = "1 value set by you" 

857PROFILE_LINE_COUNT = "{count} values set by you" 

858PROFILE_TAB_TITLE = "profile" 

859PROFILE_TAB_HELP = ( 

860 "Settings for ingest, OCR, chunking and search. Models, hardware and API keys stay as they are." 

861) 

862PROFILE_TESTED_ON = "Tested on: {text}" 

863PROFILE_CHANGES_TITLE = "Your changes" 

864PROFILE_CHANGES_HELP = "These values override {name} in this project." 

865PROFILE_CHANGES_NONE = "No values of yours override {name}." 

866PROFILE_COL_SETTING = "Setting" 

867PROFILE_COL_YOURS = "Your value" 

868PROFILE_COL_PROFILE = "Profile value" 

869PROFILE_COL_NOW = "Now" 

870PROFILE_COL_AFTER = "After" 

871PROFILE_COL_COST = "Cost" 

872PROFILE_VALUE_ON = "on" 

873PROFILE_VALUE_OFF = "off" 

874PROFILE_VALUE_NONE = "none" 

875PROFILE_ACTION_UPDATE = "Update {name}" 

876PROFILE_ACTION_SAVE_AS = "Save as new profile" 

877PROFILE_ACTION_DISCARD = "Discard my changes" 

878PROFILE_ACTION_REAPPLY = "Re-apply {name}" 

879PROFILE_APPLY_TITLE = "Switch to {name}?" 

880PROFILE_REAPPLY_TITLE = "Re-apply {name}?" 

881PROFILE_APPLY_CHANGES = "Changes" 

882PROFILE_APPLY_KEEPS = "Keeps your values" 

883PROFILE_APPLY_UNTOUCHED = "Models, hardware and API keys stay as they are." 

884PROFILE_APPLY_NOTHING = "Nothing changes; lilbee records the profile's name." 

885PROFILE_APPLY_REINDEX_ONE = "One change rebuilds the index of {files} files." 

886PROFILE_APPLY_REINDEX_MANY = "{count} changes rebuild the index of {files} files." 

887PROFILE_APPLY_SETS_REINDEX = "A rebuild applies the profile's settings that change the index." 

888PROFILE_APPLY_HELD_OUT = "A rebuild retries the files that lilbee holds out." 

889PROFILE_APPLY_REINDEX_LABEL = "Apply and reindex" 

890PROFILE_APPLY_LABEL = "Apply" 

891PROFILE_CANCEL_LABEL = "Cancel" 

892PROFILE_APPLIED = "Switched to {name}." 

893PROFILE_SAVE_TITLE = "Save as a new profile" 

894PROFILE_SAVE_PLACEHOLDER = "Profile name" 

895PROFILE_SAVE_TO = "Save to" 

896PROFILE_SAVE_GLOBAL = "All my projects (global folder)" 

897PROFILE_SAVE_PROJECT = "This project" 

898PROFILE_SAVE_EXPLAIN_NONE = "The new profile holds {name}'s values. This project switches to it." 

899PROFILE_SAVE_EXPLAIN_ONE = ( 

900 "The new profile holds {name}'s values and your 1 change. " 

901 "This project switches to it, and your change stops showing as yours." 

902) 

903PROFILE_SAVE_EXPLAIN = ( 

904 "The new profile holds {name}'s values and your {count} changes. " 

905 "This project switches to it, and your changes stop showing as yours." 

906) 

907PROFILE_SAVE_LABEL = "Save" 

908PROFILE_SAVED = "Saved {name} to {path}." 

909PROFILE_UPDATED = "Updated {name}." 

910PROFILE_DISCARDED = "Your values are gone; {name}'s values show through." 

911PROFILE_ALL_TITLE = "All profiles" 

912PROFILE_ALL_HELP = "Apply, duplicate, rename, delete, export or import any profile." 

913PROFILE_MANAGE_LABEL = "Manage profiles" 

914PROFILE_LIBRARY_TITLE = "Profiles" 

915PROFILE_LIBRARY_ACTIVE = "active" 

916PROFILE_LIBRARY_SHADOWED = "shadowed" 

917PROFILE_LIBRARY_BROKEN = "cannot load: {reason}" 

918PROFILE_LIBRARY_SHADOWED_NOTE = "Hidden by the {folder} profile with the same name." 

919PROFILE_LIBRARY_CHANGES = "Applying it changes" 

920PROFILE_LIBRARY_NO_CHANGES = "Applying it changes nothing." 

921PROFILE_LIBRARY_UNREACHABLE = ( 

922 "The name {name} picks another file, so lilbee cannot act on the {folder} one. " 

923 "Rename or remove it in its folder." 

924) 

925PROFILE_KEY_APPLY = PROFILE_APPLY_LABEL 

926PROFILE_KEY_DUPLICATE = "Duplicate" 

927PROFILE_KEY_RENAME = "Rename" 

928PROFILE_KEY_DELETE = "Delete" 

929PROFILE_KEY_EXPORT = "Export" 

930PROFILE_KEY_IMPORT = "Import" 

931PROFILE_KEY_DUPLICATE_SHORT = "Dup" 

932PROFILE_KEY_RENAME_SHORT = "Ren" 

933PROFILE_KEY_DELETE_SHORT = "Del" 

934PROFILE_KEY_EXPORT_SHORT = "Exp" 

935PROFILE_KEY_IMPORT_SHORT = "Imp" 

936PROFILE_KEY_CLOSE = "Close" 

937PROFILE_DUPLICATE_TITLE = "Duplicate {name}" 

938PROFILE_DUPLICATE_EXPLAIN = "The copy keeps {name}'s values, description and credit." 

939PROFILE_DUPLICATE_INITIAL = "{name} copy" 

940PROFILE_SCAN_FAILED = "Could not read the profiles: {reason}" 

941PROFILE_RENAME_TITLE = "Rename {name}" 

942PROFILE_RENAME_EXPLAIN = "The file stays in its folder. This project follows the new name." 

943PROFILE_RENAMED = "Renamed {old} to {name}." 

944PROFILE_DELETE_TITLE = "Delete {name}?" 

945PROFILE_DELETE_MESSAGE = "This removes {path}. Projects that use it keep their recorded copy." 

946PROFILE_DELETED = "Deleted {path}." 

947PROFILE_PATH_PLACEHOLDER = "Path to a file or a folder" 

948PROFILE_PATH_EMPTY = "Type a path." 

949PROFILE_EXPORT_TITLE = "Export {name}" 

950PROFILE_EXPORT_EXPLAIN = "Writes a file anyone can import. Give a folder or a file path." 

951PROFILE_EXPORT_LABEL = PROFILE_KEY_EXPORT 

952PROFILE_EXPORTED = "Exported {name} to {path}." 

953PROFILE_IMPORT_TITLE = "Import a profile" 

954PROFILE_IMPORT_EXPLAIN = "Checks a profile file and copies it into a profile folder." 

955PROFILE_IMPORT_LABEL = PROFILE_KEY_IMPORT 

956PROFILE_IMPORTED = "Imported {name} to {path}." 

957SETTINGS_REINDEX_MESSAGE = ( 

958 "The index was built with values you changed. Rebuild it so search uses the new values." 

959) 

960 

961PROFILE_EFFECT_TEXT: dict[ProfileEffect, str] = { 

962 ProfileEffect.REINDEX: "reindex", 

963 ProfileEffect.NEW_FILES_ONLY: "new files only", 

964 ProfileEffect.NOW: "", 

965} 

966PROFILE_FOLDER_TEXT: dict[ProfileFolder, str] = { 

967 ProfileFolder.PROJECT: "Saved in this project.", 

968 ProfileFolder.GLOBAL: "Saved for all projects.", 

969 ProfileFolder.BUILTIN: "A built-in profile that ships with lilbee.", 

970} 

971PROFILE_FOLDER_TAG: dict[ProfileFolder, str] = { 

972 ProfileFolder.PROJECT: "project", 

973 ProfileFolder.GLOBAL: "global", 

974 ProfileFolder.BUILTIN: "built-in", 

975} 

976PROFILE_SAVE_FOLDER_TEXT: dict[ProfileFolder, str] = { 

977 ProfileFolder.GLOBAL: PROFILE_SAVE_GLOBAL, 

978 ProfileFolder.PROJECT: PROFILE_SAVE_PROJECT, 

979} 

980PROFILE_STATUS_PILL: dict[ProfileStatus, str] = { 

981 ProfileStatus.CURRENT: "", 

982 ProfileStatus.CHANGED: "file changed", 

983 ProfileStatus.MISSING: "file missing", 

984 ProfileStatus.BROKEN: "file broken", 

985} 

986PROFILE_NAME_PROBLEM_TEXT: dict[NameProblem, str] = { 

987 NameProblem.EMPTY: "Type a name for the profile.", 

988 NameProblem.INVALID: ( 

989 "Use 1 to 40 ASCII letters, digits, spaces, hyphens, underscores, parentheses." 

990 ), 

991 NameProblem.RESERVED: "A built-in profile already has this name.", 

992 NameProblem.RESERVED_WORD: "This name already names a lilbee page or a Windows device.", 

993 NameProblem.TAKEN: "A profile in that folder already has this name.", 

994} 

995 

996# /analyze, the analyze report, and the analyze tip on the /add hint 

997ANALYZE_STATUS_FINDING = "Finding files..." 

998ANALYZE_STATUS_READING = "Reading {done} of {total}: {file}" 

999ANALYZE_NOT_A_FOLDER = "{path} is not a folder." 

1000ANALYZE_FAILED = "Analyze failed: {error}" 

1001ANALYZE_TIP_HIDDEN = "The analyze tip is hidden for this project." 

1002ANALYZE_DONE = "Analyze of {folder} finished. Type /analyze report to see it." 

1003ANALYZE_NO_REPORT = "No analyze report yet. Run /analyze first." 

1004ANALYZE_TITLE = "What's in {folder}" 

1005ANALYZE_YOUR_DOCUMENTS = "your documents" 

1006ANALYZE_READ = "Read {read} of {total} documents without OCR. No AI model runs." 

1007ANALYZE_READ_COUNTED = ( 

1008 "Read {read} of {total} documents without OCR and counted {counted} code, image and " 

1009 "archive files. No AI model runs." 

1010) 

1011ANALYZE_SAMPLED = "The documents are sampled evenly; analyze_max_files is {cap}." 

1012ANALYZE_FOUND = "Found" 

1013ANALYZE_FACT_TYPES = "File types" 

1014ANALYZE_FACT_CODE = "Code" 

1015ANALYZE_FACT_SCANS = "Scanned pages" 

1016ANALYZE_FACT_TABLES = "Tables" 

1017ANALYZE_FACT_LANGUAGES = "Languages" 

1018ANALYZE_FACT_PDF_LENGTH = "Typical PDF length" 

1019ANALYZE_FACT_TEXT_LENGTH = "Typical length" 

1020ANALYZE_TYPE_COUNT = "{count} {kind}" 

1021ANALYZE_SHARE_OF_FILES = "{share:.0%} of files" 

1022ANALYZE_SHARE_OF_PAGES = "{share:.0%} of pages" 

1023ANALYZE_TABLES_VALUE = "in {tables} of {files} PDFs" 

1024ANALYZE_LANGUAGE_SHARE = "{name} {share:.0%}" 

1025ANALYZE_LANGUAGES_TEXT = "{languages} (text files)" 

1026ANALYZE_LANGUAGES_NONE = "none detected" 

1027ANALYZE_PAGES_VALUE = "{pages:g} pages" 

1028ANALYZE_CHARS_VALUE = "{chars:,.0f} characters" 

1029ANALYZE_FAILED_TITLE = "Could not read" 

1030ANALYZE_UNREADABLE_TITLE = "Could not open" 

1031ANALYZE_COL_FOLDER = "Folder" 

1032ANALYZE_COL_FILE = "File" 

1033ANALYZE_COL_REASON = "Reason" 

1034ANALYZE_COL_WHY = "Why" 

1035ANALYZE_WHY_BUILTIN = "{builtin} sets it" 

1036ANALYZE_RECOMMEND_TITLE = "Recommendation: {name}" 

1037ANALYZE_RECOMMEND_DERIVED = ( 

1038 "{builtin}, with these changes. Apply saves it to your profiles and switches this " 

1039 "project to it." 

1040) 

1041ANALYZE_RECOMMEND_BUILTIN = "{builtin} fits these files. Apply switches this project to it." 

1042ANALYZE_PICKED = "Picked because {reason}." 

1043ANALYZE_NO_CHANGES = "Applying it changes nothing." 

1044ANALYZE_KEEPS = "Keeps your values of {keys}." 

1045ANALYZE_NOTES_TITLE = "Notes" 

1046ANALYZE_SAVE_LABEL = "Save only" 

1047ANALYZE_TIP_LABEL = "tip" 

1048ANALYZE_TIP = ( 

1049 "This project uses the Default profile. Run /analyze on the same path first to pick " 

1050 "settings for it, or /analyze off to hide this tip." 

1051) 

1052 

1053 

1054def profile_save_explain(name: str, count: int) -> str: 

1055 """The Save-as note: what the new profile holds, worded for 0, 1 or many changes.""" 

1056 if count == 0: 

1057 return PROFILE_SAVE_EXPLAIN_NONE.format(name=name) 

1058 if count == 1: 

1059 return PROFILE_SAVE_EXPLAIN_ONE.format(name=name) 

1060 return PROFILE_SAVE_EXPLAIN.format(name=name, count=count) 

1061 

1062 

1063def profile_count_text(count: int) -> str: 

1064 """The profile line's count of values set by you; empty when there are none.""" 

1065 if count == 0: 

1066 return "" 

1067 return PROFILE_LINE_COUNT_ONE if count == 1 else PROFILE_LINE_COUNT.format(count=count) 

1068 

1069 

1070def profile_rebuild_reason_text(reason: RebuildReason, count: int, files: int) -> str: 

1071 """The Apply dialog's line on why it offers a rebuild; empty when there is nothing to say.""" 

1072 if reason is RebuildReason.HELD_OUT: 

1073 return PROFILE_APPLY_HELD_OUT 

1074 if reason is RebuildReason.SETS_REINDEX: 

1075 return PROFILE_APPLY_SETS_REINDEX 

1076 return profile_reindex_text(count, files) if files else "" 

1077 

1078 

1079def settings_rebuild_text(reindex: bool, held_out: bool) -> str | None: 

1080 """The rebuild question on leaving Settings: one text for both reasons, None for neither.""" 

1081 reasons = ((SETTINGS_REINDEX_MESSAGE, reindex), (PROFILE_APPLY_HELD_OUT, held_out)) 

1082 return " ".join(text for text, applies in reasons if applies) or None 

1083 

1084 

1085def profile_reindex_text(count: int, files: int) -> str: 

1086 """The Apply dialog's summary of the changes that rebuild the index.""" 

1087 if count == 1: 

1088 return PROFILE_APPLY_REINDEX_ONE.format(files=files) 

1089 return PROFILE_APPLY_REINDEX_MANY.format(count=count, files=files)