feat: Refactor CLI tool configuration management and introduce skill context loader

- Updated `claude-cli-tools.ts` to support new model configurations and migration from older versions.
- Added `getPredefinedModels` and `getAllPredefinedModels` functions for better model management.
- Deprecated `cli-config-manager.ts` in favor of `claude-cli-tools.ts`, maintaining backward compatibility.
- Introduced `skill-context-loader.ts` to handle skill context loading based on user prompts and keywords.
- Enhanced tool configuration functions to include secondary models and improved migration logic.
- Updated index file to register the new skill context loader tool.
This commit is contained in:
catlog22
2026-01-11 13:56:20 +08:00
parent 2c11392848
commit 16083130f8
11 changed files with 1959 additions and 420 deletions

View File

@@ -308,3 +308,311 @@
background: hsl(var(--destructive) / 0.1);
}
/* ========================================
* CLI Manager Split Layout (Claude Config)
* ======================================== */
.cli-manager-split {
display: flex;
gap: var(--spacing-md, 1rem);
height: 100%;
min-height: 400px;
}
.cli-manager-sidebar {
width: 280px;
flex-shrink: 0;
overflow-y: auto;
background: hsl(var(--card));
border-radius: 0.5rem;
border: 1px solid hsl(var(--border));
padding: 1rem;
}
.cli-manager-sidebar h3 {
font-size: 0.875rem;
font-weight: 600;
margin: 0 0 0.75rem 0;
display: flex;
align-items: center;
gap: 0.5rem;
color: hsl(var(--foreground));
}
.cli-manager-main {
flex: 1;
overflow-y: auto;
padding: 1.25rem;
background: hsl(var(--card));
border-radius: 0.5rem;
border: 1px solid hsl(var(--border));
}
/* Tool List Items */
.cli-tool-list-item {
padding: 0.625rem 0.75rem;
cursor: pointer;
border-radius: 0.375rem;
transition: all 0.15s ease;
display: flex;
align-items: center;
justify-content: space-between;
margin-bottom: 0.25rem;
}
.cli-tool-list-item:hover {
background: hsl(var(--accent));
}
.cli-tool-list-item.selected {
background: hsl(var(--primary) / 0.1);
border-left: 3px solid hsl(var(--primary));
padding-left: calc(0.75rem - 3px);
}
.cli-tool-list-item .tool-name {
font-weight: 500;
font-size: 0.8125rem;
display: flex;
align-items: center;
gap: 0.5rem;
}
.cli-tool-list-item .tool-status {
width: 8px;
height: 8px;
border-radius: 50%;
flex-shrink: 0;
}
.cli-tool-list-item .tool-status.installed {
background: hsl(var(--success));
}
.cli-tool-list-item .tool-status.not-installed {
background: hsl(var(--muted-foreground) / 0.4);
}
/* Config Mode Toggle */
.config-mode-toggle {
display: flex;
gap: 0.5rem;
margin-bottom: 1.25rem;
background: hsl(var(--muted) / 0.5);
padding: 0.25rem;
border-radius: 0.5rem;
}
.config-mode-btn {
flex: 1;
padding: 0.5rem 0.75rem;
border: none;
border-radius: 0.375rem;
background: transparent;
color: hsl(var(--muted-foreground));
font-size: 0.8125rem;
font-weight: 500;
cursor: pointer;
transition: all 0.15s ease;
display: flex;
align-items: center;
justify-content: center;
gap: 0.375rem;
}
.config-mode-btn:hover {
color: hsl(var(--foreground));
}
.config-mode-btn.active {
background: hsl(var(--background));
color: hsl(var(--primary));
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
}
/* Tool Detail Header */
.tool-detail-header {
display: flex;
align-items: center;
justify-content: space-between;
margin-bottom: 1.25rem;
padding-bottom: 1rem;
border-bottom: 1px solid hsl(var(--border));
}
.tool-detail-header h3 {
font-size: 1rem;
font-weight: 600;
margin: 0;
display: flex;
align-items: center;
gap: 0.5rem;
}
/* Claude Settings Form */
.claude-config-form {
display: flex;
flex-direction: column;
gap: 1rem;
}
.claude-config-form .form-group {
display: flex;
flex-direction: column;
gap: 0.375rem;
}
.claude-config-form .form-group label {
font-size: 0.75rem;
font-weight: 600;
color: hsl(var(--muted-foreground));
text-transform: uppercase;
letter-spacing: 0.025em;
}
.claude-config-form .form-control {
width: 100%;
padding: 0.5rem 0.75rem;
font-size: 0.8125rem;
font-family: inherit;
border: 1px solid hsl(var(--border));
border-radius: 0.375rem;
background: hsl(var(--background));
color: hsl(var(--foreground));
transition: border-color 0.15s ease, box-shadow 0.15s ease;
}
.claude-config-form .form-control:focus {
outline: none;
border-color: hsl(var(--primary));
box-shadow: 0 0 0 2px hsl(var(--primary) / 0.2);
}
.claude-config-form .form-control::placeholder {
color: hsl(var(--muted-foreground) / 0.6);
}
/* Model Config Section */
.model-config-section {
background: hsl(var(--muted) / 0.3);
padding: 1rem;
border-radius: 0.5rem;
margin-top: 0.5rem;
}
.model-config-section h4 {
font-size: 0.8125rem;
font-weight: 600;
margin: 0 0 0.75rem 0;
display: flex;
align-items: center;
gap: 0.375rem;
}
.model-config-grid {
display: grid;
grid-template-columns: repeat(2, 1fr);
gap: 0.75rem;
}
@media (max-width: 768px) {
.model-config-grid {
grid-template-columns: 1fr;
}
}
/* Empty State */
.cli-manager-main .empty-state {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
height: 100%;
min-height: 200px;
color: hsl(var(--muted-foreground));
font-size: 0.875rem;
gap: 0.5rem;
}
.cli-manager-main .empty-state i {
opacity: 0.5;
}
/* Endpoints List in Config Panel */
.claude-endpoints-list {
margin-top: 1.5rem;
padding-top: 1rem;
border-top: 1px solid hsl(var(--border));
}
.claude-endpoints-list h4 {
font-size: 0.8125rem;
font-weight: 600;
margin: 0 0 0.75rem 0;
display: flex;
align-items: center;
gap: 0.375rem;
}
.claude-endpoint-item {
display: flex;
align-items: center;
justify-content: space-between;
padding: 0.625rem 0.75rem;
background: hsl(var(--muted) / 0.3);
border-radius: 0.375rem;
margin-bottom: 0.5rem;
}
.claude-endpoint-item:last-child {
margin-bottom: 0;
}
.claude-endpoint-info {
display: flex;
flex-direction: column;
gap: 0.125rem;
}
.claude-endpoint-name {
font-weight: 500;
font-size: 0.8125rem;
}
.claude-endpoint-meta {
font-size: 0.6875rem;
color: hsl(var(--muted-foreground));
display: flex;
align-items: center;
gap: 0.5rem;
}
.claude-endpoint-actions {
display: flex;
align-items: center;
gap: 0.375rem;
}
/* Config Source Badge */
.config-source-badge {
display: inline-flex;
align-items: center;
gap: 0.25rem;
padding: 0.125rem 0.375rem;
font-size: 0.625rem;
font-weight: 500;
border-radius: 9999px;
text-transform: uppercase;
letter-spacing: 0.025em;
}
.config-source-badge.provider {
background: hsl(var(--primary) / 0.15);
color: hsl(var(--primary));
}
.config-source-badge.direct {
background: hsl(var(--success) / 0.15);
color: hsl(var(--success));
}

View File

@@ -2329,4 +2329,132 @@ select.cli-input {
width: 1rem;
height: 1rem;
flex-shrink: 0;
}
/* ===========================
Model Pool Detail Info Grid
=========================== */
.provider-detail {
padding: 1.5rem;
overflow-y: auto;
}
.provider-detail .provider-detail-header {
display: flex;
align-items: flex-start;
justify-content: space-between;
margin-bottom: 1.5rem;
padding-bottom: 1rem;
border-bottom: 1px solid hsl(var(--border));
}
.provider-detail .provider-detail-header h2 {
margin: 0;
font-size: 1.25rem;
font-weight: 600;
color: hsl(var(--foreground));
}
.provider-detail .provider-detail-body {
display: flex;
flex-direction: column;
gap: 1.5rem;
}
.provider-detail .form-section {
padding: 1.25rem;
background: hsl(var(--card));
border: 1px solid hsl(var(--border));
border-radius: 0.75rem;
}
.provider-detail .form-section h3 {
margin: 0 0 1rem 0;
font-size: 0.875rem;
font-weight: 600;
color: hsl(var(--muted-foreground));
text-transform: uppercase;
letter-spacing: 0.05em;
}
.provider-detail .form-section p {
margin: 0;
font-size: 0.875rem;
color: hsl(var(--foreground));
line-height: 1.5;
}
/* Info Grid - Used in Model Pool Detail */
.info-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(200px, 1fr));
gap: 1rem;
}
.info-item {
display: flex;
flex-direction: column;
gap: 0.25rem;
}
.info-item label {
font-size: 0.75rem;
font-weight: 500;
color: hsl(var(--muted-foreground));
text-transform: uppercase;
letter-spacing: 0.03em;
}
.info-item span {
font-size: 0.875rem;
color: hsl(var(--foreground));
}
/* Status badge inside info-item should not stretch */
.info-item .status-badge {
display: inline-flex;
width: auto;
}
/* Excluded Providers List */
.excluded-providers-list {
display: flex;
flex-wrap: wrap;
gap: 0.5rem;
}
.excluded-providers-list .tag {
display: inline-flex;
align-items: center;
padding: 0.25rem 0.625rem;
font-size: 0.75rem;
font-weight: 500;
background: hsl(var(--muted) / 0.5);
border: 1px solid hsl(var(--border));
border-radius: 0.375rem;
color: hsl(var(--muted-foreground));
}
/* Provider Actions in Detail Header */
.provider-actions {
display: flex;
align-items: center;
gap: 0.5rem;
}
@media (max-width: 640px) {
.info-grid {
grid-template-columns: 1fr;
}
.provider-detail .provider-detail-header {
flex-direction: column;
gap: 1rem;
}
.provider-actions {
width: 100%;
justify-content: flex-end;
}
}

View File

@@ -10,6 +10,7 @@ let defaultCliTool = 'gemini';
let promptConcatFormat = localStorage.getItem('ccw-prompt-format') || 'plain'; // plain, yaml, json
let cliToolsConfig = {}; // CLI tools enable/disable config
let apiEndpoints = []; // API endpoints from LiteLLM config
let cliSettingsEndpoints = []; // CLI Settings endpoints (for Claude wrapper)
// Smart Context settings
let smartContextEnabled = localStorage.getItem('ccw-smart-context') === 'true';
@@ -43,10 +44,11 @@ async function loadAllStatuses() {
semanticStatus = data.semantic || { available: false };
ccwInstallStatus = data.ccwInstall || { installed: true, workflowsInstalled: true, missingFiles: [], installPath: '' };
// Load CLI tools config and API endpoints
// Load CLI tools config, API endpoints, and CLI Settings
await Promise.all([
loadCliToolsConfig(),
loadApiEndpoints()
loadApiEndpoints(),
loadCliSettingsEndpoints()
]);
// Update badges
@@ -285,6 +287,22 @@ async function loadApiEndpoints() {
}
}
/**
* Load CLI Settings endpoints (Claude wrapper configurations)
*/
async function loadCliSettingsEndpoints() {
try {
const response = await fetch('/api/cli/settings');
if (!response.ok) return [];
const data = await response.json();
cliSettingsEndpoints = data.endpoints || [];
return cliSettingsEndpoints;
} catch (err) {
console.error('Failed to load CLI settings endpoints:', err);
return [];
}
}
// ========== Badge Update ==========
function updateCliBadge() {
const badge = document.getElementById('badgeCliTools');
@@ -355,6 +373,52 @@ function renderCliStatus() {
const isEnabled = config.enabled !== false;
const canSetDefault = isAvailable && isEnabled && !isDefault;
// Special handling for Claude: show CLI Settings info
const isClaude = tool === 'claude';
const enabledCliSettings = isClaude ? cliSettingsEndpoints.filter(ep => ep.enabled) : [];
const hasCliSettings = enabledCliSettings.length > 0;
// Build CLI Settings badge for Claude
let cliSettingsBadge = '';
if (isClaude && hasCliSettings) {
cliSettingsBadge = `<span class="cli-tool-badge cli-settings-badge" title="${enabledCliSettings.length} endpoint(s) configured">${enabledCliSettings.length} Endpoint${enabledCliSettings.length > 1 ? 's' : ''}</span>`;
}
// Build CLI Settings info for Claude
let cliSettingsInfo = '';
if (isClaude) {
if (hasCliSettings) {
const epNames = enabledCliSettings.slice(0, 2).map(ep => ep.name).join(', ');
const moreCount = enabledCliSettings.length > 2 ? ` +${enabledCliSettings.length - 2}` : '';
cliSettingsInfo = `
<div class="cli-settings-info mt-2 p-2 rounded bg-muted/50 text-xs">
<div class="flex items-center gap-1 text-muted-foreground mb-1">
<i data-lucide="settings-2" class="w-3 h-3"></i>
<span>CLI Wrapper Endpoints:</span>
</div>
<div class="text-foreground font-medium">${epNames}${moreCount}</div>
<a href="#" onclick="navigateToApiSettings('cli-settings'); return false;" class="text-primary hover:underline mt-1 inline-flex items-center gap-1">
<i data-lucide="external-link" class="w-3 h-3"></i>
Configure
</a>
</div>
`;
} else {
cliSettingsInfo = `
<div class="cli-settings-info mt-2 p-2 rounded bg-muted/30 text-xs">
<div class="flex items-center gap-1 text-muted-foreground">
<i data-lucide="info" class="w-3 h-3"></i>
<span>No CLI wrapper configured</span>
</div>
<a href="#" onclick="navigateToApiSettings('cli-settings'); return false;" class="text-primary hover:underline mt-1 inline-flex items-center gap-1">
<i data-lucide="plus" class="w-3 h-3"></i>
Add Endpoint
</a>
</div>
`;
}
}
return `
<div class="cli-tool-card tool-${tool} ${isAvailable ? 'available' : 'unavailable'} ${!isEnabled ? 'disabled' : ''}">
<div class="cli-tool-header">
@@ -362,6 +426,7 @@ function renderCliStatus() {
<span class="cli-tool-name">${tool.charAt(0).toUpperCase() + tool.slice(1)}</span>
${isDefault ? '<span class="cli-tool-badge">Default</span>' : ''}
${!isEnabled && isAvailable ? '<span class="cli-tool-badge-disabled">Disabled</span>' : ''}
${cliSettingsBadge}
</div>
<div class="cli-tool-desc text-xs text-muted-foreground mt-1">
${toolDescriptions[tool]}
@@ -376,6 +441,7 @@ function renderCliStatus() {
}
</div>
</div>
${cliSettingsInfo}
<div class="cli-tool-actions mt-3 flex gap-2">
${isAvailable ? (isEnabled
? `<button class="btn-sm btn-outline-warning flex items-center gap-1" onclick="toggleCliTool('${tool}', false)">
@@ -1323,3 +1389,39 @@ async function startSemanticInstall() {
}
}
// ========== Navigation ==========
/**
* Navigate to API Settings page with optional section
* @param {string} section - Target section: 'cli-settings', 'providers', 'endpoints'
*/
function navigateToApiSettings(section) {
// Try to switch to API Settings view
if (typeof switchView === 'function') {
switchView('api-settings');
} else if (window.switchView) {
window.switchView('api-settings');
}
// After view switch, select the target section
setTimeout(() => {
if (section === 'cli-settings') {
// Click CLI Settings tab if exists
const cliSettingsTab = document.querySelector('[data-section="cli-settings"]');
if (cliSettingsTab) {
cliSettingsTab.click();
} else {
// Fallback: try to find and click by text content
const tabs = document.querySelectorAll('.api-settings-sidebar .sidebar-item, .api-settings-tabs .tab-btn');
tabs.forEach(tab => {
if (tab.textContent.includes('CLI') || tab.textContent.includes('Wrapper')) {
tab.click();
}
});
}
}
}, 100);
}
// Export navigation function
window.navigateToApiSettings = navigateToApiSettings;

View File

@@ -27,6 +27,9 @@ let poolDiscoveredProviders = {};
// CLI Settings state
let cliSettingsData = null;
let selectedCliSettingsId = null;
let cliConfigMode = 'provider'; // 'provider' | 'direct'
let isAddingCliSettings = false;
let editingCliSettingsId = null;
// Cache for ccw-litellm status (frontend cache with TTL)
let ccwLitellmStatusCache = null;
@@ -3711,14 +3714,21 @@ function renderCliSettingsList() {
var html = '';
endpoints.forEach(function(endpoint) {
var isSelected = endpoint.id === selectedCliSettingsId;
html += '<div class="provider-item' + (isSelected ? ' selected' : '') + '" onclick="selectCliSettings(\'' + endpoint.id + '\')">' +
var isEditing = endpoint.id === editingCliSettingsId;
var settings = endpoint.settings || {};
var configMode = settings.configMode || (settings.providerId ? 'provider' : 'direct');
var configIcon = configMode === 'provider' ? 'link' : 'key';
html += '<div class="provider-item' + (isSelected || isEditing ? ' selected' : '') + '" onclick="selectCliSettings(\'' + endpoint.id + '\')">' +
'<div class="provider-item-content">' +
'<div class="provider-icon">' +
'<i data-lucide="settings"></i>' +
'<i data-lucide="' + configIcon + '"></i>' +
'</div>' +
'<div class="provider-info">' +
'<div class="provider-name">' + escapeHtml(endpoint.name) + '</div>' +
'<div class="provider-type">' + (endpoint.settings.model || 'sonnet') + '</div>' +
'<div class="provider-type" style="font-size: 0.75rem; color: var(--text-muted);">' +
(configMode === 'provider' ? 'Provider' : 'Direct') +
'</div>' +
'</div>' +
'</div>' +
'<div class="provider-status' + (endpoint.enabled ? ' enabled' : ' disabled') + '">' +
@@ -3759,10 +3769,45 @@ function renderCliSettingsDetail(endpointId) {
var settings = endpoint.settings || {};
var env = settings.env || {};
var configMode = settings.configMode || (settings.providerId ? 'provider' : 'direct');
var configModeLabel = configMode === 'provider' ? (t('apiSettings.providerBinding') || 'Provider Binding') : (t('apiSettings.directConfig') || 'Direct Configuration');
// Build model config display
var modelConfigHtml = '';
if (env.ANTHROPIC_MODEL || env.ANTHROPIC_DEFAULT_HAIKU_MODEL || env.ANTHROPIC_DEFAULT_SONNET_MODEL || env.ANTHROPIC_DEFAULT_OPUS_MODEL) {
modelConfigHtml =
'<div class="detail-section">' +
'<h3>' + (t('apiSettings.modelConfig') || 'Model Configuration') + '</h3>' +
'<div class="detail-grid">' +
'<div class="detail-item">' +
'<label>ANTHROPIC_MODEL</label>' +
'<span class="mono">' + escapeHtml(env.ANTHROPIC_MODEL || '-') + '</span>' +
'</div>' +
'<div class="detail-item">' +
'<label>ANTHROPIC_DEFAULT_HAIKU_MODEL</label>' +
'<span class="mono">' + escapeHtml(env.ANTHROPIC_DEFAULT_HAIKU_MODEL || '-') + '</span>' +
'</div>' +
'<div class="detail-item">' +
'<label>ANTHROPIC_DEFAULT_SONNET_MODEL</label>' +
'<span class="mono">' + escapeHtml(env.ANTHROPIC_DEFAULT_SONNET_MODEL || '-') + '</span>' +
'</div>' +
'<div class="detail-item">' +
'<label>ANTHROPIC_DEFAULT_OPUS_MODEL</label>' +
'<span class="mono">' + escapeHtml(env.ANTHROPIC_DEFAULT_OPUS_MODEL || '-') + '</span>' +
'</div>' +
'</div>' +
'</div>';
}
container.innerHTML =
'<div class="provider-detail-header">' +
'<div>' +
'<h2>' + escapeHtml(endpoint.name) + '</h2>' +
'<span class="config-source-badge ' + configMode + '" style="margin-top: 0.5rem; display: inline-block;">' +
(configMode === 'provider' ? '<i data-lucide="link" style="width: 12px; height: 12px;"></i> ' : '<i data-lucide="key" style="width: 12px; height: 12px;"></i> ') +
configModeLabel +
'</span>' +
'</div>' +
'<div class="provider-detail-actions">' +
'<button class="btn btn-ghost" onclick="editCliSettings(\'' + endpoint.id + '\')" title="' + t('common.edit') + '">' +
'<i data-lucide="edit-2"></i>' +
@@ -3781,15 +3826,12 @@ function renderCliSettingsDetail(endpointId) {
'<span class="mono">' + escapeHtml(endpoint.id) + '</span>' +
'</div>' +
'<div class="detail-item">' +
'<label>' + t('apiSettings.model') + '</label>' +
'<span>' + escapeHtml(settings.model || 'sonnet') + '</span>' +
'</div>' +
'<div class="detail-item">' +
'<label>' + t('apiSettings.status') + '</label>' +
'<span class="status-badge ' + (endpoint.enabled ? 'enabled' : 'disabled') + '">' +
(endpoint.enabled ? t('common.enabled') : t('common.disabled')) +
'</span>' +
'</div>' +
(endpoint.description ? '<div class="detail-item detail-item-full"><label>' + t('apiSettings.description') + '</label><span>' + escapeHtml(endpoint.description) + '</span></div>' : '') +
'</div>' +
'</div>' +
'<div class="detail-section">' +
@@ -3805,6 +3847,7 @@ function renderCliSettingsDetail(endpointId) {
'</div>' +
'</div>' +
'</div>' +
modelConfigHtml +
'<div class="detail-section">' +
'<h3>' + t('apiSettings.settingsFilePath') + '</h3>' +
'<div class="code-block">' +
@@ -3817,22 +3860,407 @@ function renderCliSettingsDetail(endpointId) {
}
/**
* Render CLI Settings empty state
* Render CLI Settings empty state or add form
*/
function renderCliSettingsEmptyState() {
var container = document.getElementById('provider-detail-panel');
if (!container) return;
// If adding new settings, show the form
if (isAddingCliSettings) {
renderCliSettingsForm(null);
return;
}
container.innerHTML =
'<div class="provider-empty-state">' +
'<i data-lucide="settings" class="empty-icon"></i>' +
'<h3>' + t('apiSettings.noCliSettingsSelected') + '</h3>' +
'<p>' + t('apiSettings.cliSettingsHint') + '</p>' +
'<button class="btn btn-primary" style="margin-top: 1rem;" onclick="startAddCliSettings()">' +
'<i data-lucide="plus"></i> ' + t('apiSettings.addCliSettings') +
'</button>' +
'</div>';
if (window.lucide) lucide.createIcons();
}
/**
* Start adding new CLI Settings (show form in panel)
*/
function startAddCliSettings() {
isAddingCliSettings = true;
selectedCliSettingsId = null;
editingCliSettingsId = null;
cliConfigMode = 'provider';
renderCliSettingsForm(null);
renderCliSettingsList();
}
/**
* Cancel adding/editing CLI Settings
*/
function cancelCliSettingsForm() {
isAddingCliSettings = false;
editingCliSettingsId = null;
// Re-render detail panel
if (selectedCliSettingsId) {
renderCliSettingsDetail(selectedCliSettingsId);
} else {
renderCliSettingsEmptyState();
}
renderCliSettingsList();
}
/**
* Render CLI Settings form in detail panel (for add or edit)
*/
function renderCliSettingsForm(existingEndpoint) {
var container = document.getElementById('provider-detail-panel');
if (!container) return;
var isEdit = !!existingEndpoint;
var settings = existingEndpoint ? existingEndpoint.settings : { env: {}, model: '' };
var env = settings.env || {};
// Determine initial config mode for editing
if (isEdit) {
// If settings has configMode, use it; otherwise detect based on providerId
cliConfigMode = settings.configMode || (settings.providerId ? 'provider' : 'direct');
}
// Build mode toggle
var modeToggleHtml =
'<div class="config-mode-toggle">' +
'<button type="button" class="config-mode-btn' + (cliConfigMode === 'provider' ? ' active' : '') + '" data-mode="provider" onclick="switchCliConfigMode(\'provider\')">' +
'<i data-lucide="link"></i> ' + (t('apiSettings.providerBinding') || 'Provider Binding') +
'</button>' +
'<button type="button" class="config-mode-btn' + (cliConfigMode === 'direct' ? ' active' : '') + '" data-mode="direct" onclick="switchCliConfigMode(\'direct\')">' +
'<i data-lucide="key"></i> ' + (t('apiSettings.directConfig') || 'Direct Configuration') +
'</button>' +
'</div>';
// Common fields
var commonFieldsHtml =
'<div class="form-group">' +
'<label for="cli-settings-name">' + t('apiSettings.endpointName') + ' *</label>' +
'<input type="text" id="cli-settings-name" class="form-control" value="' + escapeHtml(existingEndpoint ? existingEndpoint.name : '') + '" placeholder="My Claude Endpoint" required />' +
'</div>' +
'<div class="form-group">' +
'<label for="cli-settings-description">' + t('apiSettings.description') + '</label>' +
'<input type="text" id="cli-settings-description" class="form-control" value="' + escapeHtml(existingEndpoint ? (existingEndpoint.description || '') : '') + '" placeholder="Optional description" />' +
'</div>';
// Mode-specific form content container
var formContentHtml = '<div id="cli-config-mode-content"></div>';
// Enabled toggle
var enabledHtml =
'<div class="form-group" style="margin-top: 1rem;">' +
'<label class="checkbox-label">' +
'<input type="checkbox" id="cli-settings-enabled"' + (existingEndpoint ? (existingEndpoint.enabled ? ' checked' : '') : ' checked') + ' />' +
' ' + t('common.enabled') +
'</label>' +
'</div>';
// Action buttons
var actionsHtml =
'<div class="form-actions" style="margin-top: 1.5rem; display: flex; gap: 0.75rem; justify-content: flex-end;">' +
'<button type="button" class="btn btn-secondary" onclick="cancelCliSettingsForm()">' + t('common.cancel') + '</button>' +
'<button type="button" class="btn btn-primary" onclick="submitCliSettingsForm()">' +
'<i data-lucide="save"></i> ' + (isEdit ? t('common.save') : t('common.create')) +
'</button>' +
'</div>';
container.innerHTML =
'<div class="tool-detail-header">' +
'<h3><i data-lucide="settings"></i> ' + (isEdit ? t('apiSettings.editCliSettings') : t('apiSettings.addCliSettings')) + '</h3>' +
'</div>' +
'<div class="claude-config-form">' +
(isEdit ? '<input type="hidden" id="cli-settings-id" value="' + existingEndpoint.id + '">' : '') +
'<input type="hidden" id="cli-config-mode" value="' + cliConfigMode + '">' +
modeToggleHtml +
commonFieldsHtml +
formContentHtml +
enabledHtml +
actionsHtml +
'</div>';
if (window.lucide) lucide.createIcons();
// Render mode-specific content
renderCliConfigModeContent(existingEndpoint);
}
/**
* Switch CLI config mode
*/
function switchCliConfigMode(mode) {
cliConfigMode = mode;
// Update hidden input
var modeInput = document.getElementById('cli-config-mode');
if (modeInput) modeInput.value = mode;
// Update toggle buttons
document.querySelectorAll('.config-mode-btn').forEach(function(btn) {
btn.classList.toggle('active', btn.dataset.mode === mode);
});
// Re-render mode content while preserving form data
var existingEndpoint = null;
var idInput = document.getElementById('cli-settings-id');
if (idInput && idInput.value && cliSettingsData && cliSettingsData.endpoints) {
existingEndpoint = cliSettingsData.endpoints.find(function(e) { return e.id === idInput.value; });
}
renderCliConfigModeContent(existingEndpoint);
}
/**
* Render CLI config mode-specific content
*/
function renderCliConfigModeContent(existingEndpoint) {
var container = document.getElementById('cli-config-mode-content');
if (!container) return;
var settings = existingEndpoint ? existingEndpoint.settings : { env: {}, model: '' };
var env = settings.env || {};
if (cliConfigMode === 'provider') {
renderProviderModeContent(container, settings);
} else {
renderDirectModeContent(container, env);
}
if (window.lucide) lucide.createIcons();
}
/**
* Render Provider Binding mode content
*/
function renderProviderModeContent(container, settings) {
var providers = getAvailableAnthropicProviders();
var hasProviders = providers.length > 0;
var selectedProviderId = settings.providerId || '';
var providerOptionsHtml = buildCliProviderOptions(selectedProviderId);
var env = settings.env || {};
var noProvidersWarning = !hasProviders ?
'<div class="info-message" style="margin-bottom: 1rem; padding: 0.75rem; background: hsl(var(--warning) / 0.1); border-radius: 0.375rem; display: flex; align-items: center; gap: 0.5rem;">' +
'<i data-lucide="alert-circle" style="width: 16px; height: 16px; color: hsl(var(--warning));"></i>' +
'<span style="font-size: 0.8125rem;">' + (t('apiSettings.noAnthropicProviders') || 'No Anthropic providers configured. Please add a provider first.') + '</span>' +
'</div>' : '';
container.innerHTML = noProvidersWarning +
'<div class="form-group">' +
'<label for="cli-settings-provider">' + t('apiSettings.provider') + ' *</label>' +
'<select id="cli-settings-provider" class="form-control" onchange="onCliProviderChange()"' + (!hasProviders ? ' disabled' : '') + '>' +
providerOptionsHtml +
'</select>' +
'</div>' +
// Model Config Section
'<div class="model-config-section">' +
'<h4><i data-lucide="cpu"></i> ' + (t('apiSettings.modelConfig') || 'Model Configuration') + '</h4>' +
'<div class="model-config-grid">' +
'<div class="form-group">' +
'<label for="cli-model-default">ANTHROPIC_MODEL</label>' +
'<input type="text" id="cli-model-default" class="form-control" placeholder="claude-3-5-sonnet-20241022" value="' + escapeHtml(env.ANTHROPIC_MODEL || '') + '" />' +
'</div>' +
'<div class="form-group">' +
'<label for="cli-model-haiku">ANTHROPIC_DEFAULT_HAIKU_MODEL</label>' +
'<input type="text" id="cli-model-haiku" class="form-control" placeholder="claude-3-haiku-20240307" value="' + escapeHtml(env.ANTHROPIC_DEFAULT_HAIKU_MODEL || '') + '" />' +
'</div>' +
'<div class="form-group">' +
'<label for="cli-model-sonnet">ANTHROPIC_DEFAULT_SONNET_MODEL</label>' +
'<input type="text" id="cli-model-sonnet" class="form-control" placeholder="claude-3-5-sonnet-20241022" value="' + escapeHtml(env.ANTHROPIC_DEFAULT_SONNET_MODEL || '') + '" />' +
'</div>' +
'<div class="form-group">' +
'<label for="cli-model-opus">ANTHROPIC_DEFAULT_OPUS_MODEL</label>' +
'<input type="text" id="cli-model-opus" class="form-control" placeholder="claude-3-opus-20240229" value="' + escapeHtml(env.ANTHROPIC_DEFAULT_OPUS_MODEL || '') + '" />' +
'</div>' +
'</div>' +
'</div>';
}
/**
* Render Direct Configuration mode content
*/
function renderDirectModeContent(container, env) {
container.innerHTML =
'<div class="form-group">' +
'<label for="cli-auth-token">ANTHROPIC_AUTH_TOKEN *</label>' +
'<input type="password" id="cli-auth-token" class="form-control" placeholder="sk-ant-..." value="' + escapeHtml(env.ANTHROPIC_AUTH_TOKEN || '') + '" />' +
'</div>' +
'<div class="form-group">' +
'<label for="cli-base-url">ANTHROPIC_BASE_URL</label>' +
'<input type="text" id="cli-base-url" class="form-control" placeholder="https://api.anthropic.com (optional)" value="' + escapeHtml(env.ANTHROPIC_BASE_URL || '') + '" />' +
'</div>' +
// Model Config Section
'<div class="model-config-section">' +
'<h4><i data-lucide="cpu"></i> ' + (t('apiSettings.modelConfig') || 'Model Configuration') + '</h4>' +
'<div class="model-config-grid">' +
'<div class="form-group">' +
'<label for="cli-model-default">ANTHROPIC_MODEL</label>' +
'<input type="text" id="cli-model-default" class="form-control" placeholder="claude-3-5-sonnet-20241022" value="' + escapeHtml(env.ANTHROPIC_MODEL || '') + '" />' +
'</div>' +
'<div class="form-group">' +
'<label for="cli-model-haiku">ANTHROPIC_DEFAULT_HAIKU_MODEL</label>' +
'<input type="text" id="cli-model-haiku" class="form-control" placeholder="claude-3-haiku-20240307" value="' + escapeHtml(env.ANTHROPIC_DEFAULT_HAIKU_MODEL || '') + '" />' +
'</div>' +
'<div class="form-group">' +
'<label for="cli-model-sonnet">ANTHROPIC_DEFAULT_SONNET_MODEL</label>' +
'<input type="text" id="cli-model-sonnet" class="form-control" placeholder="claude-3-5-sonnet-20241022" value="' + escapeHtml(env.ANTHROPIC_DEFAULT_SONNET_MODEL || '') + '" />' +
'</div>' +
'<div class="form-group">' +
'<label for="cli-model-opus">ANTHROPIC_DEFAULT_OPUS_MODEL</label>' +
'<input type="text" id="cli-model-opus" class="form-control" placeholder="claude-3-opus-20240229" value="' + escapeHtml(env.ANTHROPIC_DEFAULT_OPUS_MODEL || '') + '" />' +
'</div>' +
'</div>' +
'</div>';
}
/**
* Submit CLI Settings Form (handles both Provider and Direct modes)
*/
async function submitCliSettingsForm() {
// Get common fields
var name = document.getElementById('cli-settings-name').value.trim();
var description = document.getElementById('cli-settings-description').value.trim();
var enabled = document.getElementById('cli-settings-enabled').checked;
var idInput = document.getElementById('cli-settings-id');
var id = idInput ? idInput.value : null;
var configMode = cliConfigMode;
// Get model configuration fields
var anthropicModel = document.getElementById('cli-model-default').value.trim();
var haikuModel = document.getElementById('cli-model-haiku').value.trim();
var sonnetModel = document.getElementById('cli-model-sonnet').value.trim();
var opusModel = document.getElementById('cli-model-opus').value.trim();
// Validate common fields
if (!name) {
showRefreshToast(t('apiSettings.nameRequired'), 'error');
return;
}
var data = {
name: name,
description: description,
enabled: enabled,
settings: {
env: {
DISABLE_AUTOUPDATER: '1'
},
configMode: configMode,
includeCoAuthoredBy: false
}
};
// Mode-specific handling
if (configMode === 'provider') {
// Provider binding mode
var providerId = document.getElementById('cli-settings-provider').value;
if (!providerId) {
showRefreshToast(t('apiSettings.providerRequired'), 'error');
return;
}
// Get provider credentials
var providers = getAvailableAnthropicProviders();
var provider = providers.find(function(p) { return p.id === providerId; });
if (!provider) {
showRefreshToast(t('apiSettings.providerNotFound'), 'error');
return;
}
// Copy provider credentials to env
data.settings.env.ANTHROPIC_AUTH_TOKEN = provider.apiKey || '';
if (provider.apiBase) {
data.settings.env.ANTHROPIC_BASE_URL = provider.apiBase;
}
data.settings.providerId = providerId;
} else {
// Direct configuration mode
var authToken = document.getElementById('cli-auth-token').value.trim();
var baseUrl = document.getElementById('cli-base-url').value.trim();
if (!authToken) {
showRefreshToast(t('apiSettings.authTokenRequired') || 'Auth token is required', 'error');
return;
}
data.settings.env.ANTHROPIC_AUTH_TOKEN = authToken;
if (baseUrl) {
data.settings.env.ANTHROPIC_BASE_URL = baseUrl;
}
}
// Add model configuration
if (anthropicModel) {
data.settings.env.ANTHROPIC_MODEL = anthropicModel;
}
if (haikuModel) {
data.settings.env.ANTHROPIC_DEFAULT_HAIKU_MODEL = haikuModel;
}
if (sonnetModel) {
data.settings.env.ANTHROPIC_DEFAULT_SONNET_MODEL = sonnetModel;
}
if (opusModel) {
data.settings.env.ANTHROPIC_DEFAULT_OPUS_MODEL = opusModel;
}
// Set ID if editing
if (id) {
data.id = id;
}
// Save endpoint
var result = await saveCliSettingsEndpoint(data);
if (result && result.success) {
// Reset form state
isAddingCliSettings = false;
editingCliSettingsId = null;
// Select the newly created/updated endpoint
if (result.endpoint && result.endpoint.id) {
selectedCliSettingsId = result.endpoint.id;
}
// Refresh view
await loadCliSettings();
renderCliSettingsList();
if (selectedCliSettingsId) {
renderCliSettingsDetail(selectedCliSettingsId);
}
}
}
/**
* Edit CLI Settings in panel (new panel-based approach)
*/
function editCliSettingsInPanel(endpointId) {
var endpoint = null;
if (cliSettingsData && cliSettingsData.endpoints) {
endpoint = cliSettingsData.endpoints.find(function(e) { return e.id === endpointId; });
}
if (endpoint) {
isAddingCliSettings = false;
editingCliSettingsId = endpointId;
// Determine config mode from existing settings
var settings = endpoint.settings || {};
cliConfigMode = settings.configMode || (settings.providerId ? 'provider' : 'direct');
renderCliSettingsForm(endpoint);
renderCliSettingsList();
}
}
/**
* Get available Anthropic providers
*/
@@ -3964,16 +4392,10 @@ function showAddCliSettingsModal(existingEndpoint) {
}
/**
* Edit CLI Settings
* Edit CLI Settings (uses panel-based form)
*/
function editCliSettings(endpointId) {
var endpoint = null;
if (cliSettingsData && cliSettingsData.endpoints) {
endpoint = cliSettingsData.endpoints.find(function(e) { return e.id === endpointId; });
}
if (endpoint) {
showAddCliSettingsModal(endpoint);
}
editCliSettingsInPanel(endpointId);
}
/**
@@ -4381,8 +4803,150 @@ async function submitModelPool(event) {
* Edit model pool
*/
function editModelPool(poolId) {
// TODO: Implement edit modal
showRefreshToast('Edit functionality coming soon', 'info');
var pool = modelPools.find(function(p) { return p.id === poolId; });
if (!pool) {
showRefreshToast(t('common.error') + ': Pool not found', 'error');
return;
}
var modalHtml = '<div class="generic-modal-overlay active" id="edit-pool-modal">' +
'<div class="generic-modal" style="max-width: 600px;">' +
'<div class="generic-modal-header">' +
'<h3 class="generic-modal-title">' + t('apiSettings.editModelPool') + '</h3>' +
'<button class="generic-modal-close" onclick="closeEditPoolModal()">&times;</button>' +
'</div>' +
'<div class="generic-modal-body">' +
'<form id="edit-pool-form" class="api-settings-form" onsubmit="submitEditModelPool(event, \'' + poolId + '\')">' +
'<div class="form-group">' +
'<label>' + t('apiSettings.modelType') + '</label>' +
'<input type="text" class="cli-input" value="' + (pool.modelType === 'embedding' ? 'Embedding' : pool.modelType === 'llm' ? 'LLM' : 'Reranker') + '" disabled />' +
'</div>' +
'<div class="form-group">' +
'<label>' + t('apiSettings.poolName') + '</label>' +
'<input type="text" id="edit-pool-name" class="cli-input" value="' + escapeHtml(pool.name || '') + '" placeholder="e.g., Primary Embedding Pool" />' +
'</div>' +
'<div class="form-group">' +
'<label>' + t('apiSettings.targetModel') + '</label>' +
'<input type="text" class="cli-input" value="' + escapeHtml(pool.targetModel) + '" disabled />' +
'</div>' +
'<div class="form-group">' +
'<label>' + t('apiSettings.strategy') + ' *</label>' +
'<select id="edit-pool-strategy" class="cli-input" required>' +
'<option value="round_robin"' + (pool.strategy === 'round_robin' ? ' selected' : '') + '>Round Robin</option>' +
'<option value="latency_aware"' + (pool.strategy === 'latency_aware' ? ' selected' : '') + '>Latency Aware</option>' +
'<option value="weighted_random"' + (pool.strategy === 'weighted_random' ? ' selected' : '') + '>Weighted Random</option>' +
'</select>' +
'</div>' +
'<div class="form-group">' +
'<label>' + t('apiSettings.cooldown') + ' (seconds)</label>' +
'<input type="number" id="edit-pool-cooldown" class="cli-input" value="' + (pool.defaultCooldown || 60) + '" min="0" />' +
'</div>' +
'<div class="form-group">' +
'<label>' + t('apiSettings.maxConcurrent') + '</label>' +
'<input type="number" id="edit-pool-max-concurrent" class="cli-input" value="' + (pool.defaultMaxConcurrentPerKey || 4) + '" min="1" />' +
'</div>' +
'<div class="form-group">' +
'<label>' + t('apiSettings.description') + '</label>' +
'<textarea id="edit-pool-description" class="cli-input" rows="2" placeholder="Optional description">' + escapeHtml(pool.description || '') + '</textarea>' +
'</div>' +
'<div class="form-group">' +
'<label class="checkbox-label">' +
'<input type="checkbox" id="edit-pool-enabled"' + (pool.enabled ? ' checked' : '') + ' /> ' + t('apiSettings.enablePool') +
'</label>' +
'</div>' +
'<div class="form-group">' +
'<label class="checkbox-label">' +
'<input type="checkbox" id="edit-pool-auto-discover"' + (pool.autoDiscover !== false ? ' checked' : '') + ' /> ' + t('apiSettings.autoDiscoverProviders') +
'</label>' +
'</div>' +
'</form>' +
'</div>' +
'<div class="generic-modal-footer">' +
'<button class="btn btn-secondary" onclick="closeEditPoolModal()">' + t('common.cancel') + '</button>' +
'<button class="btn btn-primary" onclick="document.getElementById(\'edit-pool-form\').requestSubmit()">' + t('common.save') + '</button>' +
'</div>' +
'</div>' +
'</div>';
document.body.insertAdjacentHTML('beforeend', modalHtml);
if (window.lucide) lucide.createIcons();
}
/**
* Close edit pool modal
*/
function closeEditPoolModal() {
var modal = document.getElementById('edit-pool-modal');
if (modal) modal.remove();
}
/**
* Submit edit model pool form
*/
async function submitEditModelPool(event, poolId) {
event.preventDefault();
var pool = modelPools.find(function(p) { return p.id === poolId; });
if (!pool) {
showRefreshToast(t('common.error') + ': Pool not found', 'error');
return;
}
var name = document.getElementById('edit-pool-name').value.trim();
var strategy = document.getElementById('edit-pool-strategy').value;
var cooldown = parseInt(document.getElementById('edit-pool-cooldown').value || '60');
var maxConcurrent = parseInt(document.getElementById('edit-pool-max-concurrent').value || '4');
var description = document.getElementById('edit-pool-description').value.trim();
var enabled = document.getElementById('edit-pool-enabled').checked;
var autoDiscover = document.getElementById('edit-pool-auto-discover').checked;
var poolData = {
id: poolId,
modelType: pool.modelType,
enabled: enabled,
name: name || pool.targetModel,
targetModel: pool.targetModel,
strategy: strategy,
autoDiscover: autoDiscover,
defaultCooldown: cooldown,
defaultMaxConcurrentPerKey: maxConcurrent,
description: description || undefined,
excludedProviderIds: pool.excludedProviderIds || []
};
try {
await initCsrfToken();
var response = await csrfFetch('/api/litellm-api/model-pools/' + poolId, {
method: 'PUT',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(poolData)
});
if (!response.ok) {
var err = await response.json();
throw new Error(err.error || 'Failed to update pool');
}
showRefreshToast(t('apiSettings.poolUpdated'), 'success');
closeEditPoolModal();
// Reload pools and refresh view
await loadModelPools();
renderModelPoolsList();
renderModelPoolDetail(poolId);
} catch (err) {
showRefreshToast(t('common.error') + ': ' + err.message, 'error');
}
}
/**
@@ -4426,6 +4990,8 @@ window.closeAddPoolModal = closeAddPoolModal;
window.onPoolModelTypeChange = onPoolModelTypeChange;
window.submitModelPool = submitModelPool;
window.editModelPool = editModelPool;
window.closeEditPoolModal = closeEditPoolModal;
window.submitEditModelPool = submitEditModelPool;
window.deleteModelPool = deleteModelPool;
// Make CLI Settings functions globally accessible
@@ -4441,6 +5007,12 @@ window.editCliSettings = editCliSettings;
window.closeCliSettingsModal = closeCliSettingsModal;
window.submitCliSettings = submitCliSettings;
window.onCliProviderChange = onCliProviderChange;
// New panel-based CLI Settings functions
window.startAddCliSettings = startAddCliSettings;
window.cancelCliSettingsForm = cancelCliSettingsForm;
window.switchCliConfigMode = switchCliConfigMode;
window.submitCliSettingsForm = submitCliSettingsForm;
window.editCliSettingsInPanel = editCliSettingsInPanel;
// ========== Utility Functions ==========

View File

@@ -1,6 +1,75 @@
// CodexLens Manager - Configuration, Model Management, and Semantic Dependencies
// Extracted from cli-manager.js for better maintainability
// ============================================================
// CACHE MANAGEMENT
// ============================================================
// Cache TTL in milliseconds (30 seconds default)
const CODEXLENS_CACHE_TTL = 30000;
// Cache storage for CodexLens data
const codexLensCache = {
workspaceStatus: { data: null, timestamp: 0 },
config: { data: null, timestamp: 0 },
status: { data: null, timestamp: 0 },
env: { data: null, timestamp: 0 },
models: { data: null, timestamp: 0 },
rerankerModels: { data: null, timestamp: 0 },
semanticStatus: { data: null, timestamp: 0 },
gpuList: { data: null, timestamp: 0 },
indexes: { data: null, timestamp: 0 }
};
/**
* Check if cache is valid (not expired)
* @param {string} key - Cache key
* @param {number} ttl - Optional custom TTL
* @returns {boolean}
*/
function isCacheValid(key, ttl = CODEXLENS_CACHE_TTL) {
const cache = codexLensCache[key];
if (!cache || !cache.data) return false;
return (Date.now() - cache.timestamp) < ttl;
}
/**
* Get cached data
* @param {string} key - Cache key
* @returns {*} Cached data or null
*/
function getCachedData(key) {
return codexLensCache[key]?.data || null;
}
/**
* Set cache data
* @param {string} key - Cache key
* @param {*} data - Data to cache
*/
function setCacheData(key, data) {
if (codexLensCache[key]) {
codexLensCache[key].data = data;
codexLensCache[key].timestamp = Date.now();
}
}
/**
* Invalidate specific cache or all caches
* @param {string} key - Cache key (optional, if not provided clears all)
*/
function invalidateCache(key) {
if (key && codexLensCache[key]) {
codexLensCache[key].data = null;
codexLensCache[key].timestamp = 0;
} else if (!key) {
Object.keys(codexLensCache).forEach(function(k) {
codexLensCache[k].data = null;
codexLensCache[k].timestamp = 0;
});
}
}
// ============================================================
// UTILITY FUNCTIONS
// ============================================================
@@ -25,8 +94,9 @@ function escapeHtml(str) {
/**
* Refresh workspace index status (FTS and Vector coverage)
* Updates both the detailed panel (if exists) and header badges
* @param {boolean} forceRefresh - Force refresh, bypass cache
*/
async function refreshWorkspaceIndexStatus() {
async function refreshWorkspaceIndexStatus(forceRefresh) {
var container = document.getElementById('workspaceIndexStatusContent');
var headerFtsEl = document.getElementById('headerFtsPercent');
var headerVectorEl = document.getElementById('headerVectorPercent');
@@ -34,6 +104,13 @@ async function refreshWorkspaceIndexStatus() {
// If neither container nor header elements exist, nothing to update
if (!container && !headerFtsEl) return;
// Check cache first (unless force refresh)
if (!forceRefresh && isCacheValid('workspaceStatus')) {
var cachedResult = getCachedData('workspaceStatus');
updateWorkspaceStatusUI(cachedResult, container, headerFtsEl, headerVectorEl);
return;
}
// Show loading state in container
if (container) {
container.innerHTML = '<div class="text-xs text-muted-foreground text-center py-2">' +
@@ -46,106 +123,10 @@ async function refreshWorkspaceIndexStatus() {
var response = await fetch('/api/codexlens/workspace-status');
var result = await response.json();
if (result.success) {
var ftsPercent = result.hasIndex ? (result.fts.percent || 0) : 0;
var vectorPercent = result.hasIndex ? (result.vector.percent || 0) : 0;
// Cache the result
setCacheData('workspaceStatus', result);
// Update header badges (always update if elements exist)
if (headerFtsEl) {
headerFtsEl.textContent = ftsPercent + '%';
headerFtsEl.className = 'text-sm font-medium ' +
(ftsPercent >= 100 ? 'text-success' : (ftsPercent > 0 ? 'text-blue-500' : 'text-muted-foreground'));
}
if (headerVectorEl) {
headerVectorEl.textContent = vectorPercent.toFixed(1) + '%';
headerVectorEl.className = 'text-sm font-medium ' +
(vectorPercent >= 100 ? 'text-success' : (vectorPercent >= 50 ? 'text-purple-500' : (vectorPercent > 0 ? 'text-purple-400' : 'text-muted-foreground')));
}
// Update detailed container (if exists)
if (container) {
var html = '';
if (!result.hasIndex) {
// No index for current workspace
html = '<div class="text-center py-3">' +
'<div class="text-sm text-muted-foreground mb-2">' +
'<i data-lucide="alert-circle" class="w-4 h-4 inline mr-1"></i> ' +
(t('codexlens.noIndexFound') || 'No index found for current workspace') +
'</div>' +
'<button onclick="runFtsFullIndex()" class="text-xs text-primary hover:underline">' +
(t('codexlens.createIndex') || 'Create Index') +
'</button>' +
'</div>';
} else {
// FTS Status
var ftsColor = ftsPercent >= 100 ? 'bg-success' : (ftsPercent > 0 ? 'bg-blue-500' : 'bg-muted-foreground');
var ftsTextColor = ftsPercent >= 100 ? 'text-success' : (ftsPercent > 0 ? 'text-blue-500' : 'text-muted-foreground');
html += '<div class="space-y-1">' +
'<div class="flex items-center justify-between text-xs">' +
'<span class="flex items-center gap-1.5">' +
'<i data-lucide="file-text" class="w-3.5 h-3.5 text-blue-500"></i> ' +
'<span class="font-medium">' + (t('codexlens.ftsIndex') || 'FTS Index') + '</span>' +
'</span>' +
'<span class="' + ftsTextColor + ' font-medium">' + ftsPercent + '%</span>' +
'</div>' +
'<div class="h-1.5 bg-muted rounded-full overflow-hidden">' +
'<div class="h-full ' + ftsColor + ' transition-all duration-300" style="width: ' + ftsPercent + '%"></div>' +
'</div>' +
'<div class="text-xs text-muted-foreground">' +
(result.fts.indexedFiles || 0) + ' / ' + (result.fts.totalFiles || 0) + ' ' + (t('codexlens.filesIndexed') || 'files indexed') +
'</div>' +
'</div>';
// Vector Status
var vectorColor = vectorPercent >= 100 ? 'bg-success' : (vectorPercent >= 50 ? 'bg-purple-500' : (vectorPercent > 0 ? 'bg-purple-400' : 'bg-muted-foreground'));
var vectorTextColor = vectorPercent >= 100 ? 'text-success' : (vectorPercent >= 50 ? 'text-purple-500' : (vectorPercent > 0 ? 'text-purple-400' : 'text-muted-foreground'));
html += '<div class="space-y-1 mt-3">' +
'<div class="flex items-center justify-between text-xs">' +
'<span class="flex items-center gap-1.5">' +
'<i data-lucide="brain" class="w-3.5 h-3.5 text-purple-500"></i> ' +
'<span class="font-medium">' + (t('codexlens.vectorIndex') || 'Vector Index') + '</span>' +
'</span>' +
'<span class="' + vectorTextColor + ' font-medium">' + vectorPercent.toFixed(1) + '%</span>' +
'</div>' +
'<div class="h-1.5 bg-muted rounded-full overflow-hidden">' +
'<div class="h-full ' + vectorColor + ' transition-all duration-300" style="width: ' + vectorPercent + '%"></div>' +
'</div>' +
'<div class="text-xs text-muted-foreground">' +
(result.vector.filesWithEmbeddings || 0) + ' / ' + (result.vector.totalFiles || 0) + ' ' + (t('codexlens.filesWithEmbeddings') || 'files with embeddings') +
(result.vector.totalChunks > 0 ? ' (' + result.vector.totalChunks + ' chunks)' : '') +
'</div>' +
'</div>';
// Vector search availability indicator
if (vectorPercent >= 50) {
html += '<div class="flex items-center gap-1.5 mt-2 pt-2 border-t border-border">' +
'<i data-lucide="check-circle-2" class="w-3.5 h-3.5 text-success"></i>' +
'<span class="text-xs text-success">' + (t('codexlens.vectorSearchEnabled') || 'Vector search enabled') + '</span>' +
'</div>';
} else if (vectorPercent > 0) {
html += '<div class="flex items-center gap-1.5 mt-2 pt-2 border-t border-border">' +
'<i data-lucide="alert-triangle" class="w-3.5 h-3.5 text-warning"></i>' +
'<span class="text-xs text-warning">' + (t('codexlens.vectorSearchPartial') || 'Vector search requires ≥50% coverage') + '</span>' +
'</div>';
}
}
container.innerHTML = html;
}
} else {
// Error from API
if (headerFtsEl) headerFtsEl.textContent = '--';
if (headerVectorEl) headerVectorEl.textContent = '--';
if (container) {
container.innerHTML = '<div class="text-xs text-destructive text-center py-2">' +
'<i data-lucide="alert-circle" class="w-4 h-4 inline mr-1"></i> ' +
(result.error || t('common.error') || 'Error loading status') +
'</div>';
}
}
updateWorkspaceStatusUI(result, container, headerFtsEl, headerVectorEl);
} catch (err) {
console.error('[CodexLens] Failed to load workspace status:', err);
if (headerFtsEl) headerFtsEl.textContent = '--';
@@ -161,24 +142,151 @@ async function refreshWorkspaceIndexStatus() {
if (window.lucide) lucide.createIcons();
}
/**
* Update workspace status UI with result data
* @param {Object} result - API result
* @param {HTMLElement} container - Container element
* @param {HTMLElement} headerFtsEl - FTS header element
* @param {HTMLElement} headerVectorEl - Vector header element
*/
function updateWorkspaceStatusUI(result, container, headerFtsEl, headerVectorEl) {
if (result.success) {
var ftsPercent = result.hasIndex ? (result.fts.percent || 0) : 0;
var vectorPercent = result.hasIndex ? (result.vector.percent || 0) : 0;
// Update header badges (always update if elements exist)
if (headerFtsEl) {
headerFtsEl.textContent = ftsPercent + '%';
headerFtsEl.className = 'text-sm font-medium ' +
(ftsPercent >= 100 ? 'text-success' : (ftsPercent > 0 ? 'text-blue-500' : 'text-muted-foreground'));
}
if (headerVectorEl) {
headerVectorEl.textContent = vectorPercent.toFixed(1) + '%';
headerVectorEl.className = 'text-sm font-medium ' +
(vectorPercent >= 100 ? 'text-success' : (vectorPercent >= 50 ? 'text-purple-500' : (vectorPercent > 0 ? 'text-purple-400' : 'text-muted-foreground')));
}
// Update detailed container (if exists)
if (container) {
var html = '';
if (!result.hasIndex) {
// No index for current workspace
html = '<div class="text-center py-3">' +
'<div class="text-sm text-muted-foreground mb-2">' +
'<i data-lucide="alert-circle" class="w-4 h-4 inline mr-1"></i> ' +
(t('codexlens.noIndexFound') || 'No index found for current workspace') +
'</div>' +
'<button onclick="runFtsFullIndex()" class="text-xs text-primary hover:underline">' +
(t('codexlens.createIndex') || 'Create Index') +
'</button>' +
'</div>';
} else {
// FTS Status
var ftsColor = ftsPercent >= 100 ? 'bg-success' : (ftsPercent > 0 ? 'bg-blue-500' : 'bg-muted-foreground');
var ftsTextColor = ftsPercent >= 100 ? 'text-success' : (ftsPercent > 0 ? 'text-blue-500' : 'text-muted-foreground');
html += '<div class="space-y-1">' +
'<div class="flex items-center justify-between text-xs">' +
'<span class="flex items-center gap-1.5">' +
'<i data-lucide="file-text" class="w-3.5 h-3.5 text-blue-500"></i> ' +
'<span class="font-medium">' + (t('codexlens.ftsIndex') || 'FTS Index') + '</span>' +
'</span>' +
'<span class="' + ftsTextColor + ' font-medium">' + ftsPercent + '%</span>' +
'</div>' +
'<div class="h-1.5 bg-muted rounded-full overflow-hidden">' +
'<div class="h-full ' + ftsColor + ' transition-all duration-300" style="width: ' + ftsPercent + '%"></div>' +
'</div>' +
'<div class="text-xs text-muted-foreground">' +
(result.fts.indexedFiles || 0) + ' / ' + (result.fts.totalFiles || 0) + ' ' + (t('codexlens.filesIndexed') || 'files indexed') +
'</div>' +
'</div>';
// Vector Status
var vectorColor = vectorPercent >= 100 ? 'bg-success' : (vectorPercent >= 50 ? 'bg-purple-500' : (vectorPercent > 0 ? 'bg-purple-400' : 'bg-muted-foreground'));
var vectorTextColor = vectorPercent >= 100 ? 'text-success' : (vectorPercent >= 50 ? 'text-purple-500' : (vectorPercent > 0 ? 'text-purple-400' : 'text-muted-foreground'));
html += '<div class="space-y-1 mt-3">' +
'<div class="flex items-center justify-between text-xs">' +
'<span class="flex items-center gap-1.5">' +
'<i data-lucide="brain" class="w-3.5 h-3.5 text-purple-500"></i> ' +
'<span class="font-medium">' + (t('codexlens.vectorIndex') || 'Vector Index') + '</span>' +
'</span>' +
'<span class="' + vectorTextColor + ' font-medium">' + vectorPercent.toFixed(1) + '%</span>' +
'</div>' +
'<div class="h-1.5 bg-muted rounded-full overflow-hidden">' +
'<div class="h-full ' + vectorColor + ' transition-all duration-300" style="width: ' + vectorPercent + '%"></div>' +
'</div>' +
'<div class="text-xs text-muted-foreground">' +
(result.vector.filesWithEmbeddings || 0) + ' / ' + (result.vector.totalFiles || 0) + ' ' + (t('codexlens.filesWithEmbeddings') || 'files with embeddings') +
(result.vector.totalChunks > 0 ? ' (' + result.vector.totalChunks + ' chunks)' : '') +
'</div>' +
'</div>';
// Vector search availability indicator
if (vectorPercent >= 50) {
html += '<div class="flex items-center gap-1.5 mt-2 pt-2 border-t border-border">' +
'<i data-lucide="check-circle-2" class="w-3.5 h-3.5 text-success"></i>' +
'<span class="text-xs text-success">' + (t('codexlens.vectorSearchEnabled') || 'Vector search enabled') + '</span>' +
'</div>';
} else if (vectorPercent > 0) {
html += '<div class="flex items-center gap-1.5 mt-2 pt-2 border-t border-border">' +
'<i data-lucide="alert-triangle" class="w-3.5 h-3.5 text-warning"></i>' +
'<span class="text-xs text-warning">' + (t('codexlens.vectorSearchPartial') || 'Vector search requires ≥50% coverage') + '</span>' +
'</div>';
}
}
container.innerHTML = html;
}
} else {
// Error from API
if (headerFtsEl) headerFtsEl.textContent = '--';
if (headerVectorEl) headerVectorEl.textContent = '--';
if (container) {
container.innerHTML = '<div class="text-xs text-destructive text-center py-2">' +
'<i data-lucide="alert-circle" class="w-4 h-4 inline mr-1"></i> ' +
(result.error || t('common.error') || 'Error loading status') +
'</div>';
}
}
if (window.lucide) lucide.createIcons();
}
// ============================================================
// CODEXLENS CONFIGURATION MODAL
// ============================================================
/**
* Show CodexLens configuration modal
* @param {boolean} forceRefresh - Force refresh, bypass cache
*/
async function showCodexLensConfigModal() {
async function showCodexLensConfigModal(forceRefresh) {
try {
showRefreshToast(t('codexlens.loadingConfig'), 'info');
// Check cache first for config and status
var config, status;
var usedCache = false;
// Fetch current config and status in parallel
const [configResponse, statusResponse] = await Promise.all([
fetch('/api/codexlens/config'),
fetch('/api/codexlens/status')
]);
const config = await configResponse.json();
const status = await statusResponse.json();
if (!forceRefresh && isCacheValid('config') && isCacheValid('status')) {
config = getCachedData('config');
status = getCachedData('status');
usedCache = true;
} else {
showRefreshToast(t('codexlens.loadingConfig'), 'info');
// Fetch current config and status in parallel
const [configResponse, statusResponse] = await Promise.all([
fetch('/api/codexlens/config'),
fetch('/api/codexlens/status')
]);
config = await configResponse.json();
status = await statusResponse.json();
// Cache the results
setCacheData('config', config);
setCacheData('status', status);
}
// Update window.cliToolsStatus to ensure isInstalled is correct
if (!window.cliToolsStatus) {
@@ -6642,3 +6750,13 @@ async function initIgnorePatternsCount() {
}
}
window.initIgnorePatternsCount = initIgnorePatternsCount;
// ============================================================
// CACHE MANAGEMENT - Global Exports
// ============================================================
window.invalidateCodexLensCache = invalidateCache;
window.refreshCodexLensData = async function(forceRefresh) {
invalidateCache();
await refreshWorkspaceIndexStatus(true);
showRefreshToast(t('common.refreshed') || 'Refreshed', 'success');
};