mirror of
https://github.com/catlog22/Claude-Code-Workflow.git
synced 2026-02-15 02:42:45 +08:00
feat: enhance codexlens frontend integration with reranker config, MCP tools card, and LSP management
Add three integration improvements to the CodexLens management panel: - Enhance SettingsTab with RerankerConfigCard using /reranker/config endpoint for dynamic backend/model/provider dropdowns - Add CcwToolsCard to AdvancedTab showing CCW registered tools with codex-lens tools highlighted - Add LspServerCard to OverviewTab with start/stop/restart controls mirroring the FileWatcherCard pattern - Create LSP lifecycle backend endpoints (start/stop/restart) bridging to Python StandaloneLspManager - Add corresponding TanStack Query hooks, API functions, and i18n keys
This commit is contained in:
@@ -14,6 +14,7 @@ import { Badge } from '@/components/ui/Badge';
|
|||||||
import { useCodexLensEnv, useUpdateCodexLensEnv } from '@/hooks';
|
import { useCodexLensEnv, useUpdateCodexLensEnv } from '@/hooks';
|
||||||
import { useNotifications } from '@/hooks';
|
import { useNotifications } from '@/hooks';
|
||||||
import { cn } from '@/lib/utils';
|
import { cn } from '@/lib/utils';
|
||||||
|
import { CcwToolsCard } from './CcwToolsCard';
|
||||||
|
|
||||||
interface AdvancedTabProps {
|
interface AdvancedTabProps {
|
||||||
enabled?: boolean;
|
enabled?: boolean;
|
||||||
@@ -238,6 +239,9 @@ export function AdvancedTab({ enabled = true }: AdvancedTabProps) {
|
|||||||
</Card>
|
</Card>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
{/* CCW Tools Card */}
|
||||||
|
<CcwToolsCard />
|
||||||
|
|
||||||
{/* Environment Variables Editor */}
|
{/* Environment Variables Editor */}
|
||||||
<Card className="p-6">
|
<Card className="p-6">
|
||||||
<div className="flex items-center justify-between mb-4">
|
<div className="flex items-center justify-between mb-4">
|
||||||
|
|||||||
153
ccw/frontend/src/components/codexlens/CcwToolsCard.tsx
Normal file
153
ccw/frontend/src/components/codexlens/CcwToolsCard.tsx
Normal file
@@ -0,0 +1,153 @@
|
|||||||
|
// ========================================
|
||||||
|
// CCW Tools Card Component
|
||||||
|
// ========================================
|
||||||
|
// Displays all registered CCW tools, highlighting codex-lens related tools
|
||||||
|
|
||||||
|
import { useIntl } from 'react-intl';
|
||||||
|
import { Wrench, AlertCircle, Loader2 } from 'lucide-react';
|
||||||
|
import { Card, CardHeader, CardTitle, CardContent } from '@/components/ui/Card';
|
||||||
|
import { Badge } from '@/components/ui/Badge';
|
||||||
|
import { useCcwToolsList } from '@/hooks';
|
||||||
|
import type { CcwToolInfo } from '@/lib/api';
|
||||||
|
|
||||||
|
const CODEX_LENS_PREFIX = 'codex_lens';
|
||||||
|
|
||||||
|
function isCodexLensTool(tool: CcwToolInfo): boolean {
|
||||||
|
return tool.name.startsWith(CODEX_LENS_PREFIX);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function CcwToolsCard() {
|
||||||
|
const { formatMessage } = useIntl();
|
||||||
|
const { tools, isLoading, error } = useCcwToolsList();
|
||||||
|
|
||||||
|
const codexLensTools = tools.filter(isCodexLensTool);
|
||||||
|
const otherTools = tools.filter((t) => !isCodexLensTool(t));
|
||||||
|
|
||||||
|
if (isLoading) {
|
||||||
|
return (
|
||||||
|
<Card>
|
||||||
|
<CardHeader>
|
||||||
|
<CardTitle className="text-base flex items-center gap-2">
|
||||||
|
<Wrench className="w-4 h-4" />
|
||||||
|
<span>{formatMessage({ id: 'codexlens.mcp.title' })}</span>
|
||||||
|
</CardTitle>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent>
|
||||||
|
<div className="flex items-center gap-2 text-sm text-muted-foreground">
|
||||||
|
<Loader2 className="w-4 h-4 animate-spin" />
|
||||||
|
<span>{formatMessage({ id: 'codexlens.mcp.loading' })}</span>
|
||||||
|
</div>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (error) {
|
||||||
|
return (
|
||||||
|
<Card className="border-destructive/20">
|
||||||
|
<CardHeader>
|
||||||
|
<CardTitle className="text-base flex items-center gap-2">
|
||||||
|
<Wrench className="w-4 h-4" />
|
||||||
|
<span>{formatMessage({ id: 'codexlens.mcp.title' })}</span>
|
||||||
|
</CardTitle>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent>
|
||||||
|
<div className="flex items-start gap-2 text-sm">
|
||||||
|
<AlertCircle className="w-4 h-4 text-destructive flex-shrink-0 mt-0.5" />
|
||||||
|
<div>
|
||||||
|
<p className="font-medium text-destructive">
|
||||||
|
{formatMessage({ id: 'codexlens.mcp.error' })}
|
||||||
|
</p>
|
||||||
|
<p className="text-xs text-muted-foreground mt-1">
|
||||||
|
{formatMessage({ id: 'codexlens.mcp.errorDesc' })}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (tools.length === 0) {
|
||||||
|
return (
|
||||||
|
<Card>
|
||||||
|
<CardHeader>
|
||||||
|
<CardTitle className="text-base flex items-center gap-2">
|
||||||
|
<Wrench className="w-4 h-4" />
|
||||||
|
<span>{formatMessage({ id: 'codexlens.mcp.title' })}</span>
|
||||||
|
</CardTitle>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent>
|
||||||
|
<p className="text-sm text-muted-foreground">
|
||||||
|
{formatMessage({ id: 'codexlens.mcp.emptyDesc' })}
|
||||||
|
</p>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Card>
|
||||||
|
<CardHeader>
|
||||||
|
<CardTitle className="text-base flex items-center justify-between">
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<Wrench className="w-4 h-4" />
|
||||||
|
<span>{formatMessage({ id: 'codexlens.mcp.title' })}</span>
|
||||||
|
</div>
|
||||||
|
<Badge variant="outline" className="text-xs">
|
||||||
|
{formatMessage({ id: 'codexlens.mcp.totalCount' }, { count: tools.length })}
|
||||||
|
</Badge>
|
||||||
|
</CardTitle>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent className="space-y-4">
|
||||||
|
{/* CodexLens Tools Section */}
|
||||||
|
{codexLensTools.length > 0 && (
|
||||||
|
<div>
|
||||||
|
<p className="text-xs font-medium text-muted-foreground uppercase mb-2">
|
||||||
|
{formatMessage({ id: 'codexlens.mcp.codexLensSection' })}
|
||||||
|
</p>
|
||||||
|
<div className="space-y-1.5">
|
||||||
|
{codexLensTools.map((tool) => (
|
||||||
|
<ToolRow key={tool.name} tool={tool} variant="default" />
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Other Tools Section */}
|
||||||
|
{otherTools.length > 0 && (
|
||||||
|
<div>
|
||||||
|
<p className="text-xs font-medium text-muted-foreground uppercase mb-2">
|
||||||
|
{formatMessage({ id: 'codexlens.mcp.otherSection' })}
|
||||||
|
</p>
|
||||||
|
<div className="space-y-1.5">
|
||||||
|
{otherTools.map((tool) => (
|
||||||
|
<ToolRow key={tool.name} tool={tool} variant="secondary" />
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
interface ToolRowProps {
|
||||||
|
tool: CcwToolInfo;
|
||||||
|
variant: 'default' | 'secondary';
|
||||||
|
}
|
||||||
|
|
||||||
|
function ToolRow({ tool, variant }: ToolRowProps) {
|
||||||
|
return (
|
||||||
|
<div className="flex items-center gap-2 py-1 px-2 rounded-md hover:bg-muted/50 transition-colors">
|
||||||
|
<Badge variant={variant} className="text-xs font-mono shrink-0">
|
||||||
|
{tool.name}
|
||||||
|
</Badge>
|
||||||
|
<span className="text-xs text-muted-foreground truncate" title={tool.description}>
|
||||||
|
{tool.description}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default CcwToolsCard;
|
||||||
157
ccw/frontend/src/components/codexlens/LspServerCard.tsx
Normal file
157
ccw/frontend/src/components/codexlens/LspServerCard.tsx
Normal file
@@ -0,0 +1,157 @@
|
|||||||
|
// ========================================
|
||||||
|
// CodexLens LSP Server Card
|
||||||
|
// ========================================
|
||||||
|
// Displays LSP server status, stats, and start/stop/restart controls
|
||||||
|
|
||||||
|
import { useIntl } from 'react-intl';
|
||||||
|
import {
|
||||||
|
Server,
|
||||||
|
Power,
|
||||||
|
PowerOff,
|
||||||
|
RotateCw,
|
||||||
|
FolderOpen,
|
||||||
|
Layers,
|
||||||
|
Cpu,
|
||||||
|
} from 'lucide-react';
|
||||||
|
import { Card, CardHeader, CardTitle, CardContent } from '@/components/ui/Card';
|
||||||
|
import { Badge } from '@/components/ui/Badge';
|
||||||
|
import { Button } from '@/components/ui/Button';
|
||||||
|
import { cn } from '@/lib/utils';
|
||||||
|
import { useCodexLensLspStatus, useCodexLensLspMutations } from '@/hooks';
|
||||||
|
import { useWorkflowStore, selectProjectPath } from '@/stores/workflowStore';
|
||||||
|
|
||||||
|
interface LspServerCardProps {
|
||||||
|
disabled?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function LspServerCard({ disabled = false }: LspServerCardProps) {
|
||||||
|
const { formatMessage } = useIntl();
|
||||||
|
const projectPath = useWorkflowStore(selectProjectPath);
|
||||||
|
const {
|
||||||
|
available,
|
||||||
|
semanticAvailable,
|
||||||
|
projectCount,
|
||||||
|
modes,
|
||||||
|
isLoading,
|
||||||
|
} = useCodexLensLspStatus();
|
||||||
|
const { startLsp, stopLsp, restartLsp, isStarting, isStopping, isRestarting } = useCodexLensLspMutations();
|
||||||
|
|
||||||
|
const isMutating = isStarting || isStopping || isRestarting;
|
||||||
|
|
||||||
|
const handleToggle = async () => {
|
||||||
|
if (available) {
|
||||||
|
await stopLsp(projectPath);
|
||||||
|
} else {
|
||||||
|
await startLsp(projectPath);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleRestart = async () => {
|
||||||
|
await restartLsp(projectPath);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Card>
|
||||||
|
<CardHeader>
|
||||||
|
<CardTitle className="text-base flex items-center justify-between">
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<Server className="w-4 h-4" />
|
||||||
|
<span>{formatMessage({ id: 'codexlens.lsp.title' })}</span>
|
||||||
|
</div>
|
||||||
|
<Badge variant={available ? 'success' : 'secondary'}>
|
||||||
|
{available
|
||||||
|
? formatMessage({ id: 'codexlens.lsp.status.running' })
|
||||||
|
: formatMessage({ id: 'codexlens.lsp.status.stopped' })
|
||||||
|
}
|
||||||
|
</Badge>
|
||||||
|
</CardTitle>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent className="space-y-4">
|
||||||
|
{/* Stats Grid */}
|
||||||
|
<div className="grid grid-cols-3 gap-3">
|
||||||
|
<div className="flex items-center gap-2 text-sm">
|
||||||
|
<FolderOpen className={cn('w-4 h-4', available ? 'text-success' : 'text-muted-foreground')} />
|
||||||
|
<div>
|
||||||
|
<p className="text-muted-foreground text-xs">
|
||||||
|
{formatMessage({ id: 'codexlens.lsp.projects' })}
|
||||||
|
</p>
|
||||||
|
<p className="font-semibold text-foreground">
|
||||||
|
{available ? projectCount : '--'}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center gap-2 text-sm">
|
||||||
|
<Cpu className={cn('w-4 h-4', semanticAvailable ? 'text-info' : 'text-muted-foreground')} />
|
||||||
|
<div>
|
||||||
|
<p className="text-muted-foreground text-xs">
|
||||||
|
{formatMessage({ id: 'codexlens.lsp.semanticAvailable' })}
|
||||||
|
</p>
|
||||||
|
<p className="font-semibold text-foreground">
|
||||||
|
{semanticAvailable
|
||||||
|
? formatMessage({ id: 'codexlens.lsp.available' })
|
||||||
|
: formatMessage({ id: 'codexlens.lsp.unavailable' })
|
||||||
|
}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center gap-2 text-sm">
|
||||||
|
<Layers className={cn('w-4 h-4', available && modes.length > 0 ? 'text-accent' : 'text-muted-foreground')} />
|
||||||
|
<div>
|
||||||
|
<p className="text-muted-foreground text-xs">
|
||||||
|
{formatMessage({ id: 'codexlens.lsp.modes' })}
|
||||||
|
</p>
|
||||||
|
<p className="font-semibold text-foreground">
|
||||||
|
{available ? modes.length : '--'}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Action Buttons */}
|
||||||
|
<div className="flex gap-2">
|
||||||
|
<Button
|
||||||
|
variant={available ? 'outline' : 'default'}
|
||||||
|
size="sm"
|
||||||
|
className="flex-1"
|
||||||
|
onClick={handleToggle}
|
||||||
|
disabled={disabled || isMutating || isLoading}
|
||||||
|
>
|
||||||
|
{available ? (
|
||||||
|
<>
|
||||||
|
<PowerOff className="w-4 h-4 mr-2" />
|
||||||
|
{isStopping
|
||||||
|
? formatMessage({ id: 'codexlens.lsp.stopping' })
|
||||||
|
: formatMessage({ id: 'codexlens.lsp.stop' })
|
||||||
|
}
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
|
<Power className="w-4 h-4 mr-2" />
|
||||||
|
{isStarting
|
||||||
|
? formatMessage({ id: 'codexlens.lsp.starting' })
|
||||||
|
: formatMessage({ id: 'codexlens.lsp.start' })
|
||||||
|
}
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</Button>
|
||||||
|
{available && (
|
||||||
|
<Button
|
||||||
|
variant="outline"
|
||||||
|
size="sm"
|
||||||
|
onClick={handleRestart}
|
||||||
|
disabled={disabled || isMutating || isLoading}
|
||||||
|
>
|
||||||
|
<RotateCw className={cn('w-4 h-4 mr-2', isRestarting && 'animate-spin')} />
|
||||||
|
{isRestarting
|
||||||
|
? formatMessage({ id: 'codexlens.lsp.restarting' })
|
||||||
|
: formatMessage({ id: 'codexlens.lsp.restart' })
|
||||||
|
}
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default LspServerCard;
|
||||||
@@ -16,6 +16,7 @@ import { cn } from '@/lib/utils';
|
|||||||
import type { CodexLensVenvStatus, CodexLensConfig } from '@/lib/api';
|
import type { CodexLensVenvStatus, CodexLensConfig } from '@/lib/api';
|
||||||
import { IndexOperations } from './IndexOperations';
|
import { IndexOperations } from './IndexOperations';
|
||||||
import { FileWatcherCard } from './FileWatcherCard';
|
import { FileWatcherCard } from './FileWatcherCard';
|
||||||
|
import { LspServerCard } from './LspServerCard';
|
||||||
|
|
||||||
interface OverviewTabProps {
|
interface OverviewTabProps {
|
||||||
installed: boolean;
|
installed: boolean;
|
||||||
@@ -143,8 +144,11 @@ export function OverviewTab({ installed, status, config, isLoading, onRefresh }:
|
|||||||
</Card>
|
</Card>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* File Watcher */}
|
{/* Service Management */}
|
||||||
<FileWatcherCard disabled={!isReady} />
|
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||||
|
<FileWatcherCard disabled={!isReady} />
|
||||||
|
<LspServerCard disabled={!isReady} />
|
||||||
|
</div>
|
||||||
|
|
||||||
{/* Index Operations */}
|
{/* Index Operations */}
|
||||||
<IndexOperations disabled={!isReady} onRefresh={onRefresh} />
|
<IndexOperations disabled={!isReady} onRefresh={onRefresh} />
|
||||||
|
|||||||
@@ -18,6 +18,8 @@ vi.mock('@/hooks', async (importOriginal) => {
|
|||||||
useCodexLensEnv: vi.fn(),
|
useCodexLensEnv: vi.fn(),
|
||||||
useUpdateCodexLensEnv: vi.fn(),
|
useUpdateCodexLensEnv: vi.fn(),
|
||||||
useCodexLensModels: vi.fn(),
|
useCodexLensModels: vi.fn(),
|
||||||
|
useCodexLensRerankerConfig: vi.fn(),
|
||||||
|
useUpdateRerankerConfig: vi.fn(),
|
||||||
useNotifications: vi.fn(() => ({
|
useNotifications: vi.fn(() => ({
|
||||||
toasts: [],
|
toasts: [],
|
||||||
wsStatus: 'disconnected' as const,
|
wsStatus: 'disconnected' as const,
|
||||||
@@ -48,6 +50,8 @@ import {
|
|||||||
useCodexLensEnv,
|
useCodexLensEnv,
|
||||||
useUpdateCodexLensEnv,
|
useUpdateCodexLensEnv,
|
||||||
useCodexLensModels,
|
useCodexLensModels,
|
||||||
|
useCodexLensRerankerConfig,
|
||||||
|
useUpdateRerankerConfig,
|
||||||
useNotifications,
|
useNotifications,
|
||||||
} from '@/hooks';
|
} from '@/hooks';
|
||||||
|
|
||||||
@@ -102,6 +106,25 @@ function setupDefaultMocks() {
|
|||||||
error: null,
|
error: null,
|
||||||
refetch: vi.fn(),
|
refetch: vi.fn(),
|
||||||
});
|
});
|
||||||
|
vi.mocked(useCodexLensRerankerConfig).mockReturnValue({
|
||||||
|
data: undefined,
|
||||||
|
backend: 'fastembed',
|
||||||
|
modelName: '',
|
||||||
|
apiProvider: '',
|
||||||
|
apiKeySet: false,
|
||||||
|
availableBackends: ['onnx', 'api', 'litellm', 'legacy'],
|
||||||
|
apiProviders: ['siliconflow', 'cohere', 'jina'],
|
||||||
|
litellmModels: undefined,
|
||||||
|
configSource: 'default',
|
||||||
|
isLoading: false,
|
||||||
|
error: null,
|
||||||
|
refetch: vi.fn(),
|
||||||
|
});
|
||||||
|
vi.mocked(useUpdateRerankerConfig).mockReturnValue({
|
||||||
|
updateConfig: vi.fn().mockResolvedValue({ success: true, message: 'Saved' }),
|
||||||
|
isUpdating: false,
|
||||||
|
error: null,
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
describe('SettingsTab', () => {
|
describe('SettingsTab', () => {
|
||||||
@@ -324,6 +347,25 @@ describe('SettingsTab', () => {
|
|||||||
error: null,
|
error: null,
|
||||||
refetch: vi.fn(),
|
refetch: vi.fn(),
|
||||||
});
|
});
|
||||||
|
vi.mocked(useCodexLensRerankerConfig).mockReturnValue({
|
||||||
|
data: undefined,
|
||||||
|
backend: 'fastembed',
|
||||||
|
modelName: '',
|
||||||
|
apiProvider: '',
|
||||||
|
apiKeySet: false,
|
||||||
|
availableBackends: [],
|
||||||
|
apiProviders: [],
|
||||||
|
litellmModels: undefined,
|
||||||
|
configSource: 'default',
|
||||||
|
isLoading: false,
|
||||||
|
error: null,
|
||||||
|
refetch: vi.fn(),
|
||||||
|
});
|
||||||
|
vi.mocked(useUpdateRerankerConfig).mockReturnValue({
|
||||||
|
updateConfig: vi.fn().mockResolvedValue({ success: true }),
|
||||||
|
isUpdating: false,
|
||||||
|
error: null,
|
||||||
|
});
|
||||||
|
|
||||||
render(<SettingsTab enabled={true} />);
|
render(<SettingsTab enabled={true} />);
|
||||||
|
|
||||||
|
|||||||
@@ -7,22 +7,265 @@
|
|||||||
|
|
||||||
import { useState, useEffect, useCallback, useMemo } from 'react';
|
import { useState, useEffect, useCallback, useMemo } from 'react';
|
||||||
import { useIntl } from 'react-intl';
|
import { useIntl } from 'react-intl';
|
||||||
import { Save, RefreshCw } from 'lucide-react';
|
import { Save, RefreshCw, Loader2 } from 'lucide-react';
|
||||||
import { Card } from '@/components/ui/Card';
|
import { Card } from '@/components/ui/Card';
|
||||||
import { Input } from '@/components/ui/Input';
|
import { Input } from '@/components/ui/Input';
|
||||||
import { Button } from '@/components/ui/Button';
|
import { Button } from '@/components/ui/Button';
|
||||||
import { Label } from '@/components/ui/Label';
|
import { Label } from '@/components/ui/Label';
|
||||||
|
import {
|
||||||
|
Select,
|
||||||
|
SelectContent,
|
||||||
|
SelectItem,
|
||||||
|
SelectTrigger,
|
||||||
|
SelectValue,
|
||||||
|
SelectGroup,
|
||||||
|
SelectLabel,
|
||||||
|
} from '@/components/ui/Select';
|
||||||
import {
|
import {
|
||||||
useCodexLensConfig,
|
useCodexLensConfig,
|
||||||
useCodexLensEnv,
|
useCodexLensEnv,
|
||||||
useUpdateCodexLensEnv,
|
useUpdateCodexLensEnv,
|
||||||
useCodexLensModels,
|
useCodexLensModels,
|
||||||
|
useCodexLensRerankerConfig,
|
||||||
|
useUpdateRerankerConfig,
|
||||||
} from '@/hooks';
|
} from '@/hooks';
|
||||||
import { useNotifications } from '@/hooks';
|
import { useNotifications } from '@/hooks';
|
||||||
import { cn } from '@/lib/utils';
|
import { cn } from '@/lib/utils';
|
||||||
import { SchemaFormRenderer } from './SchemaFormRenderer';
|
import { SchemaFormRenderer } from './SchemaFormRenderer';
|
||||||
import { envVarGroupsSchema, getSchemaDefaults } from './envVarSchema';
|
import { envVarGroupsSchema, getSchemaDefaults } from './envVarSchema';
|
||||||
|
|
||||||
|
// ========== Reranker Configuration Card ==========
|
||||||
|
|
||||||
|
interface RerankerConfigCardProps {
|
||||||
|
enabled?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
function RerankerConfigCard({ enabled = true }: RerankerConfigCardProps) {
|
||||||
|
const { formatMessage } = useIntl();
|
||||||
|
const { success: showSuccess, error: showError } = useNotifications();
|
||||||
|
|
||||||
|
const {
|
||||||
|
backend: serverBackend,
|
||||||
|
modelName: serverModelName,
|
||||||
|
apiProvider: serverApiProvider,
|
||||||
|
apiKeySet,
|
||||||
|
availableBackends,
|
||||||
|
apiProviders,
|
||||||
|
litellmModels,
|
||||||
|
configSource,
|
||||||
|
isLoading,
|
||||||
|
} = useCodexLensRerankerConfig({ enabled });
|
||||||
|
|
||||||
|
const { updateConfig, isUpdating } = useUpdateRerankerConfig();
|
||||||
|
|
||||||
|
const [backend, setBackend] = useState('');
|
||||||
|
const [modelName, setModelName] = useState('');
|
||||||
|
const [apiProvider, setApiProvider] = useState('');
|
||||||
|
const [hasChanges, setHasChanges] = useState(false);
|
||||||
|
|
||||||
|
// Initialize form from server data
|
||||||
|
useEffect(() => {
|
||||||
|
setBackend(serverBackend);
|
||||||
|
setModelName(serverModelName);
|
||||||
|
setApiProvider(serverApiProvider);
|
||||||
|
setHasChanges(false);
|
||||||
|
}, [serverBackend, serverModelName, serverApiProvider]);
|
||||||
|
|
||||||
|
// Detect changes
|
||||||
|
useEffect(() => {
|
||||||
|
const changed =
|
||||||
|
backend !== serverBackend ||
|
||||||
|
modelName !== serverModelName ||
|
||||||
|
apiProvider !== serverApiProvider;
|
||||||
|
setHasChanges(changed);
|
||||||
|
}, [backend, modelName, apiProvider, serverBackend, serverModelName, serverApiProvider]);
|
||||||
|
|
||||||
|
const handleSave = async () => {
|
||||||
|
try {
|
||||||
|
const request: Record<string, string> = {};
|
||||||
|
if (backend !== serverBackend) request.backend = backend;
|
||||||
|
if (modelName !== serverModelName) {
|
||||||
|
// When backend is litellm, model_name is sent as litellm_endpoint
|
||||||
|
if (backend === 'litellm') {
|
||||||
|
request.litellm_endpoint = modelName;
|
||||||
|
} else {
|
||||||
|
request.model_name = modelName;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (apiProvider !== serverApiProvider) request.api_provider = apiProvider;
|
||||||
|
|
||||||
|
const result = await updateConfig(request);
|
||||||
|
if (result.success) {
|
||||||
|
showSuccess(
|
||||||
|
formatMessage({ id: 'codexlens.reranker.saveSuccess' }),
|
||||||
|
result.message || ''
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
showError(
|
||||||
|
formatMessage({ id: 'codexlens.reranker.saveFailed' }),
|
||||||
|
result.error || ''
|
||||||
|
);
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
showError(
|
||||||
|
formatMessage({ id: 'codexlens.reranker.saveFailed' }),
|
||||||
|
err instanceof Error ? err.message : ''
|
||||||
|
);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Determine whether to show litellm model dropdown or text input
|
||||||
|
const showLitellmModelSelect = backend === 'litellm' && litellmModels && litellmModels.length > 0;
|
||||||
|
// Show provider dropdown only for api backend
|
||||||
|
const showProviderSelect = backend === 'api';
|
||||||
|
|
||||||
|
if (isLoading) {
|
||||||
|
return (
|
||||||
|
<Card className="p-6">
|
||||||
|
<div className="flex items-center gap-2 text-muted-foreground">
|
||||||
|
<Loader2 className="w-4 h-4 animate-spin" />
|
||||||
|
<span>{formatMessage({ id: 'common.loading' })}</span>
|
||||||
|
</div>
|
||||||
|
</Card>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Card className="p-6">
|
||||||
|
<h3 className="text-lg font-semibold text-foreground mb-1">
|
||||||
|
{formatMessage({ id: 'codexlens.reranker.title' })}
|
||||||
|
</h3>
|
||||||
|
<p className="text-sm text-muted-foreground mb-4">
|
||||||
|
{formatMessage({ id: 'codexlens.reranker.description' })}
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<div className="space-y-4">
|
||||||
|
{/* Backend Select */}
|
||||||
|
<div className="space-y-2">
|
||||||
|
<Label>{formatMessage({ id: 'codexlens.reranker.backend' })}</Label>
|
||||||
|
<Select value={backend} onValueChange={setBackend}>
|
||||||
|
<SelectTrigger>
|
||||||
|
<SelectValue placeholder={formatMessage({ id: 'codexlens.reranker.selectBackend' })} />
|
||||||
|
</SelectTrigger>
|
||||||
|
<SelectContent>
|
||||||
|
{availableBackends.length > 0 ? (
|
||||||
|
availableBackends.map((b) => (
|
||||||
|
<SelectItem key={b} value={b}>
|
||||||
|
{b}
|
||||||
|
</SelectItem>
|
||||||
|
))
|
||||||
|
) : (
|
||||||
|
<SelectItem value="_none" disabled>
|
||||||
|
{formatMessage({ id: 'codexlens.reranker.noBackends' })}
|
||||||
|
</SelectItem>
|
||||||
|
)}
|
||||||
|
</SelectContent>
|
||||||
|
</Select>
|
||||||
|
<p className="text-xs text-muted-foreground">
|
||||||
|
{formatMessage({ id: 'codexlens.reranker.backendHint' })}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Model - Select for litellm, Input for others */}
|
||||||
|
<div className="space-y-2">
|
||||||
|
<Label>{formatMessage({ id: 'codexlens.reranker.model' })}</Label>
|
||||||
|
{showLitellmModelSelect ? (
|
||||||
|
<Select value={modelName} onValueChange={setModelName}>
|
||||||
|
<SelectTrigger>
|
||||||
|
<SelectValue placeholder={formatMessage({ id: 'codexlens.reranker.selectModel' })} />
|
||||||
|
</SelectTrigger>
|
||||||
|
<SelectContent>
|
||||||
|
<SelectGroup>
|
||||||
|
<SelectLabel>
|
||||||
|
{formatMessage({ id: 'codexlens.reranker.litellmModels' })}
|
||||||
|
</SelectLabel>
|
||||||
|
{litellmModels!.map((m) => (
|
||||||
|
<SelectItem key={m.modelId} value={m.modelId}>
|
||||||
|
{m.modelName}
|
||||||
|
</SelectItem>
|
||||||
|
))}
|
||||||
|
</SelectGroup>
|
||||||
|
</SelectContent>
|
||||||
|
</Select>
|
||||||
|
) : (
|
||||||
|
<Input
|
||||||
|
value={modelName}
|
||||||
|
onChange={(e) => setModelName(e.target.value)}
|
||||||
|
placeholder={formatMessage({ id: 'codexlens.reranker.selectModel' })}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
<p className="text-xs text-muted-foreground">
|
||||||
|
{formatMessage({ id: 'codexlens.reranker.modelHint' })}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Provider Select (only for api backend) */}
|
||||||
|
{showProviderSelect && (
|
||||||
|
<div className="space-y-2">
|
||||||
|
<Label>{formatMessage({ id: 'codexlens.reranker.provider' })}</Label>
|
||||||
|
<Select value={apiProvider} onValueChange={setApiProvider}>
|
||||||
|
<SelectTrigger>
|
||||||
|
<SelectValue placeholder={formatMessage({ id: 'codexlens.reranker.selectProvider' })} />
|
||||||
|
</SelectTrigger>
|
||||||
|
<SelectContent>
|
||||||
|
{apiProviders.length > 0 ? (
|
||||||
|
apiProviders.map((p) => (
|
||||||
|
<SelectItem key={p} value={p}>
|
||||||
|
{p}
|
||||||
|
</SelectItem>
|
||||||
|
))
|
||||||
|
) : (
|
||||||
|
<SelectItem value="_none" disabled>
|
||||||
|
{formatMessage({ id: 'codexlens.reranker.noProviders' })}
|
||||||
|
</SelectItem>
|
||||||
|
)}
|
||||||
|
</SelectContent>
|
||||||
|
</Select>
|
||||||
|
<p className="text-xs text-muted-foreground">
|
||||||
|
{formatMessage({ id: 'codexlens.reranker.providerHint' })}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Status Row */}
|
||||||
|
<div className="flex items-center gap-4 text-sm text-muted-foreground pt-2 border-t">
|
||||||
|
<span>
|
||||||
|
{formatMessage({ id: 'codexlens.reranker.apiKeyStatus' })}:{' '}
|
||||||
|
<span className={apiKeySet ? 'text-green-600' : 'text-yellow-600'}>
|
||||||
|
{apiKeySet
|
||||||
|
? formatMessage({ id: 'codexlens.reranker.apiKeySet' })
|
||||||
|
: formatMessage({ id: 'codexlens.reranker.apiKeyNotSet' })}
|
||||||
|
</span>
|
||||||
|
</span>
|
||||||
|
<span>
|
||||||
|
{formatMessage({ id: 'codexlens.reranker.configSource' })}: {configSource}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Save Button */}
|
||||||
|
<div className="flex items-center gap-2 pt-2">
|
||||||
|
<Button
|
||||||
|
onClick={handleSave}
|
||||||
|
disabled={isUpdating || !hasChanges}
|
||||||
|
size="sm"
|
||||||
|
>
|
||||||
|
{isUpdating ? (
|
||||||
|
<Loader2 className="w-4 h-4 mr-2 animate-spin" />
|
||||||
|
) : (
|
||||||
|
<Save className="w-4 h-4 mr-2" />
|
||||||
|
)}
|
||||||
|
{isUpdating
|
||||||
|
? formatMessage({ id: 'codexlens.reranker.saving' })
|
||||||
|
: formatMessage({ id: 'codexlens.reranker.save' })}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Card>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ========== Settings Tab ==========
|
||||||
|
|
||||||
interface SettingsTabProps {
|
interface SettingsTabProps {
|
||||||
enabled?: boolean;
|
enabled?: boolean;
|
||||||
}
|
}
|
||||||
@@ -219,6 +462,9 @@ export function SettingsTab({ enabled = true }: SettingsTabProps) {
|
|||||||
</div>
|
</div>
|
||||||
</Card>
|
</Card>
|
||||||
|
|
||||||
|
{/* Reranker Configuration */}
|
||||||
|
<RerankerConfigCard enabled={enabled} />
|
||||||
|
|
||||||
{/* General Configuration */}
|
{/* General Configuration */}
|
||||||
<Card className="p-6">
|
<Card className="p-6">
|
||||||
<h3 className="text-lg font-semibold text-foreground mb-4">
|
<h3 className="text-lg font-semibold text-foreground mb-4">
|
||||||
|
|||||||
@@ -286,6 +286,11 @@ export {
|
|||||||
useCancelIndexing,
|
useCancelIndexing,
|
||||||
useCodexLensWatcher,
|
useCodexLensWatcher,
|
||||||
useCodexLensWatcherMutations,
|
useCodexLensWatcherMutations,
|
||||||
|
useCodexLensLspStatus,
|
||||||
|
useCodexLensLspMutations,
|
||||||
|
useCodexLensRerankerConfig,
|
||||||
|
useUpdateRerankerConfig,
|
||||||
|
useCcwToolsList,
|
||||||
} from './useCodexLens';
|
} from './useCodexLens';
|
||||||
export type {
|
export type {
|
||||||
UseCodexLensDashboardOptions,
|
UseCodexLensDashboardOptions,
|
||||||
@@ -323,4 +328,11 @@ export type {
|
|||||||
UseCodexLensWatcherOptions,
|
UseCodexLensWatcherOptions,
|
||||||
UseCodexLensWatcherReturn,
|
UseCodexLensWatcherReturn,
|
||||||
UseCodexLensWatcherMutationsReturn,
|
UseCodexLensWatcherMutationsReturn,
|
||||||
|
UseCodexLensLspStatusOptions,
|
||||||
|
UseCodexLensLspStatusReturn,
|
||||||
|
UseCodexLensLspMutationsReturn,
|
||||||
|
UseCodexLensRerankerConfigOptions,
|
||||||
|
UseCodexLensRerankerConfigReturn,
|
||||||
|
UseUpdateRerankerConfigReturn,
|
||||||
|
UseCcwToolsListReturn,
|
||||||
} from './useCodexLens';
|
} from './useCodexLens';
|
||||||
|
|||||||
@@ -64,8 +64,18 @@ import {
|
|||||||
type CodexLensLspStatusResponse,
|
type CodexLensLspStatusResponse,
|
||||||
type CodexLensSemanticSearchParams,
|
type CodexLensSemanticSearchParams,
|
||||||
type CodexLensSemanticSearchResponse,
|
type CodexLensSemanticSearchResponse,
|
||||||
|
type RerankerConfigResponse,
|
||||||
|
type RerankerConfigUpdateRequest,
|
||||||
|
type RerankerConfigUpdateResponse,
|
||||||
fetchCodexLensLspStatus,
|
fetchCodexLensLspStatus,
|
||||||
|
startCodexLensLsp,
|
||||||
|
stopCodexLensLsp,
|
||||||
|
restartCodexLensLsp,
|
||||||
semanticSearchCodexLens,
|
semanticSearchCodexLens,
|
||||||
|
fetchRerankerConfig,
|
||||||
|
updateRerankerConfig,
|
||||||
|
fetchCcwTools,
|
||||||
|
type CcwToolInfo,
|
||||||
} from '../lib/api';
|
} from '../lib/api';
|
||||||
import { useWorkflowStore, selectProjectPath } from '@/stores/workflowStore';
|
import { useWorkflowStore, selectProjectPath } from '@/stores/workflowStore';
|
||||||
|
|
||||||
@@ -91,6 +101,8 @@ export const codexLensKeys = {
|
|||||||
lspStatus: () => [...codexLensKeys.all, 'lspStatus'] as const,
|
lspStatus: () => [...codexLensKeys.all, 'lspStatus'] as const,
|
||||||
semanticSearch: (params: CodexLensSemanticSearchParams) => [...codexLensKeys.all, 'semanticSearch', params] as const,
|
semanticSearch: (params: CodexLensSemanticSearchParams) => [...codexLensKeys.all, 'semanticSearch', params] as const,
|
||||||
watcher: () => [...codexLensKeys.all, 'watcher'] as const,
|
watcher: () => [...codexLensKeys.all, 'watcher'] as const,
|
||||||
|
rerankerConfig: () => [...codexLensKeys.all, 'rerankerConfig'] as const,
|
||||||
|
ccwTools: () => [...codexLensKeys.all, 'ccwTools'] as const,
|
||||||
};
|
};
|
||||||
|
|
||||||
// Default stale times
|
// Default stale times
|
||||||
@@ -1384,6 +1396,8 @@ export interface UseCodexLensLspStatusReturn {
|
|||||||
available: boolean;
|
available: boolean;
|
||||||
semanticAvailable: boolean;
|
semanticAvailable: boolean;
|
||||||
vectorIndex: boolean;
|
vectorIndex: boolean;
|
||||||
|
projectCount: number;
|
||||||
|
embeddings: Record<string, unknown> | undefined;
|
||||||
modes: string[];
|
modes: string[];
|
||||||
strategies: string[];
|
strategies: string[];
|
||||||
isLoading: boolean;
|
isLoading: boolean;
|
||||||
@@ -1393,6 +1407,7 @@ export interface UseCodexLensLspStatusReturn {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Hook for checking CodexLens LSP/semantic search availability
|
* Hook for checking CodexLens LSP/semantic search availability
|
||||||
|
* Polls every 5 seconds when the LSP server is available
|
||||||
*/
|
*/
|
||||||
export function useCodexLensLspStatus(options: UseCodexLensLspStatusOptions = {}): UseCodexLensLspStatusReturn {
|
export function useCodexLensLspStatus(options: UseCodexLensLspStatusOptions = {}): UseCodexLensLspStatusReturn {
|
||||||
const { enabled = true, staleTime = STALE_TIME_MEDIUM } = options;
|
const { enabled = true, staleTime = STALE_TIME_MEDIUM } = options;
|
||||||
@@ -1402,6 +1417,10 @@ export function useCodexLensLspStatus(options: UseCodexLensLspStatusOptions = {}
|
|||||||
queryFn: fetchCodexLensLspStatus,
|
queryFn: fetchCodexLensLspStatus,
|
||||||
staleTime,
|
staleTime,
|
||||||
enabled,
|
enabled,
|
||||||
|
refetchInterval: (query) => {
|
||||||
|
const data = query.state.data as CodexLensLspStatusResponse | undefined;
|
||||||
|
return data?.available ? 5000 : false;
|
||||||
|
},
|
||||||
retry: 2,
|
retry: 2,
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -1414,6 +1433,8 @@ export function useCodexLensLspStatus(options: UseCodexLensLspStatusOptions = {}
|
|||||||
available: query.data?.available ?? false,
|
available: query.data?.available ?? false,
|
||||||
semanticAvailable: query.data?.semantic_available ?? false,
|
semanticAvailable: query.data?.semantic_available ?? false,
|
||||||
vectorIndex: query.data?.vector_index ?? false,
|
vectorIndex: query.data?.vector_index ?? false,
|
||||||
|
projectCount: query.data?.project_count ?? 0,
|
||||||
|
embeddings: query.data?.embeddings,
|
||||||
modes: query.data?.modes ?? [],
|
modes: query.data?.modes ?? [],
|
||||||
strategies: query.data?.strategies ?? [],
|
strategies: query.data?.strategies ?? [],
|
||||||
isLoading: query.isLoading,
|
isLoading: query.isLoading,
|
||||||
@@ -1422,6 +1443,84 @@ export function useCodexLensLspStatus(options: UseCodexLensLspStatusOptions = {}
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface UseCodexLensLspMutationsReturn {
|
||||||
|
startLsp: (path?: string) => Promise<{ success: boolean; message?: string; error?: string }>;
|
||||||
|
stopLsp: (path?: string) => Promise<{ success: boolean; message?: string; error?: string }>;
|
||||||
|
restartLsp: (path?: string) => Promise<{ success: boolean; message?: string; error?: string }>;
|
||||||
|
isStarting: boolean;
|
||||||
|
isStopping: boolean;
|
||||||
|
isRestarting: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Hook for LSP server start/stop/restart mutations
|
||||||
|
*/
|
||||||
|
export function useCodexLensLspMutations(): UseCodexLensLspMutationsReturn {
|
||||||
|
const queryClient = useQueryClient();
|
||||||
|
const formatMessage = useFormatMessage();
|
||||||
|
const { success, error: errorToast } = useNotifications();
|
||||||
|
|
||||||
|
const startMutation = useMutation({
|
||||||
|
mutationFn: ({ path }: { path?: string }) => startCodexLensLsp(path),
|
||||||
|
onSuccess: () => {
|
||||||
|
queryClient.invalidateQueries({ queryKey: codexLensKeys.lspStatus() });
|
||||||
|
success(
|
||||||
|
formatMessage({ id: 'common.success' }),
|
||||||
|
formatMessage({ id: 'codexlens.lsp.started' })
|
||||||
|
);
|
||||||
|
},
|
||||||
|
onError: (err) => {
|
||||||
|
const sanitized = sanitizeErrorMessage(err, 'codexLensLspStart');
|
||||||
|
const message = formatMessage({ id: sanitized.messageKey });
|
||||||
|
const title = formatMessage({ id: 'common.error' });
|
||||||
|
errorToast(title, message);
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const stopMutation = useMutation({
|
||||||
|
mutationFn: ({ path }: { path?: string }) => stopCodexLensLsp(path),
|
||||||
|
onSuccess: () => {
|
||||||
|
queryClient.invalidateQueries({ queryKey: codexLensKeys.lspStatus() });
|
||||||
|
success(
|
||||||
|
formatMessage({ id: 'common.success' }),
|
||||||
|
formatMessage({ id: 'codexlens.lsp.stopped' })
|
||||||
|
);
|
||||||
|
},
|
||||||
|
onError: (err) => {
|
||||||
|
const sanitized = sanitizeErrorMessage(err, 'codexLensLspStop');
|
||||||
|
const message = formatMessage({ id: sanitized.messageKey });
|
||||||
|
const title = formatMessage({ id: 'common.error' });
|
||||||
|
errorToast(title, message);
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const restartMutation = useMutation({
|
||||||
|
mutationFn: ({ path }: { path?: string }) => restartCodexLensLsp(path),
|
||||||
|
onSuccess: () => {
|
||||||
|
queryClient.invalidateQueries({ queryKey: codexLensKeys.lspStatus() });
|
||||||
|
success(
|
||||||
|
formatMessage({ id: 'common.success' }),
|
||||||
|
formatMessage({ id: 'codexlens.lsp.restarted' })
|
||||||
|
);
|
||||||
|
},
|
||||||
|
onError: (err) => {
|
||||||
|
const sanitized = sanitizeErrorMessage(err, 'codexLensLspRestart');
|
||||||
|
const message = formatMessage({ id: sanitized.messageKey });
|
||||||
|
const title = formatMessage({ id: 'common.error' });
|
||||||
|
errorToast(title, message);
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
return {
|
||||||
|
startLsp: (path?: string) => startMutation.mutateAsync({ path }),
|
||||||
|
stopLsp: (path?: string) => stopMutation.mutateAsync({ path }),
|
||||||
|
restartLsp: (path?: string) => restartMutation.mutateAsync({ path }),
|
||||||
|
isStarting: startMutation.isPending,
|
||||||
|
isStopping: stopMutation.isPending,
|
||||||
|
isRestarting: restartMutation.isPending,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
export interface UseCodexLensSemanticSearchOptions {
|
export interface UseCodexLensSemanticSearchOptions {
|
||||||
enabled?: boolean;
|
enabled?: boolean;
|
||||||
}
|
}
|
||||||
@@ -1568,3 +1667,127 @@ export function useCodexLensWatcherMutations(): UseCodexLensWatcherMutationsRetu
|
|||||||
isStopping: stopMutation.isPending,
|
isStopping: stopMutation.isPending,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ========== Reranker Config Hooks ==========
|
||||||
|
|
||||||
|
export interface UseCodexLensRerankerConfigOptions {
|
||||||
|
enabled?: boolean;
|
||||||
|
staleTime?: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface UseCodexLensRerankerConfigReturn {
|
||||||
|
data: RerankerConfigResponse | undefined;
|
||||||
|
backend: string;
|
||||||
|
modelName: string;
|
||||||
|
apiProvider: string;
|
||||||
|
apiKeySet: boolean;
|
||||||
|
availableBackends: string[];
|
||||||
|
apiProviders: string[];
|
||||||
|
litellmModels: RerankerConfigResponse['litellm_models'];
|
||||||
|
configSource: string;
|
||||||
|
isLoading: boolean;
|
||||||
|
error: Error | null;
|
||||||
|
refetch: () => Promise<void>;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Hook for fetching reranker configuration (backends, models, providers)
|
||||||
|
*/
|
||||||
|
export function useCodexLensRerankerConfig(
|
||||||
|
options: UseCodexLensRerankerConfigOptions = {}
|
||||||
|
): UseCodexLensRerankerConfigReturn {
|
||||||
|
const { enabled = true, staleTime = STALE_TIME_MEDIUM } = options;
|
||||||
|
|
||||||
|
const query = useQuery({
|
||||||
|
queryKey: codexLensKeys.rerankerConfig(),
|
||||||
|
queryFn: fetchRerankerConfig,
|
||||||
|
staleTime,
|
||||||
|
enabled,
|
||||||
|
retry: 2,
|
||||||
|
});
|
||||||
|
|
||||||
|
const refetch = async () => {
|
||||||
|
await query.refetch();
|
||||||
|
};
|
||||||
|
|
||||||
|
return {
|
||||||
|
data: query.data,
|
||||||
|
backend: query.data?.backend ?? 'fastembed',
|
||||||
|
modelName: query.data?.model_name ?? '',
|
||||||
|
apiProvider: query.data?.api_provider ?? '',
|
||||||
|
apiKeySet: query.data?.api_key_set ?? false,
|
||||||
|
availableBackends: query.data?.available_backends ?? [],
|
||||||
|
apiProviders: query.data?.api_providers ?? [],
|
||||||
|
litellmModels: query.data?.litellm_models,
|
||||||
|
configSource: query.data?.config_source ?? 'default',
|
||||||
|
isLoading: query.isLoading,
|
||||||
|
error: query.error,
|
||||||
|
refetch,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface UseUpdateRerankerConfigReturn {
|
||||||
|
updateConfig: (request: RerankerConfigUpdateRequest) => Promise<RerankerConfigUpdateResponse>;
|
||||||
|
isUpdating: boolean;
|
||||||
|
error: Error | null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Hook for updating reranker configuration
|
||||||
|
*/
|
||||||
|
export function useUpdateRerankerConfig(): UseUpdateRerankerConfigReturn {
|
||||||
|
const queryClient = useQueryClient();
|
||||||
|
const formatMessage = useFormatMessage();
|
||||||
|
const { success, error: errorToast } = useNotifications();
|
||||||
|
|
||||||
|
const mutation = useMutation({
|
||||||
|
mutationFn: (request: RerankerConfigUpdateRequest) => updateRerankerConfig(request),
|
||||||
|
onSuccess: () => {
|
||||||
|
queryClient.invalidateQueries({ queryKey: codexLensKeys.rerankerConfig() });
|
||||||
|
queryClient.invalidateQueries({ queryKey: codexLensKeys.env() });
|
||||||
|
success(
|
||||||
|
formatMessage({ id: 'common.success' }),
|
||||||
|
formatMessage({ id: 'codexlens.reranker.saveSuccess' })
|
||||||
|
);
|
||||||
|
},
|
||||||
|
onError: (err) => {
|
||||||
|
const sanitized = sanitizeErrorMessage(err, 'rerankerConfigUpdate');
|
||||||
|
const message = formatMessage({ id: sanitized.messageKey });
|
||||||
|
const title = formatMessage({ id: 'common.error' });
|
||||||
|
errorToast(title, message);
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
return {
|
||||||
|
updateConfig: mutation.mutateAsync,
|
||||||
|
isUpdating: mutation.isPending,
|
||||||
|
error: mutation.error,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// ========== CCW Tools Hook ==========
|
||||||
|
|
||||||
|
export interface UseCcwToolsListReturn {
|
||||||
|
tools: CcwToolInfo[];
|
||||||
|
isLoading: boolean;
|
||||||
|
error: Error | null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Hook for fetching all registered CCW tools
|
||||||
|
* Uses LONG stale time since tool list rarely changes
|
||||||
|
*/
|
||||||
|
export function useCcwToolsList(): UseCcwToolsListReturn {
|
||||||
|
const query = useQuery({
|
||||||
|
queryKey: codexLensKeys.ccwTools(),
|
||||||
|
queryFn: fetchCcwTools,
|
||||||
|
staleTime: STALE_TIME_LONG,
|
||||||
|
retry: 2,
|
||||||
|
});
|
||||||
|
|
||||||
|
return {
|
||||||
|
tools: query.data ?? [],
|
||||||
|
isLoading: query.isLoading,
|
||||||
|
error: query.error,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|||||||
@@ -3411,7 +3411,7 @@ export interface CcwMcpConfig {
|
|||||||
enabledTools: string[];
|
enabledTools: string[];
|
||||||
projectRoot?: string;
|
projectRoot?: string;
|
||||||
allowedDirs?: string;
|
allowedDirs?: string;
|
||||||
disableSandbox?: boolean;
|
enableSandbox?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -3426,7 +3426,7 @@ function buildCcwMcpServerConfig(config: {
|
|||||||
enabledTools?: string[];
|
enabledTools?: string[];
|
||||||
projectRoot?: string;
|
projectRoot?: string;
|
||||||
allowedDirs?: string;
|
allowedDirs?: string;
|
||||||
disableSandbox?: boolean;
|
enableSandbox?: boolean;
|
||||||
}): { command: string; args: string[]; env: Record<string, string> } {
|
}): { command: string; args: string[]; env: Record<string, string> } {
|
||||||
const env: Record<string, string> = {};
|
const env: Record<string, string> = {};
|
||||||
|
|
||||||
@@ -3442,8 +3442,8 @@ function buildCcwMcpServerConfig(config: {
|
|||||||
if (config.allowedDirs) {
|
if (config.allowedDirs) {
|
||||||
env.CCW_ALLOWED_DIRS = config.allowedDirs;
|
env.CCW_ALLOWED_DIRS = config.allowedDirs;
|
||||||
}
|
}
|
||||||
if (config.disableSandbox) {
|
if (config.enableSandbox) {
|
||||||
env.CCW_DISABLE_SANDBOX = '1';
|
env.CCW_ENABLE_SANDBOX = '1';
|
||||||
}
|
}
|
||||||
|
|
||||||
// Cross-platform config
|
// Cross-platform config
|
||||||
@@ -3508,7 +3508,7 @@ export async function fetchCcwMcpConfig(): Promise<CcwMcpConfig> {
|
|||||||
enabledTools,
|
enabledTools,
|
||||||
projectRoot: env.CCW_PROJECT_ROOT,
|
projectRoot: env.CCW_PROJECT_ROOT,
|
||||||
allowedDirs: env.CCW_ALLOWED_DIRS,
|
allowedDirs: env.CCW_ALLOWED_DIRS,
|
||||||
disableSandbox: env.CCW_DISABLE_SANDBOX === '1',
|
enableSandbox: env.CCW_ENABLE_SANDBOX === '1',
|
||||||
};
|
};
|
||||||
} catch {
|
} catch {
|
||||||
return {
|
return {
|
||||||
@@ -3525,7 +3525,7 @@ export async function updateCcwConfig(config: {
|
|||||||
enabledTools?: string[];
|
enabledTools?: string[];
|
||||||
projectRoot?: string;
|
projectRoot?: string;
|
||||||
allowedDirs?: string;
|
allowedDirs?: string;
|
||||||
disableSandbox?: boolean;
|
enableSandbox?: boolean;
|
||||||
}): Promise<CcwMcpConfig> {
|
}): Promise<CcwMcpConfig> {
|
||||||
const serverConfig = buildCcwMcpServerConfig(config);
|
const serverConfig = buildCcwMcpServerConfig(config);
|
||||||
|
|
||||||
@@ -3630,7 +3630,7 @@ export async function fetchCcwMcpConfigForCodex(): Promise<CcwMcpConfig> {
|
|||||||
enabledTools,
|
enabledTools,
|
||||||
projectRoot: env.CCW_PROJECT_ROOT,
|
projectRoot: env.CCW_PROJECT_ROOT,
|
||||||
allowedDirs: env.CCW_ALLOWED_DIRS,
|
allowedDirs: env.CCW_ALLOWED_DIRS,
|
||||||
disableSandbox: env.CCW_DISABLE_SANDBOX === '1',
|
enableSandbox: env.CCW_ENABLE_SANDBOX === '1',
|
||||||
};
|
};
|
||||||
} catch {
|
} catch {
|
||||||
return { isInstalled: false, enabledTools: [] };
|
return { isInstalled: false, enabledTools: [] };
|
||||||
@@ -3644,7 +3644,7 @@ function buildCcwMcpServerConfigForCodex(config: {
|
|||||||
enabledTools?: string[];
|
enabledTools?: string[];
|
||||||
projectRoot?: string;
|
projectRoot?: string;
|
||||||
allowedDirs?: string;
|
allowedDirs?: string;
|
||||||
disableSandbox?: boolean;
|
enableSandbox?: boolean;
|
||||||
}): { command: string; args: string[]; env: Record<string, string> } {
|
}): { command: string; args: string[]; env: Record<string, string> } {
|
||||||
const env: Record<string, string> = {};
|
const env: Record<string, string> = {};
|
||||||
|
|
||||||
@@ -3660,8 +3660,8 @@ function buildCcwMcpServerConfigForCodex(config: {
|
|||||||
if (config.allowedDirs) {
|
if (config.allowedDirs) {
|
||||||
env.CCW_ALLOWED_DIRS = config.allowedDirs;
|
env.CCW_ALLOWED_DIRS = config.allowedDirs;
|
||||||
}
|
}
|
||||||
if (config.disableSandbox) {
|
if (config.enableSandbox) {
|
||||||
env.CCW_DISABLE_SANDBOX = '1';
|
env.CCW_ENABLE_SANDBOX = '1';
|
||||||
}
|
}
|
||||||
|
|
||||||
return { command: 'ccw-mcp', args: [], env };
|
return { command: 'ccw-mcp', args: [], env };
|
||||||
@@ -3700,7 +3700,7 @@ export async function updateCcwConfigForCodex(config: {
|
|||||||
enabledTools?: string[];
|
enabledTools?: string[];
|
||||||
projectRoot?: string;
|
projectRoot?: string;
|
||||||
allowedDirs?: string;
|
allowedDirs?: string;
|
||||||
disableSandbox?: boolean;
|
enableSandbox?: boolean;
|
||||||
}): Promise<CcwMcpConfig> {
|
}): Promise<CcwMcpConfig> {
|
||||||
const serverConfig = buildCcwMcpServerConfigForCodex(config);
|
const serverConfig = buildCcwMcpServerConfigForCodex(config);
|
||||||
|
|
||||||
@@ -4540,6 +4540,74 @@ export async function updateCodexLensIgnorePatterns(request: CodexLensUpdateIgno
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ========== CodexLens Reranker Config API ==========
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Reranker LiteLLM model info
|
||||||
|
*/
|
||||||
|
export interface RerankerLitellmModel {
|
||||||
|
modelId: string;
|
||||||
|
modelName: string;
|
||||||
|
providers: string[];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Reranker configuration response from GET /api/codexlens/reranker/config
|
||||||
|
*/
|
||||||
|
export interface RerankerConfigResponse {
|
||||||
|
success: boolean;
|
||||||
|
backend: string;
|
||||||
|
model_name: string;
|
||||||
|
api_provider: string;
|
||||||
|
api_key_set: boolean;
|
||||||
|
available_backends: string[];
|
||||||
|
api_providers: string[];
|
||||||
|
litellm_endpoints: string[];
|
||||||
|
litellm_models?: RerankerLitellmModel[];
|
||||||
|
config_source: string;
|
||||||
|
error?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Reranker configuration update request for POST /api/codexlens/reranker/config
|
||||||
|
*/
|
||||||
|
export interface RerankerConfigUpdateRequest {
|
||||||
|
backend?: string;
|
||||||
|
model_name?: string;
|
||||||
|
api_provider?: string;
|
||||||
|
api_key?: string;
|
||||||
|
litellm_endpoint?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Reranker configuration update response
|
||||||
|
*/
|
||||||
|
export interface RerankerConfigUpdateResponse {
|
||||||
|
success: boolean;
|
||||||
|
message?: string;
|
||||||
|
updates?: string[];
|
||||||
|
error?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Fetch reranker configuration (backends, models, providers)
|
||||||
|
*/
|
||||||
|
export async function fetchRerankerConfig(): Promise<RerankerConfigResponse> {
|
||||||
|
return fetchApi<RerankerConfigResponse>('/api/codexlens/reranker/config');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Update reranker configuration
|
||||||
|
*/
|
||||||
|
export async function updateRerankerConfig(
|
||||||
|
request: RerankerConfigUpdateRequest
|
||||||
|
): Promise<RerankerConfigUpdateResponse> {
|
||||||
|
return fetchApi<RerankerConfigUpdateResponse>('/api/codexlens/reranker/config', {
|
||||||
|
method: 'POST',
|
||||||
|
body: JSON.stringify(request),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
// ========== CodexLens Search API ==========
|
// ========== CodexLens Search API ==========
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -4709,6 +4777,36 @@ export async function fetchCodexLensLspStatus(): Promise<CodexLensLspStatusRespo
|
|||||||
return fetchApi<CodexLensLspStatusResponse>('/api/codexlens/lsp/status');
|
return fetchApi<CodexLensLspStatusResponse>('/api/codexlens/lsp/status');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Start CodexLens LSP server
|
||||||
|
*/
|
||||||
|
export async function startCodexLensLsp(path?: string): Promise<{ success: boolean; message?: string; workspace_root?: string; error?: string }> {
|
||||||
|
return fetchApi('/api/codexlens/lsp/start', {
|
||||||
|
method: 'POST',
|
||||||
|
body: JSON.stringify({ path }),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Stop CodexLens LSP server
|
||||||
|
*/
|
||||||
|
export async function stopCodexLensLsp(path?: string): Promise<{ success: boolean; message?: string; error?: string }> {
|
||||||
|
return fetchApi('/api/codexlens/lsp/stop', {
|
||||||
|
method: 'POST',
|
||||||
|
body: JSON.stringify({ path }),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Restart CodexLens LSP server
|
||||||
|
*/
|
||||||
|
export async function restartCodexLensLsp(path?: string): Promise<{ success: boolean; message?: string; workspace_root?: string; error?: string }> {
|
||||||
|
return fetchApi('/api/codexlens/lsp/restart', {
|
||||||
|
method: 'POST',
|
||||||
|
body: JSON.stringify({ path }),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Perform semantic search using CodexLens Python API
|
* Perform semantic search using CodexLens Python API
|
||||||
*/
|
*/
|
||||||
@@ -5845,6 +5943,25 @@ export async function upgradeCcwInstallation(
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ========== CCW Tools API ==========
|
||||||
|
|
||||||
|
/**
|
||||||
|
* CCW tool info returned by /api/ccw/tools
|
||||||
|
*/
|
||||||
|
export interface CcwToolInfo {
|
||||||
|
name: string;
|
||||||
|
description: string;
|
||||||
|
parameters?: Record<string, unknown>;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Fetch all registered CCW tools
|
||||||
|
*/
|
||||||
|
export async function fetchCcwTools(): Promise<CcwToolInfo[]> {
|
||||||
|
const data = await fetchApi<{ tools: CcwToolInfo[] }>('/api/ccw/tools');
|
||||||
|
return data.tools;
|
||||||
|
}
|
||||||
|
|
||||||
// ========== Team API ==========
|
// ========== Team API ==========
|
||||||
|
|
||||||
export async function fetchTeams(): Promise<{ teams: Array<{ name: string; messageCount: number; lastActivity: string }> }> {
|
export async function fetchTeams(): Promise<{ teams: Array<{ name: string; messageCount: number; lastActivity: string }> }> {
|
||||||
|
|||||||
@@ -255,6 +255,31 @@
|
|||||||
"description": "Please install CodexLens to use semantic code search features."
|
"description": "Please install CodexLens to use semantic code search features."
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"reranker": {
|
||||||
|
"title": "Reranker Configuration",
|
||||||
|
"description": "Configure the reranker backend, model, and provider for search result ranking.",
|
||||||
|
"backend": "Backend",
|
||||||
|
"backendHint": "Inference backend for reranking",
|
||||||
|
"model": "Model",
|
||||||
|
"modelHint": "Reranker model name or LiteLLM endpoint",
|
||||||
|
"provider": "API Provider",
|
||||||
|
"providerHint": "API provider for reranker service",
|
||||||
|
"apiKeyStatus": "API Key",
|
||||||
|
"apiKeySet": "Configured",
|
||||||
|
"apiKeyNotSet": "Not configured",
|
||||||
|
"configSource": "Config Source",
|
||||||
|
"save": "Save Reranker Config",
|
||||||
|
"saving": "Saving...",
|
||||||
|
"saveSuccess": "Reranker configuration saved",
|
||||||
|
"saveFailed": "Failed to save reranker configuration",
|
||||||
|
"noBackends": "No backends available",
|
||||||
|
"noModels": "No models available",
|
||||||
|
"noProviders": "No providers available",
|
||||||
|
"litellmModels": "LiteLLM Models",
|
||||||
|
"selectBackend": "Select backend...",
|
||||||
|
"selectModel": "Select model...",
|
||||||
|
"selectProvider": "Select provider..."
|
||||||
|
},
|
||||||
"envGroup": {
|
"envGroup": {
|
||||||
"embedding": "Embedding",
|
"embedding": "Embedding",
|
||||||
"reranker": "Reranker",
|
"reranker": "Reranker",
|
||||||
@@ -309,6 +334,16 @@
|
|||||||
"installNow": "Install Now",
|
"installNow": "Install Now",
|
||||||
"installing": "Installing..."
|
"installing": "Installing..."
|
||||||
},
|
},
|
||||||
|
"mcp": {
|
||||||
|
"title": "CCW Tools Registry",
|
||||||
|
"loading": "Loading tools...",
|
||||||
|
"error": "Failed to load tools",
|
||||||
|
"errorDesc": "Unable to fetch CCW tools list. Please check if the server is running.",
|
||||||
|
"emptyDesc": "No tools are currently registered.",
|
||||||
|
"totalCount": "{count} tools",
|
||||||
|
"codexLensSection": "CodexLens Tools",
|
||||||
|
"otherSection": "Other Tools"
|
||||||
|
},
|
||||||
"watcher": {
|
"watcher": {
|
||||||
"title": "File Watcher",
|
"title": "File Watcher",
|
||||||
"status": {
|
"status": {
|
||||||
@@ -323,5 +358,27 @@
|
|||||||
"stopping": "Stopping...",
|
"stopping": "Stopping...",
|
||||||
"started": "File watcher started",
|
"started": "File watcher started",
|
||||||
"stopped": "File watcher stopped"
|
"stopped": "File watcher stopped"
|
||||||
|
},
|
||||||
|
"lsp": {
|
||||||
|
"title": "LSP Server",
|
||||||
|
"status": {
|
||||||
|
"running": "Running",
|
||||||
|
"stopped": "Stopped"
|
||||||
|
},
|
||||||
|
"projects": "Projects",
|
||||||
|
"embeddings": "Embeddings",
|
||||||
|
"modes": "Modes",
|
||||||
|
"semanticAvailable": "Semantic",
|
||||||
|
"available": "Available",
|
||||||
|
"unavailable": "Unavailable",
|
||||||
|
"start": "Start Server",
|
||||||
|
"starting": "Starting...",
|
||||||
|
"stop": "Stop Server",
|
||||||
|
"stopping": "Stopping...",
|
||||||
|
"restart": "Restart",
|
||||||
|
"restarting": "Restarting...",
|
||||||
|
"started": "LSP server started",
|
||||||
|
"stopped": "LSP server stopped",
|
||||||
|
"restarted": "LSP server restarted"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -255,6 +255,31 @@
|
|||||||
"description": "请先安装 CodexLens 以使用语义代码搜索功能。"
|
"description": "请先安装 CodexLens 以使用语义代码搜索功能。"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"reranker": {
|
||||||
|
"title": "重排序配置",
|
||||||
|
"description": "配置重排序后端、模型和提供商,用于搜索结果排序。",
|
||||||
|
"backend": "后端",
|
||||||
|
"backendHint": "重排序推理后端",
|
||||||
|
"model": "模型",
|
||||||
|
"modelHint": "重排序模型名称或 LiteLLM 端点",
|
||||||
|
"provider": "API 提供商",
|
||||||
|
"providerHint": "重排序服务的 API 提供商",
|
||||||
|
"apiKeyStatus": "API 密钥",
|
||||||
|
"apiKeySet": "已配置",
|
||||||
|
"apiKeyNotSet": "未配置",
|
||||||
|
"configSource": "配置来源",
|
||||||
|
"save": "保存重排序配置",
|
||||||
|
"saving": "保存中...",
|
||||||
|
"saveSuccess": "重排序配置已保存",
|
||||||
|
"saveFailed": "保存重排序配置失败",
|
||||||
|
"noBackends": "无可用后端",
|
||||||
|
"noModels": "无可用模型",
|
||||||
|
"noProviders": "无可用提供商",
|
||||||
|
"litellmModels": "LiteLLM 模型",
|
||||||
|
"selectBackend": "选择后端...",
|
||||||
|
"selectModel": "选择模型...",
|
||||||
|
"selectProvider": "选择提供商..."
|
||||||
|
},
|
||||||
"envGroup": {
|
"envGroup": {
|
||||||
"embedding": "嵌入模型",
|
"embedding": "嵌入模型",
|
||||||
"reranker": "重排序",
|
"reranker": "重排序",
|
||||||
@@ -309,6 +334,16 @@
|
|||||||
"installNow": "立即安装",
|
"installNow": "立即安装",
|
||||||
"installing": "安装中..."
|
"installing": "安装中..."
|
||||||
},
|
},
|
||||||
|
"mcp": {
|
||||||
|
"title": "CCW 工具注册表",
|
||||||
|
"loading": "加载工具中...",
|
||||||
|
"error": "加载工具失败",
|
||||||
|
"errorDesc": "无法获取 CCW 工具列表。请检查服务器是否正在运行。",
|
||||||
|
"emptyDesc": "当前没有已注册的工具。",
|
||||||
|
"totalCount": "{count} 个工具",
|
||||||
|
"codexLensSection": "CodexLens 工具",
|
||||||
|
"otherSection": "其他工具"
|
||||||
|
},
|
||||||
"watcher": {
|
"watcher": {
|
||||||
"title": "文件监听器",
|
"title": "文件监听器",
|
||||||
"status": {
|
"status": {
|
||||||
@@ -323,5 +358,27 @@
|
|||||||
"stopping": "停止中...",
|
"stopping": "停止中...",
|
||||||
"started": "文件监听器已启动",
|
"started": "文件监听器已启动",
|
||||||
"stopped": "文件监听器已停止"
|
"stopped": "文件监听器已停止"
|
||||||
|
},
|
||||||
|
"lsp": {
|
||||||
|
"title": "LSP 服务器",
|
||||||
|
"status": {
|
||||||
|
"running": "运行中",
|
||||||
|
"stopped": "已停止"
|
||||||
|
},
|
||||||
|
"projects": "项目数",
|
||||||
|
"embeddings": "嵌入模型",
|
||||||
|
"modes": "模式",
|
||||||
|
"semanticAvailable": "语义搜索",
|
||||||
|
"available": "可用",
|
||||||
|
"unavailable": "不可用",
|
||||||
|
"start": "启动服务",
|
||||||
|
"starting": "启动中...",
|
||||||
|
"stop": "停止服务",
|
||||||
|
"stopping": "停止中...",
|
||||||
|
"restart": "重启",
|
||||||
|
"restarting": "重启中...",
|
||||||
|
"started": "LSP 服务器已启动",
|
||||||
|
"stopped": "LSP 服务器已停止",
|
||||||
|
"restarted": "LSP 服务器已重启"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -251,6 +251,30 @@ const mockMessages: Record<Locale, Record<string, string>> = {
|
|||||||
'codexlens.models.notInstalled.description': 'Please install CodexLens to use model management features.',
|
'codexlens.models.notInstalled.description': 'Please install CodexLens to use model management features.',
|
||||||
'codexlens.models.empty.title': 'No models found',
|
'codexlens.models.empty.title': 'No models found',
|
||||||
'codexlens.models.empty.description': 'Try adjusting your search or filter criteria',
|
'codexlens.models.empty.description': 'Try adjusting your search or filter criteria',
|
||||||
|
// Reranker
|
||||||
|
'codexlens.reranker.title': 'Reranker Configuration',
|
||||||
|
'codexlens.reranker.description': 'Configure the reranker backend, model, and provider for search result ranking.',
|
||||||
|
'codexlens.reranker.backend': 'Backend',
|
||||||
|
'codexlens.reranker.backendHint': 'Inference backend for reranking',
|
||||||
|
'codexlens.reranker.model': 'Model',
|
||||||
|
'codexlens.reranker.modelHint': 'Reranker model name or LiteLLM endpoint',
|
||||||
|
'codexlens.reranker.provider': 'API Provider',
|
||||||
|
'codexlens.reranker.providerHint': 'API provider for reranker service',
|
||||||
|
'codexlens.reranker.apiKeyStatus': 'API Key',
|
||||||
|
'codexlens.reranker.apiKeySet': 'Configured',
|
||||||
|
'codexlens.reranker.apiKeyNotSet': 'Not configured',
|
||||||
|
'codexlens.reranker.configSource': 'Config Source',
|
||||||
|
'codexlens.reranker.save': 'Save Reranker Config',
|
||||||
|
'codexlens.reranker.saving': 'Saving...',
|
||||||
|
'codexlens.reranker.saveSuccess': 'Reranker configuration saved',
|
||||||
|
'codexlens.reranker.saveFailed': 'Failed to save reranker configuration',
|
||||||
|
'codexlens.reranker.noBackends': 'No backends available',
|
||||||
|
'codexlens.reranker.noModels': 'No models available',
|
||||||
|
'codexlens.reranker.noProviders': 'No providers available',
|
||||||
|
'codexlens.reranker.litellmModels': 'LiteLLM Models',
|
||||||
|
'codexlens.reranker.selectBackend': 'Select backend...',
|
||||||
|
'codexlens.reranker.selectModel': 'Select model...',
|
||||||
|
'codexlens.reranker.selectProvider': 'Select provider...',
|
||||||
'navigation.codexlens': 'CodexLens',
|
'navigation.codexlens': 'CodexLens',
|
||||||
},
|
},
|
||||||
zh: {
|
zh: {
|
||||||
@@ -491,6 +515,30 @@ const mockMessages: Record<Locale, Record<string, string>> = {
|
|||||||
'codexlens.models.notInstalled.description': '请先安装 CodexLens 以使用模型管理功能。',
|
'codexlens.models.notInstalled.description': '请先安装 CodexLens 以使用模型管理功能。',
|
||||||
'codexlens.models.empty.title': '没有找到模型',
|
'codexlens.models.empty.title': '没有找到模型',
|
||||||
'codexlens.models.empty.description': '尝试调整搜索或筛选条件',
|
'codexlens.models.empty.description': '尝试调整搜索或筛选条件',
|
||||||
|
// Reranker
|
||||||
|
'codexlens.reranker.title': '重排序配置',
|
||||||
|
'codexlens.reranker.description': '配置重排序后端、模型和提供商,用于搜索结果排序。',
|
||||||
|
'codexlens.reranker.backend': '后端',
|
||||||
|
'codexlens.reranker.backendHint': '重排序推理后端',
|
||||||
|
'codexlens.reranker.model': '模型',
|
||||||
|
'codexlens.reranker.modelHint': '重排序模型名称或 LiteLLM 端点',
|
||||||
|
'codexlens.reranker.provider': 'API 提供商',
|
||||||
|
'codexlens.reranker.providerHint': '重排序服务的 API 提供商',
|
||||||
|
'codexlens.reranker.apiKeyStatus': 'API 密钥',
|
||||||
|
'codexlens.reranker.apiKeySet': '已配置',
|
||||||
|
'codexlens.reranker.apiKeyNotSet': '未配置',
|
||||||
|
'codexlens.reranker.configSource': '配置来源',
|
||||||
|
'codexlens.reranker.save': '保存重排序配置',
|
||||||
|
'codexlens.reranker.saving': '保存中...',
|
||||||
|
'codexlens.reranker.saveSuccess': '重排序配置已保存',
|
||||||
|
'codexlens.reranker.saveFailed': '保存重排序配置失败',
|
||||||
|
'codexlens.reranker.noBackends': '无可用后端',
|
||||||
|
'codexlens.reranker.noModels': '无可用模型',
|
||||||
|
'codexlens.reranker.noProviders': '无可用提供商',
|
||||||
|
'codexlens.reranker.litellmModels': 'LiteLLM 模型',
|
||||||
|
'codexlens.reranker.selectBackend': '选择后端...',
|
||||||
|
'codexlens.reranker.selectModel': '选择模型...',
|
||||||
|
'codexlens.reranker.selectProvider': '选择提供商...',
|
||||||
'navigation.codexlens': 'CodexLens',
|
'navigation.codexlens': 'CodexLens',
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1079,6 +1079,106 @@ except Exception as e:
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// API: LSP Start - Start the standalone LSP manager
|
||||||
|
if (pathname === '/api/codexlens/lsp/start' && req.method === 'POST') {
|
||||||
|
handlePostRequest(req, res, async (body) => {
|
||||||
|
const { path: workspacePath } = body as { path?: unknown };
|
||||||
|
const targetPath = typeof workspacePath === 'string' && workspacePath.trim().length > 0
|
||||||
|
? workspacePath : initialPath;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const venvStatus = await checkVenvStatus();
|
||||||
|
if (!venvStatus.ready) {
|
||||||
|
return { success: false, error: 'CodexLens not installed', status: 400 };
|
||||||
|
}
|
||||||
|
|
||||||
|
const result = await executeCodexLensPythonAPI('lsp_start', {
|
||||||
|
workspace_root: targetPath,
|
||||||
|
}, 30000);
|
||||||
|
|
||||||
|
if (result.success) {
|
||||||
|
return {
|
||||||
|
success: true,
|
||||||
|
message: 'LSP server started',
|
||||||
|
workspace_root: targetPath,
|
||||||
|
...((result.results && typeof result.results === 'object') ? result.results : {}),
|
||||||
|
};
|
||||||
|
} else {
|
||||||
|
return { success: false, error: result.error || 'Failed to start LSP server', status: 500 };
|
||||||
|
}
|
||||||
|
} catch (err: unknown) {
|
||||||
|
return { success: false, error: err instanceof Error ? err.message : String(err), status: 500 };
|
||||||
|
}
|
||||||
|
});
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// API: LSP Stop - Stop the standalone LSP manager
|
||||||
|
if (pathname === '/api/codexlens/lsp/stop' && req.method === 'POST') {
|
||||||
|
handlePostRequest(req, res, async (body) => {
|
||||||
|
const { path: workspacePath } = body as { path?: unknown };
|
||||||
|
const targetPath = typeof workspacePath === 'string' && workspacePath.trim().length > 0
|
||||||
|
? workspacePath : initialPath;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const venvStatus = await checkVenvStatus();
|
||||||
|
if (!venvStatus.ready) {
|
||||||
|
return { success: false, error: 'CodexLens not installed', status: 400 };
|
||||||
|
}
|
||||||
|
|
||||||
|
const result = await executeCodexLensPythonAPI('lsp_stop', {
|
||||||
|
workspace_root: targetPath,
|
||||||
|
}, 15000);
|
||||||
|
|
||||||
|
if (result.success) {
|
||||||
|
return {
|
||||||
|
success: true,
|
||||||
|
message: 'LSP server stopped',
|
||||||
|
};
|
||||||
|
} else {
|
||||||
|
return { success: false, error: result.error || 'Failed to stop LSP server', status: 500 };
|
||||||
|
}
|
||||||
|
} catch (err: unknown) {
|
||||||
|
return { success: false, error: err instanceof Error ? err.message : String(err), status: 500 };
|
||||||
|
}
|
||||||
|
});
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// API: LSP Restart - Stop then start the standalone LSP manager
|
||||||
|
if (pathname === '/api/codexlens/lsp/restart' && req.method === 'POST') {
|
||||||
|
handlePostRequest(req, res, async (body) => {
|
||||||
|
const { path: workspacePath } = body as { path?: unknown };
|
||||||
|
const targetPath = typeof workspacePath === 'string' && workspacePath.trim().length > 0
|
||||||
|
? workspacePath : initialPath;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const venvStatus = await checkVenvStatus();
|
||||||
|
if (!venvStatus.ready) {
|
||||||
|
return { success: false, error: 'CodexLens not installed', status: 400 };
|
||||||
|
}
|
||||||
|
|
||||||
|
const result = await executeCodexLensPythonAPI('lsp_restart', {
|
||||||
|
workspace_root: targetPath,
|
||||||
|
}, 45000);
|
||||||
|
|
||||||
|
if (result.success) {
|
||||||
|
return {
|
||||||
|
success: true,
|
||||||
|
message: 'LSP server restarted',
|
||||||
|
workspace_root: targetPath,
|
||||||
|
...((result.results && typeof result.results === 'object') ? result.results : {}),
|
||||||
|
};
|
||||||
|
} else {
|
||||||
|
return { success: false, error: result.error || 'Failed to restart LSP server', status: 500 };
|
||||||
|
}
|
||||||
|
} catch (err: unknown) {
|
||||||
|
return { success: false, error: err instanceof Error ? err.message : String(err), status: 500 };
|
||||||
|
}
|
||||||
|
});
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
// API: LSP Semantic Search - Advanced semantic search via Python API
|
// API: LSP Semantic Search - Advanced semantic search via Python API
|
||||||
if (pathname === '/api/codexlens/lsp/search' && req.method === 'POST') {
|
if (pathname === '/api/codexlens/lsp/search' && req.method === 'POST') {
|
||||||
handlePostRequest(req, res, async (body) => {
|
handlePostRequest(req, res, async (body) => {
|
||||||
|
|||||||
@@ -61,6 +61,7 @@ from .hover import get_hover
|
|||||||
from .file_context import file_context
|
from .file_context import file_context
|
||||||
from .references import find_references
|
from .references import find_references
|
||||||
from .semantic import semantic_search
|
from .semantic import semantic_search
|
||||||
|
from .lsp_lifecycle import lsp_start, lsp_stop, lsp_restart
|
||||||
|
|
||||||
__all__ = [
|
__all__ = [
|
||||||
# Dataclasses
|
# Dataclasses
|
||||||
@@ -85,4 +86,8 @@ __all__ = [
|
|||||||
"file_context",
|
"file_context",
|
||||||
"find_references",
|
"find_references",
|
||||||
"semantic_search",
|
"semantic_search",
|
||||||
|
# LSP lifecycle
|
||||||
|
"lsp_start",
|
||||||
|
"lsp_stop",
|
||||||
|
"lsp_restart",
|
||||||
]
|
]
|
||||||
|
|||||||
124
codex-lens/src/codexlens/api/lsp_lifecycle.py
Normal file
124
codex-lens/src/codexlens/api/lsp_lifecycle.py
Normal file
@@ -0,0 +1,124 @@
|
|||||||
|
"""LSP server lifecycle management API.
|
||||||
|
|
||||||
|
Provides synchronous wrappers around StandaloneLspManager's async
|
||||||
|
start/stop methods for use via the executeCodexLensPythonAPI bridge.
|
||||||
|
"""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import asyncio
|
||||||
|
import shutil
|
||||||
|
from typing import Any, Dict
|
||||||
|
|
||||||
|
|
||||||
|
def lsp_start(workspace_root: str) -> Dict[str, Any]:
|
||||||
|
"""Start the standalone LSP manager and report configured servers.
|
||||||
|
|
||||||
|
Loads configuration and checks which language server commands are
|
||||||
|
available on the system. Does NOT start individual language servers
|
||||||
|
(they start on demand when a file of that type is opened).
|
||||||
|
|
||||||
|
Args:
|
||||||
|
workspace_root: Absolute path to the workspace root directory.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Dict with keys: servers (list of server info dicts),
|
||||||
|
workspace_root (str).
|
||||||
|
"""
|
||||||
|
from codexlens.lsp.standalone_manager import StandaloneLspManager
|
||||||
|
|
||||||
|
async def _run() -> Dict[str, Any]:
|
||||||
|
manager = StandaloneLspManager(workspace_root=workspace_root)
|
||||||
|
await manager.start()
|
||||||
|
|
||||||
|
servers = []
|
||||||
|
for language_id, cfg in sorted(manager._configs.items()):
|
||||||
|
cmd0 = cfg.command[0] if cfg.command else None
|
||||||
|
servers.append({
|
||||||
|
"language_id": language_id,
|
||||||
|
"display_name": cfg.display_name,
|
||||||
|
"extensions": list(cfg.extensions),
|
||||||
|
"command": list(cfg.command),
|
||||||
|
"command_available": bool(shutil.which(cmd0)) if cmd0 else False,
|
||||||
|
})
|
||||||
|
|
||||||
|
# Stop the manager - individual servers are started on demand
|
||||||
|
await manager.stop()
|
||||||
|
|
||||||
|
return {
|
||||||
|
"servers": servers,
|
||||||
|
"server_count": len(servers),
|
||||||
|
"workspace_root": workspace_root,
|
||||||
|
}
|
||||||
|
|
||||||
|
return asyncio.run(_run())
|
||||||
|
|
||||||
|
|
||||||
|
def lsp_stop(workspace_root: str) -> Dict[str, Any]:
|
||||||
|
"""Stop all running language servers for the given workspace.
|
||||||
|
|
||||||
|
Creates a temporary manager instance, starts it (loads config),
|
||||||
|
then immediately stops it -- which terminates any running server
|
||||||
|
processes that match this workspace root.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
workspace_root: Absolute path to the workspace root directory.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Dict confirming shutdown.
|
||||||
|
"""
|
||||||
|
from codexlens.lsp.standalone_manager import StandaloneLspManager
|
||||||
|
|
||||||
|
async def _run() -> Dict[str, Any]:
|
||||||
|
manager = StandaloneLspManager(workspace_root=workspace_root)
|
||||||
|
await manager.start()
|
||||||
|
await manager.stop()
|
||||||
|
return {"stopped": True}
|
||||||
|
|
||||||
|
return asyncio.run(_run())
|
||||||
|
|
||||||
|
|
||||||
|
def lsp_restart(workspace_root: str) -> Dict[str, Any]:
|
||||||
|
"""Restart the standalone LSP manager (stop then start).
|
||||||
|
|
||||||
|
Equivalent to calling lsp_stop followed by lsp_start, but avoids
|
||||||
|
the overhead of two separate Python process invocations.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
workspace_root: Absolute path to the workspace root directory.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Dict with keys: servers, server_count, workspace_root.
|
||||||
|
"""
|
||||||
|
from codexlens.lsp.standalone_manager import StandaloneLspManager
|
||||||
|
|
||||||
|
async def _run() -> Dict[str, Any]:
|
||||||
|
# Stop phase
|
||||||
|
stop_manager = StandaloneLspManager(workspace_root=workspace_root)
|
||||||
|
await stop_manager.start()
|
||||||
|
await stop_manager.stop()
|
||||||
|
|
||||||
|
# Start phase
|
||||||
|
start_manager = StandaloneLspManager(workspace_root=workspace_root)
|
||||||
|
await start_manager.start()
|
||||||
|
|
||||||
|
servers = []
|
||||||
|
for language_id, cfg in sorted(start_manager._configs.items()):
|
||||||
|
cmd0 = cfg.command[0] if cfg.command else None
|
||||||
|
servers.append({
|
||||||
|
"language_id": language_id,
|
||||||
|
"display_name": cfg.display_name,
|
||||||
|
"extensions": list(cfg.extensions),
|
||||||
|
"command": list(cfg.command),
|
||||||
|
"command_available": bool(shutil.which(cmd0)) if cmd0 else False,
|
||||||
|
})
|
||||||
|
|
||||||
|
await start_manager.stop()
|
||||||
|
|
||||||
|
return {
|
||||||
|
"servers": servers,
|
||||||
|
"server_count": len(servers),
|
||||||
|
"workspace_root": workspace_root,
|
||||||
|
}
|
||||||
|
|
||||||
|
return asyncio.run(_run())
|
||||||
Reference in New Issue
Block a user