Remove outdated tests for CodexLens and LiteLLM client, refactor Smart Search MCP usage tests to use new command structure, and clean up unified vector index tests.

This commit is contained in:
catlog22
2026-03-18 11:35:51 +08:00
parent ad9d3f94e0
commit df69f997e4
45 changed files with 64 additions and 11170 deletions

View File

@@ -14,7 +14,6 @@ import { Sparkline } from '@/components/charts/Sparkline';
import { useWorkflowStatusCounts } from '@/hooks/useWorkflowStatusCounts';
import { useDashboardStats } from '@/hooks/useDashboardStats';
import { useProjectOverview } from '@/hooks/useProjectOverview';
import { useIndexStatus } from '@/hooks/useIndex';
import { useSessions } from '@/hooks/useSessions';
import { cn } from '@/lib/utils';
import type { TaskData } from '@/types/store';
@@ -40,7 +39,6 @@ import {
Sparkles,
BarChart3,
PieChart as PieChartIcon,
Database,
} from 'lucide-react';
export interface WorkflowTaskWidgetProps {
@@ -187,8 +185,6 @@ function WorkflowTaskWidgetComponent({ className }: WorkflowTaskWidgetProps) {
const { data, isLoading } = useWorkflowStatusCounts();
const { stats, isLoading: statsLoading } = useDashboardStats({ refetchInterval: 60000 });
const { projectOverview, isLoading: projectLoading } = useProjectOverview();
const { status: indexStatus } = useIndexStatus({ refetchInterval: 30000 });
// Fetch real sessions data
const { activeSessions, isLoading: sessionsLoading } = useSessions({
filter: { location: 'active' },
@@ -328,34 +324,6 @@ function WorkflowTaskWidgetComponent({ className }: WorkflowTaskWidgetProps) {
<span className="text-muted-foreground">{formatMessage({ id: 'projectOverview.devIndex.category.enhancements' })}</span>
</div>
{/* Index Status Indicator */}
<div className="flex items-center gap-2">
<div className="relative">
<Database className={cn(
"h-3.5 w-3.5",
indexStatus?.status === 'building' && "text-blue-600 animate-pulse",
indexStatus?.status === 'completed' && "text-emerald-600",
indexStatus?.status === 'idle' && "text-slate-500",
indexStatus?.status === 'failed' && "text-red-600"
)} />
{indexStatus?.status === 'building' && (
<span className="absolute -top-0.5 -right-0.5 flex h-2 w-2">
<span className="animate-ping absolute inline-flex h-full w-full rounded-full bg-blue-400 opacity-75"></span>
<span className="relative inline-flex rounded-full h-2 w-2 bg-blue-500"></span>
</span>
)}
</div>
<span className={cn(
"font-semibold",
indexStatus?.status === 'building' && "text-blue-600",
indexStatus?.status === 'completed' && "text-emerald-600",
indexStatus?.status === 'idle' && "text-slate-500",
indexStatus?.status === 'failed' && "text-red-600"
)}>
{indexStatus?.totalFiles || 0}
</span>
<span className="text-muted-foreground">{formatMessage({ id: 'home.indexStatus.label' })}</span>
</div>
</div>
{/* Date + Expand Button */}

View File

@@ -114,7 +114,6 @@ const navGroupDefinitions: NavGroupDef[] = [
titleKey: 'navigation.groups.configuration',
icon: Cog,
items: [
{ path: '/settings/codexlens', labelKey: 'navigation.main.codexlens', icon: Sparkles },
{ path: '/api-settings', labelKey: 'navigation.main.apiSettings', icon: Server },
{ path: '/settings', labelKey: 'navigation.main.settings', icon: Settings, end: true },
],

View File

@@ -139,7 +139,7 @@ describe('CcwToolsMcpCard', () => {
render(
<CcwToolsMcpCard
isInstalled={true}
enabledTools={['write_file', 'smart_search']}
enabledTools={['write_file', 'edit_file']}
onToggleTool={vi.fn()}
onUpdateConfig={vi.fn()}
onInstall={vi.fn()}
@@ -170,7 +170,7 @@ describe('CcwToolsMcpCard', () => {
const [payload] = updateClaudeMock.mock.calls[0] ?? [];
expect(payload).toEqual(
expect.objectContaining({
enabledTools: ['write_file', 'smart_search'],
enabledTools: ['write_file', 'edit_file'],
})
);
});

View File

@@ -18,7 +18,6 @@ import {
HardDrive,
MessageCircleQuestion,
MessagesSquare,
SearchCode,
ChevronDown,
ChevronRight,
Globe,
@@ -110,7 +109,6 @@ export const CCW_MCP_TOOLS: CcwTool[] = [
{ name: 'read_many_files', desc: 'Read multiple files/dirs', core: true },
{ name: 'core_memory', desc: 'Core memory management', core: true },
{ name: 'ask_question', desc: 'Interactive questions (A2UI)', core: false },
{ name: 'smart_search', desc: 'Intelligent code search', core: true },
{ name: 'team_msg', desc: 'Agent team message bus', core: false },
];
@@ -572,8 +570,6 @@ function getToolIcon(toolName: string): React.ReactElement {
return <Settings {...iconProps} />;
case 'ask_question':
return <MessageCircleQuestion {...iconProps} />;
case 'smart_search':
return <SearchCode {...iconProps} />;
case 'team_msg':
return <MessagesSquare {...iconProps} />;
default:

View File

@@ -1,227 +0,0 @@
// ========================================
// IndexManager Component
// ========================================
// Component for managing code index with status display and rebuild functionality
import * as React from 'react';
import { useIntl } from 'react-intl';
import { Database, RefreshCw, AlertCircle, CheckCircle2, Clock } from 'lucide-react';
import { Card } from '@/components/ui/Card';
import { Button } from '@/components/ui/Button';
import { StatCard } from '@/components/shared/StatCard';
import { Badge } from '@/components/ui/Badge';
import { useIndex } from '@/hooks/useIndex';
import { cn } from '@/lib/utils';
// ========== Types ==========
export interface IndexManagerProps {
className?: string;
}
// ========== Helper Components ==========
/**
* Progress bar for index rebuild
*/
function IndexProgressBar({ progress, status }: { progress?: number; status: string }) {
const { formatMessage } = useIntl();
if (status !== 'building' || progress === undefined) return null;
return (
<div className="mt-4 space-y-2">
<div className="flex items-center justify-between text-sm">
<span className="text-muted-foreground">
{formatMessage({ id: 'index.status.building' })}
</span>
<span className="font-medium text-foreground">{progress}%</span>
</div>
<div className="h-2 w-full bg-muted rounded-full overflow-hidden">
<div
className="h-full bg-primary transition-all duration-300 ease-out"
style={{ width: `${progress}%` }}
/>
</div>
</div>
);
}
/**
* Status badge component
*/
function IndexStatusBadge({ status }: { status: string }) {
const { formatMessage } = useIntl();
const config: Record<string, { variant: 'default' | 'secondary' | 'destructive' | 'outline'; label: string }> = {
idle: { variant: 'secondary', label: formatMessage({ id: 'index.status.idle' }) },
building: { variant: 'default', label: formatMessage({ id: 'index.status.building' }) },
completed: { variant: 'outline', label: formatMessage({ id: 'index.status.completed' }) },
failed: { variant: 'destructive', label: formatMessage({ id: 'index.status.failed' }) },
};
const { variant, label } = config[status] ?? config.idle;
return (
<Badge variant={variant} className="text-xs">
{label}
</Badge>
);
}
// ========== Main Component ==========
/**
* IndexManager component for displaying index status and managing rebuild operations
*
* @example
* ```tsx
* <IndexManager />
* ```
*/
export function IndexManager({ className }: IndexManagerProps) {
const { formatMessage } = useIntl();
const { status, isLoading, rebuildIndex, isRebuilding, rebuildError, refetch } = useIndex();
// Auto-refresh during rebuild
const refetchInterval = status?.status === 'building' ? 2000 : 0;
React.useEffect(() => {
if (status?.status === 'building') {
const interval = setInterval(() => {
refetch();
}, refetchInterval);
return () => clearInterval(interval);
}
}, [status?.status, refetchInterval, refetch]);
// Handle rebuild button click
const handleRebuild = async () => {
try {
await rebuildIndex({ force: false });
} catch (error) {
console.error('[IndexManager] Rebuild failed:', error);
}
};
// Format build time (ms to human readable)
const formatBuildTime = (ms: number): string => {
if (ms < 1000) return `${ms}ms`;
if (ms < 60000) return `${(ms / 1000).toFixed(1)}s`;
return `${Math.floor(ms / 60000)}m ${Math.floor((ms % 60000) / 1000)}s`;
};
// Format last updated time
const formatLastUpdated = (isoString: string): string => {
const date = new Date(isoString);
const now = new Date();
const diffMs = now.getTime() - date.getTime();
const diffMins = Math.floor(diffMs / 60000);
const diffHours = Math.floor(diffMs / 3600000);
const diffDays = Math.floor(diffMs / 86400000);
if (diffMins < 1) return formatMessage({ id: 'index.time.justNow' });
if (diffMins < 60) return formatMessage({ id: 'index.time.minutesAgo' }, { value: diffMins });
if (diffHours < 24) return formatMessage({ id: 'index.time.hoursAgo' }, { value: diffHours });
return formatMessage({ id: 'index.time.daysAgo' }, { value: diffDays });
};
return (
<Card className={cn('p-6', className)}>
{/* Header */}
<div className="flex items-center justify-between mb-6">
<div className="flex items-center gap-2">
<Database className="w-5 h-5 text-primary" />
<h2 className="text-lg font-semibold text-foreground">
{formatMessage({ id: 'index.title' })}
</h2>
{status && <IndexStatusBadge status={status.status} />}
</div>
<Button
variant="outline"
size="sm"
onClick={handleRebuild}
disabled={isRebuilding || status?.status === 'building'}
className="h-8"
>
<RefreshCw className={cn('w-4 h-4 mr-1', isRebuilding && 'animate-spin')} />
{formatMessage({ id: 'index.actions.rebuild' })}
</Button>
</div>
{/* Description */}
<p className="text-sm text-muted-foreground mb-4">
{formatMessage({ id: 'index.description' })}
</p>
{/* Error message */}
{rebuildError && (
<div className="mb-4 p-3 bg-destructive/10 border border-destructive/30 rounded-lg flex items-start gap-2">
<AlertCircle className="w-4 h-4 text-destructive mt-0.5 flex-shrink-0" />
<div className="flex-1">
<p className="text-sm font-medium text-destructive">
{formatMessage({ id: 'index.errors.rebuildFailed' })}
</p>
<p className="text-xs text-destructive/80 mt-1">{rebuildError.message}</p>
</div>
</div>
)}
{/* Status error */}
{status?.error && (
<div className="mb-4 p-3 bg-destructive/10 border border-destructive/30 rounded-lg flex items-start gap-2">
<AlertCircle className="w-4 h-4 text-destructive mt-0.5 flex-shrink-0" />
<p className="text-sm text-destructive">{status.error}</p>
</div>
)}
{/* Progress Bar */}
{status && <IndexProgressBar progress={status.progress} status={status.status} />}
{/* Current file being indexed */}
{status?.currentFile && status.status === 'building' && (
<div className="mt-3 flex items-center gap-2 text-xs text-muted-foreground">
<RefreshCw className="w-3 h-3 animate-spin" />
<span className="truncate">{status.currentFile}</span>
</div>
)}
{/* Stat Cards */}
<div className="grid grid-cols-1 md:grid-cols-3 gap-4 mt-6">
{/* Total Files */}
<StatCard
title={formatMessage({ id: 'index.stats.totalFiles' })}
value={status?.totalFiles ?? 0}
icon={Database}
variant="primary"
isLoading={isLoading}
description={formatMessage({ id: 'index.stats.totalFilesDesc' })}
/>
{/* Last Updated */}
<StatCard
title={formatMessage({ id: 'index.stats.lastUpdated' })}
value={status?.lastUpdated ? formatLastUpdated(status.lastUpdated) : '-'}
icon={Clock}
variant="info"
isLoading={isLoading}
description={status?.lastUpdated
? new Date(status.lastUpdated).toLocaleString()
: formatMessage({ id: 'index.stats.never' })
}
/>
{/* Build Time */}
<StatCard
title={formatMessage({ id: 'index.stats.buildTime' })}
value={status?.buildTime ? formatBuildTime(status.buildTime) : '-'}
icon={status?.status === 'completed' ? CheckCircle2 : AlertCircle}
variant={status?.status === 'completed' ? 'success' : 'warning'}
isLoading={isLoading}
description={formatMessage({ id: 'index.stats.buildTimeDesc' })}
/>
</div>
</Card>
);
}
export default IndexManager;

View File

@@ -146,9 +146,6 @@ export type { RuleDialogProps } from './RuleDialog';
// Tools and utility components
export { ThemeSelector } from './ThemeSelector';
export { IndexManager } from './IndexManager';
export type { IndexManagerProps } from './IndexManager';
export { ExplorerToolbar } from './ExplorerToolbar';
export type { ExplorerToolbarProps } from './ExplorerToolbar';

View File

@@ -290,16 +290,6 @@ export type {
WorkspaceQueryKeys,
} from './useWorkspaceQueryKeys';
// ========== CodexLens (v2) ==========
export {
useV2SearchManager,
} from './useV2SearchManager';
export type {
V2IndexStatus,
V2SearchTestResult,
UseV2SearchManagerReturn,
} from './useV2SearchManager';
// ========== Skill Hub ==========
export {
useRemoteSkills,

View File

@@ -1,142 +0,0 @@
// ========================================
// useIndex Hook
// ========================================
// TanStack Query hooks for index management with real-time updates
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
import {
fetchIndexStatus,
rebuildIndex,
type IndexStatus,
type IndexRebuildRequest,
} from '../lib/api';
import { useWorkflowStore, selectProjectPath } from '@/stores/workflowStore';
import { workspaceQueryKeys } from '@/lib/queryKeys';
// ========== Stale Time ==========
// Default stale time: 30 seconds (index status updates less frequently)
const STALE_TIME = 30 * 1000;
// ========== Query Hook ==========
export interface UseIndexStatusOptions {
enabled?: boolean;
staleTime?: number;
refetchInterval?: number;
}
export interface UseIndexStatusReturn {
status: IndexStatus | null;
isLoading: boolean;
isFetching: boolean;
error: Error | null;
refetch: () => Promise<void>;
invalidate: () => Promise<void>;
}
/**
* Hook for fetching index status
*
* @example
* ```tsx
* const { status, isLoading, refetch } = useIndexStatus();
* ```
*/
export function useIndexStatus(options: UseIndexStatusOptions = {}): UseIndexStatusReturn {
const { staleTime = STALE_TIME, enabled = true, refetchInterval = 0 } = options;
const queryClient = useQueryClient();
const projectPath = useWorkflowStore(selectProjectPath);
const queryEnabled = enabled && !!projectPath;
const query = useQuery({
queryKey: workspaceQueryKeys.indexStatus(projectPath),
queryFn: () => fetchIndexStatus(projectPath),
staleTime,
enabled: queryEnabled,
refetchInterval: refetchInterval > 0 ? refetchInterval : false,
retry: 2,
});
const refetch = async () => {
await query.refetch();
};
const invalidate = async () => {
await queryClient.invalidateQueries({ queryKey: workspaceQueryKeys.index(projectPath) });
};
return {
status: query.data ?? null,
isLoading: query.isLoading,
isFetching: query.isFetching,
error: query.error,
refetch,
invalidate,
};
}
// ========== Mutation Hooks ==========
export interface UseRebuildIndexReturn {
rebuildIndex: (request?: IndexRebuildRequest) => Promise<IndexStatus>;
isRebuilding: boolean;
error: Error | null;
}
/**
* Hook for rebuilding index
*
* @example
* ```tsx
* const { rebuildIndex, isRebuilding } = useRebuildIndex();
*
* const handleRebuild = async () => {
* await rebuildIndex({ force: true });
* };
* ```
*/
export function useRebuildIndex(): UseRebuildIndexReturn {
const queryClient = useQueryClient();
const projectPath = useWorkflowStore(selectProjectPath);
const mutation = useMutation({
mutationFn: rebuildIndex,
onSuccess: (updatedStatus) => {
// Update the status query cache
queryClient.setQueryData(workspaceQueryKeys.indexStatus(projectPath), updatedStatus);
},
});
return {
rebuildIndex: mutation.mutateAsync,
isRebuilding: mutation.isPending,
error: mutation.error,
};
}
/**
* Combined hook for all index operations
*
* @example
* ```tsx
* const {
* status,
* isLoading,
* rebuildIndex,
* isRebuilding,
* } = useIndex();
* ```
*/
export function useIndex() {
const status = useIndexStatus();
const rebuild = useRebuildIndex();
return {
...status,
rebuildIndex: rebuild.rebuildIndex,
isRebuilding: rebuild.isRebuilding,
rebuildError: rebuild.error,
};
}

View File

@@ -1,159 +0,0 @@
// ========================================
// useV2SearchManager Hook
// ========================================
// React hook for v2 search management via smart_search tool
import { useState, useCallback } from 'react';
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
// ========== Types ==========
export interface V2IndexStatus {
indexed: boolean;
totalFiles: number;
totalChunks: number;
lastIndexedAt: string | null;
dbSizeBytes: number;
vectorDimension: number | null;
ftsEnabled: boolean;
}
export interface V2SearchTestResult {
query: string;
results: Array<{
file: string;
score: number;
snippet: string;
}>;
timingMs: number;
totalResults: number;
}
export interface UseV2SearchManagerReturn {
status: V2IndexStatus | null;
isLoadingStatus: boolean;
statusError: Error | null;
refetchStatus: () => void;
search: (query: string) => Promise<V2SearchTestResult>;
isSearching: boolean;
searchResult: V2SearchTestResult | null;
reindex: () => Promise<void>;
isReindexing: boolean;
}
// ========== API helpers ==========
async function fetchWithJson<T>(url: string, body?: Record<string, unknown>): Promise<T> {
const response = await fetch(url, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
credentials: 'same-origin',
body: JSON.stringify(body),
});
if (!response.ok) {
throw new Error(`Request failed: ${response.status}`);
}
return response.json();
}
async function fetchV2Status(): Promise<V2IndexStatus> {
const data = await fetchWithJson<{ result?: V2IndexStatus; error?: string }>('/api/tools', {
tool_name: 'smart_search',
action: 'status',
});
if (data.error) {
throw new Error(data.error);
}
// Provide defaults for fields that may be missing
return {
indexed: false,
totalFiles: 0,
totalChunks: 0,
lastIndexedAt: null,
dbSizeBytes: 0,
vectorDimension: null,
ftsEnabled: false,
...data.result,
};
}
async function fetchV2Search(query: string): Promise<V2SearchTestResult> {
const data = await fetchWithJson<{ result?: V2SearchTestResult; error?: string }>('/api/tools', {
tool_name: 'smart_search',
action: 'search',
params: { query, limit: 10 },
});
if (data.error) {
throw new Error(data.error);
}
return data.result ?? { query, results: [], timingMs: 0, totalResults: 0 };
}
async function fetchV2Reindex(): Promise<void> {
const data = await fetchWithJson<{ error?: string }>('/api/tools', {
tool_name: 'smart_search',
action: 'reindex',
});
if (data.error) {
throw new Error(data.error);
}
}
// ========== Query Keys ==========
export const v2SearchKeys = {
all: ['v2-search'] as const,
status: () => [...v2SearchKeys.all, 'status'] as const,
};
// ========== Hook ==========
export function useV2SearchManager(): UseV2SearchManagerReturn {
const queryClient = useQueryClient();
const [searchResult, setSearchResult] = useState<V2SearchTestResult | null>(null);
// Status query
const statusQuery = useQuery({
queryKey: v2SearchKeys.status(),
queryFn: fetchV2Status,
staleTime: 30_000,
retry: 1,
});
// Search mutation
const searchMutation = useMutation({
mutationFn: (query: string) => fetchV2Search(query),
onSuccess: (data) => {
setSearchResult(data);
},
});
// Reindex mutation
const reindexMutation = useMutation({
mutationFn: fetchV2Reindex,
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: v2SearchKeys.status() });
},
});
const search = useCallback(async (query: string) => {
const result = await searchMutation.mutateAsync(query);
return result;
}, [searchMutation]);
const reindex = useCallback(async () => {
await reindexMutation.mutateAsync();
}, [reindexMutation]);
return {
status: statusQuery.data ?? null,
isLoadingStatus: statusQuery.isLoading,
statusError: statusQuery.error as Error | null,
refetchStatus: () => statusQuery.refetch(),
search,
isSearching: searchMutation.isPending,
searchResult,
reindex,
isReindexing: reindexMutation.isPending,
};
}

View File

@@ -3,11 +3,11 @@
// ========================================
// Typed fetch functions for API communication with CSRF token handling
import type { SessionMetadata, TaskData, IndexStatus, IndexRebuildRequest, Rule, RuleCreateInput, RulesResponse, Prompt, PromptInsight, Pattern, Suggestion, McpTemplate, McpTemplateInstallRequest, AllProjectsResponse, OtherProjectsServersResponse, CrossCliCopyRequest, CrossCliCopyResponse } from '../types/store';
import type { SessionMetadata, TaskData, Rule, RuleCreateInput, RulesResponse, Prompt, PromptInsight, Pattern, Suggestion, McpTemplate, McpTemplateInstallRequest, AllProjectsResponse, OtherProjectsServersResponse, CrossCliCopyRequest, CrossCliCopyResponse } from '../types/store';
import type { TeamArtifactsResponse } from '../types/team';
// Re-export types for backward compatibility
export type { IndexStatus, IndexRebuildRequest, Rule, RuleCreateInput, RulesResponse, Prompt, PromptInsight, Pattern, Suggestion, McpTemplate, McpTemplateInstallRequest, AllProjectsResponse, OtherProjectsServersResponse, CrossCliCopyRequest, CrossCliCopyResponse };
export type { Rule, RuleCreateInput, RulesResponse, Prompt, PromptInsight, Pattern, Suggestion, McpTemplate, McpTemplateInstallRequest, AllProjectsResponse, OtherProjectsServersResponse, CrossCliCopyRequest, CrossCliCopyResponse };
/**
@@ -4648,10 +4648,10 @@ export async function fetchCcwMcpConfig(currentProjectPath?: string): Promise<Cc
let enabledTools: string[];
if (enabledToolsStr === undefined || enabledToolsStr === null) {
// No setting = use default tools
enabledTools = ['write_file', 'edit_file', 'read_file', 'core_memory', 'ask_question', 'smart_search'];
enabledTools = ['write_file', 'edit_file', 'read_file', 'core_memory', 'ask_question'];
} else if (enabledToolsStr === '' || enabledToolsStr === 'all') {
// Empty string = all tools disabled, 'all' = default set (for backward compatibility)
enabledTools = enabledToolsStr === '' ? [] : ['write_file', 'edit_file', 'read_file', 'core_memory', 'ask_question', 'smart_search'];
enabledTools = enabledToolsStr === '' ? [] : ['write_file', 'edit_file', 'read_file', 'core_memory', 'ask_question'];
} else {
// Comma-separated list
enabledTools = enabledToolsStr.split(',').map((t: string) => t.trim()).filter(Boolean);
@@ -4710,7 +4710,7 @@ export async function installCcwMcp(
scope,
projectPath: path,
env: {
enabledTools: ['write_file', 'edit_file', 'read_file', 'core_memory', 'ask_question', 'smart_search'],
enabledTools: ['write_file', 'edit_file', 'read_file', 'core_memory', 'ask_question'],
},
}),
});
@@ -4793,10 +4793,10 @@ export async function fetchCcwMcpConfigForCodex(): Promise<CcwMcpConfig> {
let enabledTools: string[];
if (enabledToolsStr === undefined || enabledToolsStr === null) {
// No setting = use default tools
enabledTools = ['write_file', 'edit_file', 'read_file', 'core_memory', 'ask_question', 'smart_search'];
enabledTools = ['write_file', 'edit_file', 'read_file', 'core_memory', 'ask_question'];
} else if (enabledToolsStr === '' || enabledToolsStr === 'all') {
// Empty string = all tools disabled, 'all' = default set (for backward compatibility)
enabledTools = enabledToolsStr === '' ? [] : ['write_file', 'edit_file', 'read_file', 'core_memory', 'ask_question', 'smart_search'];
enabledTools = enabledToolsStr === '' ? [] : ['write_file', 'edit_file', 'read_file', 'core_memory', 'ask_question'];
} else {
// Comma-separated list
enabledTools = enabledToolsStr.split(',').map((t: string) => t.trim()).filter(Boolean);
@@ -4831,7 +4831,7 @@ function buildCcwMcpServerConfigForCodex(config: {
if (config.enabledTools !== undefined) {
env.CCW_ENABLED_TOOLS = config.enabledTools.join(',');
} else {
env.CCW_ENABLED_TOOLS = 'write_file,edit_file,read_file,core_memory,ask_question,smart_search';
env.CCW_ENABLED_TOOLS = 'write_file,edit_file,read_file,core_memory,ask_question';
}
if (config.projectRoot) {
@@ -4852,7 +4852,7 @@ function buildCcwMcpServerConfigForCodex(config: {
*/
export async function installCcwMcpToCodex(): Promise<CcwMcpConfig> {
const serverConfig = buildCcwMcpServerConfigForCodex({
enabledTools: ['write_file', 'edit_file', 'read_file', 'core_memory', 'ask_question', 'smart_search'],
enabledTools: ['write_file', 'edit_file', 'read_file', 'core_memory', 'ask_question'],
});
const result = await addCodexMcpServer('ccw-tools', serverConfig);
@@ -4892,42 +4892,6 @@ export async function updateCcwConfigForCodex(config: {
return fetchCcwMcpConfigForCodex();
}
// ========== Index Management API ==========
/**
* Fetch current index status for a specific workspace
* @param projectPath - Optional project path to filter data by workspace
*/
export async function fetchIndexStatus(_projectPath?: string): Promise<IndexStatus> {
const resp = await fetchApi<{ result?: { indexed?: boolean; totalFiles?: number } }>('/api/tools', {
method: 'POST',
body: JSON.stringify({ tool_name: 'smart_search', action: 'status' }),
});
const result = resp.result ?? {};
return {
totalFiles: result.totalFiles ?? 0,
lastUpdated: new Date().toISOString(),
buildTime: 0,
status: result.indexed ? 'completed' : 'idle',
};
}
/**
* Rebuild index
*/
export async function rebuildIndex(_request: IndexRebuildRequest = {}): Promise<IndexStatus> {
await fetchApi<{ error?: string }>('/api/tools', {
method: 'POST',
body: JSON.stringify({ tool_name: 'smart_search', action: 'reindex' }),
});
return {
totalFiles: 0,
lastUpdated: new Date().toISOString(),
buildTime: 0,
status: 'building',
};
}
// ========== Prompt History API ==========
/**

View File

@@ -183,10 +183,6 @@
"name": "ask_question",
"desc": "Ask interactive questions through A2UI interface"
},
"smart_search": {
"name": "smart_search",
"desc": "Intelligent code search with fuzzy and semantic modes"
},
"team_msg": {
"name": "team_msg",
"desc": "Persistent JSONL message bus for Agent Team communication"

View File

@@ -172,10 +172,6 @@
"name": "ask_question",
"desc": "通过 A2UI 界面发起交互式问答"
},
"smart_search": {
"name": "smart_search",
"desc": "智能代码搜索,支持模糊和语义搜索模式"
},
"team_msg": {
"name": "team_msg",
"desc": "Agent Team 持久化消息总线,用于团队协作通信"

View File

@@ -1,196 +0,0 @@
// ========================================
// CodexLens Manager Page Tests (v2)
// ========================================
// Tests for v2 search management page
import { describe, it, expect, beforeEach, vi } from 'vitest';
import { render, screen } from '@/test/i18n';
import userEvent from '@testing-library/user-event';
import { CodexLensManagerPage } from './CodexLensManagerPage';
// Mock the v2 search manager hook
vi.mock('@/hooks/useV2SearchManager', () => ({
useV2SearchManager: vi.fn(),
}));
import { useV2SearchManager } from '@/hooks/useV2SearchManager';
const mockStatus = {
indexed: true,
totalFiles: 150,
totalChunks: 1200,
lastIndexedAt: '2026-03-17T10:00:00Z',
dbSizeBytes: 5242880,
vectorDimension: 384,
ftsEnabled: true,
};
const defaultHookReturn = {
status: mockStatus,
isLoadingStatus: false,
statusError: null,
refetchStatus: vi.fn(),
search: vi.fn().mockResolvedValue({
query: 'test',
results: [],
timingMs: 12.5,
totalResults: 0,
}),
isSearching: false,
searchResult: null,
reindex: vi.fn().mockResolvedValue(undefined),
isReindexing: false,
};
describe('CodexLensManagerPage (v2)', () => {
beforeEach(() => {
vi.clearAllMocks();
(vi.mocked(useV2SearchManager) as any).mockReturnValue(defaultHookReturn);
});
it('should render page title', () => {
render(<CodexLensManagerPage />);
// The title comes from i18n codexlens.title
expect(screen.getByRole('heading', { level: 1 })).toBeInTheDocument();
});
it('should render index status section', () => {
render(<CodexLensManagerPage />);
// Check for file count display
expect(screen.getByText('150')).toBeInTheDocument();
});
it('should render search input', () => {
render(<CodexLensManagerPage />);
const input = screen.getByPlaceholderText(/search query/i);
expect(input).toBeInTheDocument();
});
it('should call refetchStatus on refresh click', async () => {
const refetchStatus = vi.fn();
(vi.mocked(useV2SearchManager) as any).mockReturnValue({
...defaultHookReturn,
refetchStatus,
});
const user = userEvent.setup();
render(<CodexLensManagerPage />);
const refreshButton = screen.getByText(/Refresh/i);
await user.click(refreshButton);
expect(refetchStatus).toHaveBeenCalledOnce();
});
it('should call search when clicking search button', async () => {
const searchFn = vi.fn().mockResolvedValue({
query: 'test query',
results: [],
timingMs: 5,
totalResults: 0,
});
(vi.mocked(useV2SearchManager) as any).mockReturnValue({
...defaultHookReturn,
search: searchFn,
});
const user = userEvent.setup();
render(<CodexLensManagerPage />);
const input = screen.getByPlaceholderText(/search query/i);
await user.type(input, 'test query');
const searchButton = screen.getByText(/Search/i);
await user.click(searchButton);
expect(searchFn).toHaveBeenCalledWith('test query');
});
it('should display search results', () => {
(vi.mocked(useV2SearchManager) as any).mockReturnValue({
...defaultHookReturn,
searchResult: {
query: 'auth',
results: [
{ file: 'src/auth.ts', score: 0.95, snippet: 'export function authenticate()' },
],
timingMs: 8.2,
totalResults: 1,
},
});
render(<CodexLensManagerPage />);
expect(screen.getByText('src/auth.ts')).toBeInTheDocument();
expect(screen.getByText('95.0%')).toBeInTheDocument();
expect(screen.getByText('export function authenticate()')).toBeInTheDocument();
});
it('should call reindex on button click', async () => {
const reindexFn = vi.fn().mockResolvedValue(undefined);
(vi.mocked(useV2SearchManager) as any).mockReturnValue({
...defaultHookReturn,
reindex: reindexFn,
});
const user = userEvent.setup();
render(<CodexLensManagerPage />);
const reindexButton = screen.getByText(/Reindex/i);
await user.click(reindexButton);
expect(reindexFn).toHaveBeenCalledOnce();
});
it('should show loading skeleton when status is loading', () => {
(vi.mocked(useV2SearchManager) as any).mockReturnValue({
...defaultHookReturn,
status: null,
isLoadingStatus: true,
});
render(<CodexLensManagerPage />);
// Should have pulse animation elements
const pulseElements = document.querySelectorAll('.animate-pulse');
expect(pulseElements.length).toBeGreaterThan(0);
});
it('should show error alert when status fetch fails', () => {
(vi.mocked(useV2SearchManager) as any).mockReturnValue({
...defaultHookReturn,
status: null,
statusError: new Error('Network error'),
});
render(<CodexLensManagerPage />);
// Error message should be visible
expect(screen.getByText(/Failed to load/i)).toBeInTheDocument();
});
it('should show not indexed state', () => {
(vi.mocked(useV2SearchManager) as any).mockReturnValue({
...defaultHookReturn,
status: {
...mockStatus,
indexed: false,
totalFiles: 0,
totalChunks: 0,
},
});
render(<CodexLensManagerPage />);
expect(screen.getByText(/Not Indexed/i)).toBeInTheDocument();
});
describe('i18n - Chinese locale', () => {
it('should display translated text in Chinese', () => {
render(<CodexLensManagerPage />, { locale: 'zh' });
// Page title from zh codexlens.json
expect(screen.getByRole('heading', { level: 1 })).toBeInTheDocument();
});
});
});

View File

@@ -1,277 +0,0 @@
// ========================================
// CodexLens Manager Page (v2)
// ========================================
// V2 search management interface with index status, search test, and configuration
import { useState } from 'react';
import { useIntl } from 'react-intl';
import {
Search,
RefreshCw,
Database,
Zap,
AlertCircle,
CheckCircle2,
Clock,
FileText,
HardDrive,
} from 'lucide-react';
import { Card } from '@/components/ui/Card';
import { Button } from '@/components/ui/Button';
import { useV2SearchManager } from '@/hooks';
import { cn } from '@/lib/utils';
function formatBytes(bytes: number): string {
if (bytes === 0) return '0 B';
const units = ['B', 'KB', 'MB', 'GB'];
const i = Math.floor(Math.log(bytes) / Math.log(1024));
return `${(bytes / Math.pow(1024, i)).toFixed(1)} ${units[i]}`;
}
function formatDate(dateStr: string | null): string {
if (!dateStr) return '-';
try {
return new Date(dateStr).toLocaleString();
} catch {
return dateStr;
}
}
export function CodexLensManagerPage() {
const { formatMessage } = useIntl();
const [searchQuery, setSearchQuery] = useState('');
const {
status,
isLoadingStatus,
statusError,
refetchStatus,
search,
isSearching,
searchResult,
reindex,
isReindexing,
} = useV2SearchManager();
const handleSearch = async () => {
if (!searchQuery.trim()) return;
await search(searchQuery.trim());
};
const handleKeyDown = (e: React.KeyboardEvent) => {
if (e.key === 'Enter') {
handleSearch();
}
};
return (
<div className="space-y-6">
{/* Page Header */}
<div className="flex flex-col sm:flex-row sm:items-center sm:justify-between gap-4">
<div>
<h1 className="text-2xl font-bold text-foreground flex items-center gap-2">
<Search className="w-6 h-6 text-primary" />
{formatMessage({ id: 'codexlens.title' })}
</h1>
<p className="text-muted-foreground mt-1">
{formatMessage({ id: 'codexlens.description' })}
</p>
</div>
<div className="flex gap-2">
<Button
variant="outline"
onClick={refetchStatus}
disabled={isLoadingStatus}
>
<RefreshCw className={cn('w-4 h-4 mr-2', isLoadingStatus && 'animate-spin')} />
{formatMessage({ id: 'common.actions.refresh' })}
</Button>
<Button
onClick={() => reindex()}
disabled={isReindexing}
>
<Zap className={cn('w-4 h-4 mr-2', isReindexing && 'animate-spin')} />
{isReindexing
? formatMessage({ id: 'codexlens.reindexing' })
: formatMessage({ id: 'codexlens.reindex' })
}
</Button>
</div>
</div>
{/* Error Alert */}
{statusError && (
<Card className="p-4 bg-destructive/10 border-destructive/20">
<div className="flex items-center gap-2">
<AlertCircle className="w-4 h-4 text-destructive" />
<p className="text-sm text-destructive">
{formatMessage({ id: 'codexlens.statusError' })}
</p>
</div>
</Card>
)}
{/* Index Status Section */}
<Card className="p-6">
<h2 className="text-lg font-semibold mb-4 flex items-center gap-2">
<Database className="w-5 h-5 text-primary" />
{formatMessage({ id: 'codexlens.indexStatus.title' })}
</h2>
{isLoadingStatus ? (
<div className="grid grid-cols-2 md:grid-cols-4 gap-4">
{[1, 2, 3, 4].map((i) => (
<div key={i} className="h-16 bg-muted/50 rounded-lg animate-pulse" />
))}
</div>
) : status ? (
<div className="grid grid-cols-2 md:grid-cols-4 gap-4">
<div className="flex items-start gap-3 p-3 rounded-lg bg-muted/30">
{status.indexed ? (
<CheckCircle2 className="w-5 h-5 text-green-500 mt-0.5" />
) : (
<AlertCircle className="w-5 h-5 text-yellow-500 mt-0.5" />
)}
<div>
<p className="text-xs text-muted-foreground">
{formatMessage({ id: 'codexlens.indexStatus.status' })}
</p>
<p className="text-sm font-medium">
{status.indexed
? formatMessage({ id: 'codexlens.indexStatus.ready' })
: formatMessage({ id: 'codexlens.indexStatus.notIndexed' })
}
</p>
</div>
</div>
<div className="flex items-start gap-3 p-3 rounded-lg bg-muted/30">
<FileText className="w-5 h-5 text-blue-500 mt-0.5" />
<div>
<p className="text-xs text-muted-foreground">
{formatMessage({ id: 'codexlens.indexStatus.files' })}
</p>
<p className="text-sm font-medium">{status.totalFiles.toLocaleString()}</p>
</div>
</div>
<div className="flex items-start gap-3 p-3 rounded-lg bg-muted/30">
<HardDrive className="w-5 h-5 text-purple-500 mt-0.5" />
<div>
<p className="text-xs text-muted-foreground">
{formatMessage({ id: 'codexlens.indexStatus.dbSize' })}
</p>
<p className="text-sm font-medium">{formatBytes(status.dbSizeBytes)}</p>
</div>
</div>
<div className="flex items-start gap-3 p-3 rounded-lg bg-muted/30">
<Clock className="w-5 h-5 text-orange-500 mt-0.5" />
<div>
<p className="text-xs text-muted-foreground">
{formatMessage({ id: 'codexlens.indexStatus.lastIndexed' })}
</p>
<p className="text-sm font-medium">{formatDate(status.lastIndexedAt)}</p>
</div>
</div>
</div>
) : (
<p className="text-sm text-muted-foreground">
{formatMessage({ id: 'codexlens.indexStatus.unavailable' })}
</p>
)}
{status && (
<div className="mt-4 flex gap-4 text-xs text-muted-foreground">
<span>
{formatMessage({ id: 'codexlens.indexStatus.chunks' })}: {status.totalChunks.toLocaleString()}
</span>
{status.vectorDimension && (
<span>
{formatMessage({ id: 'codexlens.indexStatus.vectorDim' })}: {status.vectorDimension}
</span>
)}
<span>
FTS: {status.ftsEnabled
? formatMessage({ id: 'codexlens.indexStatus.enabled' })
: formatMessage({ id: 'codexlens.indexStatus.disabled' })
}
</span>
</div>
)}
</Card>
{/* Search Test Section */}
<Card className="p-6">
<h2 className="text-lg font-semibold mb-4 flex items-center gap-2">
<Search className="w-5 h-5 text-primary" />
{formatMessage({ id: 'codexlens.searchTest.title' })}
</h2>
<div className="flex gap-2 mb-4">
<input
type="text"
value={searchQuery}
onChange={(e) => setSearchQuery(e.target.value)}
onKeyDown={handleKeyDown}
placeholder={formatMessage({ id: 'codexlens.searchTest.placeholder' })}
className="flex-1 px-3 py-2 border border-input rounded-md bg-background text-sm focus:outline-none focus:ring-2 focus:ring-ring"
/>
<Button
onClick={handleSearch}
disabled={isSearching || !searchQuery.trim()}
>
{isSearching ? (
<RefreshCw className="w-4 h-4 mr-2 animate-spin" />
) : (
<Search className="w-4 h-4 mr-2" />
)}
{formatMessage({ id: 'codexlens.searchTest.button' })}
</Button>
</div>
{searchResult && (
<div>
<div className="flex items-center justify-between mb-2">
<p className="text-sm text-muted-foreground">
{searchResult.totalResults} {formatMessage({ id: 'codexlens.searchTest.results' })}
</p>
<p className="text-xs text-muted-foreground">
{searchResult.timingMs.toFixed(1)}ms
</p>
</div>
{searchResult.results.length > 0 ? (
<div className="space-y-2 max-h-96 overflow-y-auto">
{searchResult.results.map((result, idx) => (
<div
key={idx}
className="p-3 rounded-lg border border-border bg-muted/20 hover:bg-muted/40 transition-colors"
>
<div className="flex items-center justify-between mb-1">
<span className="text-sm font-mono text-primary truncate">
{result.file}
</span>
<span className="text-xs text-muted-foreground ml-2 shrink-0">
{(result.score * 100).toFixed(1)}%
</span>
</div>
<pre className="text-xs text-muted-foreground whitespace-pre-wrap line-clamp-3">
{result.snippet}
</pre>
</div>
))}
</div>
) : (
<p className="text-sm text-muted-foreground text-center py-4">
{formatMessage({ id: 'codexlens.searchTest.noResults' })}
</p>
)}
</div>
)}
</Card>
</div>
);
}
export default CodexLensManagerPage;

View File

@@ -29,7 +29,6 @@ export { RulesManagerPage } from './RulesManagerPage';
export { PromptHistoryPage } from './PromptHistoryPage';
export { ExplorerPage } from './ExplorerPage';
export { GraphExplorerPage } from './GraphExplorerPage';
export { CodexLensManagerPage } from './CodexLensManagerPage';
export { ApiSettingsPage } from './ApiSettingsPage';
export { CliViewerPage } from './CliViewerPage';
export { CliSessionSharePage } from './CliSessionSharePage';

View File

@@ -35,7 +35,6 @@ const RulesManagerPage = lazy(() => import('@/pages/RulesManagerPage').then(m =>
const PromptHistoryPage = lazy(() => import('@/pages/PromptHistoryPage').then(m => ({ default: m.PromptHistoryPage })));
const ExplorerPage = lazy(() => import('@/pages/ExplorerPage').then(m => ({ default: m.ExplorerPage })));
const GraphExplorerPage = lazy(() => import('@/pages/GraphExplorerPage').then(m => ({ default: m.GraphExplorerPage })));
const CodexLensManagerPage = lazy(() => import('@/pages/CodexLensManagerPage').then(m => ({ default: m.CodexLensManagerPage })));
const ApiSettingsPage = lazy(() => import('@/pages/ApiSettingsPage').then(m => ({ default: m.ApiSettingsPage })));
const CliViewerPage = lazy(() => import('@/pages/CliViewerPage').then(m => ({ default: m.CliViewerPage })));
const CliSessionSharePage = lazy(() => import('@/pages/CliSessionSharePage').then(m => ({ default: m.CliSessionSharePage })));
@@ -170,10 +169,6 @@ const routes: RouteObject[] = [
path: 'settings/specs',
element: withErrorHandling(<SpecsSettingsPage />),
},
{
path: 'settings/codexlens',
element: withErrorHandling(<CodexLensManagerPage />),
},
{
path: 'api-settings',
element: withErrorHandling(<ApiSettingsPage />),
@@ -260,7 +255,6 @@ export const ROUTES = {
ENDPOINTS: '/settings/endpoints',
INSTALLATIONS: '/settings/installations',
SETTINGS_RULES: '/settings/rules',
CODEXLENS_MANAGER: '/settings/codexlens',
API_SETTINGS: '/api-settings',
EXPLORER: '/explorer',
GRAPH: '/graph',

View File

@@ -172,8 +172,6 @@ const mockMessages: Record<Locale, Record<string, string>> = {
'mcp.ccw.tools.core_memory.desc': 'Core memory management',
'mcp.ccw.tools.ask_question.name': 'Ask Question',
'mcp.ccw.tools.ask_question.desc': 'Interactive questions (A2UI)',
'mcp.ccw.tools.smart_search.name': 'Smart Search',
'mcp.ccw.tools.smart_search.desc': 'Intelligent code search',
'mcp.ccw.tools.team_msg.name': 'Team Message',
'mcp.ccw.tools.team_msg.desc': 'Agent team message bus',
'mcp.ccw.paths.label': 'Paths',
@@ -348,8 +346,6 @@ const mockMessages: Record<Locale, Record<string, string>> = {
'mcp.ccw.tools.core_memory.desc': '核心记忆管理',
'mcp.ccw.tools.ask_question.name': '提问',
'mcp.ccw.tools.ask_question.desc': '交互式问题A2UI',
'mcp.ccw.tools.smart_search.name': '智能搜索',
'mcp.ccw.tools.smart_search.desc': '智能代码搜索',
'mcp.ccw.tools.team_msg.name': '团队消息',
'mcp.ccw.tools.team_msg.desc': '代理团队消息总线',
'mcp.ccw.paths.label': '路径',

View File

@@ -40,9 +40,6 @@ export type {
NotificationState,
NotificationActions,
NotificationStore,
// Index Manager
IndexStatus,
IndexRebuildRequest,
// Rules
Rule,
RuleCreateInput,