All files / src/locales en.ts

100% Statements 179/179
100% Branches 30/30
100% Functions 148/148
100% Lines 174/174

Press n or j to go to the next uncovered block, b, p or k for the previous block.

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 398 399 400 401 402 403 404 405 406 407 408 409 410 411 412 413 414 415 416 417 418 419 420 421 422 423 424 425 426 427 428 429 430 431 432 433 434 435 436 437 438 439 440 441 442 443 444 445 446 447 448 449 450 451 452 453 454 455 456 457 458 459 460 461 462 463 464 465 466 467 468 469 470 471 472 473 474 475 476 477 478 479 480 481 482 483 484 485 486 487 488 489 490 491 492 493 494 495 496 497 498 499 500 501 502 503 504 505 506 507 508 509 510 511 512 513 514 515 516 517 518 519 520 521 522 523 524 525 526 527 528 529 530 531 532 533 534 535 536 537 538 539 540 541 542 543 544 545 546 547 548 549 550 551 552 553 554 555 556 557 558 559 560 561 562 563 564 565 566 567 568 569 570 571 572 573 574 575 576 577 578 579 580 581 582 583 584 585 586 587 588 589 590 591 592 593 594 595 596 597 598 599 600 601 602 603 604 605 606 607 608 609 610 611 612 613 614 615 616 617 618 619 620 621 622 623 624 625 626 627 628 629 630 631 632 633 634 635 636 637 638 639 640 641 642 643 644 645 646 647 648 649 650 651 652 653 654 655 656 657 658 659 660 661 662 663 664 665 666 667 668 669 670 671 672 673 674 675 676 677 678 679 680 681 682 683 684 685 686 687 688 689 690 691 692 693 694 695 696 697 698 699 700 701 702 703 704 705 706 707 708 709 710 711 712 713 714 715 716 717 718 719 720 721 722 723 724 725 726 727 728 729 730 731 732 733 734 735 736 737 738 739 740 741 742 743 744 745 746 747 748 749 750 751 752 753 754 755 756 757 758 759 760 761 762 763 764 765 766 767 768 769 770 771 772 773 774 775 776 777 778 779 780 781 782 783 784 785 786 787 788 789 790 791 792 793 794 795 796 797 798 799 800 801 802 803 804 805 806 807 808 809 810 811 812 813 814 815 816 817 818 819 820 821 822 823 824 825 826 827 828 829 830 831 832 833 834 835 836 837 838 839 840 841 842 843 844 845 846 847 848 849 850 851 852 853 854 855 856 857 858 859 860 861 862 863 864 865 866 867 868 869 870 871 872 873 874 875 876 877 878 879 880 881 882 883 884 885 886 887 888 889 890 891 892 893 894 895 896 897 898 899 900 901 902 903 904 905 906 907 908 909 910 911 912 913 914 915 916 917 918 919 920 921 922 923 924 925 926 927 928 929 930 931 932 933 934 935 936 937 938 939 940 941 942 943 944 945 946 947 948 949 950 951 952 953 954 955 956 957 958 959 960 961 962 963 964 965 966 967 968 969 970 971 972 973 974 975 976 977 978 979 980 981 982 983 984 985 986 987 988 989 990 991 992 993 994 995 996 997 998 999 1000 1001 1002 1003 1004 1005 1006 1007 1008 1009 1010 1011 1012 1013 1014 1015 1016 1017 1018 1019 1020 1021 1022 1023 1024 1025 1026 1027 1028 1029 1030 1031 1032 1033 1034 1035 1036 1037 1038 1039 1040 1041 1042 1043 1044 1045 1046 1047 1048 1049 1050 1051 1052 1053 1054 1055 1056 1057 1058 1059 1060 1061 1062 1063 1064 1065 1066 1067 1068 1069 1070 1071 1072 1073 1074 1075 1076 1077 1078 1079 1080 1081 1082 1083 1084 1085 1086 1087 1088 1089 1090 1091 1092 1093 1094 1095 1096 1097 1098 1099 1100 1101 1102 1103 1104 1105 1106 1107 1108 1109 1110 1111 1112 1113 1114 1115 1116 1117 1118 1119 1120 1121 1122 1123 1124 1125 1126 1127 1128 1129 1130 1131 1132 1133 1134 1135 1136 1137 1138 1139 1140 1141 1142 1143 1144 1145 1146 1147 1148 1149 1150 1151 1152 1153 1154 1155 1156 1157 1158 1159 1160 1161 1162 1163 1164 1165 1166 1167 1168 1169 1170 1171 1172 1173 1174 1175 1176 1177 1178 1179 1180 1181 1182 1183 1184 1185 1186 1187 1188 1189 1190 1191 1192 1193 1194 1195 1196 1197 1198 1199 1200 1201 1202 1203 1204 1205 1206 1207 1208 1209 1210 1211 1212 1213 1214 1215 1216 1217 1218 1219 1220 1221 1222 1223 1224 1225 1226 1227 1228 1229 1230 1231 1232 1233 1234 1235 1236 1237 1238 1239 1240 1241 1242 1243 1244 1245 1246 1247 1248 1249 1250 1251 1252 1253 1254 1255 1256 1257 1258 1259 1260 1261 1262 1263 1264 1265 1266 1267 1268 1269 1270 1271 1272 1273 1274 1275 1276 1277 1278 1279 1280 1281 1282 1283 1284 1285 1286 1287 1288 1289 1290 1291 1292 1293 1294 1295 1296 1297 1298 1299 1300 1301 1302 1303 1304 1305 1306 1307 1308 1309 1310 1311 1312 1313 1314 1315 1316 1317 1318 1319 1320 1321 1322 1323 1324 1325 1326 1327 1328 1329 1330 1331 1332 1333 1334 1335 1336 1337 1338 1339 1340 1341 1342 1343 1344 1345 1346 1347 1348 1349 1350 1351 1352 1353 1354 1355 1356 1357 1358 1359 1360 1361 1362 1363 1364 1365 1366 1367 1368 1369 1370 1371 1372 1373 1374 1375 1376 1377 1378 1379 1380 1381 1382 1383 1384 1385 1386 1387 1388 1389 1390 1391 1392 1393 1394 1395 1396 1397 1398 1399 1400 1401 1402 1403 1404 1405 1406 1407 1408 1409 1410 1411 1412 1413 1414 1415 1416 1417 1418 1419 1420 1421 1422 1423 1424 1425 1426 1427 1428 1429 1430 1431 1432 1433 1434 1435 1436 1437 1438 1439 1440 1441 1442                              45x                         45x           500x   45x 100x   45x   45x               68x   1x   61x   1x   1x   1x   3x       45x 9x                                                   2x                                             3x 8x 6x                                                                                                                                                                                                                                                                                                                                                   3x   4x       1x 4x           326x             1x                                                                                     508x                                                                             19x                             6x                                                 8x   30x                       17x 14x 3x 5x   4x       100x 37x     69x 69x 68x 68x 25x 1x 24x 1x   23x     69x       4x   219x                                               313x   10x                                   1x 2x       1x     2x       4x 103x                                                                                                                                                                                         527x                   40x     27x             6x 8x   3x 2x     24x   2x 3x   2x 1x 1x 1x 1x 1x                           12x   6x 3x 13x                               4x                                                         3x                             24x 25x 3x 3x       7x                 3x                                           10x 3x             2x     14x   4x       33x                           4x 3x 2x 2x       5x 6x 30x 4x 3x 2x 32x 107x 33x 2x                           7x 6x 4x 9x 12x 14x                 7x   4x                                                     2x 9x   2x     2x   6x                                                 48x       13x 12x 7x   100x                   74x                           35x 35x       7x 4x 5x 2x                             6x   6x       13x                                                                                                                                                                                       3x               3x 5x 5x                 2x   2x         2x                                                                                                                                                                                                                                       2x   290x   50x   50x       199x 4x 10x 10x 10x                     2x 3x           6x                                                                   2x     1x     1x                           3x 1x   2x                                     2x   1x 2x                           18x                                 5x   1x 1x       3x 1x                       16x         45x             45x           45x                                             45x             45x                                            
import {
    AGENT_CLIENT,
    AMD_PROBE_STATUS,
    type AmdProbe,
    CUDA_MIN_CEILING,
    type GpuDetection,
    MODEL_TASK,
    NVIDIA_PROBE_STATUS,
    type RocmRefusal,
    SERVER_VARIANT,
    type ServerVariant,
    type WorkerRole,
} from "../types";
 
/** How each server build is named in Settings and notices. */
const SERVER_BUILD_LABEL: Record<ServerVariant, string> = {
    [SERVER_VARIANT.DEFAULT]: "default",
    [SERVER_VARIANT.CU121]: "CUDA 12.1",
    [SERVER_VARIANT.CU124]: "CUDA 12.4",
    [SERVER_VARIANT.CU125]: "CUDA 12.5",
    [SERVER_VARIANT.ROCM]: "ROCm",
    [SERVER_VARIANT.COMPAT]: "compat",
    [SERVER_VARIANT.COMPAT_CU124]: "compat + CUDA 12.4",
    [SERVER_VARIANT.COMPAT_ROCM]: "compat + ROCm",
};
 
// Natural-language noun for each worker role, so tooltips read "embedding
// worker" rather than the internal "embed".
const ROLE_NOUN: Record<WorkerRole, string> = {
    chat: "chat",
    embed: "embedding",
    vision: "vision",
    rerank: "reranking",
};
const roleNoun = (role: WorkerRole): string => ROLE_NOUN[role];
/** A CUDA ceiling as the driver prints it: 1204 -> "12.4". */
const cudaVersionLabel = (ceiling: number): string => `${Math.floor(ceiling / 100)}.${ceiling % 100}`;
const indefinite = (word: string): string => (/^[aeiou]/i.test(word) ? "an" : "a");
 
const RETRY_SKIPPED_COMMAND = "Retry skipped documents";
 
const ROCM_REFUSAL_TEXT: Record<RocmRefusal, string> = {
    "no-asset": "This release has no ROCm build, so the default build runs.",
    "no-manifest": "The ROCm build's kernel list could not be read, so the default build runs.",
    "missing-kernels": "The ROCm build ships no kernels for these targets, so the default build runs.",
};
 
/** The AMD half of the detection sentence. */
function amdDetectionText(amd: AmdProbe): string {
    switch (amd.status) {
        case AMD_PROBE_STATUS.SKIPPED:
            return "lilbee ships a ROCm build for Linux only.";
        case AMD_PROBE_STATUS.MISSING:
            return "No AMD compute device.";
        case AMD_PROBE_STATUS.SANDBOXED:
            return "An amdgpu driver is loaded, but this Obsidian cannot reach /dev/kfd, so the GPU is unknown. Flatpak and Snap sandboxes hide it.";
        case AMD_PROBE_STATUS.UNREADABLE:
            return "The amdgpu driver is present, but its topology could not be read.";
        case AMD_PROBE_STATUS.DETECTED:
            return `AMD targets: ${amd.gfxTargets.join(", ")}.`;
        case AMD_PROBE_STATUS.UNSUPPORTED:
            return `AMD targets: ${amd.gfxTargets.join(", ")}. ${ROCM_REFUSAL_TEXT[amd.reason]}`;
    }
}
 
export const MESSAGES = {
    CONFIRM_REINDEX: (name: string): string => `"${name}" is already indexed. Re-add it?`,
    LABEL_VAULT_ROOT: "Vault root",
    CONFIRM_SYNC_REBUILD:
        "Rebuilding the index drops every stored document and re-embeds your whole vault from scratch. This can take a while. Continue?",
    GATEKEEPER_TITLE: "macOS blocked the lilbee server",
    GATEKEEPER_INTRO:
        "The lilbee server is an open-source binary that isn't code-signed, so macOS Gatekeeper quarantined it. The plugin tried to clear the quarantine flag automatically but couldn't. You can allow it in a few steps:",
    GATEKEEPER_STEP_1: "Open System Settings → Privacy & Security.",
    GATEKEEPER_STEP_2:
        'Scroll to the Security section. You\'ll see a message that "lilbee" was blocked. Click "Allow Anyway".',
    GATEKEEPER_STEP_3:
        "Reopen this vault (or run the lilbee command again). On the next launch, click Open when macOS asks.",
    GATEKEEPER_RETRY: "This is a one-time step. Once allowed, the server starts normally from then on.",
    GATEKEEPER_SOURCE: "lilbee is open source. You can review the server's code or build it yourself:",
    LINK_LILBEE_REPO: "github.com/tobocop2/lilbee",
    BUTTON_GOT_IT: "Got it",
    BUTTON_SKIP_SETUP: "Skip setup",
    BUTTON_GET_STARTED: "Get started",
    BUTTON_BACK: "Back",
    BUTTON_NEXT: "Next",
    BUTTON_CONTINUE: "Continue",
    BUTTON_CANCEL: "Cancel",
    BUTTON_PULL: "Pull",
    BUTTON_PULL_MODEL: "Pull Model",
    BUTTON_PULL_ANYWAY: "Pull anyway",
    WARNING_MODEL_EXCEEDS_RAM: (needed: number, have: number) =>
        `This model needs ${needed} GB of RAM but your system has only ${have} GB. Loading it will likely fail.`,
    BUTTON_USE: "Use",
    BUTTON_REMOVE: "Delete",
    BUTTON_REFRESH: "Refresh",
    BUTTON_BROWSE_CATALOG: "Browse Catalog",
    BUTTON_BROWSE_FULL_CATALOG: "Browse full catalog",
    BUTTON_BROWSE_MORE: "Browse more…",
    BUTTON_DOWNLOAD_CONTINUE: "Download & continue",
    BUTTON_DELETE_SELECTED: "Remove selected",
    BUTTON_CRAWL: "Crawl",
    BUTTON_START: "Start",
    BUTTON_STOP: "Stop",
    BUTTON_RESTART: "Restart",
    BUTTON_TEST: "Test",
    BUTTON_RESET_MANAGED: "Reset to managed",
    BUTTON_RUN_SETUP_WIZARD: "Run setup wizard",
    BUTTON_INSTALL_SERVER: "Install server",
    BUTTON_REINSTALL: "Reinstall",
    BUTTON_RETRY: "Retry",
    BUTTON_UNINSTALL_SERVER: "Uninstall server",
    BUTTON_UNINSTALL: "Uninstall",
    BUTTON_DOWNLOADING: "Downloading...",
    BUTTON_CANCEL_DOWNLOAD: "Cancel download",
    BUTTON_INSTALL_TAG: (tag: string) => `Install ${tag}`,
    BUTTON_UPDATE_TO: (tag: string) => `Update to ${tag}`,
    BUTTON_DOWNGRADE_TO: (tag: string) => `Downgrade to ${tag}`,
    BUTTON_CLEAR_CHAT: "Clear chat",
    BUTTON_CLEAR_TASKS: "Clear",
    BUTTON_SEND: "Send",
    BUTTON_OPEN_CHAT: "Open chat",
 
    LABEL_NOT_SET: "Not set",
    LABEL_NO_MODEL_SELECTED: "no model selected",
    LABEL_UNKNOWN: "unknown version",
    LABEL_ALL_TASKS: "All tasks",
    LABEL_ALL_SIZES: "All sizes",
    LABEL_FEATURED: "Featured",
    LABEL_DOWNLOADS: "Downloads",
    LABEL_NAME: "Name",
    LABEL_SIZE_ASC: "Size (asc)",
    LABEL_SIZE_DESC: "Size (desc)",
    LABEL_ACTIVE: "Active",
    LABEL_INSTALLED: "Installed (shared)",
    LABEL_NOT_INSTALLED: " (not installed)",
    LABEL_MODEL: "Model",
    LABEL_SIZE: "Size",
    LABEL_MIN_RAM: "Minimum RAM:",
    LABEL_DESCRIPTION: "Description",
    LABEL_CHAT_MODEL: "Chat Model",
    LABEL_REASONING: "Reasoning",
    LABEL_SOURCES: "Sources",
    LABEL_DEPTH: "Depth cap",
    LABEL_MAX_PAGES: "Max pages",
    LABEL_CRAWL_ADVANCED: "Advanced",
    LABEL_CRAWL_RECURSIVE: "Crawl whole site",
    LABEL_CRAWL_RECURSIVE_INFO: "About whole-site crawl",
    NOTICE_CRAWL_RECURSIVE:
        "Whole-site crawl follows every internal link it finds on the same origin. Large sites can fetch hundreds of pages and take a while. Use Advanced to cap depth or page count. Request delay, concurrency and retry behavior live in Settings → Crawling.",
    LABEL_CRAWL_USE_BROWSER: "Use browser (enables JavaScript, uses more memory)",
    LABEL_CRAWL_SUBDOMAINS: "Include subdomains",
    TOOLTIP_CRAWL_URL: "Web page to crawl. A scheme is added for you if you leave it off.",
    TOOLTIP_CRAWL_RECURSIVE: "Follow internal links on the same site instead of crawling just this page.",
    TOOLTIP_CRAWL_SUBDOMAINS:
        "Also follow links to subdomains of the starting site (for example docs.example.com from example.com).",
    TOOLTIP_CRAWL_USE_BROWSER:
        "Render pages in Chromium so content built with JavaScript is captured. Use it for sites that render client-side.",
    TOOLTIP_CRAWL_DEPTH: "How many links deep to follow from the starting page. Blank means no limit.",
    TOOLTIP_CRAWL_MAX_PAGES: "Stop after fetching this many pages. Blank means no limit.",
    HINT_CRAWL_BLANK_NO_LIMIT: "blank = no limit",
    ERROR_CRAWL_MAX_PAGES_POSITIVE: "Max pages must be a positive integer or blank",
    ERROR_CRAWL_DEPTH_INVALID: "Depth cap must be a non-negative integer or blank",
    LABEL_SERVER_MODE: "Server mode",
    LABEL_SERVER_STATUS: "Server status",
    LABEL_SERVER_CONTROLS: "Server controls",
    LABEL_SERVER_VERSION: "Server version",
    LABEL_INCLUDE_DEV_BUILDS: "Include dev builds",
    LABEL_SERVER_AUTO_UPDATE: "Automatically update the server",
    LABEL_SERVER_UPDATE_REMINDER: "Remind me about server updates",
    LABEL_UNINSTALL: "Uninstall",
    LABEL_UNINSTALL_SERVER: "Uninstall server",
    LABEL_INSTALL_SERVER: "Install server",
    LABEL_SERVER_URL: "Server URL",
    LABEL_SETUP_WIZARD: "Setup wizard",
    LABEL_SWITCH_MANAGED: "Switch to managed server",
    LABEL_MODELS: "Models",
    LABEL_REFRESH_MODELS: "Refresh models",
    LABEL_CHAT_SECTION: "Chat",
    LABEL_SEARCH_RETRIEVAL: "Search & Retrieval",
    LABEL_RESULTS_COUNT: "Results count",
    LABEL_MAX_DISTANCE: "Search strictness",
    LABEL_ADAPTIVE_THRESHOLD: "Adaptive threshold",
    LABEL_GENERATION: "Advanced settings",
    LABEL_RAG_SYSTEM_PROMPT: "RAG system prompt (when answering with documents)",
    LABEL_GENERAL_SYSTEM_PROMPT: "Chat system prompt (when answering without documents)",
    LABEL_CHAT_MODE: "Chat mode",
    LABEL_CHAT_MODE_SEARCH: "Search",
    LABEL_CHAT_MODE_CHAT: "Chat",
    TAB_LOCAL: "Local",
    TAB_FRONTIER: "Hosted",
    TAB_DISCOVER: "Discover",
    TAB_CHAT_MODELS: "Chat",
    TAB_EMBED_MODELS: "Embed",
    TAB_VISION_MODELS: "Vision",
    TAB_RERANK_MODELS: "Rerank",
    TAB_LIBRARY: "Library",
    RAIL_FOR_YOU: "For you",
    RAIL_YOUR_COLLECTION: "Your collection",
    RAIL_FRESH: "Fresh",
    RAIL_NO_ITEMS: "Nothing here yet.",
    RAIL_FOR_YOU_HELP: "Featured models picked for your active chat task.",
    RAIL_YOUR_COLLECTION_HELP: "Models you've already pulled, across every task.",
    RAIL_FRESH_HELP: "The most-downloaded models in the catalog right now.",
    PILL_KEY_READY: "Ready",
    PILL_KEY_NEEDS_KEY: "Needs key",
    MODEL_PICKER_TITLE_CHAT: "Pick chat model",
    MODEL_PICKER_TITLE_EMBED: "Pick embedding model",
    MODEL_PICKER_SEARCH_PLACEHOLDER: "Search models…",
    MODEL_PICKER_LOCAL_HEADING: "Local",
    MODEL_PICKER_EMPTY: "No models match.",
    COMMAND_MODEL_PICKER_CHAT: "Pick chat model",
    COMMAND_MODEL_PICKER_EMBED: "Pick embedding model",
    DESC_CHAT_MODE: "Search uses your documents and returns citations. Chat is a general assistant with no retrieval.",
    TOOLTIP_CHAT_MODE_NEEDS_EMBEDDING: "Configure an embedding model to enable Search.",
    TOOLTIP_MODE_SEARCH: "Search your vault — finds relevant passages and answers with citations.",
    TOOLTIP_MODE_CHAT: "Chat with the model directly — no vault retrieval.",
    TOOLTIP_ROLE_CHAT: "Chat model — writes the answers.",
    TOOLTIP_ROLE_EMBED: "Embedding model — indexes your notes so search can find them.",
    TOOLTIP_ROLE_VISION: "Vision model — reads scanned or image-only PDFs. Optional.",
    TOOLTIP_ROLE_RERANK: "Reranker — reorders search hits for sharper relevance. Optional.",
    LABEL_GEN_TEMPERATURE: "Creativity",
    LABEL_GEN_TOP_P: "Top P",
    LABEL_GEN_TOP_K: "Top K (sampling)",
    LABEL_GEN_REPEAT_PENALTY: "Repetition penalty",
    LABEL_GEN_SEED: "Seed",
    LABEL_GEN_MAX_TOKENS: "Max tokens",
    LABEL_SHOW_REASONING: "Show thinking",
    LABEL_CHAT_COMPACTION: "Condense long conversations",
    LABEL_GEN_MAX_REASONING_CHARS: "Max reasoning characters",
    LABEL_GEN_MODEL_KEEP_ALIVE: "Model keep-alive (s)",
    LABEL_GEN_GPU_MEMORY_FRACTION: "GPU memory fraction",
    LABEL_WORKER_POOL: "Worker pool",
    LABEL_WORKER_POOL_HELP:
        "Tuning knobs for the background worker processes that run embedding, vision and rerank models.",
    LABEL_WORKER_POOL_CALL_TIMEOUT: "Pool call timeout (s)",
    LABEL_WORKER_POOL_EAGER_START: "Eager start workers",
    LABEL_WORKER_POOL_MAX_IDLE: "Worker idle timeout (s)",
    LABEL_INGEST: "Ingest",
    LABEL_INGEST_HELP: "How documents are split, OCR'd and prepared before they enter the index.",
    LABEL_CHUNK_SIZE: "Chunk size (tokens)",
    LABEL_CHUNK_OVERLAP: "Chunk overlap (tokens)",
    LABEL_MAX_CHUNKS_PER_FILE: "Max chunks per file",
    LABEL_TESSERACT_TIMEOUT: "Tesseract per-page timeout (s)",
    LABEL_VISION_LOAD_BUDGET: "Vision OCR pool budget (s)",
    LABEL_RETRIEVAL_ADVANCED: "Retrieval (advanced)",
    LABEL_RETRIEVAL_ADVANCED_HELP:
        "Lower-level retrieval knobs that shape candidate pools and answer-source diversity.",
    LABEL_CANDIDATE_MULTIPLIER: "Candidate multiplier",
    LABEL_MIN_RELEVANCE_SCORE: "Minimum relevance score",
    LABEL_MAX_CONTEXT_SOURCES: "Max context sources",
    LABEL_DIVERSITY_MAX_PER_SOURCE: "Max chunks per source",
    LABEL_MMR_LAMBDA: "MMR lambda",
    LABEL_CRAWLING: "Crawling",
    LABEL_CRAWL_MAX_DEPTH: "Crawl depth",
    LABEL_CRAWL_MAX_PAGES: "Page limit",
    LABEL_CRAWL_TIMEOUT: "Page timeout",
    LABEL_CRAWL_MEAN_DELAY: "Request delay (seconds)",
    LABEL_CRAWL_MAX_DELAY_RANGE: "Delay jitter (seconds)",
    LABEL_CRAWL_CONCURRENT_REQUESTS: "Concurrent requests",
    LABEL_CRAWL_RETRY_ON_RATE_LIMIT: "Retry on rate-limit",
    LABEL_CRAWL_RETRY_BASE_DELAY_MIN: "Retry base delay min (seconds)",
    LABEL_CRAWL_RETRY_BASE_DELAY_MAX: "Retry base delay max (seconds)",
    LABEL_CRAWL_RETRY_MAX_BACKOFF: "Retry max backoff (seconds)",
    LABEL_CRAWL_RETRY_MAX_ATTEMPTS: "Retry attempts",
    LABEL_CRAWL_RENDER_MODE: "Render mode",
    DESC_CRAWL_RENDER_MODE:
        "How pages are fetched. Lightweight is a fast HTTP fetch with no JavaScript. Browser runs Chromium so JavaScript-rendered pages work, at the cost of more memory and slower crawls.",
    LABEL_CRAWL_RENDER_MODE_HTTP: "Lightweight (HTTP, no JavaScript)",
    LABEL_CRAWL_RENDER_MODE_BROWSER: "Browser (Chromium, JavaScript)",
    LABEL_CRAWL_EXCLUDE_PATTERNS: "URL exclude patterns",
    DESC_CRAWL_EXCLUDE_PATTERNS:
        "Regex patterns that skip URLs at link-discovery during recursive crawls. One per line. Blank = no filtering.",
    LABEL_RESET_TO_DEFAULT: "Reset to default",
    LABEL_RESET_ALL_SETTINGS: "Reset server-backed settings to defaults",
    DESC_RESET_ALL_SETTINGS:
        "Restore every server-backed setting below to its default. API keys and local plugin preferences are preserved.",
    CONFIRM_RESET_ALL_SETTINGS:
        "Reset every server-backed setting to its default? API keys and local plugin preferences are preserved.",
    BUTTON_RESET_ALL: "Reset all",
    LABEL_ADVANCED: "Advanced",
    LABEL_STORE_CONTENT_IN_VAULT: "Store lilbee content in vault",
    DESC_STORE_CONTENT_IN_VAULT:
        "Materialize crawled pages and imported files inside your vault so you can browse them in Obsidian. Disabled for external servers — their files live on the server machine, not your computer.",
    NOTICE_STORAGE_REORGANIZING: "Reorganizing lilbee storage into your vault…",
    NOTICE_STORAGE_REORGANIZED: "Lilbee storage is now inside your vault.",
    NOTICE_STORAGE_REORGANIZE_FAILED: "Could not move lilbee storage into your vault: ",
    NOTICE_TAKE_OVER_SUCCESS: (vaultName: string): string => `lilbee switched from "${vaultName}" to this vault.`,
    NOTICE_TAKE_OVER_DECLINED: (vaultName: string): string =>
        `lilbee stays with "${vaultName}". This vault will run without managed lilbee until you take over or close the other vault.`,
    NOTICE_TAKE_OVER_TIMEOUT:
        "The other lilbee process did not shut down in time. Try again, or close the other Obsidian window first.",
    CONFIRM_TAKE_OVER: (vaultName: string): string =>
        `lilbee is currently serving "${vaultName}". Switch it to this vault? The other vault will lose its lilbee connection until you reopen it.`,
    STATUS_LOCKED_BY_OTHER: (vaultName: string): string => `lilbee: serving "${vaultName}"`,
    COMMAND_TAKE_OVER: "Take over the managed lilbee server",
    TOOLTIP_MODEL_INSTALLED_SHARED:
        "Installed in the shared lilbee models directory — every vault on this computer can use it without re-downloading.",
    LABEL_SHARED_ROOT: "Shared lilbee directory",
    DESC_SHARED_ROOT: (resolved: string): string =>
        `Holds the lilbee binary, model cache, and one subfolder per vault. Leave blank to use the platform default (currently ${resolved}). Restart the plugin or Obsidian for changes to take effect.`,
    LABEL_ADOPT_DATA_DIR: "Use existing lilbee data directory",
    DESC_ADOPT_DATA_DIR:
        "Point this vault at a lilbee data directory you already have on disk (e.g. one moved from the old per-vault location, or one created by `lilbee serve`). The plugin restarts the managed server against the new path.",
    PLACEHOLDER_ADOPT_DATA_DIR: "/path/to/lilbee/data-dir",
    BUTTON_ADOPT_DATA_DIR: "Use this folder",
    NOTICE_ADOPT_DATA_DIR_BLANK: "Enter a path first.",
    NOTICE_ADOPT_DATA_DIR_DONE: (path: string): string => `lilbee now uses ${path} for this vault.`,
    LABEL_STORAGE_REPORT: "Disk usage",
    DESC_STORAGE_REPORT:
        "How much disk space the shared binary, model cache, and this vault's data are using right now.",
    LABEL_STORAGE_BIN: "Binary",
    LABEL_STORAGE_MODELS: "Models cache",
    LABEL_STORAGE_VAULT: "This vault",
    LABEL_STORAGE_TOTAL: "Total",
    LABEL_EMBEDDING_MODEL: "Embedding model",
    LABEL_RERANKER_TITLE: "Reranker model",
    LABEL_RERANKER_DISABLED: "(disabled)",
    LABEL_RERANKER_HOSTED_GROUP: "Hosted via LiteLLM",
    LABEL_RERANKER_CANDIDATES: "Rerank candidates",
    DESC_RERANKER_MODEL: "Reorders search results for better relevance. Disabled by default.",
    DESC_RERANKER_CANDIDATES: "How many candidate results to rerank (1-100). Higher = slower but more accurate.",
    NOTICE_FAILED_RERANKER: "lilbee: failed to update reranker model",
    NOTICE_RERANKER_UPDATED: "lilbee: reranker model updated",
    NOTICE_RERANKER_LOAD_FAILED: "lilbee: failed to load reranker options",
    LABEL_VISION_TITLE: "Vision model",
    LABEL_VISION_DISABLED: "(disabled)",
    LABEL_VISION_HOSTED_GROUP: "Hosted via LiteLLM",
    DESC_VISION_MODEL:
        "Used to read scanned PDFs when OCR fails. Separate from the chat model — vision models run only on image-only documents.",
    NOTICE_VISION_UPDATED: "lilbee: vision model updated",
    NOTICE_FAILED_VISION: "lilbee: failed to update vision model",
    NOTICE_VISION_LOAD_FAILED: "lilbee: failed to load vision options",
    PLACEHOLDER_RERANK_CANDIDATES: "20",
    LABEL_LLM_PROVIDER: "AI backend",
    LABEL_OPENAI_API_KEY: "OpenAI API key",
    DESC_OPENAI_API_KEY: "For GPT models via litellm",
    LABEL_ANTHROPIC_API_KEY: "Anthropic API key",
    DESC_ANTHROPIC_API_KEY: "For Claude models via litellm",
    LABEL_GEMINI_API_KEY: "Gemini API key",
    DESC_GEMINI_API_KEY: "For Gemini models via litellm",
    LABEL_HF_TOKEN: "HuggingFace token",
    LABEL_OLLAMA_BASE_URL: "Ollama URL",
    LABEL_LM_STUDIO_BASE_URL: "LM Studio URL",
    LABEL_DELETE_MODEL: "Delete model",
    LABEL_GENERATION_HELP: "Fine-tune AI responses. Defaults work well for most users.",
    LABEL_ADVANCED_HELP:
        "These settings affect how your documents are processed. Only change if you know what you're doing.",
    LABEL_CLOSE_GLYPH: "\u00D7",
    LABEL_TASK_CENTER: "Task Center",
    LABEL_CLOSE_VIEW: (view: string): string => `Close ${view}`,
    LABEL_TASK_CAP_PILL: "{active}/{cap} running",
    LABEL_TASK_COUNTERS: "{active} running · {queued} queued · {done} done",
    LABEL_ACTIVE_TASKS: "ACTIVE",
    LABEL_QUEUED_TASKS: "QUEUED",
    LABEL_COMPLETED_TASKS: "COMPLETED",
    LABEL_NO_ACTIVE_TASKS: "No active tasks",
    LABEL_NO_QUEUED_TASKS: "No queued tasks",
    LABEL_NO_COMPLETED_TASKS: "No completed tasks",
    LABEL_CANCEL_TASK: "Cancel task",
    LABEL_RETRY_TASK: "Retry",
    LABEL_TASK_STATE_QUEUED: "queued",
    LABEL_TASK_STATE_DONE: "done",
    LABEL_TASK_STATE_FAILED: "failed",
    LABEL_TASK_STATE_CANCELLED: "cancelled",
    LABEL_TASK_STATE_WAITING: "waiting on server",
    STATUS_WAITING_ON_SERVER: "Already ingesting on the server — waiting for it to finish",
    STATUS_TASKS_RUNNING_PLURAL: "{count} tasks running · {name} {pct}%",
    STATUS_TASK_RUNNING_SINGLE: "{name} {pct}%",
    // A job whose length cannot be known reads as stuck when it is labelled 0%,
    // so those drop the number rather than reporting a false one.
    STATUS_TASK_RUNNING_SINGLE_INDETERMINATE: "{name}",
    STATUS_TASKS_RUNNING_PLURAL_INDETERMINATE: "{count} tasks running · {name}",
    STATUS_TASKS_QUEUED_ONLY: "{count} queued",
    STATUS_TASK_DONE_FLASH: "Done · {name}",
    STATUS_TASKS_DONE_FLASH: "{count} tasks done · {name}",
    STATUS_TASK_FAILED_FLASH: "{count} task failed · {name}",
    STATUS_TASKS_FAILED_FLASH: "{count} tasks failed · {name}",
    LABEL_ADD_FILE: "Add file",
    LABEL_SAVE_VAULT: "Save to vault",
    LABEL_OPEN_GPU_ACTIVITY: "GPU activity beside chat",
    LABEL_STOPPED: "(stopped)",
    LABEL_STOPPED_MD: "*(stopped)*",
    LABEL_CONNECTING: "(connecting...)",
    LABEL_OFFLINE: "(offline)",
    LABEL_CHAT_VIEW: "lilbee Chat",
    LABEL_TASKS_VIEW: "lilbee Tasks",
    LABEL_RIBBON_OPEN_CHAT: "Open lilbee chat",
    LABEL_RIBBON_UPDATE_AVAILABLE: (version: string) =>
        `lilbee server ${version} is available. Open the update settings.`,
    LABEL_STATUSBAR_OPEN_SETTINGS: "Open lilbee settings",
    ERROR_STREAM_IDLE: "server stopped sending events — check that lilbee is running",
    LABEL_OUR_PICKS: "Our picks",
    LABEL_SECTION_INSTALLED: "Installed",
    LABEL_SECTION_CHAT: "Chat",
    LABEL_SECTION_EMBEDDING: "Embedding",
    LABEL_SECTION_VISION: "Vision",
    LABEL_SECTION_RERANK: "Rerank",
    LABEL_PICK: "pick",
    LABEL_FIT_FITS: "fits",
    LABEL_FIT_TIGHT: "tight",
    LABEL_FIT_WONT_RUN: "won't run",
    LABEL_COMPAT_UNSUPPORTED: "Unsupported",
    TOOLTIP_COMPAT_UNSUPPORTED: (architecture: string) =>
        architecture
            ? `Your lilbee server doesn't support this model's architecture (${architecture}).`
            : "Your lilbee server doesn't support this model's architecture.",
    TOOLTIP_PULL_UNSUPPORTED: "This model isn't supported by your lilbee server.",
    LABEL_SWITCH_TO_LIST: "Switch to list view",
    LABEL_SWITCH_TO_GRID: "Switch to grid view",
    LABEL_VIEW_TOGGLE_CTA: "Switch to list view for the full catalog",
    LABEL_NO_MODELS_FOUND: "No models match your filters.",
    LABEL_DETAIL_VARIANTS: "Size variants",
    LABEL_DETAIL_DESCRIPTION: "Description",
    LABEL_DETAIL_DOWNLOADS: "Downloads",
    LABEL_DETAIL_INSTALL_STATUS: "Status",
    BUTTON_DRAWER_TOGGLE: "Toggle details",
    LABEL_DRAWER_NO_SELECTION: "Focus a model to see its details.",
    MODEL_INFO_TITLE: "Model info",
    LABEL_MODEL_INFO_BTN: "Show details",
    MODEL_INFO_TASK: "Task",
    MODEL_INFO_PARAMS: "Parameters",
    MODEL_INFO_CONTEXT: "Context window",
    MODEL_INFO_QUANT: "Quantization",
    MODEL_INFO_RAM: "Recommended RAM",
    MODEL_INFO_DOWNLOADS: "Downloads",
    MODEL_INFO_HF_LINK_LABEL: "View on Hugging Face",
    COMMAND_MODEL_INFO_CHAT: "Show info for active chat model",
    COMMAND_MODEL_INFO_EMBED: "Show info for active embedding model",
    NOTICE_NO_ACTIVE_MODEL: (task: string): string => `No active ${task} model is set.`,
    LABEL_TASK: "Task",
    LABEL_DOWNLOADS_COUNT: (count: string) => `${count} downloads`,
    LABEL_SIZE_SMALL: "Small",
    LABEL_SIZE_MEDIUM: "Medium",
    LABEL_SIZE_LARGE: "Large",
 
    DESC_SERVER_MODE: "How the lilbee server is managed",
    DESC_MANAGED_BUILTIN: "Managed (built-in)",
    DESC_EXTERNAL_MANUAL: "External (manual)",
    DESC_SETUP_WIZARD: "Walk through initial setup again",
    DESC_SERVER_STATUS_CURRENT: "Current state of the managed lilbee server",
    DESC_SERVER_CONTROLS_START_STOP: "Start, stop, or restart the managed server",
    DESC_SERVER_VERSION_UNKNOWN: "Unknown",
    DESC_SERVER_VERSION_INSTALLED: (tag: string) => `${tag} installed. This is the latest release.`,
    DESC_SERVER_VERSION_OUTDATED: (tag: string) => `${tag} installed. Newer releases are available.`,
    DESC_SERVER_VERSION_PLAIN: (tag: string) => `${tag} installed.`,
    DESC_SERVER_VERSION_UPDATE: (installed: string, tag: string) => `${installed} installed. ${tag} is available.`,
    DESC_SERVER_VERSION_DOWNGRADE: (installed: string) =>
        `${installed} installed. Downgrading replaces it with an older build and turns off automatic server updates.`,
    DESC_SERVER_VERSION_LOADING: "Reading the release list from GitHub...",
    /** How a server build is named wherever the user sees one. macOS ships Metal in the
     *  default build and Linux and Windows ship Vulkan, so the default is named for neither. */
    LABEL_SERVER_BUILD: (variant: ServerVariant): string => SERVER_BUILD_LABEL[variant],
    DESC_SERVER_BUILD: (build: string) => ` Running the ${build} build.`,
    /** Why the GPU probe chose the build it did. */
    DESC_GPU_DETECTION: (detection: GpuDetection): string => {
        const { nvidia } = detection;
        if (nvidia.status === NVIDIA_PROBE_STATUS.SKIPPED) return "lilbee ships one macOS build, so nothing is probed.";
        const amd = amdDetectionText(detection.amd);
        if (nvidia.status === NVIDIA_PROBE_STATUS.MISSING) return `nvidia-smi did not run: ${nvidia.error}. ${amd}`;
        if (nvidia.status === NVIDIA_PROBE_STATUS.SANDBOXED)
            return `An NVIDIA driver is present, but this Obsidian cannot reach nvidia-smi, so its CUDA version is unknown. Flatpak and Snap sandboxes hide it. ${amd}`;
        if (nvidia.status === NVIDIA_PROBE_STATUS.UNREADABLE)
            return `nvidia-smi ran but reported no CUDA version. ${amd}`;
        const floor =
            nvidia.cudaCeiling < CUDA_MIN_CEILING
                ? ` lilbee ships CUDA builds for ${cudaVersionLabel(CUDA_MIN_CEILING)} and newer, so the default build runs.`
                : "";
        return `The NVIDIA driver reports CUDA ${cudaVersionLabel(nvidia.cudaCeiling)}.${floor} ${amd}`;
    },
    DESC_GPU_DETECTION_NONE: "(none recorded)",
    DESC_SERVER_VERSION_OFFLINE: (tag: string, reason: string) =>
        `${tag} installed. The release list could not be read from GitHub: ${reason}`,
    DESC_DEV_BUILD_AVAILABLE: (tag: string) =>
        `A newer dev build (${tag}) is out. Turn on "Include dev builds" to try it.`,
    DESC_SERVER_AUTO_UPDATE:
        "Install the latest server release when the plugin updates. Turned off automatically when you install an older version, so an explicit downgrade sticks.",
    DESC_SERVER_UPDATE_REMINDER:
        "When automatic updates are off, show a reminder on launch while a newer server release is available. The ribbon shows an update icon either way.",
    DESC_INCLUDE_DEV_BUILDS:
        "Offer in-development builds in the version list and track them for updates. They ship the newest features " +
        "but get less testing than a stable release.",
    BUG_FEEDBACK_PREFIX: "Found a bug? Report it on ",
    BUG_FEEDBACK_GITHUB: "GitHub issues",
    BUG_FEEDBACK_IRC: "#lilbee on Libera Chat",
    BUG_FEEDBACK_SUFFIX: ". Reports help improve lilbee, and reporters get credited in the release notes.",
 
    TOOLTIP_SERVER_VERSION_SUPPORT:
        "This plugin is built and tested against the latest server release. Older releases still install and run, " +
        "but they are not supported, and features the plugin expects may be missing. Use a non-default release at your own risk.",
    TOOLTIP_UNINSTALL_SECTION:
        "Obsidian does not manage the lilbee server executable or its models, this plugin does. " +
        "Removing the plugin from Obsidian's Community plugins pane leaves both on disk. Uninstall here first.",
    CALLOUT_UNINSTALL_FIRST:
        "Removing the plugin does not remove the server. The executable, the downloaded models, and this vault's " +
        "index live outside your vault, so Obsidian never touches them. Uninstall here before you remove the plugin " +
        "and nothing is left behind.",
    DESC_UNINSTALL_SERVER: (size: string) =>
        `Deletes the executable, the models, and this vault's index. Frees ${size}. Your notes are not touched.`,
    DESC_INSTALL_SERVER: (size: string) =>
        `Downloads the lilbee server, about ${size}. Models are pulled on demand afterwards.`,
    DESC_SERVER_DOWNLOADING: "The lilbee server is downloading. Progress is in the status bar.",
    DESC_SERVER_NOT_INSTALLED:
        "The server is not installed. Chat, search, and sync are unavailable until you install it.",
 
    CONFIRM_UNINSTALL_TITLE: "Uninstall the lilbee server?",
    CONFIRM_UNINSTALL_BODY:
        "This removes everything lilbee downloaded or built for this vault. It cannot be undone, but you can " +
        "install the server again at any time.",
    LABEL_UNINSTALL_BINARY: "Server executable",
    LABEL_UNINSTALL_MODELS: "Downloaded models",
    LABEL_UNINSTALL_INDEX: "Search index for this vault",
    LABEL_UNINSTALL_CACHE: "Unpacked server files",
    LABEL_UNINSTALL_KEEP: "Your notes and attachments",
    LABEL_UNINSTALL_KEEP_VALUE: "untouched",
    LABEL_UNINSTALL_DELETE_TAG: "Delete",
    LABEL_UNINSTALL_KEEP_TAG: "Keep",
 
    NOTICE_UNINSTALLED: (size: string) => `lilbee server uninstalled. ${size} freed.`,
    NOTICE_INSTALLED: (tag: string) => `lilbee server ${tag} installed.`,
    NOTICE_DOWNLOAD_CANCELED: "lilbee server download cancelled.",
    ERROR_UNINSTALL_FAILED: "Could not uninstall the lilbee server",
    ERROR_UNINSTALL_SERVER_IN_USE: (vaultName: string) =>
        `The lilbee server is running for ${vaultName}. Close that vault, then uninstall.`,
    ERROR_INSTALL_FAILED: "Could not install the lilbee server",
    ERROR_DOWNLOAD_NO_SPACE: (needed: string, free: string) =>
        `Not enough disk space for the lilbee server: need about ${needed} free, but only ${free} is available. Free up some space and try again.`,
    ERROR_DOWNLOAD_STALLED: "The lilbee server download stalled. Check your connection and try again.",
    ERROR_DOWNLOAD_UNVERIFIED:
        "The downloaded lilbee server could not be verified against its checksum and was discarded. Please try again.",
    ERROR_GITHUB_STATUS: (status: number) => `GitHub API responded ${status}`,
    ERROR_RELEASE_LIST: (reason: string) => `Could not read the lilbee release list from GitHub: ${reason}`,
    DESC_SERVER_URL_HELP: "Address of the lilbee HTTP server",
    LABEL_MANUAL_TOKEN: "Session token",
    DESC_MANUAL_TOKEN:
        "Paste the server's session token (required for remote servers). When the lilbee server runs on this machine the plugin discovers the token automatically — leave this blank.",
    DESC_SWITCH_MANAGED: "Stop using an external server and start the built-in one",
    DESC_MODELS_HELP: "Browse the catalog for available models. Requires the lilbee server.",
    DESC_REFRESH_MODELS: "Fetch available models from the server",
    DESC_RESULTS_COUNT: "How many matching passages to return when you search or ask a question",
    DESC_MAX_DISTANCE:
        "How closely results must match your query. Lower = only very close matches, higher = broader results",
    DESC_ADAPTIVE_THRESHOLD: "Automatically broaden the search if too few results are found",
    DESC_RAG_SYSTEM_PROMPT:
        "System prompt sent to the chat model when the answer is grounded in retrieved documents (Search mode with at least one match). Leave blank for the default.",
    DESC_GENERAL_SYSTEM_PROMPT:
        "System prompt sent to the chat model in Chat mode and when Search returns zero results. Leave blank for the default.",
    DESC_GEN_TEMPERATURE: "Higher = more creative and varied responses, lower = more focused and predictable",
    DESC_GEN_TOP_P: "Controls response diversity. Most users should leave this at the default.",
    DESC_GEN_TOP_K: "Limits which words the AI considers. Most users should leave this at the default.",
    DESC_GEN_REPEAT_PENALTY: "How strongly to avoid repeating the same phrases",
    DESC_GEN_SEED: "Set a number for reproducible responses. Leave blank for varied answers.",
    DESC_GEN_MAX_TOKENS: "Hard cap on generated tokens per response (blank = no cap)",
    DESC_SHOW_REASONING:
        "Show a reasoning model's thinking in the chat, in a collapsible block above each answer. Off strips it.",
    DESC_CHAT_COMPACTION:
        "When a conversation outgrows the model's context, fold the oldest messages into a summary instead of dropping them.",
    DESC_GEN_MAX_REASONING_CHARS:
        "Maximum reasoning characters before lilbee forces the model to answer (per-model overrides apply on top)",
    DESC_GEN_MODEL_KEEP_ALIVE: "Seconds the loaded model stays warm between calls (0 = unload immediately)",
    DESC_GEN_GPU_MEMORY_FRACTION: "Fraction of GPU memory the model is allowed to claim (0.1-1.0)",
    DESC_WORKER_POOL_CALL_TIMEOUT:
        "Per-call deadline for one worker-pool round-trip in seconds. Raise this for very large embed batches on slow machines",
    DESC_WORKER_POOL_EAGER_START:
        "Spawn every worker process when the lilbee server starts instead of on first use. Trades 1–3 seconds of cold-start per role for first-call latency.",
    DESC_WORKER_POOL_MAX_IDLE:
        "Shut a worker down after this many seconds idle to free RAM/VRAM. 0 disables idle reaping",
    DESC_TESSERACT_TIMEOUT: "Per-page Tesseract timeout in seconds (used when no vision model is set)",
    DESC_VISION_LOAD_BUDGET:
        "Outer wall-clock budget for the vision-OCR pool drain in seconds. Includes load grace plus per-page time",
    DESC_CANDIDATE_MULTIPLIER: "Candidate-pool multiplier over top_k before reranking",
    DESC_MIN_RELEVANCE_SCORE: "Minimum RRF relevance score for hybrid search results (0.0 = no filter)",
    DESC_MAX_CONTEXT_SOURCES: "Maximum unique sources contributing chunks to a single answer",
    DESC_DIVERSITY_MAX_PER_SOURCE: "Maximum chunks accepted from any one source (caps source dominance)",
    DESC_MMR_LAMBDA: "MMR lambda balancing relevance vs diversity (0 = max diversity, 1 = max relevance)",
    DESC_CRAWL_MAX_DEPTH: "Optional ceiling on crawl depth. Leave blank for no cap.",
    DESC_CRAWL_MAX_PAGES: "Optional ceiling on pages per crawl. Leave blank for no cap.",
    DESC_CRAWL_TIMEOUT: "How long to wait for each page to load before giving up",
    DESC_CRAWL_MEAN_DELAY: "Average delay between requests within a crawl, in seconds.",
    DESC_CRAWL_MAX_DELAY_RANGE: "Random jitter added to the mean delay, in seconds.",
    DESC_CRAWL_CONCURRENT_REQUESTS: "Maximum concurrent in-flight URLs within a single crawl.",
    DESC_CRAWL_RETRY_ON_RATE_LIMIT: "Retry with backoff when servers return 429 or 503.",
    DESC_CRAWL_RETRY_BASE_DELAY_MIN: "Minimum base backoff delay on rate-limit responses, in seconds.",
    DESC_CRAWL_RETRY_BASE_DELAY_MAX: "Maximum base backoff delay on rate-limit responses, in seconds.",
    DESC_CRAWL_RETRY_MAX_BACKOFF: "Upper bound on any single backoff wait, in seconds.",
    DESC_CRAWL_RETRY_MAX_ATTEMPTS: "Retry count per URL before giving up.",
    DESC_CHUNK_SIZE: "Document chunk size in tokens (changes invalidate the index)",
    DESC_CHUNK_OVERLAP: "Tokens of overlap between adjacent chunks (preserves context across boundaries)",
    DESC_MAX_CHUNKS_PER_FILE:
        "Most chunks one file can add to the index. A file over the limit is skipped and reported. 0 means no limit. Raise it for a long document at a small chunk size.",
    DESC_EMBEDDING_MODEL: "The AI model used to understand your documents. Changing requires re-indexing.",
    DESC_LLM_PROVIDER: "Auto picks the best available. Use External to connect to OpenAI, Claude, or other services.",
    DESC_LLM_PROVIDER_AUTO: "Auto (recommended)",
    DESC_LLM_PROVIDER_LOCAL: "Local only (llama-cpp)",
    DESC_LLM_PROVIDER_EXTERNAL: "External (OpenAI, Claude, etc.)",
    DESC_HF_TOKEN: "Needed for some models. Get one free at huggingface.co/settings/tokens",
    DESC_OLLAMA_BASE_URL:
        "Where your Ollama server is listening. Leave blank for the default (http://localhost:11434).",
    DESC_LM_STUDIO_BASE_URL:
        "Where your LM Studio server is listening. Leave blank for the default (http://localhost:1234/v1).",
    DESC_REINDEX_WARNING: "Changing {field} will require re-indexing all documents. Continue?",
    DESC_EMBEDDING_REINDEX_WARNING: "Changing the embedding model will require re-indexing all documents. Continue?",
 
    PLACEHOLDER_DEFAULT: "Default",
    PLACEHOLDER_NOT_SET: "Not set",
    PLACEHOLDER_SEARCH_MODELS: "Search models...",
    PLACEHOLDER_SEARCH_DOCUMENTS: "Search documents...",
    PLACEHOLDER_TYPE_SEARCH: "Type to search...",
    PLACEHOLDER_ASK_SOMETHING: "Ask something...",
    PLACEHOLDER_FILTER_SETTINGS: "Filter settings...",
    PLACEHOLDER_HTTP_LOCALHOST: "http://127.0.0.1:7433",
    PLACEHOLDER_SK: "sk-...",
    PLACEHOLDER_HF_TOKEN: "hf_...",
    PLACEHOLDER_PICK_VAULT_FILE: "Pick a vault file to add to lilbee...",
    PLACEHOLDER_URL: "https://example.com",
    PLACEHOLDER_30: "30",
 
    LABEL_ENTER_QUERY: "Enter a query to begin.",
    LABEL_NO_RESULTS: "No results found.",
    ERROR_SEARCH_CONNECT: "Error: could not connect to lilbee server.",
 
    TITLE_SEARCH: "Search knowledge base",
    TITLE_MODEL_CATALOG: "Model Catalog",
    TITLE_DOCUMENTS: "Documents",
    LABEL_DOCUMENT_CHUNKS: (count: number): string => `${count} chunks`,
    TITLE_SESSIONS: "Sessions",
 
    LABEL_OPEN_SESSIONS: "Saved conversations",
    LABEL_SESSION_SUMMARY: "Summary of earlier messages",
    LABEL_NEW_SESSION: "New chat",
    LABEL_RENAME_SESSION: "Rename",
    LABEL_DELETE_SESSION: "Delete",
    PLACEHOLDER_FILTER_SESSIONS: "Filter conversations...",
    PLACEHOLDER_RENAME_SESSION: "New name (enter saves, esc cancels)",
    SESSIONS_COUNT: (count: number): string => `${count} saved`,
    SESSIONS_EMPTY: "No saved conversations yet.",
    SESSIONS_NO_MATCH: "No conversations match that filter.",
    SESSIONS_ROW_META: (count: number, model: string): string => `${count} msgs · ${model}`,
    SESSIONS_UNTITLED: "Untitled chat",
    CHAT_WARMING: "Loading the chat model…",
    TOOLTIP_COMPACTION:
        "Condensing keeps the model aware of everything you have discussed, instead of letting the oldest messages fall out of what it can see. Your transcript keeps them either way. Turn it off with \u201cCondense long conversations\u201d in Settings.",
    CHAT_COMPACTING: "Condensing earlier messages…",
    CHAT_COMPACTING_PROGRESS: (batch: number, total: number): string =>
        `Condensing earlier messages… (${batch} of ${total})`,
    CHAT_COMPACTED: (count: number): string => `${count} messages condensed into a summary`,
    CHAT_COMPACTED_PARTIAL: (condensed: number, stranded: number): string =>
        `${condensed} condensed, ${stranded} dropped without notes`,
    CHAT_STRANDED: (count: number): string => `${count} earlier messages dropped without notes`,
    SESSIONS_DISABLED: "Saved conversations are turned off on this server.",
    LABEL_ENABLE_SESSIONS: "Turn on",
    NOTICE_SESSION_RESUMED: (title: string): string => `Resumed · ${title}`,
    NOTICE_SESSION_NEW: "Started a new chat",
    NOTICE_SESSION_DELETED: (title: string): string => `Deleted · ${title}`,
    NOTICE_CONFIRM_DELETE_SESSION: (title: string): string => `Delete "${title}"? This cannot be undone.`,
    NOTICE_SESSION_MODEL_UNAVAILABLE: (model: string, current: string): string =>
        `This conversation used ${model}, which isn't installed. Keeping ${current}.`,
    ERROR_SESSIONS_LOAD_FAILED: (reason: string): string => `Could not load conversations: ${reason}`,
    ERROR_SESSION_RESUME_FAILED: (reason: string): string => `Could not resume conversation: ${reason}`,
    ERROR_SESSION_RENAME_FAILED: (reason: string): string => `Could not rename conversation: ${reason}`,
    ERROR_SESSION_DELETE_FAILED: (reason: string): string => `Could not delete conversation: ${reason}`,
    ERROR_SESSIONS_ENABLE_FAILED: (reason: string): string => `Could not turn on saved conversations: ${reason}`,
    TITLE_CRAWL_WEB_PAGE: "Crawl web page",
    TITLE_DOWNLOAD_MODEL: "Download model?",
    TITLE_WELCOME: "Welcome to lilbee",
    TITLE_SERVER_MODE: "How do you want to run lilbee?",
    TITLE_PICK_MODEL: "Pick a chat model",
    TITLE_INDEX_VAULT: "Index your vault",
    TITLE_ALL_SET: "You're all set!",
    TITLE_MANAGED_RECOMMENDED: "Managed (recommended)",
    TITLE_EXTERNAL: "External",
 
    STATUS_DOWNLOADING: "lilbee: downloading...",
    STATUS_DOWNLOAD_STARTING: "Downloading...",
    STATUS_FETCHING_RELEASE: "Fetching latest release info...",
    STATUS_DOWNLOADING_PERCENT: (percent: number) => `lilbee: downloading ${percent}%`,
    STATUS_DOWNLOAD_PROGRESS: (percent: number, received: string, total: string) =>
        `Downloading... ${percent}% (${received} of ${total})`,
    STATUS_DOWNLOAD_RECEIVED: (received: string) => `Downloading... ${received}`,
    STATUS_UPDATE_SIZE: (tag: string, size: string) => `Updating to ${tag} · ${size} download`,
    STATUS_STARTING: "lilbee: starting...",
    STATUS_WARMING: "lilbee: warming up...",
    STATUS_READY: "lilbee: ready",
    STATUS_READY_EXTERNAL: "lilbee: ready [external]",
    STATUS_ERROR: "lilbee: error",
    STATUS_AUTH_ERROR: "lilbee: auth error",
    NOTICE_SERVER_UNREACHABLE: "lilbee: server unreachable",
    STATUS_STOPPED: "lilbee: stopped",
    STATUS_NOT_INSTALLED: "lilbee: server not installed",
    STATUS_ADDING: "lilbee: adding {label}...",
    STATUS_NOTHING_NEW: "lilbee: nothing new to add",
    STATUS_ADD_CANCELLED: "lilbee: add cancelled",
    STATUS_SYNCED: "lilbee: synced — {summary}",
    STATUS_SYNC_CANCELLED: "lilbee: sync cancelled",
    STATUS_SYNC_FAILED: "lilbee: sync failed",
    STATUS_SYNC_PILL: (n: number) => `⟳ ${n}`,
    TOOLTIP_PENDING_SYNC_HINT: "Click to sync new documents",
    STATUS_STARTING_SERVER: "Starting server...",
    WIZARD_SETUP_HEAD: "Setting up lilbee",
    WIZARD_SETUP_RUNNING: "lilbee server is running",
    WIZARD_SETUP_DOWNLOAD_PENDING: "Download lilbee server",
    WIZARD_SETUP_DOWNLOAD_ACTIVE: "Downloading lilbee server",
    WIZARD_SETUP_DOWNLOAD_DONE: "Downloaded lilbee server",
    WIZARD_SETUP_START_PENDING: "Start the server",
    WIZARD_SETUP_START_ACTIVE: "Starting the server",
    WIZARD_SETUP_START_DONE: "Started the server",
    WIZARD_SETUP_READY_PENDING: "Server ready",
    WIZARD_SETUP_READY_DONE: "Server ready · listening on 127.0.0.1",
    WIZARD_SETUP_DOWNLOAD_HINT: "Downloading… up to ~1 min on first run.",
    WIZARD_SETUP_START_HINT: "Booting · waiting for the server to respond…",
    WIZARD_SETUP_GATE: "You can't continue until the server is ready.",
    WIZARD_SETUP_SOURCE: "Server source:",
    WIZARD_EXTERNAL_TOKEN_HINT:
        "Paste the session token from your lilbee server (found with `lilbee token`). Required for the plugin to authenticate against a remote server.",
    PLACEHOLDER_MANUAL_TOKEN: "lilbee-…",
    STATUS_CHECKING_CONNECTION: "Checking connection...",
    STATUS_DOWNLOADING_MODEL: "Downloading {model}...",
    STATUS_DOWNLOADING_MODEL_PCT: "Downloading {model}... {pct}%",
    STATUS_PROCESSING_FILES: "Processing {current}/{total} files",
    STATUS_TASK_FILE: "file {current}/{total}",
    STATUS_TASK_SYNCING_FILE: "syncing {current}/{total}",
    STATUS_TASK_EXTRACTING: "extracting {file} p{page}/{total}",
    STATUS_TASK_EMBEDDING: "embedding chunk {chunk}/{total}",
    STATUS_TASK_BATCH: (current: number, total: number, file: string, status: string) =>
        `${status} ${current}/${total} ${file}`,
    STATUS_TASK_CRAWLER_PREPARING: "Preparing crawler…",
    STATUS_TASK_SETUP_PROGRESS: "chromium: {downloaded}/{total} MB",
    STATUS_TASK_SETUP_PROGRESS_INDETERMINATE: "chromium: {downloaded} MB",
    STATUS_INDEXING: "Indexing: {file}",
    STATUS_DONE: "Done!",
    STATUS_REMOVING: "Removing...",
    STATUS_SETTING: "Setting...",
    ERROR_UNKNOWN: "unknown error",
    ERROR_CANNOT_CONNECT: "cannot connect to server",
 
    ERROR_COULD_NOT_CONNECT: "lilbee: cannot connect to server",
    ERROR_SERVER_CRASHED: "lilbee: server crashed after multiple restarts",
    ERROR_SERVER_SHUTDOWN_FAILED: "lilbee: failed to stop the managed server",
    ERROR_FAILED_UPDATE: "lilbee: update failed",
    NOTICE_SERVER_AUTO_UPDATING: (version: string) => `lilbee: updating server to ${version}...`,
    NOTICE_SERVER_AUTO_UPDATED: (version: string) => `lilbee server updated to ${version}`,
    NOTICE_SERVER_SWITCHING_BUILD: (build: string) => `lilbee: switching to the ${build} build for your GPU...`,
    NOTICE_SERVER_SWITCHED_BUILD: (build: string) => `lilbee server now runs the ${build} build`,
    NOTICE_SERVER_AUTO_UPDATE_FAILED: "lilbee: automatic server update failed. You can retry from settings.",
    UPDATE_MODAL_TITLE: "A newer lilbee server is available",
    UPDATE_MODAL_BODY: (version: string, build: string | null) =>
        build
            ? `${version} is available, and the ${build} build matches this machine better than the installed one. Automatic updates are off, so the server stays on its current version until you update it.`
            : `${version} is available. Automatic updates are off, so the server stays on its current version until you update it.`,
    UPDATE_MODAL_HOW_TO_DISABLE:
        'To stop this reminder, turn off "Remind me about server updates" in the lilbee settings. To install updates without asking, turn on "Automatically update the server".',
    BUTTON_OPEN_UPDATE_SETTINGS: "Open update settings",
    BUTTON_NOT_NOW: "Not now",
    BUTTON_STOP_REMINDING: "Stop reminding me",
    NOTICE_EXTERNAL_SERVER_OUTDATED: (current: string, latest: string): string =>
        `Your lilbee server (${current}) is behind the latest release (${latest}). Update it to get the newest features and fixes.`,
    ERROR_LOAD_CATALOG: "lilbee: failed to load catalog",
    ERROR_LOAD_DOCUMENTS: "lilbee: failed to load documents",
    ERROR_DELETE_DOCUMENTS: "lilbee: failed to delete documents",
    ERROR_SWITCH_MODEL: "lilbee: failed to switch model",
    ERROR_PULL_MODEL: "lilbee: failed to pull {model}",
    ERROR_REMOVE_MODEL: "Failed to remove {model}",
    ERROR_SET_MODEL: "Failed to set {model}",
    ERROR_DELETE_MODEL: "Failed to delete {model}",
    ERROR_SAVE_CHAT: "Failed to save chat",
    ERROR_FILE_PICKER: "lilbee: could not open file picker",
    ERROR_LOAD_MODELS: "Could not load models from server.",
    ERROR_START_SERVER: "Failed to start server. Check the settings tab for details.",
    ERROR_COULD_NOT_CONNECT_EXT: "Could not connect. Check the URL and make sure the server is running.",
    ERROR_DOWNLOAD_FAILED: "Download failed. Please try again or pick a different model.",
    ERROR_INDEXING_FAILED: "Indexing failed. You can retry from the settings tab.",
    ERROR_CRAWL_FAILED: "lilbee: crawl failed — {msg}",
    ERROR_CRAWL_ERROR: "lilbee: crawl error — {msg}",
    ERROR_CRAWLER_SETUP_FAILED:
        "Crawler setup failed: {error}. Run 'make crawl-setup' in the lilbee repo or 'lilbee setup crawler' to retry.",
    ERROR_CRAWLER_SETUP_FAILED_SHORT: "Chromium setup failed — see setup task",
    ERROR_SERVER_UNREACHABLE: "Could not connect to lilbee server. Is it running?",
    ERROR_STREAM: (msg: string) => `lilbee: ${msg}`,
    ERROR_CHAT_FAILED: (reason: string) => `Chat failed: ${reason}`,
    ERROR_STREAM_INTERRUPTED_MANAGED:
        "Chat failed: the server closed the stream unexpectedly. Check the server logs in the logs folder of lilbee's data directory (inside Obsidian's app data folder), then try again.",
    ERROR_STREAM_INTERRUPTED_EXTERNAL:
        "Chat failed: the server closed the stream unexpectedly. Check the output of your lilbee serve process, then try again.",
    NOTICE_MODEL_UNAVAILABLE_SETUP: "Your model isn't available. Opening setup so you can pick a working one.",
    ERROR_RATE_LIMITED: (retryAfterSeconds: number | null) =>
        retryAfterSeconds !== null
            ? `lilbee is busy with another request. Try again in ${retryAfterSeconds} seconds.`
            : `lilbee is busy with another request. Try again in a moment.`,
    ERROR_ADD_FAILED_DETAIL: (msg: string) => `lilbee: add failed — ${msg}`,
    NOTICE_ALREADY_INGESTING: (source: string) =>
        `lilbee: server is already ingesting ${source} — waiting for it to finish`,
 
    NOTICE_NO_CHAT_MODEL: "lilbee: no chat model set — select one in settings",
    NOTICE_FLEET_WARMING: "lilbee: the model is still loading — try again in a moment",
    NOTICE_MODEL_ACTIVATED: (model: string) => `Now using ${model}`,
    NOTICE_PULL_CANCELLED: "lilbee: pull cancelled",
    NOTICE_NO_TOKEN_MANAGED: "lilbee: managed server didn't produce a session token — try restarting the plugin",
    NOTICE_NO_TOKEN_EXTERNAL:
        "lilbee: no session token — run 'lilbee token' and paste it in Settings → Session token, or set LILBEE_DATA",
    NOTICE_SESSION_TOKEN_INVALID: "lilbee: session token invalid — paste a new one in Settings → Session token",
    NOTICE_SESSION_TOKEN_INVALID_MANAGED:
        "lilbee: managed server rejected its own token — restart the server from Settings → Switch to managed server, or open Status for logs",
    NOTICE_QUEUE_FULL: "lilbee: too many tasks queued — wait for some to finish",
    NOTICE_SYNC_IN_PROGRESS:
        "lilbee: a sync is already running or queued. Wait for it to finish, or cancel it in the Task Center.",
    LABEL_DATASET_FILTER: "lilbee dataset",
    LABEL_DATASET_IMPORT_TASK: "Importing dataset",
    STATUS_DATASET_IMPORTING: "Re-embedding pages…",
    STATUS_DATASET_EMBEDDING: (file: string) => `Re-embedding ${file}…`,
    NOTICE_DATASET_EXPORTED: (path: string) => `lilbee: exported dataset to ${path}`,
    ERROR_DATASET_EXPORT: (msg: string) => `lilbee: export failed — ${msg}`,
    ERROR_DATASET_READ: (msg: string) => `lilbee: could not read dataset — ${msg}`,
    ERROR_DATASET_TOO_LARGE:
        "lilbee: dataset exceeds the 10 MB upload limit — use `lilbee import` from the CLI for larger files",
    NOTICE_DATASET_IMPORTED: (sources: number, pages: number, chunks: number, folder: string) =>
        `lilbee: imported ${sources} source(s) into "${folder}/" (${pages} page(s), ${chunks} chunk(s))`,
    ERROR_DATASET_IMPORT: (msg: string) => `lilbee: import failed — ${msg}`,
    NOTICE_MODEL_ACTIVATED_FULL: (model: string) => `lilbee: ${model} pulled and activated`,
    NOTICE_SET_MODEL: (type: string, model: string) => `${type} set to ${model}`,
    NOTICE_FAILED_SET_MODEL: (type: string) => `Failed to set ${type} model`,
    NOTICE_UPDATED: (field: string) => `lilbee: ${field} updated`,
    NOTICE_FAILED_UPDATE: (field: string) => `lilbee: failed to update ${field}`,
    NOTICE_FIELD_UPDATED: (field: string) => `lilbee: ${field} updated`,
    NOTICE_FIELD_RESET: (field: string) => `lilbee: ${field} reset to default`,
    NOTICE_FAILED_RESET: (field: string) => `lilbee: failed to reset ${field}`,
    NOTICE_SETTINGS_RESET: "lilbee: settings reset to defaults",
    NOTICE_FAILED_RESET_ALL: "lilbee: failed to reset settings",
    NOTICE_REINDEX_REQUIRED: "lilbee: re-indexing required — starting sync...",
    NOTICE_API_KEY_SAVED: "lilbee: API key saved",
    NOTICE_HF_TOKEN_SAVED: "lilbee: HuggingFace token saved",
    NOTICE_FAILED_HF_TOKEN: "lilbee: failed to save HuggingFace token",
    NOTICE_FAILED_SAVE_KEY: "lilbee: failed to save API key",
    NOTICE_LLM_UPDATED: "lilbee: LLM provider updated",
    NOTICE_FAILED_LLM: "lilbee: failed to update LLM provider",
    NOTICE_LOCAL_SERVER_URL_UPDATED: "lilbee: local server URL updated",
    NOTICE_FAILED_LOCAL_SERVER_URL: "lilbee: failed to update local server URL",
    NOTICE_EMBEDDING_UPDATED: "lilbee: embedding model updated",
    NOTICE_FAILED_EMBEDDING: "lilbee: failed to update embedding model",
    NOTICE_UPDATED_TO: (version: string) => `lilbee: updated to ${version}`,
    NOTICE_DELETED: (count: number) => `lilbee: removed ${count} documents`,
    NOTICE_SAVED: (path: string) => `Saved to ${path}`,
    NOTICE_REMOVED: (model: string) => `Deleted ${model}`,
    NOTICE_SYNC_SUMMARY: (summary: string) => `lilbee: ${summary}`,
    NOTICE_CRAWL_DONE: (pages: number) => `lilbee: crawl done — ${pages} pages`,
    NOTICE_ENTER_URL: "lilbee: please enter a URL",
    NOTICE_DOWNLOAD_CANCELLED: "lilbee: download cancelled",
    NOTICE_INDEXING_CANCELLED: "lilbee: indexing cancelled",
    NOTICE_NOTHING_SAVE: "Nothing to save",
    NOTICE_NO_MODELS_INSTALLED: "No models installed.",
    NOTICE_NO_MODELS_DESCRIPTION: "Download a model to start chatting with your notes",
    NOTICE_CONFIRM_CANCEL:
        "This operation is already in progress on the server. Canceling will hide it from the task center, but it may still complete. Continue?",
    NOTICE_CONFIRM_REMOVE: (model: string) => `Remove ${model}? This deletes the model file from disk.`,
    NOTICE_CONFIRM_DELETE_DOCS: (count: number) =>
        `Remove ${count} document(s) from the index? The files on disk stay where they are.`,
 
    COMMAND_SEARCH: "Search knowledge base",
    COMMAND_CHAT: "Open chat",
    COMMAND_MEMORIES: "Open memories",
    COMMAND_PLACEMENT: "Open GPU placement",
    COMMAND_PLACEMENT_BESIDE_CHAT: "Show GPU activity beside chat",
    COMMAND_REMEMBER: "Remember…",
    COMMAND_ADD_FILE: "Add current file",
    COMMAND_ADD_FOLDER: "Add current folder",
    COMMAND_SYNC: "Sync vault",
    COMMAND_SYNC_RETRY_SKIPPED: RETRY_SKIPPED_COMMAND,
    COMMAND_SYNC_REBUILD: "Rebuild index",
    COMMAND_EXPORT_DATASET: "Export dataset",
    COMMAND_IMPORT_DATASET: "Import dataset",
    COMMAND_CATALOG: "Browse model catalog",
    COMMAND_CRAWL: "Crawl web page",
    COMMAND_DOCUMENTS: "Browse documents",
    COMMAND_SETUP: "Run setup wizard",
    COMMAND_TASKS: "Show task center",
    COMMAND_STATUS: "Show status",
 
    // Status modal
    TITLE_STATUS: "lilbee Status",
    LABEL_STATUS_DOCUMENTS: "Documents",
    LABEL_STATUS_CHUNKS: "Chunks",
    LABEL_STATUS_DOCUMENTS_EMPTY: "The index has no documents.",
    LABEL_STATUS_DOCUMENTS_SHOWING: (shown: number, total: number): string => `Showing ${shown} of ${total}.`,
    LABEL_STATUS_DOCUMENTS_COMPLETE: (total: number): string => `Showing all ${total}.`,
    LABEL_STATUS_HELD_OUT: "Held out of the index",
    LABEL_STATUS_HELD_OUT_MORE: (count: number): string => `${count} more held out.`,
    LABEL_STATUS_HELD_OUT_RETRY: `Run "${RETRY_SKIPPED_COMMAND}" to index them again.`,
    LABEL_STATUS_DOCUMENTS_FAILED: "Could not load the document list. Scroll to try again.",
    NOTICE_DOCUMENT_NOT_IN_VAULT: (name: string): string => `${name} is not in this vault.`,
    NOTICE_DOCUMENT_IN_ARCHIVE: (name: string, archive: string): string =>
        `${name} is inside ${archive}. Showing the archive in the file explorer.`,
    LABEL_STATUS_CHAT_MODEL: "Chat model",
    LABEL_STATUS_OCR: "OCR",
    STATUS_VALUE_OCR_AUTO: "Auto",
    STATUS_VALUE_OCR_ON: "On",
    STATUS_VALUE_OCR_OFF: "Off",
    LABEL_STATUS_ARCHITECTURE: "Architecture",
    LABEL_STATUS_CONTEXT_LENGTH: "Context length",
    LABEL_STATUS_SERVED_CONTEXT: "Serving context",
    LABEL_STATUS_WIKI: "Wiki",
    LABEL_STATUS_WIKI_PAGES: "Pages",
    LABEL_STATUS_WIKI_DRAFTS: "Drafts",
    LABEL_STATUS_WIKI_LAST_LINT: "Last lint",
    LABEL_STATUS_NONE: "none",
    LABEL_STATUS_NOT_AVAILABLE: "n/a",
    LABEL_STATUS_ENABLED: "enabled",
    LABEL_STATUS_DISABLED: "disabled",
    COMMAND_ADD_TO_LILBEE: "Add to lilbee",
 
    // Wiki view
    LABEL_WIKI_VIEW: "lilbee Wiki",
    LABEL_WIKI: "Wiki",
    LABEL_WIKI_SUMMARIES: "Summaries",
    LABEL_WIKI_CONCEPTS: "Concepts",
    LABEL_WIKI_ENTITIES: "Entities",
    LABEL_WIKI_UNWRITTEN: (count: number) => `Not written yet (${count})`,
    LABEL_WIKI_NOT_WRITTEN: "not written yet",
    LABEL_WIKI_WRITING: "writing...",
    CONFIRM_WIKI_GENERATE: (name: string) =>
        `Write a wiki page for "${name}"? This runs the chat model once and can take a few minutes on a large model.`,
    TASK_WIKI_GENERATE: (name: string) => `Write ${name}`,
    NOTICE_WIKI_GENERATED: (name: string) => `lilbee: wrote "${name}"`,
    LABEL_WIKI_NO_PAGES: "No wiki pages yet",
    LABEL_WIKI_SOURCES_COUNT: (n: number) => `${n} sources`,
 
    // Citation modal
    TITLE_CITATIONS: "Citation provenance",
    LABEL_CITATION_FACT: "fact",
    LABEL_CITATION_INFERENCE: "inference",
    LABEL_CITATION_CURRENT: "current",
    LABEL_CITATION_STALE: "stale",
 
    // Drafts
    TITLE_DRAFTS: (n: number) => `Wiki drafts (${n})`,
    LABEL_DRAFT_REFRESH: "Refresh",
    LABEL_DRAFT_ACCEPT: "Accept",
    LABEL_DRAFT_REJECT: "Reject",
    LABEL_DRAFT_NO_SELECTION: "Select a draft to see its diff.",
    LABEL_DRAFT_NO_DIFF: "No diff (draft has no published counterpart).",
    LABEL_DRAFT_KIND_DRIFT: "DRIFT",
    LABEL_DRAFT_KIND_PARSE: "PARSE",
    LABEL_DRAFT_KIND_COLLISION: "COLLISION",
    LABEL_DRAFT_KIND_LOW_FAITH: "LOW FAITH",
    LABEL_DRAFT_KIND_BAD_TITLE: "BAD TITLE",
    LABEL_DRAFT_PUBLISHED: "pub",
    LABEL_DRAFT_NEW: "new",
    LABEL_DRAFT_FAITH_NA: "faith —",
    LABEL_DRAFT_FAITH: (score: number) => `faith ${score.toFixed(2)}`,
    LABEL_DRAFT_DRIFT: (pct: number) => `${pct}% drift`,
    LABEL_NO_DRAFTS: "No pending wiki drafts.",
    COMMAND_REVIEW_DRAFTS: "Review wiki drafts",
    NOTICE_CONFIRM_DRAFT_ACCEPT: (slug: string) =>
        `Accept draft "${slug}"? This overwrites the published page and re-indexes its chunks.`,
    NOTICE_CONFIRM_DRAFT_REJECT: (slug: string) => `Reject draft "${slug}"? The draft file will be deleted.`,
    NOTICE_DRAFT_ACCEPTED: (slug: string, chunks: number) => `Accepted ${slug} (${chunks} chunks indexed).`,
    NOTICE_DRAFT_REJECTED: (slug: string) => `Rejected ${slug}.`,
    NOTICE_DRAFT_ACTION_FAILED: "Draft action failed. Check the server log.",
    ERROR_LOAD_PAGE: "Failed to load page.",
    ERROR_LOAD_CITATIONS: "Failed to load citations.",
    ERROR_LOAD_DRAFTS: "Failed to load drafts.",
    ERROR_LOAD_DIFF: "Failed to load diff.",
    LABEL_NO_CITATIONS: "No citations found.",
 
    // Source preview modal (external server / non-vault sources)
    TITLE_SOURCE_PREVIEW: "Source preview",
    LABEL_PREVIEW_CLOSE: "Close",
    LABEL_PREVIEW_SAVE_TO_VAULT: "Save to vault",
    LABEL_PREVIEW_OPEN_IN_VAULT: "Open in vault",
    TOOLTIP_PREVIEW_SAVE_SOON: "Coming soon — available once the server supports vault storage.",
    ERROR_PREVIEW_INVALID_SOURCE: "Cannot preview: source reference is empty.",
    ERROR_PREVIEW_LOAD: (reason: string) => `Failed to load source: ${reason}`,
    ERROR_PREVIEW_UNSUPPORTED: (mime: string) =>
        `Cannot preview content of type "${mime}". Open the original file in your vault to view it.`,
 
    // Lint
    TITLE_LINT_RESULTS: "Wiki lint results",
    LABEL_LINT_ISSUES: (n: number, pages: number) => `${n} issues across ${pages} pages`,
    LABEL_LINT_STATUS_VALID: "valid",
    LABEL_LINT_STATUS_STALE: "stale hash",
    LABEL_LINT_STATUS_DELETED: "source deleted",
    LABEL_LINT_STATUS_MISSING: "excerpt missing",
    LABEL_LINT_STATUS_MODEL: "model changed",
 
    // Wiki settings
    // Retrieval quality (server PR #557)
    LABEL_TITLE_SEARCH: "Match on titles",
    DESC_TITLE_SEARCH:
        "Also search document titles, not just their text. Helps when you remember what a file is called but not what it says.",
    LABEL_TITLE_SEARCH_WEIGHT: "Title weight",
    DESC_TITLE_SEARCH_WEIGHT: "How much a title match counts next to a body match. Higher favours the title.",
    LABEL_ADAPTIVE_FUSION: "Adaptive fusion",
    DESC_ADAPTIVE_FUSION:
        "Let the server decide per query how much to trust keyword matching against meaning matching.",
    LABEL_ADAPTIVE_FUSION_MARGIN: "Fusion margin",
    DESC_ADAPTIVE_FUSION_MARGIN: "How clear a lead one method needs before the server leans on it.",
    LABEL_LEXICAL_FUSION_WEIGHT: "Keyword weight",
    DESC_LEXICAL_FUSION_WEIGHT: "How much keyword matching counts when results are combined.",
    LABEL_NEIGHBOR_EXPANSION: "Include neighbouring text",
    DESC_NEIGHBOR_EXPANSION:
        "Pull in this many passages either side of a match, so an answer is not cut off mid-thought. 0 keeps just the match.",
    LABEL_FILTER_STRUCTURAL_CHUNKS: "Skip boilerplate",
    DESC_FILTER_STRUCTURAL_CHUNKS: "Drop tables of contents, headers and footers from results.",
    LABEL_RERANK_MIN_SCORE: "Reranker cutoff",
    DESC_RERANK_MIN_SCORE:
        "Discard reranked results below this score. Leave empty to keep everything the reranker returns.",
    LABEL_FTS_LANGUAGE: "Keyword search language",
    DESC_FTS_LANGUAGE: "Language used for stemming in keyword search, so 'running' matches 'run'.",
    LABEL_CONTEXTUAL_ENRICHMENT: "Add context when indexing",
    DESC_CONTEXTUAL_ENRICHMENT:
        "Give each passage a short note about where it sits in the document. Makes retrieval better and indexing slower.",
    LABEL_EMBED_TITLES: "Index titles with text",
    DESC_EMBED_TITLES: "Fold the document title into each passage as it is indexed.",
    LABEL_TOKEN_SIZING: "Size passages by tokens",
    DESC_TOKEN_SIZING:
        "Split documents on token counts rather than characters, so passages fit the model more predictably.",
    // Extraction quality (xberg 1.0)
    LABEL_LAYOUT_DETECTION: "Detect page layout",
    DESC_LAYOUT_DETECTION:
        "Work out columns, headings and reading order before extracting. Much better on PDFs with complex layouts, and noticeably slower to ingest, which is why it is off by default.",
    LABEL_TABLE_EXTRACTION: "Extract tables",
    DESC_TABLE_EXTRACTION: "Pull tables out as tables instead of flattening them into a run of text.",
    LABEL_TABLE_MODEL: "Table model",
    DESC_TABLE_MODEL:
        "Which model reads tables. Auto suits most documents; the wired and wireless variants suit tables with and without ruling lines.",
    LABEL_OCR_LANGUAGE: "OCR languages",
    DESC_OCR_LANGUAGE:
        "Languages to expect when reading scanned pages, one per line. Three-letter codes, for example eng or deu.",
    // Wiki
    LABEL_WIKI_AUTO_UPDATE: "Update the wiki after ingesting",
    DESC_WIKI_AUTO_UPDATE:
        "Refresh wiki pages whenever documents are added. This is GPU work, so leave it off if you would rather run it yourself.",
    LABEL_WIKI_STUB_MAX_CHUNK_REFS: "Passages per page",
    DESC_WIKI_STUB_MAX_CHUNK_REFS: "How many source passages a generated page may draw on.",
    LABEL_WIKI_ENTITY_PAGE_PROMPT: "Entity page prompt",
    DESC_WIKI_ENTITY_PAGE_PROMPT:
        "The instructions used to write a page about a person, place or thing. Leave empty for the built-in prompt.",
    // Engine, placement and ingest
    LABEL_INGEST_PROCESSES: "Ingest workers",
    DESC_INGEST_PROCESSES: "How many documents are processed at once. 0 lets the server pick based on your machine.",
    LABEL_SYSTEM_MEMORY_RESERVE_GB: "Memory to leave free",
    DESC_SYSTEM_MEMORY_RESERVE_GB:
        "Gigabytes of system memory to keep for everything else, so loading a model does not starve your machine.",
    LABEL_USABLE_VRAM_FRACTION: "Usable VRAM",
    DESC_USABLE_VRAM_FRACTION:
        "Fraction of each card's memory the planner may fill. Lower it if other software shares the GPU.",
    LABEL_FAST_MODEL_DOWNLOADS: "Fast model downloads",
    DESC_FAST_MODEL_DOWNLOADS:
        "Download models over several connections at once. Much faster on a good link, heavier on a weak one.",
    LABEL_WIKI_SECTION: "Wiki (beta)",
    LABEL_WIKI_ENABLE_TOGGLE: "Enable wiki",
    DESC_WIKI_ENABLE_TOGGLE: "Generate AI-written summaries of your documents. This feature is in beta.",
    LABEL_WIKI_STATUS: "Wiki status",
    LABEL_WIKI_PRUNE_RAW: "Remove source duplicates",
    LABEL_WIKI_FAITHFULNESS: "Summary accuracy",
    LABEL_WIKI_SEARCH_MODE: "Default search mode",
    LABEL_WIKI_RUN_LINT: "Check wiki health",
    LABEL_WIKI_RUN_PRUNE: "Clean up wiki",
    DESC_WIKI_PRUNE_RAW: "After wiki summaries are created, remove the original text chunks that were summarized",
    DESC_WIKI_FAITHFULNESS: "How accurate wiki summaries must be. Higher = stricter quality, lower = more content",
    DESC_WIKI_RUN_LINT: "Scan wiki pages for broken references, missing sources, or outdated content",
    DESC_WIKI_RUN_PRUNE: "Remove wiki pages that are outdated or no longer have valid sources",
    DESC_WIKI_SEARCH_MODE: "Which chunks to search by default",
    LABEL_WIKI_SYNC_TO_VAULT: "Sync wiki to vault",
    DESC_WIKI_SYNC_TO_VAULT:
        "Write wiki pages as files in your vault so they appear in graph view, search, and backlinks",
    LABEL_WIKI_VAULT_FOLDER: "Wiki vault folder",
    DESC_WIKI_VAULT_FOLDER: "Folder in your vault where wiki pages are written",
    NOTICE_WIKI_SYNC: (written: number, removed: number) =>
        `lilbee: wiki sync — ${written} written, ${removed} removed`,
 
    // Search mode
    LABEL_SEARCH_ALL: "All",
    LABEL_SEARCH_WIKI: "Wiki",
    LABEL_SEARCH_RAW: "Raw",
 
    // Wiki tasks/notices
    NOTICE_WIKI_LINT_DONE: (issues: number) => `lilbee: lint complete — ${issues} issues found`,
    NOTICE_WIKI_UPDATE_DONE: (count: number) => `lilbee: wiki updated, ${count} pages`,
    NOTICE_WIKI_PRUNE_DONE: (archived: number) => `lilbee: pruned ${archived} pages`,
    NOTICE_WIKI_PRUNE_CONFIRM: "This will archive wiki pages with broken citations. Continue?",
 
    // Wiki commands
    COMMAND_WIKI: "Browse wiki",
    COMMAND_WIKI_LINT: "Run wiki lint",
    COMMAND_WIKI_UPDATE: "Update wiki",
    TASK_WIKI_UPDATE: "Update wiki",
    TASK_WIKI_PAGE: (label: string, current: number, total: number) =>
        total > 0 ? `${label} (${current} of ${total})` : label,
    TASK_WIKI_PHASE: (phase: string) => {
        const phases: Record<string, string> = {
            extract: "Finding entities",
            generate: "Writing pages",
            index: "Building the index",
        };
        return phases[phase] ?? phase;
    },
 
    WIZARD_INTRO_DESC: "lilbee turns your Obsidian vault into a searchable knowledge base powered by AI.",
    WIZARD_INTRO_STEPS: "This wizard will help you:",
    WIZARD_STEP_CHOOSE_SERVER: "Choose where lilbee runs (this machine, or an existing server)",
    WIZARD_STEP_CHOOSE_MODEL: "Choose an AI model that fits the host's RAM",
    WIZARD_STEP_INDEX: "Index your vault so you can search and chat",
    WIZARD_LOCAL_ONLY_MANAGED: "Managed mode runs on this machine — your notes never leave your laptop.",
    WIZARD_LOCAL_ONLY_EXTERNAL:
        "External mode talks to your existing lilbee server. Your notes stay on whichever host runs it.",
    WIZARD_MODEL_HELP:
        "This is the AI that answers your questions about your notes. Bigger models are smarter but need more RAM and disk space.",
    WIZARD_SYSTEM_RAM: "Your system: {ram} GB RAM",
    WIZARD_SYNC_HELP:
        "lilbee needs to read your notes once to make them searchable. This happens locally on your machine.",
    WIZARD_STEP_BADGE: "Step {num} · {label}",
    WIZARD_PROGRESS_BACKGROUND: "Downloads continue in the background.",
    WIZARD_WIKI_TRADEOFFS_LABEL: "See the tradeoffs",
    WIZARD_SUMMARY_HEADING: "Setup complete",
    WIZARD_SUMMARY_MODEL: "Chat model: {model}",
    WIZARD_SUMMARY_FILES: "{count} files indexed",
    WIZARD_SUMMARY_PROCESSED: "{count} files processed",
    WIZARD_TIPS: "What to try next",
    WIZARD_TIP_CHAT: "Open the chat panel to ask questions about your notes",
    WIZARD_TIP_SEARCH: "Use the search command to find specific content",
    WIZARD_TIP_DRAG: "Drag files into the chat to add context",
    WIZARD_CHANGE_SETTINGS: "You can change models and settings anytime in the lilbee settings tab.",
    WIZARD_MANAGED_DESC: "lilbee starts and stops automatically with Obsidian. No terminal needed.",
    WIZARD_EXTERNAL_DESC: "You run the lilbee server yourself. For advanced users or shared setups.",
    WIZARD_STATUS_STARTING: "Starting...",
    WIZARD_SELECT_MODEL: "Please select a model first.",
 
    // Wiki wizard step
    WIZARD_WIKI_TITLE: "Wiki (optional)",
    WIZARD_WIKI_DESC:
        "Wiki pages are AI-written summaries of your documents, like Wikipedia articles derived from what you already have. Generation runs on demand from the command palette or Settings; enabling it here just unlocks those controls.",
    WIZARD_WIKI_CAVEATS_HEADING: "Before you turn this on",
    WIZARD_WIKI_CAVEATS_INTRO: "Two things worth knowing:",
    WIZARD_WIKI_CAVEAT_QUALITY:
        "Everything lands as a draft first. Every page is gated by a title/body coherence check and a cosine-similarity score against the source chunks, then ships as a reviewable draft. Accept, edit, or reject it from the drafts panel before it becomes a published page.",
    WIZARD_WIKI_CAVEAT_COST:
        "Building the wiki is the heaviest thing lilbee does. It runs your chat model over the whole corpus and holds the GPU for the duration, so chat is unavailable while it works. Pages sharing a primary source are batched into one call, so a 200-page vault over roughly 30 sources costs about 30 calls rather than 200. Run it during a break, not in the middle of research.",
    TOOLTIP_WIZARD_WIKI_ENABLE:
        "Turning this on only unlocks the commands. Nothing is generated until you run a build, and a build is GPU heavy: it runs your chat model across every source and blocks chat while it does. Pages arrive as drafts for you to review.",
    WIZARD_WIKI_PROS_HEADING: "What it adds",
    WIZARD_WIKI_PRO_SUMMARIES: "Summarized, structured overviews of your documents",
    WIZARD_WIKI_PRO_CROSSREFS: "Related concepts become discoverable through cross-references",
    WIZARD_WIKI_PRO_ANSWERS: "Chat and search can reference wiki pages for more coherent answers",
    WIZARD_WIKI_CONS_HEADING: "Worth knowing",
    WIZARD_WIKI_CON_TOKENS: "Each page run uses LLM compute / API tokens",
    WIZARD_WIKI_CON_ACCURACY: "Small models can hallucinate — always verify against source",
    WIZARD_WIKI_CON_SEARCH: "Search may prioritise wiki chunks over raw ones if you include both",
    WIZARD_WIKI_CON_COMPLEXITY: "Adds a second index shape to maintain alongside raw embeddings",
    WIZARD_WIKI_ENABLE: "Enable wiki (on-demand generation)",
    WIZARD_WIKI_DISABLE: "Skip for now (recommended)",
    WIZARD_WIKI_ENABLE_DESC: "Unlocks the wiki commands and settings. Nothing generates until you ask for it.",
    WIZARD_WIKI_DISABLE_DESC:
        "Use raw document search only. You can enable the wiki later from Settings → Wiki, or re-open this wizard from Settings → Setup wizard.",
    TITLE_PICK_EMBEDDING: "Pick an embedding model",
    WIZARD_EMBEDDING_HELP:
        "The embedding model converts your documents into searchable vectors. The default works well for most users.",
    WIZARD_EMBEDDING_RECOMMENDED: "Recommended",
 
    WIZARD_FILE_PICKER_VAULT: "From vault",
    WIZARD_FILE_PICKER_DISK: "Files from disk",
    WIZARD_FOLDER_PICKER_DISK: "Folder from disk",
    WIZARD_CRAWL_WEB: "Crawl web page",
 
    // Managed-mode consent modal
    MANAGED_CONSENT_TITLE: "How should lilbee run?",
    MANAGED_CONSENT_SUBTITLE: "lilbee indexes your vault from a local server. Pick who runs it.",
    MANAGED_CONSENT_PILL_RECOMMENDED: "Recommended",
    MANAGED_CONSENT_CARD_MANAGED_TITLE: "Managed",
    MANAGED_CONSENT_CARD_MANAGED_DESC:
        "This plugin downloads the server and handles start, stop, and updates. Nothing else to install.",
    MANAGED_CONSENT_CARD_EXTERNAL_TITLE: "External",
    MANAGED_CONSENT_CARD_EXTERNAL_DESC: "You run lilbee serve yourself. The plugin connects to it.",
    MANAGED_CONSENT_CARD_EXTERNAL_HINT: "For advanced / custom setups",
    MANAGED_CONSENT_CONFIGURE_LINK: "Set your server URL in Settings",
    MANAGED_CONSENT_PROV_LABEL: "Source · verified",
    MANAGED_CONSENT_PROV_PENDING: "Fetching release info…",
    MANAGED_CONSENT_PROV_FAILED: "(release info unavailable)",
    MANAGED_CONSENT_PROV_RELEASE_NOTES: "View release notes ↗",
    MANAGED_CONSENT_PROV_ONE_TIME: "one-time",
    MANAGED_CONSENT_BTN_CANCEL: "Cancel",
    MANAGED_CONSENT_BTN_DOWNLOAD: "↓ Download & manage server",
    NOTICE_SERVER_DOWNLOAD_CANCELED: "Server download canceled.",
    NOTICE_SWITCHED_TO_EXTERNAL: "Switched to external mode. Set your server URL in Settings to connect.",
 
    // Chat-view model rail
    RAIL_LABEL_CHAT: "Chat",
    RAIL_LABEL_EMBED: "Embed",
    RAIL_LABEL_VISION: "Vision",
    RAIL_LABEL_RERANK: "Rerank",
    RAIL_BROWSE_CATALOG: "Browse catalog…",
 
    // GPU placement / fleet
    LABEL_PLACEMENT_VIEW: "lilbee GPU placement",
    PLACEMENT_TITLE: "GPU placement",
    PLACEMENT_SECTION_HARDWARE: "Hardware",
    PLACEMENT_SECTION_ROLES: "Roles",
    PLACEMENT_HOST_APPLE: "Apple Silicon",
    PLACEMENT_HOST_APPLE_SUB: "unified memory · Metal",
    PLACEMENT_HOST_CPU: "CPU",
    PLACEMENT_HOST_CPU_SUB: "no GPU detected",
    PLACEMENT_STATE_AUTO: "auto",
    PLACEMENT_STATE_MANUAL: "manual",
    PLACEMENT_STATE_EDITED: "edited",
    PLACEMENT_STATE_APPLYING: "applying…",
    PLACEMENT_AUTO_MANAGED: "Auto-managed. The planner balances roles across your hardware.",
    PLACEMENT_EDIT: "Edit manually",
    PLACEMENT_PREVIEW: "Preview",
    PLACEMENT_APPLY: "Apply",
    PLACEMENT_RESET: "Reset to auto",
    PLACEMENT_FITS: "Fits all roles",
    PLACEMENT_WONT_FIT: (roles: string): string => `Won't fit: ${roles}. Move it to another GPU or free up memory.`,
    PLACEMENT_NOT_SET: "not set",
    PLACEMENT_TIP_CHIP: (role: WorkerRole, gpu: string): string => `Run ${roleNoun(role)} on ${gpu}`,
    PLACEMENT_TIP_REPLICA_ADD: (role: WorkerRole): string =>
        `Add ${indefinite(roleNoun(role))} ${roleNoun(role)} worker`,
    PLACEMENT_TIP_REPLICA_REMOVE: (role: WorkerRole): string =>
        `Remove ${indefinite(roleNoun(role))} ${roleNoun(role)} worker`,
    PLACEMENT_HINT_EDIT_MANUALLY: 'Click "Edit manually" first to change GPU placement.',
    PLACEMENT_HINT_REPLICAS_SETTINGS:
        "This device runs everything together. Set worker counts in Settings → lilbee → Hardware / fleet.",
    PLACEMENT_MEM_FREE: (free: string, total: string): string => `${free} / ${total} free`,
    PLACEMENT_MEM_UNIFIED: (total: string): string => `${total} unified`,
    PLACEMENT_ROLE_VRAM: (size: string): string => `~${size}`,
    PLACEMENT_TIP_ROLE_VRAM: (role: WorkerRole): string => `Estimated memory the ${roleNoun(role)} model needs`,
    PLACEMENT_UTIL: (pct: number): string => `${pct}%`,
    PLACEMENT_UTIL_NA: "—",
    PLACEMENT_METER_UTIL: "util",
    PLACEMENT_METER_VRAM: "vram",
    PLACEMENT_HINT_SPLIT: "split",
    PLACEMENT_HINT_MIRROR: "mirror",
    PLACEMENT_HINT_SINGLE: "one card",
    PLACEMENT_REBUILDING: "Rebuilding fleet…",
    PLACEMENT_APPLIED: "Placement applied.",
    PLACEMENT_RESET_DONE: "Back to automatic placement.",
    PLACEMENT_APPLY_NOT_ENABLED: "Applying placement isn't enabled on this lilbee server.",
    PLACEMENT_APPLY_FAILED: (msg: string): string => `Couldn't apply placement: ${msg}`,
    PLACEMENT_LOAD_FAILED: (msg: string): string => `Couldn't load placement: ${msg}`,
    PLACEMENT_UPDATE_SERVER_PROMPT:
        "GPU placement and monitoring need lilbee server 0.6.90 or newer. Continue to Settings to update the server.",
    PLACEMENT_NEEDS_NEWER_SERVER:
        "GPU placement and monitoring aren't available on this lilbee server. Update the server to 0.6.90 or newer, then reopen this view.",
    PLACEMENT_WAITING_SERVER: "Waiting for the lilbee server to start. This view loads automatically once it's up.",
    PLACEMENT_GPU_NOTICE: (notice: string): string => `GPU monitoring: ${notice}`,
    PLACEMENT_GPU_NOTICE_TOOLTIP:
        "The lilbee server can't read GPU utilization on this host. The hint below tells you which tool to install or permission to grant.",
 
    // Hardware / fleet settings
    LABEL_FLEET: "Hardware / fleet",
    LABEL_FLEET_HELP:
        "How the model fleet uses your GPUs. These apply to local (managed) and self-hosted servers; they have no effect when lilbee points at a hosted provider.",
    LABEL_FLASH_ATTENTION: "Flash attention",
    DESC_FLASH_ATTENTION:
        "Faster attention kernels that cut chat context memory on supported GPUs. Reset to let lilbee decide per model.",
    LABEL_KV_CACHE_TYPE: "KV cache type",
    DESC_KV_CACHE_TYPE:
        "Smaller types use less memory for the chat context. q8_0 roughly halves it versus f16 with no noticeable quality loss; q4_0 quarters it.",
    LABEL_N_GPU_LAYERS: "GPU layers (chat)",
    DESC_N_GPU_LAYERS:
        "How many chat-model layers to offload to the GPU. Leave empty to offload all; lower it to fit a large model in limited VRAM.",
    LABEL_EMBED_REPLICAS: "Embedding replicas",
    DESC_EMBED_REPLICAS: "How many embedding workers to run while indexing. 0 lets lilbee pick (one per GPU).",
    LABEL_VISION_REPLICAS: "Vision replicas",
    DESC_VISION_REPLICAS: "How many OCR workers to run while indexing. 0 lets lilbee pick (one per GPU).",
    LABEL_GPU_DEVICES: "Visible GPUs",
    DESC_GPU_DEVICES:
        "Restrict lilbee to specific GPUs by index, comma separated (for example 0,1). Leave empty to use all.",
    PLACEHOLDER_GPU_DEVICES: "all",
 
    // Memory
    LABEL_MEMORIES_VIEW: "lilbee Memories",
    LABEL_MEMORIES_TITLE: "Memories",
    MEMORIES_SEARCH_PLACEHOLDER: "Filter memories…",
    MEMORIES_FLAG_YES: "yes",
    MEMORIES_FLAG_NO: "no",
    MEMORIES_EMPTY: "No memories stored. Use the Remember command to add one.",
    MEMORIES_DISABLED: "Memory is off. Enable it in Settings → lilbee → Memory.",
    MEMORIES_LOAD_FAILED: (detail: string): string => `Failed to load memories: ${detail}`,
    MEMORIES_DELETE_CONFIRM: "Delete this memory? This cannot be undone.",
    MEMORIES_DELETED: "Deleted memory",
    MEMORIES_DELETE_FAILED: (detail: string): string => `Delete failed: ${detail}`,
    MEMORIES_SHARED_ON: "Shared with agents",
    MEMORIES_SHARED_OFF: "No longer shared with agents",
    MEMORIES_FLAG_FAILED: (detail: string): string => `Update failed: ${detail}`,
    MEMORIES_DELETE_TOOLTIP: "Delete memory",
    MEMORIES_SHARE_TOOLTIP: "Toggle sharing with agents",
    BUTTON_ADD_MEMORY: "Add memory",
 
    LABEL_REMEMBER_TITLE: "Remember",
    LABEL_REMEMBER_TEXT_LABEL: "Memory",
    LABEL_REMEMBER_TEXT_PLACEHOLDER: "What should lilbee remember?",
    LABEL_REMEMBER_KIND_LABEL: "Kind",
    LABEL_REMEMBER_KIND_FACT: "Fact",
    LABEL_REMEMBER_KIND_PREFERENCE: "Preference",
    LABEL_REMEMBER_SHARED_LABEL: "Share with agents",
    BUTTON_REMEMBER: "Remember",
    REMEMBER_EMPTY: "Enter something to remember.",
    REMEMBER_SUCCESS: (kind: string): string => `Remembered (${kind}).`,
    REMEMBER_FAILED: (detail: string): string => `Could not save the memory: ${detail}`,
    MEMORY_EXTRACTED_NOTICE: (count: number): string =>
        `Noted ${count} ${count === 1 ? "memory" : "memories"} to review in Memories.`,
 
    LABEL_MEMORY_SECTION: "Memory",
    LABEL_MEMORY_ENABLED: "Enable memory",
    DESC_MEMORY_ENABLED: "Let lilbee recall saved facts and preferences during chat. Requires an embedding model.",
    LABEL_MEMORY_AUTO_EXTRACT: "Auto-extract memories from chat",
    DESC_MEMORY_AUTO_EXTRACT: "After each answer, lilbee proposes memories to save. Review them in the Memories view.",
    NOTICE_MEMORY_CONFIG_FAILED: "lilbee: could not update memory settings",
 
    // Diagnostics export
    DIAG_REVIEW_WARNING:
        "Review this bundle before sharing. Secrets are redacted, but log lines can include file paths and note titles.",
    DIAG_REMOTE_SERVER_NOTE:
        "External server data dir is not locally readable; server logs must be fetched from the machine running lilbee serve.",
    BUTTON_EXPORT_DIAGNOSTICS: "Export diagnostics",
    LABEL_EXPORT_DIAGNOSTICS: "Diagnostics",
    DESC_EXPORT_DIAGNOSTICS:
        "Bundle logs, settings (secrets removed), and recent errors into a zip you can attach to a bug report.",
    NOTICE_DIAGNOSTICS_EXPORTED: (path: string): string =>
        `Diagnostics saved to ${path}. The summary is on your clipboard. Review the contents before sharing.`,
    NOTICE_DIAGNOSTICS_SUMMARY_ONLY: (path: string): string =>
        `Zip creation failed; wrote summary.md to ${path} instead. Review the contents before sharing.`,
    NOTICE_DIAGNOSTICS_FAILED: (detail: string): string => `Diagnostics export failed: ${detail}`,
 
    // Agent integration
    LABEL_AGENT_SECTION: "Agent integration",
    DESC_AGENT_SECTION: "Connect a coding agent to lilbee, so it thinks with your models and can search your vault.",
    LABEL_AGENT_SUPPORTED: "Supported agents",
    BUTTON_AGENT_RESCAN: "Rescan",
    LABEL_AGENT_CHOICE: "Coding agent",
    DESC_AGENT_CHOICE: "The agent lilbee connects to your models. Installed agents appear here.",
    AGENT_OPTION_NONE: "None",
    LABEL_AGENT_KEEP_FRESH: "Keep the agent connected",
    DESC_AGENT_KEEP_FRESH: "Reconnect the agent to your models automatically after a restart.",
    LABEL_AGENT_MODEL: "Agent model",
    BUTTON_AGENT_CHANGE_MODEL: "Change model",
    AGENT_CONTEXT_BADGE: (tokens: number): string => `${Math.round(tokens / 1000)}k context`,
    AGENT_CONTEXT_UNKNOWN: "Context window unknown until the model loads.",
    AGENT_CONTEXT_WARNING: "Coding agents need strong tool calling and a large context window.",
    LABEL_AGENT_CLAUDIAN: "Claudian panel",
    DESC_AGENT_CLAUDIAN_CONFIGURED: "Claudian's coding agent now runs on your models.",
    PILL_AGENT_CLAUDIAN_CONFIGURED: "Connected",
    BUTTON_OPEN_CLAUDIAN: "Open Claudian",
    DESC_AGENT_CLAUDIAN_MISSING:
        "Claudian brings a coding agent into Obsidian. Install it from the community plugin store to chat with the agent without leaving your vault.",
    DESC_AGENT_CLAUDIAN_SKIPPED: "Finish connecting the agent in Claudian's settings.",
    AGENT_STATUS_CONNECTED: "Connected to your models",
    AGENT_STATUS_PENDING: "Connects when the server starts.",
    AGENT_DETECT_UNAVAILABLE: "lilbee could not check for agents. Make sure the server is running, then rescan.",
    AGENT_STATUS_GLOBAL: "Hermes uses one global config. Copy the block below into it.",
    LABEL_AGENT_COPY_CONFIG: "Config to copy",
    BUTTON_AGENT_COPY: "Copy",
    NOTICE_AGENT_COPIED: "Config copied.",
    NOTICE_AGENT_CONNECTED: (agent: string): string => `${agent} is connected to lilbee.`,
    NOTICE_AGENT_CONNECTED_GLOBAL: (agent: string): string =>
        `${agent} config is ready to copy from lilbee's settings.`,
    NOTICE_AGENT_CONFIG_FAILED: (detail: string): string => `lilbee could not connect the agent: ${detail}`,
    NOTICE_CLAUDIAN_UPDATED: "Reload Claudian to finish connecting.",
    BUTTON_RELOAD_CLAUDIAN: "Reload Claudian",
    ERROR_AGENT_CONFIG_UNREADABLE: (path: string): string =>
        `${path} is not valid JSON, so lilbee left it alone. Fix or delete it and try again.`,
    ERROR_AGENT_CONFIG_EMPTY: (client: string): string => `The server returned no ${client} config to write.`,
 
    // Agent picker modal
    AGENT_PICKER_TITLE: "Connect a coding agent",
    AGENT_PICKER_SUBTITLE: "lilbee found an agent on this machine. Want it to work with this vault?",
    AGENT_PICKER_OPENCODE_DESC: "Full pairing: your lilbee models are the agent's brain, vault search included.",
    AGENT_PICKER_BADGE_CLAUDIAN: "works with Claudian",
    AGENT_PICKER_CLAUDE_DESC: "Gets lilbee's vault search and wiki tools. Models stay with Anthropic.",
    AGENT_PICKER_HERMES_DESC: "Runs on your lilbee models with vault search, from its own global config.",
    AGENT_PICKER_NOT_DETECTED: "Not installed",
    AGENT_PICKER_REMEMBER: "Remember my choice",
    BUTTON_AGENT_NOT_NOW: "Not now",
    BUTTON_AGENT_CONNECT: (agent: string): string => `Connect ${agent}`,
    BUTTON_AGENT_CONNECT_NONE: "Connect",
} as const;
 
/** Product names for the agent clients; the wire values are lowercase ids. */
export const AGENT_LABELS = {
    [AGENT_CLIENT.CLAUDE]: "Claude Code",
    [AGENT_CLIENT.HERMES]: "hermes",
    [AGENT_CLIENT.OPENCODE]: "OpenCode",
} as const;
 
/** Where to get each agent, shown when none is installed. */
export const AGENT_LINKS: readonly { label: string; url: string }[] = [
    { label: "opencode.ai", url: "https://opencode.ai" },
    { label: "hermes-agent.ai", url: "https://hermes-agent.ai" },
    { label: "claude.com/code", url: "https://claude.com/code" },
];
 
export const FILTERS = {
    TASK: {
        ALL: "",
        CHAT: "chat",
        EMBEDDING: "embedding",
        VISION: "vision",
        RERANK: "rerank",
    } as const,
    SIZE: {
        ALL: "",
        SMALL: "small",
        MEDIUM: "medium",
        LARGE: "large",
    } as const,
    SORT: {
        FEATURED: "featured",
        DOWNLOADS: "downloads",
        NAME: "name",
        SIZE_ASC: "size_asc",
        SIZE_DESC: "size_desc",
    } as const,
} as const;
 
export const TASK_LABELS = {
    [MODEL_TASK.CHAT]: "Chat",
    [MODEL_TASK.VISION]: "Vision",
    [MODEL_TASK.EMBEDDING]: "Embedding",
    [MODEL_TASK.RERANK]: "Rerank",
} as const;
 
export const CATALOG_FILTERS = {
    TASK: [
        [FILTERS.TASK.ALL, MESSAGES.LABEL_ALL_TASKS],
        [FILTERS.TASK.CHAT, TASK_LABELS[MODEL_TASK.CHAT]],
        [FILTERS.TASK.EMBEDDING, TASK_LABELS[MODEL_TASK.EMBEDDING]],
        [FILTERS.TASK.VISION, TASK_LABELS[MODEL_TASK.VISION]],
        [FILTERS.TASK.RERANK, TASK_LABELS[MODEL_TASK.RERANK]],
    ] as const,
    SIZE: [
        [FILTERS.SIZE.ALL, MESSAGES.LABEL_ALL_SIZES],
        [FILTERS.SIZE.SMALL, MESSAGES.LABEL_SIZE_SMALL],
        [FILTERS.SIZE.MEDIUM, MESSAGES.LABEL_SIZE_MEDIUM],
        [FILTERS.SIZE.LARGE, MESSAGES.LABEL_SIZE_LARGE],
    ] as const,
    SORT: [
        [FILTERS.SORT.FEATURED, MESSAGES.LABEL_FEATURED],
        [FILTERS.SORT.DOWNLOADS, MESSAGES.LABEL_DOWNLOADS],
        [FILTERS.SORT.NAME, MESSAGES.LABEL_NAME],
        [FILTERS.SORT.SIZE_ASC, MESSAGES.LABEL_SIZE_ASC],
        [FILTERS.SORT.SIZE_DESC, MESSAGES.LABEL_SIZE_DESC],
    ] as const,
} as const;