mirror of
https://github.com/catlog22/Claude-Code-Workflow.git
synced 2026-03-20 19:03:51 +08:00
feat: add CodexLens v2 management features including model management, index operations, env config, and MCP config
- Implemented CodexLens routes for model listing, downloading, and deleting. - Added hooks for managing CodexLens models, index status, environment variables, and MCP configuration. - Created frontend components for managing environment settings, index status, and models. - Developed the main CodexLens page with tab navigation for easy access to different management features. - Introduced a new file structure for CodexLens related components and hooks.
This commit is contained in:
208
ccw/frontend/src/components/codexlens/EnvSettingsTab.tsx
Normal file
208
ccw/frontend/src/components/codexlens/EnvSettingsTab.tsx
Normal file
@@ -0,0 +1,208 @@
|
|||||||
|
// ========================================
|
||||||
|
// EnvSettingsTab Component
|
||||||
|
// ========================================
|
||||||
|
// Grouped env var form with save/reset actions
|
||||||
|
|
||||||
|
import { useState, useEffect } from 'react';
|
||||||
|
import { useIntl } from 'react-intl';
|
||||||
|
import { Eye, EyeOff, Save, RotateCcw } from 'lucide-react';
|
||||||
|
import { Card, CardHeader, CardTitle, CardContent } from '@/components/ui/Card';
|
||||||
|
import { Button } from '@/components/ui/Button';
|
||||||
|
import { Input } from '@/components/ui/Input';
|
||||||
|
import { useCodexLensEnv, useSaveCodexLensEnv } from '@/hooks/useCodexLens';
|
||||||
|
|
||||||
|
// ========================================
|
||||||
|
// ENV group definitions
|
||||||
|
// ========================================
|
||||||
|
|
||||||
|
interface EnvField {
|
||||||
|
key: string;
|
||||||
|
label: string;
|
||||||
|
sensitive?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface EnvGroup {
|
||||||
|
title: string;
|
||||||
|
fields: EnvField[];
|
||||||
|
}
|
||||||
|
|
||||||
|
const ENV_GROUPS: EnvGroup[] = [
|
||||||
|
{
|
||||||
|
title: 'embed',
|
||||||
|
fields: [
|
||||||
|
{ key: 'CODEXLENS_EMBED_API_URL', label: 'Embed API URL' },
|
||||||
|
{ key: 'CODEXLENS_EMBED_API_KEY', label: 'Embed API Key', sensitive: true },
|
||||||
|
{ key: 'CODEXLENS_EMBED_API_MODEL', label: 'Embed API Model' },
|
||||||
|
{ key: 'CODEXLENS_EMBED_DIM', label: 'Embed Dimension' },
|
||||||
|
{ key: 'CODEXLENS_EMBED_API_ENDPOINTS', label: 'Embed API Endpoints', sensitive: true },
|
||||||
|
{ key: 'CODEXLENS_EMBED_BATCH_SIZE', label: 'Embed Batch Size' },
|
||||||
|
{ key: 'CODEXLENS_EMBED_API_CONCURRENCY', label: 'Embed API Concurrency' },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: 'reranker',
|
||||||
|
fields: [
|
||||||
|
{ key: 'CODEXLENS_RERANKER_API_URL', label: 'Reranker API URL' },
|
||||||
|
{ key: 'CODEXLENS_RERANKER_API_KEY', label: 'Reranker API Key', sensitive: true },
|
||||||
|
{ key: 'CODEXLENS_RERANKER_API_MODEL', label: 'Reranker API Model' },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: 'performance',
|
||||||
|
fields: [
|
||||||
|
{ key: 'CODEXLENS_BINARY_TOP_K', label: 'Binary Top K' },
|
||||||
|
{ key: 'CODEXLENS_ANN_TOP_K', label: 'ANN Top K' },
|
||||||
|
{ key: 'CODEXLENS_FTS_TOP_K', label: 'FTS Top K' },
|
||||||
|
{ key: 'CODEXLENS_FUSION_K', label: 'Fusion K' },
|
||||||
|
{ key: 'CODEXLENS_RERANKER_TOP_K', label: 'Reranker Top K' },
|
||||||
|
{ key: 'CODEXLENS_RERANKER_BATCH_SIZE', label: 'Reranker Batch Size' },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: 'index',
|
||||||
|
fields: [
|
||||||
|
{ key: 'CODEXLENS_DB_PATH', label: 'DB Path' },
|
||||||
|
{ key: 'CODEXLENS_INDEX_WORKERS', label: 'Index Workers' },
|
||||||
|
{ key: 'CODEXLENS_CODE_AWARE_CHUNKING', label: 'Code Aware Chunking' },
|
||||||
|
{ key: 'CODEXLENS_MAX_FILE_SIZE', label: 'Max File Size' },
|
||||||
|
{ key: 'CODEXLENS_HNSW_EF', label: 'HNSW EF' },
|
||||||
|
{ key: 'CODEXLENS_HNSW_M', label: 'HNSW M' },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
// Collect all keys
|
||||||
|
const ALL_KEYS = ENV_GROUPS.flatMap((g) => g.fields.map((f) => f.key));
|
||||||
|
|
||||||
|
function buildEmptyEnv(): Record<string, string> {
|
||||||
|
return Object.fromEntries(ALL_KEYS.map((k) => [k, '']));
|
||||||
|
}
|
||||||
|
|
||||||
|
// ========================================
|
||||||
|
// Sensitive field with show/hide toggle
|
||||||
|
// ========================================
|
||||||
|
|
||||||
|
interface SensitiveInputProps {
|
||||||
|
value: string;
|
||||||
|
onChange: (v: string) => void;
|
||||||
|
id: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
function SensitiveInput({ value, onChange, id }: SensitiveInputProps) {
|
||||||
|
const [show, setShow] = useState(false);
|
||||||
|
return (
|
||||||
|
<div className="relative">
|
||||||
|
<Input
|
||||||
|
id={id}
|
||||||
|
type={show ? 'text' : 'password'}
|
||||||
|
value={value}
|
||||||
|
onChange={(e) => onChange(e.target.value)}
|
||||||
|
className="pr-10"
|
||||||
|
/>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className="absolute inset-y-0 right-2 flex items-center text-muted-foreground hover:text-foreground"
|
||||||
|
onClick={() => setShow((s) => !s)}
|
||||||
|
tabIndex={-1}
|
||||||
|
>
|
||||||
|
{show ? <EyeOff className="w-4 h-4" /> : <Eye className="w-4 h-4" />}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ========================================
|
||||||
|
// Main component
|
||||||
|
// ========================================
|
||||||
|
|
||||||
|
export function EnvSettingsTab() {
|
||||||
|
const { formatMessage } = useIntl();
|
||||||
|
const { data: serverEnv, isLoading } = useCodexLensEnv();
|
||||||
|
const { saveEnv, isSaving } = useSaveCodexLensEnv();
|
||||||
|
|
||||||
|
const [localEnv, setLocalEnv] = useState<Record<string, string>>(buildEmptyEnv);
|
||||||
|
|
||||||
|
// Sync server state into local when loaded
|
||||||
|
useEffect(() => {
|
||||||
|
if (serverEnv) {
|
||||||
|
setLocalEnv((prev) => {
|
||||||
|
const next = { ...prev };
|
||||||
|
ALL_KEYS.forEach((k) => {
|
||||||
|
next[k] = serverEnv[k] ?? '';
|
||||||
|
});
|
||||||
|
return next;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}, [serverEnv]);
|
||||||
|
|
||||||
|
const serverRecord = serverEnv ?? {};
|
||||||
|
|
||||||
|
const isDirty = ALL_KEYS.some((k) => localEnv[k] !== (serverRecord[k] ?? ''));
|
||||||
|
|
||||||
|
const handleChange = (key: string, value: string) => {
|
||||||
|
setLocalEnv((prev) => ({ ...prev, [key]: value }));
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleSave = async () => {
|
||||||
|
await saveEnv(localEnv);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleReset = () => {
|
||||||
|
setLocalEnv(buildEmptyEnv());
|
||||||
|
};
|
||||||
|
|
||||||
|
if (isLoading) {
|
||||||
|
return <p className="text-sm text-muted-foreground p-4">{formatMessage({ id: 'codexlens.env.loading' })}</p>;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="space-y-6">
|
||||||
|
{ENV_GROUPS.map((group) => (
|
||||||
|
<Card key={group.title}>
|
||||||
|
<CardHeader className="pb-3">
|
||||||
|
<CardTitle className="text-base">{formatMessage({ id: `codexlens.env.sections.${group.title}` })}</CardTitle>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent className="space-y-3">
|
||||||
|
{group.fields.map((field) => (
|
||||||
|
<div key={field.key} className="grid grid-cols-3 gap-3 items-center">
|
||||||
|
<label
|
||||||
|
htmlFor={field.key}
|
||||||
|
className="text-sm text-muted-foreground col-span-1"
|
||||||
|
>
|
||||||
|
{field.label}
|
||||||
|
</label>
|
||||||
|
<div className="col-span-2">
|
||||||
|
{field.sensitive ? (
|
||||||
|
<SensitiveInput
|
||||||
|
id={field.key}
|
||||||
|
value={localEnv[field.key] ?? ''}
|
||||||
|
onChange={(v) => handleChange(field.key, v)}
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
<Input
|
||||||
|
id={field.key}
|
||||||
|
value={localEnv[field.key] ?? ''}
|
||||||
|
onChange={(e) => handleChange(field.key, e.target.value)}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
))}
|
||||||
|
|
||||||
|
{/* Action buttons */}
|
||||||
|
<div className="flex justify-between pt-2">
|
||||||
|
<Button variant="outline" onClick={handleReset}>
|
||||||
|
<RotateCcw className="w-4 h-4 mr-2" />
|
||||||
|
{formatMessage({ id: 'codexlens.env.clearForm' })}
|
||||||
|
</Button>
|
||||||
|
<Button onClick={handleSave} disabled={!isDirty || isSaving}>
|
||||||
|
<Save className="w-4 h-4 mr-2" />
|
||||||
|
{isSaving ? formatMessage({ id: 'codexlens.env.saving' }) : formatMessage({ id: 'codexlens.env.save' })}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
162
ccw/frontend/src/components/codexlens/IndexManagerTab.tsx
Normal file
162
ccw/frontend/src/components/codexlens/IndexManagerTab.tsx
Normal file
@@ -0,0 +1,162 @@
|
|||||||
|
// ========================================
|
||||||
|
// IndexManagerTab Component
|
||||||
|
// ========================================
|
||||||
|
// Project path input, index status display, and sync/rebuild actions
|
||||||
|
|
||||||
|
import { useState } from 'react';
|
||||||
|
import { useIntl } from 'react-intl';
|
||||||
|
import { Plus, Trash2, RefreshCw, Loader2 } from 'lucide-react';
|
||||||
|
import { useQueryClient } from '@tanstack/react-query';
|
||||||
|
import { Card, CardHeader, CardTitle, CardContent } from '@/components/ui/Card';
|
||||||
|
import { Button } from '@/components/ui/Button';
|
||||||
|
import { Input } from '@/components/ui/Input';
|
||||||
|
import { useIndexStatus, useSyncIndex, useRebuildIndex, codexLensKeys, type IndexStatusData } from '@/hooks/useCodexLens';
|
||||||
|
|
||||||
|
interface ProjectStatusCardProps {
|
||||||
|
projectPath: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
function ProjectStatusCard({ projectPath }: ProjectStatusCardProps) {
|
||||||
|
const { formatMessage } = useIntl();
|
||||||
|
const queryClient = useQueryClient();
|
||||||
|
const { data: statusData, isLoading, isError } = useIndexStatus(projectPath);
|
||||||
|
const { syncIndex, isSyncing } = useSyncIndex();
|
||||||
|
const { rebuildIndex, isRebuilding } = useRebuildIndex();
|
||||||
|
|
||||||
|
const status: IndexStatusData | undefined = statusData;
|
||||||
|
|
||||||
|
const handleRefresh = () => {
|
||||||
|
queryClient.invalidateQueries({ queryKey: codexLensKeys.indexStatus(projectPath) });
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Card>
|
||||||
|
<CardHeader className="pb-3">
|
||||||
|
<CardTitle className="text-sm font-medium truncate">{projectPath}</CardTitle>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent className="space-y-4">
|
||||||
|
{/* Status stats */}
|
||||||
|
{isLoading && (
|
||||||
|
<div className="flex items-center gap-2 text-sm text-muted-foreground">
|
||||||
|
<Loader2 className="w-4 h-4 animate-spin" />
|
||||||
|
{formatMessage({ id: 'codexlens.index.loading' })}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
{isError && (
|
||||||
|
<p className="text-sm text-destructive">{formatMessage({ id: 'codexlens.index.error' })}</p>
|
||||||
|
)}
|
||||||
|
{!isLoading && !isError && status && (
|
||||||
|
<div className="grid grid-cols-3 gap-3">
|
||||||
|
<div className="text-center">
|
||||||
|
<p className="text-lg font-semibold">{status.files_tracked ?? 0}</p>
|
||||||
|
<p className="text-xs text-muted-foreground">{formatMessage({ id: 'codexlens.index.filesTracked' })}</p>
|
||||||
|
</div>
|
||||||
|
<div className="text-center">
|
||||||
|
<p className="text-lg font-semibold">{status.total_chunks ?? 0}</p>
|
||||||
|
<p className="text-xs text-muted-foreground">{formatMessage({ id: 'codexlens.index.totalChunks' })}</p>
|
||||||
|
</div>
|
||||||
|
<div className="text-center">
|
||||||
|
<p className="text-lg font-semibold">{status.deleted_chunks ?? 0}</p>
|
||||||
|
<p className="text-xs text-muted-foreground">{formatMessage({ id: 'codexlens.index.deletedChunks' })}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Action buttons */}
|
||||||
|
<div className="flex gap-2 flex-wrap">
|
||||||
|
<Button
|
||||||
|
variant="outline"
|
||||||
|
size="sm"
|
||||||
|
onClick={() => syncIndex(projectPath)}
|
||||||
|
disabled={isSyncing}
|
||||||
|
>
|
||||||
|
{isSyncing ? <Loader2 className="w-4 h-4 animate-spin mr-1" /> : null}
|
||||||
|
{formatMessage({ id: 'codexlens.index.sync' })}
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
variant="outline"
|
||||||
|
size="sm"
|
||||||
|
onClick={() => rebuildIndex(projectPath)}
|
||||||
|
disabled={isRebuilding}
|
||||||
|
>
|
||||||
|
{isRebuilding ? <Loader2 className="w-4 h-4 animate-spin mr-1" /> : null}
|
||||||
|
{formatMessage({ id: 'codexlens.index.rebuild' })}
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
variant="outline"
|
||||||
|
size="sm"
|
||||||
|
onClick={handleRefresh}
|
||||||
|
>
|
||||||
|
<RefreshCw className="w-4 h-4 mr-1" />
|
||||||
|
{formatMessage({ id: 'codexlens.index.refresh' })}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function IndexManagerTab() {
|
||||||
|
const { formatMessage } = useIntl();
|
||||||
|
const [paths, setPaths] = useState<string[]>([]);
|
||||||
|
const [inputValue, setInputValue] = useState('');
|
||||||
|
|
||||||
|
const handleAdd = () => {
|
||||||
|
const trimmed = inputValue.trim();
|
||||||
|
if (trimmed && !paths.includes(trimmed)) {
|
||||||
|
setPaths((prev) => [...prev, trimmed]);
|
||||||
|
}
|
||||||
|
setInputValue('');
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleRemove = (path: string) => {
|
||||||
|
setPaths((prev) => prev.filter((p) => p !== path));
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleKeyDown = (e: React.KeyboardEvent<HTMLInputElement>) => {
|
||||||
|
if (e.key === 'Enter') {
|
||||||
|
handleAdd();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="space-y-4">
|
||||||
|
{/* Add project path */}
|
||||||
|
<div className="flex gap-2">
|
||||||
|
<Input
|
||||||
|
placeholder={formatMessage({ id: 'codexlens.index.pathPlaceholder' })}
|
||||||
|
value={inputValue}
|
||||||
|
onChange={(e) => setInputValue(e.target.value)}
|
||||||
|
onKeyDown={handleKeyDown}
|
||||||
|
className="flex-1"
|
||||||
|
/>
|
||||||
|
<Button onClick={handleAdd} disabled={!inputValue.trim()}>
|
||||||
|
<Plus className="w-4 h-4 mr-1" />
|
||||||
|
{formatMessage({ id: 'codexlens.index.add' })}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{paths.length === 0 && (
|
||||||
|
<p className="text-sm text-muted-foreground">
|
||||||
|
{formatMessage({ id: 'codexlens.index.empty' })}
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Project status cards */}
|
||||||
|
{paths.map((path) => (
|
||||||
|
<div key={path} className="relative">
|
||||||
|
<Button
|
||||||
|
variant="ghost"
|
||||||
|
size="sm"
|
||||||
|
className="absolute top-2 right-2 z-10 h-7 w-7 p-0"
|
||||||
|
onClick={() => handleRemove(path)}
|
||||||
|
title={formatMessage({ id: 'codexlens.index.removeProject' })}
|
||||||
|
>
|
||||||
|
<Trash2 className="w-4 h-4 text-muted-foreground" />
|
||||||
|
</Button>
|
||||||
|
<ProjectStatusCard projectPath={path} />
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
105
ccw/frontend/src/components/codexlens/McpConfigTab.tsx
Normal file
105
ccw/frontend/src/components/codexlens/McpConfigTab.tsx
Normal file
@@ -0,0 +1,105 @@
|
|||||||
|
// ========================================
|
||||||
|
// McpConfigTab Component
|
||||||
|
// ========================================
|
||||||
|
// Read-only MCP config JSON display with copy-to-clipboard and regenerate
|
||||||
|
|
||||||
|
import { useState } from 'react';
|
||||||
|
import { useIntl } from 'react-intl';
|
||||||
|
import { Copy, RefreshCw, Check } from 'lucide-react';
|
||||||
|
import { Card, CardHeader, CardTitle, CardContent } from '@/components/ui/Card';
|
||||||
|
import { Button } from '@/components/ui/Button';
|
||||||
|
import { Badge } from '@/components/ui/Badge';
|
||||||
|
import { useCodexLensMcpConfig, useCodexLensEnv } from '@/hooks/useCodexLens';
|
||||||
|
|
||||||
|
export function McpConfigTab() {
|
||||||
|
const { formatMessage } = useIntl();
|
||||||
|
const { data: mcpConfig, isLoading, isError, refetch } = useCodexLensMcpConfig();
|
||||||
|
const { data: envData } = useCodexLensEnv();
|
||||||
|
const [copied, setCopied] = useState(false);
|
||||||
|
|
||||||
|
const hasApiUrl = !!(envData?.CODEXLENS_EMBED_API_URL);
|
||||||
|
const embedMode = hasApiUrl ? 'API' : 'Local fastembed';
|
||||||
|
|
||||||
|
const configJson = mcpConfig ? JSON.stringify(mcpConfig, null, 2) : '';
|
||||||
|
|
||||||
|
const handleCopy = () => {
|
||||||
|
navigator.clipboard.writeText(configJson).then(() => {
|
||||||
|
setCopied(true);
|
||||||
|
setTimeout(() => setCopied(false), 2000);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleRegenerate = () => {
|
||||||
|
refetch();
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="space-y-4">
|
||||||
|
{/* Embed mode badge */}
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<span className="text-sm text-muted-foreground">{formatMessage({ id: 'codexlens.mcp.embedMode' })}:</span>
|
||||||
|
<Badge variant={hasApiUrl ? 'success' : 'secondary'}>
|
||||||
|
{embedMode}
|
||||||
|
</Badge>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Config JSON block */}
|
||||||
|
<Card>
|
||||||
|
<CardHeader className="pb-3">
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
|
<CardTitle className="text-base">{formatMessage({ id: 'codexlens.mcp.configTitle' })}</CardTitle>
|
||||||
|
<div className="flex gap-2">
|
||||||
|
<Button
|
||||||
|
variant="outline"
|
||||||
|
size="sm"
|
||||||
|
onClick={handleCopy}
|
||||||
|
disabled={!configJson || isLoading}
|
||||||
|
>
|
||||||
|
{copied ? <Check className="w-4 h-4 mr-1" /> : <Copy className="w-4 h-4 mr-1" />}
|
||||||
|
{copied ? formatMessage({ id: 'codexlens.mcp.copied' }) : formatMessage({ id: 'codexlens.mcp.copy' })}
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
variant="outline"
|
||||||
|
size="sm"
|
||||||
|
onClick={handleRegenerate}
|
||||||
|
disabled={isLoading}
|
||||||
|
>
|
||||||
|
<RefreshCw className={`w-4 h-4 mr-1 ${isLoading ? 'animate-spin' : ''}`} />
|
||||||
|
{formatMessage({ id: 'codexlens.mcp.regenerate' })}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent>
|
||||||
|
{isLoading && (
|
||||||
|
<p className="text-sm text-muted-foreground">{formatMessage({ id: 'codexlens.mcp.loading' })}</p>
|
||||||
|
)}
|
||||||
|
{isError && (
|
||||||
|
<p className="text-sm text-destructive">{formatMessage({ id: 'codexlens.mcp.error' })}</p>
|
||||||
|
)}
|
||||||
|
{!isLoading && !isError && (
|
||||||
|
<pre className="bg-muted rounded-md p-4 text-xs overflow-auto max-h-96 font-mono">
|
||||||
|
{configJson || formatMessage({ id: 'codexlens.mcp.noConfig' })}
|
||||||
|
</pre>
|
||||||
|
)}
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
|
||||||
|
{/* Installation instructions */}
|
||||||
|
<Card>
|
||||||
|
<CardHeader className="pb-3">
|
||||||
|
<CardTitle className="text-base">{formatMessage({ id: 'codexlens.mcp.installTitle' })}</CardTitle>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent>
|
||||||
|
<ol className="list-decimal list-inside space-y-2 text-sm text-muted-foreground">
|
||||||
|
<li>{formatMessage({ id: 'codexlens.mcp.installSteps.step1' })}</li>
|
||||||
|
<li>{formatMessage({ id: 'codexlens.mcp.installSteps.step2' })}</li>
|
||||||
|
<li>{formatMessage({ id: 'codexlens.mcp.installSteps.step3' })}</li>
|
||||||
|
<li>{formatMessage({ id: 'codexlens.mcp.installSteps.step4' })}</li>
|
||||||
|
<li>{formatMessage({ id: 'codexlens.mcp.installSteps.step5' })}</li>
|
||||||
|
</ol>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
109
ccw/frontend/src/components/codexlens/ModelManagerTab.tsx
Normal file
109
ccw/frontend/src/components/codexlens/ModelManagerTab.tsx
Normal file
@@ -0,0 +1,109 @@
|
|||||||
|
// ========================================
|
||||||
|
// ModelManagerTab Component
|
||||||
|
// ========================================
|
||||||
|
// Model list with download/delete actions and embed mode banner
|
||||||
|
|
||||||
|
import { useIntl } from 'react-intl';
|
||||||
|
import { Download, Trash2, Loader2 } from 'lucide-react';
|
||||||
|
import { Card, CardContent } from '@/components/ui/Card';
|
||||||
|
import { Button } from '@/components/ui/Button';
|
||||||
|
import { Badge } from '@/components/ui/Badge';
|
||||||
|
import {
|
||||||
|
useCodexLensModels,
|
||||||
|
useDownloadModel,
|
||||||
|
useDeleteModel,
|
||||||
|
useCodexLensEnv,
|
||||||
|
type ModelEntry,
|
||||||
|
} from '@/hooks/useCodexLens';
|
||||||
|
|
||||||
|
export function ModelManagerTab() {
|
||||||
|
const { formatMessage } = useIntl();
|
||||||
|
const { data: modelsData, isLoading, isError } = useCodexLensModels();
|
||||||
|
const { data: envData } = useCodexLensEnv();
|
||||||
|
const { downloadModel, isDownloading } = useDownloadModel();
|
||||||
|
const { deleteModel, isDeleting } = useDeleteModel();
|
||||||
|
|
||||||
|
const hasApiUrl = !!(envData?.CODEXLENS_EMBED_API_URL);
|
||||||
|
const embedMode = hasApiUrl ? 'API' : 'Local fastembed';
|
||||||
|
|
||||||
|
const models: ModelEntry[] = modelsData ?? [];
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="space-y-4">
|
||||||
|
{/* Embed mode banner */}
|
||||||
|
<div className="flex items-center gap-2 p-3 rounded-md bg-muted text-sm">
|
||||||
|
<span className="text-muted-foreground">{formatMessage({ id: 'codexlens.models.embedMode' })}:</span>
|
||||||
|
<Badge variant={hasApiUrl ? 'success' : 'secondary'}>{embedMode}</Badge>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Loading / Error states */}
|
||||||
|
{isLoading && (
|
||||||
|
<div className="flex items-center gap-2 text-sm text-muted-foreground p-4">
|
||||||
|
<Loader2 className="w-4 h-4 animate-spin" />
|
||||||
|
{formatMessage({ id: 'codexlens.models.loading' })}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{isError && (
|
||||||
|
<p className="text-sm text-destructive p-4">{formatMessage({ id: 'codexlens.models.error' })}</p>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Model list */}
|
||||||
|
{!isLoading && !isError && models.length === 0 && (
|
||||||
|
<p className="text-sm text-muted-foreground p-4">{formatMessage({ id: 'codexlens.models.noModels' })}</p>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{!isLoading && !isError && models.length > 0 && (
|
||||||
|
<Card>
|
||||||
|
<CardContent className="p-0">
|
||||||
|
<div className="divide-y divide-border">
|
||||||
|
{models.map((model) => (
|
||||||
|
<div
|
||||||
|
key={model.name}
|
||||||
|
className="flex items-center justify-between px-4 py-3"
|
||||||
|
>
|
||||||
|
<div className="flex items-center gap-3">
|
||||||
|
<span className="text-sm font-medium">{model.name}</span>
|
||||||
|
<Badge variant={model.installed ? 'success' : 'secondary'}>
|
||||||
|
{model.installed
|
||||||
|
? formatMessage({ id: 'codexlens.models.installed' })
|
||||||
|
: formatMessage({ id: 'codexlens.models.notInstalled' })}
|
||||||
|
</Badge>
|
||||||
|
</div>
|
||||||
|
<div className="flex gap-2">
|
||||||
|
<Button
|
||||||
|
variant="outline"
|
||||||
|
size="sm"
|
||||||
|
onClick={() => downloadModel(model.name)}
|
||||||
|
disabled={isDownloading}
|
||||||
|
>
|
||||||
|
{isDownloading ? (
|
||||||
|
<Loader2 className="w-4 h-4 animate-spin" />
|
||||||
|
) : (
|
||||||
|
<Download className="w-4 h-4" />
|
||||||
|
)}
|
||||||
|
<span className="ml-1">{formatMessage({ id: 'codexlens.models.download' })}</span>
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
variant="outline"
|
||||||
|
size="sm"
|
||||||
|
onClick={() => deleteModel(model.name)}
|
||||||
|
disabled={!model.installed || isDeleting}
|
||||||
|
>
|
||||||
|
{isDeleting ? (
|
||||||
|
<Loader2 className="w-4 h-4 animate-spin" />
|
||||||
|
) : (
|
||||||
|
<Trash2 className="w-4 h-4" />
|
||||||
|
)}
|
||||||
|
<span className="ml-1">{formatMessage({ id: 'codexlens.models.delete' })}</span>
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -29,6 +29,7 @@ import {
|
|||||||
ScrollText,
|
ScrollText,
|
||||||
Clock,
|
Clock,
|
||||||
BookOpen,
|
BookOpen,
|
||||||
|
Search,
|
||||||
} from 'lucide-react';
|
} from 'lucide-react';
|
||||||
import { cn } from '@/lib/utils';
|
import { cn } from '@/lib/utils';
|
||||||
import { Button } from '@/components/ui/Button';
|
import { Button } from '@/components/ui/Button';
|
||||||
@@ -107,6 +108,7 @@ const navGroupDefinitions: NavGroupDef[] = [
|
|||||||
{ path: '/hooks', labelKey: 'navigation.main.hooks', icon: GitFork },
|
{ path: '/hooks', labelKey: 'navigation.main.hooks', icon: GitFork },
|
||||||
{ path: '/settings/mcp', labelKey: 'navigation.main.mcp', icon: Server },
|
{ path: '/settings/mcp', labelKey: 'navigation.main.mcp', icon: Server },
|
||||||
{ path: '/settings/specs', labelKey: 'navigation.main.specs', icon: ScrollText },
|
{ path: '/settings/specs', labelKey: 'navigation.main.specs', icon: ScrollText },
|
||||||
|
{ path: '/codexlens', labelKey: 'navigation.main.codexlens', icon: Search },
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -127,8 +127,9 @@ const RECOMMENDED_MCP_DEFINITIONS: RecommendedMcpDefinition[] = [
|
|||||||
},
|
},
|
||||||
],
|
],
|
||||||
buildConfig: (values) => {
|
buildConfig: (values) => {
|
||||||
const env = values.apiKey ? { EXA_API_KEY: values.apiKey } : undefined;
|
const baseUrl = 'https://mcp.exa.ai/mcp';
|
||||||
return buildCrossPlatformMcpConfig('npx', ['-y', 'exa-mcp-server'], { env });
|
const url = values.apiKey ? `${baseUrl}?exaApiKey=${values.apiKey}` : baseUrl;
|
||||||
|
return buildCrossPlatformMcpConfig('npx', ['-y', 'mcp-remote', url]);
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|||||||
214
ccw/frontend/src/hooks/useCodexLens.ts
Normal file
214
ccw/frontend/src/hooks/useCodexLens.ts
Normal file
@@ -0,0 +1,214 @@
|
|||||||
|
// ========================================
|
||||||
|
// useCodexLens Hook
|
||||||
|
// ========================================
|
||||||
|
// TanStack Query hooks for CodexLens v2 API management
|
||||||
|
|
||||||
|
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
|
||||||
|
|
||||||
|
// ========================================
|
||||||
|
// Domain types (exported for component use)
|
||||||
|
// ========================================
|
||||||
|
|
||||||
|
export interface ModelEntry {
|
||||||
|
name: string;
|
||||||
|
installed: boolean;
|
||||||
|
type?: string;
|
||||||
|
cache_path?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface IndexStatusData {
|
||||||
|
status?: string;
|
||||||
|
files_tracked?: number;
|
||||||
|
total_chunks?: number;
|
||||||
|
deleted_chunks?: number;
|
||||||
|
db_path?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export type McpConfigData = Record<string, unknown>;
|
||||||
|
|
||||||
|
// Internal API response wrappers
|
||||||
|
interface ModelsResponse { success: boolean; models: ModelEntry[] }
|
||||||
|
interface IndexStatusResponse { success: boolean; status: IndexStatusData }
|
||||||
|
interface EnvResponse { success: boolean; env: Record<string, string> }
|
||||||
|
interface McpConfigResponse { success: boolean; config: McpConfigData }
|
||||||
|
|
||||||
|
// ========================================
|
||||||
|
// Query Key Factory
|
||||||
|
// ========================================
|
||||||
|
|
||||||
|
export const codexLensKeys = {
|
||||||
|
all: ['codexLens'] as const,
|
||||||
|
models: () => [...codexLensKeys.all, 'models'] as const,
|
||||||
|
indexStatus: (path: string) => [...codexLensKeys.all, 'indexStatus', path] as const,
|
||||||
|
env: () => [...codexLensKeys.all, 'env'] as const,
|
||||||
|
mcpConfig: () => [...codexLensKeys.all, 'mcpConfig'] as const,
|
||||||
|
};
|
||||||
|
|
||||||
|
// ========================================
|
||||||
|
// Internal fetch helper (mirrors fetchApi pattern from api.ts)
|
||||||
|
// ========================================
|
||||||
|
|
||||||
|
async function fetchApi<T>(url: string, options: RequestInit = {}): Promise<T> {
|
||||||
|
const headers = new Headers(options.headers);
|
||||||
|
if (options.body && typeof options.body === 'string') {
|
||||||
|
headers.set('Content-Type', 'application/json');
|
||||||
|
}
|
||||||
|
const response = await fetch(url, {
|
||||||
|
...options,
|
||||||
|
headers,
|
||||||
|
credentials: 'same-origin',
|
||||||
|
});
|
||||||
|
if (!response.ok) {
|
||||||
|
const text = await response.text().catch(() => response.statusText);
|
||||||
|
throw new Error(text || `Request failed: ${response.status}`);
|
||||||
|
}
|
||||||
|
return response.json() as Promise<T>;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ========================================
|
||||||
|
// Models Hooks
|
||||||
|
// ========================================
|
||||||
|
|
||||||
|
export function useCodexLensModels() {
|
||||||
|
return useQuery({
|
||||||
|
queryKey: codexLensKeys.models(),
|
||||||
|
queryFn: () => fetchApi<ModelsResponse>('/api/codexlens/models').then(r => r.models),
|
||||||
|
staleTime: 30_000,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export function useDownloadModel() {
|
||||||
|
const queryClient = useQueryClient();
|
||||||
|
|
||||||
|
const mutation = useMutation({
|
||||||
|
mutationFn: (modelName: string) =>
|
||||||
|
fetchApi<{ success: boolean }>('/api/codexlens/models/download', {
|
||||||
|
method: 'POST',
|
||||||
|
body: JSON.stringify({ name: modelName }),
|
||||||
|
}),
|
||||||
|
onSuccess: () => {
|
||||||
|
queryClient.invalidateQueries({ queryKey: codexLensKeys.models() });
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
return {
|
||||||
|
downloadModel: mutation.mutateAsync,
|
||||||
|
isDownloading: mutation.isPending,
|
||||||
|
error: mutation.error,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export function useDeleteModel() {
|
||||||
|
const queryClient = useQueryClient();
|
||||||
|
|
||||||
|
const mutation = useMutation({
|
||||||
|
mutationFn: (modelName: string) =>
|
||||||
|
fetchApi<{ success: boolean }>('/api/codexlens/models/delete', {
|
||||||
|
method: 'POST',
|
||||||
|
body: JSON.stringify({ name: modelName }),
|
||||||
|
}),
|
||||||
|
onSuccess: () => {
|
||||||
|
queryClient.invalidateQueries({ queryKey: codexLensKeys.models() });
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
return {
|
||||||
|
deleteModel: mutation.mutateAsync,
|
||||||
|
isDeleting: mutation.isPending,
|
||||||
|
error: mutation.error,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// ========================================
|
||||||
|
// Index Hooks
|
||||||
|
// ========================================
|
||||||
|
|
||||||
|
export function useIndexStatus(projectPath: string) {
|
||||||
|
return useQuery({
|
||||||
|
queryKey: codexLensKeys.indexStatus(projectPath),
|
||||||
|
queryFn: () =>
|
||||||
|
fetchApi<IndexStatusResponse>(
|
||||||
|
`/api/codexlens/index/status?projectPath=${encodeURIComponent(projectPath)}`
|
||||||
|
).then(r => r.status),
|
||||||
|
enabled: !!projectPath,
|
||||||
|
staleTime: 10_000,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export function useSyncIndex() {
|
||||||
|
const mutation = useMutation({
|
||||||
|
mutationFn: (projectPath: string) =>
|
||||||
|
fetchApi<{ success: boolean }>('/api/codexlens/index/sync', {
|
||||||
|
method: 'POST',
|
||||||
|
body: JSON.stringify({ projectPath }),
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
|
||||||
|
return {
|
||||||
|
syncIndex: mutation.mutateAsync,
|
||||||
|
isSyncing: mutation.isPending,
|
||||||
|
error: mutation.error,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export function useRebuildIndex() {
|
||||||
|
const mutation = useMutation({
|
||||||
|
mutationFn: (projectPath: string) =>
|
||||||
|
fetchApi<{ success: boolean }>('/api/codexlens/index/rebuild', {
|
||||||
|
method: 'POST',
|
||||||
|
body: JSON.stringify({ projectPath }),
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
|
||||||
|
return {
|
||||||
|
rebuildIndex: mutation.mutateAsync,
|
||||||
|
isRebuilding: mutation.isPending,
|
||||||
|
error: mutation.error,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// ========================================
|
||||||
|
// Env Hooks
|
||||||
|
// ========================================
|
||||||
|
|
||||||
|
export function useCodexLensEnv() {
|
||||||
|
return useQuery({
|
||||||
|
queryKey: codexLensKeys.env(),
|
||||||
|
queryFn: () => fetchApi<EnvResponse>('/api/codexlens/env').then(r => r.env),
|
||||||
|
staleTime: 60_000,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export function useSaveCodexLensEnv() {
|
||||||
|
const queryClient = useQueryClient();
|
||||||
|
|
||||||
|
const mutation = useMutation({
|
||||||
|
mutationFn: (env: Record<string, string>) =>
|
||||||
|
fetchApi<{ success: boolean }>('/api/codexlens/env', {
|
||||||
|
method: 'POST',
|
||||||
|
body: JSON.stringify({ env }),
|
||||||
|
}),
|
||||||
|
onSuccess: () => {
|
||||||
|
queryClient.invalidateQueries({ queryKey: codexLensKeys.env() });
|
||||||
|
queryClient.invalidateQueries({ queryKey: codexLensKeys.mcpConfig() });
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
return {
|
||||||
|
saveEnv: mutation.mutateAsync,
|
||||||
|
isSaving: mutation.isPending,
|
||||||
|
error: mutation.error,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// ========================================
|
||||||
|
// MCP Config Hooks
|
||||||
|
// ========================================
|
||||||
|
|
||||||
|
export function useCodexLensMcpConfig() {
|
||||||
|
return useQuery({
|
||||||
|
queryKey: codexLensKeys.mcpConfig(),
|
||||||
|
queryFn: () => fetchApi<McpConfigResponse>('/api/codexlens/mcp-config').then(r => r.config),
|
||||||
|
staleTime: 30_000,
|
||||||
|
});
|
||||||
|
}
|
||||||
@@ -1,6 +1,70 @@
|
|||||||
{
|
{
|
||||||
"title": "Search Manager",
|
"title": "Search Manager",
|
||||||
"description": "V2 semantic search index management",
|
"description": "V2 semantic search index management",
|
||||||
|
"page": {
|
||||||
|
"title": "CodexLens v2",
|
||||||
|
"description": "Semantic code search engine — manage MCP config, embedding models, index state, and environment settings."
|
||||||
|
},
|
||||||
|
"tabs": {
|
||||||
|
"mcp": "MCP Config",
|
||||||
|
"models": "Model Manager",
|
||||||
|
"index": "Index Manager",
|
||||||
|
"env": "Env Settings"
|
||||||
|
},
|
||||||
|
"mcp": {
|
||||||
|
"embedMode": "Embed mode",
|
||||||
|
"configTitle": "MCP Config JSON",
|
||||||
|
"copy": "Copy",
|
||||||
|
"copied": "Copied",
|
||||||
|
"regenerate": "Regenerate Config",
|
||||||
|
"loading": "Loading config...",
|
||||||
|
"error": "Failed to load MCP config.",
|
||||||
|
"noConfig": "No config available.",
|
||||||
|
"installTitle": "Installation Instructions",
|
||||||
|
"installSteps": {
|
||||||
|
"step1": "Copy the MCP config JSON above using the Copy button.",
|
||||||
|
"step2": "Open your Claude Desktop or MCP-compatible client configuration file.",
|
||||||
|
"step3": "Add the copied JSON under the mcpServers key.",
|
||||||
|
"step4": "Save the configuration file and restart your client.",
|
||||||
|
"step5": "Verify the CodexLens server appears as an available MCP tool."
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"models": {
|
||||||
|
"embedMode": "Current embed mode",
|
||||||
|
"loading": "Loading models...",
|
||||||
|
"error": "Failed to load models.",
|
||||||
|
"noModels": "No models found.",
|
||||||
|
"installed": "Installed",
|
||||||
|
"notInstalled": "Not installed",
|
||||||
|
"download": "Download",
|
||||||
|
"delete": "Delete"
|
||||||
|
},
|
||||||
|
"index": {
|
||||||
|
"pathPlaceholder": "Enter project path (e.g. /home/user/myproject)",
|
||||||
|
"add": "Add",
|
||||||
|
"loading": "Loading status...",
|
||||||
|
"error": "Failed to load index status.",
|
||||||
|
"filesTracked": "Files tracked",
|
||||||
|
"totalChunks": "Total chunks",
|
||||||
|
"deletedChunks": "Deleted chunks",
|
||||||
|
"sync": "Sync",
|
||||||
|
"rebuild": "Rebuild",
|
||||||
|
"refresh": "Refresh Status",
|
||||||
|
"empty": "Enter a project path above to view index status and manage indexing.",
|
||||||
|
"removeProject": "Remove project"
|
||||||
|
},
|
||||||
|
"env": {
|
||||||
|
"loading": "Loading environment settings...",
|
||||||
|
"save": "Save",
|
||||||
|
"saving": "Saving...",
|
||||||
|
"clearForm": "Clear Form",
|
||||||
|
"sections": {
|
||||||
|
"embed": "Embed Config",
|
||||||
|
"reranker": "Reranker Config",
|
||||||
|
"performance": "Performance Tuning",
|
||||||
|
"index": "Index Params"
|
||||||
|
}
|
||||||
|
},
|
||||||
"reindex": "Reindex",
|
"reindex": "Reindex",
|
||||||
"reindexing": "Reindexing...",
|
"reindexing": "Reindexing...",
|
||||||
"statusError": "Failed to load search index status",
|
"statusError": "Failed to load search index status",
|
||||||
|
|||||||
@@ -426,10 +426,10 @@
|
|||||||
},
|
},
|
||||||
"exa": {
|
"exa": {
|
||||||
"name": "Exa Search",
|
"name": "Exa Search",
|
||||||
"desc": "AI-powered web search with real-time crawling capabilities",
|
"desc": "AI-powered web search via remote MCP server (mcp.exa.ai)",
|
||||||
"field": {
|
"field": {
|
||||||
"apiKey": "API Key",
|
"apiKey": "API Key",
|
||||||
"apiKey.desc": "Your Exa API key (optional, some features may require it)"
|
"apiKey.desc": "Your Exa API key (optional, adds to URL to overcome free plan rate limits)"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"enterprise": {
|
"enterprise": {
|
||||||
|
|||||||
@@ -4,6 +4,70 @@
|
|||||||
"reindex": "重建索引",
|
"reindex": "重建索引",
|
||||||
"reindexing": "重建中...",
|
"reindexing": "重建中...",
|
||||||
"statusError": "加载搜索索引状态失败",
|
"statusError": "加载搜索索引状态失败",
|
||||||
|
"page": {
|
||||||
|
"title": "CodexLens v2",
|
||||||
|
"description": "语义代码搜索引擎 — 管理 MCP 配置、嵌入模型、索引状态和环境变量"
|
||||||
|
},
|
||||||
|
"tabs": {
|
||||||
|
"mcp": "MCP 配置",
|
||||||
|
"models": "模型管理",
|
||||||
|
"index": "索引管理",
|
||||||
|
"env": "环境变量"
|
||||||
|
},
|
||||||
|
"mcp": {
|
||||||
|
"embedMode": "嵌入模式",
|
||||||
|
"configTitle": "MCP 配置 JSON",
|
||||||
|
"copy": "复制",
|
||||||
|
"copied": "已复制",
|
||||||
|
"regenerate": "重新生成配置",
|
||||||
|
"loading": "加载配置中...",
|
||||||
|
"error": "加载 MCP 配置失败",
|
||||||
|
"noConfig": "暂无配置",
|
||||||
|
"installTitle": "安装说明",
|
||||||
|
"installSteps": {
|
||||||
|
"step1": "点击复制按钮复制 MCP 配置 JSON。",
|
||||||
|
"step2": "打开 Claude Desktop 或兼容 MCP 的客户端配置文件。",
|
||||||
|
"step3": "将复制的 JSON 添加到 mcpServers 键下。",
|
||||||
|
"step4": "保存配置文件并重启客户端。",
|
||||||
|
"step5": "验证 CodexLens 服务器是否作为可用 MCP 工具出现。"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"models": {
|
||||||
|
"embedMode": "当前嵌入模式",
|
||||||
|
"loading": "加载模型中...",
|
||||||
|
"error": "加载模型失败",
|
||||||
|
"noModels": "未找到模型",
|
||||||
|
"installed": "已安装",
|
||||||
|
"notInstalled": "未安装",
|
||||||
|
"download": "下载",
|
||||||
|
"delete": "删除"
|
||||||
|
},
|
||||||
|
"index": {
|
||||||
|
"pathPlaceholder": "输入项目路径(例如 /home/user/myproject)",
|
||||||
|
"add": "添加",
|
||||||
|
"loading": "加载状态中...",
|
||||||
|
"error": "加载索引状态失败",
|
||||||
|
"filesTracked": "跟踪文件数",
|
||||||
|
"totalChunks": "总分块数",
|
||||||
|
"deletedChunks": "已删除分块",
|
||||||
|
"sync": "同步",
|
||||||
|
"rebuild": "重建",
|
||||||
|
"refresh": "刷新状态",
|
||||||
|
"empty": "在上方输入项目路径以查看索引状态和管理索引",
|
||||||
|
"removeProject": "移除项目"
|
||||||
|
},
|
||||||
|
"env": {
|
||||||
|
"loading": "加载环境设置中...",
|
||||||
|
"save": "保存",
|
||||||
|
"saving": "保存中...",
|
||||||
|
"clearForm": "清空表单",
|
||||||
|
"sections": {
|
||||||
|
"embed": "嵌入配置",
|
||||||
|
"reranker": "重排序配置",
|
||||||
|
"performance": "性能调优",
|
||||||
|
"index": "索引参数"
|
||||||
|
}
|
||||||
|
},
|
||||||
"indexStatus": {
|
"indexStatus": {
|
||||||
"title": "索引状态",
|
"title": "索引状态",
|
||||||
"status": "状态",
|
"status": "状态",
|
||||||
|
|||||||
@@ -415,10 +415,10 @@
|
|||||||
},
|
},
|
||||||
"exa": {
|
"exa": {
|
||||||
"name": "Exa 搜索",
|
"name": "Exa 搜索",
|
||||||
"desc": "AI 驱动的网络搜索,支持实时抓取",
|
"desc": "通过远程 MCP 服务器 (mcp.exa.ai) 实现 AI 驱动的网络搜索",
|
||||||
"field": {
|
"field": {
|
||||||
"apiKey": "API 密钥",
|
"apiKey": "API 密钥",
|
||||||
"apiKey.desc": "您的 Exa API 密钥(可选,部分功能可能需要)"
|
"apiKey.desc": "您的 Exa API 密钥(可选,填入后将作为 URL 参数传递以突破免费计划限速)"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"enterprise": {
|
"enterprise": {
|
||||||
|
|||||||
57
ccw/frontend/src/pages/CodexLensPage.tsx
Normal file
57
ccw/frontend/src/pages/CodexLensPage.tsx
Normal file
@@ -0,0 +1,57 @@
|
|||||||
|
// ========================================
|
||||||
|
// CodexLens Page
|
||||||
|
// ========================================
|
||||||
|
// Main page for CodexLens v2 management: MCP config, models, index, env
|
||||||
|
|
||||||
|
import { useState } from 'react';
|
||||||
|
import { useIntl } from 'react-intl';
|
||||||
|
import { Search } from 'lucide-react';
|
||||||
|
import { TabsNavigation } from '@/components/ui/TabsNavigation';
|
||||||
|
import { McpConfigTab } from '@/components/codexlens/McpConfigTab';
|
||||||
|
import { ModelManagerTab } from '@/components/codexlens/ModelManagerTab';
|
||||||
|
import { IndexManagerTab } from '@/components/codexlens/IndexManagerTab';
|
||||||
|
import { EnvSettingsTab } from '@/components/codexlens/EnvSettingsTab';
|
||||||
|
|
||||||
|
type TabType = 'mcp' | 'models' | 'index' | 'env';
|
||||||
|
|
||||||
|
export function CodexLensPage() {
|
||||||
|
const { formatMessage } = useIntl();
|
||||||
|
const [activeTab, setActiveTab] = useState<TabType>('mcp');
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="space-y-6">
|
||||||
|
{/* Page Header */}
|
||||||
|
<div>
|
||||||
|
<h1 className="text-2xl font-bold text-foreground flex items-center gap-2">
|
||||||
|
<Search className="w-6 h-6 text-primary" />
|
||||||
|
{formatMessage({ id: 'codexlens.page.title' })}
|
||||||
|
</h1>
|
||||||
|
<p className="text-muted-foreground mt-1">
|
||||||
|
{formatMessage({ id: 'codexlens.page.description' })}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Tab Navigation */}
|
||||||
|
<TabsNavigation
|
||||||
|
value={activeTab}
|
||||||
|
onValueChange={(v) => setActiveTab(v as TabType)}
|
||||||
|
tabs={[
|
||||||
|
{ value: 'mcp', label: formatMessage({ id: 'codexlens.tabs.mcp' }) },
|
||||||
|
{ value: 'models', label: formatMessage({ id: 'codexlens.tabs.models' }) },
|
||||||
|
{ value: 'index', label: formatMessage({ id: 'codexlens.tabs.index' }) },
|
||||||
|
{ value: 'env', label: formatMessage({ id: 'codexlens.tabs.env' }) },
|
||||||
|
]}
|
||||||
|
/>
|
||||||
|
|
||||||
|
{/* Tab Content */}
|
||||||
|
<div className="mt-4">
|
||||||
|
{activeTab === 'mcp' && <McpConfigTab />}
|
||||||
|
{activeTab === 'models' && <ModelManagerTab />}
|
||||||
|
{activeTab === 'index' && <IndexManagerTab />}
|
||||||
|
{activeTab === 'env' && <EnvSettingsTab />}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default CodexLensPage;
|
||||||
@@ -43,6 +43,7 @@ const TerminalDashboardPage = lazy(() => import('@/pages/TerminalDashboardPage')
|
|||||||
const AnalysisPage = lazy(() => import('@/pages/AnalysisPage').then(m => ({ default: m.AnalysisPage })));
|
const AnalysisPage = lazy(() => import('@/pages/AnalysisPage').then(m => ({ default: m.AnalysisPage })));
|
||||||
const SpecsSettingsPage = lazy(() => import('@/pages/SpecsSettingsPage').then(m => ({ default: m.SpecsSettingsPage })));
|
const SpecsSettingsPage = lazy(() => import('@/pages/SpecsSettingsPage').then(m => ({ default: m.SpecsSettingsPage })));
|
||||||
const DeepWikiPage = lazy(() => import('@/pages/DeepWikiPage').then(m => ({ default: m.DeepWikiPage })));
|
const DeepWikiPage = lazy(() => import('@/pages/DeepWikiPage').then(m => ({ default: m.DeepWikiPage })));
|
||||||
|
const CodexLensPage = lazy(() => import('@/pages/CodexLensPage').then(m => ({ default: m.CodexLensPage })));
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Helper to wrap lazy-loaded components with error boundary and suspense
|
* Helper to wrap lazy-loaded components with error boundary and suspense
|
||||||
@@ -197,6 +198,10 @@ const routes: RouteObject[] = [
|
|||||||
path: 'deepwiki',
|
path: 'deepwiki',
|
||||||
element: withErrorHandling(<DeepWikiPage />),
|
element: withErrorHandling(<DeepWikiPage />),
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
path: 'codexlens',
|
||||||
|
element: withErrorHandling(<CodexLensPage />),
|
||||||
|
},
|
||||||
{
|
{
|
||||||
path: 'terminal-dashboard',
|
path: 'terminal-dashboard',
|
||||||
element: withErrorHandling(<TerminalDashboardPage />),
|
element: withErrorHandling(<TerminalDashboardPage />),
|
||||||
@@ -263,6 +268,7 @@ export const ROUTES = {
|
|||||||
SKILL_HUB: '/skill-hub',
|
SKILL_HUB: '/skill-hub',
|
||||||
ANALYSIS: '/analysis',
|
ANALYSIS: '/analysis',
|
||||||
DEEPWIKI: '/deepwiki',
|
DEEPWIKI: '/deepwiki',
|
||||||
|
CODEXLENS: '/codexlens',
|
||||||
} as const;
|
} as const;
|
||||||
|
|
||||||
export type RoutePath = (typeof ROUTES)[keyof typeof ROUTES];
|
export type RoutePath = (typeof ROUTES)[keyof typeof ROUTES];
|
||||||
|
|||||||
294
ccw/src/core/routes/codexlens-routes.ts
Normal file
294
ccw/src/core/routes/codexlens-routes.ts
Normal file
@@ -0,0 +1,294 @@
|
|||||||
|
/**
|
||||||
|
* CodexLens v2 Routes Module
|
||||||
|
* Handles CodexLens model management, index operations, env config, and MCP config API endpoints
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { homedir } from 'os';
|
||||||
|
import { readFileSync, writeFileSync, existsSync, mkdirSync } from 'fs';
|
||||||
|
import { join } from 'path';
|
||||||
|
import { spawn } from 'child_process';
|
||||||
|
|
||||||
|
import type { RouteContext } from './types.js';
|
||||||
|
|
||||||
|
// ========== HELPERS ==========
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Spawn a CLI command and collect stdout/stderr
|
||||||
|
*/
|
||||||
|
function spawnCli(cmd: string, args: string[]): Promise<{ stdout: string; stderr: string; exitCode: number }> {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
let stdout = '';
|
||||||
|
let stderr = '';
|
||||||
|
|
||||||
|
const child = spawn(cmd, args);
|
||||||
|
|
||||||
|
child.stdout.on('data', (chunk: Buffer) => {
|
||||||
|
stdout += chunk.toString();
|
||||||
|
});
|
||||||
|
|
||||||
|
child.stderr.on('data', (chunk: Buffer) => {
|
||||||
|
stderr += chunk.toString();
|
||||||
|
});
|
||||||
|
|
||||||
|
child.on('error', (err: Error) => {
|
||||||
|
reject(err);
|
||||||
|
});
|
||||||
|
|
||||||
|
child.on('close', (code: number | null) => {
|
||||||
|
resolve({ stdout, stderr, exitCode: code ?? 1 });
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Return the path to the codexlens env config file
|
||||||
|
*/
|
||||||
|
function getEnvFilePath(): string {
|
||||||
|
return join(homedir(), '.ccw', 'codexlens-env.json');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Read the codexlens env config file; returns {} when file does not exist
|
||||||
|
*/
|
||||||
|
function readEnvFile(): Record<string, string> {
|
||||||
|
const filePath = getEnvFilePath();
|
||||||
|
if (!existsSync(filePath)) {
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
const content = readFileSync(filePath, 'utf-8');
|
||||||
|
return JSON.parse(content) as Record<string, string>;
|
||||||
|
} catch {
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Write the codexlens env config file
|
||||||
|
*/
|
||||||
|
function writeEnvFile(env: Record<string, string>): void {
|
||||||
|
const filePath = getEnvFilePath();
|
||||||
|
const dir = join(homedir(), '.ccw');
|
||||||
|
if (!existsSync(dir)) {
|
||||||
|
mkdirSync(dir, { recursive: true });
|
||||||
|
}
|
||||||
|
writeFileSync(filePath, JSON.stringify(env, null, 2), 'utf-8');
|
||||||
|
}
|
||||||
|
|
||||||
|
// ========== ROUTE HANDLER ==========
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Handle CodexLens routes
|
||||||
|
* @returns true if route was handled, false otherwise
|
||||||
|
*/
|
||||||
|
export async function handleCodexLensRoutes(ctx: RouteContext): Promise<boolean> {
|
||||||
|
const { pathname, req, res, handlePostRequest, url } = ctx;
|
||||||
|
|
||||||
|
// ========== LIST MODELS ==========
|
||||||
|
// GET /api/codexlens/models
|
||||||
|
if (pathname === '/api/codexlens/models' && req.method === 'GET') {
|
||||||
|
try {
|
||||||
|
const result = await spawnCli('codexlens-search', ['list-models']);
|
||||||
|
if (result.exitCode !== 0) {
|
||||||
|
res.writeHead(500, { 'Content-Type': 'application/json' });
|
||||||
|
res.end(JSON.stringify({ error: result.stderr || 'Failed to list models' }));
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
let models: unknown;
|
||||||
|
try {
|
||||||
|
models = JSON.parse(result.stdout);
|
||||||
|
} catch {
|
||||||
|
models = result.stdout;
|
||||||
|
}
|
||||||
|
res.writeHead(200, { 'Content-Type': 'application/json' });
|
||||||
|
res.end(JSON.stringify({ success: true, models }));
|
||||||
|
} catch (err) {
|
||||||
|
res.writeHead(500, { 'Content-Type': 'application/json' });
|
||||||
|
res.end(JSON.stringify({ error: (err as Error).message }));
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ========== DOWNLOAD MODEL ==========
|
||||||
|
// POST /api/codexlens/models/download
|
||||||
|
if (pathname === '/api/codexlens/models/download' && req.method === 'POST') {
|
||||||
|
handlePostRequest(req, res, async (body: unknown) => {
|
||||||
|
const { name } = body as { name?: string };
|
||||||
|
if (!name) {
|
||||||
|
return { error: 'name is required', status: 400 };
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
const result = await spawnCli('codexlens-search', ['download-model', name]);
|
||||||
|
if (result.exitCode !== 0) {
|
||||||
|
return { error: result.stderr || 'Failed to download model', status: 500 };
|
||||||
|
}
|
||||||
|
return { success: true, output: result.stdout };
|
||||||
|
} catch (err) {
|
||||||
|
return { error: (err as Error).message, status: 500 };
|
||||||
|
}
|
||||||
|
});
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ========== DELETE MODEL ==========
|
||||||
|
// POST /api/codexlens/models/delete
|
||||||
|
if (pathname === '/api/codexlens/models/delete' && req.method === 'POST') {
|
||||||
|
handlePostRequest(req, res, async (body: unknown) => {
|
||||||
|
const { name } = body as { name?: string };
|
||||||
|
if (!name) {
|
||||||
|
return { error: 'name is required', status: 400 };
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
const result = await spawnCli('codexlens-search', ['delete-model', name]);
|
||||||
|
if (result.exitCode !== 0) {
|
||||||
|
return { error: result.stderr || 'Failed to delete model', status: 500 };
|
||||||
|
}
|
||||||
|
return { success: true, output: result.stdout };
|
||||||
|
} catch (err) {
|
||||||
|
return { error: (err as Error).message, status: 500 };
|
||||||
|
}
|
||||||
|
});
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ========== INDEX STATUS ==========
|
||||||
|
// GET /api/codexlens/index/status?projectPath=
|
||||||
|
if (pathname === '/api/codexlens/index/status' && req.method === 'GET') {
|
||||||
|
const projectPath = url.searchParams.get('projectPath');
|
||||||
|
if (!projectPath) {
|
||||||
|
res.writeHead(400, { 'Content-Type': 'application/json' });
|
||||||
|
res.end(JSON.stringify({ error: 'projectPath query parameter is required' }));
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
const dbPath = join(projectPath, '.codexlens');
|
||||||
|
const result = await spawnCli('codexlens-search', ['status', '--db-path', dbPath]);
|
||||||
|
if (result.exitCode !== 0) {
|
||||||
|
res.writeHead(500, { 'Content-Type': 'application/json' });
|
||||||
|
res.end(JSON.stringify({ error: result.stderr || 'Failed to get index status' }));
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
let status: unknown;
|
||||||
|
try {
|
||||||
|
status = JSON.parse(result.stdout);
|
||||||
|
} catch {
|
||||||
|
status = { raw: result.stdout };
|
||||||
|
}
|
||||||
|
res.writeHead(200, { 'Content-Type': 'application/json' });
|
||||||
|
res.end(JSON.stringify({ success: true, status }));
|
||||||
|
} catch (err) {
|
||||||
|
res.writeHead(500, { 'Content-Type': 'application/json' });
|
||||||
|
res.end(JSON.stringify({ error: (err as Error).message }));
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ========== INDEX SYNC ==========
|
||||||
|
// POST /api/codexlens/index/sync
|
||||||
|
if (pathname === '/api/codexlens/index/sync' && req.method === 'POST') {
|
||||||
|
handlePostRequest(req, res, async (body: unknown) => {
|
||||||
|
const { projectPath } = body as { projectPath?: string };
|
||||||
|
if (!projectPath) {
|
||||||
|
return { error: 'projectPath is required', status: 400 };
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
const result = await spawnCli('codexlens-search', ['sync', '--root', projectPath]);
|
||||||
|
if (result.exitCode !== 0) {
|
||||||
|
return { error: result.stderr || 'Failed to sync index', status: 500 };
|
||||||
|
}
|
||||||
|
return { success: true, output: result.stdout };
|
||||||
|
} catch (err) {
|
||||||
|
return { error: (err as Error).message, status: 500 };
|
||||||
|
}
|
||||||
|
});
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ========== INDEX REBUILD ==========
|
||||||
|
// POST /api/codexlens/index/rebuild
|
||||||
|
if (pathname === '/api/codexlens/index/rebuild' && req.method === 'POST') {
|
||||||
|
handlePostRequest(req, res, async (body: unknown) => {
|
||||||
|
const { projectPath } = body as { projectPath?: string };
|
||||||
|
if (!projectPath) {
|
||||||
|
return { error: 'projectPath is required', status: 400 };
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
const initResult = await spawnCli('codexlens-search', ['init', '--root', projectPath]);
|
||||||
|
if (initResult.exitCode !== 0) {
|
||||||
|
return { error: initResult.stderr || 'Failed to init index', status: 500 };
|
||||||
|
}
|
||||||
|
const syncResult = await spawnCli('codexlens-search', ['sync', '--root', projectPath]);
|
||||||
|
if (syncResult.exitCode !== 0) {
|
||||||
|
return { error: syncResult.stderr || 'Failed to sync after init', status: 500 };
|
||||||
|
}
|
||||||
|
return { success: true, initOutput: initResult.stdout, syncOutput: syncResult.stdout };
|
||||||
|
} catch (err) {
|
||||||
|
return { error: (err as Error).message, status: 500 };
|
||||||
|
}
|
||||||
|
});
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ========== GET ENV ==========
|
||||||
|
// GET /api/codexlens/env
|
||||||
|
if (pathname === '/api/codexlens/env' && req.method === 'GET') {
|
||||||
|
try {
|
||||||
|
const env = readEnvFile();
|
||||||
|
res.writeHead(200, { 'Content-Type': 'application/json' });
|
||||||
|
res.end(JSON.stringify({ success: true, env }));
|
||||||
|
} catch (err) {
|
||||||
|
res.writeHead(500, { 'Content-Type': 'application/json' });
|
||||||
|
res.end(JSON.stringify({ error: (err as Error).message }));
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ========== SAVE ENV ==========
|
||||||
|
// POST /api/codexlens/env
|
||||||
|
if (pathname === '/api/codexlens/env' && req.method === 'POST') {
|
||||||
|
handlePostRequest(req, res, async (body: unknown) => {
|
||||||
|
const { env } = body as { env?: Record<string, string> };
|
||||||
|
if (!env || typeof env !== 'object') {
|
||||||
|
return { error: 'env object is required', status: 400 };
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
writeEnvFile(env);
|
||||||
|
return { success: true };
|
||||||
|
} catch (err) {
|
||||||
|
return { error: (err as Error).message, status: 500 };
|
||||||
|
}
|
||||||
|
});
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ========== MCP CONFIG ==========
|
||||||
|
// GET /api/codexlens/mcp-config
|
||||||
|
if (pathname === '/api/codexlens/mcp-config' && req.method === 'GET') {
|
||||||
|
try {
|
||||||
|
const env = readEnvFile();
|
||||||
|
// Filter to non-empty string values only
|
||||||
|
const filteredEnv: Record<string, string> = {};
|
||||||
|
for (const [key, value] of Object.entries(env)) {
|
||||||
|
if (typeof value === 'string' && value.trim() !== '') {
|
||||||
|
filteredEnv[key] = value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const mcpConfig = {
|
||||||
|
mcpServers: {
|
||||||
|
codexlens: {
|
||||||
|
command: 'codexlens-mcp',
|
||||||
|
env: filteredEnv
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
res.writeHead(200, { 'Content-Type': 'application/json' });
|
||||||
|
res.end(JSON.stringify({ success: true, config: mcpConfig }));
|
||||||
|
} catch (err) {
|
||||||
|
res.writeHead(500, { 'Content-Type': 'application/json' });
|
||||||
|
res.end(JSON.stringify({ error: (err as Error).message }));
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
@@ -46,6 +46,7 @@ import { handleNotificationRoutes } from './routes/notification-routes.js';
|
|||||||
import { handleAnalysisRoutes } from './routes/analysis-routes.js';
|
import { handleAnalysisRoutes } from './routes/analysis-routes.js';
|
||||||
import { handleSpecRoutes } from './routes/spec-routes.js';
|
import { handleSpecRoutes } from './routes/spec-routes.js';
|
||||||
import { handleDeepWikiRoutes } from './routes/deepwiki-routes.js';
|
import { handleDeepWikiRoutes } from './routes/deepwiki-routes.js';
|
||||||
|
import { handleCodexLensRoutes } from './routes/codexlens-routes.js';
|
||||||
|
|
||||||
// Import WebSocket handling
|
// Import WebSocket handling
|
||||||
import { handleWebSocketUpgrade, broadcastToClients, extractSessionIdFromPath } from './websocket.js';
|
import { handleWebSocketUpgrade, broadcastToClients, extractSessionIdFromPath } from './websocket.js';
|
||||||
@@ -559,6 +560,11 @@ export async function startServer(options: ServerOptions = {}): Promise<http.Ser
|
|||||||
if (await handleDeepWikiRoutes(routeContext)) return;
|
if (await handleDeepWikiRoutes(routeContext)) return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// CodexLens routes (/api/codexlens/*)
|
||||||
|
if (pathname.startsWith('/api/codexlens')) {
|
||||||
|
if (await handleCodexLensRoutes(routeContext)) return;
|
||||||
|
}
|
||||||
|
|
||||||
// Hooks routes (/api/hooks, /api/hook)
|
// Hooks routes (/api/hooks, /api/hook)
|
||||||
if (pathname.startsWith('/api/hook')) {
|
if (pathname.startsWith('/api/hook')) {
|
||||||
if (await handleHooksRoutes(routeContext)) return;
|
if (await handleHooksRoutes(routeContext)) return;
|
||||||
|
|||||||
Reference in New Issue
Block a user