mirror of
https://github.com/catlog22/Claude-Code-Workflow.git
synced 2026-02-11 02:33:51 +08:00
feat(cli-manager): add CLI wrapper endpoints management and UI integration
- Introduced functions to load and toggle CLI wrapper endpoints from the API. - Updated the CLI manager UI to display and manage CLI wrapper endpoints. - Removed CodexLens and Semantic Search from the tools section, now managed in their dedicated pages. feat(codexlens-manager): move File Watcher card to the CodexLens Manager page - Relocated the File Watcher card from the right column to the main content area of the CodexLens Manager page. refactor(claude-cli-tools): enhance CLI tools configuration and migration - Added support for new tool types: 'cli-wrapper' and 'api-endpoint'. - Updated migration logic to handle new tool types and preserve endpoint IDs. - Deprecated previous custom endpoint handling in favor of the new structure. feat(cli-executor-core): integrate CLI settings for custom endpoint execution - Implemented execution logic for custom CLI封装 endpoints using settings files. - Enhanced error handling and output logging for CLI executions. - Updated tool identification logic to support both built-in tools and custom endpoints.
This commit is contained in:
@@ -1272,9 +1272,100 @@ select.cli-input {
|
||||
letter-spacing: 0.03em;
|
||||
}
|
||||
|
||||
/* Provider Item (used in CLI Settings list) */
|
||||
.provider-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 0.75rem;
|
||||
border-radius: 0.5rem;
|
||||
cursor: pointer;
|
||||
transition: background 0.15s ease;
|
||||
border: 1px solid transparent;
|
||||
}
|
||||
|
||||
.provider-item:hover {
|
||||
background: hsl(var(--muted) / 0.5);
|
||||
}
|
||||
|
||||
.provider-item.selected {
|
||||
background: hsl(var(--primary) / 0.1);
|
||||
border-color: hsl(var(--primary) / 0.3);
|
||||
}
|
||||
|
||||
.provider-item-content {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.75rem;
|
||||
min-width: 0;
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.provider-icon {
|
||||
width: 2rem;
|
||||
height: 2rem;
|
||||
border-radius: 0.375rem;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
flex-shrink: 0;
|
||||
background: hsl(var(--muted) / 0.5);
|
||||
color: hsl(var(--muted-foreground));
|
||||
}
|
||||
|
||||
.provider-icon i,
|
||||
.provider-icon svg {
|
||||
width: 1rem;
|
||||
height: 1rem;
|
||||
}
|
||||
|
||||
.provider-info {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.125rem;
|
||||
}
|
||||
|
||||
.provider-name {
|
||||
font-size: 0.875rem;
|
||||
font-weight: 500;
|
||||
color: hsl(var(--foreground));
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
.provider-type {
|
||||
font-size: 0.6875rem;
|
||||
color: hsl(var(--muted-foreground));
|
||||
}
|
||||
|
||||
.provider-status {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.provider-status .status-dot {
|
||||
width: 8px;
|
||||
height: 8px;
|
||||
border-radius: 50%;
|
||||
background: hsl(var(--muted-foreground));
|
||||
}
|
||||
|
||||
.provider-status.enabled .status-dot {
|
||||
background: hsl(142 76% 36%);
|
||||
}
|
||||
|
||||
.provider-status.disabled .status-dot {
|
||||
background: hsl(var(--muted-foreground) / 0.5);
|
||||
}
|
||||
|
||||
.provider-list-footer {
|
||||
padding: 1rem;
|
||||
border-top: 1px solid hsl(var(--border));
|
||||
margin-top: auto;
|
||||
}
|
||||
|
||||
.btn-full {
|
||||
@@ -1327,6 +1418,75 @@ select.cli-input {
|
||||
gap: 1.5rem;
|
||||
}
|
||||
|
||||
/* Detail Section (for CLI Settings, etc.) */
|
||||
.detail-section {
|
||||
background: hsl(var(--card));
|
||||
border: 1px solid hsl(var(--border));
|
||||
border-radius: 0.5rem;
|
||||
padding: 1rem 1.25rem;
|
||||
}
|
||||
|
||||
.detail-section h3 {
|
||||
margin: 0 0 1rem 0;
|
||||
font-size: 0.875rem;
|
||||
font-weight: 600;
|
||||
color: hsl(var(--foreground));
|
||||
}
|
||||
|
||||
.detail-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(280px, 1fr));
|
||||
gap: 1rem;
|
||||
}
|
||||
|
||||
.detail-item {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.25rem;
|
||||
}
|
||||
|
||||
.detail-item label {
|
||||
font-size: 0.75rem;
|
||||
font-weight: 500;
|
||||
color: hsl(var(--muted-foreground));
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.02em;
|
||||
}
|
||||
|
||||
.detail-item span {
|
||||
font-size: 0.875rem;
|
||||
color: hsl(var(--foreground));
|
||||
word-break: break-all;
|
||||
}
|
||||
|
||||
.detail-item span.mono {
|
||||
font-family: ui-monospace, SFMono-Regular, "SF Mono", Menlo, Consolas, monospace;
|
||||
font-size: 0.8125rem;
|
||||
background: hsl(var(--muted) / 0.3);
|
||||
padding: 0.25rem 0.5rem;
|
||||
border-radius: 0.25rem;
|
||||
}
|
||||
|
||||
.detail-item-full {
|
||||
grid-column: 1 / -1;
|
||||
}
|
||||
|
||||
/* Code Block */
|
||||
.code-block {
|
||||
background: hsl(var(--muted) / 0.3);
|
||||
border: 1px solid hsl(var(--border));
|
||||
border-radius: 0.375rem;
|
||||
padding: 0.75rem 1rem;
|
||||
overflow-x: auto;
|
||||
}
|
||||
|
||||
.code-block code {
|
||||
font-family: ui-monospace, SFMono-Regular, "SF Mono", Menlo, Consolas, monospace;
|
||||
font-size: 0.8125rem;
|
||||
color: hsl(var(--foreground));
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
/* Field Groups */
|
||||
.field-group {
|
||||
display: flex;
|
||||
@@ -1927,6 +2087,26 @@ select.cli-input {
|
||||
margin-bottom: 0.75rem;
|
||||
}
|
||||
|
||||
/* ===========================
|
||||
CLI Settings List in Sidebar
|
||||
=========================== */
|
||||
|
||||
.cli-settings-list {
|
||||
flex: 1;
|
||||
overflow-y: auto;
|
||||
padding: 0.5rem;
|
||||
}
|
||||
|
||||
/* ===========================
|
||||
Model Pools List in Sidebar
|
||||
=========================== */
|
||||
|
||||
.model-pools-list {
|
||||
flex: 1;
|
||||
overflow-y: auto;
|
||||
padding: 0.5rem;
|
||||
}
|
||||
|
||||
/* ===========================
|
||||
Main Panel Sections
|
||||
=========================== */
|
||||
@@ -2763,6 +2943,57 @@ select.cli-input {
|
||||
color: hsl(var(--muted-foreground));
|
||||
}
|
||||
|
||||
/* Parse JSON link in footer */
|
||||
.json-parse-link {
|
||||
font-size: 0.75rem;
|
||||
color: hsl(var(--primary));
|
||||
text-decoration: none;
|
||||
cursor: pointer;
|
||||
transition: color 0.2s ease;
|
||||
}
|
||||
|
||||
.json-parse-link:hover {
|
||||
color: hsl(var(--primary) / 0.8);
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
/* Input with toggle button (for password visibility) */
|
||||
.input-with-toggle {
|
||||
position: relative;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.input-with-toggle .form-control {
|
||||
flex: 1;
|
||||
padding-right: 2.5rem;
|
||||
}
|
||||
|
||||
.input-with-toggle .toggle-password {
|
||||
position: absolute;
|
||||
right: 0.25rem;
|
||||
top: 50%;
|
||||
transform: translateY(-50%);
|
||||
padding: 0.25rem 0.5rem;
|
||||
background: transparent;
|
||||
border: none;
|
||||
color: hsl(var(--muted-foreground));
|
||||
cursor: pointer;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.input-with-toggle .toggle-password:hover {
|
||||
color: hsl(var(--foreground));
|
||||
}
|
||||
|
||||
.input-with-toggle .toggle-password i,
|
||||
.input-with-toggle .toggle-password svg {
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
}
|
||||
|
||||
/* Button styles for JSON editor */
|
||||
.btn-sm {
|
||||
padding: 0.375rem 0.75rem;
|
||||
|
||||
@@ -71,8 +71,8 @@ async function loadAllStatusesFallback() {
|
||||
console.warn('[CLI Status] Using fallback individual API calls');
|
||||
await Promise.all([
|
||||
loadCliToolsConfig(), // Ensure config is loaded (auto-creates if missing)
|
||||
loadCliToolStatus(),
|
||||
loadCodexLensStatus()
|
||||
loadCliToolStatus()
|
||||
// CodexLens status removed - managed in dedicated CodexLens Manager page
|
||||
]);
|
||||
}
|
||||
|
||||
@@ -235,8 +235,8 @@ async function loadCliToolsConfig() {
|
||||
const response = await fetch('/api/cli/tools-config');
|
||||
if (!response.ok) return null;
|
||||
const data = await response.json();
|
||||
// Store full config and extract tools for backward compatibility
|
||||
cliToolsConfig = data.tools || {};
|
||||
// Store full config and extract tools object (data.tools is full config, data.tools.tools is the actual tools)
|
||||
cliToolsConfig = data.tools?.tools || {};
|
||||
window.claudeCliToolsConfig = data; // Full config available globally
|
||||
|
||||
// Load default tool from config
|
||||
@@ -308,15 +308,17 @@ async function loadCliSettingsEndpoints() {
|
||||
function updateCliBadge() {
|
||||
const badge = document.getElementById('badgeCliTools');
|
||||
if (badge) {
|
||||
// Merge tools from both status and config to get complete list
|
||||
const allTools = new Set([
|
||||
...Object.keys(cliToolStatus),
|
||||
...Object.keys(cliToolsConfig)
|
||||
]);
|
||||
// Only count builtin and cli-wrapper tools (exclude api-endpoint tools)
|
||||
const cliTools = Object.keys(cliToolsConfig).filter(t => {
|
||||
if (!t || t === '_configInfo') return false;
|
||||
const config = cliToolsConfig[t];
|
||||
// Include if: no type (legacy builtin), type is builtin, or type is cli-wrapper
|
||||
return !config?.type || config.type === 'builtin' || config.type === 'cli-wrapper';
|
||||
});
|
||||
|
||||
// Count available and enabled CLI tools
|
||||
// Count available and enabled CLI tools only
|
||||
let available = 0;
|
||||
allTools.forEach(tool => {
|
||||
cliTools.forEach(tool => {
|
||||
const status = cliToolStatus[tool] || {};
|
||||
const config = cliToolsConfig[tool] || { enabled: true };
|
||||
if (status.available && config.enabled !== false) {
|
||||
@@ -324,33 +326,12 @@ function updateCliBadge() {
|
||||
}
|
||||
});
|
||||
|
||||
// Also count CodexLens and Semantic Search
|
||||
let totalExtras = 0;
|
||||
let availableExtras = 0;
|
||||
|
||||
// CodexLens counts if ready
|
||||
if (codexLensStatus.ready) {
|
||||
totalExtras++;
|
||||
availableExtras++;
|
||||
} else if (codexLensStatus.ready === false) {
|
||||
// Only count as total if we have status info (not just initial state)
|
||||
totalExtras++;
|
||||
}
|
||||
|
||||
// Semantic Search counts if CodexLens is ready (it's a feature of CodexLens)
|
||||
if (codexLensStatus.ready) {
|
||||
totalExtras++;
|
||||
if (semanticStatus.available) {
|
||||
availableExtras++;
|
||||
}
|
||||
}
|
||||
|
||||
const total = allTools.size + totalExtras;
|
||||
const totalAvailable = available + availableExtras;
|
||||
badge.textContent = `${totalAvailable}/${total}`;
|
||||
badge.classList.toggle('text-success', totalAvailable === total && total > 0);
|
||||
badge.classList.toggle('text-warning', totalAvailable > 0 && totalAvailable < total);
|
||||
badge.classList.toggle('text-destructive', totalAvailable === 0);
|
||||
// CLI tools badge shows only CLI tools count
|
||||
const total = cliTools.length;
|
||||
badge.textContent = `${available}/${total}`;
|
||||
badge.classList.toggle('text-success', available === total && total > 0);
|
||||
badge.classList.toggle('text-warning', available > 0 && available < total);
|
||||
badge.classList.toggle('text-destructive', available === 0);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -414,10 +395,16 @@ function renderCliStatus() {
|
||||
};
|
||||
|
||||
// Get tools dynamically from config, merging with status for complete list
|
||||
// Only show builtin and cli-wrapper tools in the tools grid (api-endpoint tools show in API Endpoints section)
|
||||
const tools = [...new Set([
|
||||
...Object.keys(cliToolsConfig),
|
||||
...Object.keys(cliToolStatus)
|
||||
])].filter(t => t && t !== '_configInfo'); // Filter out metadata keys
|
||||
])].filter(t => {
|
||||
if (!t || t === '_configInfo') return false;
|
||||
const config = cliToolsConfig[t];
|
||||
// Include if: no type (legacy builtin), type is builtin, or type is cli-wrapper
|
||||
return !config?.type || config.type === 'builtin' || config.type === 'cli-wrapper';
|
||||
});
|
||||
|
||||
const toolsHtml = tools.map(tool => {
|
||||
const status = cliToolStatus[tool] || {};
|
||||
@@ -516,74 +503,8 @@ function renderCliStatus() {
|
||||
`;
|
||||
}).join('');
|
||||
|
||||
// CodexLens card with semantic search info
|
||||
const codexLensHtml = `
|
||||
<div class="cli-tool-card tool-codexlens ${codexLensStatus.ready ? 'available' : 'unavailable'}">
|
||||
<div class="cli-tool-header">
|
||||
<span class="cli-tool-status ${codexLensStatus.ready ? 'status-available' : 'status-unavailable'}"></span>
|
||||
<span class="cli-tool-name">CodexLens</span>
|
||||
<span class="badge px-1.5 py-0.5 text-xs rounded bg-muted text-muted-foreground">Index</span>
|
||||
</div>
|
||||
<div class="cli-tool-desc text-xs text-muted-foreground mt-1">
|
||||
${codexLensStatus.ready ? 'Code indexing & FTS search' : 'Full-text code search engine'}
|
||||
</div>
|
||||
<div class="cli-tool-info mt-2">
|
||||
${codexLensStatus.ready
|
||||
? `<span class="text-success flex items-center gap-1"><i data-lucide="check-circle" class="w-3 h-3"></i> v${codexLensStatus.version || 'installed'}</span>`
|
||||
: `<span class="text-muted-foreground flex items-center gap-1"><i data-lucide="circle-dashed" class="w-3 h-3"></i> Not Installed</span>`
|
||||
}
|
||||
</div>
|
||||
<div class="cli-tool-actions flex gap-2 mt-3">
|
||||
${!codexLensStatus.ready
|
||||
? `<button class="btn-sm btn-primary flex items-center gap-1" onclick="installCodexLens()">
|
||||
<i data-lucide="download" class="w-3 h-3"></i> Install
|
||||
</button>`
|
||||
: `<button class="btn-sm btn-outline flex items-center gap-1" onclick="initCodexLensIndex()">
|
||||
<i data-lucide="database" class="w-3 h-3"></i> Init Index
|
||||
</button>
|
||||
<button class="btn-sm btn-outline flex items-center gap-1" onclick="uninstallCodexLens()">
|
||||
<i data-lucide="trash-2" class="w-3 h-3"></i> Uninstall
|
||||
</button>`
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
// Semantic Search card (only show if CodexLens is installed)
|
||||
const semanticHtml = codexLensStatus.ready ? `
|
||||
<div class="cli-tool-card tool-semantic ${semanticStatus.available ? 'available' : 'unavailable'}">
|
||||
<div class="cli-tool-header">
|
||||
<span class="cli-tool-status ${semanticStatus.available ? 'status-available' : 'status-unavailable'}"></span>
|
||||
<span class="cli-tool-name">Semantic Search</span>
|
||||
<span class="badge px-1.5 py-0.5 text-xs rounded ${semanticStatus.available ? 'bg-primary/20 text-primary' : 'bg-muted text-muted-foreground'}">AI</span>
|
||||
</div>
|
||||
<div class="cli-tool-desc text-xs text-muted-foreground mt-1">
|
||||
${semanticStatus.available ? 'AI-powered code understanding' : 'Natural language code search'}
|
||||
</div>
|
||||
<div class="cli-tool-info mt-2">
|
||||
${semanticStatus.available
|
||||
? `<span class="text-success flex items-center gap-1"><i data-lucide="sparkles" class="w-3 h-3"></i> ${semanticStatus.backend || 'Ready'}</span>`
|
||||
: `<span class="text-muted-foreground flex items-center gap-1"><i data-lucide="circle-dashed" class="w-3 h-3"></i> Not Installed</span>`
|
||||
}
|
||||
</div>
|
||||
<div class="cli-tool-actions flex flex-col gap-2 mt-3">
|
||||
${!semanticStatus.available ? `
|
||||
<button class="btn-sm btn-primary w-full flex items-center justify-center gap-1" onclick="openSemanticInstallWizard()">
|
||||
<i data-lucide="brain" class="w-3 h-3"></i> Install AI Model
|
||||
</button>
|
||||
<div class="flex items-center gap-1 text-xs text-muted-foreground mt-1">
|
||||
<i data-lucide="hard-drive" class="w-3 h-3"></i>
|
||||
<span>~130MB</span>
|
||||
</div>
|
||||
` : `
|
||||
<div class="flex items-center gap-1 text-xs text-muted-foreground">
|
||||
<i data-lucide="cpu" class="w-3 h-3"></i>
|
||||
<span>bge-small-en-v1.5</span>
|
||||
</div>
|
||||
`}
|
||||
</div>
|
||||
</div>
|
||||
` : '';
|
||||
// CodexLens and Semantic Search removed from CLI status panel
|
||||
// They are managed in the dedicated CodexLens Manager page
|
||||
|
||||
// CCW Installation Status card (show warning if not fully installed)
|
||||
const ccwInstallHtml = !ccwInstallStatus.installed ? `
|
||||
@@ -637,6 +558,9 @@ function renderCliStatus() {
|
||||
<div class="cli-endpoint-info" style="margin-top: 0.25rem;">
|
||||
<span class="text-xs text-muted-foreground" style="font-size: 0.75rem; color: var(--muted-foreground);">${ep.model}</span>
|
||||
</div>
|
||||
<div class="cli-endpoint-usage" style="margin-top: 0.5rem; padding-top: 0.5rem; border-top: 1px solid var(--border);">
|
||||
<code style="font-size: 0.65rem; color: var(--muted-foreground); word-break: break-all;">--tool custom --model ${ep.id}</code>
|
||||
</div>
|
||||
</div>
|
||||
`).join('')}
|
||||
</div>
|
||||
@@ -748,8 +672,6 @@ function renderCliStatus() {
|
||||
${ccwInstallHtml}
|
||||
<div class="cli-tools-grid">
|
||||
${toolsHtml}
|
||||
${codexLensHtml}
|
||||
${semanticHtml}
|
||||
</div>
|
||||
${apiEndpointsHtml}
|
||||
${settingsHtml}
|
||||
|
||||
@@ -257,6 +257,10 @@ const i18n = {
|
||||
'cli.addToCli': 'Add to CLI',
|
||||
'cli.enabled': 'Enabled',
|
||||
'cli.disabled': 'Disabled',
|
||||
'cli.cliWrapper': 'CLI Wrapper',
|
||||
'cli.wrapper': 'Wrapper',
|
||||
'cli.customClaudeSettings': 'Custom Claude CLI settings',
|
||||
'cli.updateFailed': 'Failed to update',
|
||||
|
||||
// CodexLens Configuration
|
||||
'codexlens.config': 'CodexLens Configuration',
|
||||
@@ -1618,7 +1622,7 @@ const i18n = {
|
||||
'apiSettings.total': 'total',
|
||||
'apiSettings.testConnection': 'Test Connection',
|
||||
'apiSettings.endpointId': 'Endpoint ID',
|
||||
'apiSettings.endpointIdHint': 'Usage: ccw cli -p "..." --model <endpoint-id>',
|
||||
'apiSettings.endpointIdHint': 'Usage: ccw cli -p "..." --tool custom --model <endpoint-id> --mode analysis',
|
||||
'apiSettings.endpoints': 'Endpoints',
|
||||
'apiSettings.addEndpointHint': 'Create custom endpoint aliases for CLI usage',
|
||||
'apiSettings.endpointModel': 'Model',
|
||||
@@ -1752,12 +1756,15 @@ const i18n = {
|
||||
'apiSettings.useModelTreeToManage': 'Use the model tree to manage individual models',
|
||||
|
||||
// CLI Settings
|
||||
'apiSettings.cliSettings': 'CLI Settings',
|
||||
'apiSettings.addCliSettings': 'Add CLI Settings',
|
||||
'apiSettings.editCliSettings': 'Edit CLI Settings',
|
||||
'apiSettings.noCliSettings': 'No CLI settings configured',
|
||||
'apiSettings.noCliSettingsSelected': 'No CLI Settings Selected',
|
||||
'apiSettings.cliSettingsHint': 'Select a CLI settings endpoint or create a new one',
|
||||
'apiSettings.cliSettings': 'CLI Wrapper',
|
||||
'apiSettings.addCliSettings': 'Add CLI Wrapper',
|
||||
'apiSettings.editCliSettings': 'Edit CLI Wrapper',
|
||||
'apiSettings.noCliSettings': 'No CLI wrapper configured',
|
||||
'apiSettings.noCliSettingsSelected': 'No CLI Wrapper Selected',
|
||||
'apiSettings.cliSettingsHint': 'Select a CLI wrapper endpoint or create a new one',
|
||||
'apiSettings.showToken': 'Show',
|
||||
'apiSettings.hideToken': 'Hide',
|
||||
'apiSettings.syncFromJson': 'Parse JSON',
|
||||
'apiSettings.cliProviderHint': 'Select an Anthropic provider to use its API key and base URL',
|
||||
'apiSettings.noAnthropicProviders': 'No Anthropic providers configured. Please add one in the Providers tab first.',
|
||||
'apiSettings.selectProviderFirst': 'Select a provider first',
|
||||
@@ -1771,6 +1778,10 @@ const i18n = {
|
||||
'apiSettings.envSettings': 'Environment Settings',
|
||||
'apiSettings.settingsFilePath': 'Settings File Path',
|
||||
'apiSettings.nameRequired': 'Name is required',
|
||||
'apiSettings.nameInvalidFormat': 'Name must start with a letter and contain only letters, numbers, hyphens, and underscores',
|
||||
'apiSettings.nameTooLong': 'Name must be 32 characters or less',
|
||||
'apiSettings.nameConflict': 'Name conflicts with built-in tool',
|
||||
'apiSettings.nameFormatHint': 'Letters, numbers, hyphens, underscores only. Used as: ccw cli --tool [name]',
|
||||
'apiSettings.status': 'Status',
|
||||
'apiSettings.providerBinding': 'Provider Binding',
|
||||
'apiSettings.directConfig': 'Direct Configuration',
|
||||
@@ -2391,6 +2402,10 @@ const i18n = {
|
||||
'cli.addToCli': '添加到 CLI',
|
||||
'cli.enabled': '已启用',
|
||||
'cli.disabled': '已禁用',
|
||||
'cli.cliWrapper': 'CLI 封装',
|
||||
'cli.wrapper': '封装',
|
||||
'cli.customClaudeSettings': '自定义 Claude CLI 配置',
|
||||
'cli.updateFailed': '更新失败',
|
||||
|
||||
// CodexLens 配置
|
||||
'codexlens.config': 'CodexLens 配置',
|
||||
@@ -3761,7 +3776,7 @@ const i18n = {
|
||||
'apiSettings.total': '总计',
|
||||
'apiSettings.testConnection': '测试连接',
|
||||
'apiSettings.endpointId': '端点 ID',
|
||||
'apiSettings.endpointIdHint': '用法: ccw cli -p "..." --model <端点ID>',
|
||||
'apiSettings.endpointIdHint': '用法: ccw cli -p "..." --tool custom --model <端点ID> --mode analysis',
|
||||
'apiSettings.endpoints': '端点',
|
||||
'apiSettings.addEndpointHint': '创建用于 CLI 的自定义端点别名',
|
||||
'apiSettings.endpointModel': '模型',
|
||||
@@ -3895,12 +3910,15 @@ const i18n = {
|
||||
'apiSettings.useModelTreeToManage': '使用模型树管理各个模型',
|
||||
|
||||
// CLI Settings
|
||||
'apiSettings.cliSettings': 'CLI 配置',
|
||||
'apiSettings.addCliSettings': '添加 CLI 配置',
|
||||
'apiSettings.editCliSettings': '编辑 CLI 配置',
|
||||
'apiSettings.noCliSettings': '未配置 CLI 设置',
|
||||
'apiSettings.noCliSettingsSelected': '未选择 CLI 配置',
|
||||
'apiSettings.cliSettingsHint': '选择一个 CLI 配置端点或创建新的',
|
||||
'apiSettings.cliSettings': 'CLI 封装',
|
||||
'apiSettings.addCliSettings': '添加 CLI 封装',
|
||||
'apiSettings.editCliSettings': '编辑 CLI 封装',
|
||||
'apiSettings.noCliSettings': '未配置 CLI 封装',
|
||||
'apiSettings.noCliSettingsSelected': '未选择 CLI 封装',
|
||||
'apiSettings.cliSettingsHint': '选择一个 CLI 封装端点或创建新的',
|
||||
'apiSettings.showToken': '显示',
|
||||
'apiSettings.hideToken': '隐藏',
|
||||
'apiSettings.syncFromJson': '解析 JSON',
|
||||
'apiSettings.cliProviderHint': '选择一个 Anthropic 供应商以使用其 API 密钥和基础 URL',
|
||||
'apiSettings.noAnthropicProviders': '未配置 Anthropic 供应商。请先在供应商标签页中添加。',
|
||||
'apiSettings.selectProviderFirst': '请先选择供应商',
|
||||
@@ -3914,6 +3932,10 @@ const i18n = {
|
||||
'apiSettings.envSettings': '环境变量设置',
|
||||
'apiSettings.settingsFilePath': '配置文件路径',
|
||||
'apiSettings.nameRequired': '名称为必填项',
|
||||
'apiSettings.nameInvalidFormat': '名称必须以字母开头,只能包含字母、数字、连字符和下划线',
|
||||
'apiSettings.nameTooLong': '名称长度不能超过32个字符',
|
||||
'apiSettings.nameConflict': '名称与内置工具冲突',
|
||||
'apiSettings.nameFormatHint': '仅限字母、数字、连字符、下划线。用于命令: ccw cli --tool [名称]',
|
||||
'apiSettings.tokenRequired': 'API 令牌为必填项',
|
||||
'apiSettings.status': '状态',
|
||||
'apiSettings.providerBinding': '供应商绑定',
|
||||
|
||||
@@ -1150,6 +1150,13 @@ async function renderApiSettings() {
|
||||
// Load data (use cache by default, forceRefresh=false)
|
||||
await loadApiSettings(false);
|
||||
|
||||
// Handle pending CLI wrapper edit from status page navigation
|
||||
if (window.pendingCliWrapperEdit) {
|
||||
activeSidebarTab = 'cli-settings';
|
||||
selectedCliSettingsId = window.pendingCliWrapperEdit;
|
||||
window.pendingCliWrapperEdit = null; // Clear the pending edit flag
|
||||
}
|
||||
|
||||
if (!apiSettingsData) {
|
||||
container.innerHTML = '<div class="api-settings-container">' +
|
||||
'<div class="error-message">' + t('apiSettings.failedToLoad') + '</div>' +
|
||||
@@ -2707,7 +2714,7 @@ function renderEndpointsList() {
|
||||
'</div>' +
|
||||
'<div class="usage-hint">' +
|
||||
'<i data-lucide="terminal"></i>' +
|
||||
'<code>ccw cli -p "..." --model ' + endpoint.id + '</code>' +
|
||||
'<code>ccw cli -p "..." --tool custom --model ' + endpoint.id + ' --mode analysis</code>' +
|
||||
'</div>' +
|
||||
'</div>' +
|
||||
'</div>';
|
||||
@@ -3945,7 +3952,8 @@ function renderCliSettingsForm(existingEndpoint) {
|
||||
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 />' +
|
||||
'<input type="text" id="cli-settings-name" class="form-control" value="' + escapeHtml(existingEndpoint ? existingEndpoint.name : '') + '" placeholder="my-claude-endpoint" required pattern="^[a-zA-Z][a-zA-Z0-9_-]*$" maxlength="32" />' +
|
||||
'<small class="form-hint">' + (t('apiSettings.nameFormatHint') || 'Letters, numbers, hyphens, underscores only. Used as: ccw cli --tool [name]') + '</small>' +
|
||||
'</div>' +
|
||||
'<div class="form-group">' +
|
||||
'<label for="cli-settings-description">' + t('apiSettings.description') + '</label>' +
|
||||
@@ -4097,7 +4105,12 @@ function renderDirectModeContent(container, env, settings) {
|
||||
container.innerHTML =
|
||||
'<div class="form-group">' +
|
||||
'<label for="cli-auth-token">ANTHROPIC_AUTH_TOKEN *</label>' +
|
||||
'<div class="input-with-toggle">' +
|
||||
'<input type="password" id="cli-auth-token" class="form-control" placeholder="sk-ant-..." value="' + escapeHtml(env.ANTHROPIC_AUTH_TOKEN || '') + '" />' +
|
||||
'<button type="button" class="btn btn-sm btn-ghost toggle-password" onclick="toggleAuthTokenVisibility()" title="' + (t('apiSettings.showToken') || 'Show') + '">' +
|
||||
'<i data-lucide="eye" id="cli-auth-token-icon"></i>' +
|
||||
'</button>' +
|
||||
'</div>' +
|
||||
'</div>' +
|
||||
'<div class="form-group">' +
|
||||
'<label for="cli-base-url">ANTHROPIC_BASE_URL</label>' +
|
||||
@@ -4151,7 +4164,7 @@ function buildJsonEditorSection(settings) {
|
||||
'</div>' +
|
||||
'<div class="json-editor-footer">' +
|
||||
'<span class="json-status" id="cli-json-status"></span>' +
|
||||
'<span class="json-hint">' + (t('apiSettings.jsonEditorHint') || 'Edit JSON directly to add advanced settings') + '</span>' +
|
||||
'<a href="javascript:void(0)" class="json-parse-link" onclick="syncJsonToForm()">' + (t('apiSettings.syncFromJson') || 'Parse JSON') + '</a>' +
|
||||
'</div>' +
|
||||
'</div>';
|
||||
}
|
||||
@@ -4267,6 +4280,39 @@ function validateCliJson() {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate CLI endpoint name for CLI compatibility
|
||||
* Name must be: start with letter, alphanumeric with hyphens/underscores, no spaces
|
||||
*/
|
||||
function validateCliEndpointName(name) {
|
||||
if (!name || name.trim().length === 0) {
|
||||
return { valid: false, error: t('apiSettings.nameRequired') || 'Name is required' };
|
||||
}
|
||||
|
||||
// Check for valid characters: a-z, A-Z, 0-9, hyphen, underscore
|
||||
var validPattern = /^[a-zA-Z][a-zA-Z0-9_-]*$/;
|
||||
if (!validPattern.test(name)) {
|
||||
return {
|
||||
valid: false,
|
||||
error: t('apiSettings.nameInvalidFormat') || 'Name must start with a letter and contain only letters, numbers, hyphens, and underscores'
|
||||
};
|
||||
}
|
||||
|
||||
// Check length
|
||||
if (name.length > 32) {
|
||||
return { valid: false, error: t('apiSettings.nameTooLong') || 'Name must be 32 characters or less' };
|
||||
}
|
||||
|
||||
// Check if name conflicts with built-in tools
|
||||
var builtinTools = ['gemini', 'qwen', 'codex', 'claude', 'opencode', 'litellm'];
|
||||
if (builtinTools.indexOf(name.toLowerCase()) !== -1) {
|
||||
return { valid: false, error: (t('apiSettings.nameConflict') || 'Name conflicts with built-in tool') + ': ' + name };
|
||||
}
|
||||
|
||||
return { valid: true };
|
||||
}
|
||||
window.validateCliEndpointName = validateCliEndpointName;
|
||||
|
||||
/**
|
||||
* Format JSON in editor
|
||||
*/
|
||||
@@ -4331,6 +4377,87 @@ function syncFormToJson() {
|
||||
}
|
||||
window.syncFormToJson = syncFormToJson;
|
||||
|
||||
/**
|
||||
* Toggle ANTHROPIC_AUTH_TOKEN visibility
|
||||
*/
|
||||
function toggleAuthTokenVisibility() {
|
||||
var input = document.getElementById('cli-auth-token');
|
||||
var icon = document.getElementById('cli-auth-token-icon');
|
||||
var btn = input ? input.parentElement.querySelector('.toggle-password') : null;
|
||||
|
||||
if (!input || !icon) return;
|
||||
|
||||
if (input.type === 'password') {
|
||||
input.type = 'text';
|
||||
icon.setAttribute('data-lucide', 'eye-off');
|
||||
if (btn) btn.title = t('apiSettings.hideToken') || 'Hide';
|
||||
} else {
|
||||
input.type = 'password';
|
||||
icon.setAttribute('data-lucide', 'eye');
|
||||
if (btn) btn.title = t('apiSettings.showToken') || 'Show';
|
||||
}
|
||||
|
||||
if (window.lucide) lucide.createIcons();
|
||||
}
|
||||
window.toggleAuthTokenVisibility = toggleAuthTokenVisibility;
|
||||
|
||||
/**
|
||||
* Sync JSON editor content to form fields
|
||||
* Parses JSON and fills ANTHROPIC_AUTH_TOKEN, ANTHROPIC_BASE_URL and model fields
|
||||
*/
|
||||
function syncJsonToForm() {
|
||||
var editor = document.getElementById('cli-json-editor');
|
||||
if (!editor) return;
|
||||
|
||||
var jsonObj;
|
||||
try {
|
||||
jsonObj = JSON.parse(editor.value);
|
||||
} catch (e) {
|
||||
showRefreshToast(t('apiSettings.jsonInvalid') || 'Invalid JSON', 'error');
|
||||
return;
|
||||
}
|
||||
|
||||
var env = jsonObj.env || {};
|
||||
|
||||
// Fill ANTHROPIC_AUTH_TOKEN (only in direct mode and only if not masked)
|
||||
if (cliConfigMode === 'direct') {
|
||||
var authTokenInput = document.getElementById('cli-auth-token');
|
||||
if (authTokenInput && env.ANTHROPIC_AUTH_TOKEN) {
|
||||
// Only fill if the value is not masked (doesn't end with '...')
|
||||
if (!env.ANTHROPIC_AUTH_TOKEN.endsWith('...')) {
|
||||
authTokenInput.value = env.ANTHROPIC_AUTH_TOKEN;
|
||||
}
|
||||
}
|
||||
|
||||
var baseUrlInput = document.getElementById('cli-base-url');
|
||||
if (baseUrlInput && env.ANTHROPIC_BASE_URL !== undefined) {
|
||||
baseUrlInput.value = env.ANTHROPIC_BASE_URL || '';
|
||||
}
|
||||
}
|
||||
|
||||
// Fill model configuration fields
|
||||
var modelDefault = document.getElementById('cli-model-default');
|
||||
var modelHaiku = document.getElementById('cli-model-haiku');
|
||||
var modelSonnet = document.getElementById('cli-model-sonnet');
|
||||
var modelOpus = document.getElementById('cli-model-opus');
|
||||
|
||||
if (modelDefault && env.ANTHROPIC_MODEL !== undefined) {
|
||||
modelDefault.value = env.ANTHROPIC_MODEL || '';
|
||||
}
|
||||
if (modelHaiku && env.ANTHROPIC_DEFAULT_HAIKU_MODEL !== undefined) {
|
||||
modelHaiku.value = env.ANTHROPIC_DEFAULT_HAIKU_MODEL || '';
|
||||
}
|
||||
if (modelSonnet && env.ANTHROPIC_DEFAULT_SONNET_MODEL !== undefined) {
|
||||
modelSonnet.value = env.ANTHROPIC_DEFAULT_SONNET_MODEL || '';
|
||||
}
|
||||
if (modelOpus && env.ANTHROPIC_DEFAULT_OPUS_MODEL !== undefined) {
|
||||
modelOpus.value = env.ANTHROPIC_DEFAULT_OPUS_MODEL || '';
|
||||
}
|
||||
|
||||
showRefreshToast(t('common.success') || 'Success', 'success');
|
||||
}
|
||||
window.syncJsonToForm = syncJsonToForm;
|
||||
|
||||
/**
|
||||
* Get settings from JSON editor (merges with form data)
|
||||
*/
|
||||
@@ -4369,6 +4496,13 @@ async function submitCliSettingsForm() {
|
||||
return;
|
||||
}
|
||||
|
||||
// Validate name format for CLI compatibility
|
||||
var nameValidation = validateCliEndpointName(name);
|
||||
if (!nameValidation.valid) {
|
||||
showRefreshToast(nameValidation.error, 'error');
|
||||
return;
|
||||
}
|
||||
|
||||
var data = {
|
||||
name: name,
|
||||
description: description,
|
||||
@@ -4603,7 +4737,8 @@ function showAddCliSettingsModal(existingEndpoint) {
|
||||
(isEdit ? '<input type="hidden" id="cli-settings-id" value="' + existingEndpoint.id + '">' : '') +
|
||||
'<div class="form-group">' +
|
||||
'<label for="cli-settings-name">' + t('apiSettings.endpointName') + ' *</label>' +
|
||||
'<input type="text" id="cli-settings-name" class="cli-input" value="' + escapeHtml(existingEndpoint ? existingEndpoint.name : '') + '" required />' +
|
||||
'<input type="text" id="cli-settings-name" class="cli-input" value="' + escapeHtml(existingEndpoint ? existingEndpoint.name : '') + '" required pattern="^[a-zA-Z][a-zA-Z0-9_-]*$" maxlength="32" />' +
|
||||
'<small class="form-hint">' + (t('apiSettings.nameFormatHint') || 'Letters, numbers, hyphens, underscores only. Used as: ccw cli --tool [name]') + '</small>' +
|
||||
'</div>' +
|
||||
'<div class="form-group">' +
|
||||
'<label for="cli-settings-description">' + t('apiSettings.description') + '</label>' +
|
||||
@@ -4674,6 +4809,13 @@ async function submitCliSettings() {
|
||||
return;
|
||||
}
|
||||
|
||||
// Validate name format for CLI compatibility
|
||||
var nameValidation = validateCliEndpointName(name);
|
||||
if (!nameValidation.valid) {
|
||||
showRefreshToast(nameValidation.error, 'error');
|
||||
return;
|
||||
}
|
||||
|
||||
if (!providerId) {
|
||||
showRefreshToast(t('apiSettings.providerRequired'), 'error');
|
||||
return;
|
||||
|
||||
@@ -6,6 +6,7 @@ var currentCliExecution = null;
|
||||
var cliExecutionOutput = '';
|
||||
var ccwInstallations = [];
|
||||
var ccwEndpointTools = [];
|
||||
var cliWrapperEndpoints = []; // CLI封装 endpoints from /api/cli/settings
|
||||
var cliToolConfig = null; // Store loaded CLI config
|
||||
var predefinedModels = {}; // Store predefined models per tool
|
||||
|
||||
@@ -193,6 +194,46 @@ async function loadCliCustomEndpoints() {
|
||||
}
|
||||
}
|
||||
|
||||
// ========== CLI Wrapper Endpoints (CLI封装) ==========
|
||||
async function loadCliWrapperEndpoints() {
|
||||
try {
|
||||
var response = await fetch('/api/cli/settings');
|
||||
if (!response.ok) throw new Error('Failed to load CLI wrapper endpoints');
|
||||
var data = await response.json();
|
||||
cliWrapperEndpoints = data.endpoints || [];
|
||||
return cliWrapperEndpoints;
|
||||
} catch (err) {
|
||||
console.error('Failed to load CLI wrapper endpoints:', err);
|
||||
cliWrapperEndpoints = [];
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
async function toggleCliWrapperEnabled(endpointId, enabled) {
|
||||
try {
|
||||
await initCsrfToken();
|
||||
var response = await csrfFetch('/api/cli/settings/' + endpointId, {
|
||||
method: 'PUT',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ enabled: enabled })
|
||||
});
|
||||
if (!response.ok) throw new Error('Failed to update CLI wrapper endpoint');
|
||||
var data = await response.json();
|
||||
if (data.success) {
|
||||
// Update local state
|
||||
var idx = cliWrapperEndpoints.findIndex(function(e) { return e.id === endpointId; });
|
||||
if (idx >= 0) {
|
||||
cliWrapperEndpoints[idx].enabled = enabled;
|
||||
}
|
||||
showRefreshToast((enabled ? t('cli.enabled') || 'Enabled' : t('cli.disabled') || 'Disabled') + ': ' + endpointId, 'success');
|
||||
}
|
||||
return data;
|
||||
} catch (err) {
|
||||
showRefreshToast((t('cli.updateFailed') || 'Failed to update') + ': ' + err.message, 'error');
|
||||
throw err;
|
||||
}
|
||||
}
|
||||
|
||||
async function toggleEndpointEnabled(endpointId, enabled) {
|
||||
try {
|
||||
await initCsrfToken();
|
||||
@@ -628,7 +669,8 @@ async function renderCliManager() {
|
||||
loadCcwInstallations(),
|
||||
loadCcwEndpointTools(),
|
||||
loadLitellmApiEndpoints(),
|
||||
loadCliCustomEndpoints()
|
||||
loadCliCustomEndpoints(),
|
||||
loadCliWrapperEndpoints()
|
||||
]);
|
||||
|
||||
container.innerHTML = '<div class="status-manager">' +
|
||||
@@ -764,44 +806,8 @@ function renderToolsSection() {
|
||||
'</div>';
|
||||
}).join('');
|
||||
|
||||
// CodexLens item - simplified view with link to manager page
|
||||
var codexLensHtml = '<div class="tool-item clickable ' + (codexLensStatus.ready ? 'available' : 'unavailable') + '" onclick="navigateToCodexLensManager()">' +
|
||||
'<div class="tool-item-left">' +
|
||||
'<span class="tool-status-dot ' + (codexLensStatus.ready ? 'status-available' : 'status-unavailable') + '"></span>' +
|
||||
'<div class="tool-item-info">' +
|
||||
'<div class="tool-item-name">CodexLens <span class="tool-type-badge">Index</span>' +
|
||||
'<i data-lucide="external-link" class="w-3 h-3 tool-config-icon"></i></div>' +
|
||||
'<div class="tool-item-desc">' + (codexLensStatus.ready ? t('cli.codexLensDesc') : t('cli.codexLensDescFull')) + '</div>' +
|
||||
'</div>' +
|
||||
'</div>' +
|
||||
'<div class="tool-item-right">' +
|
||||
(codexLensStatus.ready
|
||||
? '<span class="tool-status-text success"><i data-lucide="check-circle" class="w-3.5 h-3.5"></i> v' + (codexLensStatus.version || 'installed') + '</span>' +
|
||||
'<button class="btn-sm btn-primary" onclick="event.stopPropagation(); navigateToCodexLensManager()"><i data-lucide="settings" class="w-3 h-3"></i> ' + t('cli.openManager') + '</button>'
|
||||
: '<span class="tool-status-text muted"><i data-lucide="circle-dashed" class="w-3.5 h-3.5"></i> ' + t('cli.notInstalled') + '</span>' +
|
||||
'<button class="btn-sm btn-primary" onclick="event.stopPropagation(); navigateToCodexLensManager()"><i data-lucide="settings" class="w-3 h-3"></i> ' + t('cli.openManager') + '</button>') +
|
||||
'</div>' +
|
||||
'</div>';
|
||||
|
||||
// Semantic Search item (only show if CodexLens is installed)
|
||||
var semanticHtml = '';
|
||||
if (codexLensStatus.ready) {
|
||||
semanticHtml = '<div class="tool-item ' + (semanticStatus.available ? 'available' : 'unavailable') + '">' +
|
||||
'<div class="tool-item-left">' +
|
||||
'<span class="tool-status-dot ' + (semanticStatus.available ? 'status-available' : 'status-unavailable') + '"></span>' +
|
||||
'<div class="tool-item-info">' +
|
||||
'<div class="tool-item-name">Semantic Search <span class="tool-type-badge ai">AI</span></div>' +
|
||||
'<div class="tool-item-desc">' + (semanticStatus.available ? 'AI-powered code understanding' : 'Natural language code search') + '</div>' +
|
||||
'</div>' +
|
||||
'</div>' +
|
||||
'<div class="tool-item-right">' +
|
||||
(semanticStatus.available
|
||||
? '<span class="tool-status-text success"><i data-lucide="sparkles" class="w-3.5 h-3.5"></i> ' + (semanticStatus.backend || 'Ready') + '</span>'
|
||||
: '<span class="tool-status-text muted"><i data-lucide="circle-dashed" class="w-3.5 h-3.5"></i> Not Installed</span>' +
|
||||
'<button class="btn-sm btn-primary" onclick="event.stopPropagation(); openSemanticInstallWizard()"><i data-lucide="brain" class="w-3 h-3"></i> Install</button>') +
|
||||
'</div>' +
|
||||
'</div>';
|
||||
}
|
||||
// CodexLens and Semantic Search removed from this list
|
||||
// They are managed in the dedicated CodexLens Manager page (left menu)
|
||||
|
||||
// API Endpoints section
|
||||
var apiEndpointsHtml = '';
|
||||
@@ -848,6 +854,45 @@ function renderToolsSection() {
|
||||
'</div>';
|
||||
}
|
||||
|
||||
// CLI Wrapper (CLI封装) section
|
||||
var cliWrapperHtml = '';
|
||||
if (cliWrapperEndpoints.length > 0) {
|
||||
var wrapperItems = cliWrapperEndpoints.map(function(endpoint) {
|
||||
var isEnabled = endpoint.enabled !== false;
|
||||
var desc = endpoint.description || (t('cli.customClaudeSettings') || 'Custom Claude CLI settings');
|
||||
// Show command hint with name for easy copying
|
||||
var commandHint = 'ccw cli --tool ' + endpoint.name;
|
||||
|
||||
return '<div class="tool-item clickable ' + (isEnabled ? 'available' : 'unavailable') + '" onclick="navigateToApiSettings(\'' + endpoint.id + '\')">' +
|
||||
'<div class="tool-item-left">' +
|
||||
'<span class="tool-status-dot ' + (isEnabled ? 'status-available' : 'status-unavailable') + '"></span>' +
|
||||
'<div class="tool-item-info">' +
|
||||
'<div class="tool-item-name">' + escapeHtml(endpoint.name) + ' <span class="tool-type-badge" style="background: var(--primary); color: white;">' + (t('cli.wrapper') || 'Wrapper') + '</span></div>' +
|
||||
'<div class="tool-item-desc">' + escapeHtml(desc) + '</div>' +
|
||||
'<div class="tool-item-command" style="font-family: var(--font-mono); font-size: 0.7rem; color: var(--muted-foreground); margin-top: 0.25rem;">' + escapeHtml(commandHint) + '</div>' +
|
||||
'</div>' +
|
||||
'</div>' +
|
||||
'<div class="tool-item-right">' +
|
||||
'<label class="toggle-switch" onclick="event.stopPropagation()">' +
|
||||
'<input type="checkbox" ' + (isEnabled ? 'checked' : '') + ' onchange="toggleCliWrapperEnabled(\'' + endpoint.id + '\', this.checked); renderToolsSection();">' +
|
||||
'<span class="toggle-slider"></span>' +
|
||||
'</label>' +
|
||||
'</div>' +
|
||||
'</div>';
|
||||
}).join('');
|
||||
|
||||
var enabledCount = cliWrapperEndpoints.filter(function(e) { return e.enabled !== false; }).length;
|
||||
cliWrapperHtml = '<div class="tools-subsection" style="margin-top: 1rem; padding-top: 1rem; border-top: 1px solid var(--border);">' +
|
||||
'<div class="section-header-left" style="margin-bottom: 0.5rem;">' +
|
||||
'<h4 style="font-size: 0.875rem; font-weight: 600; display: flex; align-items: center; gap: 0.5rem;">' +
|
||||
'<i data-lucide="package-2" class="w-4 h-4"></i> ' + (t('cli.cliWrapper') || 'CLI Wrapper') +
|
||||
'</h4>' +
|
||||
'<span class="section-count">' + enabledCount + '/' + cliWrapperEndpoints.length + ' ' + (t('cli.enabled') || 'enabled') + '</span>' +
|
||||
'</div>' +
|
||||
'<div class="tools-list">' + wrapperItems + '</div>' +
|
||||
'</div>';
|
||||
}
|
||||
|
||||
container.innerHTML = '<div class="section-header">' +
|
||||
'<div class="section-header-left">' +
|
||||
'<h3><i data-lucide="terminal" class="w-4 h-4"></i> ' + t('cli.tools') + '</h3>' +
|
||||
@@ -859,14 +904,34 @@ function renderToolsSection() {
|
||||
'</div>' +
|
||||
'<div class="tools-list">' +
|
||||
toolsHtml +
|
||||
codexLensHtml +
|
||||
semanticHtml +
|
||||
'</div>' +
|
||||
apiEndpointsHtml;
|
||||
apiEndpointsHtml +
|
||||
cliWrapperHtml;
|
||||
|
||||
if (window.lucide) lucide.createIcons();
|
||||
}
|
||||
|
||||
/**
|
||||
* Navigate to API Settings page and open the CLI wrapper endpoint for editing
|
||||
*/
|
||||
function navigateToApiSettings(endpointId) {
|
||||
// Store the endpoint ID to edit after navigation
|
||||
window.pendingCliWrapperEdit = endpointId;
|
||||
|
||||
var navItem = document.querySelector('.nav-item[data-view="api-settings"]');
|
||||
if (navItem) {
|
||||
navItem.click();
|
||||
} else {
|
||||
// Fallback: try to render directly
|
||||
if (typeof renderApiSettings === 'function') {
|
||||
currentView = 'api-settings';
|
||||
renderApiSettings();
|
||||
} else {
|
||||
showRefreshToast(t('common.error') + ': API Settings not available', 'error');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ========== CCW Section (Right Column) ==========
|
||||
function renderCcwSection() {
|
||||
var container = document.getElementById('ccw-section');
|
||||
|
||||
@@ -4497,6 +4497,53 @@ function buildCodexLensManagerPage(config) {
|
||||
'<div class="text-sm text-muted-foreground">Click Load to view/edit ~/.codexlens/.env</div>' +
|
||||
'</div>' +
|
||||
'</div>' +
|
||||
// File Watcher Card (moved from right column)
|
||||
'<div class="bg-card border border-border rounded-lg overflow-hidden">' +
|
||||
'<div class="bg-muted/30 border-b border-border px-4 py-3">' +
|
||||
'<div class="flex items-center justify-between">' +
|
||||
'<div class="flex items-center gap-2">' +
|
||||
'<i data-lucide="eye" class="w-4 h-4 text-primary"></i>' +
|
||||
'<h4 class="font-semibold">File Watcher</h4>' +
|
||||
'</div>' +
|
||||
'<div id="watcherStatusBadge" class="flex items-center gap-2">' +
|
||||
'<span class="text-xs px-2 py-0.5 rounded-full bg-muted text-muted-foreground">Stopped</span>' +
|
||||
'<button class="btn-sm btn-outline" onclick="toggleWatcher()" id="watcherToggleBtn">' +
|
||||
'<i data-lucide="play" class="w-3.5 h-3.5"></i>' +
|
||||
'</button>' +
|
||||
'</div>' +
|
||||
'</div>' +
|
||||
'</div>' +
|
||||
'<div class="p-4">' +
|
||||
'<p class="text-xs text-muted-foreground mb-3">Monitor file changes and auto-update index</p>' +
|
||||
// Stats row
|
||||
'<div class="grid grid-cols-3 gap-2 mb-3">' +
|
||||
'<div class="bg-muted/30 rounded p-2 text-center">' +
|
||||
'<div id="watcherFilesCount" class="text-sm font-semibold">-</div>' +
|
||||
'<div class="text-xs text-muted-foreground">Files</div>' +
|
||||
'</div>' +
|
||||
'<div class="bg-muted/30 rounded p-2 text-center">' +
|
||||
'<div id="watcherChangesCount" class="text-sm font-semibold">0</div>' +
|
||||
'<div class="text-xs text-muted-foreground">Changes</div>' +
|
||||
'</div>' +
|
||||
'<div class="bg-muted/30 rounded p-2 text-center">' +
|
||||
'<div id="watcherUptimeDisplay" class="text-sm font-semibold">-</div>' +
|
||||
'<div class="text-xs text-muted-foreground">Uptime</div>' +
|
||||
'</div>' +
|
||||
'</div>' +
|
||||
// Recent activity log
|
||||
'<div class="border border-border rounded">' +
|
||||
'<div class="bg-muted/30 px-3 py-1.5 border-b border-border text-xs font-medium text-muted-foreground flex items-center justify-between">' +
|
||||
'<span>Recent Activity</span>' +
|
||||
'<button class="text-xs hover:text-foreground" onclick="clearWatcherLog()" title="Clear log">' +
|
||||
'<i data-lucide="trash-2" class="w-3 h-3"></i>' +
|
||||
'</button>' +
|
||||
'</div>' +
|
||||
'<div id="watcherActivityLog" class="h-24 overflow-y-auto p-2 text-xs font-mono bg-background">' +
|
||||
'<div class="text-muted-foreground">No activity yet. Start watcher to monitor files.</div>' +
|
||||
'</div>' +
|
||||
'</div>' +
|
||||
'</div>' +
|
||||
'</div>' +
|
||||
'</div>' +
|
||||
// Right Column
|
||||
'<div class="space-y-6">' +
|
||||
@@ -4544,53 +4591,6 @@ function buildCodexLensManagerPage(config) {
|
||||
'</div>' +
|
||||
'</div>' +
|
||||
'</div>' +
|
||||
// File Watcher Card
|
||||
'<div class="bg-card border border-border rounded-lg overflow-hidden">' +
|
||||
'<div class="bg-muted/30 border-b border-border px-4 py-3">' +
|
||||
'<div class="flex items-center justify-between">' +
|
||||
'<div class="flex items-center gap-2">' +
|
||||
'<i data-lucide="eye" class="w-4 h-4 text-primary"></i>' +
|
||||
'<h4 class="font-semibold">File Watcher</h4>' +
|
||||
'</div>' +
|
||||
'<div id="watcherStatusBadge" class="flex items-center gap-2">' +
|
||||
'<span class="text-xs px-2 py-0.5 rounded-full bg-muted text-muted-foreground">Stopped</span>' +
|
||||
'<button class="btn-sm btn-outline" onclick="toggleWatcher()" id="watcherToggleBtn">' +
|
||||
'<i data-lucide="play" class="w-3.5 h-3.5"></i>' +
|
||||
'</button>' +
|
||||
'</div>' +
|
||||
'</div>' +
|
||||
'</div>' +
|
||||
'<div class="p-4">' +
|
||||
'<p class="text-xs text-muted-foreground mb-3">Monitor file changes and auto-update index</p>' +
|
||||
// Stats row
|
||||
'<div class="grid grid-cols-3 gap-2 mb-3">' +
|
||||
'<div class="bg-muted/30 rounded p-2 text-center">' +
|
||||
'<div id="watcherFilesCount" class="text-sm font-semibold">-</div>' +
|
||||
'<div class="text-xs text-muted-foreground">Files</div>' +
|
||||
'</div>' +
|
||||
'<div class="bg-muted/30 rounded p-2 text-center">' +
|
||||
'<div id="watcherChangesCount" class="text-sm font-semibold">0</div>' +
|
||||
'<div class="text-xs text-muted-foreground">Changes</div>' +
|
||||
'</div>' +
|
||||
'<div class="bg-muted/30 rounded p-2 text-center">' +
|
||||
'<div id="watcherUptimeDisplay" class="text-sm font-semibold">-</div>' +
|
||||
'<div class="text-xs text-muted-foreground">Uptime</div>' +
|
||||
'</div>' +
|
||||
'</div>' +
|
||||
// Recent activity log
|
||||
'<div class="border border-border rounded">' +
|
||||
'<div class="bg-muted/30 px-3 py-1.5 border-b border-border text-xs font-medium text-muted-foreground flex items-center justify-between">' +
|
||||
'<span>Recent Activity</span>' +
|
||||
'<button class="text-xs hover:text-foreground" onclick="clearWatcherLog()" title="Clear log">' +
|
||||
'<i data-lucide="trash-2" class="w-3 h-3"></i>' +
|
||||
'</button>' +
|
||||
'</div>' +
|
||||
'<div id="watcherActivityLog" class="h-24 overflow-y-auto p-2 text-xs font-mono bg-background">' +
|
||||
'<div class="text-muted-foreground">No activity yet. Start watcher to monitor files.</div>' +
|
||||
'</div>' +
|
||||
'</div>' +
|
||||
'</div>' +
|
||||
'</div>' +
|
||||
'</div>' +
|
||||
'</div>' +
|
||||
// Ignore Patterns Section
|
||||
|
||||
Reference in New Issue
Block a user