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
« prev ^ index » next coverage.py v7.15.2, created at 2026-11-06 08:17 +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.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
23log = logging.getLogger(__name__)
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"
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."
119def sync_skipped_message(result: SyncResult, log_path: Path) -> str:
120 """The skipped-files message for the OCR the skipped files' extraction ran with.
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)
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)
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)
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)
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"
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 = "░"
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
432 if draws_block_bars():
433 return PROGRESS_BAR_FILL_BLOCK, PROGRESS_BAR_TRACK_BLOCK
434 return PROGRESS_BAR_FILL, PROGRESS_BAR_TRACK
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"
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}"
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."
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)
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
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
534@lru_cache(maxsize=1)
535def _spacy_available() -> bool:
536 """True when spaCy and its NER model are both importable.
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))
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"
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"
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)
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
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)
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]
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"
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"
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}"
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..."
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"
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)
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}
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)
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)
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)
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 ""
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
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)