Coverage for src/lilbee/cli/tui/messages.py: 100%
511 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"""Centralized user-facing messages for the TUI.
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"""
8from __future__ import annotations
10import logging
11from functools import lru_cache
12from importlib.util import find_spec
13from pathlib import Path
15from lilbee.core.config import cfg
16from lilbee.data.types import SyncResult
17from lilbee.providers.fleet.gpu_backends import IntelHintKind, IntelUtilHint
18from lilbee.runtime.progress import OcrBackendUsed
19from lilbee.wiki.shared import WIKI_TYPE_HEADINGS as _WIKI_TYPE_HEADINGS
21log = logging.getLogger(__name__)
24def app_title(model: str) -> str:
25 """The window title showing the active chat model. Single source so every
26 code path that sets the title uses the same format."""
27 return f"lilbee: {model}" if model else "lilbee"
30CMD_UNKNOWN = "Unknown command: {cmd}"
31CMD_ADD_NOT_FOUND = "Not found: {path}"
32CMD_ADD_SUCCESS = "Added {count} file(s), syncing..."
33CMD_ADD_RELOCATED = "{count} already indexed, location changed: relinked (no re-embed)."
34CMD_ADD_DUPLICATE_TITLE = "File already in knowledge base"
35CMD_ADD_DUPLICATE_MESSAGE = "{name} is already in the knowledge base. Overwrite and re-sync?"
36CMD_ADD_SKIPPED_DUPLICATE = "Kept existing copy of {name}."
37CMD_ADD_NAME_TAKEN = "The name {name} is taken by another source (use --force to overwrite)."
38CMD_ADD_TRACKED = "Already tracked: {names}. Syncing."
39CMD_ADD_OVERLAPPING = "Overlaps a registered source: {names}. Syncing."
40CMD_ADD_NOTHING = "Nothing to add: no path reached the knowledge base."
41CMD_ADD_ERROR = "Error: {error}"
42CMD_CRAWL_USAGE = "Usage: /crawl <url> [--depth N] [--max-pages N]"
43CMD_CRAWL_STARTED = "Crawling {url}..."
44CMD_CRAWL_PAGE = "Crawling [{current}/{total}]: {url}"
45CMD_CRAWL_PAGE_INDETERMINATE = "Crawling... ({current} pages so far): {url}"
46MODEL_REASON_DEFAULT = "it could not be resolved"
47MODEL_FALLBACK_NOTICE = (
48 "{label} model {original!r} is unavailable ({reason}); using {effective!r} for this session. "
49 "Pick a different model or restore the original to clear this notice."
50)
51MODEL_FALLBACK_FAILED = (
52 "{label} model {original!r} is unavailable ({reason}) and the fallback {effective!r} was "
53 "rejected; keeping {original!r}. Pick a working {label} model in settings."
54)
55MODEL_UNUSABLE_NO_FALLBACK = (
56 "{label} model {original!r} is unavailable ({reason}) and nothing is installed to fall back "
57 "to. Pick one from the catalog."
58)
59MODEL_ADOPTED_LOG = "{label} model: using installed {effective!r}."
60CMD_CRAWL_SUCCESS = "Crawled {count} page(s) from {url}"
61CMD_CRAWL_PAGES_FAILED = "{count} page(s) failed; last: {reason}"
62CMD_CRAWL_FAILED = "Crawl failed: {error}"
63CMD_CRAWL_SYNCING = "Syncing crawled pages..."
64SETUP_CHROMIUM_NAME = "Install Chromium browser"
65SETUP_CHROMIUM_FAILED = "Chromium install failed: {error}"
66SETUP_CHROMIUM_DETAIL = "chromium: {done}/{total} MB"
67SETUP_CHROMIUM_DETAIL_UNKNOWN = "chromium: {done} MB"
68SETUP_CHROMIUM_CLI_PROGRESS = " chromium: {pct}%"
69SYNC_FAILED_FILES = "Sync failed for {files}"
70SYNC_SKIPPED_NO_VISION = (
71 "Skipped (no text extracted): {files}. "
72 "Configure a vision_model in Settings to OCR scanned PDFs."
73)
74SYNC_SKIPPED_OCR_OFF = (
75 "Skipped (no text extracted, OCR is off): {files}. "
76 "Set enable_ocr to true in Settings to OCR scanned PDFs."
77)
78SYNC_SKIPPED_VISION_FAILED = (
79 "Skipped (vision OCR returned no text): {files}. See {log_path} for the underlying error."
80)
81SYNC_HELD_OUT = (
82 '{count} file(s) stay held out of the index. Run "Retry skipped documents" to index them again.'
83)
84STATUS_HELD_OUT_TITLE = "Held out of the index"
85STATUS_HELD_OUT_EMPTY = "No files are held out."
86STATUS_HELD_OUT_MORE = "{count} more held out"
87CMD_RETRY_SKIPPED_NONE = "No failed files to retry; running a normal sync."
88CMD_RETRY_SKIPPED_SOME = "Retrying {count} failed file(s); removed sources stay removed."
89CMD_PRUNE_IGNORED_NONE = "Nothing indexed matches your ignore patterns."
90CMD_PRUNE_IGNORED_SOME = "Dropped {count} document(s) your ignore patterns exclude."
93def sync_skipped_message(result: SyncResult, log_path: Path) -> str:
94 """The skipped-files message for the OCR the skipped files' extraction ran with.
96 OCR off names the setting; vision OCR that returned nothing points at
97 *log_path* (the running process's own log file); otherwise (Tesseract, or
98 no OCR involved) the advice is a vision model.
99 """
100 files = ", ".join(result.skipped)
101 backends = {report.backend for report in result.skipped_ocr.values()}
102 if OcrBackendUsed.NONE in backends:
103 return SYNC_SKIPPED_OCR_OFF.format(files=files)
104 if OcrBackendUsed.VISION in backends:
105 return SYNC_SKIPPED_VISION_FAILED.format(files=files, log_path=log_path)
106 return SYNC_SKIPPED_NO_VISION.format(files=files)
109def retry_skipped_message(count: int) -> str:
110 """Toast for the 'Retry skipped documents' command."""
111 return CMD_RETRY_SKIPPED_NONE if count == 0 else CMD_RETRY_SKIPPED_SOME.format(count=count)
114def prune_ignored_message(count: int) -> str:
115 """Toast for the '/prune-ignored' command."""
116 return CMD_PRUNE_IGNORED_NONE if count == 0 else CMD_PRUNE_IGNORED_SOME.format(count=count)
119CMD_DELETE_NO_DOCS = "No documents indexed"
120CMD_DELETE_READ_FAILED = "Could not read the document list"
121CMD_DELETE_USAGE = "Documents: {names}\nUsage: /delete <filename>"
122CMD_DELETE_NOT_FOUND = "Not found: {name}"
123CMD_DELETE_SUGGESTION = "Did you mean {name}?"
124CMD_DELETE_SUCCESS = "Deleted {name}"
125CMD_REMEMBER_USAGE = "Usage: /remember <text> (prefix with 'pref:' for a preference)"
126CMD_REMEMBER_SUCCESS = "Remembered ({kind})."
127CMD_REMEMBER_NO_EMBED = "No embedding model. Press m and install one to save memories."
128MEMORY_AUTO_EXTRACTED = "Noted {count} memory(s) to review in /memories"
129CMD_EXPORT_USAGE = "Usage: /export <path.parquet|path.jsonl>"
130CMD_EXPORT_SUCCESS = "Exported {pages} page(s) to {output}"
131CMD_IMPORT_USAGE = "Usage: /import <path.parquet|path.jsonl>"
132CMD_IMPORT_SUCCESS = "Imported {sources} source(s) ({pages} pages, {chunks} chunks)"
133CMD_RESET_SUCCESS = "Knowledge base reset"
134CMD_RESET_PARTIAL = "Knowledge base reset ({skipped} item(s) could not be deleted)"
135CMD_RESET_FAILED = "Reset failed: {error}"
136CMD_RESET_CONFIRM_TITLE = "Reset the knowledge base?"
137CMD_RESET_CONFIRM_MESSAGE = "This permanently deletes all indexed data."
138CMD_REBUILD_CONFIRM_TITLE = "Rebuild the index?"
139CMD_REBUILD_CONFIRM_MESSAGE = (
140 "Drops every chunk and re-embeds the documents directory from "
141 "scratch. Takes minutes on large libraries. Source files on disk "
142 "are left alone."
143)
144TASK_NAME_SYNC = "Sync documents"
145TASK_NAME_WIKI = "Wikify"
146TASK_NAME_WIKI_WIPE = "Delete wiki"
147TASK_NAME_REBUILD = "Rebuild index"
148TASK_NAME_IMPORT = "Import {file}"
149TASK_NAME_EXPORT = "Export {file}"
150IMPORT_STATUS_LOADING = "Loading dataset..."
151EXPORT_STATUS_RUNNING = "Exporting..."
152CMD_SET_UNKNOWN = "Unknown setting: {key}"
153CMD_SET_SUCCESS = "{key} = {value}"
154# Stands in for a credential's value wherever one would otherwise be echoed.
155MASKED_VALUE = "************"
156CMD_SET_INVALID = "Invalid value for {key}: {error}"
157CMD_SET_TYPE_HINT = "{key} needs {kind}"
158CMD_SET_CHOICES = "{key} must be one of {choices}"
159CMD_SET_READONLY = "{key} is read-only; use the Models screen"
160CMD_MODEL_SET = "Model set to {name}"
161# Shown while a model swap's fleet reload runs off the event loop, so the swap
162# reads as in-progress instead of a frozen TUI. Role-neutral: shared by the chat
163# swap and the embed/vision/rerank swaps in model_pick, which name the model in
164# their own surfaces (the chat input placeholder and warm footer for chat).
165MODEL_SWAP_APPLYING = "Switching model, loading…"
166MODEL_SWAP_QUEUED = "Switching to {name} when this answer finishes"
167MODEL_SWAP_DONE = "Now using {name}"
168MODEL_SWAP_FAILED = "Could not switch model: {error}"
169# Chat-input placeholder while a swap holds the input disabled: names the target
170# model and says the input is waiting on it, so the disabled box is never a
171# silent dead end. The warm progress itself shows in the task-bar footer.
172CHAT_INPUT_SWITCHING = "Switching to {name} · chat unlocks when the model is ready…"
173# Chat-input placeholder while a placement change reloads the whole fleet.
174CHAT_INPUT_RELOADING = "Reloading the engine · one moment…"
175# Shown when the user tries to send a prompt while the new chat model is still loading.
176CHAT_MODEL_SWITCHING = "Still switching model. One moment, then send your prompt."
177FLEET_RELOADING = "Applying placement, reloading the fleet. One moment, then send your prompt."
178# Startup gate: shown from the moment the TUI paints until the app can serve.
179STARTUP_PREPARING = "Preparing lilbee"
180STARTUP_FAILED = "lilbee could not start: {error}"
181CHAT_STACK_FAILED = "lilbee could not load its chat screen: {error}"
182# Engine-load status painted into the pending answer while a prompt waits on a
183# cold engine, and the failure that wait can end in.
184ENGINE_READING_WEIGHTS = "Reading {name} weights"
185ENGINE_WARMING = "Warming up the model"
186# Names the phase, like its siblings above; "almost ready" promised a finish
187# time the engine had not committed to (same rename as TASKBAR_WARM_LOADING).
188ENGINE_ALMOST_READY = "Loading the engine"
189ENGINE_LOAD_FAILED = "The engine failed to load: {error}"
190ENGINE_FAILED_HINT = "Open the Catalog to install a model, or pick a different one in Settings."
191ENGINE_NOT_READY = "The engine is not ready yet. Send your prompt again in a moment."
192# Shown once when a prompt first waits on a cold engine and keep_engine_warm is off.
193ENGINE_WARM_TIP = "Tip: Settings > Keep engine warm makes the next launch fast"
194# Status painted into the pending answer when retrieval ran on a rewritten follow-up.
195SEARCHING_FOR = "Searching for: {query}"
196CMD_REMOVE_USAGE = "Usage: /remove <model_name>"
197CMD_REMOVE_NOT_FOUND = "{name} is not installed"
198CMD_REMOVE_SUCCESS = "Removed {name}"
199CMD_REMOVE_FAILED = "Failed to remove {name}"
200CMD_CANCEL = "Cancelled active operations"
201CMD_CLEAR = "Conversation cleared"
202SESSIONS_DISABLED_TITLE = "Sessions are turned off"
203SESSIONS_DISABLED_MESSAGE = (
204 "Conversations are not being saved. Turn sessions on in Settings to list, "
205 "resume, and manage past chats."
206)
207SESSIONS_COUNT = "{count} saved"
208SESSIONS_EMPTY = "No saved conversations yet."
209SESSIONS_FILTER_PLACEHOLDER = "Filter conversations…"
210SESSIONS_RENAME_PLACEHOLDER = "New name… enter saves, esc cancels"
211SESSIONS_ROW_META = "{count} msgs · {model}"
212SESSIONS_RESUMED = "Resumed · {title}"
213SESSIONS_MODEL_UNAVAILABLE = (
214 "This conversation used {model}, which isn't installed. Keeping {current}."
215)
216SESSIONS_NEW = "Started a new chat"
217SESSIONS_DELETED = "Deleted · {title}"
218SESSIONS_GONE = "This conversation was deleted, so it cannot be resumed."
219SESSIONS_RENAME_GONE = "This conversation was deleted, so it cannot be renamed."
220SESSIONS_DELETE_CONFIRM_TITLE = "Delete session"
221SESSIONS_DELETE_CONFIRM = "Delete “{title}”? This cannot be undone."
222SESSIONS_HINT = "↵ resume ^n new ^r rename ^d delete esc close"
223SESSIONS_SAVE_FAILED = "Could not save the answer to this session: {error}"
224FORK_PICKER_TITLE = "Fork after an answer"
225FORK_PICKER_ANSWER_TO = "Answer to: {question}"
226FORK_PICKER_HINT = "↑↓ select Enter fork Esc close"
227FORK_DONE = "Forked · {title}"
228FORK_FAILED = "Could not fork: {error}"
229FORK_NO_SESSION = "Nothing to fork yet. Send a message first."
230FORK_NO_ANSWER = "Nothing to fork yet. You can fork after the first answer."
231FORK_SESSION_GONE = "This conversation was deleted, so it cannot be forked."
232EXPORT_CHAT_DONE = "Exported to {path}"
233EXPORT_CHAT_FAILED = "Could not export: {error}"
234EXPORT_CHAT_NO_SESSION = "Nothing to export yet. Send a message first."
235EXPORT_CHAT_SESSION_GONE = "This conversation was deleted, so it cannot be exported."
236# The context chip: how much of this chat the model can still see.
237#
238# "context", not "memory": lilbee already has a Memory feature (/memories, the
239# Memory settings group), so "memory 85%" reads as though those were 85% full.
240# The jargon was never the problem; the collision was.
241CONTEXT_CHIP_USAGE = "context {percent}%"
242# Only when compaction is off and the window is nearly full: say what is about to
243# happen, not what to do. The user can still act (turn compaction on, or ask the
244# thing they care about now), which is what makes it worth saying at all. With
245# compaction on there is nothing to decide, so the plain percentage stands.
246CONTEXT_CHIP_USAGE_DROPPING = "context {percent}% · dropping soon"
247CONTEXT_CHIP_COMPACTING = "condensing…"
248CONTEXT_CHIP_TOOLTIP = (
249 "How much of this chat still fits in the model's context. Older turns drop out "
250 "when it fills; they stay on screen but the model stops seeing them. Turn on "
251 "chat_compaction in Settings to condense them into a summary instead."
252)
254# Rules drawn in the log where the model's view of the chat changes. The
255# transcript above them stays whole and scrollable, so every one of these is
256# about what the model is *sent*, never about deleting anything.
257#
258# One shape for the family -- "N earlier messages <what happened>" -- so a reader
259# learns it once instead of parsing three sentences about one idea. These are
260# titles for a rich Rule, which draws the line out to the full width itself: no
261# dashes belong in the strings.
262CHAT_COMPACTED = "{count} earlier messages condensed to a summary"
263CHAT_TRIMMED = "{count} earlier messages dropped from context"
264# Turns that fell out with no summary standing in for them: this model's context
265# is too small to carry that much conversation, however it is condensed. Say so,
266# or the model just looks like it forgot for no reason.
267CHAT_COMPACTION_STRANDED = "{count} more dropped · too much for this context"
269CHAT_COMPACTED_TOAST = "The context filled up, so earlier turns were condensed to keep them."
270CHAT_TRIMMED_TOAST = (
271 "The chat outgrew this model's context, so earlier turns dropped out of what it "
272 "can see. Turn on chat_compaction in Settings to condense them instead."
273)
274CHAT_COMPACTED_STRANDED_TOAST = (
275 "This model's context is too small for the whole conversation. Recent turns were "
276 "condensed; older ones were dropped."
277)
278CMD_THEME_UNKNOWN = "No theme called {name}. Themes: {names}"
279CMD_WIKI_DISABLED = "Wiki is disabled (set wiki = true in settings)"
280CMD_WIKI_GENERATE_NO_EVIDENCE = (
281 "Nothing left in the index for {slug}; its sources are gone. "
282 "Run `lilbee wiki index` to refresh."
283)
284CMD_WIKI_WIPE_NEEDS_YES = "Use --yes to confirm wiping the wiki in JSON mode"
285CMD_WIKI_WIPE_WARNING = (
286 "This deletes every generated wiki page and its indexed rows.\n Pages: {path}"
287)
288TASK_NAME_CRAWL = "Crawl {url}"
289STREAM_ERROR = "\n\n*Error: {error}*"
290STREAM_DISCONNECTED = "\n\n*The engine connection dropped before the answer finished.*"
291STREAM_CANCELLED = "\n\n*Response cancelled.*"
292SYNC_STATUS_SYNCING = "Syncing..."
293SYNC_STATUS_DONE = "Synced ({count} docs)"
294SYNC_STATUS_FAILED = "Sync failed"
295SYNC_FILE_PROGRESS = "Syncing [{current}/{total}]: {file}"
296SYNC_TESSERACT_OCR = (
297 "Running Tesseract OCR on the scanned pages of {file} ({total} pages in the file)"
298)
299SYNC_ALREADY_ACTIVE = "Sync in progress, please wait"
300EMBEDDING_SET = "Embedding model: {name}"
301CMD_CRAWL_UNAVAILABLE = "Web crawling is not available. Run 'uv sync --extra crawler' to enable it."
302CONFIRM_YES_LABEL = "Yes (y)"
303CONFIRM_NO_LABEL = "No (n)"
304CRAWL_DIALOG_TITLE = "Crawl a URL"
305CRAWL_DIALOG_URL_PLACEHOLDER = "example.com (https:// added automatically)"
306CRAWL_DIALOG_DEPTH_PLACEHOLDER = "blank = no limit"
307CRAWL_DIALOG_MAX_PAGES_PLACEHOLDER = "clear for unlimited"
308CRAWL_DIALOG_URL_LABEL = "URL"
309CRAWL_DIALOG_RECURSIVE_LABEL = "Recursive (crawl whole site)"
310CRAWL_DIALOG_BROWSER_LABEL = "Use browser (enables JavaScript, uses more memory)"
311CRAWL_DIALOG_DEPTH_LABEL = "Depth cap"
312CRAWL_DIALOG_MAX_PAGES_LABEL = "Max pages (clear for unlimited)"
313CRAWL_DIALOG_SUBMIT = "Crawl"
314CRAWL_DIALOG_CANCEL = "Cancel"
315CRAWL_DIALOG_URL_REQUIRED = "URL is required"
316CRAWL_DIALOG_INVALID_URL = "Invalid URL: {error}"
317CRAWL_DIALOG_INVALID_NUMBER = "{field} must be a positive integer or blank"
318THEME_SET = "Theme: {name}"
319HEADING_INSTALLED = "Installed"
320HEADING_MATCHES = "Matches"
321CATALOG_TAB_LOCAL = "Local"
322CATALOG_TAB_FRONTIER = "Frontier"
323CATALOG_TAB_DISCOVER = "Discover"
324CATALOG_TAB_CHAT = "Chat"
325CATALOG_TAB_EMBED = "Embed"
326CATALOG_TAB_VISION = "Vision"
327CATALOG_TAB_RERANK = "Rerank"
328CATALOG_TAB_LIBRARY = "Library"
329CATALOG_FRONTIER_SUMMARY = "{count} cloud models across {providers} providers"
330CATALOG_GRID_OVERFLOW = "+{count} more on HF. Press v for the full list view"
331CATALOG_GRID_LOAD_MORE = "{count} loaded · keep scrolling for more"
332CATALOG_GRID_ALL_LOADED = "All {count} models loaded"
333CATALOG_GRID_CUT_SHORT = "{count} models loaded · the listing stops here, search to narrow it"
334CATALOG_LIST_COUNT = "{count} models"
335CATALOG_LIST_CUT_SHORT = "{count} models · listing cut short, search to narrow it"
336CATALOG_GRID_LOADING_MORE = "{frame} loading more models…"
337CATALOG_USING_FRONTIER = "Using {name} via the {provider} API"
338CATALOG_NEEDS_KEY = "{provider} needs an API key. Set {key_field} in Settings to enable this model."
339CATALOG_KEY_REJECTED = (
340 "{provider} rejected the API key. Set a valid {key_field} in Settings to enable this model."
341)
342KEY_STATUS_READY = "ready"
343KEY_STATUS_NEEDS_KEY = "needs key"
344KEY_STATUS_KEY_REJECTED = "key rejected"
345CATALOG_USING_REMOTE = "Using {name} (remote)"
346CATALOG_ALREADY_INSTALLED = "{name} is already installed"
347CATALOG_ALREADY_DOWNLOADING = "{name} is already downloading, press t to watch it"
348CATALOG_QUEUED_DOWNLOAD = "Queued download: {name}"
349CATALOG_WELCOME = "Pick a model to start chatting. The fit chip shows what runs on this machine."
350CHAT_READY_TOAST = "Chat is ready. Press c."
351CATALOG_INSTALLED_OK = "{name} installed"
352CATALOG_GATED_REPO = "{name} requires login, run /login or lilbee login"
353CATALOG_DOWNLOAD_FAILED = "{name}: download failed"
354CATALOG_SELECT_FOR_INFO = "Select a model to view info"
355CATALOG_FRONTIER_NO_INFO = "Info modal is for downloadable models only"
356MODEL_INFO_HINT = "Esc / i / q to close"
357MODEL_INFO_HF_LINK = "View on HuggingFace: https://huggingface.co/{repo}"
358CATALOG_SELECT_TO_DELETE = "Select a model to delete"
359CATALOG_NOT_INSTALLED = "{name} is not installed"
360CATALOG_CONFIRM_DELETE = "Delete {name}? Press d again to confirm"
361CATALOG_DELETED = "Deleted {name}"
362CATALOG_DELETE_FAILED = "Delete failed: {error}"
363CATALOG_NO_MATCH = "No models match your filters."
364CATALOG_FILTER_PLACEHOLDER = "Filter models..."
365CATALOG_VIEW_TOGGLE_GRID = "Press v for full list view · / to search"
366CATALOG_VIEW_TOGGLE_LIST = "Press v for card view · s to sort"
367CATALOG_VIEW_GRID = "Grid"
368CATALOG_VIEW_LIST = "List"
369CATALOG_SORT_LIST_ONLY = "Sort is available in list view (press v)"
370CATALOG_SEARCHING_HF = "Searching HuggingFace…"
371CATALOG_SEARCH_HF_CTA = '→ Search HuggingFace for "{query}"'
372CHAT_INPUT_PLACEHOLDER_DEFAULT = "Ask… / commands ? keys F2 all commands"
373# Replaces Textual's default magnifying-glass emoji, which the system emoji font
374# draws in its own colors and at double width. Single cell, and distinct from the
375# task list's ▶ so the two never read as the same mark.
376COMMAND_PALETTE_ICON = "✦"
377# Box-drawing, not block elements: shade blocks are dither patterns that draw as
378# sparse dashes and full blocks seam per cell wherever the font is not cell-exact.
379# Box-drawing pair: the safe default for terminals whose fonts do not tile
380# block elements cell-exact (shade blocks render as sparse dashes there).
381PROGRESS_BAR_FILL = "━"
382PROGRESS_BAR_TRACK = "─"
383# Block pair: the full-weight bars, used where the terminal tiles them.
384PROGRESS_BAR_FILL_BLOCK = "█"
385PROGRESS_BAR_TRACK_BLOCK = "░"
388def progress_bar_glyphs() -> tuple[str, str]:
389 """Bar fill/track pair: block elements where the terminal tiles them."""
390 from lilbee.cli.tui.color_compat import draws_block_bars
392 if draws_block_bars():
393 return PROGRESS_BAR_FILL_BLOCK, PROGRESS_BAR_TRACK_BLOCK
394 return PROGRESS_BAR_FILL, PROGRESS_BAR_TRACK
397SLASH_CATALOG_TITLE = "Slash Commands"
398SLASH_CATALOG_FILTER_PLACEHOLDER = "Filter commands..."
399SLASH_CATALOG_FOOTER_HINT = "↑↓ select Enter run Esc close"
400SLASH_CATALOG_NO_MATCH = "No commands match"
401HELP_HINT_COMMANDS = "type / for commands"
402HELP_HINT_KEYS = "? for keys"
403HELP_HINT_SEPARATOR = " · "
404SCOPE_PILL_BOTH = "Both"
405SCOPE_PILL_WIKI = "Wiki"
406SCOPE_PILL_RAW = "Raw"
407CHAT_BUSY = "Already answering. Press Ctrl+C to cancel, then submit your next prompt."
408CHAT_STOPPING = "Stopping the answer. Submit again in a moment."
409CHAT_MODEL_DOWNLOADING = "{name} is still downloading. Wait for it to finish, then submit."
410MODEL_BEING_DOWNLOADED = (
411 "{name} is still downloading. Wait for it to finish before setting it active."
412)
413CHAT_WELCOME_TITLE = "lilbee"
414CHAT_WELCOME_TAGLINE = "your local AI stack and personal encyclopedia."
415CHAT_WELCOME_HINT = "Press / for commands, or just ask."
416CHAT_WELCOME_NO_MODEL_HINT = "No chat model installed yet. Press m to pick one from the catalog."
417CHAT_INPUT_NO_MODEL = "No chat model. Press m to pick one"
418CHAT_LOGIN_PROMPT = "Paste your token with /login <token>"
419CHAT_LOGGED_IN = "Logged in to HuggingFace"
420CHAT_LOGIN_FAILED = "Login failed: {error}"
421CHAT_VERSION = "lilbee {version}"
422CHAT_RENDERING = "Rendering: {label}"
423SETTINGS_READ_ONLY = "read-only"
424SETTINGS_INVALID_VALUE = "Invalid value: {error}"
425SETTINGS_RESET_TO_DEFAULT_TOOLTIP = "Reset to default"
427EMBED_SWAP_CONFIRM_TITLE = "Switch embedding model?"
428EMBED_SWAP_CONFIRM_MESSAGE = (
429 "The vector store was built under a different embedder. "
430 "Switching invalidates it: search and ingest are disabled until you rebuild. "
431 "Run `lilbee rebuild` afterward (or press S to sync) to re-embed every document. "
432 "Continue?"
433)
434EMBED_SWAP_CANCELLED = "Embedding model swap cancelled"
435MODEL_ASSIGN_REJECTED = "Model not set: {error}"
437EMBED_ADOPT_CONFIRM_TITLE = "Use this index's embedder?"
438EMBED_ADOPT_CONFIRM_MESSAGE = (
439 "This index was built with embedding model '{model}'. Use it for this vault? "
440 "lilbee will download it if needed and switch to it. No rebuild is required."
441)
442EMBED_ADOPT_NOTICE = "This index was built with a different embedder ('{model}')."
443EMBED_ADOPT_REBUILD_NOTICE = (
444 "This index needs a {dim}-dim embedder. Rebuild it (press S to sync, or run "
445 "`lilbee rebuild`) to use your current model."
446)
447EMBED_ADOPTING = "Switching to embedder '{model}'..."
448EMBED_ADOPTED = "Now embedding with '{model}'."
449EMBED_ADOPT_FAILED = "Could not adopt embedder: {error}"
450EMBED_ADOPT_CANCELLED = "Kept the current embedder."
452SETTINGS_RESET_ALL_LABEL = "Reset all defaults"
453SETTINGS_RESET_ALL_CONFIRM_TITLE = "Reset all settings?"
454SETTINGS_RESET_ALL_CONFIRM_MESSAGE = (
455 "Every writable setting will be restored to its built-in default. "
456 "Readonly fields (like installed models) are not affected."
457)
458SETTINGS_RESET_ALL_SUCCESS = "All settings reset to defaults"
459SETTINGS_LIST_EDITOR_TITLE = "{key} ({count} lines)"
460SETTINGS_LIST_EDITOR_INVALID_REGEX = "Invalid regex on line {n}: {error}"
461SETTINGS_LIST_EDITOR_RESTORE_DEFAULTS = "Restore defaults"
462WIKI_EMPTY_STATE = "No wiki pages found"
463# spaCy installs its NER model as an importable top-level package.
464_SPACY_MODEL_PACKAGE = "en_core_web_sm"
465WIKI_EMPTY_NEEDS_SPACY_LEAF = "spaCy not installed (see right pane)"
466WIKI_EMPTY_NEEDS_SPACY_DETAIL = (
467 "## Wiki entity extraction needs spaCy\n\n"
468 "Install it then re-ingest documents:\n\n"
469 "```sh\n"
470 "uv pip install spacy\n"
471 "python -m spacy download en_core_web_sm\n"
472 "```"
473)
476def wiki_empty_state_leaf() -> str:
477 """Single-line sidebar tree leaf for the empty-wiki state."""
478 if not _spacy_available():
479 return WIKI_EMPTY_NEEDS_SPACY_LEAF
480 return WIKI_EMPTY_STATE
483def wiki_empty_state_detail() -> str:
484 """Right-pane markdown body for the empty-wiki state."""
485 if not _spacy_available():
486 return WIKI_EMPTY_NEEDS_SPACY_DETAIL
487 return WIKI_NO_CONTENT
490@lru_cache(maxsize=1)
491def _spacy_available() -> bool:
492 """True when spaCy and its NER model are both importable.
494 Presence check only: the empty state repaints on every view switch and
495 every filter keystroke, so loading the pipeline here would stall the UI
496 thread for a second per paint. Cached because the answer cannot change
497 within a session.
498 """
499 return all(find_spec(name) is not None for name in ("spacy", _SPACY_MODEL_PACKAGE))
502WIKI_SEARCH_PLACEHOLDER = "Filter pages..."
503WIKI_NO_CONTENT = "Select a page to view"
504WIKI_NO_MATCHES = "No pages match '{filter}'"
505WIKI_LOAD_FAILED_LEAF = "Failed to load pages (see right pane)"
506WIKI_LOAD_FAILED = "## Failed to load wiki pages\n\n{error}"
507WIKI_BUILD_STARTING = "Starting wiki run..."
508WIKI_BUILD_PHASE = "{phase}..."
509WIKI_BUILD_PAGE = "{label} ({current}/{total})"
510WIKI_BUILD_DONE = "Wiki build finished: {count} pages"
511WIKI_ALREADY_ACTIVE = "Wiki build in progress, please wait"
512WIKI_STUBS_HEADING = "Not written yet"
513WIKI_STUB_SUFFIX = "(not written)"
514WIKI_STUB_DETAIL = (
515 "# {title}\n\n"
516 "*This page has not been written yet.*\n\n"
517 "{label} appears in {sources}. Opening it offers to write the page, which "
518 "spends one LLM call and is GPU-heavy.\n\n"
519 "If you would rather not be asked about pages like this, turn the wiki off "
520 "in settings."
521)
522WIKI_STUB_CONFIRM_TITLE = "Write this page?"
523WIKI_STUB_CONFIRM_MESSAGE = (
524 "Writing {label} spends one LLM call and is GPU-heavy. It draws on {sources}.\n\n"
525 "You can turn the wiki off in settings if you would rather not be asked."
526)
527WIKI_STUB_TASK = "Write {label}"
528WIKI_STUB_DONE = "Wrote {label}"
529WIKI_STUB_FAILED = "Could not write {label}: {error}"
530WIKI_STUB_STALE = "Nothing left to write {label} from; its sources are gone"
531WIKI_WIPE_CONFIRM_TITLE = "Delete the wiki?"
532WIKI_WIPE_CONFIRM_MESSAGE = (
533 "This deletes every generated page and its indexed rows. "
534 "Your documents are not touched. This cannot be undone."
535)
536WIKI_WIPE_DISABLED_TITLE = "Wiki turned off. Delete what it generated?"
537WIKI_WIPE_DISABLED_MESSAGE = (
538 "Turning the wiki off stops new pages being written, but the pages already "
539 "generated stay on disk and in search. Delete them now?"
540)
541WIKI_WIPE_RUNNING = "Deleting wiki pages..."
542WIKI_WIPE_DONE = "Wiki deleted: {count} pages removed"
543WIKI_WIPE_NOTHING = "No wiki pages to delete"
544WIKI_INDEX_LABEL = "Index"
545WIKI_LOG_LABEL = "Log"
546WIKI_DRAFTS_TITLE = "Wiki Drafts"
547WIKI_DRAFTS_EMPTY = "No drafts pending review"
548WIKI_DRAFTS_LOAD_FAILED = "Failed to load drafts: {error}"
549WIKI_DRAFTS_COLUMN_SLUG = "Slug"
550WIKI_DRAFTS_COLUMN_KIND = "Kind"
551WIKI_DRAFTS_COLUMN_DRIFT = "Drift"
552WIKI_DRAFTS_COLUMN_FAITHFULNESS = "Faithfulness"
553WIKI_DRAFTS_COLUMN_PUBLISHED = "Published?"
554WIKI_DRAFTS_KIND_DRIFT = "drift"
555WIKI_DRAFTS_DIFF_EMPTY = "Select a draft to view its diff"
556WIKI_DRAFTS_DIFF_NONE = "(no differences)"
557WIKI_DRAFTS_DIFF_FAILED = "Failed to load diff: {error}"
558WIKI_DRAFTS_ACCEPT_CONFIRM_TITLE = "Accept draft?"
559WIKI_DRAFTS_ACCEPT_CONFIRM_MESSAGE = (
560 "Overwrite the published page with {slug} and re-index? This cannot be undone."
561)
562WIKI_DRAFTS_REJECT_CONFIRM_TITLE = "Reject draft?"
563WIKI_DRAFTS_REJECT_CONFIRM_MESSAGE = "Delete draft {slug}? The published page will not change."
564WIKI_DRAFTS_ACCEPTED = "Accepted {slug}"
565WIKI_DRAFTS_REJECTED = "Rejected {slug}"
566WIKI_DRAFTS_ACCEPT_FAILED = "Accept failed: {error}"
567WIKI_DRAFTS_REJECT_FAILED = "Reject failed: {error}"
568WIKI_DRAFTS_ACCEPT_TASK = "Accept draft {slug}"
569WIKI_DRAFTS_REJECT_TASK = "Reject draft {slug}"
570WIKI_DRAFTS_MISSING = "missing: {slug}"
571WIKI_DRAFTS_NO_MATCHES = "No drafts match '{filter}'"
572WIKI_DRAFTS_PUBLISHED_YES = "yes"
573WIKI_DRAFTS_PUBLISHED_NO = "no"
574WIKI_DRAFTS_SEARCH_PLACEHOLDER = "Filter drafts..."
575MEMORIES_EMPTY = "No memories stored. Use /remember to add one."
576MEMORIES_DISABLED = "Memory is off. Enable it with /set memory_enabled true."
577MEMORIES_LOAD_FAILED = "Failed to load memories: {error}"
578MEMORIES_COLUMN_KIND = "Kind"
579MEMORIES_COLUMN_SHARED = "Shared"
580MEMORIES_COLUMN_TEXT = "Memory"
581MEMORIES_FLAG_YES = "yes"
582MEMORIES_FLAG_NO = "no"
583MEMORIES_SEARCH_PLACEHOLDER = "Filter memories..."
584MEMORIES_EMPTY_STATE = (
585 "Memories are notes lilbee keeps about you between chats. Use /remember to save one."
586)
587MEMORIES_NO_MATCHES = "No memories match this filter."
588MEMORIES_DELETE_CONFIRM_TITLE = "Delete memory?"
589MEMORIES_DELETE_CONFIRM_MESSAGE = "Delete this memory? This cannot be undone."
590MEMORIES_DELETED = "Deleted memory"
591MEMORIES_DELETE_FAILED = "Delete failed: {error}"
592MEMORIES_DELETE_NOT_FOUND = "Memory not found; it may already be gone."
593MEMORIES_FLAG_NOT_FOUND = "Memory not found; it may already be gone."
594MEMORIES_SHARED_ON = "Shared with agents"
595MEMORIES_SHARED_OFF = "No longer shared with agents"
596MEMORIES_FLAG_FAILED = "Update failed: {error}"
597# Re-export the shared heading map with string keys so callers can
598# look up by raw ``page_type`` string without coercion.
599WIKI_TYPE_HEADINGS: dict[str, str] = {
600 kind.value: label for kind, label in _WIKI_TYPE_HEADINGS.items()
601}
602APP_QUIT_AGAIN_HINT = "Answer cancelled. Press Ctrl+C again to quit."
603SETUP_CARD_HINT = "↵ Enter to install"
604INSTALLED_CARD_HINT = "D / ⌫ to delete"
606# Architecture compatibility pill labels (catalog row).
607# SUPPORTED renders nothing to keep the row visually quiet for the common case.
608COMPAT_PILL_UNSUPPORTED = "unsupported"
609COMPAT_PILL_UNKNOWN = "untested"
611# Architecture compatibility copy for the catalog detail view + confirm modal.
612COMPAT_DETAIL_SENTENCE_SUPPORTED = "Supported by your llama.cpp build."
613COMPAT_DETAIL_SENTENCE_UNSUPPORTED = (
614 "Architecture {arch} is not in the supported set. Pull may fail at load."
615)
616COMPAT_DETAIL_SENTENCE_UNKNOWN = (
617 "Architecture unknown until download. Pull will probe the header first."
618)
619COMPAT_MODAL_TITLE = "Architecture not supported"
620COMPAT_MODAL_BODY = (
621 "This model uses architecture {arch}. Your lilbee build doesn't support it, "
622 "so loading after download will probably fail. Pull anyway?"
623)
624DEFAULT_VIEW = "Chat"
625CATALOG_VIEW = "Catalog"
626WIKI_VIEW = "Wiki"
627FLEET_VIEW = "Fleet"
628SESSIONS_VIEW = "Sessions"
629FLEET_TITLE = "Placement"
630FLEET_STATE_AUTO = "auto"
631FLEET_STATE_MANUAL = "manual"
632FLEET_STATE_EDITED = "edited · ctrl+s to apply"
633FLEET_STATE_REBUILDING = "rebuilding fleet…"
634FLEET_SINGLE_GPU_NOTE = "One graphics card: everything runs here."
635FLEET_GPU_PROBING = "probing GPUs…"
636FLEET_NO_GPUS = "(no GPUs detected)"
637# Shown when the device probe failed outright (e.g. a wedged GPU driver), so the
638# panel names the problem instead of sitting on the probing placeholder forever.
639FLEET_GPU_PROBE_FAILED = "GPU probe failed: {reason}"
640# Shown for a role that has no placement because its model isn't downloaded, so the
641# empty slot reads as a fixable state instead of "GPU placement is broken".
642FLEET_MODEL_NOT_DOWNLOADED = "{role}: {model} not downloaded, pull it to place it"
643FLEET_SAVED_PLACEMENT_IGNORED = (
644 "A saved placement does not fit this hardware and is being ignored. "
645 "Press the auto command to clear it, or set a new one."
646)
647# Shown when an Intel GPU's utilization is unreadable only because intel_gpu_top
648# lacks the CAP_PERFMON grant, so the muted "--" reads as a fixable state.
649FLEET_INTEL_UTIL_GRANT = (
650 "Intel GPU utilization needs a one-time grant: "
651 "sudo setcap cap_perfmon+ep {binary} (or Linux 6.5+ reads it with no setup)"
652)
653# Shown when intel_gpu_top is not installed at all: the i915 PMU covers kernels
654# too old to publish fdinfo engine counters, so installing igt-gpu-tools is the
655# only path to utilization there.
656FLEET_INTEL_UTIL_INSTALL = (
657 "Intel GPU utilization needs the igt-gpu-tools package "
658 "(then: sudo setcap cap_perfmon+ep $(command -v intel_gpu_top))"
659)
662def intel_util_hint_text(hint: IntelUtilHint) -> str:
663 """Localized fix instruction for an unreadable Intel util reading."""
664 if hint.kind is IntelHintKind.GRANT and hint.binary is not None:
665 return FLEET_INTEL_UTIL_GRANT.format(binary=hint.binary)
666 return FLEET_INTEL_UTIL_INSTALL
669FLEET_CMD_PREVIEW = "Preview"
670FLEET_CMD_APPLY = "Apply"
671FLEET_CMD_AUTO = "Auto"
672FLEET_TAG_SPLIT = "split"
673FLEET_TAG_SINGLE = "one card"
674FLEET_HELP_ICON = "?"
675# Single hover explanation for the whole drawer (kept friendly, no jargon).
676FLEET_HELP_TOOLTIP = (
677 "Top: how busy each GPU is right now.\n"
678 "Grid: what runs on which GPU.\n"
679 " • chat is one model split across the highlighted cards (they work as one).\n"
680 " • embed/vision run as a full copy on each highlighted card.\n"
681 " • rerank runs on one card; pick a single GPU.\n"
682 "Click a cell to change it, then Apply. Auto lets lilbee choose."
683)
684# The full nav-view universe in order. Single source for the view set: the
685# settings bar pre-creates a tab per entry (toggling Wiki visibility at
686# runtime), get_nav_views() gates Wiki, and app.get_views() derives its
687# factory map from get_nav_views().
688ALL_NAV_VIEWS: tuple[str, ...] = (
689 DEFAULT_VIEW,
690 CATALOG_VIEW,
691 "Status",
692 "Settings",
693 "Tasks",
694 WIKI_VIEW,
695 FLEET_VIEW,
696 SESSIONS_VIEW,
697)
700def get_nav_views() -> list[str]:
701 """Return the active nav view names, including Wiki when enabled."""
702 return [v for v in ALL_NAV_VIEWS if v != WIKI_VIEW or cfg.wiki]
705MODE_NORMAL = "NORMAL"
706MODE_INSERT = "INSERT"
707TASKBAR_HINT = "Press t for Tasks"
708TASKBAR_HINT_INPUT = "Esc then t for Tasks"
709CHAT_REASONING_FINISHED = "reasoning · {tokens} tokens"
711STATUS_DOCS_LOAD_FAILED = "(unable to read store)"
712STATUS_DOCS_EMPTY = "(no documents yet)"
713STATUS_DOCS_TITLE = "Documents"
714TASKBAR_STARTING_WORKER = "Starting {labels} worker..."
715TASKBAR_STARTING_WORKERS = "Starting {labels} workers..."
716# Cold-start chat warm line: a spinner, the model being loaded, and the phase
717# (with byte % while paging weights) so the held input reads as "loading {model}",
718# not "stuck". The name is the model's display label, or this fallback before the
719# warm has stamped which model it is loading.
720TASKBAR_WARM_LINE = "warming up {name} · {detail}"
721TASKBAR_WARM_FALLBACK_NAME = "chat"
722TASKBAR_WARM_STARTING = "starting engine"
723TASKBAR_WARM_READING = "reading weights {pct}%"
724# Names the phase, like its two siblings above. "almost ready" predicted a
725# finish time the engine has not promised, and was the one phase saying nothing
726# about what is happening.
727TASKBAR_WARM_LOADING = "loading into VRAM"
729TASK_CENTER_TITLE = "Background Tasks"
730TASK_CENTER_COUNTS = "{active} running · {queued} queued · {done} done"
731TASK_CENTER_HINT = "r refresh c cancel C clear done q back j/k navigate"
732TASK_CENTER_EMPTY_HEADLINE = "✓ all caught up"
733TASK_CENTER_EMPTY_DETAIL = "no background tasks"
734TASKBAR_SINGLE = "{name} [b]{pct:.1f}%[/b]"
735TASKBAR_MULTIPLE = "[b]{count} tasks running[/b]"
736TASKBAR_ONE = "[b]1 task running[/b]"
737TASKBAR_QUEUED_COUNT = "{count} queued"
738TASKBAR_ALL_DONE = "[b]Done[/b]"
739TASKBAR_FAILED = "[b]{count} task failed[/b]"
740TASKBAR_FAILED_PLURAL = "[b]{count} tasks failed[/b]"
741TASKBAR_SYNC_PENDING_ONE = "[b]1 doc to sync[/b] · S to sync"
742TASKBAR_SYNC_PENDING_PLURAL = "[b]{count} docs to sync[/b] · S to sync"
743TASKBAR_SYNC_PENDING_ONE_INPUT = "[b]1 doc to sync[/b] · Esc then S to sync"
744TASKBAR_SYNC_PENDING_PLURAL_INPUT = "[b]{count} docs to sync[/b] · Esc then S to sync"
745SYNC_CANCELLED_RESUME = "Sync cancelled. Press S to resume."
746SYNC_EMBEDDING = "Embedding {file}"
747SYNC_FILE_DONE = "Done: {file}"
748ADD_SYNCING_FILE = "Syncing {file}..."
749ADD_PAGE_PROGRESS = "{status} page {current} of {total}"
750ADD_FILE_DONE = "Done {file}"
752SETTINGS_API_KEYS_WARNING = (
753 "These keys are stored in plain text at {path}. "
754 "Anything you send to these providers leaves your machine. "
755 "Do not route sensitive documents from lilbee through them."
756)
757MODEL_BAR_CLOUD_PROVIDER_WARNING = (
758 "Chat prompts are being sent to {provider}. Do not share sensitive data."
759)
760CHAT_MODE_SEARCH_LABEL = "Search"
761CHAT_MODE_CHAT_LABEL = "Chat"
762CHAT_MODE_TOGGLE_TOOLTIP = (
763 "Search runs your question through document retrieval. "
764 "Chat skips retrieval and answers directly. Click or press F3 to flip."
765)
766CHAT_MODE_TOGGLE_DISABLED_TOOLTIP = (
767 "Search needs an embedding model. Press the pill to pick one from the catalog."
768)
769SEARCH_NEEDS_EMBEDDER = "Search needs an embedding model. Pick one to enable it."
770CHAT_MODE_SEARCH_NO_RESULTS = "Search returned 0 results, falling back to chat for this turn."
771CHAT_MODE_SET = "Mode: {label}"
772MODEL_PICKER_TITLE_CHAT = "Pick a chat model"
773MODEL_PICKER_TITLE_EMBED = "Pick an embedding model"
774MODEL_PICKER_TITLE_VISION = "Pick a vision model"
775MODEL_PICKER_TITLE_RERANK = "Pick a reranker model"
776MODEL_VALUE_NONE = "(none)"
777MODEL_PICKER_DISABLE_LABEL = "(disabled, no model)"
778MODEL_PICKER_CHAT_TOOLTIP = "Model used to answer your questions. Click to pick a different one."
779MODEL_PICKER_EMBED_TOOLTIP = (
780 "Model used to vectorize search queries (Search mode). Click to pick a different one."
781)
782MODEL_PICKER_VISION_TOOLTIP = (
783 "Optional. Model used to read scanned PDFs and images. Click to pick one or browse the catalog."
784)
785MODEL_PICKER_RERANK_TOOLTIP = (
786 "Optional. Model used to sharpen search results. Click to pick one or browse the catalog."
787)
788MODEL_PICKER_BROWSE_CATALOG = "Browse catalog to download..."
789MODEL_PICKER_SEARCH_PLACEHOLDER = "Search models..."
791# Model bar (chat-screen, below the input)
792MODEL_BAR_CHAT_LABEL = "Chat"
793MODEL_BAR_EMBED_LABEL = "Embed"
794MODEL_BAR_VISION_LABEL = "Vision"
795MODEL_BAR_RERANK_LABEL = "Rerank"
796MODEL_BAR_DISABLED = "disabled"
797MODEL_BAR_NONE = "none, pick one"
798MODEL_BAR_NOT_INSTALLED = "{name} (not installed)"
799MODEL_BAR_NOT_INSTALLED_TOOLTIP = (
800 "This model is not installed. Click to pick another, "
801 "or press m to open the Catalog and install it."
802)
803MODEL_PICKER_TURN_OFF = "Turn off this model"
804MODEL_PICKER_HINT = "Enter to pick · Esc to cancel · / to search"