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:
catlog22
2026-02-13 17:14:14 +08:00
parent ad5b35a1a5
commit 31f37751fc
15 changed files with 1363 additions and 14 deletions

View File

@@ -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">

View 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;

View 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;

View File

@@ -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} />

View File

@@ -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} />);

View File

@@ -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">

View File

@@ -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';

View File

@@ -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,
};
}

View File

@@ -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 }> }> {

View File

@@ -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"
} }
} }

View File

@@ -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 服务器已重启"
} }
} }

View File

@@ -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',
}, },
}; };

View File

@@ -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) => {

View File

@@ -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",
] ]

View 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())