claude-code-workflow 6.2.7 → 6.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (208) hide show
  1. package/.claude/CLAUDE.md +16 -1
  2. package/.claude/workflows/cli-templates/protocols/analysis-protocol.md +11 -4
  3. package/.claude/workflows/cli-templates/protocols/write-protocol.md +10 -75
  4. package/.claude/workflows/cli-tools-usage.md +14 -24
  5. package/.codex/AGENTS.md +51 -1
  6. package/.codex/prompts/compact.md +378 -0
  7. package/.gemini/GEMINI.md +57 -20
  8. package/ccw/dist/cli.d.ts.map +1 -1
  9. package/ccw/dist/cli.js +21 -8
  10. package/ccw/dist/cli.js.map +1 -1
  11. package/ccw/dist/commands/cli.d.ts +2 -0
  12. package/ccw/dist/commands/cli.d.ts.map +1 -1
  13. package/ccw/dist/commands/cli.js +129 -8
  14. package/ccw/dist/commands/cli.js.map +1 -1
  15. package/ccw/dist/commands/hook.d.ts.map +1 -1
  16. package/ccw/dist/commands/hook.js +3 -2
  17. package/ccw/dist/commands/hook.js.map +1 -1
  18. package/ccw/dist/config/litellm-api-config-manager.d.ts +180 -0
  19. package/ccw/dist/config/litellm-api-config-manager.d.ts.map +1 -0
  20. package/ccw/dist/config/litellm-api-config-manager.js +770 -0
  21. package/ccw/dist/config/litellm-api-config-manager.js.map +1 -0
  22. package/ccw/dist/config/provider-models.d.ts +73 -0
  23. package/ccw/dist/config/provider-models.d.ts.map +1 -0
  24. package/ccw/dist/config/provider-models.js +172 -0
  25. package/ccw/dist/config/provider-models.js.map +1 -0
  26. package/ccw/dist/core/cache-manager.d.ts.map +1 -1
  27. package/ccw/dist/core/cache-manager.js +3 -5
  28. package/ccw/dist/core/cache-manager.js.map +1 -1
  29. package/ccw/dist/core/dashboard-generator.d.ts.map +1 -1
  30. package/ccw/dist/core/dashboard-generator.js +3 -1
  31. package/ccw/dist/core/dashboard-generator.js.map +1 -1
  32. package/ccw/dist/core/routes/cli-routes.d.ts.map +1 -1
  33. package/ccw/dist/core/routes/cli-routes.js +169 -0
  34. package/ccw/dist/core/routes/cli-routes.js.map +1 -1
  35. package/ccw/dist/core/routes/codexlens-routes.d.ts.map +1 -1
  36. package/ccw/dist/core/routes/codexlens-routes.js +234 -18
  37. package/ccw/dist/core/routes/codexlens-routes.js.map +1 -1
  38. package/ccw/dist/core/routes/hooks-routes.d.ts.map +1 -1
  39. package/ccw/dist/core/routes/hooks-routes.js +30 -32
  40. package/ccw/dist/core/routes/hooks-routes.js.map +1 -1
  41. package/ccw/dist/core/routes/litellm-api-routes.d.ts +21 -0
  42. package/ccw/dist/core/routes/litellm-api-routes.d.ts.map +1 -0
  43. package/ccw/dist/core/routes/litellm-api-routes.js +780 -0
  44. package/ccw/dist/core/routes/litellm-api-routes.js.map +1 -0
  45. package/ccw/dist/core/routes/litellm-routes.d.ts +20 -0
  46. package/ccw/dist/core/routes/litellm-routes.d.ts.map +1 -0
  47. package/ccw/dist/core/routes/litellm-routes.js +85 -0
  48. package/ccw/dist/core/routes/litellm-routes.js.map +1 -0
  49. package/ccw/dist/core/routes/mcp-routes.js +2 -2
  50. package/ccw/dist/core/routes/mcp-routes.js.map +1 -1
  51. package/ccw/dist/core/routes/status-routes.d.ts.map +1 -1
  52. package/ccw/dist/core/routes/status-routes.js +39 -0
  53. package/ccw/dist/core/routes/status-routes.js.map +1 -1
  54. package/ccw/dist/core/routes/system-routes.js +1 -1
  55. package/ccw/dist/core/routes/system-routes.js.map +1 -1
  56. package/ccw/dist/core/server.d.ts.map +1 -1
  57. package/ccw/dist/core/server.js +15 -1
  58. package/ccw/dist/core/server.js.map +1 -1
  59. package/ccw/dist/mcp-server/index.js +1 -1
  60. package/ccw/dist/mcp-server/index.js.map +1 -1
  61. package/ccw/dist/tools/claude-cli-tools.d.ts +82 -0
  62. package/ccw/dist/tools/claude-cli-tools.d.ts.map +1 -0
  63. package/ccw/dist/tools/claude-cli-tools.js +216 -0
  64. package/ccw/dist/tools/claude-cli-tools.js.map +1 -0
  65. package/ccw/dist/tools/cli-executor.d.ts.map +1 -1
  66. package/ccw/dist/tools/cli-executor.js +76 -14
  67. package/ccw/dist/tools/cli-executor.js.map +1 -1
  68. package/ccw/dist/tools/codex-lens.d.ts +9 -2
  69. package/ccw/dist/tools/codex-lens.d.ts.map +1 -1
  70. package/ccw/dist/tools/codex-lens.js +114 -9
  71. package/ccw/dist/tools/codex-lens.js.map +1 -1
  72. package/ccw/dist/tools/context-cache-store.d.ts +136 -0
  73. package/ccw/dist/tools/context-cache-store.d.ts.map +1 -0
  74. package/ccw/dist/tools/context-cache-store.js +256 -0
  75. package/ccw/dist/tools/context-cache-store.js.map +1 -0
  76. package/ccw/dist/tools/context-cache.d.ts +56 -0
  77. package/ccw/dist/tools/context-cache.d.ts.map +1 -0
  78. package/ccw/dist/tools/context-cache.js +294 -0
  79. package/ccw/dist/tools/context-cache.js.map +1 -0
  80. package/ccw/dist/tools/core-memory.d.ts.map +1 -1
  81. package/ccw/dist/tools/core-memory.js +33 -19
  82. package/ccw/dist/tools/core-memory.js.map +1 -1
  83. package/ccw/dist/tools/index.d.ts.map +1 -1
  84. package/ccw/dist/tools/index.js +2 -0
  85. package/ccw/dist/tools/index.js.map +1 -1
  86. package/ccw/dist/tools/litellm-client.d.ts +85 -0
  87. package/ccw/dist/tools/litellm-client.d.ts.map +1 -0
  88. package/ccw/dist/tools/litellm-client.js +188 -0
  89. package/ccw/dist/tools/litellm-client.js.map +1 -0
  90. package/ccw/dist/tools/litellm-executor.d.ts +34 -0
  91. package/ccw/dist/tools/litellm-executor.d.ts.map +1 -0
  92. package/ccw/dist/tools/litellm-executor.js +192 -0
  93. package/ccw/dist/tools/litellm-executor.js.map +1 -0
  94. package/ccw/dist/tools/pattern-parser.d.ts +55 -0
  95. package/ccw/dist/tools/pattern-parser.d.ts.map +1 -0
  96. package/ccw/dist/tools/pattern-parser.js +237 -0
  97. package/ccw/dist/tools/pattern-parser.js.map +1 -0
  98. package/ccw/dist/tools/smart-search.d.ts +1 -0
  99. package/ccw/dist/tools/smart-search.d.ts.map +1 -1
  100. package/ccw/dist/tools/smart-search.js +117 -41
  101. package/ccw/dist/tools/smart-search.js.map +1 -1
  102. package/ccw/dist/types/litellm-api-config.d.ts +294 -0
  103. package/ccw/dist/types/litellm-api-config.d.ts.map +1 -0
  104. package/ccw/dist/types/litellm-api-config.js +8 -0
  105. package/ccw/dist/types/litellm-api-config.js.map +1 -0
  106. package/ccw/src/cli.ts +258 -244
  107. package/ccw/src/commands/cli.ts +153 -9
  108. package/ccw/src/commands/hook.ts +3 -2
  109. package/ccw/src/config/.litellm-api-config-manager.ts.2025-12-23T11-57-43-727Z.bak +441 -0
  110. package/ccw/src/config/litellm-api-config-manager.ts +1012 -0
  111. package/ccw/src/config/provider-models.ts +222 -0
  112. package/ccw/src/core/cache-manager.ts +292 -294
  113. package/ccw/src/core/dashboard-generator.ts +3 -1
  114. package/ccw/src/core/routes/cli-routes.ts +192 -0
  115. package/ccw/src/core/routes/codexlens-routes.ts +241 -19
  116. package/ccw/src/core/routes/hooks-routes.ts +399 -405
  117. package/ccw/src/core/routes/litellm-api-routes.ts +930 -0
  118. package/ccw/src/core/routes/litellm-routes.ts +107 -0
  119. package/ccw/src/core/routes/mcp-routes.ts +1271 -1271
  120. package/ccw/src/core/routes/status-routes.ts +51 -0
  121. package/ccw/src/core/routes/system-routes.ts +1 -1
  122. package/ccw/src/core/server.ts +15 -1
  123. package/ccw/src/mcp-server/index.ts +1 -1
  124. package/ccw/src/templates/dashboard-css/12-cli-legacy.css +44 -0
  125. package/ccw/src/templates/dashboard-css/31-api-settings.css +2265 -0
  126. package/ccw/src/templates/dashboard-js/components/cli-history.js +15 -8
  127. package/ccw/src/templates/dashboard-js/components/cli-status.js +323 -9
  128. package/ccw/src/templates/dashboard-js/components/navigation.js +329 -313
  129. package/ccw/src/templates/dashboard-js/i18n.js +583 -1
  130. package/ccw/src/templates/dashboard-js/views/api-settings.js +3362 -0
  131. package/ccw/src/templates/dashboard-js/views/cli-manager.js +199 -24
  132. package/ccw/src/templates/dashboard-js/views/codexlens-manager.js +1265 -27
  133. package/ccw/src/templates/dashboard.html +840 -831
  134. package/ccw/src/tools/claude-cli-tools.ts +300 -0
  135. package/ccw/src/tools/cli-executor.ts +83 -14
  136. package/ccw/src/tools/codex-lens.ts +146 -9
  137. package/ccw/src/tools/context-cache-store.ts +368 -0
  138. package/ccw/src/tools/context-cache.ts +393 -0
  139. package/ccw/src/tools/core-memory.ts +33 -19
  140. package/ccw/src/tools/index.ts +2 -0
  141. package/ccw/src/tools/litellm-client.ts +246 -0
  142. package/ccw/src/tools/litellm-executor.ts +241 -0
  143. package/ccw/src/tools/pattern-parser.ts +329 -0
  144. package/ccw/src/tools/smart-search.ts +142 -41
  145. package/ccw/src/types/litellm-api-config.ts +402 -0
  146. package/ccw-litellm/README.md +180 -0
  147. package/ccw-litellm/pyproject.toml +35 -0
  148. package/ccw-litellm/src/ccw_litellm/__init__.py +47 -0
  149. package/ccw-litellm/src/ccw_litellm/__pycache__/__init__.cpython-313.pyc +0 -0
  150. package/ccw-litellm/src/ccw_litellm/__pycache__/cli.cpython-313.pyc +0 -0
  151. package/ccw-litellm/src/ccw_litellm/cli.py +108 -0
  152. package/ccw-litellm/src/ccw_litellm/clients/__init__.py +12 -0
  153. package/ccw-litellm/src/ccw_litellm/clients/__pycache__/__init__.cpython-313.pyc +0 -0
  154. package/ccw-litellm/src/ccw_litellm/clients/__pycache__/litellm_embedder.cpython-313.pyc +0 -0
  155. package/ccw-litellm/src/ccw_litellm/clients/__pycache__/litellm_llm.cpython-313.pyc +0 -0
  156. package/ccw-litellm/src/ccw_litellm/clients/litellm_embedder.py +251 -0
  157. package/ccw-litellm/src/ccw_litellm/clients/litellm_llm.py +165 -0
  158. package/ccw-litellm/src/ccw_litellm/config/__init__.py +22 -0
  159. package/ccw-litellm/src/ccw_litellm/config/__pycache__/__init__.cpython-313.pyc +0 -0
  160. package/ccw-litellm/src/ccw_litellm/config/__pycache__/loader.cpython-313.pyc +0 -0
  161. package/ccw-litellm/src/ccw_litellm/config/__pycache__/models.cpython-313.pyc +0 -0
  162. package/ccw-litellm/src/ccw_litellm/config/loader.py +316 -0
  163. package/ccw-litellm/src/ccw_litellm/config/models.py +130 -0
  164. package/ccw-litellm/src/ccw_litellm/interfaces/__init__.py +14 -0
  165. package/ccw-litellm/src/ccw_litellm/interfaces/__pycache__/__init__.cpython-313.pyc +0 -0
  166. package/ccw-litellm/src/ccw_litellm/interfaces/__pycache__/embedder.cpython-313.pyc +0 -0
  167. package/ccw-litellm/src/ccw_litellm/interfaces/__pycache__/llm.cpython-313.pyc +0 -0
  168. package/ccw-litellm/src/ccw_litellm/interfaces/embedder.py +52 -0
  169. package/ccw-litellm/src/ccw_litellm/interfaces/llm.py +45 -0
  170. package/codex-lens/src/codexlens/__pycache__/config.cpython-313.pyc +0 -0
  171. package/codex-lens/src/codexlens/cli/__pycache__/commands.cpython-313.pyc +0 -0
  172. package/codex-lens/src/codexlens/cli/__pycache__/embedding_manager.cpython-313.pyc +0 -0
  173. package/codex-lens/src/codexlens/cli/__pycache__/model_manager.cpython-313.pyc +0 -0
  174. package/codex-lens/src/codexlens/cli/__pycache__/output.cpython-313.pyc +0 -0
  175. package/codex-lens/src/codexlens/cli/commands.py +378 -23
  176. package/codex-lens/src/codexlens/cli/embedding_manager.py +660 -56
  177. package/codex-lens/src/codexlens/cli/model_manager.py +31 -18
  178. package/codex-lens/src/codexlens/cli/output.py +12 -1
  179. package/codex-lens/src/codexlens/config.py +93 -0
  180. package/codex-lens/src/codexlens/search/__pycache__/chain_search.cpython-313.pyc +0 -0
  181. package/codex-lens/src/codexlens/search/__pycache__/hybrid_search.cpython-313.pyc +0 -0
  182. package/codex-lens/src/codexlens/search/__pycache__/ranking.cpython-313.pyc +0 -0
  183. package/codex-lens/src/codexlens/search/chain_search.py +6 -2
  184. package/codex-lens/src/codexlens/search/hybrid_search.py +44 -21
  185. package/codex-lens/src/codexlens/search/ranking.py +1 -1
  186. package/codex-lens/src/codexlens/semantic/__init__.py +42 -0
  187. package/codex-lens/src/codexlens/semantic/__pycache__/__init__.cpython-313.pyc +0 -0
  188. package/codex-lens/src/codexlens/semantic/__pycache__/base.cpython-313.pyc +0 -0
  189. package/codex-lens/src/codexlens/semantic/__pycache__/chunker.cpython-313.pyc +0 -0
  190. package/codex-lens/src/codexlens/semantic/__pycache__/embedder.cpython-313.pyc +0 -0
  191. package/codex-lens/src/codexlens/semantic/__pycache__/factory.cpython-313.pyc +0 -0
  192. package/codex-lens/src/codexlens/semantic/__pycache__/gpu_support.cpython-313.pyc +0 -0
  193. package/codex-lens/src/codexlens/semantic/__pycache__/litellm_embedder.cpython-313.pyc +0 -0
  194. package/codex-lens/src/codexlens/semantic/__pycache__/vector_store.cpython-313.pyc +0 -0
  195. package/codex-lens/src/codexlens/semantic/base.py +61 -0
  196. package/codex-lens/src/codexlens/semantic/chunker.py +43 -20
  197. package/codex-lens/src/codexlens/semantic/embedder.py +60 -13
  198. package/codex-lens/src/codexlens/semantic/factory.py +98 -0
  199. package/codex-lens/src/codexlens/semantic/gpu_support.py +225 -3
  200. package/codex-lens/src/codexlens/semantic/litellm_embedder.py +144 -0
  201. package/codex-lens/src/codexlens/semantic/rotational_embedder.py +434 -0
  202. package/codex-lens/src/codexlens/semantic/vector_store.py +33 -8
  203. package/codex-lens/src/codexlens/storage/__pycache__/path_mapper.cpython-313.pyc +0 -0
  204. package/codex-lens/src/codexlens/storage/migrations/__pycache__/migration_004_dual_fts.cpython-313.pyc +0 -0
  205. package/codex-lens/src/codexlens/storage/path_mapper.py +27 -1
  206. package/package.json +15 -5
  207. package/.codex/prompts.zip +0 -0
  208. package/ccw/package.json +0 -65
@@ -271,6 +271,9 @@ function initCodexLensConfigEvents(currentConfig) {
271
271
  var searchType = document.getElementById('searchTypeSelect').value;
272
272
  var searchMode = document.getElementById('searchModeSelect').value;
273
273
  var query = document.getElementById('searchQueryInput').value.trim();
274
+ var searchLimit = document.getElementById('searchLimitInput')?.value || '5';
275
+ var contentLength = document.getElementById('contentLengthInput')?.value || '200';
276
+ var extraFiles = document.getElementById('extraFilesInput')?.value || '10';
274
277
  var resultsDiv = document.getElementById('searchResults');
275
278
  var resultCount = document.getElementById('searchResultCount');
276
279
  var resultContent = document.getElementById('searchResultContent');
@@ -286,7 +289,12 @@ function initCodexLensConfigEvents(currentConfig) {
286
289
 
287
290
  try {
288
291
  var endpoint = '/api/codexlens/' + searchType;
289
- var params = new URLSearchParams({ query: query, limit: '20' });
292
+ var params = new URLSearchParams({
293
+ query: query,
294
+ limit: searchLimit,
295
+ max_content_length: contentLength,
296
+ extra_files_count: extraFiles
297
+ });
290
298
  // Add mode parameter for search and search_files (not for symbol search)
291
299
  if (searchType === 'search' || searchType === 'search_files') {
292
300
  params.append('mode', searchMode);
@@ -337,6 +345,8 @@ function initCodexLensConfigEvents(currentConfig) {
337
345
 
338
346
  // Store detected GPU info
339
347
  var detectedGpuInfo = null;
348
+ // Store available GPU devices
349
+ var availableGpuDevices = null;
340
350
 
341
351
  /**
342
352
  * Detect GPU support
@@ -363,11 +373,13 @@ async function loadSemanticDepsStatus() {
363
373
  if (!container) return;
364
374
 
365
375
  try {
366
- // Detect GPU support in parallel
376
+ // Detect GPU support and load GPU devices in parallel
367
377
  var gpuPromise = detectGpuSupport();
378
+ var gpuDevicesPromise = loadGpuDevices();
368
379
  var response = await fetch('/api/codexlens/semantic/status');
369
380
  var result = await response.json();
370
381
  var gpuInfo = await gpuPromise;
382
+ var gpuDevices = await gpuDevicesPromise;
371
383
 
372
384
  if (result.available) {
373
385
  // Build accelerator badge
@@ -379,13 +391,16 @@ async function loadSemanticDepsStatus() {
379
391
  acceleratorIcon = 'zap';
380
392
  acceleratorClass = 'bg-green-500/20 text-green-600';
381
393
  } else if (accelerator === 'DirectML') {
382
- acceleratorIcon = 'gpu-card';
394
+ acceleratorIcon = 'cpu';
383
395
  acceleratorClass = 'bg-blue-500/20 text-blue-600';
384
396
  } else if (accelerator === 'ROCm') {
385
397
  acceleratorIcon = 'flame';
386
398
  acceleratorClass = 'bg-red-500/20 text-red-600';
387
399
  }
388
400
 
401
+ // Build GPU device selector if multiple GPUs available
402
+ var gpuDeviceSelector = buildGpuDeviceSelector(gpuDevices);
403
+
389
404
  container.innerHTML =
390
405
  '<div class="space-y-2">' +
391
406
  '<div class="flex items-center gap-2 text-sm">' +
@@ -402,6 +417,7 @@ async function loadSemanticDepsStatus() {
402
417
  ? '<span class="text-xs text-muted-foreground">' + result.providers.join(', ') + '</span>'
403
418
  : '') +
404
419
  '</div>' +
420
+ gpuDeviceSelector +
405
421
  '</div>';
406
422
  } else {
407
423
  // Build GPU mode options
@@ -442,7 +458,7 @@ function buildGpuModeSelector(gpuInfo) {
442
458
  id: 'directml',
443
459
  label: 'DirectML',
444
460
  desc: t('codexlens.directmlModeDesc') || 'Windows GPU (NVIDIA/AMD/Intel)',
445
- icon: 'gpu-card',
461
+ icon: 'cpu',
446
462
  available: gpuInfo.available.includes('directml'),
447
463
  recommended: gpuInfo.mode === 'directml'
448
464
  },
@@ -506,6 +522,134 @@ function getSelectedGpuMode() {
506
522
  return selected ? selected.value : 'cpu';
507
523
  }
508
524
 
525
+ /**
526
+ * Load available GPU devices
527
+ */
528
+ async function loadGpuDevices() {
529
+ try {
530
+ var response = await fetch('/api/codexlens/gpu/list');
531
+ var result = await response.json();
532
+ if (result.success && result.result) {
533
+ availableGpuDevices = result.result;
534
+ return result.result;
535
+ }
536
+ } catch (err) {
537
+ console.error('GPU devices load failed:', err);
538
+ }
539
+ return { devices: [], selected_device_id: null };
540
+ }
541
+
542
+ /**
543
+ * Build GPU device selector HTML
544
+ */
545
+ function buildGpuDeviceSelector(gpuDevices) {
546
+ if (!gpuDevices || !gpuDevices.devices || gpuDevices.devices.length === 0) {
547
+ return '';
548
+ }
549
+
550
+ // Only show selector if there are multiple GPUs
551
+ if (gpuDevices.devices.length < 2) {
552
+ return '';
553
+ }
554
+
555
+ var html =
556
+ '<div class="mt-3 p-3 bg-muted/30 rounded-lg border border-border">' +
557
+ '<div class="text-xs font-medium text-muted-foreground flex items-center gap-1 mb-2">' +
558
+ '<i data-lucide="cpu" class="w-3 h-3"></i>' +
559
+ (t('codexlens.selectGpuDevice') || 'Select GPU Device') +
560
+ '</div>' +
561
+ '<div class="space-y-1">';
562
+
563
+ gpuDevices.devices.forEach(function(device) {
564
+ var isSelected = device.is_selected;
565
+ var vendorIcon = device.vendor === 'nvidia' ? 'zap' : (device.vendor === 'amd' ? 'flame' : 'cpu');
566
+ var vendorColor = device.vendor === 'nvidia' ? 'text-green-500' : (device.vendor === 'amd' ? 'text-red-500' : 'text-blue-500');
567
+ var typeLabel = device.is_discrete ? (t('codexlens.discrete') || 'Discrete') : (t('codexlens.integrated') || 'Integrated');
568
+
569
+ html +=
570
+ '<label class="flex items-center gap-3 p-2 rounded border cursor-pointer hover:bg-muted/50 transition-colors ' +
571
+ (isSelected ? 'border-primary bg-primary/5' : 'border-transparent') + '">' +
572
+ '<input type="radio" name="gpuDevice" value="' + device.device_id + '" ' +
573
+ (isSelected ? 'checked' : '') +
574
+ ' class="accent-primary" onchange="selectGpuDevice(' + device.device_id + ')">' +
575
+ '<div class="flex-1">' +
576
+ '<div class="flex items-center gap-2">' +
577
+ '<i data-lucide="' + vendorIcon + '" class="w-4 h-4 ' + vendorColor + '"></i>' +
578
+ '<span class="font-medium text-sm">' + device.name + '</span>' +
579
+ '</div>' +
580
+ '<div class="flex items-center gap-2 mt-0.5">' +
581
+ '<span class="text-xs text-muted-foreground">' + device.vendor.toUpperCase() + '</span>' +
582
+ '<span class="text-xs px-1.5 py-0.5 rounded ' +
583
+ (device.is_discrete ? 'bg-green-500/20 text-green-600' : 'bg-muted text-muted-foreground') + '">' +
584
+ typeLabel +
585
+ '</span>' +
586
+ (device.is_preferred ? '<span class="text-xs bg-primary/20 text-primary px-1.5 py-0.5 rounded">' + (t('common.auto') || 'Auto') + '</span>' : '') +
587
+ '</div>' +
588
+ '</div>' +
589
+ '</label>';
590
+ });
591
+
592
+ html +=
593
+ '</div>' +
594
+ '<button class="btn-xs text-muted-foreground hover:text-foreground mt-2" onclick="resetGpuDevice()">' +
595
+ '<i data-lucide="rotate-ccw" class="w-3 h-3"></i> ' + (t('codexlens.resetToAuto') || 'Reset to Auto') +
596
+ '</button>' +
597
+ '</div>';
598
+
599
+ return html;
600
+ }
601
+
602
+ /**
603
+ * Select a GPU device
604
+ */
605
+ async function selectGpuDevice(deviceId) {
606
+ try {
607
+ showRefreshToast(t('codexlens.selectingGpu') || 'Selecting GPU...', 'info');
608
+
609
+ var response = await fetch('/api/codexlens/gpu/select', {
610
+ method: 'POST',
611
+ headers: { 'Content-Type': 'application/json' },
612
+ body: JSON.stringify({ device_id: deviceId })
613
+ });
614
+
615
+ var result = await response.json();
616
+ if (result.success) {
617
+ showRefreshToast(t('codexlens.gpuSelected') || 'GPU selected', 'success');
618
+ // Reload semantic status to reflect change
619
+ loadSemanticDepsStatus();
620
+ } else {
621
+ showRefreshToast(result.error || 'Failed to select GPU', 'error');
622
+ }
623
+ } catch (err) {
624
+ showRefreshToast(err.message, 'error');
625
+ }
626
+ }
627
+
628
+ /**
629
+ * Reset GPU device selection to auto
630
+ */
631
+ async function resetGpuDevice() {
632
+ try {
633
+ showRefreshToast(t('codexlens.resettingGpu') || 'Resetting GPU selection...', 'info');
634
+
635
+ var response = await fetch('/api/codexlens/gpu/reset', {
636
+ method: 'POST',
637
+ headers: { 'Content-Type': 'application/json' }
638
+ });
639
+
640
+ var result = await response.json();
641
+ if (result.success) {
642
+ showRefreshToast(t('codexlens.gpuReset') || 'GPU selection reset to auto', 'success');
643
+ // Reload semantic status to reflect change
644
+ loadSemanticDepsStatus();
645
+ } else {
646
+ showRefreshToast(result.error || 'Failed to reset GPU', 'error');
647
+ }
648
+ } catch (err) {
649
+ showRefreshToast(err.message, 'error');
650
+ }
651
+ }
652
+
509
653
  /**
510
654
  * Install semantic dependencies with GPU mode
511
655
  */
@@ -570,9 +714,7 @@ async function installSemanticDeps() {
570
714
  function buildManualDownloadGuide() {
571
715
  var modelData = [
572
716
  { profile: 'code', name: 'jinaai/jina-embeddings-v2-base-code', size: '~150 MB' },
573
- { profile: 'fast', name: 'BAAI/bge-small-en-v1.5', size: '~80 MB' },
574
- { profile: 'balanced', name: 'mixedbread-ai/mxbai-embed-large-v1', size: '~600 MB' },
575
- { profile: 'multilingual', name: 'intfloat/multilingual-e5-large', size: '~1 GB' }
717
+ { profile: 'fast', name: 'BAAI/bge-small-en-v1.5', size: '~80 MB' }
576
718
  ];
577
719
 
578
720
  var html =
@@ -676,8 +818,8 @@ function buildManualDownloadGuide() {
676
818
  '<i data-lucide="info" class="w-3.5 h-3.5 mt-0.5 flex-shrink-0"></i>' +
677
819
  '<div>' +
678
820
  '<strong>' + (t('codexlens.cacheLocation') || 'Cache Location') + ':</strong><br>' +
679
- '<code class="text-xs">Windows: %LOCALAPPDATA%\\Temp\\fastembed_cache</code><br>' +
680
- '<code class="text-xs">Linux/Mac: ~/.cache/fastembed</code>' +
821
+ '<code class="text-xs">Default: ~/.cache/huggingface</code><br>' +
822
+ '<code class="text-xs text-muted-foreground">(Check HF_HOME env var if set)</code>' +
681
823
  '</div>' +
682
824
  '</div>' +
683
825
  '</div>' +
@@ -807,9 +949,7 @@ async function downloadModel(profile) {
807
949
  // Get model info for size estimation
808
950
  var modelSizes = {
809
951
  'fast': { size: 80, time: '1-2' },
810
- 'code': { size: 150, time: '2-5' },
811
- 'multilingual': { size: 1000, time: '5-15' },
812
- 'balanced': { size: 600, time: '3-10' }
952
+ 'code': { size: 150, time: '2-5' }
813
953
  };
814
954
 
815
955
  var modelInfo = modelSizes[profile] || { size: 100, time: '2-5' };
@@ -933,9 +1073,7 @@ async function downloadModel(profile) {
933
1073
  function showModelDownloadError(modelCard, profile, error, originalHTML) {
934
1074
  var modelNames = {
935
1075
  'fast': 'BAAI/bge-small-en-v1.5',
936
- 'code': 'jinaai/jina-embeddings-v2-base-code',
937
- 'multilingual': 'intfloat/multilingual-e5-large',
938
- 'balanced': 'mixedbread-ai/mxbai-embed-large-v1'
1076
+ 'code': 'jinaai/jina-embeddings-v2-base-code'
939
1077
  };
940
1078
 
941
1079
  var modelName = modelNames[profile] || profile;
@@ -1035,14 +1173,19 @@ async function deleteModel(profile) {
1035
1173
  /**
1036
1174
  * Initialize CodexLens index with bottom floating progress bar
1037
1175
  * @param {string} indexType - 'vector' (with embeddings), 'normal' (FTS only), or 'full' (FTS + Vector)
1038
- * @param {string} embeddingModel - Model profile: 'code', 'fast', 'multilingual', 'balanced'
1176
+ * @param {string} embeddingModel - Model profile: 'code', 'fast'
1177
+ * @param {string} embeddingBackend - Backend: 'fastembed' (local) or 'litellm' (API)
1178
+ * @param {number} maxWorkers - Max concurrent API calls for embedding generation (default: 1)
1039
1179
  */
1040
- async function initCodexLensIndex(indexType, embeddingModel) {
1180
+ async function initCodexLensIndex(indexType, embeddingModel, embeddingBackend, maxWorkers) {
1041
1181
  indexType = indexType || 'vector';
1042
1182
  embeddingModel = embeddingModel || 'code';
1183
+ embeddingBackend = embeddingBackend || 'fastembed';
1184
+ maxWorkers = maxWorkers || 1;
1043
1185
 
1044
- // For vector or full index, check if semantic dependencies are available
1045
- if (indexType === 'vector' || indexType === 'full') {
1186
+ // For vector/full index with local backend, check if semantic dependencies are available
1187
+ // LiteLLM backend uses remote embeddings and does not require fastembed/ONNX deps.
1188
+ if ((indexType === 'vector' || indexType === 'full') && embeddingBackend !== 'litellm') {
1046
1189
  try {
1047
1190
  var semanticResponse = await fetch('/api/codexlens/semantic/status');
1048
1191
  var semanticStatus = await semanticResponse.json();
@@ -1104,8 +1247,9 @@ async function initCodexLensIndex(indexType, embeddingModel) {
1104
1247
  // Add model info for vector indexes
1105
1248
  var modelLabel = '';
1106
1249
  if (indexType !== 'normal') {
1107
- var modelNames = { code: 'Code', fast: 'Fast', multilingual: 'Multi', balanced: 'Balanced' };
1108
- modelLabel = ' [' + (modelNames[embeddingModel] || embeddingModel) + ']';
1250
+ var modelNames = { code: 'Code', fast: 'Fast' };
1251
+ var backendLabel = embeddingBackend === 'litellm' ? 'API: ' : '';
1252
+ modelLabel = ' [' + backendLabel + (modelNames[embeddingModel] || embeddingModel) + ']';
1109
1253
  }
1110
1254
 
1111
1255
  progressBar.innerHTML =
@@ -1142,17 +1286,21 @@ async function initCodexLensIndex(indexType, embeddingModel) {
1142
1286
  var apiIndexType = (indexType === 'full') ? 'vector' : indexType;
1143
1287
 
1144
1288
  // Start indexing with specified type and model
1145
- startCodexLensIndexing(apiIndexType, embeddingModel);
1289
+ startCodexLensIndexing(apiIndexType, embeddingModel, embeddingBackend, maxWorkers);
1146
1290
  }
1147
1291
 
1148
1292
  /**
1149
1293
  * Start the indexing process
1150
1294
  * @param {string} indexType - 'vector' or 'normal'
1151
- * @param {string} embeddingModel - Model profile: 'code', 'fast', 'multilingual', 'balanced'
1295
+ * @param {string} embeddingModel - Model profile: 'code', 'fast'
1296
+ * @param {string} embeddingBackend - Backend: 'fastembed' (local) or 'litellm' (API)
1297
+ * @param {number} maxWorkers - Max concurrent API calls for embedding generation (default: 1)
1152
1298
  */
1153
- async function startCodexLensIndexing(indexType, embeddingModel) {
1299
+ async function startCodexLensIndexing(indexType, embeddingModel, embeddingBackend, maxWorkers) {
1154
1300
  indexType = indexType || 'vector';
1155
1301
  embeddingModel = embeddingModel || 'code';
1302
+ embeddingBackend = embeddingBackend || 'fastembed';
1303
+ maxWorkers = maxWorkers || 1;
1156
1304
  var statusText = document.getElementById('codexlensIndexStatus');
1157
1305
  var progressBar = document.getElementById('codexlensIndexProgressBar');
1158
1306
  var percentText = document.getElementById('codexlensIndexPercent');
@@ -1184,11 +1332,11 @@ async function startCodexLensIndexing(indexType, embeddingModel) {
1184
1332
  }
1185
1333
 
1186
1334
  try {
1187
- console.log('[CodexLens] Starting index for:', projectPath, 'type:', indexType, 'model:', embeddingModel);
1335
+ console.log('[CodexLens] Starting index for:', projectPath, 'type:', indexType, 'model:', embeddingModel, 'backend:', embeddingBackend, 'maxWorkers:', maxWorkers);
1188
1336
  var response = await fetch('/api/codexlens/init', {
1189
1337
  method: 'POST',
1190
1338
  headers: { 'Content-Type': 'application/json' },
1191
- body: JSON.stringify({ path: projectPath, indexType: indexType, embeddingModel: embeddingModel })
1339
+ body: JSON.stringify({ path: projectPath, indexType: indexType, embeddingModel: embeddingModel, embeddingBackend: embeddingBackend, maxWorkers: maxWorkers })
1192
1340
  });
1193
1341
 
1194
1342
  var result = await response.json();
@@ -1196,7 +1344,15 @@ async function startCodexLensIndexing(indexType, embeddingModel) {
1196
1344
 
1197
1345
  // Check if completed successfully (WebSocket might have already reported)
1198
1346
  if (result.success) {
1199
- handleIndexComplete(true, t('codexlens.indexComplete'));
1347
+ // For vector index, check if embeddings were actually generated
1348
+ var embeddingsResult = result.result && result.result.embeddings;
1349
+ if (indexType === 'vector' && embeddingsResult && !embeddingsResult.generated) {
1350
+ // FTS succeeded but embeddings failed - show partial success
1351
+ var errorMsg = embeddingsResult.error || t('codexlens.embeddingsFailed');
1352
+ handleIndexComplete(false, t('codexlens.ftsSuccessEmbeddingsFailed') || 'FTS index created, but embeddings failed: ' + errorMsg);
1353
+ } else {
1354
+ handleIndexComplete(true, t('codexlens.indexComplete'));
1355
+ }
1200
1356
  } else if (!result.success) {
1201
1357
  handleIndexComplete(false, result.error || t('common.unknownError'));
1202
1358
  }
@@ -1727,3 +1883,1085 @@ async function cleanCodexLensIndexes() {
1727
1883
  showRefreshToast(t('common.error') + ': ' + err.message, 'error');
1728
1884
  }
1729
1885
  }
1886
+
1887
+ // ============================================================
1888
+ // CODEXLENS MANAGER PAGE (Independent View)
1889
+ // ============================================================
1890
+
1891
+ /**
1892
+ * Render CodexLens Manager as an independent page view
1893
+ */
1894
+ async function renderCodexLensManager() {
1895
+ var container = document.getElementById('mainContent');
1896
+ if (!container) return;
1897
+
1898
+ // Hide stats grid and search
1899
+ var statsGrid = document.getElementById('statsGrid');
1900
+ var searchContainer = document.querySelector('.search-container');
1901
+ if (statsGrid) statsGrid.style.display = 'none';
1902
+ if (searchContainer) searchContainer.style.display = 'none';
1903
+
1904
+ container.innerHTML = '<div class="flex items-center justify-center py-12"><div class="animate-spin w-6 h-6 border-2 border-primary border-t-transparent rounded-full"></div><span class="ml-3">' + t('common.loading') + '</span></div>';
1905
+
1906
+ try {
1907
+ // Use aggregated endpoint for faster page load (single API call)
1908
+ var dashboardData = null;
1909
+ var config = { index_dir: '~/.codexlens/indexes', index_count: 0 };
1910
+
1911
+ if (typeof loadCodexLensDashboardInit === 'function') {
1912
+ console.log('[CodexLens] Using aggregated dashboard-init endpoint...');
1913
+ dashboardData = await loadCodexLensDashboardInit();
1914
+ if (dashboardData && dashboardData.config) {
1915
+ config = dashboardData.config;
1916
+ console.log('[CodexLens] Dashboard init loaded, config:', config);
1917
+ }
1918
+ } else if (typeof loadCodexLensStatus === 'function') {
1919
+ // Fallback to legacy individual calls
1920
+ console.log('[CodexLens] Fallback to legacy loadCodexLensStatus...');
1921
+ await loadCodexLensStatus();
1922
+ var response = await fetch('/api/codexlens/config');
1923
+ config = await response.json();
1924
+ }
1925
+
1926
+ // Load LiteLLM API config for embedding backend options (parallel with page render)
1927
+ var litellmPromise = (async () => {
1928
+ try {
1929
+ console.log('[CodexLens] Loading LiteLLM config...');
1930
+ var litellmResponse = await fetch('/api/litellm-api/config');
1931
+ if (litellmResponse.ok) {
1932
+ window.litellmApiConfig = await litellmResponse.json();
1933
+ console.log('[CodexLens] LiteLLM config loaded, providers:', window.litellmApiConfig?.providers?.length || 0);
1934
+ }
1935
+ } catch (e) {
1936
+ console.warn('[CodexLens] Could not load LiteLLM config:', e);
1937
+ }
1938
+ })();
1939
+
1940
+ container.innerHTML = buildCodexLensManagerPage(config);
1941
+ if (window.lucide) lucide.createIcons();
1942
+ initCodexLensManagerPageEvents(config);
1943
+
1944
+ // Load additional data in parallel (non-blocking)
1945
+ var isInstalled = window.cliToolsStatus?.codexlens?.installed || dashboardData?.installed;
1946
+
1947
+ // Wait for LiteLLM config before loading semantic deps (it may need provider info)
1948
+ await litellmPromise;
1949
+
1950
+ // Always load semantic deps status - it needs GPU detection and device list
1951
+ // which are not included in the aggregated endpoint
1952
+ loadSemanticDepsStatus();
1953
+
1954
+ loadModelList();
1955
+
1956
+ // Load index stats for the Index Manager section
1957
+ if (isInstalled) {
1958
+ loadIndexStatsForPage();
1959
+ }
1960
+ } catch (err) {
1961
+ container.innerHTML = '<div class="text-center py-12 text-destructive"><i data-lucide="alert-circle" class="w-8 h-8 mx-auto mb-2"></i><p>' + t('common.error') + ': ' + err.message + '</p></div>';
1962
+ if (window.lucide) lucide.createIcons();
1963
+ }
1964
+ }
1965
+
1966
+ /**
1967
+ * Build CodexLens Manager page content
1968
+ */
1969
+ function buildCodexLensManagerPage(config) {
1970
+ var indexDir = config.index_dir || '~/.codexlens/indexes';
1971
+ var indexCount = config.index_count || 0;
1972
+ var isInstalled = window.cliToolsStatus?.codexlens?.installed || false;
1973
+
1974
+ // Build model options for vector indexing
1975
+ var modelOptions = buildModelSelectOptionsForPage();
1976
+
1977
+ return '<div class="codexlens-manager-page space-y-6">' +
1978
+ // Header with status
1979
+ '<div class="bg-card border border-border rounded-lg p-6">' +
1980
+ '<div class="flex items-center justify-between flex-wrap gap-4">' +
1981
+ '<div class="flex items-center gap-4">' +
1982
+ '<div class="w-12 h-12 rounded-full bg-primary/10 flex items-center justify-center">' +
1983
+ '<i data-lucide="database" class="w-6 h-6 text-primary"></i>' +
1984
+ '</div>' +
1985
+ '<div>' +
1986
+ '<h2 class="text-xl font-bold">' + t('codexlens.config') + '</h2>' +
1987
+ '<p class="text-sm text-muted-foreground">' + t('codexlens.configDesc') + '</p>' +
1988
+ '</div>' +
1989
+ '</div>' +
1990
+ '<div class="flex items-center gap-4">' +
1991
+ (isInstalled
1992
+ ? '<span class="inline-flex items-center gap-1.5 px-3 py-1.5 rounded-full text-sm font-medium bg-success/10 text-success border border-success/20"><i data-lucide="check-circle" class="w-4 h-4"></i> ' + t('codexlens.installed') + '</span>'
1993
+ : '<span class="inline-flex items-center gap-1.5 px-3 py-1.5 rounded-full text-sm font-medium bg-muted text-muted-foreground border border-border"><i data-lucide="circle" class="w-4 h-4"></i> ' + t('codexlens.notInstalled') + '</span>') +
1994
+ '<div class="flex items-center gap-2 px-3 py-1.5 rounded-lg bg-primary/5 border border-primary/20">' +
1995
+ '<span class="text-sm text-muted-foreground">' + t('codexlens.indexes') + ':</span>' +
1996
+ '<span class="text-lg font-bold text-primary">' + indexCount + '</span>' +
1997
+ '</div>' +
1998
+ '</div>' +
1999
+ '</div>' +
2000
+ '</div>' +
2001
+
2002
+ (isInstalled
2003
+ ? // Installed: Show full management UI
2004
+ '<div class="grid grid-cols-1 lg:grid-cols-2 gap-6">' +
2005
+ // Left Column
2006
+ '<div class="space-y-6">' +
2007
+ // Create Index Section
2008
+ '<div class="bg-card border border-border rounded-lg p-5">' +
2009
+ '<h4 class="text-lg font-semibold mb-4 flex items-center gap-2"><i data-lucide="layers" class="w-5 h-5 text-primary"></i> ' + t('codexlens.createIndex') + '</h4>' +
2010
+ '<div class="space-y-4">' +
2011
+ // Backend selector (fastembed local or litellm API)
2012
+ '<div class="mb-4">' +
2013
+ '<label class="block text-sm font-medium mb-1.5">' + (t('codexlens.embeddingBackend') || 'Embedding Backend') + '</label>' +
2014
+ '<select id="pageBackendSelect" class="w-full px-3 py-2 border border-border rounded-lg bg-background text-sm" onchange="onEmbeddingBackendChange()">' +
2015
+ '<option value="fastembed">' + (t('codexlens.localFastembed') || 'Local (FastEmbed)') + '</option>' +
2016
+ '<option value="litellm">' + (t('codexlens.apiLitellm') || 'API (LiteLLM)') + '</option>' +
2017
+ '</select>' +
2018
+ '<p class="text-xs text-muted-foreground mt-1">' + (t('codexlens.backendHint') || 'Select local model or remote API endpoint') + '</p>' +
2019
+ '</div>' +
2020
+ // Model selector
2021
+ '<div>' +
2022
+ '<label class="block text-sm font-medium mb-1.5">' + t('codexlens.embeddingModel') + '</label>' +
2023
+ '<select id="pageModelSelect" class="w-full px-3 py-2 border border-border rounded-lg bg-background text-sm">' +
2024
+ modelOptions +
2025
+ '</select>' +
2026
+ '<p class="text-xs text-muted-foreground mt-1">' + t('codexlens.modelHint') + '</p>' +
2027
+ '</div>' +
2028
+ // Concurrency selector (only for LiteLLM backend)
2029
+ '<div id="concurrencySelector" class="hidden">' +
2030
+ '<label class="block text-sm font-medium mb-1.5">' + t('codexlens.concurrency') + '</label>' +
2031
+ '<div class="flex items-center gap-2">' +
2032
+ '<input type="number" id="pageConcurrencyInput" min="1" value="4" ' +
2033
+ 'class="w-24 px-3 py-2 border border-border rounded-lg bg-background text-sm" ' +
2034
+ 'onchange="validateConcurrencyInput(this)" />' +
2035
+ '<span class="text-sm text-muted-foreground">workers</span>' +
2036
+ '<span class="text-xs text-primary ml-2">(4 = recommended)</span>' +
2037
+ '</div>' +
2038
+ '<p class="text-xs text-muted-foreground mt-1">' + t('codexlens.concurrencyHint') + '</p>' +
2039
+ '</div>' +
2040
+ // Multi-Provider Rotation (only for LiteLLM backend) - Simplified, config in API Settings
2041
+ '<div id="rotationSection" class="hidden">' +
2042
+ '<div class="border border-border rounded-lg p-3 bg-muted/30">' +
2043
+ '<div class="flex items-center justify-between mb-2">' +
2044
+ '<div class="flex items-center gap-2">' +
2045
+ '<i data-lucide="rotate-cw" class="w-4 h-4 text-primary"></i>' +
2046
+ '<span class="text-sm font-medium">' + t('codexlens.rotation') + '</span>' +
2047
+ '</div>' +
2048
+ '<div id="rotationStatusBadge" class="text-xs px-2 py-0.5 rounded-full bg-muted text-muted-foreground">' +
2049
+ t('common.disabled') +
2050
+ '</div>' +
2051
+ '</div>' +
2052
+ '<p class="text-xs text-muted-foreground mb-2">' + t('codexlens.rotationDesc') + '</p>' +
2053
+ '<div id="rotationDetails" class="text-xs text-muted-foreground mb-3 hidden">' +
2054
+ '<span id="rotationModelName"></span> · <span id="rotationEndpointCount"></span>' +
2055
+ '</div>' +
2056
+ '<div class="flex items-center gap-2">' +
2057
+ '<a href="#" class="btn-sm btn-outline flex items-center gap-1.5" onclick="navigateToApiSettingsEmbeddingPool(); return false;">' +
2058
+ '<i data-lucide="external-link" class="w-3.5 h-3.5"></i>' +
2059
+ t('codexlens.configureInApiSettings') +
2060
+ '</a>' +
2061
+ '</div>' +
2062
+ '</div>' +
2063
+ '</div>' +
2064
+ // Index buttons - two modes: full (FTS + Vector) or FTS only
2065
+ '<div class="grid grid-cols-2 gap-3">' +
2066
+ '<button class="btn btn-primary flex items-center justify-center gap-2 py-3" onclick="initCodexLensIndexFromPage(\'full\')" title="' + t('codexlens.fullIndexDesc') + '">' +
2067
+ '<i data-lucide="layers" class="w-4 h-4"></i>' +
2068
+ '<span>' + t('codexlens.fullIndex') + '</span>' +
2069
+ '</button>' +
2070
+ '<button class="btn btn-outline flex items-center justify-center gap-2 py-3" onclick="initCodexLensIndexFromPage(\'normal\')" title="' + t('codexlens.ftsIndexDesc') + '">' +
2071
+ '<i data-lucide="file-text" class="w-4 h-4"></i>' +
2072
+ '<span>' + t('codexlens.ftsIndex') + '</span>' +
2073
+ '</button>' +
2074
+ '</div>' +
2075
+ '<p class="text-xs text-muted-foreground">' + t('codexlens.indexTypeHint') + '</p>' +
2076
+ '</div>' +
2077
+ '</div>' +
2078
+ // Storage Path Section
2079
+ '<div class="bg-card border border-border rounded-lg p-5">' +
2080
+ '<h4 class="text-lg font-semibold mb-4 flex items-center gap-2"><i data-lucide="folder" class="w-5 h-5 text-primary"></i> ' + t('codexlens.indexStoragePath') + '</h4>' +
2081
+ '<div class="space-y-3">' +
2082
+ '<div>' +
2083
+ '<label class="block text-sm font-medium mb-1.5">' + t('codexlens.currentPath') + '</label>' +
2084
+ '<div class="text-sm text-muted-foreground bg-muted/50 rounded-lg px-3 py-2 font-mono border border-border truncate" title="' + indexDir + '">' + indexDir + '</div>' +
2085
+ '</div>' +
2086
+ '<div>' +
2087
+ '<label class="block text-sm font-medium mb-1.5">' + t('codexlens.newStoragePath') + '</label>' +
2088
+ '<div class="flex gap-2">' +
2089
+ '<input type="text" id="indexDirInput" value="' + indexDir + '" class="flex-1 px-3 py-2 border border-border rounded-lg bg-background text-foreground text-sm" />' +
2090
+ '<button class="btn-sm btn-primary" id="saveIndexPathBtn"><i data-lucide="save" class="w-3.5 h-3.5"></i></button>' +
2091
+ '</div>' +
2092
+ '<p class="text-xs text-muted-foreground mt-1">' + t('codexlens.pathInfo') + '</p>' +
2093
+ '</div>' +
2094
+ '</div>' +
2095
+ '</div>' +
2096
+ // Maintenance Section
2097
+ '<div class="bg-card border border-border rounded-lg p-5">' +
2098
+ '<h4 class="text-lg font-semibold mb-4 flex items-center gap-2"><i data-lucide="settings" class="w-5 h-5 text-primary"></i> ' + t('codexlens.maintenance') + '</h4>' +
2099
+ '<div class="flex flex-wrap gap-2">' +
2100
+ '<button class="btn-sm btn-outline" onclick="cleanCurrentWorkspaceIndex()"><i data-lucide="folder-x" class="w-3.5 h-3.5"></i> ' + t('codexlens.cleanCurrentWorkspace') + '</button>' +
2101
+ '<button class="btn-sm btn-outline" onclick="cleanCodexLensIndexes()"><i data-lucide="trash" class="w-3.5 h-3.5"></i> ' + t('codexlens.cleanAllIndexes') + '</button>' +
2102
+ '<button class="btn-sm btn-destructive" onclick="uninstallCodexLensFromManager()"><i data-lucide="trash-2" class="w-3.5 h-3.5"></i> ' + t('cli.uninstall') + '</button>' +
2103
+ '</div>' +
2104
+ '</div>' +
2105
+ '</div>' +
2106
+ // Right Column
2107
+ '<div class="space-y-6">' +
2108
+ // Semantic Dependencies
2109
+ '<div class="bg-card border border-border rounded-lg p-5">' +
2110
+ '<h4 class="text-lg font-semibold mb-4 flex items-center gap-2"><i data-lucide="cpu" class="w-5 h-5 text-primary"></i> ' + t('codexlens.semanticDeps') + '</h4>' +
2111
+ '<div id="semanticDepsStatus" class="space-y-3">' +
2112
+ '<div class="flex items-center gap-2 text-sm text-muted-foreground">' +
2113
+ '<div class="animate-spin w-4 h-4 border-2 border-primary border-t-transparent rounded-full"></div> ' + t('codexlens.checkingDeps') +
2114
+ '</div>' +
2115
+ '</div>' +
2116
+ '</div>' +
2117
+ // Model Management
2118
+ '<div class="bg-card border border-border rounded-lg p-5">' +
2119
+ '<h4 class="text-lg font-semibold mb-4 flex items-center gap-2"><i data-lucide="box" class="w-5 h-5 text-primary"></i> ' + t('codexlens.modelManagement') + '</h4>' +
2120
+ '<div id="modelListContainer" class="space-y-3">' +
2121
+ '<div class="flex items-center gap-2 text-sm text-muted-foreground">' +
2122
+ '<div class="animate-spin w-4 h-4 border-2 border-primary border-t-transparent rounded-full"></div> ' + t('codexlens.loadingModels') +
2123
+ '</div>' +
2124
+ '</div>' +
2125
+ '</div>' +
2126
+ '</div>' +
2127
+ '</div>' +
2128
+ // Index Manager Section
2129
+ '<div class="bg-card border border-border rounded-lg overflow-hidden" id="indexManagerSection">' +
2130
+ '<div class="bg-muted/30 border-b border-border px-4 py-3 flex items-center justify-between">' +
2131
+ '<div class="flex items-center gap-2">' +
2132
+ '<i data-lucide="database" class="w-4 h-4 text-primary"></i>' +
2133
+ '<span class="font-medium text-foreground">' + t('index.manager') + '</span>' +
2134
+ '<span class="text-xs px-2 py-0.5 bg-muted rounded-full text-muted-foreground" id="indexTotalSize">-</span>' +
2135
+ '</div>' +
2136
+ '<div class="flex items-center gap-2">' +
2137
+ '<button onclick="loadIndexStatsForPage()" class="text-xs px-2 py-1 text-muted-foreground hover:text-foreground hover:bg-muted rounded transition-colors" title="' + t('common.refresh') + '">' +
2138
+ '<i data-lucide="refresh-cw" class="w-3.5 h-3.5"></i>' +
2139
+ '</button>' +
2140
+ '</div>' +
2141
+ '</div>' +
2142
+ '<div class="p-4">' +
2143
+ '<div class="flex items-center gap-2 mb-3 text-xs text-muted-foreground">' +
2144
+ '<i data-lucide="folder" class="w-3.5 h-3.5"></i>' +
2145
+ '<span class="font-mono truncate" id="indexDirDisplay" title="' + indexDir + '">' + indexDir + '</span>' +
2146
+ '</div>' +
2147
+ '<div class="grid grid-cols-4 gap-3 mb-4">' +
2148
+ '<div class="bg-muted/30 rounded-lg p-3 text-center">' +
2149
+ '<div class="text-lg font-semibold text-foreground" id="indexProjectCount">-</div>' +
2150
+ '<div class="text-xs text-muted-foreground">' + t('index.projects') + '</div>' +
2151
+ '</div>' +
2152
+ '<div class="bg-muted/30 rounded-lg p-3 text-center">' +
2153
+ '<div class="text-lg font-semibold text-foreground" id="indexTotalSizeVal">-</div>' +
2154
+ '<div class="text-xs text-muted-foreground">' + t('index.totalSize') + '</div>' +
2155
+ '</div>' +
2156
+ '<div class="bg-muted/30 rounded-lg p-3 text-center">' +
2157
+ '<div class="text-lg font-semibold text-foreground" id="indexVectorCount">-</div>' +
2158
+ '<div class="text-xs text-muted-foreground">' + t('index.vectorIndexes') + '</div>' +
2159
+ '</div>' +
2160
+ '<div class="bg-muted/30 rounded-lg p-3 text-center">' +
2161
+ '<div class="text-lg font-semibold text-foreground" id="indexFtsCount">-</div>' +
2162
+ '<div class="text-xs text-muted-foreground">' + t('index.ftsIndexes') + '</div>' +
2163
+ '</div>' +
2164
+ '</div>' +
2165
+ '<div class="border border-border rounded-lg overflow-hidden">' +
2166
+ '<table class="w-full text-sm">' +
2167
+ '<thead class="bg-muted/50">' +
2168
+ '<tr class="text-xs text-muted-foreground">' +
2169
+ '<th class="py-2 px-2 text-left font-medium">' + t('index.projectId') + '</th>' +
2170
+ '<th class="py-2 px-2 text-right font-medium">' + t('index.size') + '</th>' +
2171
+ '<th class="py-2 px-2 text-center font-medium">' + t('index.type') + '</th>' +
2172
+ '<th class="py-2 px-2 text-right font-medium">' + t('index.lastModified') + '</th>' +
2173
+ '<th class="py-2 px-1 w-8"></th>' +
2174
+ '</tr>' +
2175
+ '</thead>' +
2176
+ '<tbody id="indexTableBody">' +
2177
+ '<tr><td colspan="5" class="py-4 text-center text-muted-foreground text-sm">' + t('common.loading') + '</td></tr>' +
2178
+ '</tbody>' +
2179
+ '</table>' +
2180
+ '</div>' +
2181
+ '<div class="mt-4 flex justify-end">' +
2182
+ '<button onclick="cleanAllIndexesFromPage()" class="text-xs px-3 py-1.5 bg-destructive/10 text-destructive hover:bg-destructive/20 rounded transition-colors flex items-center gap-1.5">' +
2183
+ '<i data-lucide="trash" class="w-3.5 h-3.5"></i>' +
2184
+ t('index.cleanAll') +
2185
+ '</button>' +
2186
+ '</div>' +
2187
+ '</div>' +
2188
+ '</div>' +
2189
+ // Test Search Section
2190
+ '<div class="bg-card border border-border rounded-lg p-5">' +
2191
+ '<h4 class="text-lg font-semibold mb-4 flex items-center gap-2"><i data-lucide="search" class="w-5 h-5 text-primary"></i> ' + t('codexlens.testSearch') + '</h4>' +
2192
+ '<div class="space-y-4">' +
2193
+ '<div class="flex gap-3">' +
2194
+ '<select id="searchTypeSelect" class="flex-1 px-3 py-2 border border-border rounded-lg bg-background text-sm">' +
2195
+ '<option value="search">' + t('codexlens.textSearch') + '</option>' +
2196
+ '<option value="search_files">' + t('codexlens.fileSearch') + '</option>' +
2197
+ '<option value="symbol">' + t('codexlens.symbolSearch') + '</option>' +
2198
+ '</select>' +
2199
+ '<select id="searchModeSelect" class="flex-1 px-3 py-2 border border-border rounded-lg bg-background text-sm">' +
2200
+ '<option value="exact">' + t('codexlens.exactMode') + '</option>' +
2201
+ '<option value="fuzzy">' + t('codexlens.fuzzyMode') + '</option>' +
2202
+ '<option value="hybrid">' + t('codexlens.hybridMode') + '</option>' +
2203
+ '<option value="vector">' + t('codexlens.vectorMode') + '</option>' +
2204
+ '</select>' +
2205
+ '</div>' +
2206
+ '<div class="flex gap-3 items-center">' +
2207
+ '<div class="flex items-center gap-2">' +
2208
+ '<label class="text-xs text-muted-foreground whitespace-nowrap">' + t('codexlens.resultLimit') + '</label>' +
2209
+ '<input type="number" id="searchLimitInput" class="w-16 px-2 py-1.5 border border-border rounded-lg bg-background text-sm text-center" value="5" min="1" max="50" />' +
2210
+ '</div>' +
2211
+ '<div class="flex items-center gap-2">' +
2212
+ '<label class="text-xs text-muted-foreground whitespace-nowrap">' + t('codexlens.contentLength') + '</label>' +
2213
+ '<input type="number" id="contentLengthInput" class="w-20 px-2 py-1.5 border border-border rounded-lg bg-background text-sm text-center" value="200" min="50" max="2000" />' +
2214
+ '</div>' +
2215
+ '<div class="flex items-center gap-2">' +
2216
+ '<label class="text-xs text-muted-foreground whitespace-nowrap">' + t('codexlens.extraFiles') + '</label>' +
2217
+ '<input type="number" id="extraFilesInput" class="w-16 px-2 py-1.5 border border-border rounded-lg bg-background text-sm text-center" value="10" min="0" max="50" />' +
2218
+ '</div>' +
2219
+ '</div>' +
2220
+ '<div class="flex gap-3">' +
2221
+ '<input type="text" id="searchQueryInput" class="flex-1 px-3 py-2 border border-border rounded-lg bg-background text-sm" placeholder="' + t('codexlens.searchPlaceholder') + '" />' +
2222
+ '<button class="btn-sm btn-primary" id="runSearchBtn"><i data-lucide="search" class="w-3.5 h-3.5"></i> ' + t('codexlens.runSearch') + '</button>' +
2223
+ '</div>' +
2224
+ '<div id="searchResults" class="hidden">' +
2225
+ '<div class="flex items-center justify-between mb-2">' +
2226
+ '<span class="text-sm font-medium">' + t('codexlens.results') + ':</span>' +
2227
+ '<span id="searchResultCount" class="text-xs text-muted-foreground"></span>' +
2228
+ '</div>' +
2229
+ '<pre id="searchResultContent" class="bg-muted/50 border border-border p-3 rounded-lg text-xs overflow-auto max-h-64 font-mono"></pre>' +
2230
+ '</div>' +
2231
+ '</div>' +
2232
+ '</div>'
2233
+
2234
+ : // Not installed: Show install prompt
2235
+ '<div class="bg-card border border-border rounded-lg p-8">' +
2236
+ '<div class="text-center max-w-md mx-auto">' +
2237
+ '<div class="w-16 h-16 rounded-full bg-primary/10 flex items-center justify-center mx-auto mb-4">' +
2238
+ '<i data-lucide="database" class="w-8 h-8 text-primary"></i>' +
2239
+ '</div>' +
2240
+ '<h3 class="text-lg font-semibold mb-2">' + t('codexlens.installCodexLens') + '</h3>' +
2241
+ '<p class="text-sm text-muted-foreground mb-6">' + t('codexlens.installFirst') + '</p>' +
2242
+ '<button class="btn btn-primary" onclick="installCodexLensFromManager()">' +
2243
+ '<i data-lucide="download" class="w-4 h-4"></i> ' + t('codexlens.installCodexLens') +
2244
+ '</button>' +
2245
+ '</div>' +
2246
+ '</div>'
2247
+ ) +
2248
+ '</div>';
2249
+ }
2250
+
2251
+ /**
2252
+ * Build model select options for the page
2253
+ */
2254
+ function buildModelSelectOptionsForPage() {
2255
+ var installedModels = window.cliToolsStatus?.codexlens?.installedModels || [];
2256
+ var allModels = window.cliToolsStatus?.codexlens?.allModels || [];
2257
+
2258
+ if (allModels.length === 0) {
2259
+ // Fallback to default models if not loaded
2260
+ return '<option value="code">code (default)</option>' +
2261
+ '<option value="fast">fast</option>';
2262
+ }
2263
+
2264
+ var options = '';
2265
+ allModels.forEach(function(model) {
2266
+ var isInstalled = model.installed || installedModels.includes(model.profile);
2267
+ var label = model.profile + (isInstalled ? ' ✓' : '');
2268
+ var selected = model.profile === 'code' ? ' selected' : '';
2269
+ options += '<option value="' + model.profile + '"' + selected + '>' + label + '</option>';
2270
+ });
2271
+ return options;
2272
+ }
2273
+
2274
+ /**
2275
+ * Validate concurrency input value (min 1, no max limit)
2276
+ */
2277
+ function validateConcurrencyInput(input) {
2278
+ var value = parseInt(input.value, 10);
2279
+ if (isNaN(value) || value < 1) {
2280
+ input.value = 1;
2281
+ }
2282
+ }
2283
+
2284
+ /**
2285
+ * Handle embedding backend change
2286
+ */
2287
+ function onEmbeddingBackendChange() {
2288
+ var backendSelect = document.getElementById('pageBackendSelect');
2289
+ var modelSelect = document.getElementById('pageModelSelect');
2290
+ var concurrencySelector = document.getElementById('concurrencySelector');
2291
+ var rotationSection = document.getElementById('rotationSection');
2292
+ if (!backendSelect || !modelSelect) {
2293
+ console.warn('[CodexLens] Backend or model select not found');
2294
+ return;
2295
+ }
2296
+
2297
+ var backend = backendSelect.value;
2298
+ console.log('[CodexLens] Backend changed to:', backend);
2299
+ console.log('[CodexLens] Current litellmApiConfig:', window.litellmApiConfig);
2300
+
2301
+ if (backend === 'litellm') {
2302
+ // Load LiteLLM embedding models
2303
+ console.log('[CodexLens] Building LiteLLM model options...');
2304
+ var options = buildLiteLLMModelOptions();
2305
+ console.log('[CodexLens] Built options HTML:', options);
2306
+ modelSelect.innerHTML = options;
2307
+ // Show concurrency selector for API backend
2308
+ if (concurrencySelector) {
2309
+ concurrencySelector.classList.remove('hidden');
2310
+ }
2311
+ // Show rotation section and load status
2312
+ if (rotationSection) {
2313
+ rotationSection.classList.remove('hidden');
2314
+ loadRotationStatus();
2315
+ }
2316
+ } else {
2317
+ // Load local fastembed models
2318
+ modelSelect.innerHTML = buildModelSelectOptionsForPage();
2319
+ // Hide concurrency selector for local backend
2320
+ if (concurrencySelector) {
2321
+ concurrencySelector.classList.add('hidden');
2322
+ }
2323
+ // Hide rotation section for local backend
2324
+ if (rotationSection) {
2325
+ rotationSection.classList.add('hidden');
2326
+ }
2327
+ }
2328
+ }
2329
+
2330
+ /**
2331
+ * Build LiteLLM model options from config
2332
+ */
2333
+ function buildLiteLLMModelOptions() {
2334
+ var litellmConfig = window.litellmApiConfig || {};
2335
+ console.log('[CodexLens] litellmApiConfig:', litellmConfig);
2336
+
2337
+ var providers = litellmConfig.providers || [];
2338
+ console.log('[CodexLens] providers count:', providers.length);
2339
+
2340
+ var options = '';
2341
+
2342
+ providers.forEach(function(provider) {
2343
+ console.log('[CodexLens] Processing provider:', provider.id, 'enabled:', provider.enabled);
2344
+ if (!provider.enabled) return;
2345
+
2346
+ // Check embeddingModels array (config structure)
2347
+ var models = provider.embeddingModels || provider.models || [];
2348
+ console.log('[CodexLens] Provider', provider.id, 'embeddingModels:', models.length, models);
2349
+
2350
+ models.forEach(function(model) {
2351
+ console.log('[CodexLens] Processing model:', model.id, 'type:', model.type, 'enabled:', model.enabled);
2352
+ // Accept embedding type or models from embeddingModels array
2353
+ if (model.type && model.type !== 'embedding') return;
2354
+ if (!model.enabled) return;
2355
+ var label = model.name || model.id;
2356
+ var providerName = provider.name || provider.id;
2357
+ var selected = options === '' ? ' selected' : '';
2358
+ options += '<option value="' + model.id + '"' + selected + '>' + label + ' (' + providerName + ')</option>';
2359
+ console.log('[CodexLens] Added option:', label, 'from', providerName);
2360
+ });
2361
+ });
2362
+
2363
+ if (options === '') {
2364
+ console.warn('[CodexLens] No embedding models found in LiteLLM config');
2365
+ options = '<option value="" disabled selected>' + (t('codexlens.noApiModels') || 'No API embedding models configured') + '</option>';
2366
+ }
2367
+
2368
+ return options;
2369
+ }
2370
+
2371
+ // Make functions globally accessible
2372
+ window.onEmbeddingBackendChange = onEmbeddingBackendChange;
2373
+
2374
+ /**
2375
+ * Initialize index from page with selected model
2376
+ */
2377
+ function initCodexLensIndexFromPage(indexType) {
2378
+ var backendSelect = document.getElementById('pageBackendSelect');
2379
+ var modelSelect = document.getElementById('pageModelSelect');
2380
+ var concurrencyInput = document.getElementById('pageConcurrencyInput');
2381
+ var selectedBackend = backendSelect ? backendSelect.value : 'fastembed';
2382
+ var selectedModel = modelSelect ? modelSelect.value : 'code';
2383
+ var selectedConcurrency = concurrencyInput ? Math.max(1, parseInt(concurrencyInput.value, 10) || 4) : 4;
2384
+
2385
+ // For FTS-only index, model is not needed
2386
+ if (indexType === 'normal') {
2387
+ initCodexLensIndex(indexType);
2388
+ } else {
2389
+ // Pass concurrency only for litellm backend
2390
+ var maxWorkers = selectedBackend === 'litellm' ? selectedConcurrency : 1;
2391
+ initCodexLensIndex(indexType, selectedModel, selectedBackend, maxWorkers);
2392
+ }
2393
+ }
2394
+
2395
+ /**
2396
+ * Initialize CodexLens Manager page event handlers
2397
+ */
2398
+ function initCodexLensManagerPageEvents(currentConfig) {
2399
+ var saveBtn = document.getElementById('saveIndexPathBtn');
2400
+ if (saveBtn) {
2401
+ saveBtn.onclick = async function() {
2402
+ var indexDirInput = document.getElementById('indexDirInput');
2403
+ var newIndexDir = indexDirInput ? indexDirInput.value.trim() : '';
2404
+ if (!newIndexDir) { showRefreshToast(t('codexlens.pathEmpty'), 'error'); return; }
2405
+ if (newIndexDir === currentConfig.index_dir) { showRefreshToast(t('codexlens.pathUnchanged'), 'info'); return; }
2406
+ saveBtn.disabled = true;
2407
+ saveBtn.innerHTML = '<span class="animate-pulse">' + t('common.saving') + '</span>';
2408
+ try {
2409
+ var response = await fetch('/api/codexlens/config', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ index_dir: newIndexDir }) });
2410
+ var result = await response.json();
2411
+ if (result.success) { showRefreshToast(t('codexlens.configSaved'), 'success'); renderCodexLensManager(); }
2412
+ else { showRefreshToast(t('common.saveFailed') + ': ' + result.error, 'error'); }
2413
+ } catch (err) { showRefreshToast(t('common.error') + ': ' + err.message, 'error'); }
2414
+ saveBtn.disabled = false;
2415
+ saveBtn.innerHTML = '<i data-lucide="save" class="w-3.5 h-3.5"></i> ' + t('codexlens.saveConfig');
2416
+ if (window.lucide) lucide.createIcons();
2417
+ };
2418
+ }
2419
+
2420
+ var runSearchBtn = document.getElementById('runSearchBtn');
2421
+ if (runSearchBtn) {
2422
+ runSearchBtn.onclick = async function() {
2423
+ var searchType = document.getElementById('searchTypeSelect').value;
2424
+ var searchMode = document.getElementById('searchModeSelect').value;
2425
+ var query = document.getElementById('searchQueryInput').value.trim();
2426
+ var resultsDiv = document.getElementById('searchResults');
2427
+ var resultCount = document.getElementById('searchResultCount');
2428
+ var resultContent = document.getElementById('searchResultContent');
2429
+ if (!query) { showRefreshToast(t('codexlens.enterQuery'), 'warning'); return; }
2430
+ runSearchBtn.disabled = true;
2431
+ runSearchBtn.innerHTML = '<span class="animate-pulse">' + t('codexlens.searching') + '</span>';
2432
+ resultsDiv.classList.add('hidden');
2433
+ try {
2434
+ var endpoint = '/api/codexlens/' + searchType;
2435
+ var params = new URLSearchParams({ query: query, limit: '20' });
2436
+ if (searchType === 'search' || searchType === 'search_files') { params.append('mode', searchMode); }
2437
+ var response = await fetch(endpoint + '?' + params.toString());
2438
+ var result = await response.json();
2439
+ if (result.success) {
2440
+ var results = result.results || result.files || [];
2441
+ resultCount.textContent = results.length + ' ' + t('codexlens.resultsCount');
2442
+ resultContent.textContent = JSON.stringify(results, null, 2);
2443
+ resultsDiv.classList.remove('hidden');
2444
+ } else {
2445
+ resultContent.textContent = t('common.error') + ': ' + (result.error || t('common.unknownError'));
2446
+ resultsDiv.classList.remove('hidden');
2447
+ }
2448
+ } catch (err) {
2449
+ resultContent.textContent = t('common.exception') + ': ' + err.message;
2450
+ resultsDiv.classList.remove('hidden');
2451
+ }
2452
+ runSearchBtn.disabled = false;
2453
+ runSearchBtn.innerHTML = '<i data-lucide="search" class="w-3.5 h-3.5"></i> ' + t('codexlens.runSearch');
2454
+ if (window.lucide) lucide.createIcons();
2455
+ };
2456
+ }
2457
+
2458
+ var searchInput = document.getElementById('searchQueryInput');
2459
+ if (searchInput) { searchInput.onkeypress = function(e) { if (e.key === 'Enter' && runSearchBtn) { runSearchBtn.click(); } }; }
2460
+ }
2461
+
2462
+ /**
2463
+ * Show index initialization modal
2464
+ */
2465
+ function showIndexInitModal() {
2466
+ // Use initCodexLensIndex with default settings
2467
+ initCodexLensIndex('vector', 'code');
2468
+ }
2469
+
2470
+ /**
2471
+ * Load index stats for the CodexLens Manager page
2472
+ */
2473
+ async function loadIndexStatsForPage() {
2474
+ try {
2475
+ var response = await fetch('/api/codexlens/indexes');
2476
+ if (!response.ok) throw new Error('Failed to load index stats');
2477
+ var data = await response.json();
2478
+ renderIndexStatsForPage(data);
2479
+ } catch (err) {
2480
+ console.error('[CodexLens] Failed to load index stats:', err);
2481
+ var tbody = document.getElementById('indexTableBody');
2482
+ if (tbody) {
2483
+ tbody.innerHTML = '<tr><td colspan="5" class="py-4 text-center text-destructive text-sm">' + err.message + '</td></tr>';
2484
+ }
2485
+ }
2486
+ }
2487
+
2488
+ /**
2489
+ * Render index stats in the CodexLens Manager page
2490
+ */
2491
+ function renderIndexStatsForPage(data) {
2492
+ var summary = data.summary || {};
2493
+ var indexes = data.indexes || [];
2494
+ var indexDir = data.indexDir || '';
2495
+
2496
+ // Update summary stats
2497
+ var totalSizeEl = document.getElementById('indexTotalSize');
2498
+ var projectCountEl = document.getElementById('indexProjectCount');
2499
+ var totalSizeValEl = document.getElementById('indexTotalSizeVal');
2500
+ var vectorCountEl = document.getElementById('indexVectorCount');
2501
+ var ftsCountEl = document.getElementById('indexFtsCount');
2502
+ var indexDirEl = document.getElementById('indexDirDisplay');
2503
+
2504
+ if (totalSizeEl) totalSizeEl.textContent = summary.totalSizeFormatted || '0 B';
2505
+ if (projectCountEl) projectCountEl.textContent = summary.totalProjects || 0;
2506
+ if (totalSizeValEl) totalSizeValEl.textContent = summary.totalSizeFormatted || '0 B';
2507
+ if (vectorCountEl) vectorCountEl.textContent = summary.vectorIndexCount || 0;
2508
+ if (ftsCountEl) ftsCountEl.textContent = summary.normalIndexCount || 0;
2509
+ if (indexDirEl && indexDir) {
2510
+ indexDirEl.textContent = indexDir;
2511
+ indexDirEl.title = indexDir;
2512
+ }
2513
+
2514
+ // Render table rows
2515
+ var tbody = document.getElementById('indexTableBody');
2516
+ if (!tbody) return;
2517
+
2518
+ if (indexes.length === 0) {
2519
+ tbody.innerHTML = '<tr><td colspan="5" class="py-4 text-center text-muted-foreground text-sm">' + (t('index.noIndexes') || 'No indexes yet') + '</td></tr>';
2520
+ return;
2521
+ }
2522
+
2523
+ var rows = '';
2524
+ indexes.forEach(function(idx) {
2525
+ var vectorBadge = idx.hasVectorIndex
2526
+ ? '<span class="text-xs px-1.5 py-0.5 bg-primary/10 text-primary rounded">' + (t('index.vector') || 'Vector') + '</span>'
2527
+ : '';
2528
+ var normalBadge = idx.hasNormalIndex
2529
+ ? '<span class="text-xs px-1.5 py-0.5 bg-muted text-muted-foreground rounded">' + (t('index.fts') || 'FTS') + '</span>'
2530
+ : '';
2531
+
2532
+ rows += '<tr class="border-t border-border hover:bg-muted/30 transition-colors">' +
2533
+ '<td class="py-2 px-2 text-foreground">' +
2534
+ '<span class="font-mono text-xs truncate max-w-[250px] inline-block" title="' + escapeHtml(idx.id) + '">' + escapeHtml(idx.id) + '</span>' +
2535
+ '</td>' +
2536
+ '<td class="py-2 px-2 text-right text-muted-foreground">' + (idx.sizeFormatted || '-') + '</td>' +
2537
+ '<td class="py-2 px-2 text-center"><div class="flex items-center justify-center gap-1">' + vectorBadge + normalBadge + '</div></td>' +
2538
+ '<td class="py-2 px-2 text-right text-muted-foreground">' + formatTimeAgoSimple(idx.lastModified) + '</td>' +
2539
+ '<td class="py-2 px-1 text-center">' +
2540
+ '<button onclick="cleanIndexProjectFromPage(\'' + escapeHtml(idx.id) + '\')" ' +
2541
+ 'class="text-destructive/70 hover:text-destructive p-1 rounded hover:bg-destructive/10 transition-colors" ' +
2542
+ 'title="' + (t('index.cleanProject') || 'Clean Index') + '">' +
2543
+ '<i data-lucide="trash-2" class="w-3.5 h-3.5"></i>' +
2544
+ '</button>' +
2545
+ '</td>' +
2546
+ '</tr>';
2547
+ });
2548
+
2549
+ tbody.innerHTML = rows;
2550
+ if (window.lucide) lucide.createIcons();
2551
+ }
2552
+
2553
+ /**
2554
+ * Simple time ago formatter
2555
+ */
2556
+ function formatTimeAgoSimple(isoString) {
2557
+ if (!isoString) return t('common.never') || 'Never';
2558
+ var date = new Date(isoString);
2559
+ var now = new Date();
2560
+ var diffMs = now - date;
2561
+ var diffMins = Math.floor(diffMs / 60000);
2562
+ var diffHours = Math.floor(diffMins / 60);
2563
+ var diffDays = Math.floor(diffHours / 24);
2564
+ if (diffMins < 1) return t('common.justNow') || 'Just now';
2565
+ if (diffMins < 60) return diffMins + 'm ' + (t('common.ago') || 'ago');
2566
+ if (diffHours < 24) return diffHours + 'h ' + (t('common.ago') || 'ago');
2567
+ if (diffDays < 30) return diffDays + 'd ' + (t('common.ago') || 'ago');
2568
+ return date.toLocaleDateString();
2569
+ }
2570
+
2571
+ /**
2572
+ * Clean a specific project's index from the page
2573
+ */
2574
+ async function cleanIndexProjectFromPage(projectId) {
2575
+ if (!confirm((t('index.cleanProjectConfirm') || 'Clean index for') + ' ' + projectId + '?')) {
2576
+ return;
2577
+ }
2578
+
2579
+ try {
2580
+ showRefreshToast(t('index.cleaning') || 'Cleaning index...', 'info');
2581
+
2582
+ var response = await fetch('/api/codexlens/clean', {
2583
+ method: 'POST',
2584
+ headers: { 'Content-Type': 'application/json' },
2585
+ body: JSON.stringify({ projectId: projectId })
2586
+ });
2587
+
2588
+ var result = await response.json();
2589
+
2590
+ if (result.success) {
2591
+ showRefreshToast(t('index.cleanSuccess') || 'Index cleaned successfully', 'success');
2592
+ await loadIndexStatsForPage();
2593
+ } else {
2594
+ showRefreshToast((t('index.cleanFailed') || 'Clean failed') + ': ' + result.error, 'error');
2595
+ }
2596
+ } catch (err) {
2597
+ showRefreshToast((t('common.error') || 'Error') + ': ' + err.message, 'error');
2598
+ }
2599
+ }
2600
+
2601
+ /**
2602
+ * Clean all indexes from the page
2603
+ */
2604
+ async function cleanAllIndexesFromPage() {
2605
+ if (!confirm(t('index.cleanAllConfirm') || 'Are you sure you want to clean ALL indexes? This cannot be undone.')) {
2606
+ return;
2607
+ }
2608
+
2609
+ try {
2610
+ showRefreshToast(t('index.cleaning') || 'Cleaning indexes...', 'info');
2611
+
2612
+ var response = await fetch('/api/codexlens/clean', {
2613
+ method: 'POST',
2614
+ headers: { 'Content-Type': 'application/json' },
2615
+ body: JSON.stringify({ all: true })
2616
+ });
2617
+
2618
+ var result = await response.json();
2619
+
2620
+ if (result.success) {
2621
+ showRefreshToast(t('index.cleanAllSuccess') || 'All indexes cleaned', 'success');
2622
+ await loadIndexStatsForPage();
2623
+ } else {
2624
+ showRefreshToast((t('index.cleanFailed') || 'Clean failed') + ': ' + result.error, 'error');
2625
+ }
2626
+ } catch (err) {
2627
+ showRefreshToast((t('common.error') || 'Error') + ': ' + err.message, 'error');
2628
+ }
2629
+ }
2630
+
2631
+ // ============================================================
2632
+ // MULTI-PROVIDER ROTATION CONFIGURATION
2633
+ // ============================================================
2634
+
2635
+ /**
2636
+ * Load and display rotation status in the page
2637
+ */
2638
+ async function loadRotationStatus() {
2639
+ try {
2640
+ // Load from unified embedding-pool API (handles both new and legacy config)
2641
+ var response = await fetch('/api/litellm-api/embedding-pool');
2642
+ if (!response.ok) {
2643
+ console.warn('[CodexLens] Failed to load embedding pool config:', response.status);
2644
+ return;
2645
+ }
2646
+ var data = await response.json();
2647
+ window.embeddingPoolConfig = data.poolConfig;
2648
+ window.embeddingPoolAvailableModels = data.availableModels || [];
2649
+
2650
+ // Also get endpoint count
2651
+ var endpointsResponse = await fetch('/api/litellm-api/codexlens/rotation/endpoints');
2652
+ var endpointsData = endpointsResponse.ok ? await endpointsResponse.json() : { count: 0 };
2653
+
2654
+ updateRotationStatusDisplay(data.poolConfig, endpointsData.count);
2655
+ } catch (err) {
2656
+ console.error('[CodexLens] Error loading rotation status:', err);
2657
+ }
2658
+ }
2659
+
2660
+ /**
2661
+ * Update the rotation status display in the page
2662
+ * @param {Object} poolConfig - The embedding pool configuration
2663
+ * @param {number} endpointCount - Number of active endpoints
2664
+ */
2665
+ function updateRotationStatusDisplay(poolConfig, endpointCount) {
2666
+ var badge = document.getElementById('rotationStatusBadge');
2667
+ var detailsEl = document.getElementById('rotationDetails');
2668
+ var modelNameEl = document.getElementById('rotationModelName');
2669
+ var countEl = document.getElementById('rotationEndpointCount');
2670
+
2671
+ if (!badge) return;
2672
+
2673
+ if (poolConfig && poolConfig.enabled) {
2674
+ badge.textContent = t('common.enabled');
2675
+ badge.className = 'text-xs px-2 py-0.5 rounded-full bg-success/10 text-success';
2676
+
2677
+ // Show details
2678
+ if (detailsEl) {
2679
+ detailsEl.classList.remove('hidden');
2680
+ if (modelNameEl) modelNameEl.textContent = poolConfig.targetModel || '';
2681
+ if (countEl) countEl.textContent = (endpointCount || 0) + ' ' + t('codexlens.totalEndpoints').toLowerCase();
2682
+ }
2683
+ } else {
2684
+ badge.textContent = t('common.disabled');
2685
+ badge.className = 'text-xs px-2 py-0.5 rounded-full bg-muted text-muted-foreground';
2686
+ if (detailsEl) detailsEl.classList.add('hidden');
2687
+ }
2688
+ }
2689
+
2690
+ /**
2691
+ * Navigate to API Settings Embedding Pool tab
2692
+ */
2693
+ function navigateToApiSettingsEmbeddingPool() {
2694
+ // Navigate to API Settings page with embedding-pool tab
2695
+ if (typeof switchView === 'function') {
2696
+ switchView('api-settings');
2697
+ // Give time for page to render, then switch to embedding-pool tab
2698
+ setTimeout(function() {
2699
+ if (typeof switchSidebarTab === 'function') {
2700
+ switchSidebarTab('embedding-pool');
2701
+ }
2702
+ }, 100);
2703
+ }
2704
+ }
2705
+
2706
+ /**
2707
+ * Show the rotation configuration modal
2708
+ */
2709
+ async function showRotationConfigModal() {
2710
+ try {
2711
+ // Load current config if not already loaded
2712
+ if (!window.rotationConfig) {
2713
+ await loadRotationStatus();
2714
+ }
2715
+
2716
+ var rotationConfig = window.rotationConfig || {
2717
+ enabled: false,
2718
+ strategy: 'round_robin',
2719
+ defaultCooldown: 60,
2720
+ targetModel: 'qwen3-embedding',
2721
+ providers: []
2722
+ };
2723
+ var availableProviders = window.availableRotationProviders || [];
2724
+
2725
+ var modalHtml = buildRotationConfigModal(rotationConfig, availableProviders);
2726
+
2727
+ var tempContainer = document.createElement('div');
2728
+ tempContainer.innerHTML = modalHtml;
2729
+ var modal = tempContainer.firstElementChild;
2730
+ document.body.appendChild(modal);
2731
+
2732
+ if (window.lucide) lucide.createIcons();
2733
+ initRotationConfigEvents(rotationConfig, availableProviders);
2734
+ } catch (err) {
2735
+ showRefreshToast(t('common.error') + ': ' + err.message, 'error');
2736
+ }
2737
+ }
2738
+
2739
+ /**
2740
+ * Build the rotation configuration modal HTML
2741
+ */
2742
+ function buildRotationConfigModal(rotationConfig, availableProviders) {
2743
+ var isEnabled = rotationConfig.enabled || false;
2744
+ var strategy = rotationConfig.strategy || 'round_robin';
2745
+ var cooldown = rotationConfig.defaultCooldown || 60;
2746
+ var targetModel = rotationConfig.targetModel || 'qwen3-embedding';
2747
+ var configuredProviders = rotationConfig.providers || [];
2748
+
2749
+ // Build provider list HTML
2750
+ var providerListHtml = '';
2751
+ if (availableProviders.length === 0) {
2752
+ providerListHtml = '<div class="text-sm text-muted-foreground py-4 text-center">' + t('codexlens.noRotationProviders') + '</div>';
2753
+ } else {
2754
+ availableProviders.forEach(function(provider, index) {
2755
+ // Find if this provider is already configured
2756
+ var configured = configuredProviders.find(function(p) { return p.providerId === provider.providerId; });
2757
+ var isProviderEnabled = configured ? configured.enabled : false;
2758
+ var weight = configured ? configured.weight : 1;
2759
+ var maxConcurrent = configured ? configured.maxConcurrentPerKey : 4;
2760
+ var useAllKeys = configured ? configured.useAllKeys : true;
2761
+
2762
+ // Get model options
2763
+ var modelOptions = provider.embeddingModels.map(function(m) {
2764
+ var selected = configured && configured.modelId === m.modelId ? 'selected' : '';
2765
+ return '<option value="' + m.modelId + '" ' + selected + '>' + m.modelName + ' (' + m.dimensions + 'd)</option>';
2766
+ }).join('');
2767
+
2768
+ // Get key count
2769
+ var keyCount = provider.apiKeys.filter(function(k) { return k.enabled; }).length;
2770
+
2771
+ providerListHtml +=
2772
+ '<div class="border border-border rounded-lg p-3 ' + (isProviderEnabled ? 'bg-success/5 border-success/30' : 'bg-muted/30') + '" data-provider-id="' + provider.providerId + '">' +
2773
+ '<div class="flex items-center justify-between mb-2">' +
2774
+ '<div class="flex items-center gap-2">' +
2775
+ '<input type="checkbox" id="rotationProvider_' + index + '" ' + (isProviderEnabled ? 'checked' : '') +
2776
+ ' class="rotation-provider-toggle" data-provider-id="' + provider.providerId + '" />' +
2777
+ '<label for="rotationProvider_' + index + '" class="font-medium text-sm">' + provider.providerName + '</label>' +
2778
+ '<span class="text-xs px-1.5 py-0.5 bg-muted rounded text-muted-foreground">' + keyCount + ' keys</span>' +
2779
+ '</div>' +
2780
+ '</div>' +
2781
+ '<div class="grid grid-cols-2 gap-2 text-xs">' +
2782
+ '<div>' +
2783
+ '<label class="text-muted-foreground">Model</label>' +
2784
+ '<select class="w-full px-2 py-1 border border-border rounded bg-background text-sm rotation-model-select" data-provider-id="' + provider.providerId + '">' +
2785
+ modelOptions +
2786
+ '</select>' +
2787
+ '</div>' +
2788
+ '<div>' +
2789
+ '<label class="text-muted-foreground">' + t('codexlens.providerWeight') + '</label>' +
2790
+ '<input type="number" min="0.1" max="10" step="0.1" value="' + weight + '" ' +
2791
+ 'class="w-full px-2 py-1 border border-border rounded bg-background text-sm rotation-weight-input" data-provider-id="' + provider.providerId + '" />' +
2792
+ '</div>' +
2793
+ '<div>' +
2794
+ '<label class="text-muted-foreground">' + t('codexlens.maxConcurrentPerKey') + '</label>' +
2795
+ '<input type="number" min="1" max="16" value="' + maxConcurrent + '" ' +
2796
+ 'class="w-full px-2 py-1 border border-border rounded bg-background text-sm rotation-concurrent-input" data-provider-id="' + provider.providerId + '" />' +
2797
+ '</div>' +
2798
+ '<div class="flex items-center gap-1">' +
2799
+ '<input type="checkbox" id="useAllKeys_' + index + '" ' + (useAllKeys ? 'checked' : '') +
2800
+ ' class="rotation-use-all-keys" data-provider-id="' + provider.providerId + '" />' +
2801
+ '<label for="useAllKeys_' + index + '" class="text-muted-foreground">' + t('codexlens.useAllKeys') + '</label>' +
2802
+ '</div>' +
2803
+ '</div>' +
2804
+ '</div>';
2805
+ });
2806
+ }
2807
+
2808
+ return '<div class="modal-backdrop" id="rotationConfigModal">' +
2809
+ '<div class="modal-container max-w-2xl">' +
2810
+ '<div class="modal-header">' +
2811
+ '<div class="flex items-center gap-3">' +
2812
+ '<div class="modal-icon">' +
2813
+ '<i data-lucide="rotate-cw" class="w-5 h-5"></i>' +
2814
+ '</div>' +
2815
+ '<div>' +
2816
+ '<h2 class="text-lg font-bold">' + t('codexlens.rotation') + '</h2>' +
2817
+ '<p class="text-xs text-muted-foreground">' + t('codexlens.rotationDesc') + '</p>' +
2818
+ '</div>' +
2819
+ '</div>' +
2820
+ '<button onclick="closeRotationModal()" class="text-muted-foreground hover:text-foreground">' +
2821
+ '<i data-lucide="x" class="w-5 h-5"></i>' +
2822
+ '</button>' +
2823
+ '</div>' +
2824
+ '<div class="modal-body space-y-4">' +
2825
+ // Enable toggle
2826
+ '<div class="flex items-center justify-between p-3 bg-muted/30 rounded-lg">' +
2827
+ '<div class="flex items-center gap-2">' +
2828
+ '<i data-lucide="power" class="w-4 h-4 text-primary"></i>' +
2829
+ '<span class="font-medium">' + t('codexlens.rotationEnabled') + '</span>' +
2830
+ '</div>' +
2831
+ '<label class="relative inline-flex items-center cursor-pointer">' +
2832
+ '<input type="checkbox" id="rotationEnabledToggle" ' + (isEnabled ? 'checked' : '') + ' class="sr-only peer" />' +
2833
+ '<div class="w-11 h-6 bg-muted peer-focus:outline-none rounded-full peer peer-checked:after:translate-x-full peer-checked:after:border-white after:content-[\'\'] after:absolute after:top-[2px] after:left-[2px] after:bg-white after:border-gray-300 after:border after:rounded-full after:h-5 after:w-5 after:transition-all peer-checked:bg-primary"></div>' +
2834
+ '</label>' +
2835
+ '</div>' +
2836
+ // Strategy and settings
2837
+ '<div class="grid grid-cols-2 gap-4">' +
2838
+ '<div>' +
2839
+ '<label class="block text-sm font-medium mb-1.5">' + t('codexlens.rotationStrategy') + '</label>' +
2840
+ '<select id="rotationStrategy" class="w-full px-3 py-2 border border-border rounded-lg bg-background text-sm">' +
2841
+ '<option value="round_robin" ' + (strategy === 'round_robin' ? 'selected' : '') + '>' + t('codexlens.strategyRoundRobin') + '</option>' +
2842
+ '<option value="latency_aware" ' + (strategy === 'latency_aware' ? 'selected' : '') + '>' + t('codexlens.strategyLatencyAware') + '</option>' +
2843
+ '<option value="weighted_random" ' + (strategy === 'weighted_random' ? 'selected' : '') + '>' + t('codexlens.strategyWeightedRandom') + '</option>' +
2844
+ '</select>' +
2845
+ '</div>' +
2846
+ '<div>' +
2847
+ '<label class="block text-sm font-medium mb-1.5">' + t('codexlens.cooldownSeconds') + '</label>' +
2848
+ '<input type="number" id="rotationCooldown" min="1" max="300" value="' + cooldown + '" ' +
2849
+ 'class="w-full px-3 py-2 border border-border rounded-lg bg-background text-sm" />' +
2850
+ '<p class="text-xs text-muted-foreground mt-1">' + t('codexlens.cooldownHint') + '</p>' +
2851
+ '</div>' +
2852
+ '</div>' +
2853
+ // Target model
2854
+ '<div>' +
2855
+ '<label class="block text-sm font-medium mb-1.5">' + t('codexlens.targetModel') + '</label>' +
2856
+ '<input type="text" id="rotationTargetModel" value="' + targetModel + '" ' +
2857
+ 'class="w-full px-3 py-2 border border-border rounded-lg bg-background text-sm" placeholder="qwen3-embedding" />' +
2858
+ '<p class="text-xs text-muted-foreground mt-1">' + t('codexlens.targetModelHint') + '</p>' +
2859
+ '</div>' +
2860
+ // Provider list
2861
+ '<div>' +
2862
+ '<label class="block text-sm font-medium mb-1.5">' + t('codexlens.rotationProviders') + '</label>' +
2863
+ '<div class="space-y-2 max-h-64 overflow-y-auto" id="rotationProviderList">' +
2864
+ providerListHtml +
2865
+ '</div>' +
2866
+ '</div>' +
2867
+ '</div>' +
2868
+ '<div class="modal-footer">' +
2869
+ '<button onclick="closeRotationModal()" class="btn btn-outline">' + t('common.cancel') + '</button>' +
2870
+ '<button onclick="saveRotationConfig()" class="btn btn-primary">' +
2871
+ '<i data-lucide="save" class="w-4 h-4"></i> ' + t('common.save') +
2872
+ '</button>' +
2873
+ '</div>' +
2874
+ '</div>' +
2875
+ '</div>';
2876
+ }
2877
+
2878
+ /**
2879
+ * Initialize rotation config modal events
2880
+ */
2881
+ function initRotationConfigEvents(rotationConfig, availableProviders) {
2882
+ // Store in window for save function
2883
+ window._rotationAvailableProviders = availableProviders;
2884
+ }
2885
+
2886
+ /**
2887
+ * Close the rotation config modal
2888
+ */
2889
+ function closeRotationModal() {
2890
+ var modal = document.getElementById('rotationConfigModal');
2891
+ if (modal) modal.remove();
2892
+ }
2893
+
2894
+ /**
2895
+ * Save the rotation configuration
2896
+ */
2897
+ async function saveRotationConfig() {
2898
+ try {
2899
+ var enabledToggle = document.getElementById('rotationEnabledToggle');
2900
+ var strategySelect = document.getElementById('rotationStrategy');
2901
+ var cooldownInput = document.getElementById('rotationCooldown');
2902
+ var targetModelInput = document.getElementById('rotationTargetModel');
2903
+
2904
+ var enabled = enabledToggle ? enabledToggle.checked : false;
2905
+ var strategy = strategySelect ? strategySelect.value : 'round_robin';
2906
+ var cooldown = cooldownInput ? parseInt(cooldownInput.value, 10) : 60;
2907
+ var targetModel = targetModelInput ? targetModelInput.value.trim() : 'qwen3-embedding';
2908
+
2909
+ // Collect provider configurations
2910
+ var providers = [];
2911
+ var providerToggles = document.querySelectorAll('.rotation-provider-toggle');
2912
+ providerToggles.forEach(function(toggle) {
2913
+ var providerId = toggle.getAttribute('data-provider-id');
2914
+ var isEnabled = toggle.checked;
2915
+
2916
+ var modelSelect = document.querySelector('.rotation-model-select[data-provider-id="' + providerId + '"]');
2917
+ var weightInput = document.querySelector('.rotation-weight-input[data-provider-id="' + providerId + '"]');
2918
+ var concurrentInput = document.querySelector('.rotation-concurrent-input[data-provider-id="' + providerId + '"]');
2919
+ var useAllKeysToggle = document.querySelector('.rotation-use-all-keys[data-provider-id="' + providerId + '"]');
2920
+
2921
+ providers.push({
2922
+ providerId: providerId,
2923
+ modelId: modelSelect ? modelSelect.value : '',
2924
+ weight: weightInput ? parseFloat(weightInput.value) || 1 : 1,
2925
+ maxConcurrentPerKey: concurrentInput ? parseInt(concurrentInput.value, 10) || 4 : 4,
2926
+ useAllKeys: useAllKeysToggle ? useAllKeysToggle.checked : true,
2927
+ enabled: isEnabled
2928
+ });
2929
+ });
2930
+
2931
+ var rotationConfig = {
2932
+ enabled: enabled,
2933
+ strategy: strategy,
2934
+ defaultCooldown: cooldown,
2935
+ targetModel: targetModel,
2936
+ providers: providers
2937
+ };
2938
+
2939
+ var response = await fetch('/api/litellm-api/codexlens/rotation', {
2940
+ method: 'PUT',
2941
+ headers: { 'Content-Type': 'application/json' },
2942
+ body: JSON.stringify(rotationConfig)
2943
+ });
2944
+
2945
+ var result = await response.json();
2946
+
2947
+ if (result.success) {
2948
+ // Show sync result in toast
2949
+ var syncMsg = '';
2950
+ if (result.syncResult) {
2951
+ if (result.syncResult.success) {
2952
+ syncMsg = ' (' + result.syncResult.endpointCount + ' ' + t('codexlens.endpointsSynced') + ')';
2953
+ } else {
2954
+ syncMsg = ' (' + t('codexlens.syncFailed') + ': ' + result.syncResult.message + ')';
2955
+ }
2956
+ }
2957
+ showRefreshToast(t('codexlens.rotationSaved') + syncMsg, 'success');
2958
+ window.rotationConfig = rotationConfig;
2959
+ updateRotationStatusDisplay(rotationConfig);
2960
+ closeRotationModal();
2961
+ } else {
2962
+ showRefreshToast(t('common.saveFailed') + ': ' + result.error, 'error');
2963
+ }
2964
+ } catch (err) {
2965
+ showRefreshToast(t('common.error') + ': ' + err.message, 'error');
2966
+ }
2967
+ }