mirror of
https://github.com/catlog22/Claude-Code-Workflow.git
synced 2026-03-20 19:03:51 +08:00
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:
@@ -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 */}
|
||||
|
||||
@@ -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 },
|
||||
],
|
||||
|
||||
@@ -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'],
|
||||
})
|
||||
);
|
||||
});
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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;
|
||||
@@ -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';
|
||||
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
};
|
||||
}
|
||||
@@ -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,
|
||||
};
|
||||
}
|
||||
@@ -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 ==========
|
||||
|
||||
/**
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -172,10 +172,6 @@
|
||||
"name": "ask_question",
|
||||
"desc": "通过 A2UI 界面发起交互式问答"
|
||||
},
|
||||
"smart_search": {
|
||||
"name": "smart_search",
|
||||
"desc": "智能代码搜索,支持模糊和语义搜索模式"
|
||||
},
|
||||
"team_msg": {
|
||||
"name": "team_msg",
|
||||
"desc": "Agent Team 持久化消息总线,用于团队协作通信"
|
||||
|
||||
@@ -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();
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -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;
|
||||
@@ -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';
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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': '路径',
|
||||
|
||||
@@ -40,9 +40,6 @@ export type {
|
||||
NotificationState,
|
||||
NotificationActions,
|
||||
NotificationStore,
|
||||
// Index Manager
|
||||
IndexStatus,
|
||||
IndexRebuildRequest,
|
||||
// Rules
|
||||
Rule,
|
||||
RuleCreateInput,
|
||||
|
||||
Reference in New Issue
Block a user