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