mirror of
https://github.com/catlog22/Claude-Code-Workflow.git
synced 2026-02-11 02:33:51 +08:00
feat: add useApiSettings hook for managing API settings, including providers, endpoints, cache, and model pools
- Implemented hooks for CRUD operations on providers and endpoints. - Added cache management hooks for cache stats and settings. - Introduced model pool management hooks for high availability and load balancing. - Created localization files for English and Chinese translations of API settings.
This commit is contained in:
11
ccw/frontend/package-lock.json
generated
11
ccw/frontend/package-lock.json
generated
@@ -35,6 +35,7 @@
|
|||||||
"react-router-dom": "^6.28.0",
|
"react-router-dom": "^6.28.0",
|
||||||
"rehype-highlight": "^7.0.2",
|
"rehype-highlight": "^7.0.2",
|
||||||
"remark-gfm": "^4.0.1",
|
"remark-gfm": "^4.0.1",
|
||||||
|
"sonner": "^2.0.7",
|
||||||
"tailwind-merge": "^2.5.0",
|
"tailwind-merge": "^2.5.0",
|
||||||
"zod": "^3.23.8",
|
"zod": "^3.23.8",
|
||||||
"zustand": "^5.0.0"
|
"zustand": "^5.0.0"
|
||||||
@@ -8089,6 +8090,16 @@
|
|||||||
"node": ">=18"
|
"node": ">=18"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/sonner": {
|
||||||
|
"version": "2.0.7",
|
||||||
|
"resolved": "https://registry.npmjs.org/sonner/-/sonner-2.0.7.tgz",
|
||||||
|
"integrity": "sha512-W6ZN4p58k8aDKA4XPcx2hpIQXBRAgyiWVkYhT7CvK6D3iAu7xjvVyhQHg2/iaKJZ1XVJ4r7XuwGL+WGEK37i9w==",
|
||||||
|
"license": "MIT",
|
||||||
|
"peerDependencies": {
|
||||||
|
"react": "^18.0.0 || ^19.0.0 || ^19.0.0-rc",
|
||||||
|
"react-dom": "^18.0.0 || ^19.0.0 || ^19.0.0-rc"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/source-map-js": {
|
"node_modules/source-map-js": {
|
||||||
"version": "1.2.1",
|
"version": "1.2.1",
|
||||||
"resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz",
|
"resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz",
|
||||||
|
|||||||
@@ -44,6 +44,7 @@
|
|||||||
"react-router-dom": "^6.28.0",
|
"react-router-dom": "^6.28.0",
|
||||||
"rehype-highlight": "^7.0.2",
|
"rehype-highlight": "^7.0.2",
|
||||||
"remark-gfm": "^4.0.1",
|
"remark-gfm": "^4.0.1",
|
||||||
|
"sonner": "^2.0.7",
|
||||||
"tailwind-merge": "^2.5.0",
|
"tailwind-merge": "^2.5.0",
|
||||||
"zod": "^3.23.8",
|
"zod": "^3.23.8",
|
||||||
"zustand": "^5.0.0"
|
"zustand": "^5.0.0"
|
||||||
|
|||||||
@@ -5,7 +5,7 @@
|
|||||||
|
|
||||||
import { useState, useEffect } from 'react';
|
import { useState, useEffect } from 'react';
|
||||||
import { useIntl } from 'react-intl';
|
import { useIntl } from 'react-intl';
|
||||||
import { Save, RefreshCw, AlertTriangle, FileCode } from 'lucide-react';
|
import { Save, RefreshCw, AlertTriangle, FileCode, AlertCircle } from 'lucide-react';
|
||||||
import { Card } from '@/components/ui/Card';
|
import { Card } from '@/components/ui/Card';
|
||||||
import { Textarea } from '@/components/ui/Textarea';
|
import { Textarea } from '@/components/ui/Textarea';
|
||||||
import { Button } from '@/components/ui/Button';
|
import { Button } from '@/components/ui/Button';
|
||||||
@@ -32,6 +32,7 @@ export function AdvancedTab({ enabled = true }: AdvancedTabProps) {
|
|||||||
env,
|
env,
|
||||||
settings,
|
settings,
|
||||||
isLoading: isLoadingEnv,
|
isLoading: isLoadingEnv,
|
||||||
|
error: envError,
|
||||||
refetch,
|
refetch,
|
||||||
} = useCodexLensEnv({ enabled });
|
} = useCodexLensEnv({ enabled });
|
||||||
|
|
||||||
@@ -43,23 +44,25 @@ export function AdvancedTab({ enabled = true }: AdvancedTabProps) {
|
|||||||
const [hasChanges, setHasChanges] = useState(false);
|
const [hasChanges, setHasChanges] = useState(false);
|
||||||
const [showWarning, setShowWarning] = useState(false);
|
const [showWarning, setShowWarning] = useState(false);
|
||||||
|
|
||||||
// Initialize form from env
|
// Initialize form from env - handles both undefined (loading) and empty string (empty file)
|
||||||
|
// The hook returns raw directly, so we check if it's been set (not undefined means data loaded)
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (raw !== undefined) {
|
// Initialize when data is loaded (raw may be empty string but not undefined during loading)
|
||||||
setEnvInput(raw);
|
// Note: During initial load, raw is undefined. After load completes, raw is set (even if empty string)
|
||||||
|
if (!isLoadingEnv) {
|
||||||
|
setEnvInput(raw ?? ''); // Use empty string if raw is undefined/null
|
||||||
setErrors({});
|
setErrors({});
|
||||||
setHasChanges(false);
|
setHasChanges(false);
|
||||||
setShowWarning(false);
|
setShowWarning(false);
|
||||||
}
|
}
|
||||||
}, [raw]);
|
}, [raw, isLoadingEnv]);
|
||||||
|
|
||||||
const handleEnvChange = (value: string) => {
|
const handleEnvChange = (value: string) => {
|
||||||
setEnvInput(value);
|
setEnvInput(value);
|
||||||
// Check if there are changes
|
// Check if there are changes - compare with raw value (handle undefined as empty)
|
||||||
if (raw !== undefined) {
|
const currentRaw = raw ?? '';
|
||||||
setHasChanges(value !== raw);
|
setHasChanges(value !== currentRaw);
|
||||||
setShowWarning(value !== raw);
|
setShowWarning(value !== currentRaw);
|
||||||
}
|
|
||||||
if (errors.env) {
|
if (errors.env) {
|
||||||
setErrors((prev) => ({ ...prev, env: undefined }));
|
setErrors((prev) => ({ ...prev, env: undefined }));
|
||||||
}
|
}
|
||||||
@@ -132,12 +135,11 @@ export function AdvancedTab({ enabled = true }: AdvancedTabProps) {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const handleReset = () => {
|
const handleReset = () => {
|
||||||
if (raw !== undefined) {
|
// Reset to current raw value (handle undefined as empty)
|
||||||
setEnvInput(raw);
|
setEnvInput(raw ?? '');
|
||||||
setErrors({});
|
setErrors({});
|
||||||
setHasChanges(false);
|
setHasChanges(false);
|
||||||
setShowWarning(false);
|
setShowWarning(false);
|
||||||
}
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const isLoading = isLoadingEnv;
|
const isLoading = isLoadingEnv;
|
||||||
@@ -154,6 +156,32 @@ export function AdvancedTab({ enabled = true }: AdvancedTabProps) {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="space-y-6">
|
<div className="space-y-6">
|
||||||
|
{/* Error Card */}
|
||||||
|
{envError && (
|
||||||
|
<Card className="p-4 bg-destructive/10 border-destructive/20">
|
||||||
|
<div className="flex items-start gap-3">
|
||||||
|
<AlertCircle className="w-5 h-5 text-destructive flex-shrink-0 mt-0.5" />
|
||||||
|
<div className="flex-1">
|
||||||
|
<h4 className="text-sm font-medium text-destructive-foreground">
|
||||||
|
{formatMessage({ id: 'codexlens.advanced.loadError' })}
|
||||||
|
</h4>
|
||||||
|
<p className="text-xs text-destructive-foreground/80 mt-1">
|
||||||
|
{envError.message || formatMessage({ id: 'codexlens.advanced.loadErrorDesc' })}
|
||||||
|
</p>
|
||||||
|
<Button
|
||||||
|
variant="outline"
|
||||||
|
size="sm"
|
||||||
|
onClick={() => refetch()}
|
||||||
|
className="mt-2"
|
||||||
|
>
|
||||||
|
<RefreshCw className="w-3 h-3 mr-1" />
|
||||||
|
{formatMessage({ id: 'common.actions.retry' })}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Card>
|
||||||
|
)}
|
||||||
|
|
||||||
{/* Sensitivity Warning Card */}
|
{/* Sensitivity Warning Card */}
|
||||||
{showWarning && (
|
{showWarning && (
|
||||||
<Card className="p-4 bg-warning/10 border-warning/20">
|
<Card className="p-4 bg-warning/10 border-warning/20">
|
||||||
|
|||||||
286
ccw/frontend/src/components/codexlens/IndexOperations.tsx
Normal file
286
ccw/frontend/src/components/codexlens/IndexOperations.tsx
Normal file
@@ -0,0 +1,286 @@
|
|||||||
|
// ========================================
|
||||||
|
// CodexLens Index Operations Component
|
||||||
|
// ========================================
|
||||||
|
// Index management operations with progress tracking
|
||||||
|
|
||||||
|
import { useIntl } from 'react-intl';
|
||||||
|
import { useEffect, useState } from 'react';
|
||||||
|
import {
|
||||||
|
RotateCw,
|
||||||
|
Zap,
|
||||||
|
AlertCircle,
|
||||||
|
CheckCircle2,
|
||||||
|
X,
|
||||||
|
} from 'lucide-react';
|
||||||
|
import { Button } from '@/components/ui/Button';
|
||||||
|
import { Progress } from '@/components/ui/Progress';
|
||||||
|
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/Card';
|
||||||
|
import { cn } from '@/lib/utils';
|
||||||
|
import { useWorkflowStore, selectProjectPath } from '@/stores/workflowStore';
|
||||||
|
import {
|
||||||
|
useCodexLensIndexingStatus,
|
||||||
|
useRebuildIndex,
|
||||||
|
useUpdateIndex,
|
||||||
|
useCancelIndexing,
|
||||||
|
} from '@/hooks';
|
||||||
|
import { useNotifications } from '@/hooks/useNotifications';
|
||||||
|
import { useWebSocket } from '@/hooks/useWebSocket';
|
||||||
|
|
||||||
|
interface IndexOperationsProps {
|
||||||
|
disabled?: boolean;
|
||||||
|
onRefresh?: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface IndexProgress {
|
||||||
|
stage: string;
|
||||||
|
message: string;
|
||||||
|
percent: number;
|
||||||
|
path?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
type IndexOperation = {
|
||||||
|
id: string;
|
||||||
|
type: 'fts_full' | 'fts_incremental' | 'vector_full' | 'vector_incremental';
|
||||||
|
label: string;
|
||||||
|
description: string;
|
||||||
|
icon: React.ReactNode;
|
||||||
|
};
|
||||||
|
|
||||||
|
export function IndexOperations({ disabled = false, onRefresh }: IndexOperationsProps) {
|
||||||
|
const { formatMessage } = useIntl();
|
||||||
|
const { success, error: showError } = useNotifications();
|
||||||
|
const projectPath = useWorkflowStore(selectProjectPath);
|
||||||
|
const { inProgress } = useCodexLensIndexingStatus();
|
||||||
|
const { rebuildIndex, isRebuilding } = useRebuildIndex();
|
||||||
|
const { updateIndex, isUpdating } = useUpdateIndex();
|
||||||
|
const { cancelIndexing, isCancelling } = useCancelIndexing();
|
||||||
|
const { lastMessage } = useWebSocket();
|
||||||
|
|
||||||
|
const [indexProgress, setIndexProgress] = useState<IndexProgress | null>(null);
|
||||||
|
const [activeOperation, setActiveOperation] = useState<string | null>(null);
|
||||||
|
|
||||||
|
// Listen for WebSocket progress updates
|
||||||
|
useEffect(() => {
|
||||||
|
if (lastMessage?.type === 'CODEXLENS_INDEX_PROGRESS') {
|
||||||
|
const progress = lastMessage.payload as IndexProgress;
|
||||||
|
setIndexProgress(progress);
|
||||||
|
|
||||||
|
// Clear active operation when complete or error
|
||||||
|
if (progress.stage === 'complete' || progress.stage === 'error' || progress.stage === 'cancelled') {
|
||||||
|
if (progress.stage === 'complete') {
|
||||||
|
success(
|
||||||
|
formatMessage({ id: 'codexlens.index.operationComplete' }),
|
||||||
|
progress.message
|
||||||
|
);
|
||||||
|
onRefresh?.();
|
||||||
|
} else if (progress.stage === 'error') {
|
||||||
|
showError(
|
||||||
|
formatMessage({ id: 'codexlens.index.operationFailed' }),
|
||||||
|
progress.message
|
||||||
|
);
|
||||||
|
}
|
||||||
|
setActiveOperation(null);
|
||||||
|
setIndexProgress(null);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}, [lastMessage, formatMessage, success, showError, onRefresh]);
|
||||||
|
|
||||||
|
const isOperating = isRebuilding || isUpdating || inProgress || !!activeOperation;
|
||||||
|
|
||||||
|
const handleOperation = async (operation: IndexOperation) => {
|
||||||
|
if (!projectPath) {
|
||||||
|
showError(
|
||||||
|
formatMessage({ id: 'codexlens.index.noProject' }),
|
||||||
|
formatMessage({ id: 'codexlens.index.noProjectDesc' })
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
setActiveOperation(operation.id);
|
||||||
|
setIndexProgress({ stage: 'start', message: formatMessage({ id: 'codexlens.index.starting' }), percent: 0 });
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Determine index type and operation
|
||||||
|
const isVector = operation.type.includes('vector');
|
||||||
|
const isIncremental = operation.type.includes('incremental');
|
||||||
|
|
||||||
|
if (isIncremental) {
|
||||||
|
const result = await updateIndex(projectPath, {
|
||||||
|
indexType: isVector ? 'vector' : 'normal',
|
||||||
|
});
|
||||||
|
if (!result.success) {
|
||||||
|
throw new Error(result.error || 'Update failed');
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
const result = await rebuildIndex(projectPath, {
|
||||||
|
indexType: isVector ? 'vector' : 'normal',
|
||||||
|
});
|
||||||
|
if (!result.success) {
|
||||||
|
throw new Error(result.error || 'Rebuild failed');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
setActiveOperation(null);
|
||||||
|
setIndexProgress(null);
|
||||||
|
showError(
|
||||||
|
formatMessage({ id: 'codexlens.index.operationFailed' }),
|
||||||
|
err instanceof Error ? err.message : formatMessage({ id: 'codexlens.index.unknownError' })
|
||||||
|
);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleCancel = async () => {
|
||||||
|
const result = await cancelIndexing();
|
||||||
|
if (result.success) {
|
||||||
|
setActiveOperation(null);
|
||||||
|
setIndexProgress(null);
|
||||||
|
} else {
|
||||||
|
showError(
|
||||||
|
formatMessage({ id: 'codexlens.index.cancelFailed' }),
|
||||||
|
result.error || formatMessage({ id: 'codexlens.index.unknownError' })
|
||||||
|
);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const operations: IndexOperation[] = [
|
||||||
|
{
|
||||||
|
id: 'fts_full',
|
||||||
|
type: 'fts_full',
|
||||||
|
label: formatMessage({ id: 'codexlens.overview.actions.ftsFull' }),
|
||||||
|
description: formatMessage({ id: 'codexlens.overview.actions.ftsFullDesc' }),
|
||||||
|
icon: <RotateCw className="w-4 h-4" />,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'fts_incremental',
|
||||||
|
type: 'fts_incremental',
|
||||||
|
label: formatMessage({ id: 'codexlens.overview.actions.ftsIncremental' }),
|
||||||
|
description: formatMessage({ id: 'codexlens.overview.actions.ftsIncrementalDesc' }),
|
||||||
|
icon: <Zap className="w-4 h-4" />,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'vector_full',
|
||||||
|
type: 'vector_full',
|
||||||
|
label: formatMessage({ id: 'codexlens.overview.actions.vectorFull' }),
|
||||||
|
description: formatMessage({ id: 'codexlens.overview.actions.vectorFullDesc' }),
|
||||||
|
icon: <RotateCw className="w-4 h-4" />,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'vector_incremental',
|
||||||
|
type: 'vector_incremental',
|
||||||
|
label: formatMessage({ id: 'codexlens.overview.actions.vectorIncremental' }),
|
||||||
|
description: formatMessage({ id: 'codexlens.overview.actions.vectorIncrementalDesc' }),
|
||||||
|
icon: <Zap className="w-4 h-4" />,
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
if (indexProgress && activeOperation) {
|
||||||
|
const operation = operations.find((op) => op.id === activeOperation);
|
||||||
|
const isComplete = indexProgress.stage === 'complete';
|
||||||
|
const isError = indexProgress.stage === 'error';
|
||||||
|
const isCancelled = indexProgress.stage === 'cancelled';
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Card>
|
||||||
|
<CardHeader>
|
||||||
|
<CardTitle className="text-base flex items-center justify-between">
|
||||||
|
<span>{operation?.label}</span>
|
||||||
|
{!isComplete && !isError && !isCancelled && (
|
||||||
|
<Button
|
||||||
|
variant="ghost"
|
||||||
|
size="sm"
|
||||||
|
onClick={handleCancel}
|
||||||
|
disabled={isCancelling}
|
||||||
|
>
|
||||||
|
<X className="w-4 h-4 mr-1" />
|
||||||
|
{formatMessage({ id: 'common.actions.cancel' })}
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
</CardTitle>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent className="space-y-4">
|
||||||
|
{/* Status Icon */}
|
||||||
|
<div className="flex items-center gap-3">
|
||||||
|
{isComplete ? (
|
||||||
|
<CheckCircle2 className="w-6 h-6 text-success" />
|
||||||
|
) : isError || isCancelled ? (
|
||||||
|
<AlertCircle className="w-6 h-6 text-destructive" />
|
||||||
|
) : (
|
||||||
|
<RotateCw className="w-6 h-6 text-primary animate-spin" />
|
||||||
|
)}
|
||||||
|
<div className="flex-1">
|
||||||
|
<p className="text-sm font-medium text-foreground">
|
||||||
|
{isComplete
|
||||||
|
? formatMessage({ id: 'codexlens.index.complete' })
|
||||||
|
: isError
|
||||||
|
? formatMessage({ id: 'codexlens.index.failed' })
|
||||||
|
: isCancelled
|
||||||
|
? formatMessage({ id: 'codexlens.index.cancelled' })
|
||||||
|
: formatMessage({ id: 'codexlens.index.inProgress' })}
|
||||||
|
</p>
|
||||||
|
<p className="text-xs text-muted-foreground mt-1">{indexProgress.message}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Progress Bar */}
|
||||||
|
{!isComplete && !isError && !isCancelled && (
|
||||||
|
<div className="space-y-2">
|
||||||
|
<Progress value={indexProgress.percent} className="h-2" />
|
||||||
|
<p className="text-xs text-muted-foreground text-right">
|
||||||
|
{indexProgress.percent}%
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Close Button */}
|
||||||
|
{(isComplete || isError || isCancelled) && (
|
||||||
|
<div className="flex justify-end">
|
||||||
|
<Button
|
||||||
|
variant="outline"
|
||||||
|
size="sm"
|
||||||
|
onClick={() => {
|
||||||
|
setActiveOperation(null);
|
||||||
|
setIndexProgress(null);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{formatMessage({ id: 'common.actions.close' })}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Card>
|
||||||
|
<CardHeader>
|
||||||
|
<CardTitle className="text-base">
|
||||||
|
{formatMessage({ id: 'codexlens.overview.actions.title' })}
|
||||||
|
</CardTitle>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent>
|
||||||
|
<div className="grid grid-cols-1 md:grid-cols-2 gap-3">
|
||||||
|
{operations.map((operation) => (
|
||||||
|
<Button
|
||||||
|
key={operation.id}
|
||||||
|
variant="outline"
|
||||||
|
className="h-auto p-4 flex flex-col items-start gap-2 text-left"
|
||||||
|
onClick={() => handleOperation(operation)}
|
||||||
|
disabled={disabled || isOperating}
|
||||||
|
>
|
||||||
|
<div className="flex items-center gap-2 w-full">
|
||||||
|
<span className={cn('text-muted-foreground', (disabled || isOperating) && 'opacity-50')}>
|
||||||
|
{operation.icon}
|
||||||
|
</span>
|
||||||
|
<span className="font-medium">{operation.label}</span>
|
||||||
|
</div>
|
||||||
|
<p className="text-xs text-muted-foreground">{operation.description}</p>
|
||||||
|
</Button>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default IndexOperations;
|
||||||
@@ -10,6 +10,7 @@ import {
|
|||||||
RefreshCw,
|
RefreshCw,
|
||||||
Package,
|
Package,
|
||||||
Filter,
|
Filter,
|
||||||
|
AlertCircle,
|
||||||
} from 'lucide-react';
|
} from 'lucide-react';
|
||||||
import { Card } from '@/components/ui/Card';
|
import { Card } from '@/components/ui/Card';
|
||||||
import { Button } from '@/components/ui/Button';
|
import { Button } from '@/components/ui/Button';
|
||||||
@@ -69,6 +70,7 @@ export function ModelsTab({ installed = false }: ModelsTabProps) {
|
|||||||
const {
|
const {
|
||||||
models,
|
models,
|
||||||
isLoading,
|
isLoading,
|
||||||
|
error,
|
||||||
refetch,
|
refetch,
|
||||||
} = useCodexLensModels({
|
} = useCodexLensModels({
|
||||||
enabled: installed,
|
enabled: installed,
|
||||||
@@ -243,7 +245,25 @@ export function ModelsTab({ installed = false }: ModelsTabProps) {
|
|||||||
/>
|
/>
|
||||||
|
|
||||||
{/* Model List */}
|
{/* Model List */}
|
||||||
{isLoading ? (
|
{error ? (
|
||||||
|
<Card className="p-8 text-center">
|
||||||
|
<AlertCircle className="w-12 h-12 mx-auto text-destructive/50 mb-3" />
|
||||||
|
<h3 className="text-sm font-medium text-destructive-foreground mb-1">
|
||||||
|
{formatMessage({ id: 'codexlens.models.error.title' })}
|
||||||
|
</h3>
|
||||||
|
<p className="text-xs text-muted-foreground mb-3">
|
||||||
|
{error.message || formatMessage({ id: 'codexlens.models.error.description' })}
|
||||||
|
</p>
|
||||||
|
<Button
|
||||||
|
variant="outline"
|
||||||
|
size="sm"
|
||||||
|
onClick={() => refetch()}
|
||||||
|
>
|
||||||
|
<RefreshCw className="w-3 h-3 mr-1" />
|
||||||
|
{formatMessage({ id: 'common.actions.retry' })}
|
||||||
|
</Button>
|
||||||
|
</Card>
|
||||||
|
) : isLoading ? (
|
||||||
<Card className="p-8 text-center">
|
<Card className="p-8 text-center">
|
||||||
<p className="text-muted-foreground">{formatMessage({ id: 'common.actions.loading' })}</p>
|
<p className="text-muted-foreground">{formatMessage({ id: 'common.actions.loading' })}</p>
|
||||||
</Card>
|
</Card>
|
||||||
@@ -251,10 +271,16 @@ export function ModelsTab({ installed = false }: ModelsTabProps) {
|
|||||||
<Card className="p-8 text-center">
|
<Card className="p-8 text-center">
|
||||||
<Package className="w-12 h-12 mx-auto text-muted-foreground/30 mb-3" />
|
<Package className="w-12 h-12 mx-auto text-muted-foreground/30 mb-3" />
|
||||||
<h3 className="text-sm font-medium text-foreground mb-1">
|
<h3 className="text-sm font-medium text-foreground mb-1">
|
||||||
{formatMessage({ id: 'codexlens.models.empty.title' })}
|
{models && models.length > 0
|
||||||
|
? formatMessage({ id: 'codexlens.models.empty.filtered' })
|
||||||
|
: formatMessage({ id: 'codexlens.models.empty.title' })
|
||||||
|
}
|
||||||
</h3>
|
</h3>
|
||||||
<p className="text-xs text-muted-foreground">
|
<p className="text-xs text-muted-foreground">
|
||||||
{formatMessage({ id: 'codexlens.models.empty.description' })}
|
{models && models.length > 0
|
||||||
|
? formatMessage({ id: 'codexlens.models.empty.filteredDesc' })
|
||||||
|
: formatMessage({ id: 'codexlens.models.empty.description' })
|
||||||
|
}
|
||||||
</p>
|
</p>
|
||||||
</Card>
|
</Card>
|
||||||
) : (
|
) : (
|
||||||
|
|||||||
@@ -9,22 +9,22 @@ import {
|
|||||||
FileText,
|
FileText,
|
||||||
CheckCircle2,
|
CheckCircle2,
|
||||||
XCircle,
|
XCircle,
|
||||||
RotateCw,
|
|
||||||
Zap,
|
Zap,
|
||||||
} from 'lucide-react';
|
} from 'lucide-react';
|
||||||
import { Card, CardHeader, CardTitle, CardContent } from '@/components/ui/Card';
|
import { Card, CardHeader, CardTitle, CardContent } from '@/components/ui/Card';
|
||||||
import { Button } from '@/components/ui/Button';
|
|
||||||
import { cn } from '@/lib/utils';
|
import { cn } from '@/lib/utils';
|
||||||
import type { CodexLensVenvStatus, CodexLensConfig } from '@/lib/api';
|
import type { CodexLensVenvStatus, CodexLensConfig } from '@/lib/api';
|
||||||
|
import { IndexOperations } from './IndexOperations';
|
||||||
|
|
||||||
interface OverviewTabProps {
|
interface OverviewTabProps {
|
||||||
installed: boolean;
|
installed: boolean;
|
||||||
status?: CodexLensVenvStatus;
|
status?: CodexLensVenvStatus;
|
||||||
config?: CodexLensConfig;
|
config?: CodexLensConfig;
|
||||||
isLoading: boolean;
|
isLoading: boolean;
|
||||||
|
onRefresh?: () => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function OverviewTab({ installed, status, config, isLoading }: OverviewTabProps) {
|
export function OverviewTab({ installed, status, config, isLoading, onRefresh }: OverviewTabProps) {
|
||||||
const { formatMessage } = useIntl();
|
const { formatMessage } = useIntl();
|
||||||
|
|
||||||
if (isLoading) {
|
if (isLoading) {
|
||||||
@@ -142,42 +142,8 @@ export function OverviewTab({ installed, status, config, isLoading }: OverviewTa
|
|||||||
</Card>
|
</Card>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Quick Actions */}
|
{/* Index Operations */}
|
||||||
<Card>
|
<IndexOperations disabled={!isReady} onRefresh={onRefresh} />
|
||||||
<CardHeader>
|
|
||||||
<CardTitle className="text-base">
|
|
||||||
{formatMessage({ id: 'codexlens.overview.actions.title' })}
|
|
||||||
</CardTitle>
|
|
||||||
</CardHeader>
|
|
||||||
<CardContent>
|
|
||||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-3">
|
|
||||||
<QuickActionButton
|
|
||||||
icon={<RotateCw className="w-4 h-4" />}
|
|
||||||
label={formatMessage({ id: 'codexlens.overview.actions.ftsFull' })}
|
|
||||||
description={formatMessage({ id: 'codexlens.overview.actions.ftsFullDesc' })}
|
|
||||||
disabled={!isReady}
|
|
||||||
/>
|
|
||||||
<QuickActionButton
|
|
||||||
icon={<Zap className="w-4 h-4" />}
|
|
||||||
label={formatMessage({ id: 'codexlens.overview.actions.ftsIncremental' })}
|
|
||||||
description={formatMessage({ id: 'codexlens.overview.actions.ftsIncrementalDesc' })}
|
|
||||||
disabled={!isReady}
|
|
||||||
/>
|
|
||||||
<QuickActionButton
|
|
||||||
icon={<RotateCw className="w-4 h-4" />}
|
|
||||||
label={formatMessage({ id: 'codexlens.overview.actions.vectorFull' })}
|
|
||||||
description={formatMessage({ id: 'codexlens.overview.actions.vectorFullDesc' })}
|
|
||||||
disabled={!isReady}
|
|
||||||
/>
|
|
||||||
<QuickActionButton
|
|
||||||
icon={<Zap className="w-4 h-4" />}
|
|
||||||
label={formatMessage({ id: 'codexlens.overview.actions.vectorIncremental' })}
|
|
||||||
description={formatMessage({ id: 'codexlens.overview.actions.vectorIncrementalDesc' })}
|
|
||||||
disabled={!isReady}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</CardContent>
|
|
||||||
</Card>
|
|
||||||
|
|
||||||
{/* Venv Details */}
|
{/* Venv Details */}
|
||||||
{status && (
|
{status && (
|
||||||
@@ -210,37 +176,3 @@ export function OverviewTab({ installed, status, config, isLoading }: OverviewTa
|
|||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
interface QuickActionButtonProps {
|
|
||||||
icon: React.ReactNode;
|
|
||||||
label: string;
|
|
||||||
description: string;
|
|
||||||
disabled?: boolean;
|
|
||||||
}
|
|
||||||
|
|
||||||
function QuickActionButton({ icon, label, description, disabled }: QuickActionButtonProps) {
|
|
||||||
const { formatMessage } = useIntl();
|
|
||||||
|
|
||||||
const handleClick = () => {
|
|
||||||
// TODO: Implement index operations in future tasks
|
|
||||||
// For now, show a message that this feature is coming soon
|
|
||||||
alert(formatMessage({ id: 'codexlens.comingSoon' }));
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Button
|
|
||||||
variant="outline"
|
|
||||||
className="h-auto p-4 flex flex-col items-start gap-2 text-left"
|
|
||||||
onClick={handleClick}
|
|
||||||
disabled={disabled}
|
|
||||||
>
|
|
||||||
<div className="flex items-center gap-2 w-full">
|
|
||||||
<span className={cn('text-muted-foreground', disabled && 'opacity-50')}>
|
|
||||||
{icon}
|
|
||||||
</span>
|
|
||||||
<span className="font-medium">{label}</span>
|
|
||||||
</div>
|
|
||||||
<p className="text-xs text-muted-foreground">{description}</p>
|
|
||||||
</Button>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|||||||
273
ccw/frontend/src/components/codexlens/SearchTab.tsx
Normal file
273
ccw/frontend/src/components/codexlens/SearchTab.tsx
Normal file
@@ -0,0 +1,273 @@
|
|||||||
|
// ========================================
|
||||||
|
// CodexLens Search Tab
|
||||||
|
// ========================================
|
||||||
|
// Semantic code search interface with multiple search types
|
||||||
|
|
||||||
|
import { useState } from 'react';
|
||||||
|
import { useIntl } from 'react-intl';
|
||||||
|
import { Search, FileCode, Code } from 'lucide-react';
|
||||||
|
import { Button } from '@/components/ui/Button';
|
||||||
|
import { Input } from '@/components/ui/Input';
|
||||||
|
import { Label } from '@/components/ui/Label';
|
||||||
|
import {
|
||||||
|
Select,
|
||||||
|
SelectContent,
|
||||||
|
SelectItem,
|
||||||
|
SelectTrigger,
|
||||||
|
SelectValue,
|
||||||
|
} from '@/components/ui/Select';
|
||||||
|
import {
|
||||||
|
useCodexLensSearch,
|
||||||
|
useCodexLensFilesSearch,
|
||||||
|
useCodexLensSymbolSearch,
|
||||||
|
} from '@/hooks/useCodexLens';
|
||||||
|
import type { CodexLensSearchParams } from '@/lib/api';
|
||||||
|
import { cn } from '@/lib/utils';
|
||||||
|
|
||||||
|
type SearchType = 'search' | 'search_files' | 'symbol';
|
||||||
|
type SearchMode = 'dense_rerank' | 'fts' | 'fuzzy';
|
||||||
|
|
||||||
|
interface SearchTabProps {
|
||||||
|
enabled: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function SearchTab({ enabled }: SearchTabProps) {
|
||||||
|
const { formatMessage } = useIntl();
|
||||||
|
const [searchType, setSearchType] = useState<SearchType>('search');
|
||||||
|
const [searchMode, setSearchMode] = useState<SearchMode>('dense_rerank');
|
||||||
|
const [query, setQuery] = useState('');
|
||||||
|
const [hasSearched, setHasSearched] = useState(false);
|
||||||
|
|
||||||
|
// Build search params based on search type
|
||||||
|
const searchParams: CodexLensSearchParams = {
|
||||||
|
query,
|
||||||
|
limit: 20,
|
||||||
|
mode: searchType !== 'symbol' ? searchMode : undefined,
|
||||||
|
max_content_length: 200,
|
||||||
|
extra_files_count: 10,
|
||||||
|
};
|
||||||
|
|
||||||
|
// Search hooks - only enable when hasSearched is true and query is not empty
|
||||||
|
const contentSearch = useCodexLensSearch(
|
||||||
|
searchParams,
|
||||||
|
{ enabled: enabled && hasSearched && searchType === 'search' && query.trim().length > 0 }
|
||||||
|
);
|
||||||
|
|
||||||
|
const fileSearch = useCodexLensFilesSearch(
|
||||||
|
searchParams,
|
||||||
|
{ enabled: enabled && hasSearched && searchType === 'search_files' && query.trim().length > 0 }
|
||||||
|
);
|
||||||
|
|
||||||
|
const symbolSearch = useCodexLensSymbolSearch(
|
||||||
|
{ query, limit: 20 },
|
||||||
|
{ enabled: enabled && hasSearched && searchType === 'symbol' && query.trim().length > 0 }
|
||||||
|
);
|
||||||
|
|
||||||
|
// Get loading state based on search type
|
||||||
|
const isLoading = searchType === 'search'
|
||||||
|
? contentSearch.isLoading
|
||||||
|
: searchType === 'search_files'
|
||||||
|
? fileSearch.isLoading
|
||||||
|
: symbolSearch.isLoading;
|
||||||
|
|
||||||
|
const handleSearch = () => {
|
||||||
|
if (query.trim()) {
|
||||||
|
setHasSearched(true);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleKeyPress = (e: React.KeyboardEvent) => {
|
||||||
|
if (e.key === 'Enter') {
|
||||||
|
handleSearch();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleSearchTypeChange = (value: SearchType) => {
|
||||||
|
setSearchType(value);
|
||||||
|
setHasSearched(false); // Reset search state when changing type
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleSearchModeChange = (value: SearchMode) => {
|
||||||
|
setSearchMode(value);
|
||||||
|
setHasSearched(false); // Reset search state when changing mode
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleQueryChange = (value: string) => {
|
||||||
|
setQuery(value);
|
||||||
|
setHasSearched(false); // Reset search state when query changes
|
||||||
|
};
|
||||||
|
|
||||||
|
if (!enabled) {
|
||||||
|
return (
|
||||||
|
<div className="flex items-center justify-center p-12">
|
||||||
|
<div className="text-center">
|
||||||
|
<Search className="w-12 h-12 mx-auto text-muted-foreground/50 mb-4" />
|
||||||
|
<h3 className="text-lg font-medium text-foreground mb-2">
|
||||||
|
{formatMessage({ id: 'codexlens.search.notInstalled.title' })}
|
||||||
|
</h3>
|
||||||
|
<p className="text-muted-foreground">
|
||||||
|
{formatMessage({ id: 'codexlens.search.notInstalled.description' })}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="space-y-6">
|
||||||
|
{/* Search Options */}
|
||||||
|
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||||
|
{/* Search Type */}
|
||||||
|
<div className="space-y-2">
|
||||||
|
<Label>{formatMessage({ id: 'codexlens.search.type' })}</Label>
|
||||||
|
<Select value={searchType} onValueChange={handleSearchTypeChange}>
|
||||||
|
<SelectTrigger>
|
||||||
|
<SelectValue />
|
||||||
|
</SelectTrigger>
|
||||||
|
<SelectContent>
|
||||||
|
<SelectItem value="search">
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<Search className="w-4 h-4" />
|
||||||
|
{formatMessage({ id: 'codexlens.search.content' })}
|
||||||
|
</div>
|
||||||
|
</SelectItem>
|
||||||
|
<SelectItem value="search_files">
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<FileCode className="w-4 h-4" />
|
||||||
|
{formatMessage({ id: 'codexlens.search.files' })}
|
||||||
|
</div>
|
||||||
|
</SelectItem>
|
||||||
|
<SelectItem value="symbol">
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<Code className="w-4 h-4" />
|
||||||
|
{formatMessage({ id: 'codexlens.search.symbol' })}
|
||||||
|
</div>
|
||||||
|
</SelectItem>
|
||||||
|
</SelectContent>
|
||||||
|
</Select>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Search Mode - only for content and file search */}
|
||||||
|
{searchType !== 'symbol' && (
|
||||||
|
<div className="space-y-2">
|
||||||
|
<Label>{formatMessage({ id: 'codexlens.search.mode' })}</Label>
|
||||||
|
<Select value={searchMode} onValueChange={handleSearchModeChange}>
|
||||||
|
<SelectTrigger>
|
||||||
|
<SelectValue />
|
||||||
|
</SelectTrigger>
|
||||||
|
<SelectContent>
|
||||||
|
<SelectItem value="dense_rerank">
|
||||||
|
{formatMessage({ id: 'codexlens.search.mode.semantic' })}
|
||||||
|
</SelectItem>
|
||||||
|
<SelectItem value="fts">
|
||||||
|
{formatMessage({ id: 'codexlens.search.mode.exact' })}
|
||||||
|
</SelectItem>
|
||||||
|
<SelectItem value="fuzzy">
|
||||||
|
{formatMessage({ id: 'codexlens.search.mode.fuzzy' })}
|
||||||
|
</SelectItem>
|
||||||
|
</SelectContent>
|
||||||
|
</Select>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Query Input */}
|
||||||
|
<div className="space-y-2">
|
||||||
|
<Label htmlFor="search-query">{formatMessage({ id: 'codexlens.search.query' })}</Label>
|
||||||
|
<Input
|
||||||
|
id="search-query"
|
||||||
|
placeholder={formatMessage({ id: 'codexlens.search.queryPlaceholder' })}
|
||||||
|
value={query}
|
||||||
|
onChange={(e) => handleQueryChange(e.target.value)}
|
||||||
|
onKeyPress={handleKeyPress}
|
||||||
|
disabled={isLoading}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Search Button */}
|
||||||
|
<Button
|
||||||
|
onClick={handleSearch}
|
||||||
|
disabled={!query.trim() || isLoading}
|
||||||
|
className="w-full"
|
||||||
|
>
|
||||||
|
<Search className={cn('w-4 h-4 mr-2', isLoading && 'animate-spin')} />
|
||||||
|
{isLoading
|
||||||
|
? formatMessage({ id: 'codexlens.search.searching' })
|
||||||
|
: formatMessage({ id: 'codexlens.search.button' })
|
||||||
|
}
|
||||||
|
</Button>
|
||||||
|
|
||||||
|
{/* Results */}
|
||||||
|
{hasSearched && !isLoading && (
|
||||||
|
<div className="space-y-4">
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
|
<h3 className="text-sm font-medium">
|
||||||
|
{formatMessage({ id: 'codexlens.search.results' })}
|
||||||
|
</h3>
|
||||||
|
<span className="text-xs text-muted-foreground">
|
||||||
|
{searchType === 'symbol'
|
||||||
|
? (symbolSearch.data?.success
|
||||||
|
? `${symbolSearch.data.symbols?.length ?? 0} ${formatMessage({ id: 'codexlens.search.resultsCount' })}`
|
||||||
|
: ''
|
||||||
|
)
|
||||||
|
: searchType === 'search'
|
||||||
|
? (contentSearch.data?.success
|
||||||
|
? `${contentSearch.data.results?.length ?? 0} ${formatMessage({ id: 'codexlens.search.resultsCount' })}`
|
||||||
|
: ''
|
||||||
|
)
|
||||||
|
: (fileSearch.data?.success
|
||||||
|
? `${fileSearch.data.results?.length ?? 0} ${formatMessage({ id: 'codexlens.search.resultsCount' })}`
|
||||||
|
: ''
|
||||||
|
)
|
||||||
|
}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{searchType === 'symbol' && symbolSearch.data && (
|
||||||
|
symbolSearch.data.success ? (
|
||||||
|
<div className="rounded-lg border bg-muted/50 p-4">
|
||||||
|
<pre className="text-xs overflow-auto max-h-96">
|
||||||
|
{JSON.stringify(symbolSearch.data.symbols, null, 2)}
|
||||||
|
</pre>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<div className="text-sm text-destructive">
|
||||||
|
{symbolSearch.data.error || formatMessage({ id: 'common.error' })}
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
)}
|
||||||
|
|
||||||
|
{searchType === 'search' && contentSearch.data && (
|
||||||
|
contentSearch.data.success ? (
|
||||||
|
<div className="rounded-lg border bg-muted/50 p-4">
|
||||||
|
<pre className="text-xs overflow-auto max-h-96">
|
||||||
|
{JSON.stringify(contentSearch.data.results, null, 2)}
|
||||||
|
</pre>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<div className="text-sm text-destructive">
|
||||||
|
{contentSearch.data.error || formatMessage({ id: 'common.error' })}
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
)}
|
||||||
|
|
||||||
|
{searchType === 'search_files' && fileSearch.data && (
|
||||||
|
fileSearch.data.success ? (
|
||||||
|
<div className="rounded-lg border bg-muted/50 p-4">
|
||||||
|
<pre className="text-xs overflow-auto max-h-96">
|
||||||
|
{JSON.stringify(fileSearch.data.results, null, 2)}
|
||||||
|
</pre>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<div className="text-sm text-destructive">
|
||||||
|
{fileSearch.data.error || formatMessage({ id: 'common.error' })}
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default SearchTab;
|
||||||
@@ -19,6 +19,9 @@ export function ExecutionTab({ execution, isActive, onClick, onClose }: Executio
|
|||||||
// Simplify tool name (e.g., gemini-2.5-pro -> gemini)
|
// Simplify tool name (e.g., gemini-2.5-pro -> gemini)
|
||||||
const toolNameShort = execution.tool.split('-')[0];
|
const toolNameShort = execution.tool.split('-')[0];
|
||||||
|
|
||||||
|
// Mode display - use icon for visual clarity
|
||||||
|
const modeDisplay = execution.mode === 'write' ? '✏️' : '🔍';
|
||||||
|
|
||||||
// Status color mapping - using softer, semantic colors
|
// Status color mapping - using softer, semantic colors
|
||||||
const statusColor = {
|
const statusColor = {
|
||||||
running: 'bg-indigo-500 shadow-[0_0_6px_rgba(99,102,241,0.4)] animate-pulse',
|
running: 'bg-indigo-500 shadow-[0_0_6px_rgba(99,102,241,0.4)] animate-pulse',
|
||||||
@@ -31,7 +34,7 @@ export function ExecutionTab({ execution, isActive, onClick, onClose }: Executio
|
|||||||
value={execution.id}
|
value={execution.id}
|
||||||
onClick={onClick}
|
onClick={onClick}
|
||||||
className={cn(
|
className={cn(
|
||||||
'gap-1.5 text-xs px-2.5 py-1 rounded-md border border-border/50 group',
|
'gap-1.5 text-xs px-2.5 py-1 rounded-md border border-border/50 group shrink-0',
|
||||||
isActive
|
isActive
|
||||||
? 'bg-slate-100 dark:bg-slate-800 border-slate-300 dark:border-slate-600 shadow-sm'
|
? 'bg-slate-100 dark:bg-slate-800 border-slate-300 dark:border-slate-600 shadow-sm'
|
||||||
: 'bg-muted/30 hover:bg-muted/50 border-border/30',
|
: 'bg-muted/30 hover:bg-muted/50 border-border/30',
|
||||||
@@ -41,14 +44,14 @@ export function ExecutionTab({ execution, isActive, onClick, onClose }: Executio
|
|||||||
{/* Status indicator dot */}
|
{/* Status indicator dot */}
|
||||||
<span className={cn('w-1.5 h-1.5 rounded-full shrink-0', statusColor)} />
|
<span className={cn('w-1.5 h-1.5 rounded-full shrink-0', statusColor)} />
|
||||||
|
|
||||||
|
{/* Mode indicator */}
|
||||||
|
<span className="text-[10px]" title={execution.mode}>
|
||||||
|
{modeDisplay}
|
||||||
|
</span>
|
||||||
|
|
||||||
{/* Simplified tool name */}
|
{/* Simplified tool name */}
|
||||||
<span className="font-medium text-[11px]">{toolNameShort}</span>
|
<span className="font-medium text-[11px]">{toolNameShort}</span>
|
||||||
|
|
||||||
{/* Execution mode - show on hover */}
|
|
||||||
<span className="opacity-0 group-hover:opacity-70 text-[10px] transition-opacity">
|
|
||||||
{execution.mode}
|
|
||||||
</span>
|
|
||||||
|
|
||||||
{/* Line count statistics - show on hover */}
|
{/* Line count statistics - show on hover */}
|
||||||
<span className="opacity-0 group-hover:opacity-50 text-[9px] tabular-nums transition-opacity">
|
<span className="opacity-0 group-hover:opacity-50 text-[9px] tabular-nums transition-opacity">
|
||||||
{execution.output.length}
|
{execution.output.length}
|
||||||
|
|||||||
@@ -12,6 +12,114 @@ export interface JsonDetectionResult {
|
|||||||
error?: string;
|
error?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Try to recover truncated JSON by completing brackets
|
||||||
|
* This handles cases where JSON is split during streaming
|
||||||
|
*/
|
||||||
|
function tryRecoverTruncatedJson(content: string): Record<string, unknown> | null {
|
||||||
|
const trimmed = content.trim();
|
||||||
|
|
||||||
|
// Must start with { to be recoverable JSON
|
||||||
|
if (!trimmed.startsWith('{')) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Count opening vs closing braces
|
||||||
|
let openBraces = 0;
|
||||||
|
let closeBraces = 0;
|
||||||
|
let inString = false;
|
||||||
|
let escapeNext = false;
|
||||||
|
|
||||||
|
for (let i = 0; i < trimmed.length; i++) {
|
||||||
|
const char = trimmed[i];
|
||||||
|
|
||||||
|
if (escapeNext) {
|
||||||
|
escapeNext = false;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (char === '\\') {
|
||||||
|
escapeNext = true;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (char === '"') {
|
||||||
|
inString = !inString;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!inString) {
|
||||||
|
if (char === '{') openBraces++;
|
||||||
|
if (char === '}') closeBraces++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// If we're missing closing braces, try to complete them
|
||||||
|
if (openBraces > closeBraces) {
|
||||||
|
const missingBraces = openBraces - closeBraces;
|
||||||
|
const recovered = trimmed + '}'.repeat(missingBraces);
|
||||||
|
|
||||||
|
// Also close any open quote
|
||||||
|
let finalRecovered = recovered;
|
||||||
|
if (inString) {
|
||||||
|
finalRecovered = recovered + '"';
|
||||||
|
// Add closing braces after the quote
|
||||||
|
finalRecovered = finalRecovered + '}'.repeat(missingBraces);
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
return JSON.parse(finalRecovered) as Record<string, unknown>;
|
||||||
|
} catch {
|
||||||
|
// Recovery failed, try one more approach
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Try parsing as-is first
|
||||||
|
try {
|
||||||
|
return JSON.parse(trimmed) as Record<string, unknown>;
|
||||||
|
} catch {
|
||||||
|
// If still failing, try to close any hanging structures
|
||||||
|
// Remove trailing incomplete key/value and try again
|
||||||
|
const lastCommaIndex = trimmed.lastIndexOf(',');
|
||||||
|
if (lastCommaIndex > 0) {
|
||||||
|
const truncated = trimmed.substring(0, lastCommaIndex) + '}';
|
||||||
|
try {
|
||||||
|
return JSON.parse(truncated) as Record<string, unknown>;
|
||||||
|
} catch {
|
||||||
|
// Still failed
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Detect token usage stats pattern (common in CLI output)
|
||||||
|
* Pattern: {"type":"result","status":"success","stats":{"total_tokens":...,"input_tokens":...,...}
|
||||||
|
*/
|
||||||
|
function detectTokenStats(content: string): Record<string, unknown> | null {
|
||||||
|
// Check for common token stat patterns
|
||||||
|
const patterns = [
|
||||||
|
/"type"\s*:\s*"result"/,
|
||||||
|
/"status"\s*:\s*"success"/,
|
||||||
|
/"stats"\s*:\s*\{/,
|
||||||
|
/"total_tokens"\s*:\s*\d+/,
|
||||||
|
];
|
||||||
|
|
||||||
|
const matchCount = patterns.filter(p => p.test(content)).length;
|
||||||
|
|
||||||
|
// If at least 3 patterns match, this is likely token stats
|
||||||
|
if (matchCount >= 3) {
|
||||||
|
const recovered = tryRecoverTruncatedJson(content);
|
||||||
|
if (recovered) {
|
||||||
|
return recovered;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Detect if a line contains JSON data
|
* Detect if a line contains JSON data
|
||||||
* Supports multiple formats:
|
* Supports multiple formats:
|
||||||
@@ -20,17 +128,29 @@ export interface JsonDetectionResult {
|
|||||||
* - Tool Result: [Tool Result] status: {...}
|
* - Tool Result: [Tool Result] status: {...}
|
||||||
* - Embedded JSON: trailing JSON object
|
* - Embedded JSON: trailing JSON object
|
||||||
* - Code block JSON: ```json ... ```
|
* - Code block JSON: ```json ... ```
|
||||||
|
* - Truncated JSON: handles streaming incomplete JSON
|
||||||
*/
|
*/
|
||||||
export function detectJsonInLine(content: string): JsonDetectionResult {
|
export function detectJsonInLine(content: string): JsonDetectionResult {
|
||||||
const trimmed = content.trim();
|
const trimmed = content.trim();
|
||||||
|
|
||||||
// 1. Direct JSON object or array
|
// 1. Direct JSON object or array
|
||||||
if (trimmed.startsWith('{') || trimmed.startsWith('[')) {
|
if (trimmed.startsWith('{') || trimmed.startsWith('[')) {
|
||||||
|
// First try normal parse
|
||||||
try {
|
try {
|
||||||
const parsed = JSON.parse(trimmed);
|
const parsed = JSON.parse(trimmed);
|
||||||
return { isJson: true, parsed: parsed as Record<string, unknown> };
|
return { isJson: true, parsed: parsed as Record<string, unknown> };
|
||||||
} catch {
|
} catch {
|
||||||
// Continue to other patterns
|
// Normal parse failed, try recovery for truncated JSON
|
||||||
|
const recovered = tryRecoverTruncatedJson(trimmed);
|
||||||
|
if (recovered) {
|
||||||
|
return { isJson: true, parsed: recovered };
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check for token stats pattern specifically
|
||||||
|
const tokenStats = detectTokenStats(trimmed);
|
||||||
|
if (tokenStats) {
|
||||||
|
return { isJson: true, parsed: tokenStats };
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -3,7 +3,7 @@
|
|||||||
// ========================================
|
// ========================================
|
||||||
// Global CLI streaming monitor with multi-execution support
|
// Global CLI streaming monitor with multi-execution support
|
||||||
|
|
||||||
import { useEffect, useRef, useCallback, useState } from 'react';
|
import { useEffect, useRef, useCallback, useState, useMemo, memo } from 'react';
|
||||||
import { useIntl } from 'react-intl';
|
import { useIntl } from 'react-intl';
|
||||||
import {
|
import {
|
||||||
X,
|
X,
|
||||||
@@ -13,6 +13,7 @@ import {
|
|||||||
RefreshCw,
|
RefreshCw,
|
||||||
Search,
|
Search,
|
||||||
ArrowDownToLine,
|
ArrowDownToLine,
|
||||||
|
Trash2,
|
||||||
} from 'lucide-react';
|
} from 'lucide-react';
|
||||||
import { cn } from '@/lib/utils';
|
import { cn } from '@/lib/utils';
|
||||||
import { Button } from '@/components/ui/Button';
|
import { Button } from '@/components/ui/Button';
|
||||||
@@ -26,8 +27,6 @@ import { useActiveCliExecutions, useInvalidateActiveCliExecutions } from '@/hook
|
|||||||
|
|
||||||
// New components for Tab + JSON Cards
|
// New components for Tab + JSON Cards
|
||||||
import { ExecutionTab } from './CliStreamMonitor/components/ExecutionTab';
|
import { ExecutionTab } from './CliStreamMonitor/components/ExecutionTab';
|
||||||
import { OutputLine } from './CliStreamMonitor/components/OutputLine';
|
|
||||||
import { JsonCard } from './CliStreamMonitor/components/JsonCard';
|
|
||||||
import ReactMarkdown from 'react-markdown';
|
import ReactMarkdown from 'react-markdown';
|
||||||
import remarkGfm from 'remark-gfm';
|
import remarkGfm from 'remark-gfm';
|
||||||
|
|
||||||
@@ -95,55 +94,99 @@ function getBorderColorForType(type: CliOutputLine['type']): string {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Render a single output line as a card
|
* Extract content from a line (handle JSON with 'content' field)
|
||||||
*/
|
*/
|
||||||
interface OutputLineCardProps {
|
function extractContentFromLine(line: CliOutputLine): { content: string; isMarkdown: boolean } {
|
||||||
line: CliOutputLine;
|
|
||||||
onCopy?: (content: string) => void;
|
|
||||||
}
|
|
||||||
|
|
||||||
function OutputLineCard({ line, onCopy }: OutputLineCardProps) {
|
|
||||||
const borderColor = getBorderColorForType(line.type);
|
|
||||||
const trimmed = line.content.trim();
|
const trimmed = line.content.trim();
|
||||||
|
|
||||||
// Check if line is JSON with 'content' field
|
|
||||||
let contentToRender = trimmed;
|
|
||||||
let isMarkdown = false;
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
if (trimmed.startsWith('{') || trimmed.startsWith('[')) {
|
if (trimmed.startsWith('{') || trimmed.startsWith('[')) {
|
||||||
const parsed = JSON.parse(trimmed);
|
const parsed = JSON.parse(trimmed);
|
||||||
if ('content' in parsed && typeof parsed.content === 'string') {
|
if ('content' in parsed && typeof parsed.content === 'string') {
|
||||||
contentToRender = parsed.content;
|
const content = parsed.content;
|
||||||
// Check if content looks like markdown
|
const isMarkdown = !!content.match(/^#{1,6}\s|^\*{3,}$|^\s*[-*+]\s+|^\s*\d+\.\s+|\*\*.*?\*\*|`{3,}/m);
|
||||||
isMarkdown = !!contentToRender.match(/^#{1,6}\s|^\*{3,}$|^\s*[-*+]\s+|^\s*\d+\.\s+|\*\*.*?\*\*|`{3,}/m);
|
return { content, isMarkdown };
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} catch {
|
} catch {
|
||||||
// Not valid JSON, use original content
|
// Not valid JSON, use original content
|
||||||
// Check if original content looks like markdown
|
|
||||||
isMarkdown = !!trimmed.match(/^#{1,6}\s|^\*{3,}$|^\s*[-*+]\s+|^\s*\d+\.\s+|\*\*.*?\*\*|`{3,}/m);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Check if original content looks like markdown
|
||||||
|
const isMarkdown = !!trimmed.match(/^#{1,6}\s|^\*{3,}$|^\s*[-*+]\s+|^\s*\d+\.\s+|\*\*.*?\*\*|`{3,}/m);
|
||||||
|
return { content: trimmed, isMarkdown };
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Group consecutive output lines by type
|
||||||
|
*/
|
||||||
|
interface OutputLineGroup {
|
||||||
|
type: CliOutputLine['type'];
|
||||||
|
lines: CliOutputLine[];
|
||||||
|
}
|
||||||
|
|
||||||
|
function groupConsecutiveLinesByType(lines: CliOutputLine[]): OutputLineGroup[] {
|
||||||
|
const groups: OutputLineGroup[] = [];
|
||||||
|
|
||||||
|
for (const line of lines) {
|
||||||
|
// Start new group if type changes
|
||||||
|
if (groups.length === 0 || groups[groups.length - 1].type !== line.type) {
|
||||||
|
groups.push({
|
||||||
|
type: line.type,
|
||||||
|
lines: [line],
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
// Append to existing group
|
||||||
|
groups[groups.length - 1].lines.push(line);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return groups;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Render a group of output lines as a merged card
|
||||||
|
*/
|
||||||
|
interface OutputLineCardProps {
|
||||||
|
group: OutputLineGroup;
|
||||||
|
onCopy?: (content: string) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
function OutputLineCard({ group, onCopy }: OutputLineCardProps) {
|
||||||
|
const borderColor = getBorderColorForType(group.type);
|
||||||
|
|
||||||
|
// Extract content from all lines in the group
|
||||||
|
const lineContents = group.lines.map(line => extractContentFromLine(line));
|
||||||
|
|
||||||
|
// Check if any line has markdown
|
||||||
|
const hasMarkdown = lineContents.some(c => c.isMarkdown);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={`border-l-2 rounded-r my-1 py-1 px-2 group relative bg-background ${borderColor}`}>
|
<div className={`border-l-2 rounded-r my-1 py-1 px-2 group relative bg-background contain-content ${borderColor}`}>
|
||||||
<div className="pr-6">
|
<div className="pr-6 space-y-1">
|
||||||
{isMarkdown ? (
|
{lineContents.map((item, index) => (
|
||||||
<div className="prose prose-sm dark:prose-invert max-w-none text-xs leading-relaxed">
|
<div key={index} className="contain-layout">
|
||||||
<ReactMarkdown remarkPlugins={[remarkGfm]}>
|
{item.isMarkdown || hasMarkdown ? (
|
||||||
{contentToRender}
|
<div className="prose prose-sm dark:prose-invert max-w-none text-xs leading-relaxed contain-layout">
|
||||||
</ReactMarkdown>
|
<ReactMarkdown remarkPlugins={[remarkGfm]}>
|
||||||
|
{item.content}
|
||||||
|
</ReactMarkdown>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<div className="text-xs whitespace-pre-wrap break-words leading-relaxed contain-layout">
|
||||||
|
{item.content}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
) : (
|
))}
|
||||||
<div className="text-xs whitespace-pre-wrap break-words leading-relaxed">
|
|
||||||
{contentToRender}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Memoize the OutputLineCard component to prevent unnecessary re-renders
|
||||||
|
const MemoizedOutputLineCard = memo(OutputLineCard);
|
||||||
|
|
||||||
// ========== Component ==========
|
// ========== Component ==========
|
||||||
|
|
||||||
export interface CliStreamMonitorProps {
|
export interface CliStreamMonitorProps {
|
||||||
@@ -160,11 +203,15 @@ export function CliStreamMonitor({ isOpen, onClose }: CliStreamMonitorProps) {
|
|||||||
const [isUserScrolling, setIsUserScrolling] = useState(false);
|
const [isUserScrolling, setIsUserScrolling] = useState(false);
|
||||||
const [viewMode, setViewMode] = useState<'list' | 'blocks'>('list');
|
const [viewMode, setViewMode] = useState<'list' | 'blocks'>('list');
|
||||||
|
|
||||||
|
// Track last output length to detect new output
|
||||||
|
const lastOutputLengthRef = useRef<Record<string, number>>({});
|
||||||
|
|
||||||
// Store state
|
// Store state
|
||||||
const executions = useCliStreamStore((state) => state.executions);
|
const executions = useCliStreamStore((state) => state.executions);
|
||||||
const currentExecutionId = useCliStreamStore((state) => state.currentExecutionId);
|
const currentExecutionId = useCliStreamStore((state) => state.currentExecutionId);
|
||||||
const setCurrentExecution = useCliStreamStore((state) => state.setCurrentExecution);
|
const setCurrentExecution = useCliStreamStore((state) => state.setCurrentExecution);
|
||||||
const removeExecution = useCliStreamStore((state) => state.removeExecution);
|
const removeExecution = useCliStreamStore((state) => state.removeExecution);
|
||||||
|
const markExecutionClosedByUser = useCliStreamStore((state) => state.markExecutionClosedByUser);
|
||||||
|
|
||||||
// Active execution sync
|
// Active execution sync
|
||||||
const { isLoading: isSyncing, refetch } = useActiveCliExecutions(isOpen);
|
const { isLoading: isSyncing, refetch } = useActiveCliExecutions(isOpen);
|
||||||
@@ -264,21 +311,42 @@ export function CliStreamMonitor({ isOpen, onClose }: CliStreamMonitorProps) {
|
|||||||
});
|
});
|
||||||
invalidateActive();
|
invalidateActive();
|
||||||
}
|
}
|
||||||
}, [lastMessage, currentExecutionId, setCurrentExecution, invalidateActive]);
|
}, [lastMessage, invalidateActive]);
|
||||||
|
|
||||||
// Auto-scroll to bottom when new output arrives
|
// Auto-scroll to bottom when new output arrives (optimized - only scroll when output length changes)
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (autoScroll && !isUserScrolling && logsEndRef.current) {
|
if (!currentExecutionId || !autoScroll || isUserScrolling) return;
|
||||||
logsEndRef.current.scrollIntoView({ behavior: 'smooth' });
|
|
||||||
}
|
|
||||||
}, [executions, autoScroll, isUserScrolling, currentExecutionId]);
|
|
||||||
|
|
||||||
// Handle scroll to detect user scrolling
|
const currentExecution = executions[currentExecutionId];
|
||||||
|
if (!currentExecution) return;
|
||||||
|
|
||||||
|
const currentLength = currentExecution.output.length;
|
||||||
|
const lastLength = lastOutputLengthRef.current[currentExecutionId] || 0;
|
||||||
|
|
||||||
|
// Only scroll if new output was added
|
||||||
|
if (currentLength > lastLength) {
|
||||||
|
lastOutputLengthRef.current[currentExecutionId] = currentLength;
|
||||||
|
requestAnimationFrame(() => {
|
||||||
|
if (logsEndRef.current) {
|
||||||
|
logsEndRef.current.scrollIntoView({ behavior: 'smooth' });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}, [executions, currentExecutionId, autoScroll, isUserScrolling]);
|
||||||
|
|
||||||
|
// Handle scroll to detect user scrolling (with debounce for performance)
|
||||||
|
const handleScrollRef = useRef<NodeJS.Timeout | null>(null);
|
||||||
const handleScroll = useCallback(() => {
|
const handleScroll = useCallback(() => {
|
||||||
if (!logsContainerRef.current) return;
|
if (handleScrollRef.current) {
|
||||||
const { scrollTop, scrollHeight, clientHeight } = logsContainerRef.current;
|
clearTimeout(handleScrollRef.current);
|
||||||
const isAtBottom = scrollHeight - scrollTop - clientHeight < 50;
|
}
|
||||||
setIsUserScrolling(!isAtBottom);
|
|
||||||
|
handleScrollRef.current = setTimeout(() => {
|
||||||
|
if (!logsContainerRef.current) return;
|
||||||
|
const { scrollTop, scrollHeight, clientHeight } = logsContainerRef.current;
|
||||||
|
const isAtBottom = scrollHeight - scrollTop - clientHeight < 50;
|
||||||
|
setIsUserScrolling(!isAtBottom);
|
||||||
|
}, 50); // 50ms debounce
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
// Scroll to bottom handler
|
// Scroll to bottom handler
|
||||||
@@ -287,6 +355,28 @@ export function CliStreamMonitor({ isOpen, onClose }: CliStreamMonitorProps) {
|
|||||||
setIsUserScrolling(false);
|
setIsUserScrolling(false);
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
|
// Handle closing an execution tab
|
||||||
|
const handleCloseExecution = useCallback((executionId: string) => {
|
||||||
|
// Mark as closed by user so it won't be re-added by server sync
|
||||||
|
markExecutionClosedByUser(executionId);
|
||||||
|
// Remove from local state
|
||||||
|
removeExecution(executionId);
|
||||||
|
// If this was the current execution, clear current selection
|
||||||
|
if (currentExecutionId === executionId) {
|
||||||
|
const remainingIds = Object.keys(executions).filter(id => id !== executionId);
|
||||||
|
setCurrentExecution(remainingIds.length > 0 ? remainingIds[0] : null);
|
||||||
|
}
|
||||||
|
}, [markExecutionClosedByUser, removeExecution, currentExecutionId, executions, setCurrentExecution]);
|
||||||
|
|
||||||
|
// Close all executions
|
||||||
|
const handleCloseAll = useCallback(() => {
|
||||||
|
for (const id of Object.keys(executions)) {
|
||||||
|
markExecutionClosedByUser(id);
|
||||||
|
removeExecution(id);
|
||||||
|
}
|
||||||
|
setCurrentExecution(null);
|
||||||
|
}, [markExecutionClosedByUser, removeExecution, executions, setCurrentExecution]);
|
||||||
|
|
||||||
// ESC key to close
|
// ESC key to close
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const handleEsc = (e: KeyboardEvent) => {
|
const handleEsc = (e: KeyboardEvent) => {
|
||||||
@@ -302,27 +392,67 @@ export function CliStreamMonitor({ isOpen, onClose }: CliStreamMonitorProps) {
|
|||||||
return () => window.removeEventListener('keydown', handleEsc);
|
return () => window.removeEventListener('keydown', handleEsc);
|
||||||
}, [isOpen, onClose, searchQuery]);
|
}, [isOpen, onClose, searchQuery]);
|
||||||
|
|
||||||
// Get sorted execution IDs (running first, then by start time)
|
// Cleanup scroll handler timeout on unmount
|
||||||
const sortedExecutionIds = Object.keys(executions).sort((a, b) => {
|
useEffect(() => {
|
||||||
const execA = executions[a];
|
return () => {
|
||||||
const execB = executions[b];
|
if (handleScrollRef.current) {
|
||||||
if (execA.status === 'running' && execB.status !== 'running') return -1;
|
clearTimeout(handleScrollRef.current);
|
||||||
if (execA.status !== 'running' && execB.status === 'running') return 1;
|
}
|
||||||
return execB.startTime - execA.startTime;
|
};
|
||||||
});
|
}, []);
|
||||||
|
|
||||||
// Active execution count for badge
|
// Get sorted execution IDs (memoized to avoid unnecessary recalculations)
|
||||||
const activeCount = Object.values(executions).filter(e => e.status === 'running').length;
|
const sortedExecutionIds = useMemo(() => {
|
||||||
|
return Object.keys(executions).sort((a, b) => {
|
||||||
|
const execA = executions[a];
|
||||||
|
const execB = executions[b];
|
||||||
|
if (execA.status === 'running' && execB.status !== 'running') return -1;
|
||||||
|
if (execA.status !== 'running' && execB.status === 'running') return 1;
|
||||||
|
return execB.startTime - execA.startTime;
|
||||||
|
});
|
||||||
|
}, [executions]);
|
||||||
|
|
||||||
// Current execution
|
// Active execution count for badge (memoized)
|
||||||
const currentExecution = currentExecutionId ? executions[currentExecutionId] : null;
|
const activeCount = useMemo(() => {
|
||||||
|
return Object.values(executions).filter(e => e.status === 'running').length;
|
||||||
|
}, [executions]);
|
||||||
|
|
||||||
// Filter output lines based on search
|
// Current execution (memoized)
|
||||||
const filteredOutput = currentExecution && searchQuery
|
const currentExecution = useMemo(() => {
|
||||||
? currentExecution.output.filter(line =>
|
return currentExecutionId ? executions[currentExecutionId] : null;
|
||||||
|
}, [currentExecutionId, executions]);
|
||||||
|
|
||||||
|
// Maximum lines to display (for performance)
|
||||||
|
const MAX_DISPLAY_LINES = 1000;
|
||||||
|
|
||||||
|
// Filter output lines based on search (memoized with limit)
|
||||||
|
const filteredOutput = useMemo(() => {
|
||||||
|
if (!currentExecution) return [];
|
||||||
|
|
||||||
|
let output = currentExecution.output;
|
||||||
|
|
||||||
|
// Apply search filter
|
||||||
|
if (searchQuery) {
|
||||||
|
output = output.filter(line =>
|
||||||
line.content.toLowerCase().includes(searchQuery.toLowerCase())
|
line.content.toLowerCase().includes(searchQuery.toLowerCase())
|
||||||
)
|
);
|
||||||
: currentExecution?.output || [];
|
}
|
||||||
|
|
||||||
|
// Limit display for performance
|
||||||
|
if (output.length > MAX_DISPLAY_LINES) {
|
||||||
|
return output.slice(-MAX_DISPLAY_LINES);
|
||||||
|
}
|
||||||
|
|
||||||
|
return output;
|
||||||
|
}, [currentExecution, searchQuery]);
|
||||||
|
|
||||||
|
// Check if output was truncated
|
||||||
|
const isOutputTruncated = currentExecution && currentExecution.output.length > MAX_DISPLAY_LINES;
|
||||||
|
|
||||||
|
// Group output lines by type (memoized for performance)
|
||||||
|
const groupedOutput = useMemo(() => {
|
||||||
|
return groupConsecutiveLinesByType(filteredOutput);
|
||||||
|
}, [filteredOutput]);
|
||||||
|
|
||||||
if (!isOpen) {
|
if (!isOpen) {
|
||||||
return null;
|
return null;
|
||||||
@@ -367,6 +497,16 @@ export function CliStreamMonitor({ isOpen, onClose }: CliStreamMonitorProps) {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex items-center gap-1">
|
<div className="flex items-center gap-1">
|
||||||
|
{sortedExecutionIds.length > 0 && (
|
||||||
|
<Button
|
||||||
|
variant="ghost"
|
||||||
|
size="icon"
|
||||||
|
onClick={handleCloseAll}
|
||||||
|
title="Close all executions"
|
||||||
|
>
|
||||||
|
<Trash2 className="h-4 w-4" />
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
<Button
|
<Button
|
||||||
variant="ghost"
|
variant="ghost"
|
||||||
size="icon"
|
size="icon"
|
||||||
@@ -390,7 +530,7 @@ export function CliStreamMonitor({ isOpen, onClose }: CliStreamMonitorProps) {
|
|||||||
onValueChange={(v) => setCurrentExecution(v || null)}
|
onValueChange={(v) => setCurrentExecution(v || null)}
|
||||||
className="w-full"
|
className="w-full"
|
||||||
>
|
>
|
||||||
<TabsList className="w-full h-auto flex-wrap gap-1 bg-secondary/50 p-1">
|
<TabsList className="w-full h-auto gap-1 bg-secondary/50 p-1 overflow-x-auto overflow-y-hidden no-scrollbar">
|
||||||
{sortedExecutionIds.map((id) => (
|
{sortedExecutionIds.map((id) => (
|
||||||
<ExecutionTab
|
<ExecutionTab
|
||||||
key={id}
|
key={id}
|
||||||
@@ -399,7 +539,7 @@ export function CliStreamMonitor({ isOpen, onClose }: CliStreamMonitorProps) {
|
|||||||
onClick={() => setCurrentExecution(id)}
|
onClick={() => setCurrentExecution(id)}
|
||||||
onClose={(e) => {
|
onClose={(e) => {
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
removeExecution(id);
|
handleCloseExecution(id);
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
))}
|
))}
|
||||||
@@ -472,26 +612,27 @@ export function CliStreamMonitor({ isOpen, onClose }: CliStreamMonitorProps) {
|
|||||||
) : (
|
) : (
|
||||||
<div
|
<div
|
||||||
ref={logsContainerRef}
|
ref={logsContainerRef}
|
||||||
className="h-full overflow-y-auto p-3 font-mono text-xs bg-background"
|
className="h-full overflow-y-auto p-3 font-mono text-xs bg-background contain-strict"
|
||||||
onScroll={handleScroll}
|
onScroll={handleScroll}
|
||||||
>
|
>
|
||||||
|
{isOutputTruncated && (
|
||||||
|
<div className="mb-2 p-2 bg-amber-50 dark:bg-amber-950/30 border border-amber-200 dark:border-amber-800 rounded text-amber-800 dark:text-amber-200 text-xs">
|
||||||
|
Showing last {MAX_DISPLAY_LINES} lines of {currentExecution?.output.length} total lines. Use search to find specific content.
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
{filteredOutput.length === 0 ? (
|
{filteredOutput.length === 0 ? (
|
||||||
<div className="flex items-center justify-center h-full text-muted-foreground">
|
<div className="flex items-center justify-center h-full text-muted-foreground">
|
||||||
{searchQuery ? 'No matching output found' : 'Waiting for output...'}
|
{searchQuery ? 'No matching output found' : 'Waiting for output...'}
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<div className="space-y-1">
|
<div>
|
||||||
{(() => {
|
{groupedOutput.map((group, groupIndex) => (
|
||||||
// Group output lines by type
|
<MemoizedOutputLineCard
|
||||||
const groupedOutput = groupOutputLines(filteredOutput);
|
key={`group-${group.type}-${groupIndex}`}
|
||||||
return groupedOutput.map((group, groupIndex) => (
|
group={group}
|
||||||
<OutputGroupRenderer
|
onCopy={(content) => navigator.clipboard.writeText(content)}
|
||||||
key={`group-${group.type}-${groupIndex}`}
|
/>
|
||||||
group={group}
|
))}
|
||||||
onCopy={(content) => navigator.clipboard.writeText(content)}
|
|
||||||
/>
|
|
||||||
));
|
|
||||||
})()}
|
|
||||||
<div ref={logsEndRef} />
|
<div ref={logsEndRef} />
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -105,8 +105,12 @@ export function useActiveCliExecutions(
|
|||||||
refetchInterval: number = 5000
|
refetchInterval: number = 5000
|
||||||
) {
|
) {
|
||||||
const upsertExecution = useCliStreamStore(state => state.upsertExecution);
|
const upsertExecution = useCliStreamStore(state => state.upsertExecution);
|
||||||
|
const removeExecution = useCliStreamStore(state => state.removeExecution);
|
||||||
const executions = useCliStreamStore(state => state.executions);
|
const executions = useCliStreamStore(state => state.executions);
|
||||||
const setCurrentExecution = useCliStreamStore(state => state.setCurrentExecution);
|
const setCurrentExecution = useCliStreamStore(state => state.setCurrentExecution);
|
||||||
|
const markExecutionClosedByUser = useCliStreamStore(state => state.markExecutionClosedByUser);
|
||||||
|
const isExecutionClosedByUser = useCliStreamStore(state => state.isExecutionClosedByUser);
|
||||||
|
const cleanupUserClosedExecutions = useCliStreamStore(state => state.cleanupUserClosedExecutions);
|
||||||
|
|
||||||
return useQuery({
|
return useQuery({
|
||||||
queryKey: ACTIVE_CLI_EXECUTIONS_QUERY_KEY,
|
queryKey: ACTIVE_CLI_EXECUTIONS_QUERY_KEY,
|
||||||
@@ -117,11 +121,33 @@ export function useActiveCliExecutions(
|
|||||||
}
|
}
|
||||||
const data: ActiveCliExecutionsResponse = await response.json();
|
const data: ActiveCliExecutionsResponse = await response.json();
|
||||||
|
|
||||||
|
// Get server execution IDs
|
||||||
|
const serverIds = new Set(data.executions.map(e => e.id));
|
||||||
|
|
||||||
|
// Clean up userClosedExecutions - remove those no longer on server
|
||||||
|
cleanupUserClosedExecutions(serverIds);
|
||||||
|
|
||||||
|
// Remove executions that are no longer on server and were closed by user
|
||||||
|
for (const [id, exec] of Object.entries(executions)) {
|
||||||
|
if (isExecutionClosedByUser(id)) {
|
||||||
|
// User closed this execution, remove from local state
|
||||||
|
removeExecution(id);
|
||||||
|
} else if (exec.status !== 'running' && !serverIds.has(id) && exec.recovered) {
|
||||||
|
// Not running, not on server, and was recovered (not user-created)
|
||||||
|
removeExecution(id);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Process executions and sync to store
|
// Process executions and sync to store
|
||||||
let hasNewExecution = false;
|
let hasNewExecution = false;
|
||||||
const now = Date.now();
|
const now = Date.now();
|
||||||
|
|
||||||
for (const exec of data.executions) {
|
for (const exec of data.executions) {
|
||||||
|
// Skip if user closed this execution
|
||||||
|
if (isExecutionClosedByUser(exec.id)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
const existing = executions[exec.id];
|
const existing = executions[exec.id];
|
||||||
const historicalOutput = parseHistoricalOutput(exec.output || '', exec.startTime);
|
const historicalOutput = parseHistoricalOutput(exec.output || '', exec.startTime);
|
||||||
|
|
||||||
@@ -175,7 +201,7 @@ export function useActiveCliExecutions(
|
|||||||
|
|
||||||
// Set current execution to first running execution if none selected
|
// Set current execution to first running execution if none selected
|
||||||
if (hasNewExecution) {
|
if (hasNewExecution) {
|
||||||
const runningExec = data.executions.find(e => e.status === 'running');
|
const runningExec = data.executions.find(e => e.status === 'running' && !isExecutionClosedByUser(e.id));
|
||||||
if (runningExec && !executions[runningExec.id]) {
|
if (runningExec && !executions[runningExec.id]) {
|
||||||
setCurrentExecution(runningExec.id);
|
setCurrentExecution(runningExec.id);
|
||||||
}
|
}
|
||||||
|
|||||||
623
ccw/frontend/src/hooks/useApiSettings.ts
Normal file
623
ccw/frontend/src/hooks/useApiSettings.ts
Normal file
@@ -0,0 +1,623 @@
|
|||||||
|
// ========================================
|
||||||
|
// useApiSettings Hook
|
||||||
|
// ========================================
|
||||||
|
// TanStack Query hooks for API Settings management
|
||||||
|
|
||||||
|
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
|
||||||
|
import {
|
||||||
|
fetchProviders,
|
||||||
|
createProvider,
|
||||||
|
updateProvider,
|
||||||
|
deleteProvider,
|
||||||
|
testProvider,
|
||||||
|
testProviderKey,
|
||||||
|
getProviderHealthStatus,
|
||||||
|
triggerProviderHealthCheck,
|
||||||
|
fetchEndpoints,
|
||||||
|
createEndpoint,
|
||||||
|
updateEndpoint,
|
||||||
|
deleteEndpoint,
|
||||||
|
fetchCacheStats,
|
||||||
|
clearCache,
|
||||||
|
updateCacheSettings,
|
||||||
|
fetchModelPools,
|
||||||
|
fetchModelPool,
|
||||||
|
createModelPool,
|
||||||
|
updateModelPool,
|
||||||
|
deleteModelPool,
|
||||||
|
getAvailableModelsForPool,
|
||||||
|
discoverModelsForPool,
|
||||||
|
fetchApiConfig,
|
||||||
|
syncApiConfig,
|
||||||
|
previewYamlConfig,
|
||||||
|
checkCcwLitellmStatus,
|
||||||
|
installCcwLitellm,
|
||||||
|
uninstallCcwLitellm,
|
||||||
|
type ProviderCredential,
|
||||||
|
type CustomEndpoint,
|
||||||
|
type CacheStats,
|
||||||
|
type GlobalCacheSettings,
|
||||||
|
type ModelPoolConfig,
|
||||||
|
type ModelPoolType,
|
||||||
|
type DiscoveredProvider,
|
||||||
|
} from '../lib/api';
|
||||||
|
|
||||||
|
// Query key factory
|
||||||
|
export const apiSettingsKeys = {
|
||||||
|
all: ['apiSettings'] as const,
|
||||||
|
providers: () => [...apiSettingsKeys.all, 'providers'] as const,
|
||||||
|
provider: (id: string) => [...apiSettingsKeys.providers(), id] as const,
|
||||||
|
endpoints: () => [...apiSettingsKeys.all, 'endpoints'] as const,
|
||||||
|
endpoint: (id: string) => [...apiSettingsKeys.endpoints(), id] as const,
|
||||||
|
cache: () => [...apiSettingsKeys.all, 'cache'] as const,
|
||||||
|
modelPools: () => [...apiSettingsKeys.all, 'modelPools'] as const,
|
||||||
|
modelPool: (id: string) => [...apiSettingsKeys.modelPools(), id] as const,
|
||||||
|
ccwLitellm: () => [...apiSettingsKeys.all, 'ccwLitellm'] as const,
|
||||||
|
};
|
||||||
|
|
||||||
|
const STALE_TIME = 2 * 60 * 1000;
|
||||||
|
|
||||||
|
// ========================================
|
||||||
|
// Provider Hooks
|
||||||
|
// ========================================
|
||||||
|
|
||||||
|
export interface UseProvidersOptions {
|
||||||
|
staleTime?: number;
|
||||||
|
enabled?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface UseProvidersReturn {
|
||||||
|
providers: ProviderCredential[];
|
||||||
|
totalCount: number;
|
||||||
|
isLoading: boolean;
|
||||||
|
isFetching: boolean;
|
||||||
|
error: Error | null;
|
||||||
|
refetch: () => Promise<void>;
|
||||||
|
invalidate: () => Promise<void>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function useProviders(options: UseProvidersOptions = {}): UseProvidersReturn {
|
||||||
|
const { staleTime = STALE_TIME, enabled = true } = options;
|
||||||
|
const queryClient = useQueryClient();
|
||||||
|
|
||||||
|
const query = useQuery({
|
||||||
|
queryKey: apiSettingsKeys.providers(),
|
||||||
|
queryFn: fetchProviders,
|
||||||
|
staleTime,
|
||||||
|
enabled,
|
||||||
|
retry: 2,
|
||||||
|
});
|
||||||
|
|
||||||
|
const providers = query.data?.providers ?? [];
|
||||||
|
|
||||||
|
const refetch = async () => {
|
||||||
|
await query.refetch();
|
||||||
|
};
|
||||||
|
|
||||||
|
const invalidate = async () => {
|
||||||
|
await queryClient.invalidateQueries({ queryKey: apiSettingsKeys.providers() });
|
||||||
|
};
|
||||||
|
|
||||||
|
return {
|
||||||
|
providers,
|
||||||
|
totalCount: providers.length,
|
||||||
|
isLoading: query.isLoading,
|
||||||
|
isFetching: query.isFetching,
|
||||||
|
error: query.error,
|
||||||
|
refetch,
|
||||||
|
invalidate,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export function useCreateProvider() {
|
||||||
|
const queryClient = useQueryClient();
|
||||||
|
|
||||||
|
const mutation = useMutation({
|
||||||
|
mutationFn: (provider: Omit<ProviderCredential, 'id' | 'createdAt' | 'updatedAt'>) =>
|
||||||
|
createProvider(provider),
|
||||||
|
onSuccess: () => {
|
||||||
|
queryClient.invalidateQueries({ queryKey: apiSettingsKeys.providers() });
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
return {
|
||||||
|
createProvider: mutation.mutateAsync,
|
||||||
|
isCreating: mutation.isPending,
|
||||||
|
error: mutation.error,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export function useUpdateProvider() {
|
||||||
|
const queryClient = useQueryClient();
|
||||||
|
|
||||||
|
const mutation = useMutation({
|
||||||
|
mutationFn: ({ providerId, updates }: { providerId: string; updates: Partial<Omit<ProviderCredential, 'id' | 'createdAt' | 'updatedAt'>> }) =>
|
||||||
|
updateProvider(providerId, updates),
|
||||||
|
onSuccess: () => {
|
||||||
|
queryClient.invalidateQueries({ queryKey: apiSettingsKeys.providers() });
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
return {
|
||||||
|
updateProvider: (providerId: string, updates: Partial<Omit<ProviderCredential, 'id' | 'createdAt' | 'updatedAt'>>) =>
|
||||||
|
mutation.mutateAsync({ providerId, updates }),
|
||||||
|
isUpdating: mutation.isPending,
|
||||||
|
error: mutation.error,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export function useDeleteProvider() {
|
||||||
|
const queryClient = useQueryClient();
|
||||||
|
|
||||||
|
const mutation = useMutation({
|
||||||
|
mutationFn: (providerId: string) => deleteProvider(providerId),
|
||||||
|
onSuccess: () => {
|
||||||
|
queryClient.invalidateQueries({ queryKey: apiSettingsKeys.providers() });
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
return {
|
||||||
|
deleteProvider: mutation.mutateAsync,
|
||||||
|
isDeleting: mutation.isPending,
|
||||||
|
error: mutation.error,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export function useTestProvider() {
|
||||||
|
const mutation = useMutation({
|
||||||
|
mutationFn: (providerId: string) => testProvider(providerId),
|
||||||
|
});
|
||||||
|
|
||||||
|
return {
|
||||||
|
testProvider: mutation.mutateAsync,
|
||||||
|
isTesting: mutation.isPending,
|
||||||
|
error: mutation.error,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export function useTestProviderKey() {
|
||||||
|
const mutation = useMutation({
|
||||||
|
mutationFn: ({ providerId, keyId }: { providerId: string; keyId: string }) =>
|
||||||
|
testProviderKey(providerId, keyId),
|
||||||
|
});
|
||||||
|
|
||||||
|
return {
|
||||||
|
testProviderKey: mutation.mutateAsync,
|
||||||
|
isTesting: mutation.isPending,
|
||||||
|
error: mutation.error,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export function useProviderHealthStatus(providerId: string) {
|
||||||
|
return useQuery({
|
||||||
|
queryKey: [...apiSettingsKeys.provider(providerId), 'health'],
|
||||||
|
queryFn: () => getProviderHealthStatus(providerId),
|
||||||
|
enabled: !!providerId,
|
||||||
|
staleTime: 30000, // 30 seconds
|
||||||
|
refetchInterval: 60000, // Refetch every minute
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export function useTriggerProviderHealthCheck() {
|
||||||
|
const queryClient = useQueryClient();
|
||||||
|
|
||||||
|
const mutation = useMutation({
|
||||||
|
mutationFn: (providerId: string) => triggerProviderHealthCheck(providerId),
|
||||||
|
onSuccess: (_, providerId) => {
|
||||||
|
queryClient.invalidateQueries({ queryKey: [...apiSettingsKeys.provider(providerId), 'health'] });
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
return {
|
||||||
|
triggerHealthCheck: mutation.mutateAsync,
|
||||||
|
isChecking: mutation.isPending,
|
||||||
|
error: mutation.error,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// ========================================
|
||||||
|
// Endpoint Hooks
|
||||||
|
// ========================================
|
||||||
|
|
||||||
|
export interface UseEndpointsOptions {
|
||||||
|
staleTime?: number;
|
||||||
|
enabled?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface UseEndpointsReturn {
|
||||||
|
endpoints: CustomEndpoint[];
|
||||||
|
totalCount: number;
|
||||||
|
cachedCount: number;
|
||||||
|
isLoading: boolean;
|
||||||
|
isFetching: boolean;
|
||||||
|
error: Error | null;
|
||||||
|
refetch: () => Promise<void>;
|
||||||
|
invalidate: () => Promise<void>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function useEndpoints(options: UseEndpointsOptions = {}): UseEndpointsReturn {
|
||||||
|
const { staleTime = STALE_TIME, enabled = true } = options;
|
||||||
|
const queryClient = useQueryClient();
|
||||||
|
|
||||||
|
const query = useQuery({
|
||||||
|
queryKey: apiSettingsKeys.endpoints(),
|
||||||
|
queryFn: fetchEndpoints,
|
||||||
|
staleTime,
|
||||||
|
enabled,
|
||||||
|
retry: 2,
|
||||||
|
});
|
||||||
|
|
||||||
|
const endpoints = query.data?.endpoints ?? [];
|
||||||
|
const cachedEndpoints = endpoints.filter((e) => e.cacheStrategy.enabled);
|
||||||
|
|
||||||
|
const refetch = async () => {
|
||||||
|
await query.refetch();
|
||||||
|
};
|
||||||
|
|
||||||
|
const invalidate = async () => {
|
||||||
|
await queryClient.invalidateQueries({ queryKey: apiSettingsKeys.endpoints() });
|
||||||
|
};
|
||||||
|
|
||||||
|
return {
|
||||||
|
endpoints,
|
||||||
|
totalCount: endpoints.length,
|
||||||
|
cachedCount: cachedEndpoints.length,
|
||||||
|
isLoading: query.isLoading,
|
||||||
|
isFetching: query.isFetching,
|
||||||
|
error: query.error,
|
||||||
|
refetch,
|
||||||
|
invalidate,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export function useCreateEndpoint() {
|
||||||
|
const queryClient = useQueryClient();
|
||||||
|
|
||||||
|
const mutation = useMutation({
|
||||||
|
mutationFn: (endpoint: Omit<CustomEndpoint, 'createdAt' | 'updatedAt'>) =>
|
||||||
|
createEndpoint(endpoint),
|
||||||
|
onSuccess: () => {
|
||||||
|
queryClient.invalidateQueries({ queryKey: apiSettingsKeys.endpoints() });
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
return {
|
||||||
|
createEndpoint: mutation.mutateAsync,
|
||||||
|
isCreating: mutation.isPending,
|
||||||
|
error: mutation.error,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export function useUpdateEndpoint() {
|
||||||
|
const queryClient = useQueryClient();
|
||||||
|
|
||||||
|
const mutation = useMutation({
|
||||||
|
mutationFn: ({ endpointId, updates }: { endpointId: string; updates: Partial<Omit<CustomEndpoint, 'id' | 'createdAt' | 'updatedAt'>> }) =>
|
||||||
|
updateEndpoint(endpointId, updates),
|
||||||
|
onSuccess: () => {
|
||||||
|
queryClient.invalidateQueries({ queryKey: apiSettingsKeys.endpoints() });
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
return {
|
||||||
|
updateEndpoint: (endpointId: string, updates: Partial<Omit<CustomEndpoint, 'id' | 'createdAt' | 'updatedAt'>>) =>
|
||||||
|
mutation.mutateAsync({ endpointId, updates }),
|
||||||
|
isUpdating: mutation.isPending,
|
||||||
|
error: mutation.error,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export function useDeleteEndpoint() {
|
||||||
|
const queryClient = useQueryClient();
|
||||||
|
|
||||||
|
const mutation = useMutation({
|
||||||
|
mutationFn: (endpointId: string) => deleteEndpoint(endpointId),
|
||||||
|
onSuccess: () => {
|
||||||
|
queryClient.invalidateQueries({ queryKey: apiSettingsKeys.endpoints() });
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
return {
|
||||||
|
deleteEndpoint: mutation.mutateAsync,
|
||||||
|
isDeleting: mutation.isPending,
|
||||||
|
error: mutation.error,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// ========================================
|
||||||
|
// Cache Hooks
|
||||||
|
// ========================================
|
||||||
|
|
||||||
|
export interface UseCacheStatsOptions {
|
||||||
|
staleTime?: number;
|
||||||
|
enabled?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface UseCacheStatsReturn {
|
||||||
|
stats: CacheStats | null;
|
||||||
|
isLoading: boolean;
|
||||||
|
isFetching: boolean;
|
||||||
|
error: Error | null;
|
||||||
|
refetch: () => Promise<void>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function useCacheStats(options: UseCacheStatsOptions = {}): UseCacheStatsReturn {
|
||||||
|
const { staleTime = 30000, enabled = true } = options; // 30 seconds stale time for cache stats
|
||||||
|
|
||||||
|
const query = useQuery({
|
||||||
|
queryKey: apiSettingsKeys.cache(),
|
||||||
|
queryFn: fetchCacheStats,
|
||||||
|
staleTime,
|
||||||
|
enabled,
|
||||||
|
retry: 2,
|
||||||
|
});
|
||||||
|
|
||||||
|
const refetch = async () => {
|
||||||
|
await query.refetch();
|
||||||
|
};
|
||||||
|
|
||||||
|
return {
|
||||||
|
stats: query.data ?? null,
|
||||||
|
isLoading: query.isLoading,
|
||||||
|
isFetching: query.isFetching,
|
||||||
|
error: query.error,
|
||||||
|
refetch,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export function useClearCache() {
|
||||||
|
const queryClient = useQueryClient();
|
||||||
|
|
||||||
|
const mutation = useMutation({
|
||||||
|
mutationFn: () => clearCache(),
|
||||||
|
onSuccess: () => {
|
||||||
|
queryClient.invalidateQueries({ queryKey: apiSettingsKeys.cache() });
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
return {
|
||||||
|
clearCache: mutation.mutateAsync,
|
||||||
|
isClearing: mutation.isPending,
|
||||||
|
error: mutation.error,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export function useUpdateCacheSettings() {
|
||||||
|
const queryClient = useQueryClient();
|
||||||
|
|
||||||
|
const mutation = useMutation({
|
||||||
|
mutationFn: (settings: Partial<{ enabled: boolean; cacheDir: string; maxTotalSizeMB: number }>) =>
|
||||||
|
updateCacheSettings(settings),
|
||||||
|
onSuccess: () => {
|
||||||
|
queryClient.invalidateQueries({ queryKey: apiSettingsKeys.cache() });
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
return {
|
||||||
|
updateCacheSettings: mutation.mutateAsync,
|
||||||
|
isUpdating: mutation.isPending,
|
||||||
|
error: mutation.error,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// ========================================
|
||||||
|
// Model Pool Hooks
|
||||||
|
// ========================================
|
||||||
|
|
||||||
|
export interface UseModelPoolsOptions {
|
||||||
|
staleTime?: number;
|
||||||
|
enabled?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface UseModelPoolsReturn {
|
||||||
|
pools: ModelPoolConfig[];
|
||||||
|
totalCount: number;
|
||||||
|
enabledCount: number;
|
||||||
|
isLoading: boolean;
|
||||||
|
isFetching: boolean;
|
||||||
|
error: Error | null;
|
||||||
|
refetch: () => Promise<void>;
|
||||||
|
invalidate: () => Promise<void>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function useModelPools(options: UseModelPoolsOptions = {}): UseModelPoolsReturn {
|
||||||
|
const { staleTime = STALE_TIME, enabled = true } = options;
|
||||||
|
const queryClient = useQueryClient();
|
||||||
|
|
||||||
|
const query = useQuery({
|
||||||
|
queryKey: apiSettingsKeys.modelPools(),
|
||||||
|
queryFn: fetchModelPools,
|
||||||
|
staleTime,
|
||||||
|
enabled,
|
||||||
|
retry: 2,
|
||||||
|
});
|
||||||
|
|
||||||
|
const pools = query.data?.pools ?? [];
|
||||||
|
const enabledPools = pools.filter((p) => p.enabled);
|
||||||
|
|
||||||
|
const refetch = async () => {
|
||||||
|
await query.refetch();
|
||||||
|
};
|
||||||
|
|
||||||
|
const invalidate = async () => {
|
||||||
|
await queryClient.invalidateQueries({ queryKey: apiSettingsKeys.modelPools() });
|
||||||
|
};
|
||||||
|
|
||||||
|
return {
|
||||||
|
pools,
|
||||||
|
totalCount: pools.length,
|
||||||
|
enabledCount: enabledPools.length,
|
||||||
|
isLoading: query.isLoading,
|
||||||
|
isFetching: query.isFetching,
|
||||||
|
error: query.error,
|
||||||
|
refetch,
|
||||||
|
invalidate,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export function useModelPool(poolId: string) {
|
||||||
|
return useQuery({
|
||||||
|
queryKey: apiSettingsKeys.modelPool(poolId),
|
||||||
|
queryFn: () => fetchModelPool(poolId),
|
||||||
|
enabled: !!poolId,
|
||||||
|
staleTime: STALE_TIME,
|
||||||
|
retry: 2,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export function useCreateModelPool() {
|
||||||
|
const queryClient = useQueryClient();
|
||||||
|
|
||||||
|
const mutation = useMutation({
|
||||||
|
mutationFn: (pool: Omit<ModelPoolConfig, 'id'>) => createModelPool(pool),
|
||||||
|
onSuccess: () => {
|
||||||
|
queryClient.invalidateQueries({ queryKey: apiSettingsKeys.modelPools() });
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
return {
|
||||||
|
createModelPool: mutation.mutateAsync,
|
||||||
|
isCreating: mutation.isPending,
|
||||||
|
error: mutation.error,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export function useUpdateModelPool() {
|
||||||
|
const queryClient = useQueryClient();
|
||||||
|
|
||||||
|
const mutation = useMutation({
|
||||||
|
mutationFn: ({ poolId, updates }: { poolId: string; updates: Partial<ModelPoolConfig> }) =>
|
||||||
|
updateModelPool(poolId, updates),
|
||||||
|
onSuccess: (_, variables) => {
|
||||||
|
queryClient.invalidateQueries({ queryKey: apiSettingsKeys.modelPools() });
|
||||||
|
queryClient.invalidateQueries({ queryKey: apiSettingsKeys.modelPool(variables.poolId) });
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
return {
|
||||||
|
updateModelPool: (poolId: string, updates: Partial<ModelPoolConfig>) =>
|
||||||
|
mutation.mutateAsync({ poolId, updates }),
|
||||||
|
isUpdating: mutation.isPending,
|
||||||
|
error: mutation.error,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export function useDeleteModelPool() {
|
||||||
|
const queryClient = useQueryClient();
|
||||||
|
|
||||||
|
const mutation = useMutation({
|
||||||
|
mutationFn: (poolId: string) => deleteModelPool(poolId),
|
||||||
|
onSuccess: () => {
|
||||||
|
queryClient.invalidateQueries({ queryKey: apiSettingsKeys.modelPools() });
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
return {
|
||||||
|
deleteModelPool: mutation.mutateAsync,
|
||||||
|
isDeleting: mutation.isPending,
|
||||||
|
error: mutation.error,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export function useAvailableModelsForPool(modelType: ModelPoolType) {
|
||||||
|
return useQuery({
|
||||||
|
queryKey: [...apiSettingsKeys.modelPools(), 'available', modelType],
|
||||||
|
queryFn: () => getAvailableModelsForPool(modelType),
|
||||||
|
enabled: !!modelType,
|
||||||
|
staleTime: STALE_TIME,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export function useDiscoverModelsForPool() {
|
||||||
|
const mutation = useMutation({
|
||||||
|
mutationFn: ({ modelType, targetModel }: { modelType: ModelPoolType; targetModel: string }) =>
|
||||||
|
discoverModelsForPool(modelType, targetModel),
|
||||||
|
});
|
||||||
|
|
||||||
|
return {
|
||||||
|
discoverModels: (modelType: ModelPoolType, targetModel: string) =>
|
||||||
|
mutation.mutateAsync({ modelType, targetModel }),
|
||||||
|
isDiscovering: mutation.isPending,
|
||||||
|
error: mutation.error,
|
||||||
|
data: mutation.data,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// ========================================
|
||||||
|
// Config Hooks
|
||||||
|
// ========================================
|
||||||
|
|
||||||
|
export function useApiConfig() {
|
||||||
|
return useQuery({
|
||||||
|
queryKey: [...apiSettingsKeys.all, 'config'],
|
||||||
|
queryFn: fetchApiConfig,
|
||||||
|
staleTime: STALE_TIME,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export function useSyncApiConfig() {
|
||||||
|
return useMutation({
|
||||||
|
mutationFn: () => syncApiConfig(),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export function usePreviewYamlConfig() {
|
||||||
|
return useMutation({
|
||||||
|
mutationFn: () => previewYamlConfig(),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// ========================================
|
||||||
|
// CCW-LiteLLM Package Hooks
|
||||||
|
// ========================================
|
||||||
|
|
||||||
|
export interface UseCcwLitellmStatusOptions {
|
||||||
|
staleTime?: number;
|
||||||
|
enabled?: boolean;
|
||||||
|
refresh?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function useCcwLitellmStatus(options: UseCcwLitellmStatusOptions = {}) {
|
||||||
|
const { staleTime = 5 * 60 * 1000, enabled = true, refresh = false } = options;
|
||||||
|
|
||||||
|
return useQuery({
|
||||||
|
queryKey: [...apiSettingsKeys.ccwLitellm(), 'status', refresh],
|
||||||
|
queryFn: () => checkCcwLitellmStatus(refresh),
|
||||||
|
staleTime,
|
||||||
|
enabled,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export function useInstallCcwLitellm() {
|
||||||
|
const queryClient = useQueryClient();
|
||||||
|
|
||||||
|
const mutation = useMutation({
|
||||||
|
mutationFn: () => installCcwLitellm(),
|
||||||
|
onSuccess: () => {
|
||||||
|
queryClient.invalidateQueries({ queryKey: apiSettingsKeys.ccwLitellm() });
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
return {
|
||||||
|
install: mutation.mutateAsync,
|
||||||
|
isInstalling: mutation.isPending,
|
||||||
|
error: mutation.error,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export function useUninstallCcwLitellm() {
|
||||||
|
const queryClient = useQueryClient();
|
||||||
|
|
||||||
|
const mutation = useMutation({
|
||||||
|
mutationFn: () => uninstallCcwLitellm(),
|
||||||
|
onSuccess: () => {
|
||||||
|
queryClient.invalidateQueries({ queryKey: apiSettingsKeys.ccwLitellm() });
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
return {
|
||||||
|
uninstall: mutation.mutateAsync,
|
||||||
|
isUninstalling: mutation.isPending,
|
||||||
|
error: mutation.error,
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -26,6 +26,14 @@ import {
|
|||||||
resetCodexLensGpu,
|
resetCodexLensGpu,
|
||||||
fetchCodexLensIgnorePatterns,
|
fetchCodexLensIgnorePatterns,
|
||||||
updateCodexLensIgnorePatterns,
|
updateCodexLensIgnorePatterns,
|
||||||
|
searchCodexLens,
|
||||||
|
searchFilesCodexLens,
|
||||||
|
searchSymbolCodexLens,
|
||||||
|
fetchCodexLensIndexes,
|
||||||
|
rebuildCodexLensIndex,
|
||||||
|
updateCodexLensIndex,
|
||||||
|
cancelCodexLensIndexing,
|
||||||
|
checkCodexLensIndexingStatus,
|
||||||
type CodexLensDashboardInitResponse,
|
type CodexLensDashboardInitResponse,
|
||||||
type CodexLensVenvStatus,
|
type CodexLensVenvStatus,
|
||||||
type CodexLensConfig,
|
type CodexLensConfig,
|
||||||
@@ -39,6 +47,11 @@ import {
|
|||||||
type CodexLensUpdateEnvRequest,
|
type CodexLensUpdateEnvRequest,
|
||||||
type CodexLensUpdateIgnorePatternsRequest,
|
type CodexLensUpdateIgnorePatternsRequest,
|
||||||
type CodexLensWorkspaceStatus,
|
type CodexLensWorkspaceStatus,
|
||||||
|
type CodexLensSearchParams,
|
||||||
|
type CodexLensSearchResponse,
|
||||||
|
type CodexLensSymbolSearchResponse,
|
||||||
|
type CodexLensIndexesResponse,
|
||||||
|
type CodexLensIndexingStatusResponse,
|
||||||
} from '../lib/api';
|
} from '../lib/api';
|
||||||
import { useWorkflowStore, selectProjectPath } from '@/stores/workflowStore';
|
import { useWorkflowStore, selectProjectPath } from '@/stores/workflowStore';
|
||||||
|
|
||||||
@@ -56,6 +69,11 @@ export const codexLensKeys = {
|
|||||||
gpuList: () => [...codexLensKeys.gpu(), 'list'] as const,
|
gpuList: () => [...codexLensKeys.gpu(), 'list'] as const,
|
||||||
gpuDetect: () => [...codexLensKeys.gpu(), 'detect'] as const,
|
gpuDetect: () => [...codexLensKeys.gpu(), 'detect'] as const,
|
||||||
ignorePatterns: () => [...codexLensKeys.all, 'ignorePatterns'] as const,
|
ignorePatterns: () => [...codexLensKeys.all, 'ignorePatterns'] as const,
|
||||||
|
indexes: () => [...codexLensKeys.all, 'indexes'] as const,
|
||||||
|
indexingStatus: () => [...codexLensKeys.all, 'indexingStatus'] as const,
|
||||||
|
search: (params: CodexLensSearchParams) => [...codexLensKeys.all, 'search', params] as const,
|
||||||
|
filesSearch: (params: CodexLensSearchParams) => [...codexLensKeys.all, 'filesSearch', params] as const,
|
||||||
|
symbolSearch: (params: Pick<CodexLensSearchParams, 'query' | 'limit'>) => [...codexLensKeys.all, 'symbolSearch', params] as const,
|
||||||
};
|
};
|
||||||
|
|
||||||
// Default stale times
|
// Default stale times
|
||||||
@@ -715,6 +733,189 @@ export function useUpdateIgnorePatterns(): UseUpdateIgnorePatternsReturn {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ========== Index Management Hooks ==========
|
||||||
|
|
||||||
|
export interface UseCodexLensIndexesOptions {
|
||||||
|
enabled?: boolean;
|
||||||
|
staleTime?: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface UseCodexLensIndexesReturn {
|
||||||
|
data: CodexLensIndexesResponse | undefined;
|
||||||
|
indexes: CodexLensIndexesResponse['indexes'] | undefined;
|
||||||
|
isLoading: boolean;
|
||||||
|
error: Error | null;
|
||||||
|
refetch: () => Promise<void>;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Hook for fetching CodexLens indexes
|
||||||
|
*/
|
||||||
|
export function useCodexLensIndexes(options: UseCodexLensIndexesOptions = {}): UseCodexLensIndexesReturn {
|
||||||
|
const { enabled = true, staleTime = STALE_TIME_MEDIUM } = options;
|
||||||
|
|
||||||
|
const query = useQuery({
|
||||||
|
queryKey: codexLensKeys.indexes(),
|
||||||
|
queryFn: fetchCodexLensIndexes,
|
||||||
|
staleTime,
|
||||||
|
enabled,
|
||||||
|
retry: 2,
|
||||||
|
});
|
||||||
|
|
||||||
|
const refetch = async () => {
|
||||||
|
await query.refetch();
|
||||||
|
};
|
||||||
|
|
||||||
|
return {
|
||||||
|
data: query.data,
|
||||||
|
indexes: query.data?.indexes,
|
||||||
|
isLoading: query.isLoading,
|
||||||
|
error: query.error,
|
||||||
|
refetch,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface UseCodexLensIndexingStatusReturn {
|
||||||
|
data: CodexLensIndexingStatusResponse | undefined;
|
||||||
|
inProgress: boolean;
|
||||||
|
isLoading: boolean;
|
||||||
|
error: Error | null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Hook for checking CodexLens indexing status
|
||||||
|
*/
|
||||||
|
export function useCodexLensIndexingStatus(): UseCodexLensIndexingStatusReturn {
|
||||||
|
const query = useQuery({
|
||||||
|
queryKey: codexLensKeys.indexingStatus(),
|
||||||
|
queryFn: checkCodexLensIndexingStatus,
|
||||||
|
staleTime: STALE_TIME_SHORT,
|
||||||
|
refetchInterval: (data) => (data?.inProgress ? 2000 : false), // Poll every 2s when indexing
|
||||||
|
retry: false,
|
||||||
|
});
|
||||||
|
|
||||||
|
return {
|
||||||
|
data: query.data,
|
||||||
|
inProgress: query.data?.inProgress ?? false,
|
||||||
|
isLoading: query.isLoading,
|
||||||
|
error: query.error,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface UseRebuildIndexReturn {
|
||||||
|
rebuildIndex: (projectPath: string, options?: {
|
||||||
|
indexType?: 'normal' | 'vector';
|
||||||
|
embeddingModel?: string;
|
||||||
|
embeddingBackend?: 'fastembed' | 'litellm';
|
||||||
|
maxWorkers?: number;
|
||||||
|
}) => Promise<{ success: boolean; message?: string; error?: string }>;
|
||||||
|
isRebuilding: boolean;
|
||||||
|
error: Error | null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Hook for rebuilding CodexLens index (full rebuild)
|
||||||
|
*/
|
||||||
|
export function useRebuildIndex(): UseRebuildIndexReturn {
|
||||||
|
const queryClient = useQueryClient();
|
||||||
|
|
||||||
|
const mutation = useMutation({
|
||||||
|
mutationFn: async ({
|
||||||
|
projectPath,
|
||||||
|
options = {},
|
||||||
|
}: {
|
||||||
|
projectPath: string;
|
||||||
|
options?: {
|
||||||
|
indexType?: 'normal' | 'vector';
|
||||||
|
embeddingModel?: string;
|
||||||
|
embeddingBackend?: 'fastembed' | 'litellm';
|
||||||
|
maxWorkers?: number;
|
||||||
|
};
|
||||||
|
}) => rebuildCodexLensIndex(projectPath, options),
|
||||||
|
onSuccess: () => {
|
||||||
|
queryClient.invalidateQueries({ queryKey: codexLensKeys.indexes() });
|
||||||
|
queryClient.invalidateQueries({ queryKey: codexLensKeys.dashboard() });
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
return {
|
||||||
|
rebuildIndex: (projectPath, options) =>
|
||||||
|
mutation.mutateAsync({ projectPath, options }),
|
||||||
|
isRebuilding: mutation.isPending,
|
||||||
|
error: mutation.error,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface UseUpdateIndexReturn {
|
||||||
|
updateIndex: (projectPath: string, options?: {
|
||||||
|
indexType?: 'normal' | 'vector';
|
||||||
|
embeddingModel?: string;
|
||||||
|
embeddingBackend?: 'fastembed' | 'litellm';
|
||||||
|
maxWorkers?: number;
|
||||||
|
}) => Promise<{ success: boolean; message?: string; error?: string }>;
|
||||||
|
isUpdating: boolean;
|
||||||
|
error: Error | null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Hook for updating CodexLens index (incremental update)
|
||||||
|
*/
|
||||||
|
export function useUpdateIndex(): UseUpdateIndexReturn {
|
||||||
|
const queryClient = useQueryClient();
|
||||||
|
|
||||||
|
const mutation = useMutation({
|
||||||
|
mutationFn: async ({
|
||||||
|
projectPath,
|
||||||
|
options = {},
|
||||||
|
}: {
|
||||||
|
projectPath: string;
|
||||||
|
options?: {
|
||||||
|
indexType?: 'normal' | 'vector';
|
||||||
|
embeddingModel?: string;
|
||||||
|
embeddingBackend?: 'fastembed' | 'litellm';
|
||||||
|
maxWorkers?: number;
|
||||||
|
};
|
||||||
|
}) => updateCodexLensIndex(projectPath, options),
|
||||||
|
onSuccess: () => {
|
||||||
|
queryClient.invalidateQueries({ queryKey: codexLensKeys.indexes() });
|
||||||
|
queryClient.invalidateQueries({ queryKey: codexLensKeys.dashboard() });
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
return {
|
||||||
|
updateIndex: (projectPath, options) =>
|
||||||
|
mutation.mutateAsync({ projectPath, options }),
|
||||||
|
isUpdating: mutation.isPending,
|
||||||
|
error: mutation.error,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface UseCancelIndexingReturn {
|
||||||
|
cancelIndexing: () => Promise<{ success: boolean; error?: string }>;
|
||||||
|
isCancelling: boolean;
|
||||||
|
error: Error | null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Hook for canceling CodexLens indexing
|
||||||
|
*/
|
||||||
|
export function useCancelIndexing(): UseCancelIndexingReturn {
|
||||||
|
const queryClient = useQueryClient();
|
||||||
|
|
||||||
|
const mutation = useMutation({
|
||||||
|
mutationFn: cancelCodexLensIndexing,
|
||||||
|
onSuccess: () => {
|
||||||
|
queryClient.invalidateQueries({ queryKey: codexLensKeys.indexingStatus() });
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
return {
|
||||||
|
cancelIndexing: mutation.mutateAsync,
|
||||||
|
isCancelling: mutation.isPending,
|
||||||
|
error: mutation.error,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Combined hook for all CodexLens mutations
|
* Combined hook for all CodexLens mutations
|
||||||
*/
|
*/
|
||||||
@@ -727,6 +928,9 @@ export function useCodexLensMutations() {
|
|||||||
const updateEnv = useUpdateCodexLensEnv();
|
const updateEnv = useUpdateCodexLensEnv();
|
||||||
const gpu = useSelectGpu();
|
const gpu = useSelectGpu();
|
||||||
const updatePatterns = useUpdateIgnorePatterns();
|
const updatePatterns = useUpdateIgnorePatterns();
|
||||||
|
const rebuildIndex = useRebuildIndex();
|
||||||
|
const updateIndex = useUpdateIndex();
|
||||||
|
const cancelIndexing = useCancelIndexing();
|
||||||
|
|
||||||
return {
|
return {
|
||||||
updateConfig: updateConfig.updateConfig,
|
updateConfig: updateConfig.updateConfig,
|
||||||
@@ -748,6 +952,12 @@ export function useCodexLensMutations() {
|
|||||||
isSelectingGpu: gpu.isSelecting || gpu.isResetting,
|
isSelectingGpu: gpu.isSelecting || gpu.isResetting,
|
||||||
updatePatterns: updatePatterns.updatePatterns,
|
updatePatterns: updatePatterns.updatePatterns,
|
||||||
isUpdatingPatterns: updatePatterns.isUpdating,
|
isUpdatingPatterns: updatePatterns.isUpdating,
|
||||||
|
rebuildIndex: rebuildIndex.rebuildIndex,
|
||||||
|
isRebuildingIndex: rebuildIndex.isRebuilding,
|
||||||
|
updateIndex: updateIndex.updateIndex,
|
||||||
|
isUpdatingIndex: updateIndex.isUpdating,
|
||||||
|
cancelIndexing: cancelIndexing.cancelIndexing,
|
||||||
|
isCancellingIndexing: cancelIndexing.isCancelling,
|
||||||
isMutating:
|
isMutating:
|
||||||
updateConfig.isUpdating ||
|
updateConfig.isUpdating ||
|
||||||
bootstrap.isBootstrapping ||
|
bootstrap.isBootstrapping ||
|
||||||
@@ -757,6 +967,119 @@ export function useCodexLensMutations() {
|
|||||||
updateEnv.isUpdating ||
|
updateEnv.isUpdating ||
|
||||||
gpu.isSelecting ||
|
gpu.isSelecting ||
|
||||||
gpu.isResetting ||
|
gpu.isResetting ||
|
||||||
updatePatterns.isUpdating,
|
updatePatterns.isUpdating ||
|
||||||
|
rebuildIndex.isRebuilding ||
|
||||||
|
updateIndex.isUpdating ||
|
||||||
|
cancelIndexing.isCancelling,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// ========== Search Hooks ==========
|
||||||
|
|
||||||
|
export interface UseCodexLensSearchOptions {
|
||||||
|
enabled?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface UseCodexLensSearchReturn {
|
||||||
|
data: CodexLensSearchResponse | undefined;
|
||||||
|
results: CodexLensSearchResponse['results'] | undefined;
|
||||||
|
isLoading: boolean;
|
||||||
|
error: Error | null;
|
||||||
|
refetch: () => Promise<void>;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Hook for content search using CodexLens
|
||||||
|
*/
|
||||||
|
export function useCodexLensSearch(params: CodexLensSearchParams, options: UseCodexLensSearchOptions = {}): UseCodexLensSearchReturn {
|
||||||
|
const { enabled = false } = options;
|
||||||
|
|
||||||
|
const query = useQuery({
|
||||||
|
queryKey: codexLensKeys.search(params),
|
||||||
|
queryFn: () => searchCodexLens(params),
|
||||||
|
enabled,
|
||||||
|
staleTime: STALE_TIME_SHORT,
|
||||||
|
retry: 1,
|
||||||
|
});
|
||||||
|
|
||||||
|
const refetch = async () => {
|
||||||
|
await query.refetch();
|
||||||
|
};
|
||||||
|
|
||||||
|
return {
|
||||||
|
data: query.data,
|
||||||
|
results: query.data?.results,
|
||||||
|
isLoading: query.isLoading,
|
||||||
|
error: query.error,
|
||||||
|
refetch,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Hook for file search using CodexLens
|
||||||
|
*/
|
||||||
|
export function useCodexLensFilesSearch(params: CodexLensSearchParams, options: UseCodexLensSearchOptions = {}): UseCodexLensSearchReturn {
|
||||||
|
const { enabled = false } = options;
|
||||||
|
|
||||||
|
const query = useQuery({
|
||||||
|
queryKey: codexLensKeys.filesSearch(params),
|
||||||
|
queryFn: () => searchFilesCodexLens(params),
|
||||||
|
enabled,
|
||||||
|
staleTime: STALE_TIME_SHORT,
|
||||||
|
retry: 1,
|
||||||
|
});
|
||||||
|
|
||||||
|
const refetch = async () => {
|
||||||
|
await query.refetch();
|
||||||
|
};
|
||||||
|
|
||||||
|
return {
|
||||||
|
data: query.data,
|
||||||
|
results: query.data?.results,
|
||||||
|
isLoading: query.isLoading,
|
||||||
|
error: query.error,
|
||||||
|
refetch,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface UseCodexLensSymbolSearchOptions {
|
||||||
|
enabled?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface UseCodexLensSymbolSearchReturn {
|
||||||
|
data: CodexLensSymbolSearchResponse | undefined;
|
||||||
|
symbols: CodexLensSymbolSearchResponse['symbols'] | undefined;
|
||||||
|
isLoading: boolean;
|
||||||
|
error: Error | null;
|
||||||
|
refetch: () => Promise<void>;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Hook for symbol search using CodexLens
|
||||||
|
*/
|
||||||
|
export function useCodexLensSymbolSearch(
|
||||||
|
params: Pick<CodexLensSearchParams, 'query' | 'limit'>,
|
||||||
|
options: UseCodexLensSymbolSearchOptions = {}
|
||||||
|
): UseCodexLensSymbolSearchReturn {
|
||||||
|
const { enabled = false } = options;
|
||||||
|
|
||||||
|
const query = useQuery({
|
||||||
|
queryKey: codexLensKeys.symbolSearch(params),
|
||||||
|
queryFn: () => searchSymbolCodexLens(params),
|
||||||
|
enabled,
|
||||||
|
staleTime: STALE_TIME_SHORT,
|
||||||
|
retry: 1,
|
||||||
|
});
|
||||||
|
|
||||||
|
const refetch = async () => {
|
||||||
|
await query.refetch();
|
||||||
|
};
|
||||||
|
|
||||||
|
return {
|
||||||
|
data: query.data,
|
||||||
|
symbols: query.data?.symbols,
|
||||||
|
isLoading: query.isLoading,
|
||||||
|
error: query.error,
|
||||||
|
refetch,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,8 +3,8 @@
|
|||||||
// ========================================
|
// ========================================
|
||||||
// TanStack Query hook for project overview data
|
// TanStack Query hook for project overview data
|
||||||
|
|
||||||
import { useQuery } from '@tanstack/react-query';
|
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
|
||||||
import { fetchProjectOverview } from '../lib/api';
|
import { fetchProjectOverview, updateProjectGuidelines, type ProjectGuidelines } from '../lib/api';
|
||||||
import { useWorkflowStore, selectProjectPath } from '@/stores/workflowStore';
|
import { useWorkflowStore, selectProjectPath } from '@/stores/workflowStore';
|
||||||
|
|
||||||
// Query key factory
|
// Query key factory
|
||||||
@@ -53,3 +53,39 @@ export function useProjectOverview(options: UseProjectOverviewOptions = {}) {
|
|||||||
refetch: query.refetch,
|
refetch: query.refetch,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ========== Mutations ==========
|
||||||
|
|
||||||
|
export interface UseUpdateGuidelinesReturn {
|
||||||
|
updateGuidelines: (guidelines: ProjectGuidelines) => Promise<{ success: boolean; guidelines?: ProjectGuidelines; error?: string }>;
|
||||||
|
isUpdating: boolean;
|
||||||
|
error: Error | null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Hook for updating project guidelines
|
||||||
|
*
|
||||||
|
* @example
|
||||||
|
* ```tsx
|
||||||
|
* const { updateGuidelines, isUpdating } = useUpdateGuidelines();
|
||||||
|
* await updateGuidelines({ conventions: { ... }, constraints: { ... } });
|
||||||
|
* ```
|
||||||
|
*/
|
||||||
|
export function useUpdateGuidelines(): UseUpdateGuidelinesReturn {
|
||||||
|
const queryClient = useQueryClient();
|
||||||
|
const projectPath = useWorkflowStore(selectProjectPath);
|
||||||
|
|
||||||
|
const mutation = useMutation({
|
||||||
|
mutationFn: (guidelines: ProjectGuidelines) => updateProjectGuidelines(guidelines, projectPath),
|
||||||
|
onSuccess: () => {
|
||||||
|
// Invalidate project overview cache to trigger refetch
|
||||||
|
queryClient.invalidateQueries({ queryKey: projectOverviewKeys.detail() });
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
return {
|
||||||
|
updateGuidelines: mutation.mutateAsync,
|
||||||
|
isUpdating: mutation.isPending,
|
||||||
|
error: mutation.error,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|||||||
@@ -1249,6 +1249,11 @@ export interface ProjectGuidelines {
|
|||||||
constraints?: Record<string, string[]>;
|
constraints?: Record<string, string[]>;
|
||||||
quality_rules?: GuidelineEntry[];
|
quality_rules?: GuidelineEntry[];
|
||||||
learnings?: LearningEntry[];
|
learnings?: LearningEntry[];
|
||||||
|
_metadata?: {
|
||||||
|
created_at?: string;
|
||||||
|
updated_at?: string;
|
||||||
|
version?: string;
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface ProjectOverviewMetadata {
|
export interface ProjectOverviewMetadata {
|
||||||
@@ -1285,6 +1290,23 @@ export async function fetchProjectOverview(projectPath?: string): Promise<Projec
|
|||||||
return data.projectOverview ?? null;
|
return data.projectOverview ?? null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Update project guidelines for a specific workspace
|
||||||
|
*/
|
||||||
|
export async function updateProjectGuidelines(
|
||||||
|
guidelines: ProjectGuidelines,
|
||||||
|
projectPath?: string
|
||||||
|
): Promise<{ success: boolean; guidelines?: ProjectGuidelines; error?: string }> {
|
||||||
|
const url = projectPath
|
||||||
|
? `/api/ccw/guidelines?path=${encodeURIComponent(projectPath)}`
|
||||||
|
: '/api/ccw/guidelines';
|
||||||
|
return fetchApi(url, {
|
||||||
|
method: 'PUT',
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
body: JSON.stringify(guidelines),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
// ========== Session Detail API ==========
|
// ========== Session Detail API ==========
|
||||||
|
|
||||||
export interface SessionDetailContext {
|
export interface SessionDetailContext {
|
||||||
@@ -2939,3 +2961,727 @@ export async function updateCodexLensIgnorePatterns(request: CodexLensUpdateIgno
|
|||||||
body: JSON.stringify(request),
|
body: JSON.stringify(request),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ========== CodexLens Search API ==========
|
||||||
|
|
||||||
|
/**
|
||||||
|
* CodexLens search request parameters
|
||||||
|
*/
|
||||||
|
export interface CodexLensSearchParams {
|
||||||
|
query: string;
|
||||||
|
limit?: number;
|
||||||
|
mode?: 'dense_rerank' | 'fts' | 'fuzzy';
|
||||||
|
max_content_length?: number;
|
||||||
|
extra_files_count?: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* CodexLens search result
|
||||||
|
*/
|
||||||
|
export interface CodexLensSearchResult {
|
||||||
|
path: string;
|
||||||
|
score: number;
|
||||||
|
content?: string;
|
||||||
|
line_start?: number;
|
||||||
|
line_end?: number;
|
||||||
|
[key: string]: unknown;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* CodexLens search response
|
||||||
|
*/
|
||||||
|
export interface CodexLensSearchResponse {
|
||||||
|
success: boolean;
|
||||||
|
results: CodexLensSearchResult[];
|
||||||
|
total?: number;
|
||||||
|
query: string;
|
||||||
|
error?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* CodexLens symbol search response
|
||||||
|
*/
|
||||||
|
export interface CodexLensSymbolSearchResponse {
|
||||||
|
success: boolean;
|
||||||
|
symbols: Array<{
|
||||||
|
name: string;
|
||||||
|
kind: string;
|
||||||
|
path: string;
|
||||||
|
line: number;
|
||||||
|
[key: string]: unknown;
|
||||||
|
}>;
|
||||||
|
error?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Perform content search using CodexLens
|
||||||
|
*/
|
||||||
|
export async function searchCodexLens(params: CodexLensSearchParams): Promise<CodexLensSearchResponse> {
|
||||||
|
const queryParams = new URLSearchParams();
|
||||||
|
queryParams.append('query', params.query);
|
||||||
|
if (params.limit) queryParams.append('limit', String(params.limit));
|
||||||
|
if (params.mode) queryParams.append('mode', params.mode);
|
||||||
|
if (params.max_content_length) queryParams.append('max_content_length', String(params.max_content_length));
|
||||||
|
if (params.extra_files_count) queryParams.append('extra_files_count', String(params.extra_files_count));
|
||||||
|
|
||||||
|
return fetchApi<CodexLensSearchResponse>(`/api/codexlens/search?${queryParams.toString()}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Perform file search using CodexLens
|
||||||
|
*/
|
||||||
|
export async function searchFilesCodexLens(params: CodexLensSearchParams): Promise<CodexLensSearchResponse> {
|
||||||
|
const queryParams = new URLSearchParams();
|
||||||
|
queryParams.append('query', params.query);
|
||||||
|
if (params.limit) queryParams.append('limit', String(params.limit));
|
||||||
|
if (params.mode) queryParams.append('mode', params.mode);
|
||||||
|
if (params.max_content_length) queryParams.append('max_content_length', String(params.max_content_length));
|
||||||
|
if (params.extra_files_count) queryParams.append('extra_files_count', String(params.extra_files_count));
|
||||||
|
|
||||||
|
return fetchApi<CodexLensSearchResponse>(`/api/codexlens/search_files?${queryParams.toString()}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Perform symbol search using CodexLens
|
||||||
|
*/
|
||||||
|
export async function searchSymbolCodexLens(params: Pick<CodexLensSearchParams, 'query' | 'limit'>): Promise<CodexLensSymbolSearchResponse> {
|
||||||
|
const queryParams = new URLSearchParams();
|
||||||
|
queryParams.append('query', params.query);
|
||||||
|
if (params.limit) queryParams.append('limit', String(params.limit));
|
||||||
|
|
||||||
|
return fetchApi<CodexLensSymbolSearchResponse>(`/api/codexlens/symbol?${queryParams.toString()}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ========== CodexLens Index Management API ==========
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Index operation type
|
||||||
|
*/
|
||||||
|
export type CodexLensIndexOperation = 'fts_full' | 'fts_incremental' | 'vector_full' | 'vector_incremental';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* CodexLens index entry
|
||||||
|
*/
|
||||||
|
export interface CodexLensIndex {
|
||||||
|
id: string;
|
||||||
|
path: string;
|
||||||
|
indexPath: string;
|
||||||
|
size: number;
|
||||||
|
sizeFormatted: string;
|
||||||
|
fileCount: number;
|
||||||
|
dirCount: number;
|
||||||
|
hasVectorIndex: boolean;
|
||||||
|
hasNormalIndex: boolean;
|
||||||
|
status: string;
|
||||||
|
lastModified: string | null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* CodexLens index list response
|
||||||
|
*/
|
||||||
|
export interface CodexLensIndexesResponse {
|
||||||
|
success: boolean;
|
||||||
|
indexDir: string;
|
||||||
|
indexes: CodexLensIndex[];
|
||||||
|
summary: {
|
||||||
|
totalSize: number;
|
||||||
|
totalSizeFormatted: string;
|
||||||
|
vectorIndexCount: number;
|
||||||
|
normalIndexCount: number;
|
||||||
|
totalProjects?: number;
|
||||||
|
totalFiles?: number;
|
||||||
|
totalDirs?: number;
|
||||||
|
indexSizeBytes?: number;
|
||||||
|
indexSizeMb?: number;
|
||||||
|
embeddings?: any;
|
||||||
|
fullIndexDirSize?: number;
|
||||||
|
fullIndexDirSizeFormatted?: string;
|
||||||
|
};
|
||||||
|
error?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* CodexLens index operation request
|
||||||
|
*/
|
||||||
|
export interface CodexLensIndexOperationRequest {
|
||||||
|
path: string;
|
||||||
|
operation: CodexLensIndexOperation;
|
||||||
|
indexType?: 'normal' | 'vector';
|
||||||
|
embeddingModel?: string;
|
||||||
|
embeddingBackend?: 'fastembed' | 'litellm';
|
||||||
|
maxWorkers?: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* CodexLens index operation response
|
||||||
|
*/
|
||||||
|
export interface CodexLensIndexOperationResponse {
|
||||||
|
success: boolean;
|
||||||
|
message?: string;
|
||||||
|
error?: string;
|
||||||
|
result?: any;
|
||||||
|
output?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* CodexLens indexing status response
|
||||||
|
*/
|
||||||
|
export interface CodexLensIndexingStatusResponse {
|
||||||
|
success: boolean;
|
||||||
|
inProgress: boolean;
|
||||||
|
error?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Fetch all CodexLens indexes
|
||||||
|
*/
|
||||||
|
export async function fetchCodexLensIndexes(): Promise<CodexLensIndexesResponse> {
|
||||||
|
return fetchApi<CodexLensIndexesResponse>('/api/codexlens/indexes');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Rebuild CodexLens index (full rebuild)
|
||||||
|
* @param projectPath - Project path to index
|
||||||
|
* @param options - Index options
|
||||||
|
*/
|
||||||
|
export async function rebuildCodexLensIndex(
|
||||||
|
projectPath: string,
|
||||||
|
options: {
|
||||||
|
indexType?: 'normal' | 'vector';
|
||||||
|
embeddingModel?: string;
|
||||||
|
embeddingBackend?: 'fastembed' | 'litellm';
|
||||||
|
maxWorkers?: number;
|
||||||
|
} = {}
|
||||||
|
): Promise<CodexLensIndexOperationResponse> {
|
||||||
|
return fetchApi<CodexLensIndexOperationResponse>('/api/codexlens/init', {
|
||||||
|
method: 'POST',
|
||||||
|
body: JSON.stringify({
|
||||||
|
path: projectPath,
|
||||||
|
indexType: options.indexType || 'vector',
|
||||||
|
embeddingModel: options.embeddingModel || 'code',
|
||||||
|
embeddingBackend: options.embeddingBackend || 'fastembed',
|
||||||
|
maxWorkers: options.maxWorkers || 1
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Incremental update CodexLens index
|
||||||
|
* @param projectPath - Project path to update
|
||||||
|
* @param options - Index options
|
||||||
|
*/
|
||||||
|
export async function updateCodexLensIndex(
|
||||||
|
projectPath: string,
|
||||||
|
options: {
|
||||||
|
indexType?: 'normal' | 'vector';
|
||||||
|
embeddingModel?: string;
|
||||||
|
embeddingBackend?: 'fastembed' | 'litellm';
|
||||||
|
maxWorkers?: number;
|
||||||
|
} = {}
|
||||||
|
): Promise<CodexLensIndexOperationResponse> {
|
||||||
|
return fetchApi<CodexLensIndexOperationResponse>('/api/codexlens/update', {
|
||||||
|
method: 'POST',
|
||||||
|
body: JSON.stringify({
|
||||||
|
path: projectPath,
|
||||||
|
indexType: options.indexType || 'vector',
|
||||||
|
embeddingModel: options.embeddingModel || 'code',
|
||||||
|
embeddingBackend: options.embeddingBackend || 'fastembed',
|
||||||
|
maxWorkers: options.maxWorkers || 1
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Cancel ongoing CodexLens indexing
|
||||||
|
*/
|
||||||
|
export async function cancelCodexLensIndexing(): Promise<{ success: boolean; error?: string }> {
|
||||||
|
return fetchApi('/api/codexlens/cancel', {
|
||||||
|
method: 'POST',
|
||||||
|
body: JSON.stringify({}),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if CodexLens indexing is in progress
|
||||||
|
*/
|
||||||
|
export async function checkCodexLensIndexingStatus(): Promise<CodexLensIndexingStatusResponse> {
|
||||||
|
return fetchApi<CodexLensIndexingStatusResponse>('/api/codexlens/indexing-status');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Clean CodexLens indexes
|
||||||
|
* @param options - Clean options
|
||||||
|
*/
|
||||||
|
export async function cleanCodexLensIndexes(options: {
|
||||||
|
all?: boolean;
|
||||||
|
path?: string;
|
||||||
|
} = {}): Promise<{ success: boolean; message?: string; error?: string }> {
|
||||||
|
return fetchApi('/api/codexlens/clean', {
|
||||||
|
method: 'POST',
|
||||||
|
body: JSON.stringify(options),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// ========== LiteLLM API Settings API ==========
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Provider credential types
|
||||||
|
*/
|
||||||
|
export type ProviderType = 'openai' | 'anthropic' | 'custom';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Advanced provider settings
|
||||||
|
*/
|
||||||
|
export interface ProviderAdvancedSettings {
|
||||||
|
timeout?: number;
|
||||||
|
maxRetries?: number;
|
||||||
|
organization?: string;
|
||||||
|
apiVersion?: string;
|
||||||
|
customHeaders?: Record<string, string>;
|
||||||
|
rpm?: number;
|
||||||
|
tpm?: number;
|
||||||
|
proxy?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Routing strategy types
|
||||||
|
*/
|
||||||
|
export type RoutingStrategy = 'simple-shuffle' | 'weighted' | 'latency-based' | 'cost-based' | 'least-busy';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Individual API key entry
|
||||||
|
*/
|
||||||
|
export interface ApiKeyEntry {
|
||||||
|
id: string;
|
||||||
|
key: string;
|
||||||
|
label?: string;
|
||||||
|
weight?: number;
|
||||||
|
enabled: boolean;
|
||||||
|
healthStatus?: 'healthy' | 'unhealthy' | 'unknown';
|
||||||
|
lastHealthCheck?: string;
|
||||||
|
lastError?: string;
|
||||||
|
lastLatencyMs?: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Health check configuration
|
||||||
|
*/
|
||||||
|
export interface HealthCheckConfig {
|
||||||
|
enabled: boolean;
|
||||||
|
intervalSeconds: number;
|
||||||
|
cooldownSeconds: number;
|
||||||
|
failureThreshold: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Model capabilities
|
||||||
|
*/
|
||||||
|
export interface ModelCapabilities {
|
||||||
|
streaming?: boolean;
|
||||||
|
functionCalling?: boolean;
|
||||||
|
vision?: boolean;
|
||||||
|
contextWindow?: number;
|
||||||
|
embeddingDimension?: number;
|
||||||
|
maxOutputTokens?: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Model endpoint settings
|
||||||
|
*/
|
||||||
|
export interface ModelEndpointSettings {
|
||||||
|
baseUrl?: string;
|
||||||
|
timeout?: number;
|
||||||
|
maxRetries?: number;
|
||||||
|
customHeaders?: Record<string, string>;
|
||||||
|
cacheStrategy?: CacheStrategy;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Model definition
|
||||||
|
*/
|
||||||
|
export interface ModelDefinition {
|
||||||
|
id: string;
|
||||||
|
name: string;
|
||||||
|
type: 'llm' | 'embedding' | 'reranker';
|
||||||
|
series: string;
|
||||||
|
enabled: boolean;
|
||||||
|
capabilities?: ModelCapabilities;
|
||||||
|
endpointSettings?: ModelEndpointSettings;
|
||||||
|
description?: string;
|
||||||
|
createdAt: string;
|
||||||
|
updatedAt: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Provider credential
|
||||||
|
*/
|
||||||
|
export interface ProviderCredential {
|
||||||
|
id: string;
|
||||||
|
name: string;
|
||||||
|
type: ProviderType;
|
||||||
|
apiKey: string;
|
||||||
|
apiBase?: string;
|
||||||
|
enabled: boolean;
|
||||||
|
advancedSettings?: ProviderAdvancedSettings;
|
||||||
|
apiKeys?: ApiKeyEntry[];
|
||||||
|
routingStrategy?: RoutingStrategy;
|
||||||
|
healthCheck?: HealthCheckConfig;
|
||||||
|
llmModels?: ModelDefinition[];
|
||||||
|
embeddingModels?: ModelDefinition[];
|
||||||
|
rerankerModels?: ModelDefinition[];
|
||||||
|
createdAt: string;
|
||||||
|
updatedAt: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Cache strategy
|
||||||
|
*/
|
||||||
|
export interface CacheStrategy {
|
||||||
|
enabled: boolean;
|
||||||
|
ttlMinutes: number;
|
||||||
|
maxSizeKB: number;
|
||||||
|
filePatterns: string[];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Custom endpoint
|
||||||
|
*/
|
||||||
|
export interface CustomEndpoint {
|
||||||
|
id: string;
|
||||||
|
name: string;
|
||||||
|
providerId: string;
|
||||||
|
model: string;
|
||||||
|
description?: string;
|
||||||
|
cacheStrategy: CacheStrategy;
|
||||||
|
enabled: boolean;
|
||||||
|
createdAt: string;
|
||||||
|
updatedAt: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Global cache settings
|
||||||
|
*/
|
||||||
|
export interface GlobalCacheSettings {
|
||||||
|
enabled: boolean;
|
||||||
|
cacheDir: string;
|
||||||
|
maxTotalSizeMB: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Cache statistics
|
||||||
|
*/
|
||||||
|
export interface CacheStats {
|
||||||
|
totalSize: number;
|
||||||
|
maxSize: number;
|
||||||
|
entries: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Model pool type
|
||||||
|
*/
|
||||||
|
export type ModelPoolType = 'embedding' | 'llm' | 'reranker';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Model pool config
|
||||||
|
*/
|
||||||
|
export interface ModelPoolConfig {
|
||||||
|
id: string;
|
||||||
|
modelType: ModelPoolType;
|
||||||
|
enabled: boolean;
|
||||||
|
targetModel: string;
|
||||||
|
strategy: 'round_robin' | 'latency_aware' | 'weighted_random';
|
||||||
|
autoDiscover: boolean;
|
||||||
|
excludedProviderIds?: string[];
|
||||||
|
defaultCooldown: number;
|
||||||
|
defaultMaxConcurrentPerKey: number;
|
||||||
|
name?: string;
|
||||||
|
description?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Provider for model pool discovery
|
||||||
|
*/
|
||||||
|
export interface DiscoveredProvider {
|
||||||
|
providerId: string;
|
||||||
|
providerName: string;
|
||||||
|
models: string[];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* CLI settings mode
|
||||||
|
*/
|
||||||
|
export type CliSettingsMode = 'provider-based' | 'direct';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* CLI settings
|
||||||
|
*/
|
||||||
|
export interface CliSettings {
|
||||||
|
id: string;
|
||||||
|
name: string;
|
||||||
|
description?: string;
|
||||||
|
enabled: boolean;
|
||||||
|
mode: CliSettingsMode;
|
||||||
|
providerId?: string;
|
||||||
|
settings?: Record<string, unknown>;
|
||||||
|
createdAt: string;
|
||||||
|
updatedAt: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ========== Provider Management ==========
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Fetch all providers
|
||||||
|
*/
|
||||||
|
export async function fetchProviders(): Promise<{ providers: ProviderCredential[]; count: number }> {
|
||||||
|
return fetchApi('/api/litellm-api/providers');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create provider
|
||||||
|
*/
|
||||||
|
export async function createProvider(provider: Omit<ProviderCredential, 'id' | 'createdAt' | 'updatedAt'>): Promise<{ success: boolean; provider: ProviderCredential }> {
|
||||||
|
return fetchApi('/api/litellm-api/providers', {
|
||||||
|
method: 'POST',
|
||||||
|
body: JSON.stringify(provider),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Update provider
|
||||||
|
*/
|
||||||
|
export async function updateProvider(providerId: string, updates: Partial<Omit<ProviderCredential, 'id' | 'createdAt' | 'updatedAt'>>): Promise<{ success: boolean; provider: ProviderCredential }> {
|
||||||
|
return fetchApi(`/api/litellm-api/providers/${encodeURIComponent(providerId)}`, {
|
||||||
|
method: 'PUT',
|
||||||
|
body: JSON.stringify(updates),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Delete provider
|
||||||
|
*/
|
||||||
|
export async function deleteProvider(providerId: string): Promise<{ success: boolean; message: string }> {
|
||||||
|
return fetchApi(`/api/litellm-api/providers/${encodeURIComponent(providerId)}`, {
|
||||||
|
method: 'DELETE',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Test provider connection
|
||||||
|
*/
|
||||||
|
export async function testProvider(providerId: string): Promise<{ success: boolean; provider: string; latencyMs?: number; error?: string }> {
|
||||||
|
return fetchApi(`/api/litellm-api/providers/${encodeURIComponent(providerId)}/test`, {
|
||||||
|
method: 'POST',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Test specific API key
|
||||||
|
*/
|
||||||
|
export async function testProviderKey(providerId: string, keyId: string): Promise<{ valid: boolean; error?: string; latencyMs?: number; keyLabel?: string }> {
|
||||||
|
return fetchApi(`/api/litellm-api/providers/${encodeURIComponent(providerId)}/test-key`, {
|
||||||
|
method: 'POST',
|
||||||
|
body: JSON.stringify({ keyId }),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get provider health status
|
||||||
|
*/
|
||||||
|
export async function getProviderHealthStatus(providerId: string): Promise<{ providerId: string; providerName: string; keys: Array<{ keyId: string; label: string; status: string; lastCheck?: string; lastLatencyMs?: number; consecutiveFailures?: number; inCooldown?: boolean; lastError?: string }> }> {
|
||||||
|
return fetchApi(`/api/litellm-api/providers/${encodeURIComponent(providerId)}/health-status`);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Trigger health check now
|
||||||
|
*/
|
||||||
|
export async function triggerProviderHealthCheck(providerId: string): Promise<{ success: boolean; providerId: string; providerName?: string; keys: Array<any>; checkedAt: string }> {
|
||||||
|
return fetchApi(`/api/litellm-api/providers/${encodeURIComponent(providerId)}/health-check-now`, {
|
||||||
|
method: 'POST',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// ========== Endpoint Management ==========
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Fetch all endpoints
|
||||||
|
*/
|
||||||
|
export async function fetchEndpoints(): Promise<{ endpoints: CustomEndpoint[]; count: number }> {
|
||||||
|
return fetchApi('/api/litellm-api/endpoints');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create endpoint
|
||||||
|
*/
|
||||||
|
export async function createEndpoint(endpoint: Omit<CustomEndpoint, 'createdAt' | 'updatedAt'>): Promise<{ success: boolean; endpoint: CustomEndpoint }> {
|
||||||
|
return fetchApi('/api/litellm-api/endpoints', {
|
||||||
|
method: 'POST',
|
||||||
|
body: JSON.stringify(endpoint),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Update endpoint
|
||||||
|
*/
|
||||||
|
export async function updateEndpoint(endpointId: string, updates: Partial<Omit<CustomEndpoint, 'id' | 'createdAt' | 'updatedAt'>>): Promise<{ success: boolean; endpoint: CustomEndpoint }> {
|
||||||
|
return fetchApi(`/api/litellm-api/endpoints/${encodeURIComponent(endpointId)}`, {
|
||||||
|
method: 'PUT',
|
||||||
|
body: JSON.stringify(updates),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Delete endpoint
|
||||||
|
*/
|
||||||
|
export async function deleteEndpoint(endpointId: string): Promise<{ success: boolean; message: string }> {
|
||||||
|
return fetchApi(`/api/litellm-api/endpoints/${encodeURIComponent(endpointId)}`, {
|
||||||
|
method: 'DELETE',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// ========== Model Discovery ==========
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get available models for provider type
|
||||||
|
*/
|
||||||
|
export async function getProviderModels(providerType: string): Promise<{ providerType: string; models: Array<{ id: string; name: string; provider: string; description?: string }>; count: number }> {
|
||||||
|
return fetchApi(`/api/litellm-api/models/${encodeURIComponent(providerType)}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ========== Cache Management ==========
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Fetch cache statistics
|
||||||
|
*/
|
||||||
|
export async function fetchCacheStats(): Promise<CacheStats> {
|
||||||
|
return fetchApi('/api/litellm-api/cache/stats');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Clear cache
|
||||||
|
*/
|
||||||
|
export async function clearCache(): Promise<{ success: boolean; removed: number }> {
|
||||||
|
return fetchApi('/api/litellm-api/cache/clear', {
|
||||||
|
method: 'POST',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Update cache settings
|
||||||
|
*/
|
||||||
|
export async function updateCacheSettings(settings: Partial<{ enabled: boolean; cacheDir: string; maxTotalSizeMB: number }>): Promise<{ success: boolean; settings: GlobalCacheSettings }> {
|
||||||
|
return fetchApi('/api/litellm-api/config/cache', {
|
||||||
|
method: 'PUT',
|
||||||
|
body: JSON.stringify(settings),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// ========== Model Pool Management ==========
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Fetch all model pools
|
||||||
|
*/
|
||||||
|
export async function fetchModelPools(): Promise<{ pools: ModelPoolConfig[] }> {
|
||||||
|
return fetchApi('/api/litellm-api/model-pools');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Fetch single model pool
|
||||||
|
*/
|
||||||
|
export async function fetchModelPool(poolId: string): Promise<{ pool: ModelPoolConfig }> {
|
||||||
|
return fetchApi(`/api/litellm-api/model-pools/${encodeURIComponent(poolId)}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create model pool
|
||||||
|
*/
|
||||||
|
export async function createModelPool(pool: Omit<ModelPoolConfig, 'id'>): Promise<{ success: boolean; poolId: string; syncResult?: any }> {
|
||||||
|
return fetchApi('/api/litellm-api/model-pools', {
|
||||||
|
method: 'POST',
|
||||||
|
body: JSON.stringify(pool),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Update model pool
|
||||||
|
*/
|
||||||
|
export async function updateModelPool(poolId: string, updates: Partial<ModelPoolConfig>): Promise<{ success: boolean; poolId?: string; syncResult?: any }> {
|
||||||
|
return fetchApi(`/api/litellm-api/model-pools/${encodeURIComponent(poolId)}`, {
|
||||||
|
method: 'PUT',
|
||||||
|
body: JSON.stringify(updates),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Delete model pool
|
||||||
|
*/
|
||||||
|
export async function deleteModelPool(poolId: string): Promise<{ success: boolean; syncResult?: any }> {
|
||||||
|
return fetchApi(`/api/litellm-api/model-pools/${encodeURIComponent(poolId)}`, {
|
||||||
|
method: 'DELETE',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get available models for pool type
|
||||||
|
*/
|
||||||
|
export async function getAvailableModelsForPool(modelType: ModelPoolType): Promise<{ availableModels: Array<{ modelId: string; modelName: string; providers: string[] }> }> {
|
||||||
|
return fetchApi(`/api/litellm-api/model-pools/available-models/${encodeURIComponent(modelType)}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Discover providers for model
|
||||||
|
*/
|
||||||
|
export async function discoverModelsForPool(modelType: ModelPoolType, targetModel: string): Promise<{ modelType: string; targetModel: string; discovered: DiscoveredProvider[]; count: number }> {
|
||||||
|
return fetchApi(`/api/litellm-api/model-pools/discover/${encodeURIComponent(modelType)}/${encodeURIComponent(targetModel)}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ========== Config Management ==========
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get full config
|
||||||
|
*/
|
||||||
|
export async function fetchApiConfig(): Promise<any> {
|
||||||
|
return fetchApi('/api/litellm-api/config');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sync config to YAML
|
||||||
|
*/
|
||||||
|
export async function syncApiConfig(): Promise<{ success: boolean; message: string; yamlPath?: string }> {
|
||||||
|
return fetchApi('/api/litellm-api/config/sync', {
|
||||||
|
method: 'POST',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Preview YAML config
|
||||||
|
*/
|
||||||
|
export async function previewYamlConfig(): Promise<{ success: boolean; config: string }> {
|
||||||
|
return fetchApi('/api/litellm-api/config/yaml-preview');
|
||||||
|
}
|
||||||
|
|
||||||
|
// ========== CCW-LiteLLM Package Management ==========
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check ccw-litellm status
|
||||||
|
*/
|
||||||
|
export async function checkCcwLitellmStatus(refresh = false): Promise<{ installed: boolean; version?: string; error?: string }> {
|
||||||
|
return fetchApi(`/api/litellm-api/ccw-litellm/status${refresh ? '?refresh=true' : ''}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Install ccw-litellm
|
||||||
|
*/
|
||||||
|
export async function installCcwLitellm(): Promise<{ success: boolean; message?: string; error?: string; path?: string }> {
|
||||||
|
return fetchApi('/api/litellm-api/ccw-litellm/install', {
|
||||||
|
method: 'POST',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Uninstall ccw-litellm
|
||||||
|
*/
|
||||||
|
export async function uninstallCcwLitellm(): Promise<{ success: boolean; message?: string; error?: string }> {
|
||||||
|
return fetchApi('/api/litellm-api/ccw-litellm/uninstall', {
|
||||||
|
method: 'POST',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|||||||
@@ -112,3 +112,19 @@ export const workspaceQueryKeys = {
|
|||||||
cliExecutionDetail: (projectPath: string, executionId: string) =>
|
cliExecutionDetail: (projectPath: string, executionId: string) =>
|
||||||
[...workspaceQueryKeys.cliHistory(projectPath), 'detail', executionId] as const,
|
[...workspaceQueryKeys.cliHistory(projectPath), 'detail', executionId] as const,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// ========== API Settings Keys ==========
|
||||||
|
/**
|
||||||
|
* API Settings query keys (global, not workspace-specific)
|
||||||
|
*/
|
||||||
|
export const apiSettingsKeys = {
|
||||||
|
all: ['apiSettings'] as const,
|
||||||
|
providers: () => [...apiSettingsKeys.all, 'providers'] as const,
|
||||||
|
provider: (id: string) => [...apiSettingsKeys.providers(), id] as const,
|
||||||
|
endpoints: () => [...apiSettingsKeys.all, 'endpoints'] as const,
|
||||||
|
endpoint: (id: string) => [...apiSettingsKeys.endpoints(), id] as const,
|
||||||
|
cache: () => [...apiSettingsKeys.all, 'cache'] as const,
|
||||||
|
modelPools: () => [...apiSettingsKeys.all, 'modelPools'] as const,
|
||||||
|
modelPool: (id: string) => [...apiSettingsKeys.modelPools(), id] as const,
|
||||||
|
ccwLitellm: () => [...apiSettingsKeys.all, 'ccwLitellm'] as const,
|
||||||
|
};
|
||||||
|
|||||||
335
ccw/frontend/src/locales/en/api-settings.json
Normal file
335
ccw/frontend/src/locales/en/api-settings.json
Normal file
@@ -0,0 +1,335 @@
|
|||||||
|
{
|
||||||
|
"title": "API Settings",
|
||||||
|
"description": "Manage LiteLLM providers, endpoints, cache settings, and model pools",
|
||||||
|
"tabs": {
|
||||||
|
"providers": "Providers",
|
||||||
|
"endpoints": "Endpoints",
|
||||||
|
"cache": "Cache",
|
||||||
|
"modelPools": "Model Pools",
|
||||||
|
"cliSettings": "CLI Settings"
|
||||||
|
},
|
||||||
|
"providers": {
|
||||||
|
"title": "Providers",
|
||||||
|
"description": "Configure API provider credentials and multi-key settings",
|
||||||
|
"stats": {
|
||||||
|
"total": "Total Providers",
|
||||||
|
"enabled": "Enabled"
|
||||||
|
},
|
||||||
|
"actions": {
|
||||||
|
"add": "Add Provider",
|
||||||
|
"edit": "Edit Provider",
|
||||||
|
"delete": "Delete Provider",
|
||||||
|
"test": "Test Connection",
|
||||||
|
"multiKeySettings": "Multi-Key Settings",
|
||||||
|
"syncToCodexLens": "Sync to CodexLens",
|
||||||
|
"manageModels": "Manage Models",
|
||||||
|
"addModel": "Add Model"
|
||||||
|
},
|
||||||
|
"deleteConfirm": "Are you sure you want to delete the provider \"{name}\"?",
|
||||||
|
"emptyState": {
|
||||||
|
"title": "No Providers Found",
|
||||||
|
"message": "Add a provider to configure API credentials and models."
|
||||||
|
},
|
||||||
|
"apiFormat": "API Format",
|
||||||
|
"openaiCompatible": "OpenAI Compatible",
|
||||||
|
"customFormat": "Custom Format",
|
||||||
|
"apiFormatHint": "Most providers use OpenAI-compatible format",
|
||||||
|
"displayName": "Display Name",
|
||||||
|
"apiKey": "API Key",
|
||||||
|
"useEnvVar": "Use environment variable (e.g., ${OPENAI_API_KEY})",
|
||||||
|
"apiBaseUrl": "API Base URL",
|
||||||
|
"preview": "Preview",
|
||||||
|
"enableProvider": "Enable this provider",
|
||||||
|
"advancedSettings": "Advanced Settings",
|
||||||
|
"timeout": "Timeout",
|
||||||
|
"timeoutHint": "Request timeout in seconds (default: 300)",
|
||||||
|
"maxRetries": "Max Retries",
|
||||||
|
"organization": "Organization",
|
||||||
|
"organizationHint": "OpenAI-specific organization ID",
|
||||||
|
"apiVersion": "API Version",
|
||||||
|
"apiVersionHint": "Azure-specific API version (e.g., 2024-02-01)",
|
||||||
|
"rpm": "RPM",
|
||||||
|
"tpm": "TPM",
|
||||||
|
"unlimited": "Unlimited",
|
||||||
|
"proxy": "Proxy URL",
|
||||||
|
"customHeaders": "Custom Headers",
|
||||||
|
"customHeadersHint": "JSON format, e.g., {\"X-Custom\": \"value\"}",
|
||||||
|
"testConnection": "Test Connection",
|
||||||
|
"connectionSuccess": "Connection successful",
|
||||||
|
"connectionFailed": "Connection failed",
|
||||||
|
"addProviderFirst": "Please add a provider first",
|
||||||
|
"saveProviderFirst": "Please save the provider first",
|
||||||
|
"llmModels": "LLM Models",
|
||||||
|
"embeddingModels": "Embedding Models",
|
||||||
|
"rerankerModels": "Reranker Models",
|
||||||
|
"noModels": "No models configured for this provider",
|
||||||
|
"selectProvider": "Select Provider",
|
||||||
|
"selectProviderHint": "Choose a provider to view and manage their models",
|
||||||
|
"modelSettings": "Model Settings",
|
||||||
|
"deleteModel": "Delete Model",
|
||||||
|
"multiKeySettings": "Multi-Key Settings",
|
||||||
|
"keyLabel": "Key Label",
|
||||||
|
"keyValue": "Key Value",
|
||||||
|
"keyWeight": "Weight",
|
||||||
|
"routingStrategy": "Routing Strategy",
|
||||||
|
"healthCheck": "Health Check",
|
||||||
|
"healthStatus": "Health Status",
|
||||||
|
"lastCheck": "Last check",
|
||||||
|
"lastError": "Last error",
|
||||||
|
"lastLatency": "Latency",
|
||||||
|
"cooldown": "Cooldown",
|
||||||
|
"consecutiveFailures": "Failures",
|
||||||
|
"inCooldown": "In cooldown",
|
||||||
|
"healthy": "Healthy",
|
||||||
|
"unhealthy": "Unhealthy",
|
||||||
|
"unknown": "Unknown",
|
||||||
|
"justNow": "just now",
|
||||||
|
"minutesAgo": "m ago",
|
||||||
|
"hoursAgo": "h ago",
|
||||||
|
"daysAgo": "d ago",
|
||||||
|
"seconds": "seconds",
|
||||||
|
"testKey": "Test Key",
|
||||||
|
"addKey": "Add Key",
|
||||||
|
"enableKey": "Enable",
|
||||||
|
"disableKey": "Disable",
|
||||||
|
"deleteKey": "Delete Key",
|
||||||
|
"simpleShuffle": "Simple Shuffle",
|
||||||
|
"weighted": "Weighted",
|
||||||
|
"latencyBased": "Latency Based",
|
||||||
|
"costBased": "Cost Based",
|
||||||
|
"leastBusy": "Least Busy",
|
||||||
|
"enableHealthCheck": "Enable health checks",
|
||||||
|
"checkInterval": "Check interval (seconds)",
|
||||||
|
"cooldownPeriod": "Cooldown period (seconds)",
|
||||||
|
"failureThreshold": "Failure threshold",
|
||||||
|
"addLlmModel": "Add LLM Model",
|
||||||
|
"addEmbeddingModel": "Add Embedding Model",
|
||||||
|
"addRerankerModel": "Add Reranker Model",
|
||||||
|
"selectFromPresets": "Select from presets",
|
||||||
|
"customModel": "Custom model...",
|
||||||
|
"modelId": "Model ID",
|
||||||
|
"modelName": "Model Name",
|
||||||
|
"modelSeries": "Model Series",
|
||||||
|
"contextWindow": "Context Window",
|
||||||
|
"capabilities": "Capabilities",
|
||||||
|
"streaming": "Streaming",
|
||||||
|
"functionCalling": "Function Calling",
|
||||||
|
"vision": "Vision",
|
||||||
|
"embeddingMaxTokens": "Max Tokens",
|
||||||
|
"rerankerTopK": "Top K",
|
||||||
|
"rerankerTopKHint": "Number of results to return",
|
||||||
|
"embeddingDimensions": "Dimensions",
|
||||||
|
"description": "Description",
|
||||||
|
"optional": "Optional",
|
||||||
|
"modelIdExists": "Model ID already exists",
|
||||||
|
"useModelTreeToManage": "Use provider card to manage models",
|
||||||
|
"endpointPreview": "Endpoint Preview",
|
||||||
|
"modelBaseUrlOverride": "Base URL Override",
|
||||||
|
"modelBaseUrlHint": "Override base URL for this model",
|
||||||
|
"basicInfo": "Basic Information",
|
||||||
|
"endpointSettings": "Endpoint Settings",
|
||||||
|
"apiBaseUpdated": "Base URL updated"
|
||||||
|
},
|
||||||
|
"endpoints": {
|
||||||
|
"title": "Endpoints",
|
||||||
|
"description": "Configure custom API endpoints with caching strategies",
|
||||||
|
"stats": {
|
||||||
|
"totalEndpoints": "Total Endpoints",
|
||||||
|
"cachedEndpoints": "Cached Endpoints"
|
||||||
|
},
|
||||||
|
"actions": {
|
||||||
|
"add": "Add Endpoint",
|
||||||
|
"edit": "Edit Endpoint",
|
||||||
|
"delete": "Delete Endpoint",
|
||||||
|
"enable": "Enable",
|
||||||
|
"disable": "Disable"
|
||||||
|
},
|
||||||
|
"deleteConfirm": "Are you sure you want to delete the endpoint \"{id}\"?",
|
||||||
|
"emptyState": {
|
||||||
|
"title": "No Endpoints Found",
|
||||||
|
"message": "Add an endpoint to configure custom API mappings."
|
||||||
|
},
|
||||||
|
"endpointId": "Endpoint ID",
|
||||||
|
"endpointIdHint": "Unique CLI identifier (e.g., my-gpt4o)",
|
||||||
|
"name": "Name",
|
||||||
|
"provider": "Provider",
|
||||||
|
"model": "Model",
|
||||||
|
"noModelsConfigured": "No models configured for this provider",
|
||||||
|
"selectModel": "Select a model...",
|
||||||
|
"cacheStrategy": "Cache Strategy",
|
||||||
|
"enableContextCaching": "Enable context caching",
|
||||||
|
"cacheTTL": "Cache TTL (minutes)",
|
||||||
|
"cacheMaxSize": "Max Size (KB)",
|
||||||
|
"autoCachePatterns": "Auto-cache patterns",
|
||||||
|
"filePatternsHint": "Comma-separated glob patterns (e.g., *.md,*.ts)",
|
||||||
|
"enabled": "Enabled",
|
||||||
|
"disabled": "Disabled",
|
||||||
|
"noEndpoints": "No Endpoints Configured",
|
||||||
|
"noEndpointsHint": "Add an endpoint to create custom API mappings with caching.",
|
||||||
|
"providerBased": "Provider-based",
|
||||||
|
"direct": "Direct",
|
||||||
|
"off": "Off"
|
||||||
|
},
|
||||||
|
"cache": {
|
||||||
|
"title": "Cache",
|
||||||
|
"description": "Manage global cache settings and view cache statistics",
|
||||||
|
"settings": {
|
||||||
|
"title": "Cache Settings",
|
||||||
|
"globalCache": "Global Cache",
|
||||||
|
"enableGlobalCaching": "Enable global caching",
|
||||||
|
"cacheDirectory": "Cache directory",
|
||||||
|
"maxSize": "Max total size (MB)",
|
||||||
|
"cacheUsage": "Cache usage",
|
||||||
|
"cacheEntries": "Cache entries",
|
||||||
|
"cacheSize": "Cache size",
|
||||||
|
"used": "used",
|
||||||
|
"total": "total",
|
||||||
|
"actions": "Cache Actions",
|
||||||
|
"clearCache": "Clear Cache",
|
||||||
|
"confirmClearCache": "Are you sure you want to clear the cache? This will remove all cached entries."
|
||||||
|
},
|
||||||
|
"statistics": {
|
||||||
|
"title": "Cache Statistics",
|
||||||
|
"cachedEntries": "Cached Entries",
|
||||||
|
"storageUsed": "Storage Used",
|
||||||
|
"totalSize": "Total Size"
|
||||||
|
},
|
||||||
|
"actions": {
|
||||||
|
"clear": "Clear Cache",
|
||||||
|
"confirmClear": "Are you sure you want to clear the cache?"
|
||||||
|
},
|
||||||
|
"messages": {
|
||||||
|
"cacheCleared": "Cache cleared",
|
||||||
|
"cacheSettingsUpdated": "Cache settings updated"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"modelPools": {
|
||||||
|
"title": "Model Pools",
|
||||||
|
"description": "Configure model pools for high availability and load balancing",
|
||||||
|
"stats": {
|
||||||
|
"total": "Total Pools",
|
||||||
|
"enabled": "Enabled"
|
||||||
|
},
|
||||||
|
"actions": {
|
||||||
|
"add": "Add Model Pool",
|
||||||
|
"edit": "Edit Model Pool",
|
||||||
|
"delete": "Delete Model Pool",
|
||||||
|
"autoDiscover": "Auto-Discover"
|
||||||
|
},
|
||||||
|
"deleteConfirm": "Are you sure you want to delete the model pool \"{name}\"?",
|
||||||
|
"emptyState": {
|
||||||
|
"title": "No Model Pools Found",
|
||||||
|
"message": "Add a model pool to enable high availability and load balancing."
|
||||||
|
},
|
||||||
|
"modelType": "Model Type",
|
||||||
|
"embedding": "Embedding",
|
||||||
|
"llm": "LLM",
|
||||||
|
"reranker": "Reranker",
|
||||||
|
"targetModel": "Target Model",
|
||||||
|
"selectTargetModel": "Select target model...",
|
||||||
|
"strategy": "Strategy",
|
||||||
|
"roundRobin": "Round Robin",
|
||||||
|
"latencyAware": "Latency Aware",
|
||||||
|
"weightedRandom": "Weighted Random",
|
||||||
|
"poolEnabled": "Pool Enabled",
|
||||||
|
"autoDiscover": "Auto-discover providers",
|
||||||
|
"excludedProviders": "Excluded Providers",
|
||||||
|
"defaultCooldown": "Default Cooldown (s)",
|
||||||
|
"defaultConcurrent": "Default Concurrent",
|
||||||
|
"discoveredProviders": "Discovered Providers",
|
||||||
|
"excludeProvider": "Exclude",
|
||||||
|
"includeProvider": "Include",
|
||||||
|
"noProvidersFound": "No providers found offering this model",
|
||||||
|
"poolSaved": "Model pool saved",
|
||||||
|
"embeddingPoolDesc": "Configure model pools for high availability and load balancing across multiple providers",
|
||||||
|
"embeddingPool": "Embedding Pool",
|
||||||
|
"discovered": "Discovered",
|
||||||
|
"providers": "providers"
|
||||||
|
},
|
||||||
|
"cliSettings": {
|
||||||
|
"title": "CLI Settings",
|
||||||
|
"description": "Configure CLI tool settings and modes",
|
||||||
|
"stats": {
|
||||||
|
"total": "Total Settings",
|
||||||
|
"enabled": "Enabled"
|
||||||
|
},
|
||||||
|
"actions": {
|
||||||
|
"add": "Add CLI Settings",
|
||||||
|
"edit": "Edit CLI Settings",
|
||||||
|
"delete": "Delete CLI Settings"
|
||||||
|
},
|
||||||
|
"deleteConfirm": "Are you sure you want to delete the CLI settings \"{name}\"?",
|
||||||
|
"emptyState": {
|
||||||
|
"title": "No CLI Settings Found",
|
||||||
|
"message": "Add CLI settings to configure tool-specific options."
|
||||||
|
},
|
||||||
|
"mode": "Mode",
|
||||||
|
"providerBased": "Provider-based",
|
||||||
|
"direct": "Direct",
|
||||||
|
"authToken": "Auth Token",
|
||||||
|
"baseUrl": "Base URL",
|
||||||
|
"model": "Model"
|
||||||
|
},
|
||||||
|
"ccwLitellm": {
|
||||||
|
"title": "CCW-LiteLLM Package",
|
||||||
|
"description": "Manage ccw-litellm Python package installation",
|
||||||
|
"status": {
|
||||||
|
"installed": "Installed",
|
||||||
|
"notInstalled": "Not Installed",
|
||||||
|
"version": "Version"
|
||||||
|
},
|
||||||
|
"actions": {
|
||||||
|
"install": "Install",
|
||||||
|
"uninstall": "Uninstall",
|
||||||
|
"refreshStatus": "Refresh Status"
|
||||||
|
},
|
||||||
|
"messages": {
|
||||||
|
"installSuccess": "ccw-litellm installed successfully",
|
||||||
|
"installFailed": "Failed to install ccw-litellm",
|
||||||
|
"uninstallSuccess": "ccw-litellm uninstalled successfully",
|
||||||
|
"uninstallFailed": "Failed to uninstall ccw-litellm"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"common": {
|
||||||
|
"save": "Save",
|
||||||
|
"cancel": "Cancel",
|
||||||
|
"confirm": "Confirm",
|
||||||
|
"delete": "Delete",
|
||||||
|
"edit": "Edit",
|
||||||
|
"add": "Add",
|
||||||
|
"close": "Close",
|
||||||
|
"loading": "Loading...",
|
||||||
|
"error": "Error",
|
||||||
|
"success": "Success",
|
||||||
|
"warning": "Warning",
|
||||||
|
"optional": "Optional",
|
||||||
|
"enabled": "Enabled",
|
||||||
|
"disabled": "Disabled",
|
||||||
|
"searchPlaceholder": "Search...",
|
||||||
|
"noResults": "No results found",
|
||||||
|
"actions": "Actions",
|
||||||
|
"name": "Name",
|
||||||
|
"description": "Description",
|
||||||
|
"type": "Type",
|
||||||
|
"status": "Status"
|
||||||
|
},
|
||||||
|
"messages": {
|
||||||
|
"settingsSaved": "Settings saved successfully",
|
||||||
|
"settingsDeleted": "Settings deleted successfully",
|
||||||
|
"providerSaved": "Provider saved successfully",
|
||||||
|
"providerDeleted": "Provider deleted successfully",
|
||||||
|
"providerUpdated": "Provider updated successfully",
|
||||||
|
"endpointSaved": "Endpoint saved successfully",
|
||||||
|
"endpointDeleted": "Endpoint deleted successfully",
|
||||||
|
"confirmDeleteProvider": "Are you sure you want to delete this provider?",
|
||||||
|
"confirmDeleteEndpoint": "Are you sure you want to delete this endpoint?",
|
||||||
|
"confirmClearCache": "Are you sure you want to clear the cache?",
|
||||||
|
"invalidJsonHeaders": "Invalid JSON format for custom headers",
|
||||||
|
"failedToLoad": "Failed to load data",
|
||||||
|
"noProviders": "No Providers",
|
||||||
|
"noProvidersHint": "Add a provider to get started with API configuration.",
|
||||||
|
"noEndpoints": "No Endpoints",
|
||||||
|
"noEndpointsHint": "Add an endpoint to create custom API mappings.",
|
||||||
|
"configSynced": "Configuration synced to YAML file"
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -13,6 +13,7 @@
|
|||||||
"overview": "Overview",
|
"overview": "Overview",
|
||||||
"settings": "Settings",
|
"settings": "Settings",
|
||||||
"models": "Models",
|
"models": "Models",
|
||||||
|
"search": "Search",
|
||||||
"advanced": "Advanced"
|
"advanced": "Advanced"
|
||||||
},
|
},
|
||||||
"overview": {
|
"overview": {
|
||||||
@@ -46,6 +47,19 @@
|
|||||||
"lastCheck": "Last Check Time"
|
"lastCheck": "Last Check Time"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"index": {
|
||||||
|
"operationComplete": "Index Operation Complete",
|
||||||
|
"operationFailed": "Index Operation Failed",
|
||||||
|
"noProject": "No Project Selected",
|
||||||
|
"noProjectDesc": "Please open a project to perform index operations.",
|
||||||
|
"starting": "Starting index operation...",
|
||||||
|
"cancelFailed": "Failed to cancel operation",
|
||||||
|
"unknownError": "An unknown error occurred",
|
||||||
|
"complete": "Complete",
|
||||||
|
"failed": "Failed",
|
||||||
|
"cancelled": "Cancelled",
|
||||||
|
"inProgress": "In Progress"
|
||||||
|
},
|
||||||
"settings": {
|
"settings": {
|
||||||
"currentCount": "Current Index Count",
|
"currentCount": "Current Index Count",
|
||||||
"currentWorkers": "Current Workers",
|
"currentWorkers": "Current Workers",
|
||||||
@@ -114,6 +128,8 @@
|
|||||||
"advanced": {
|
"advanced": {
|
||||||
"warningTitle": "Sensitive Operations Warning",
|
"warningTitle": "Sensitive Operations Warning",
|
||||||
"warningMessage": "Modifying environment variables may affect CodexLens operation. Ensure you understand each variable's purpose.",
|
"warningMessage": "Modifying environment variables may affect CodexLens operation. Ensure you understand each variable's purpose.",
|
||||||
|
"loadError": "Failed to load environment variables",
|
||||||
|
"loadErrorDesc": "Unable to fetch environment configuration. Please check if CodexLens is properly installed.",
|
||||||
"currentVars": "Current Environment Variables",
|
"currentVars": "Current Environment Variables",
|
||||||
"settingsVars": "Settings Variables",
|
"settingsVars": "Settings Variables",
|
||||||
"customVars": "Custom Variables",
|
"customVars": "Custom Variables",
|
||||||
@@ -170,9 +186,35 @@
|
|||||||
"title": "CodexLens Not Installed",
|
"title": "CodexLens Not Installed",
|
||||||
"description": "Please install CodexLens to use model management features."
|
"description": "Please install CodexLens to use model management features."
|
||||||
},
|
},
|
||||||
|
"error": {
|
||||||
|
"title": "Failed to load models",
|
||||||
|
"description": "Unable to fetch model list. Please check if CodexLens is properly installed."
|
||||||
|
},
|
||||||
"empty": {
|
"empty": {
|
||||||
"title": "No models found",
|
"title": "No models found",
|
||||||
"description": "Try adjusting your search or filter criteria"
|
"description": "No models are available. Try downloading models from the list.",
|
||||||
|
"filtered": "No models match your filter",
|
||||||
|
"filteredDesc": "Try adjusting your search or filter criteria"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"search": {
|
||||||
|
"type": "Search Type",
|
||||||
|
"content": "Content Search",
|
||||||
|
"files": "File Search",
|
||||||
|
"symbol": "Symbol Search",
|
||||||
|
"mode": "Mode",
|
||||||
|
"mode.semantic": "Semantic (default)",
|
||||||
|
"mode.exact": "Exact (FTS)",
|
||||||
|
"mode.fuzzy": "Fuzzy",
|
||||||
|
"query": "Query",
|
||||||
|
"queryPlaceholder": "Enter search query...",
|
||||||
|
"button": "Search",
|
||||||
|
"searching": "Searching...",
|
||||||
|
"results": "Results",
|
||||||
|
"resultsCount": "results",
|
||||||
|
"notInstalled": {
|
||||||
|
"title": "CodexLens Not Installed",
|
||||||
|
"description": "Please install CodexLens to use semantic code search features."
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
335
ccw/frontend/src/locales/zh/api-settings.json
Normal file
335
ccw/frontend/src/locales/zh/api-settings.json
Normal file
@@ -0,0 +1,335 @@
|
|||||||
|
{
|
||||||
|
"title": "API 设置",
|
||||||
|
"description": "管理 LiteLLM 提供商、端点、缓存设置和模型池",
|
||||||
|
"tabs": {
|
||||||
|
"providers": "提供商",
|
||||||
|
"endpoints": "端点",
|
||||||
|
"cache": "缓存",
|
||||||
|
"modelPools": "模型池",
|
||||||
|
"cliSettings": "CLI 设置"
|
||||||
|
},
|
||||||
|
"providers": {
|
||||||
|
"title": "提供商",
|
||||||
|
"description": "配置 API 提供商凭据和多密钥设置",
|
||||||
|
"stats": {
|
||||||
|
"total": "总提供商数",
|
||||||
|
"enabled": "已启用"
|
||||||
|
},
|
||||||
|
"actions": {
|
||||||
|
"add": "添加提供商",
|
||||||
|
"edit": "编辑提供商",
|
||||||
|
"delete": "删除提供商",
|
||||||
|
"test": "测试连接",
|
||||||
|
"multiKeySettings": "多密钥设置",
|
||||||
|
"syncToCodexLens": "同步到 CodexLens",
|
||||||
|
"manageModels": "管理模型",
|
||||||
|
"addModel": "添加模型"
|
||||||
|
},
|
||||||
|
"deleteConfirm": "确定要删除提供商 \"{name}\" 吗?",
|
||||||
|
"emptyState": {
|
||||||
|
"title": "未找到提供商",
|
||||||
|
"message": "添加提供商以配置 API 凭据和模型。"
|
||||||
|
},
|
||||||
|
"apiFormat": "API 格式",
|
||||||
|
"openaiCompatible": "OpenAI 兼容",
|
||||||
|
"customFormat": "自定义格式",
|
||||||
|
"apiFormatHint": "大多数提供商使用 OpenAI 兼容格式",
|
||||||
|
"displayName": "显示名称",
|
||||||
|
"apiKey": "API 密钥",
|
||||||
|
"useEnvVar": "使用环境变量(例如:${OPENAI_API_KEY})",
|
||||||
|
"apiBaseUrl": "API 基础 URL",
|
||||||
|
"preview": "预览",
|
||||||
|
"enableProvider": "启用此提供商",
|
||||||
|
"advancedSettings": "高级设置",
|
||||||
|
"timeout": "超时",
|
||||||
|
"timeoutHint": "请求超时时间(秒)(默认:300)",
|
||||||
|
"maxRetries": "最大重试次数",
|
||||||
|
"organization": "组织",
|
||||||
|
"organizationHint": "OpenAI 特定的组织 ID",
|
||||||
|
"apiVersion": "API 版本",
|
||||||
|
"apiVersionHint": "Azure 特定的 API 版本(例如:2024-02-01)",
|
||||||
|
"rpm": "RPM",
|
||||||
|
"tpm": "TPM",
|
||||||
|
"unlimited": "无限制",
|
||||||
|
"proxy": "代理 URL",
|
||||||
|
"customHeaders": "自定义请求头",
|
||||||
|
"customHeadersHint": "JSON 格式,例如:{\"X-Custom\": \"value\"}",
|
||||||
|
"testConnection": "测试连接",
|
||||||
|
"connectionSuccess": "连接成功",
|
||||||
|
"connectionFailed": "连接失败",
|
||||||
|
"addProviderFirst": "请先添加提供商",
|
||||||
|
"saveProviderFirst": "请先保存提供商",
|
||||||
|
"llmModels": "LLM 模型",
|
||||||
|
"embeddingModels": "嵌入模型",
|
||||||
|
"rerankerModels": "重排序模型",
|
||||||
|
"noModels": "此提供商未配置模型",
|
||||||
|
"selectProvider": "选择提供商",
|
||||||
|
"selectProviderHint": "选择提供商以查看和管理其模型",
|
||||||
|
"modelSettings": "模型设置",
|
||||||
|
"deleteModel": "删除模型",
|
||||||
|
"multiKeySettings": "多密钥设置",
|
||||||
|
"keyLabel": "密钥标签",
|
||||||
|
"keyValue": "密钥值",
|
||||||
|
"keyWeight": "权重",
|
||||||
|
"routingStrategy": "路由策略",
|
||||||
|
"healthCheck": "健康检查",
|
||||||
|
"healthStatus": "健康状态",
|
||||||
|
"lastCheck": "最后检查",
|
||||||
|
"lastError": "最后错误",
|
||||||
|
"lastLatency": "延迟",
|
||||||
|
"cooldown": "冷却",
|
||||||
|
"consecutiveFailures": "失败次数",
|
||||||
|
"inCooldown": "冷却中",
|
||||||
|
"healthy": "健康",
|
||||||
|
"unhealthy": "不健康",
|
||||||
|
"unknown": "未知",
|
||||||
|
"justNow": "刚刚",
|
||||||
|
"minutesAgo": "分钟前",
|
||||||
|
"hoursAgo": "小时前",
|
||||||
|
"daysAgo": "天前",
|
||||||
|
"seconds": "秒",
|
||||||
|
"testKey": "测试密钥",
|
||||||
|
"addKey": "添加密钥",
|
||||||
|
"enableKey": "启用",
|
||||||
|
"disableKey": "禁用",
|
||||||
|
"deleteKey": "删除密钥",
|
||||||
|
"simpleShuffle": "简单随机",
|
||||||
|
"weighted": "加权",
|
||||||
|
"latencyBased": "基于延迟",
|
||||||
|
"costBased": "基于成本",
|
||||||
|
"leastBusy": "最少繁忙",
|
||||||
|
"enableHealthCheck": "启用健康检查",
|
||||||
|
"checkInterval": "检查间隔(秒)",
|
||||||
|
"cooldownPeriod": "冷却周期(秒)",
|
||||||
|
"failureThreshold": "失败阈值",
|
||||||
|
"addLlmModel": "添加 LLM 模型",
|
||||||
|
"addEmbeddingModel": "添加嵌入模型",
|
||||||
|
"addRerankerModel": "添加重排序模型",
|
||||||
|
"selectFromPresets": "从预设选择",
|
||||||
|
"customModel": "自定义模型...",
|
||||||
|
"modelId": "模型 ID",
|
||||||
|
"modelName": "模型名称",
|
||||||
|
"modelSeries": "模型系列",
|
||||||
|
"contextWindow": "上下文窗口",
|
||||||
|
"capabilities": "功能",
|
||||||
|
"streaming": "流式传输",
|
||||||
|
"functionCalling": "函数调用",
|
||||||
|
"vision": "视觉",
|
||||||
|
"embeddingMaxTokens": "最大令牌数",
|
||||||
|
"rerankerTopK": "Top K",
|
||||||
|
"rerankerTopKHint": "返回的结果数量",
|
||||||
|
"embeddingDimensions": "维度",
|
||||||
|
"description": "描述",
|
||||||
|
"optional": "可选",
|
||||||
|
"modelIdExists": "模型 ID 已存在",
|
||||||
|
"useModelTreeToManage": "使用提供商卡片管理模型",
|
||||||
|
"endpointPreview": "端点预览",
|
||||||
|
"modelBaseUrlOverride": "基础 URL 覆盖",
|
||||||
|
"modelBaseUrlHint": "为此模型覆盖基础 URL",
|
||||||
|
"basicInfo": "基本信息",
|
||||||
|
"endpointSettings": "端点设置",
|
||||||
|
"apiBaseUpdated": "基础 URL 已更新"
|
||||||
|
},
|
||||||
|
"endpoints": {
|
||||||
|
"title": "端点",
|
||||||
|
"description": "配置带有缓存策略的自定义 API 端点",
|
||||||
|
"stats": {
|
||||||
|
"totalEndpoints": "总端点数",
|
||||||
|
"cachedEndpoints": "已缓存端点"
|
||||||
|
},
|
||||||
|
"actions": {
|
||||||
|
"add": "添加端点",
|
||||||
|
"edit": "编辑端点",
|
||||||
|
"delete": "删除端点",
|
||||||
|
"enable": "启用",
|
||||||
|
"disable": "禁用"
|
||||||
|
},
|
||||||
|
"deleteConfirm": "确定要删除端点 \"{id}\" 吗?",
|
||||||
|
"emptyState": {
|
||||||
|
"title": "未找到端点",
|
||||||
|
"message": "添加端点以配置自定义 API 映射。"
|
||||||
|
},
|
||||||
|
"endpointId": "端点 ID",
|
||||||
|
"endpointIdHint": "唯一的 CLI 标识符(例如:my-gpt4o)",
|
||||||
|
"name": "名称",
|
||||||
|
"provider": "提供商",
|
||||||
|
"model": "模型",
|
||||||
|
"noModelsConfigured": "此提供商未配置模型",
|
||||||
|
"selectModel": "选择模型...",
|
||||||
|
"cacheStrategy": "缓存策略",
|
||||||
|
"enableContextCaching": "启用上下文缓存",
|
||||||
|
"cacheTTL": "缓存 TTL(分钟)",
|
||||||
|
"cacheMaxSize": "最大大小(KB)",
|
||||||
|
"autoCachePatterns": "自动缓存模式",
|
||||||
|
"filePatternsHint": "逗号分隔的 glob 模式(例如:*.md,*.ts)",
|
||||||
|
"enabled": "已启用",
|
||||||
|
"disabled": "已禁用",
|
||||||
|
"noEndpoints": "未配置端点",
|
||||||
|
"noEndpointsHint": "添加端点以创建带有缓存的自定义 API 映射。",
|
||||||
|
"providerBased": "基于提供商",
|
||||||
|
"direct": "直接",
|
||||||
|
"off": "关闭"
|
||||||
|
},
|
||||||
|
"cache": {
|
||||||
|
"title": "缓存",
|
||||||
|
"description": "管理全局缓存设置和查看缓存统计信息",
|
||||||
|
"settings": {
|
||||||
|
"title": "缓存设置",
|
||||||
|
"globalCache": "全局缓存",
|
||||||
|
"enableGlobalCaching": "启用全局缓存",
|
||||||
|
"cacheDirectory": "缓存目录",
|
||||||
|
"maxSize": "最大总大小(MB)",
|
||||||
|
"cacheUsage": "缓存使用",
|
||||||
|
"cacheEntries": "缓存条目",
|
||||||
|
"cacheSize": "缓存大小",
|
||||||
|
"used": "已使用",
|
||||||
|
"total": "总计",
|
||||||
|
"actions": "缓存操作",
|
||||||
|
"clearCache": "清除缓存",
|
||||||
|
"confirmClearCache": "确定要清除缓存吗?这将删除所有缓存条目。"
|
||||||
|
},
|
||||||
|
"statistics": {
|
||||||
|
"title": "缓存统计",
|
||||||
|
"cachedEntries": "已缓存条目",
|
||||||
|
"storageUsed": "已用存储",
|
||||||
|
"totalSize": "总大小"
|
||||||
|
},
|
||||||
|
"actions": {
|
||||||
|
"clear": "清除缓存",
|
||||||
|
"confirmClear": "确定要清除缓存吗?"
|
||||||
|
},
|
||||||
|
"messages": {
|
||||||
|
"cacheCleared": "缓存已清除",
|
||||||
|
"cacheSettingsUpdated": "缓存设置已更新"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"modelPools": {
|
||||||
|
"title": "模型池",
|
||||||
|
"description": "配置模型池以实现高可用性和负载均衡",
|
||||||
|
"stats": {
|
||||||
|
"total": "总池数",
|
||||||
|
"enabled": "已启用"
|
||||||
|
},
|
||||||
|
"actions": {
|
||||||
|
"add": "添加模型池",
|
||||||
|
"edit": "编辑模型池",
|
||||||
|
"delete": "删除模型池",
|
||||||
|
"autoDiscover": "自动发现"
|
||||||
|
},
|
||||||
|
"deleteConfirm": "确定要删除模型池 \"{name}\" 吗?",
|
||||||
|
"emptyState": {
|
||||||
|
"title": "未找到模型池",
|
||||||
|
"message": "添加模型池以启用高可用性和负载均衡。"
|
||||||
|
},
|
||||||
|
"modelType": "模型类型",
|
||||||
|
"embedding": "嵌入",
|
||||||
|
"llm": "LLM",
|
||||||
|
"reranker": "重排序",
|
||||||
|
"targetModel": "目标模型",
|
||||||
|
"selectTargetModel": "选择目标模型...",
|
||||||
|
"strategy": "策略",
|
||||||
|
"roundRobin": "轮询",
|
||||||
|
"latencyAware": "延迟感知",
|
||||||
|
"weightedRandom": "加权随机",
|
||||||
|
"poolEnabled": "池已启用",
|
||||||
|
"autoDiscover": "自动发现提供商",
|
||||||
|
"excludedProviders": "排除的提供商",
|
||||||
|
"defaultCooldown": "默认冷却时间(秒)",
|
||||||
|
"defaultConcurrent": "默认并发数",
|
||||||
|
"discoveredProviders": "已发现的提供商",
|
||||||
|
"excludeProvider": "排除",
|
||||||
|
"includeProvider": "包含",
|
||||||
|
"noProvidersFound": "未找到提供此模型的提供商",
|
||||||
|
"poolSaved": "模型池已保存",
|
||||||
|
"embeddingPoolDesc": "配置模型池以实现跨多个提供商的高可用性和负载均衡",
|
||||||
|
"embeddingPool": "嵌入池",
|
||||||
|
"discovered": "已发现",
|
||||||
|
"providers": "提供商"
|
||||||
|
},
|
||||||
|
"cliSettings": {
|
||||||
|
"title": "CLI 设置",
|
||||||
|
"description": "配置 CLI 工具设置和模式",
|
||||||
|
"stats": {
|
||||||
|
"total": "总设置数",
|
||||||
|
"enabled": "已启用"
|
||||||
|
},
|
||||||
|
"actions": {
|
||||||
|
"add": "添加 CLI 设置",
|
||||||
|
"edit": "编辑 CLI 设置",
|
||||||
|
"delete": "删除 CLI 设置"
|
||||||
|
},
|
||||||
|
"deleteConfirm": "确定要删除 CLI 设置 \"{name}\" 吗?",
|
||||||
|
"emptyState": {
|
||||||
|
"title": "未找到 CLI 设置",
|
||||||
|
"message": "添加 CLI 设置以配置工具特定选项。"
|
||||||
|
},
|
||||||
|
"mode": "模式",
|
||||||
|
"providerBased": "基于提供商",
|
||||||
|
"direct": "直接",
|
||||||
|
"authToken": "认证令牌",
|
||||||
|
"baseUrl": "基础 URL",
|
||||||
|
"model": "模型"
|
||||||
|
},
|
||||||
|
"ccwLitellm": {
|
||||||
|
"title": "CCW-LiteLLM 包",
|
||||||
|
"description": "管理 ccw-litellm Python 包安装",
|
||||||
|
"status": {
|
||||||
|
"installed": "已安装",
|
||||||
|
"notInstalled": "未安装",
|
||||||
|
"version": "版本"
|
||||||
|
},
|
||||||
|
"actions": {
|
||||||
|
"install": "安装",
|
||||||
|
"uninstall": "卸载",
|
||||||
|
"refreshStatus": "刷新状态"
|
||||||
|
},
|
||||||
|
"messages": {
|
||||||
|
"installSuccess": "ccw-litellm 安装成功",
|
||||||
|
"installFailed": "ccw-litellm 安装失败",
|
||||||
|
"uninstallSuccess": "ccw-litellm 卸载成功",
|
||||||
|
"uninstallFailed": "ccw-litellm 卸载失败"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"common": {
|
||||||
|
"save": "保存",
|
||||||
|
"cancel": "取消",
|
||||||
|
"confirm": "确认",
|
||||||
|
"delete": "删除",
|
||||||
|
"edit": "编辑",
|
||||||
|
"add": "添加",
|
||||||
|
"close": "关闭",
|
||||||
|
"loading": "加载中...",
|
||||||
|
"error": "错误",
|
||||||
|
"success": "成功",
|
||||||
|
"warning": "警告",
|
||||||
|
"optional": "可选",
|
||||||
|
"enabled": "已启用",
|
||||||
|
"disabled": "已禁用",
|
||||||
|
"searchPlaceholder": "搜索...",
|
||||||
|
"noResults": "未找到结果",
|
||||||
|
"actions": "操作",
|
||||||
|
"name": "名称",
|
||||||
|
"description": "描述",
|
||||||
|
"type": "类型",
|
||||||
|
"status": "状态"
|
||||||
|
},
|
||||||
|
"messages": {
|
||||||
|
"settingsSaved": "设置保存成功",
|
||||||
|
"settingsDeleted": "设置删除成功",
|
||||||
|
"providerSaved": "提供商保存成功",
|
||||||
|
"providerDeleted": "提供商删除成功",
|
||||||
|
"providerUpdated": "提供商更新成功",
|
||||||
|
"endpointSaved": "端点保存成功",
|
||||||
|
"endpointDeleted": "端点删除成功",
|
||||||
|
"confirmDeleteProvider": "确定要删除此提供商吗?",
|
||||||
|
"confirmDeleteEndpoint": "确定要删除此端点吗?",
|
||||||
|
"confirmClearCache": "确定要清除缓存吗?",
|
||||||
|
"invalidJsonHeaders": "自定义请求头的 JSON 格式无效",
|
||||||
|
"failedToLoad": "加载数据失败",
|
||||||
|
"noProviders": "无提供商",
|
||||||
|
"noProvidersHint": "添加提供商以开始配置 API。",
|
||||||
|
"noEndpoints": "无端点",
|
||||||
|
"noEndpointsHint": "添加端点以创建自定义 API 映射。",
|
||||||
|
"configSynced": "配置已同步到 YAML 文件"
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -13,6 +13,7 @@
|
|||||||
"overview": "概览",
|
"overview": "概览",
|
||||||
"settings": "设置",
|
"settings": "设置",
|
||||||
"models": "模型",
|
"models": "模型",
|
||||||
|
"search": "搜索",
|
||||||
"advanced": "高级"
|
"advanced": "高级"
|
||||||
},
|
},
|
||||||
"overview": {
|
"overview": {
|
||||||
@@ -114,6 +115,8 @@
|
|||||||
"advanced": {
|
"advanced": {
|
||||||
"warningTitle": "敏感操作警告",
|
"warningTitle": "敏感操作警告",
|
||||||
"warningMessage": "修改环境变量可能影响 CodexLens 的正常运行。请确保您了解每个变量的作用。",
|
"warningMessage": "修改环境变量可能影响 CodexLens 的正常运行。请确保您了解每个变量的作用。",
|
||||||
|
"loadError": "加载环境变量失败",
|
||||||
|
"loadErrorDesc": "无法获取环境配置。请检查 CodexLens 是否正确安装。",
|
||||||
"currentVars": "当前环境变量",
|
"currentVars": "当前环境变量",
|
||||||
"settingsVars": "设置变量",
|
"settingsVars": "设置变量",
|
||||||
"customVars": "自定义变量",
|
"customVars": "自定义变量",
|
||||||
@@ -170,9 +173,35 @@
|
|||||||
"title": "CodexLens 未安装",
|
"title": "CodexLens 未安装",
|
||||||
"description": "请先安装 CodexLens 以使用模型管理功能。"
|
"description": "请先安装 CodexLens 以使用模型管理功能。"
|
||||||
},
|
},
|
||||||
|
"error": {
|
||||||
|
"title": "加载模型失败",
|
||||||
|
"description": "无法获取模型列表。请检查 CodexLens 是否正确安装。"
|
||||||
|
},
|
||||||
"empty": {
|
"empty": {
|
||||||
"title": "没有找到模型",
|
"title": "没有找到模型",
|
||||||
"description": "尝试调整搜索或筛选条件"
|
"description": "当前没有可用模型。请从列表中下载模型。",
|
||||||
|
"filtered": "没有匹配的模型",
|
||||||
|
"filteredDesc": "尝试调整搜索或筛选条件"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"search": {
|
||||||
|
"type": "搜索类型",
|
||||||
|
"content": "内容搜索",
|
||||||
|
"files": "文件搜索",
|
||||||
|
"symbol": "符号搜索",
|
||||||
|
"mode": "模式",
|
||||||
|
"mode.semantic": "语义(默认)",
|
||||||
|
"mode.exact": "精确(FTS)",
|
||||||
|
"mode.fuzzy": "模糊",
|
||||||
|
"query": "查询",
|
||||||
|
"queryPlaceholder": "输入搜索查询...",
|
||||||
|
"button": "搜索",
|
||||||
|
"searching": "搜索中...",
|
||||||
|
"results": "结果",
|
||||||
|
"resultsCount": "个结果",
|
||||||
|
"notInstalled": {
|
||||||
|
"title": "CodexLens 未安装",
|
||||||
|
"description": "请先安装 CodexLens 以使用语义代码搜索功能。"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -31,6 +31,7 @@ import { SettingsTab } from '@/components/codexlens/SettingsTab';
|
|||||||
import { AdvancedTab } from '@/components/codexlens/AdvancedTab';
|
import { AdvancedTab } from '@/components/codexlens/AdvancedTab';
|
||||||
import { GpuSelector } from '@/components/codexlens/GpuSelector';
|
import { GpuSelector } from '@/components/codexlens/GpuSelector';
|
||||||
import { ModelsTab } from '@/components/codexlens/ModelsTab';
|
import { ModelsTab } from '@/components/codexlens/ModelsTab';
|
||||||
|
import { SearchTab } from '@/components/codexlens/SearchTab';
|
||||||
import { useCodexLensDashboard, useCodexLensMutations } from '@/hooks';
|
import { useCodexLensDashboard, useCodexLensMutations } from '@/hooks';
|
||||||
import { cn } from '@/lib/utils';
|
import { cn } from '@/lib/utils';
|
||||||
|
|
||||||
@@ -172,6 +173,9 @@ export function CodexLensManagerPage() {
|
|||||||
<TabsTrigger value="models">
|
<TabsTrigger value="models">
|
||||||
{formatMessage({ id: 'codexlens.tabs.models' })}
|
{formatMessage({ id: 'codexlens.tabs.models' })}
|
||||||
</TabsTrigger>
|
</TabsTrigger>
|
||||||
|
<TabsTrigger value="search">
|
||||||
|
{formatMessage({ id: 'codexlens.tabs.search' })}
|
||||||
|
</TabsTrigger>
|
||||||
<TabsTrigger value="advanced">
|
<TabsTrigger value="advanced">
|
||||||
{formatMessage({ id: 'codexlens.tabs.advanced' })}
|
{formatMessage({ id: 'codexlens.tabs.advanced' })}
|
||||||
</TabsTrigger>
|
</TabsTrigger>
|
||||||
@@ -183,6 +187,7 @@ export function CodexLensManagerPage() {
|
|||||||
status={status}
|
status={status}
|
||||||
config={config}
|
config={config}
|
||||||
isLoading={isLoading}
|
isLoading={isLoading}
|
||||||
|
onRefresh={handleRefresh}
|
||||||
/>
|
/>
|
||||||
</TabsContent>
|
</TabsContent>
|
||||||
|
|
||||||
@@ -194,6 +199,10 @@ export function CodexLensManagerPage() {
|
|||||||
<ModelsTab installed={installed} />
|
<ModelsTab installed={installed} />
|
||||||
</TabsContent>
|
</TabsContent>
|
||||||
|
|
||||||
|
<TabsContent value="search">
|
||||||
|
<SearchTab enabled={installed} />
|
||||||
|
</TabsContent>
|
||||||
|
|
||||||
<TabsContent value="advanced">
|
<TabsContent value="advanced">
|
||||||
<AdvancedTab enabled={installed} />
|
<AdvancedTab enabled={installed} />
|
||||||
</TabsContent>
|
</TabsContent>
|
||||||
|
|||||||
@@ -77,6 +77,7 @@ interface CliStreamState extends BlockCacheState {
|
|||||||
outputs: Record<string, CliOutputLine[]>;
|
outputs: Record<string, CliOutputLine[]>;
|
||||||
executions: Record<string, CliExecutionState>;
|
executions: Record<string, CliExecutionState>;
|
||||||
currentExecutionId: string | null;
|
currentExecutionId: string | null;
|
||||||
|
userClosedExecutions: Set<string>; // Track executions closed by user
|
||||||
|
|
||||||
// Legacy methods
|
// Legacy methods
|
||||||
addOutput: (executionId: string, line: CliOutputLine) => void;
|
addOutput: (executionId: string, line: CliOutputLine) => void;
|
||||||
@@ -87,6 +88,9 @@ interface CliStreamState extends BlockCacheState {
|
|||||||
getAllExecutions: () => CliExecutionState[];
|
getAllExecutions: () => CliExecutionState[];
|
||||||
upsertExecution: (executionId: string, exec: Partial<CliExecutionState> & { tool?: string; mode?: string }) => void;
|
upsertExecution: (executionId: string, exec: Partial<CliExecutionState> & { tool?: string; mode?: string }) => void;
|
||||||
removeExecution: (executionId: string) => void;
|
removeExecution: (executionId: string) => void;
|
||||||
|
markExecutionClosedByUser: (executionId: string) => void;
|
||||||
|
isExecutionClosedByUser: (executionId: string) => boolean;
|
||||||
|
cleanupUserClosedExecutions: (serverIds: Set<string>) => void;
|
||||||
setCurrentExecution: (executionId: string | null) => void;
|
setCurrentExecution: (executionId: string | null) => void;
|
||||||
|
|
||||||
// Block cache methods
|
// Block cache methods
|
||||||
@@ -320,6 +324,7 @@ export const useCliStreamStore = create<CliStreamState>()(
|
|||||||
outputs: {},
|
outputs: {},
|
||||||
executions: {},
|
executions: {},
|
||||||
currentExecutionId: null,
|
currentExecutionId: null,
|
||||||
|
userClosedExecutions: new Set<string>(),
|
||||||
|
|
||||||
// Block cache state
|
// Block cache state
|
||||||
blocks: {},
|
blocks: {},
|
||||||
@@ -426,6 +431,35 @@ export const useCliStreamStore = create<CliStreamState>()(
|
|||||||
}, false, 'cliStream/removeExecution');
|
}, false, 'cliStream/removeExecution');
|
||||||
},
|
},
|
||||||
|
|
||||||
|
markExecutionClosedByUser: (executionId: string) => {
|
||||||
|
set((state) => {
|
||||||
|
const newUserClosedExecutions = new Set(state.userClosedExecutions);
|
||||||
|
newUserClosedExecutions.add(executionId);
|
||||||
|
return {
|
||||||
|
userClosedExecutions: newUserClosedExecutions,
|
||||||
|
};
|
||||||
|
}, false, 'cliStream/markExecutionClosedByUser');
|
||||||
|
},
|
||||||
|
|
||||||
|
isExecutionClosedByUser: (executionId: string) => {
|
||||||
|
return get().userClosedExecutions.has(executionId);
|
||||||
|
},
|
||||||
|
|
||||||
|
cleanupUserClosedExecutions: (serverIds: Set<string>) => {
|
||||||
|
set((state) => {
|
||||||
|
const newUserClosedExecutions = new Set<string>();
|
||||||
|
for (const executionId of state.userClosedExecutions) {
|
||||||
|
// Only keep if still on server (user might want to keep it closed)
|
||||||
|
if (serverIds.has(executionId)) {
|
||||||
|
newUserClosedExecutions.add(executionId);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
userClosedExecutions: newUserClosedExecutions,
|
||||||
|
};
|
||||||
|
}, false, 'cliStream/cleanupUserClosedExecutions');
|
||||||
|
},
|
||||||
|
|
||||||
setCurrentExecution: (executionId: string | null) => {
|
setCurrentExecution: (executionId: string | null) => {
|
||||||
set({ currentExecutionId: executionId }, false, 'cliStream/setCurrentExecution');
|
set({ currentExecutionId: executionId }, false, 'cliStream/setCurrentExecution');
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -442,4 +442,573 @@ test.describe('[CodexLens Manager] - CodexLens Management Tests', () => {
|
|||||||
monitoring.assertClean({ allowWarnings: true });
|
monitoring.assertClean({ allowWarnings: true });
|
||||||
monitoring.stop();
|
monitoring.stop();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// ========================================
|
||||||
|
// Search Tab Tests
|
||||||
|
// ========================================
|
||||||
|
test.describe('[CodexLens Manager] - Search Tab Tests', () => {
|
||||||
|
test('L4.19 - should navigate to Search tab', async ({ page }) => {
|
||||||
|
const monitoring = setupEnhancedMonitoring(page);
|
||||||
|
|
||||||
|
await page.goto('/settings/codexlens', { waitUntil: 'networkidle' as const });
|
||||||
|
|
||||||
|
// Click Search tab
|
||||||
|
const searchTab = page.getByRole('tab', { name: /Search/i });
|
||||||
|
const searchTabVisible = await searchTab.isVisible().catch(() => false);
|
||||||
|
if (searchTabVisible) {
|
||||||
|
await searchTab.click();
|
||||||
|
|
||||||
|
// Verify tab is active
|
||||||
|
await expect(searchTab).toHaveAttribute('data-state', 'active');
|
||||||
|
|
||||||
|
// Verify search content is visible
|
||||||
|
const searchContent = page.getByText(/Search/i).or(
|
||||||
|
page.getByPlaceholder(/Search query/i)
|
||||||
|
);
|
||||||
|
const contentVisible = await searchContent.isVisible().catch(() => false);
|
||||||
|
if (contentVisible) {
|
||||||
|
await expect(searchContent.first()).toBeVisible();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
monitoring.assertClean({ allowWarnings: true });
|
||||||
|
monitoring.stop();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('L4.20 - should display all search UI elements', async ({ page }) => {
|
||||||
|
const monitoring = setupEnhancedMonitoring(page);
|
||||||
|
|
||||||
|
await page.goto('/settings/codexlens', { waitUntil: 'networkidle' as const });
|
||||||
|
|
||||||
|
const searchTab = page.getByRole('tab', { name: /Search/i });
|
||||||
|
const searchTabVisible = await searchTab.isVisible().catch(() => false);
|
||||||
|
if (searchTabVisible) {
|
||||||
|
await searchTab.click();
|
||||||
|
|
||||||
|
// Verify search type selector exists
|
||||||
|
const searchTypeSelector = page.getByLabel(/Search Type/i).or(
|
||||||
|
page.getByRole('combobox', { name: /Search Type/i })
|
||||||
|
).or(page.getByText(/Search Type/i));
|
||||||
|
const typeVisible = await searchTypeSelector.isVisible().catch(() => false);
|
||||||
|
if (typeVisible) {
|
||||||
|
await expect(searchTypeSelector.first()).toBeVisible();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Verify search mode selector exists
|
||||||
|
const searchModeSelector = page.getByLabel(/Search Mode/i).or(
|
||||||
|
page.getByRole('combobox', { name: /Search Mode/i })
|
||||||
|
).or(page.getByText(/Search Mode/i));
|
||||||
|
const modeVisible = await searchModeSelector.isVisible().catch(() => false);
|
||||||
|
if (modeVisible) {
|
||||||
|
await expect(searchModeSelector.first()).toBeVisible();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Verify query input field exists
|
||||||
|
const queryInput = page.getByPlaceholder(/Search query/i).or(
|
||||||
|
page.getByLabel(/Query/i)
|
||||||
|
).or(page.getByRole('textbox', { name: /query/i }));
|
||||||
|
const inputVisible = await queryInput.isVisible().catch(() => false);
|
||||||
|
if (inputVisible) {
|
||||||
|
await expect(queryInput.first()).toBeVisible();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Verify search button exists
|
||||||
|
const searchButton = page.getByRole('button', { name: /Search/i });
|
||||||
|
const buttonVisible = await searchButton.isVisible().catch(() => false);
|
||||||
|
if (buttonVisible) {
|
||||||
|
await expect(searchButton.first()).toBeVisible();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
monitoring.assertClean({ allowWarnings: true });
|
||||||
|
monitoring.stop();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('L4.21 - should show search type options', async ({ page }) => {
|
||||||
|
const monitoring = setupEnhancedMonitoring(page);
|
||||||
|
|
||||||
|
await page.goto('/settings/codexlens', { waitUntil: 'networkidle' as const });
|
||||||
|
|
||||||
|
const searchTab = page.getByRole('tab', { name: /Search/i });
|
||||||
|
const searchTabVisible = await searchTab.isVisible().catch(() => false);
|
||||||
|
if (searchTabVisible) {
|
||||||
|
await searchTab.click();
|
||||||
|
|
||||||
|
// Click on search type selector to open dropdown
|
||||||
|
const searchTypeSelector = page.getByLabel(/Search Type/i).or(
|
||||||
|
page.getByRole('combobox', { name: /Search Type/i })
|
||||||
|
);
|
||||||
|
const typeVisible = await searchTypeSelector.isVisible().catch(() => false);
|
||||||
|
if (typeVisible) {
|
||||||
|
await searchTypeSelector.first().click();
|
||||||
|
await page.waitForTimeout(300);
|
||||||
|
|
||||||
|
// Check for expected search type options
|
||||||
|
const searchTypes = ['Content Search', 'File Search', 'Symbol Search'];
|
||||||
|
for (const type of searchTypes) {
|
||||||
|
const option = page.getByRole('option', { name: new RegExp(type, 'i') });
|
||||||
|
const optionVisible = await option.isVisible().catch(() => false);
|
||||||
|
if (optionVisible) {
|
||||||
|
await expect(option).toBeVisible();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
monitoring.assertClean({ allowWarnings: true });
|
||||||
|
monitoring.stop();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('L4.22 - should show search mode options for Content Search', async ({ page }) => {
|
||||||
|
const monitoring = setupEnhancedMonitoring(page);
|
||||||
|
|
||||||
|
await page.goto('/settings/codexlens', { waitUntil: 'networkidle' as const });
|
||||||
|
|
||||||
|
const searchTab = page.getByRole('tab', { name: /Search/i });
|
||||||
|
const searchTabVisible = await searchTab.isVisible().catch(() => false);
|
||||||
|
if (searchTabVisible) {
|
||||||
|
await searchTab.click();
|
||||||
|
|
||||||
|
// Select Content Search first
|
||||||
|
const searchTypeSelector = page.getByLabel(/Search Type/i).or(
|
||||||
|
page.getByRole('combobox', { name: /Search Type/i })
|
||||||
|
);
|
||||||
|
const typeVisible = await searchTypeSelector.isVisible().catch(() => false);
|
||||||
|
if (typeVisible) {
|
||||||
|
await searchTypeSelector.first().click();
|
||||||
|
await page.waitForTimeout(300);
|
||||||
|
|
||||||
|
const contentOption = page.getByRole('option', { name: /Content Search/i });
|
||||||
|
const contentVisible = await contentOption.isVisible().catch(() => false);
|
||||||
|
if (contentVisible) {
|
||||||
|
await contentOption.click();
|
||||||
|
|
||||||
|
// Click on search mode selector to open dropdown
|
||||||
|
const searchModeSelector = page.getByLabel(/Search Mode/i).or(
|
||||||
|
page.getByRole('combobox', { name: /Search Mode/i })
|
||||||
|
);
|
||||||
|
const modeVisible = await searchModeSelector.isVisible().catch(() => false);
|
||||||
|
if (modeVisible) {
|
||||||
|
await searchModeSelector.first().click();
|
||||||
|
await page.waitForTimeout(300);
|
||||||
|
|
||||||
|
// Check for expected search mode options
|
||||||
|
const searchModes = ['Semantic', 'Exact', 'Fuzzy'];
|
||||||
|
for (const mode of searchModes) {
|
||||||
|
const option = page.getByRole('option', { name: new RegExp(mode, 'i') });
|
||||||
|
const optionVisible = await option.isVisible().catch(() => false);
|
||||||
|
if (optionVisible) {
|
||||||
|
await expect(option).toBeVisible();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
monitoring.assertClean({ allowWarnings: true });
|
||||||
|
monitoring.stop();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('L4.23 - should hide search mode for Symbol Search', async ({ page }) => {
|
||||||
|
const monitoring = setupEnhancedMonitoring(page);
|
||||||
|
|
||||||
|
await page.goto('/settings/codexlens', { waitUntil: 'networkidle' as const });
|
||||||
|
|
||||||
|
const searchTab = page.getByRole('tab', { name: /Search/i });
|
||||||
|
const searchTabVisible = await searchTab.isVisible().catch(() => false);
|
||||||
|
if (searchTabVisible) {
|
||||||
|
await searchTab.click();
|
||||||
|
|
||||||
|
// Select Symbol Search
|
||||||
|
const searchTypeSelector = page.getByLabel(/Search Type/i).or(
|
||||||
|
page.getByRole('combobox', { name: /Search Type/i })
|
||||||
|
);
|
||||||
|
const typeVisible = await searchTypeSelector.isVisible().catch(() => false);
|
||||||
|
if (typeVisible) {
|
||||||
|
await searchTypeSelector.first().click();
|
||||||
|
await page.waitForTimeout(300);
|
||||||
|
|
||||||
|
const symbolOption = page.getByRole('option', { name: /Symbol Search/i });
|
||||||
|
const symbolVisible = await symbolOption.isVisible().catch(() => false);
|
||||||
|
if (symbolVisible) {
|
||||||
|
await symbolOption.click();
|
||||||
|
await page.waitForTimeout(300);
|
||||||
|
|
||||||
|
// Verify search mode selector is hidden or removed
|
||||||
|
const searchModeSelector = page.getByLabel(/Search Mode/i).or(
|
||||||
|
page.getByRole('combobox', { name: /Search Mode/i })
|
||||||
|
);
|
||||||
|
const modeVisible = await searchModeSelector.isVisible().catch(() => false);
|
||||||
|
|
||||||
|
// Search mode should not be visible for Symbol Search
|
||||||
|
expect(modeVisible).toBe(false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
monitoring.assertClean({ allowWarnings: true });
|
||||||
|
monitoring.stop();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('L4.24 - should disable search button with empty query', async ({ page }) => {
|
||||||
|
const monitoring = setupEnhancedMonitoring(page);
|
||||||
|
|
||||||
|
await page.goto('/settings/codexlens', { waitUntil: 'networkidle' as const });
|
||||||
|
|
||||||
|
const searchTab = page.getByRole('tab', { name: /Search/i });
|
||||||
|
const searchTabVisible = await searchTab.isVisible().catch(() => false);
|
||||||
|
if (searchTabVisible) {
|
||||||
|
await searchTab.click();
|
||||||
|
|
||||||
|
// Verify search button is disabled with empty query
|
||||||
|
const searchButton = page.getByRole('button', { name: /Search/i });
|
||||||
|
const buttonVisible = await searchButton.isVisible().catch(() => false);
|
||||||
|
if (buttonVisible) {
|
||||||
|
// Check if button is disabled when query is empty
|
||||||
|
const isDisabled = await searchButton.first().isDisabled().catch(() => false);
|
||||||
|
if (isDisabled) {
|
||||||
|
await expect(searchButton.first()).toBeDisabled();
|
||||||
|
} else {
|
||||||
|
// If not disabled, check for validation on click
|
||||||
|
const queryInput = page.getByPlaceholder(/Search query/i).or(
|
||||||
|
page.getByLabel(/Query/i)
|
||||||
|
);
|
||||||
|
const inputVisible = await queryInput.isVisible().catch(() => false);
|
||||||
|
if (inputVisible) {
|
||||||
|
// Ensure input is empty
|
||||||
|
const inputValue = await queryInput.first().inputValue();
|
||||||
|
expect(inputValue || '').toBe('');
|
||||||
|
|
||||||
|
// Try clicking search button
|
||||||
|
await searchButton.first().click();
|
||||||
|
await page.waitForTimeout(300);
|
||||||
|
|
||||||
|
// Check for error message
|
||||||
|
const errorMessage = page.getByText(/required|enter a query|empty query/i);
|
||||||
|
const errorVisible = await errorMessage.isVisible().catch(() => false);
|
||||||
|
if (errorVisible) {
|
||||||
|
await expect(errorMessage).toBeVisible();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
monitoring.assertClean({ allowWarnings: true });
|
||||||
|
monitoring.stop();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('L4.25 - should enable search button with valid query', async ({ page }) => {
|
||||||
|
const monitoring = setupEnhancedMonitoring(page);
|
||||||
|
|
||||||
|
await page.goto('/settings/codexlens', { waitUntil: 'networkidle' as const });
|
||||||
|
|
||||||
|
const searchTab = page.getByRole('tab', { name: /Search/i });
|
||||||
|
const searchTabVisible = await searchTab.isVisible().catch(() => false);
|
||||||
|
if (searchTabVisible) {
|
||||||
|
await searchTab.click();
|
||||||
|
|
||||||
|
// Enter query text
|
||||||
|
const queryInput = page.getByPlaceholder(/Search query/i).or(
|
||||||
|
page.getByLabel(/Query/i)
|
||||||
|
);
|
||||||
|
const inputVisible = await queryInput.isVisible().catch(() => false);
|
||||||
|
if (inputVisible) {
|
||||||
|
await queryInput.first().fill('test query');
|
||||||
|
await page.waitForTimeout(300);
|
||||||
|
|
||||||
|
// Verify search button is enabled
|
||||||
|
const searchButton = page.getByRole('button', { name: /Search/i });
|
||||||
|
const buttonVisible = await searchButton.isVisible().catch(() => false);
|
||||||
|
if (buttonVisible) {
|
||||||
|
const isEnabled = await searchButton.first().isEnabled().catch(() => true);
|
||||||
|
expect(isEnabled).toBe(true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
monitoring.assertClean({ allowWarnings: true });
|
||||||
|
monitoring.stop();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('L4.26 - should show loading state during search', async ({ page }) => {
|
||||||
|
const monitoring = setupEnhancedMonitoring(page);
|
||||||
|
|
||||||
|
await page.goto('/settings/codexlens', { waitUntil: 'networkidle' as const });
|
||||||
|
|
||||||
|
const searchTab = page.getByRole('tab', { name: /Search/i });
|
||||||
|
const searchTabVisible = await searchTab.isVisible().catch(() => false);
|
||||||
|
if (searchTabVisible) {
|
||||||
|
await searchTab.click();
|
||||||
|
|
||||||
|
// Enter query text
|
||||||
|
const queryInput = page.getByPlaceholder(/Search query/i).or(
|
||||||
|
page.getByLabel(/Query/i)
|
||||||
|
);
|
||||||
|
const inputVisible = await queryInput.isVisible().catch(() => false);
|
||||||
|
if (inputVisible) {
|
||||||
|
await queryInput.first().fill('test query');
|
||||||
|
await page.waitForTimeout(300);
|
||||||
|
|
||||||
|
// Click search button
|
||||||
|
const searchButton = page.getByRole('button', { name: /Search/i });
|
||||||
|
const buttonVisible = await searchButton.isVisible().catch(() => false);
|
||||||
|
if (buttonVisible) {
|
||||||
|
// Set up route handler to delay response
|
||||||
|
await page.route('**/api/codexlens/search**', async (route) => {
|
||||||
|
// Delay response to observe loading state
|
||||||
|
await new Promise(resolve => setTimeout(resolve, 1000));
|
||||||
|
route.continue();
|
||||||
|
});
|
||||||
|
|
||||||
|
await searchButton.first().click();
|
||||||
|
|
||||||
|
// Check for loading indicator
|
||||||
|
const loadingIndicator = page.getByText(/Searching|Loading/i).or(
|
||||||
|
page.getByRole('button', { name: /Search/i }).filter({ hasText: /Searching|Loading/i })
|
||||||
|
).or(page.locator('[aria-busy="true"]'));
|
||||||
|
|
||||||
|
// Wait briefly to see if loading state appears
|
||||||
|
await page.waitForTimeout(200);
|
||||||
|
const loadingVisible = await loadingIndicator.isVisible().catch(() => false);
|
||||||
|
|
||||||
|
// Clean up route
|
||||||
|
await page.unroute('**/api/codexlens/search**');
|
||||||
|
|
||||||
|
// Loading state may or may not be visible depending on speed
|
||||||
|
if (loadingVisible) {
|
||||||
|
await expect(loadingIndicator.first()).toBeVisible();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
monitoring.assertClean({ allowWarnings: true });
|
||||||
|
monitoring.stop();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('L4.27 - should display search results', async ({ page }) => {
|
||||||
|
const monitoring = setupEnhancedMonitoring(page);
|
||||||
|
|
||||||
|
await page.goto('/settings/codexlens', { waitUntil: 'networkidle' as const });
|
||||||
|
|
||||||
|
const searchTab = page.getByRole('tab', { name: /Search/i });
|
||||||
|
const searchTabVisible = await searchTab.isVisible().catch(() => false);
|
||||||
|
if (searchTabVisible) {
|
||||||
|
await searchTab.click();
|
||||||
|
|
||||||
|
// Select Content Search
|
||||||
|
const searchTypeSelector = page.getByLabel(/Search Type/i).or(
|
||||||
|
page.getByRole('combobox', { name: /Search Type/i })
|
||||||
|
);
|
||||||
|
const typeVisible = await searchTypeSelector.isVisible().catch(() => false);
|
||||||
|
if (typeVisible) {
|
||||||
|
await searchTypeSelector.first().click();
|
||||||
|
await page.waitForTimeout(300);
|
||||||
|
|
||||||
|
const contentOption = page.getByRole('option', { name: /Content Search/i });
|
||||||
|
const contentVisible = await contentOption.isVisible().catch(() => false);
|
||||||
|
if (contentVisible) {
|
||||||
|
await contentOption.click();
|
||||||
|
|
||||||
|
// Enter query text
|
||||||
|
const queryInput = page.getByPlaceholder(/Search query/i).or(
|
||||||
|
page.getByLabel(/Query/i)
|
||||||
|
);
|
||||||
|
const inputVisible = await queryInput.isVisible().catch(() => false);
|
||||||
|
if (inputVisible) {
|
||||||
|
await queryInput.first().fill('test');
|
||||||
|
await page.waitForTimeout(300);
|
||||||
|
|
||||||
|
// Click search button
|
||||||
|
const searchButton = page.getByRole('button', { name: /Search/i });
|
||||||
|
const buttonVisible = await searchButton.isVisible().catch(() => false);
|
||||||
|
if (buttonVisible) {
|
||||||
|
await searchButton.first().click();
|
||||||
|
|
||||||
|
// Wait for results
|
||||||
|
await page.waitForTimeout(2000);
|
||||||
|
|
||||||
|
// Check for results area
|
||||||
|
const resultsArea = page.getByText(/Results|No results|Found/i).or(
|
||||||
|
page.locator('[data-testid="search-results"]')
|
||||||
|
).or(page.locator('.search-results'));
|
||||||
|
const resultsVisible = await resultsArea.isVisible().catch(() => false);
|
||||||
|
|
||||||
|
if (resultsVisible) {
|
||||||
|
await expect(resultsArea.first()).toBeVisible();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
monitoring.assertClean({ allowWarnings: true });
|
||||||
|
monitoring.stop();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('L4.28 - should handle search between different types', async ({ page }) => {
|
||||||
|
const monitoring = setupEnhancedMonitoring(page);
|
||||||
|
|
||||||
|
await page.goto('/settings/codexlens', { waitUntil: 'networkidle' as const });
|
||||||
|
|
||||||
|
const searchTab = page.getByRole('tab', { name: /Search/i });
|
||||||
|
const searchTabVisible = await searchTab.isVisible().catch(() => false);
|
||||||
|
if (searchTabVisible) {
|
||||||
|
await searchTab.click();
|
||||||
|
|
||||||
|
// Start with Content Search
|
||||||
|
const searchTypeSelector = page.getByLabel(/Search Type/i).or(
|
||||||
|
page.getByRole('combobox', { name: /Search Type/i })
|
||||||
|
);
|
||||||
|
const typeVisible = await searchTypeSelector.isVisible().catch(() => false);
|
||||||
|
if (typeVisible) {
|
||||||
|
// Select Content Search
|
||||||
|
await searchTypeSelector.first().click();
|
||||||
|
await page.waitForTimeout(300);
|
||||||
|
const contentOption = page.getByRole('option', { name: /Content Search/i });
|
||||||
|
const contentVisible = await contentOption.isVisible().catch(() => false);
|
||||||
|
if (contentVisible) {
|
||||||
|
await contentOption.click();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Verify mode selector is visible
|
||||||
|
const searchModeSelector = page.getByLabel(/Search Mode/i).or(
|
||||||
|
page.getByRole('combobox', { name: /Search Mode/i })
|
||||||
|
);
|
||||||
|
let modeVisible = await searchModeSelector.isVisible().catch(() => false);
|
||||||
|
expect(modeVisible).toBe(true);
|
||||||
|
|
||||||
|
// Switch to Symbol Search
|
||||||
|
await searchTypeSelector.first().click();
|
||||||
|
await page.waitForTimeout(300);
|
||||||
|
const symbolOption = page.getByRole('option', { name: /Symbol Search/i });
|
||||||
|
const symbolVisible = await symbolOption.isVisible().catch(() => false);
|
||||||
|
if (symbolVisible) {
|
||||||
|
await symbolOption.click();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Verify mode selector is hidden
|
||||||
|
modeVisible = await searchModeSelector.isVisible().catch(() => false);
|
||||||
|
expect(modeVisible).toBe(false);
|
||||||
|
|
||||||
|
// Switch to File Search
|
||||||
|
await searchTypeSelector.first().click();
|
||||||
|
await page.waitForTimeout(300);
|
||||||
|
const fileOption = page.getByRole('option', { name: /File Search/i });
|
||||||
|
const fileVisible = await fileOption.isVisible().catch(() => false);
|
||||||
|
if (fileVisible) {
|
||||||
|
await fileOption.click();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Verify mode selector is visible again
|
||||||
|
modeVisible = await searchModeSelector.isVisible().catch(() => false);
|
||||||
|
expect(modeVisible).toBe(true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
monitoring.assertClean({ allowWarnings: true });
|
||||||
|
monitoring.stop();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('L4.29 - should handle empty search results gracefully', async ({ page }) => {
|
||||||
|
const monitoring = setupEnhancedMonitoring(page);
|
||||||
|
|
||||||
|
await page.goto('/settings/codexlens', { waitUntil: 'networkidle' as const });
|
||||||
|
|
||||||
|
const searchTab = page.getByRole('tab', { name: /Search/i });
|
||||||
|
const searchTabVisible = await searchTab.isVisible().catch(() => false);
|
||||||
|
if (searchTabVisible) {
|
||||||
|
await searchTab.click();
|
||||||
|
|
||||||
|
// Enter a unique query that likely has no results
|
||||||
|
const queryInput = page.getByPlaceholder(/Search query/i).or(
|
||||||
|
page.getByLabel(/Query/i)
|
||||||
|
);
|
||||||
|
const inputVisible = await queryInput.isVisible().catch(() => false);
|
||||||
|
if (inputVisible) {
|
||||||
|
await queryInput.first().fill('nonexistent-unique-query-xyz-123');
|
||||||
|
await page.waitForTimeout(300);
|
||||||
|
|
||||||
|
// Click search button
|
||||||
|
const searchButton = page.getByRole('button', { name: /Search/i });
|
||||||
|
const buttonVisible = await searchButton.isVisible().catch(() => false);
|
||||||
|
if (buttonVisible) {
|
||||||
|
await searchButton.first().click();
|
||||||
|
|
||||||
|
// Wait for results
|
||||||
|
await page.waitForTimeout(2000);
|
||||||
|
|
||||||
|
// Check for empty state message
|
||||||
|
const emptyState = page.getByText(/No results|Found 0|No matches/i);
|
||||||
|
const emptyVisible = await emptyState.isVisible().catch(() => false);
|
||||||
|
if (emptyVisible) {
|
||||||
|
await expect(emptyState.first()).toBeVisible();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
monitoring.assertClean({ allowWarnings: true });
|
||||||
|
monitoring.stop();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('L4.30 - should handle search mode selection', async ({ page }) => {
|
||||||
|
const monitoring = setupEnhancedMonitoring(page);
|
||||||
|
|
||||||
|
await page.goto('/settings/codexlens', { waitUntil: 'networkidle' as const });
|
||||||
|
|
||||||
|
const searchTab = page.getByRole('tab', { name: /Search/i });
|
||||||
|
const searchTabVisible = await searchTab.isVisible().catch(() => false);
|
||||||
|
if (searchTabVisible) {
|
||||||
|
await searchTab.click();
|
||||||
|
|
||||||
|
// Ensure we're on Content or File Search (which have modes)
|
||||||
|
const searchTypeSelector = page.getByLabel(/Search Type/i).or(
|
||||||
|
page.getByRole('combobox', { name: /Search Type/i })
|
||||||
|
);
|
||||||
|
const typeVisible = await searchTypeSelector.isVisible().catch(() => false);
|
||||||
|
if (typeVisible) {
|
||||||
|
await searchTypeSelector.first().click();
|
||||||
|
await page.waitForTimeout(300);
|
||||||
|
|
||||||
|
const contentOption = page.getByRole('option', { name: /Content Search/i });
|
||||||
|
const contentVisible = await contentOption.isVisible().catch(() => false);
|
||||||
|
if (contentVisible) {
|
||||||
|
await contentOption.click();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Try different search modes
|
||||||
|
const searchModes = ['Semantic', 'Exact'];
|
||||||
|
for (const mode of searchModes) {
|
||||||
|
const searchModeSelector = page.getByLabel(/Search Mode/i).or(
|
||||||
|
page.getByRole('combobox', { name: /Search Mode/i })
|
||||||
|
);
|
||||||
|
const modeVisible = await searchModeSelector.isVisible().catch(() => false);
|
||||||
|
if (modeVisible) {
|
||||||
|
await searchModeSelector.first().click();
|
||||||
|
await page.waitForTimeout(300);
|
||||||
|
|
||||||
|
const modeOption = page.getByRole('option', { name: new RegExp(mode, 'i') });
|
||||||
|
const optionVisible = await modeOption.isVisible().catch(() => false);
|
||||||
|
if (optionVisible) {
|
||||||
|
await modeOption.click();
|
||||||
|
await page.waitForTimeout(300);
|
||||||
|
|
||||||
|
// Verify selection
|
||||||
|
await expect(searchModeSelector.first()).toContainText(mode, { timeout: 2000 }).catch(() => {
|
||||||
|
// Selection may or may not be reflected in selector text
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
monitoring.assertClean({ allowWarnings: true });
|
||||||
|
monitoring.stop();
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ import { listTools } from '../../tools/index.js';
|
|||||||
import { loadProjectOverview } from '../data-aggregator.js';
|
import { loadProjectOverview } from '../data-aggregator.js';
|
||||||
import { resolvePath } from '../../utils/path-resolver.js';
|
import { resolvePath } from '../../utils/path-resolver.js';
|
||||||
import { join } from 'path';
|
import { join } from 'path';
|
||||||
|
import { readFileSync, writeFileSync, existsSync } from 'fs';
|
||||||
import type { RouteContext } from './types.js';
|
import type { RouteContext } from './types.js';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -45,6 +46,77 @@ export async function handleCcwRoutes(ctx: RouteContext): Promise<boolean> {
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// API: Get Project Guidelines
|
||||||
|
if (pathname === '/api/ccw/guidelines' && req.method === 'GET') {
|
||||||
|
const projectPath = url.searchParams.get('path') || initialPath;
|
||||||
|
const resolvedPath = resolvePath(projectPath);
|
||||||
|
const guidelinesFile = join(resolvedPath, '.workflow', 'project-guidelines.json');
|
||||||
|
|
||||||
|
if (!existsSync(guidelinesFile)) {
|
||||||
|
res.writeHead(200, { 'Content-Type': 'application/json' });
|
||||||
|
res.end(JSON.stringify({ guidelines: null }));
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const content = readFileSync(guidelinesFile, 'utf-8');
|
||||||
|
const guidelines = JSON.parse(content);
|
||||||
|
res.writeHead(200, { 'Content-Type': 'application/json' });
|
||||||
|
res.end(JSON.stringify({ guidelines }));
|
||||||
|
} catch (err) {
|
||||||
|
res.writeHead(500, { 'Content-Type': 'application/json' });
|
||||||
|
res.end(JSON.stringify({ error: 'Failed to read guidelines file' }));
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// API: Update Project Guidelines
|
||||||
|
if (pathname === '/api/ccw/guidelines' && req.method === 'PUT') {
|
||||||
|
handlePostRequest(req, res, async (body) => {
|
||||||
|
const projectPath = url.searchParams.get('path') || initialPath;
|
||||||
|
const resolvedPath = resolvePath(projectPath);
|
||||||
|
const guidelinesFile = join(resolvedPath, '.workflow', 'project-guidelines.json');
|
||||||
|
|
||||||
|
try {
|
||||||
|
const data = body as Record<string, unknown>;
|
||||||
|
|
||||||
|
// Read existing file to preserve _metadata.created_at
|
||||||
|
let existingMetadata: Record<string, unknown> = {};
|
||||||
|
if (existsSync(guidelinesFile)) {
|
||||||
|
try {
|
||||||
|
const existing = JSON.parse(readFileSync(guidelinesFile, 'utf-8'));
|
||||||
|
existingMetadata = existing._metadata || {};
|
||||||
|
} catch { /* ignore parse errors */ }
|
||||||
|
}
|
||||||
|
|
||||||
|
// Build the guidelines object
|
||||||
|
const guidelines = {
|
||||||
|
conventions: data.conventions || { coding_style: [], naming_patterns: [], file_structure: [], documentation: [] },
|
||||||
|
constraints: data.constraints || { architecture: [], tech_stack: [], performance: [], security: [] },
|
||||||
|
quality_rules: data.quality_rules || [],
|
||||||
|
learnings: data.learnings || [],
|
||||||
|
_metadata: {
|
||||||
|
created_at: (existingMetadata.created_at as string) || new Date().toISOString(),
|
||||||
|
updated_at: new Date().toISOString(),
|
||||||
|
version: (existingMetadata.version as string) || '1.0.0',
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
writeFileSync(guidelinesFile, JSON.stringify(guidelines, null, 2), 'utf-8');
|
||||||
|
|
||||||
|
broadcastToClients({
|
||||||
|
type: 'PROJECT_GUIDELINES_UPDATED',
|
||||||
|
payload: { timestamp: new Date().toISOString() },
|
||||||
|
});
|
||||||
|
|
||||||
|
return { success: true, guidelines };
|
||||||
|
} catch (err) {
|
||||||
|
return { error: (err as Error).message, status: 500 };
|
||||||
|
}
|
||||||
|
});
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
// API: CCW Upgrade
|
// API: CCW Upgrade
|
||||||
if (pathname === '/api/ccw/upgrade' && req.method === 'POST') {
|
if (pathname === '/api/ccw/upgrade' && req.method === 'POST') {
|
||||||
handlePostRequest(req, res, async (body) => {
|
handlePostRequest(req, res, async (body) => {
|
||||||
|
|||||||
@@ -58,13 +58,24 @@ interface ActiveExecution {
|
|||||||
mode: string;
|
mode: string;
|
||||||
prompt: string;
|
prompt: string;
|
||||||
startTime: number;
|
startTime: number;
|
||||||
output: string;
|
output: string[]; // Array-based buffer to limit memory usage
|
||||||
status: 'running' | 'completed' | 'error';
|
status: 'running' | 'completed' | 'error';
|
||||||
completedTimestamp?: number; // When execution completed (for 5-minute retention)
|
completedTimestamp?: number; // When execution completed (for 5-minute retention)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// API response type with output as string (for backward compatibility)
|
||||||
|
type ActiveExecutionDto = Omit<ActiveExecution, 'output'> & { output: string };
|
||||||
|
|
||||||
const activeExecutions = new Map<string, ActiveExecution>();
|
const activeExecutions = new Map<string, ActiveExecution>();
|
||||||
const EXECUTION_RETENTION_MS = 5 * 60 * 1000; // 5 minutes
|
const EXECUTION_RETENTION_MS = 5 * 60 * 1000; // 5 minutes
|
||||||
|
const CLEANUP_INTERVAL_MS = 60 * 1000; // 1 minute - periodic cleanup interval
|
||||||
|
const MAX_OUTPUT_BUFFER_LINES = 1000; // Max lines to keep in memory per execution
|
||||||
|
const MAX_ACTIVE_EXECUTIONS = 200; // Max concurrent executions in memory
|
||||||
|
|
||||||
|
// Enable periodic cleanup to prevent memory buildup
|
||||||
|
setInterval(() => {
|
||||||
|
cleanupStaleExecutions();
|
||||||
|
}, CLEANUP_INTERVAL_MS);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Cleanup stale completed executions older than retention period
|
* Cleanup stale completed executions older than retention period
|
||||||
@@ -93,9 +104,13 @@ export function cleanupStaleExecutions(): void {
|
|||||||
/**
|
/**
|
||||||
* Get all active CLI executions
|
* Get all active CLI executions
|
||||||
* Used by frontend to restore state when view is opened during execution
|
* Used by frontend to restore state when view is opened during execution
|
||||||
|
* Note: Converts output array back to string for API compatibility
|
||||||
*/
|
*/
|
||||||
export function getActiveExecutions(): ActiveExecution[] {
|
export function getActiveExecutions(): ActiveExecutionDto[] {
|
||||||
return Array.from(activeExecutions.values());
|
return Array.from(activeExecutions.values()).map(exec => ({
|
||||||
|
...exec,
|
||||||
|
output: exec.output.join('') // Convert array buffer to string for API
|
||||||
|
}));
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -122,21 +137,30 @@ export function updateActiveExecution(event: {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (type === 'started') {
|
if (type === 'started') {
|
||||||
// Create new active execution
|
// Check map size limit before creating new execution
|
||||||
|
if (activeExecutions.size >= MAX_ACTIVE_EXECUTIONS) {
|
||||||
|
console.warn(`[ActiveExec] Max executions limit reached (${MAX_ACTIVE_EXECUTIONS}), cleanup may be needed`);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create new active execution with array-based output buffer
|
||||||
activeExecutions.set(executionId, {
|
activeExecutions.set(executionId, {
|
||||||
id: executionId,
|
id: executionId,
|
||||||
tool: tool || 'unknown',
|
tool: tool || 'unknown',
|
||||||
mode: mode || 'analysis',
|
mode: mode || 'analysis',
|
||||||
prompt: (prompt || '').substring(0, 500),
|
prompt: (prompt || '').substring(0, 500),
|
||||||
startTime: Date.now(),
|
startTime: Date.now(),
|
||||||
output: '',
|
output: [], // Initialize as empty array instead of empty string
|
||||||
status: 'running'
|
status: 'running'
|
||||||
});
|
});
|
||||||
} else if (type === 'output') {
|
} else if (type === 'output') {
|
||||||
// Append output to existing execution
|
// Append output to existing execution using array with size limit
|
||||||
const activeExec = activeExecutions.get(executionId);
|
const activeExec = activeExecutions.get(executionId);
|
||||||
if (activeExec && output) {
|
if (activeExec && output) {
|
||||||
activeExec.output += output;
|
activeExec.output.push(output);
|
||||||
|
// Keep buffer size under limit by shifting old entries
|
||||||
|
if (activeExec.output.length > MAX_OUTPUT_BUFFER_LINES) {
|
||||||
|
activeExec.output.shift(); // Remove oldest entry
|
||||||
|
}
|
||||||
}
|
}
|
||||||
} else if (type === 'completed') {
|
} else if (type === 'completed') {
|
||||||
// Mark as completed with timestamp for retention-based cleanup
|
// Mark as completed with timestamp for retention-based cleanup
|
||||||
@@ -487,6 +511,7 @@ export async function handleCliRoutes(ctx: RouteContext): Promise<boolean> {
|
|||||||
const activeExec = activeExecutions.get(executionId);
|
const activeExec = activeExecutions.get(executionId);
|
||||||
if (activeExec) {
|
if (activeExec) {
|
||||||
// Return active execution data as conversation record format
|
// Return active execution data as conversation record format
|
||||||
|
// Note: Convert output array buffer back to string for API compatibility
|
||||||
const activeConversation = {
|
const activeConversation = {
|
||||||
id: activeExec.id,
|
id: activeExec.id,
|
||||||
tool: activeExec.tool,
|
tool: activeExec.tool,
|
||||||
@@ -497,7 +522,7 @@ export async function handleCliRoutes(ctx: RouteContext): Promise<boolean> {
|
|||||||
turn: 1,
|
turn: 1,
|
||||||
timestamp: new Date(activeExec.startTime).toISOString(),
|
timestamp: new Date(activeExec.startTime).toISOString(),
|
||||||
prompt: activeExec.prompt,
|
prompt: activeExec.prompt,
|
||||||
output: { stdout: activeExec.output, stderr: '' },
|
output: { stdout: activeExec.output.join(''), stderr: '' }, // Convert array to string
|
||||||
duration_ms: activeExec.completedTimestamp
|
duration_ms: activeExec.completedTimestamp
|
||||||
? activeExec.completedTimestamp - activeExec.startTime
|
? activeExec.completedTimestamp - activeExec.startTime
|
||||||
: Date.now() - activeExec.startTime
|
: Date.now() - activeExec.startTime
|
||||||
@@ -662,13 +687,17 @@ export async function handleCliRoutes(ctx: RouteContext): Promise<boolean> {
|
|||||||
const executionId = `${Date.now()}-${tool}`;
|
const executionId = `${Date.now()}-${tool}`;
|
||||||
|
|
||||||
// Store active execution for state recovery
|
// Store active execution for state recovery
|
||||||
|
// Check map size limit before creating new execution
|
||||||
|
if (activeExecutions.size >= MAX_ACTIVE_EXECUTIONS) {
|
||||||
|
console.warn(`[ActiveExec] Max executions limit reached (${MAX_ACTIVE_EXECUTIONS}), cleanup may be needed`);
|
||||||
|
}
|
||||||
activeExecutions.set(executionId, {
|
activeExecutions.set(executionId, {
|
||||||
id: executionId,
|
id: executionId,
|
||||||
tool,
|
tool,
|
||||||
mode: mode || 'analysis',
|
mode: mode || 'analysis',
|
||||||
prompt: prompt.substring(0, 500), // Truncate for display
|
prompt: prompt.substring(0, 500), // Truncate for display
|
||||||
startTime: Date.now(),
|
startTime: Date.now(),
|
||||||
output: '',
|
output: [], // Initialize as empty array for memory-efficient buffering
|
||||||
status: 'running'
|
status: 'running'
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -701,10 +730,14 @@ export async function handleCliRoutes(ctx: RouteContext): Promise<boolean> {
|
|||||||
// CliOutputUnit handler: use SmartContentFormatter for intelligent formatting (never returns null)
|
// CliOutputUnit handler: use SmartContentFormatter for intelligent formatting (never returns null)
|
||||||
const content = SmartContentFormatter.format(unit.content, unit.type);
|
const content = SmartContentFormatter.format(unit.content, unit.type);
|
||||||
|
|
||||||
// Append to active execution buffer
|
// Append to active execution buffer using array with size limit
|
||||||
const activeExec = activeExecutions.get(executionId);
|
const activeExec = activeExecutions.get(executionId);
|
||||||
if (activeExec) {
|
if (activeExec) {
|
||||||
activeExec.output += content || '';
|
activeExec.output.push(content || '');
|
||||||
|
// Keep buffer size under limit by shifting old entries
|
||||||
|
if (activeExec.output.length > MAX_OUTPUT_BUFFER_LINES) {
|
||||||
|
activeExec.output.shift(); // Remove oldest entry
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
broadcastToClients({
|
broadcastToClients({
|
||||||
@@ -753,7 +786,9 @@ export async function handleCliRoutes(ctx: RouteContext): Promise<boolean> {
|
|||||||
|
|
||||||
return {
|
return {
|
||||||
success: result.success,
|
success: result.success,
|
||||||
execution: result.execution
|
execution: result.execution,
|
||||||
|
parsedOutput: result.parsedOutput, // Filtered output (excludes metadata/progress)
|
||||||
|
finalOutput: result.finalOutput // Agent message only (for --final flag)
|
||||||
};
|
};
|
||||||
|
|
||||||
} catch (error: unknown) {
|
} catch (error: unknown) {
|
||||||
|
|||||||
@@ -341,6 +341,108 @@ export async function handleCodexLensIndexRoutes(ctx: RouteContext): Promise<boo
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// API: CodexLens Update (Incremental index update)
|
||||||
|
if (pathname === '/api/codexlens/update' && req.method === 'POST') {
|
||||||
|
handlePostRequest(req, res, async (body) => {
|
||||||
|
const { path: projectPath, indexType = 'vector', embeddingModel = 'code', embeddingBackend = 'fastembed', maxWorkers = 1 } = body as {
|
||||||
|
path?: unknown;
|
||||||
|
indexType?: unknown;
|
||||||
|
embeddingModel?: unknown;
|
||||||
|
embeddingBackend?: unknown;
|
||||||
|
maxWorkers?: unknown;
|
||||||
|
};
|
||||||
|
const targetPath = typeof projectPath === 'string' && projectPath.trim().length > 0 ? projectPath : initialPath;
|
||||||
|
const resolvedIndexType = indexType === 'normal' ? 'normal' : 'vector';
|
||||||
|
const resolvedEmbeddingModel = typeof embeddingModel === 'string' && embeddingModel.trim().length > 0 ? embeddingModel : 'code';
|
||||||
|
const resolvedEmbeddingBackend = typeof embeddingBackend === 'string' && embeddingBackend.trim().length > 0 ? embeddingBackend : 'fastembed';
|
||||||
|
const resolvedMaxWorkers = typeof maxWorkers === 'number' ? maxWorkers : Number(maxWorkers);
|
||||||
|
|
||||||
|
// Pre-check: Verify embedding backend availability before proceeding with vector indexing
|
||||||
|
if (resolvedIndexType !== 'normal') {
|
||||||
|
if (resolvedEmbeddingBackend === 'litellm') {
|
||||||
|
const installResult = await ensureLiteLLMEmbedderReady();
|
||||||
|
if (!installResult.success) {
|
||||||
|
return {
|
||||||
|
success: false,
|
||||||
|
error: installResult.error || 'LiteLLM embedding backend is not available. Please install ccw-litellm first.',
|
||||||
|
status: 500
|
||||||
|
};
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
const semanticStatus = await checkSemanticStatus();
|
||||||
|
if (!semanticStatus.available) {
|
||||||
|
return {
|
||||||
|
success: false,
|
||||||
|
error: semanticStatus.error || 'FastEmbed semantic backend is not available. Please install semantic dependencies first.',
|
||||||
|
status: 500
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Build CLI arguments for incremental update using 'index update' subcommand
|
||||||
|
const args = ['index', 'update', targetPath, '--json'];
|
||||||
|
if (resolvedIndexType === 'normal') {
|
||||||
|
args.push('--no-embeddings');
|
||||||
|
} else {
|
||||||
|
args.push('--model', resolvedEmbeddingModel);
|
||||||
|
if (resolvedEmbeddingBackend && resolvedEmbeddingBackend !== 'fastembed') {
|
||||||
|
args.push('--backend', resolvedEmbeddingBackend);
|
||||||
|
}
|
||||||
|
if (!Number.isNaN(resolvedMaxWorkers) && resolvedMaxWorkers > 1) {
|
||||||
|
args.push('--max-workers', String(resolvedMaxWorkers));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Broadcast start event
|
||||||
|
broadcastToClients({
|
||||||
|
type: 'CODEXLENS_INDEX_PROGRESS',
|
||||||
|
payload: { stage: 'start', message: 'Starting incremental index update...', percent: 0, path: targetPath, indexType: resolvedIndexType }
|
||||||
|
});
|
||||||
|
|
||||||
|
try {
|
||||||
|
const result = await executeCodexLens(args, {
|
||||||
|
cwd: targetPath,
|
||||||
|
timeout: 1800000,
|
||||||
|
onProgress: (progress: ProgressInfo) => {
|
||||||
|
broadcastToClients({
|
||||||
|
type: 'CODEXLENS_INDEX_PROGRESS',
|
||||||
|
payload: { ...progress, path: targetPath }
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
if (result.success) {
|
||||||
|
broadcastToClients({
|
||||||
|
type: 'CODEXLENS_INDEX_PROGRESS',
|
||||||
|
payload: { stage: 'complete', message: 'Incremental update complete', percent: 100, path: targetPath }
|
||||||
|
});
|
||||||
|
|
||||||
|
try {
|
||||||
|
const parsed = extractJSON(result.output ?? '');
|
||||||
|
return { success: true, result: parsed };
|
||||||
|
} catch {
|
||||||
|
return { success: true, output: result.output ?? '' };
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
broadcastToClients({
|
||||||
|
type: 'CODEXLENS_INDEX_PROGRESS',
|
||||||
|
payload: { stage: 'error', message: result.error || 'Unknown error', percent: 0, path: targetPath }
|
||||||
|
});
|
||||||
|
return { success: false, error: result.error, status: 500 };
|
||||||
|
}
|
||||||
|
} catch (err: unknown) {
|
||||||
|
const message = err instanceof Error ? err.message : String(err);
|
||||||
|
broadcastToClients({
|
||||||
|
type: 'CODEXLENS_INDEX_PROGRESS',
|
||||||
|
payload: { stage: 'error', message, percent: 0, path: targetPath }
|
||||||
|
});
|
||||||
|
return { success: false, error: message, status: 500 };
|
||||||
|
}
|
||||||
|
});
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
// API: Check if indexing is in progress
|
// API: Check if indexing is in progress
|
||||||
if (pathname === '/api/codexlens/indexing-status') {
|
if (pathname === '/api/codexlens/indexing-status') {
|
||||||
const inProgress = isIndexingInProgress();
|
const inProgress = isIndexingInProgress();
|
||||||
|
|||||||
@@ -174,8 +174,11 @@ type Params = z.infer<typeof ParamsSchema>;
|
|||||||
|
|
||||||
interface ReadyStatus {
|
interface ReadyStatus {
|
||||||
ready: boolean;
|
ready: boolean;
|
||||||
|
installed: boolean;
|
||||||
error?: string;
|
error?: string;
|
||||||
version?: string;
|
version?: string;
|
||||||
|
pythonVersion?: string;
|
||||||
|
venvPath?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface SemanticStatus {
|
interface SemanticStatus {
|
||||||
@@ -246,28 +249,32 @@ async function checkVenvStatus(force = false): Promise<ReadyStatus> {
|
|||||||
return venvStatusCache.status;
|
return venvStatusCache.status;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const venvPath = getCodexLensVenvDir();
|
||||||
|
|
||||||
// Check venv exists
|
// Check venv exists
|
||||||
if (!existsSync(getCodexLensVenvDir())) {
|
if (!existsSync(venvPath)) {
|
||||||
const result = { ready: false, error: 'Venv not found' };
|
const result = { ready: false, installed: false, error: 'Venv not found', venvPath };
|
||||||
venvStatusCache = { status: result, timestamp: Date.now() };
|
venvStatusCache = { status: result, timestamp: Date.now() };
|
||||||
console.log(`[PERF][CodexLens] checkVenvStatus (no venv): ${Date.now() - funcStart}ms`);
|
console.log(`[PERF][CodexLens] checkVenvStatus (no venv): ${Date.now() - funcStart}ms`);
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const pythonPath = getCodexLensPython();
|
||||||
|
|
||||||
// Check python executable exists
|
// Check python executable exists
|
||||||
if (!existsSync(getCodexLensPython())) {
|
if (!existsSync(pythonPath)) {
|
||||||
const result = { ready: false, error: 'Python executable not found in venv' };
|
const result = { ready: false, installed: false, error: 'Python executable not found in venv', venvPath };
|
||||||
venvStatusCache = { status: result, timestamp: Date.now() };
|
venvStatusCache = { status: result, timestamp: Date.now() };
|
||||||
console.log(`[PERF][CodexLens] checkVenvStatus (no python): ${Date.now() - funcStart}ms`);
|
console.log(`[PERF][CodexLens] checkVenvStatus (no python): ${Date.now() - funcStart}ms`);
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check codexlens and core dependencies are importable
|
// Check codexlens and core dependencies are importable, and get Python version
|
||||||
const spawnStart = Date.now();
|
const spawnStart = Date.now();
|
||||||
console.log('[PERF][CodexLens] checkVenvStatus spawning Python...');
|
console.log('[PERF][CodexLens] checkVenvStatus spawning Python...');
|
||||||
|
|
||||||
return new Promise((resolve) => {
|
return new Promise((resolve) => {
|
||||||
const child = spawn(getCodexLensPython(), ['-c', 'import codexlens; import watchdog; print(codexlens.__version__)'], {
|
const child = spawn(pythonPath, ['-c', 'import sys; import codexlens; import watchdog; print(f"{sys.version_info.major}.{sys.version_info.minor}.{sys.version_info.micro}"); print(codexlens.__version__)'], {
|
||||||
stdio: ['ignore', 'pipe', 'pipe'],
|
stdio: ['ignore', 'pipe', 'pipe'],
|
||||||
timeout: 10000,
|
timeout: 10000,
|
||||||
});
|
});
|
||||||
@@ -285,9 +292,18 @@ async function checkVenvStatus(force = false): Promise<ReadyStatus> {
|
|||||||
child.on('close', (code) => {
|
child.on('close', (code) => {
|
||||||
let result: ReadyStatus;
|
let result: ReadyStatus;
|
||||||
if (code === 0) {
|
if (code === 0) {
|
||||||
result = { ready: true, version: stdout.trim() };
|
const lines = stdout.trim().split('\n');
|
||||||
|
const pythonVersion = lines[0]?.trim() || '';
|
||||||
|
const codexlensVersion = lines[1]?.trim() || '';
|
||||||
|
result = {
|
||||||
|
ready: true,
|
||||||
|
installed: true,
|
||||||
|
version: codexlensVersion,
|
||||||
|
pythonVersion,
|
||||||
|
venvPath
|
||||||
|
};
|
||||||
} else {
|
} else {
|
||||||
result = { ready: false, error: `CodexLens not installed: ${stderr}` };
|
result = { ready: false, installed: false, error: `CodexLens not installed: ${stderr}`, venvPath };
|
||||||
}
|
}
|
||||||
// Cache the result
|
// Cache the result
|
||||||
venvStatusCache = { status: result, timestamp: Date.now() };
|
venvStatusCache = { status: result, timestamp: Date.now() };
|
||||||
@@ -296,7 +312,7 @@ async function checkVenvStatus(force = false): Promise<ReadyStatus> {
|
|||||||
});
|
});
|
||||||
|
|
||||||
child.on('error', (err) => {
|
child.on('error', (err) => {
|
||||||
const result = { ready: false, error: `Failed to check venv: ${err.message}` };
|
const result = { ready: false, installed: false, error: `Failed to check venv: ${err.message}`, venvPath };
|
||||||
venvStatusCache = { status: result, timestamp: Date.now() };
|
venvStatusCache = { status: result, timestamp: Date.now() };
|
||||||
console.log(`[PERF][CodexLens] checkVenvStatus ERROR: ${Date.now() - funcStart}ms`);
|
console.log(`[PERF][CodexLens] checkVenvStatus ERROR: ${Date.now() - funcStart}ms`);
|
||||||
resolve(result);
|
resolve(result);
|
||||||
|
|||||||
Reference in New Issue
Block a user