mirror of
https://github.com/catlog22/Claude-Code-Workflow.git
synced 2026-02-12 02:37:45 +08:00
feat: Add CodexLens Manager Page with tabbed interface for managing CodexLens features
feat: Implement ConflictTab component to display conflict resolution decisions in session detail feat: Create ImplPlanTab component to show implementation plan with modal viewer in session detail feat: Develop ReviewTab component to display review findings by dimension in session detail test: Add end-to-end tests for CodexLens Manager functionality including navigation, tab switching, and settings validation
This commit is contained in:
@@ -195,4 +195,55 @@ export {
|
||||
} from './useWorkspaceQueryKeys';
|
||||
export type {
|
||||
WorkspaceQueryKeys,
|
||||
} from './useWorkspaceQueryKeys';
|
||||
} from './useWorkspaceQueryKeys';
|
||||
|
||||
// ========== CodexLens ==========
|
||||
export {
|
||||
useCodexLensDashboard,
|
||||
useCodexLensStatus,
|
||||
useCodexLensWorkspaceStatus,
|
||||
useCodexLensConfig,
|
||||
useCodexLensModels,
|
||||
useCodexLensModelInfo,
|
||||
useCodexLensEnv,
|
||||
useCodexLensGpu,
|
||||
useCodexLensIgnorePatterns,
|
||||
useUpdateCodexLensConfig,
|
||||
useBootstrapCodexLens,
|
||||
useUninstallCodexLens,
|
||||
useDownloadModel,
|
||||
useDeleteModel,
|
||||
useUpdateCodexLensEnv,
|
||||
useSelectGpu,
|
||||
useUpdateIgnorePatterns,
|
||||
useCodexLensMutations,
|
||||
codexLensKeys,
|
||||
} from './useCodexLens';
|
||||
export type {
|
||||
UseCodexLensDashboardOptions,
|
||||
UseCodexLensDashboardReturn,
|
||||
UseCodexLensStatusOptions,
|
||||
UseCodexLensStatusReturn,
|
||||
UseCodexLensWorkspaceStatusOptions,
|
||||
UseCodexLensWorkspaceStatusReturn,
|
||||
UseCodexLensConfigOptions,
|
||||
UseCodexLensConfigReturn,
|
||||
UseCodexLensModelsOptions,
|
||||
UseCodexLensModelsReturn,
|
||||
UseCodexLensModelInfoOptions,
|
||||
UseCodexLensModelInfoReturn,
|
||||
UseCodexLensEnvOptions,
|
||||
UseCodexLensEnvReturn,
|
||||
UseCodexLensGpuOptions,
|
||||
UseCodexLensGpuReturn,
|
||||
UseCodexLensIgnorePatternsOptions,
|
||||
UseCodexLensIgnorePatternsReturn,
|
||||
UseUpdateCodexLensConfigReturn,
|
||||
UseBootstrapCodexLensReturn,
|
||||
UseUninstallCodexLensReturn,
|
||||
UseDownloadModelReturn,
|
||||
UseDeleteModelReturn,
|
||||
UseUpdateCodexLensEnvReturn,
|
||||
UseSelectGpuReturn,
|
||||
UseUpdateIgnorePatternsReturn,
|
||||
} from './useCodexLens';
|
||||
@@ -388,13 +388,11 @@ export function useRules(options: UseRulesOptions = {}): UseRulesReturn {
|
||||
const queryClient = useQueryClient();
|
||||
const projectPath = useWorkflowStore(selectProjectPath);
|
||||
|
||||
const queryEnabled = enabled && !!projectPath;
|
||||
|
||||
const query = useQuery({
|
||||
queryKey: workspaceQueryKeys.rulesList(projectPath),
|
||||
queryFn: () => fetchRules(projectPath),
|
||||
staleTime,
|
||||
enabled: queryEnabled,
|
||||
enabled: enabled, // Remove projectPath requirement
|
||||
retry: 2,
|
||||
});
|
||||
|
||||
|
||||
427
ccw/frontend/src/hooks/useCodexLens.test.tsx
Normal file
427
ccw/frontend/src/hooks/useCodexLens.test.tsx
Normal file
@@ -0,0 +1,427 @@
|
||||
// ========================================
|
||||
// useCodexLens Hook Tests
|
||||
// ========================================
|
||||
// Tests for all CodexLens TanStack Query hooks
|
||||
|
||||
import { describe, it, expect, beforeEach, vi } from 'vitest';
|
||||
import { renderHook, waitFor } from '@testing-library/react';
|
||||
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
|
||||
import * as api from '../lib/api';
|
||||
import {
|
||||
useCodexLensDashboard,
|
||||
useCodexLensStatus,
|
||||
useCodexLensConfig,
|
||||
useCodexLensModels,
|
||||
useCodexLensEnv,
|
||||
useCodexLensGpu,
|
||||
useUpdateCodexLensConfig,
|
||||
useBootstrapCodexLens,
|
||||
useUninstallCodexLens,
|
||||
useDownloadModel,
|
||||
useDeleteModel,
|
||||
useUpdateCodexLensEnv,
|
||||
useSelectGpu,
|
||||
} from './useCodexLens';
|
||||
|
||||
// Mock api module
|
||||
vi.mock('../lib/api', () => ({
|
||||
fetchCodexLensDashboardInit: vi.fn(),
|
||||
fetchCodexLensStatus: vi.fn(),
|
||||
fetchCodexLensConfig: vi.fn(),
|
||||
updateCodexLensConfig: vi.fn(),
|
||||
bootstrapCodexLens: vi.fn(),
|
||||
uninstallCodexLens: vi.fn(),
|
||||
fetchCodexLensModels: vi.fn(),
|
||||
fetchCodexLensModelInfo: vi.fn(),
|
||||
downloadCodexLensModel: vi.fn(),
|
||||
downloadCodexLensCustomModel: vi.fn(),
|
||||
deleteCodexLensModel: vi.fn(),
|
||||
deleteCodexLensModelByPath: vi.fn(),
|
||||
fetchCodexLensEnv: vi.fn(),
|
||||
updateCodexLensEnv: vi.fn(),
|
||||
fetchCodexLensGpuDetect: vi.fn(),
|
||||
fetchCodexLensGpuList: vi.fn(),
|
||||
selectCodexLensGpu: vi.fn(),
|
||||
resetCodexLensGpu: vi.fn(),
|
||||
fetchCodexLensIgnorePatterns: vi.fn(),
|
||||
updateCodexLensIgnorePatterns: vi.fn(),
|
||||
}));
|
||||
|
||||
// Mock workflowStore
|
||||
vi.mock('../stores/workflowStore', () => ({
|
||||
useWorkflowStore: vi.fn(() => () => '/test/project'),
|
||||
selectProjectPath: vi.fn(() => '/test/project'),
|
||||
}));
|
||||
|
||||
const mockDashboardData = {
|
||||
installed: true,
|
||||
status: {
|
||||
ready: true,
|
||||
installed: true,
|
||||
version: '1.0.0',
|
||||
pythonVersion: '3.11.0',
|
||||
venvPath: '/path/to/venv',
|
||||
},
|
||||
config: {
|
||||
index_dir: '~/.codexlens/indexes',
|
||||
index_count: 100,
|
||||
api_max_workers: 4,
|
||||
api_batch_size: 8,
|
||||
},
|
||||
semantic: { available: true },
|
||||
};
|
||||
|
||||
const mockModelsData = {
|
||||
models: [
|
||||
{
|
||||
profile: 'model1',
|
||||
name: 'Embedding Model 1',
|
||||
type: 'embedding',
|
||||
backend: 'onnx',
|
||||
installed: true,
|
||||
cache_path: '/path/to/cache1',
|
||||
},
|
||||
{
|
||||
profile: 'model2',
|
||||
name: 'Reranker Model 1',
|
||||
type: 'reranker',
|
||||
backend: 'onnx',
|
||||
installed: false,
|
||||
cache_path: '/path/to/cache2',
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
function createTestQueryClient() {
|
||||
return new QueryClient({
|
||||
defaultOptions: {
|
||||
queries: { retry: false, gcTime: 0 },
|
||||
mutations: { retry: false },
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
function wrapper({ children }: { children: React.ReactNode }) {
|
||||
const queryClient = createTestQueryClient();
|
||||
return <QueryClientProvider client={queryClient}>{children}</QueryClientProvider>;
|
||||
}
|
||||
|
||||
describe('useCodexLens Hook', () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
});
|
||||
|
||||
describe('useCodexLensDashboard', () => {
|
||||
it('should fetch dashboard data', async () => {
|
||||
vi.mocked(api.fetchCodexLensDashboardInit).mockResolvedValue(mockDashboardData);
|
||||
|
||||
const { result } = renderHook(() => useCodexLensDashboard(), { wrapper });
|
||||
|
||||
await waitFor(() => expect(result.current.isLoading).toBe(false));
|
||||
|
||||
expect(api.fetchCodexLensDashboardInit).toHaveBeenCalledOnce();
|
||||
expect(result.current.installed).toBe(true);
|
||||
expect(result.current.status?.ready).toBe(true);
|
||||
expect(result.current.config?.index_dir).toBe('~/.codexlens/indexes');
|
||||
});
|
||||
|
||||
it('should handle errors', async () => {
|
||||
vi.mocked(api.fetchCodexLensDashboardInit).mockRejectedValue(new Error('API Error'));
|
||||
|
||||
const { result } = renderHook(() => useCodexLensDashboard(), { wrapper });
|
||||
|
||||
await waitFor(() => expect(result.current.isLoading).toBe(false));
|
||||
|
||||
expect(result.current.error).toBeTruthy();
|
||||
expect(result.current.error?.message).toBe('API Error');
|
||||
});
|
||||
|
||||
it('should be disabled when enabled is false', async () => {
|
||||
const { result } = renderHook(() => useCodexLensDashboard({ enabled: false }), { wrapper });
|
||||
|
||||
expect(api.fetchCodexLensDashboardInit).not.toHaveBeenCalled();
|
||||
expect(result.current.isLoading).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('useCodexLensStatus', () => {
|
||||
it('should fetch status data', async () => {
|
||||
const mockStatus = { ready: true, installed: true, version: '1.0.0' };
|
||||
vi.mocked(api.fetchCodexLensStatus).mockResolvedValue(mockStatus);
|
||||
|
||||
const { result } = renderHook(() => useCodexLensStatus(), { wrapper });
|
||||
|
||||
await waitFor(() => expect(result.current.isLoading).toBe(false));
|
||||
|
||||
expect(api.fetchCodexLensStatus).toHaveBeenCalledOnce();
|
||||
expect(result.current.ready).toBe(true);
|
||||
expect(result.current.installed).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe('useCodexLensConfig', () => {
|
||||
it('should fetch config data', async () => {
|
||||
const mockConfig = {
|
||||
index_dir: '~/.codexlens/indexes',
|
||||
index_count: 100,
|
||||
api_max_workers: 4,
|
||||
api_batch_size: 8,
|
||||
};
|
||||
vi.mocked(api.fetchCodexLensConfig).mockResolvedValue(mockConfig);
|
||||
|
||||
const { result } = renderHook(() => useCodexLensConfig(), { wrapper });
|
||||
|
||||
await waitFor(() => expect(result.current.isLoading).toBe(false));
|
||||
|
||||
expect(api.fetchCodexLensConfig).toHaveBeenCalledOnce();
|
||||
expect(result.current.indexDir).toBe('~/.codexlens/indexes');
|
||||
expect(result.current.indexCount).toBe(100);
|
||||
expect(result.current.apiMaxWorkers).toBe(4);
|
||||
expect(result.current.apiBatchSize).toBe(8);
|
||||
});
|
||||
});
|
||||
|
||||
describe('useCodexLensModels', () => {
|
||||
it('should fetch and filter models by type', async () => {
|
||||
vi.mocked(api.fetchCodexLensModels).mockResolvedValue(mockModelsData);
|
||||
|
||||
const { result } = renderHook(() => useCodexLensModels(), { wrapper });
|
||||
|
||||
await waitFor(() => expect(result.current.isLoading).toBe(false));
|
||||
|
||||
expect(result.current.models).toHaveLength(2);
|
||||
expect(result.current.embeddingModels).toHaveLength(1);
|
||||
expect(result.current.rerankerModels).toHaveLength(1);
|
||||
expect(result.current.embeddingModels?.[0].type).toBe('embedding');
|
||||
});
|
||||
});
|
||||
|
||||
describe('useCodexLensEnv', () => {
|
||||
it('should fetch environment variables', async () => {
|
||||
const mockEnv = {
|
||||
env: { KEY1: 'value1', KEY2: 'value2' },
|
||||
settings: { SETTING1: 'setting1' },
|
||||
raw: 'KEY1=value1\nKEY2=value2',
|
||||
};
|
||||
vi.mocked(api.fetchCodexLensEnv).mockResolvedValue(mockEnv);
|
||||
|
||||
const { result } = renderHook(() => useCodexLensEnv(), { wrapper });
|
||||
|
||||
await waitFor(() => expect(result.current.isLoading).toBe(false));
|
||||
|
||||
expect(api.fetchCodexLensEnv).toHaveBeenCalledOnce();
|
||||
expect(result.current.env).toEqual({ KEY1: 'value1', KEY2: 'value2' });
|
||||
expect(result.current.settings).toEqual({ SETTING1: 'setting1' });
|
||||
expect(result.current.raw).toBe('KEY1=value1\nKEY2=value2');
|
||||
});
|
||||
});
|
||||
|
||||
describe('useCodexLensGpu', () => {
|
||||
it('should fetch GPU detect and list data', async () => {
|
||||
const mockDetect = { supported: true, has_cuda: true };
|
||||
const mockList = {
|
||||
devices: [
|
||||
{ id: 0, name: 'GPU 0', type: 'cuda', driver: '12.0', memory: '8GB' },
|
||||
],
|
||||
selected_device_id: 0,
|
||||
};
|
||||
vi.mocked(api.fetchCodexLensGpuDetect).mockResolvedValue(mockDetect);
|
||||
vi.mocked(api.fetchCodexLensGpuList).mockResolvedValue(mockList);
|
||||
|
||||
const { result } = renderHook(() => useCodexLensGpu(), { wrapper });
|
||||
|
||||
await waitFor(() => expect(result.current.isLoadingDetect).toBe(false));
|
||||
await waitFor(() => expect(result.current.isLoadingList).toBe(false));
|
||||
|
||||
expect(api.fetchCodexLensGpuDetect).toHaveBeenCalledOnce();
|
||||
expect(api.fetchCodexLensGpuList).toHaveBeenCalledOnce();
|
||||
expect(result.current.supported).toBe(true);
|
||||
expect(result.current.devices).toHaveLength(1);
|
||||
expect(result.current.selectedDeviceId).toBe(0);
|
||||
});
|
||||
});
|
||||
|
||||
describe('useUpdateCodexLensConfig', () => {
|
||||
it('should update config and invalidate queries', async () => {
|
||||
vi.mocked(api.updateCodexLensConfig).mockResolvedValue({
|
||||
success: true,
|
||||
message: 'Config updated',
|
||||
});
|
||||
|
||||
const { result } = renderHook(() => useUpdateCodexLensConfig(), { wrapper });
|
||||
|
||||
const updateResult = await result.current.updateConfig({
|
||||
index_dir: '~/.codexlens/indexes',
|
||||
api_max_workers: 8,
|
||||
api_batch_size: 16,
|
||||
});
|
||||
|
||||
expect(api.updateCodexLensConfig).toHaveBeenCalledWith({
|
||||
index_dir: '~/.codexlens/indexes',
|
||||
api_max_workers: 8,
|
||||
api_batch_size: 16,
|
||||
});
|
||||
expect(updateResult.success).toBe(true);
|
||||
expect(updateResult.message).toBe('Config updated');
|
||||
});
|
||||
});
|
||||
|
||||
describe('useBootstrapCodexLens', () => {
|
||||
it('should bootstrap CodexLens and invalidate queries', async () => {
|
||||
vi.mocked(api.bootstrapCodexLens).mockResolvedValue({
|
||||
success: true,
|
||||
version: '1.0.0',
|
||||
});
|
||||
|
||||
const { result } = renderHook(() => useBootstrapCodexLens(), { wrapper });
|
||||
|
||||
const bootstrapResult = await result.current.bootstrap();
|
||||
|
||||
expect(api.bootstrapCodexLens).toHaveBeenCalledOnce();
|
||||
expect(bootstrapResult.success).toBe(true);
|
||||
expect(bootstrapResult.version).toBe('1.0.0');
|
||||
});
|
||||
});
|
||||
|
||||
describe('useUninstallCodexLens', () => {
|
||||
it('should uninstall CodexLens and invalidate queries', async () => {
|
||||
vi.mocked(api.uninstallCodexLens).mockResolvedValue({
|
||||
success: true,
|
||||
message: 'CodexLens uninstalled',
|
||||
});
|
||||
|
||||
const { result } = renderHook(() => useUninstallCodexLens(), { wrapper });
|
||||
|
||||
const uninstallResult = await result.current.uninstall();
|
||||
|
||||
expect(api.uninstallCodexLens).toHaveBeenCalledOnce();
|
||||
expect(uninstallResult.success).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe('useDownloadModel', () => {
|
||||
it('should download model by profile', async () => {
|
||||
vi.mocked(api.downloadCodexLensModel).mockResolvedValue({
|
||||
success: true,
|
||||
message: 'Model downloaded',
|
||||
});
|
||||
|
||||
const { result } = renderHook(() => useDownloadModel(), { wrapper });
|
||||
|
||||
const downloadResult = await result.current.downloadModel('model1');
|
||||
|
||||
expect(api.downloadCodexLensModel).toHaveBeenCalledWith('model1');
|
||||
expect(downloadResult.success).toBe(true);
|
||||
});
|
||||
|
||||
it('should download custom model', async () => {
|
||||
vi.mocked(api.downloadCodexLensCustomModel).mockResolvedValue({
|
||||
success: true,
|
||||
message: 'Custom model downloaded',
|
||||
});
|
||||
|
||||
const { result } = renderHook(() => useDownloadModel(), { wrapper });
|
||||
|
||||
const downloadResult = await result.current.downloadCustomModel('custom/model', 'embedding');
|
||||
|
||||
expect(api.downloadCodexLensCustomModel).toHaveBeenCalledWith('custom/model', 'embedding');
|
||||
expect(downloadResult.success).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe('useDeleteModel', () => {
|
||||
it('should delete model by profile', async () => {
|
||||
vi.mocked(api.deleteCodexLensModel).mockResolvedValue({
|
||||
success: true,
|
||||
message: 'Model deleted',
|
||||
});
|
||||
|
||||
const { result } = renderHook(() => useDeleteModel(), { wrapper });
|
||||
|
||||
const deleteResult = await result.current.deleteModel('model1');
|
||||
|
||||
expect(api.deleteCodexLensModel).toHaveBeenCalledWith('model1');
|
||||
expect(deleteResult.success).toBe(true);
|
||||
});
|
||||
|
||||
it('should delete model by path', async () => {
|
||||
vi.mocked(api.deleteCodexLensModelByPath).mockResolvedValue({
|
||||
success: true,
|
||||
message: 'Model deleted',
|
||||
});
|
||||
|
||||
const { result } = renderHook(() => useDeleteModel(), { wrapper });
|
||||
|
||||
const deleteResult = await result.current.deleteModelByPath('/path/to/model');
|
||||
|
||||
expect(api.deleteCodexLensModelByPath).toHaveBeenCalledWith('/path/to/model');
|
||||
expect(deleteResult.success).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe('useUpdateCodexLensEnv', () => {
|
||||
it('should update environment variables', async () => {
|
||||
vi.mocked(api.updateCodexLensEnv).mockResolvedValue({
|
||||
success: true,
|
||||
env: { KEY1: 'newvalue' },
|
||||
settings: {},
|
||||
raw: 'KEY1=newvalue',
|
||||
});
|
||||
|
||||
const { result } = renderHook(() => useUpdateCodexLensEnv(), { wrapper });
|
||||
|
||||
const updateResult = await result.current.updateEnv({
|
||||
raw: 'KEY1=newvalue',
|
||||
});
|
||||
|
||||
expect(api.updateCodexLensEnv).toHaveBeenCalledWith({ raw: 'KEY1=newvalue' });
|
||||
expect(updateResult.success).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe('useSelectGpu', () => {
|
||||
it('should select GPU', async () => {
|
||||
vi.mocked(api.selectCodexLensGpu).mockResolvedValue({
|
||||
success: true,
|
||||
message: 'GPU selected',
|
||||
});
|
||||
|
||||
const { result } = renderHook(() => useSelectGpu(), { wrapper });
|
||||
|
||||
const selectResult = await result.current.selectGpu(0);
|
||||
|
||||
expect(api.selectCodexLensGpu).toHaveBeenCalledWith(0);
|
||||
expect(selectResult.success).toBe(true);
|
||||
});
|
||||
|
||||
it('should reset GPU', async () => {
|
||||
vi.mocked(api.resetCodexLensGpu).mockResolvedValue({
|
||||
success: true,
|
||||
message: 'GPU reset',
|
||||
});
|
||||
|
||||
const { result } = renderHook(() => useSelectGpu(), { wrapper });
|
||||
|
||||
const resetResult = await result.current.resetGpu();
|
||||
|
||||
expect(api.resetCodexLensGpu).toHaveBeenCalledOnce();
|
||||
expect(resetResult.success).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe('query refetch', () => {
|
||||
it('should refetch dashboard data', async () => {
|
||||
vi.mocked(api.fetchCodexLensDashboardInit).mockResolvedValue(mockDashboardData);
|
||||
|
||||
const { result } = renderHook(() => useCodexLensDashboard(), { wrapper });
|
||||
|
||||
await waitFor(() => expect(result.current.isLoading).toBe(false));
|
||||
|
||||
expect(api.fetchCodexLensDashboardInit).toHaveBeenCalledTimes(1);
|
||||
|
||||
await result.current.refetch();
|
||||
|
||||
expect(api.fetchCodexLensDashboardInit).toHaveBeenCalledTimes(2);
|
||||
});
|
||||
});
|
||||
});
|
||||
762
ccw/frontend/src/hooks/useCodexLens.ts
Normal file
762
ccw/frontend/src/hooks/useCodexLens.ts
Normal file
@@ -0,0 +1,762 @@
|
||||
// ========================================
|
||||
// useCodexLens Hook
|
||||
// ========================================
|
||||
// TanStack Query hooks for CodexLens management
|
||||
|
||||
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
|
||||
import {
|
||||
fetchCodexLensDashboardInit,
|
||||
fetchCodexLensStatus,
|
||||
fetchCodexLensWorkspaceStatus,
|
||||
fetchCodexLensConfig,
|
||||
updateCodexLensConfig,
|
||||
bootstrapCodexLens,
|
||||
uninstallCodexLens,
|
||||
fetchCodexLensModels,
|
||||
fetchCodexLensModelInfo,
|
||||
downloadCodexLensModel,
|
||||
downloadCodexLensCustomModel,
|
||||
deleteCodexLensModel,
|
||||
deleteCodexLensModelByPath,
|
||||
fetchCodexLensEnv,
|
||||
updateCodexLensEnv,
|
||||
fetchCodexLensGpuDetect,
|
||||
fetchCodexLensGpuList,
|
||||
selectCodexLensGpu,
|
||||
resetCodexLensGpu,
|
||||
fetchCodexLensIgnorePatterns,
|
||||
updateCodexLensIgnorePatterns,
|
||||
type CodexLensDashboardInitResponse,
|
||||
type CodexLensVenvStatus,
|
||||
type CodexLensConfig,
|
||||
type CodexLensModelsResponse,
|
||||
type CodexLensModelInfoResponse,
|
||||
type CodexLensEnvResponse,
|
||||
type CodexLensUpdateEnvResponse,
|
||||
type CodexLensGpuDetectResponse,
|
||||
type CodexLensGpuListResponse,
|
||||
type CodexLensIgnorePatternsResponse,
|
||||
type CodexLensUpdateEnvRequest,
|
||||
type CodexLensUpdateIgnorePatternsRequest,
|
||||
type CodexLensWorkspaceStatus,
|
||||
} from '../lib/api';
|
||||
import { useWorkflowStore, selectProjectPath } from '@/stores/workflowStore';
|
||||
|
||||
// Query key factory
|
||||
export const codexLensKeys = {
|
||||
all: ['codexLens'] as const,
|
||||
dashboard: () => [...codexLensKeys.all, 'dashboard'] as const,
|
||||
status: () => [...codexLensKeys.all, 'status'] as const,
|
||||
workspace: (path?: string) => [...codexLensKeys.all, 'workspace', path] as const,
|
||||
config: () => [...codexLensKeys.all, 'config'] as const,
|
||||
models: () => [...codexLensKeys.all, 'models'] as const,
|
||||
modelInfo: (profile: string) => [...codexLensKeys.models(), 'info', profile] as const,
|
||||
env: () => [...codexLensKeys.all, 'env'] as const,
|
||||
gpu: () => [...codexLensKeys.all, 'gpu'] as const,
|
||||
gpuList: () => [...codexLensKeys.gpu(), 'list'] as const,
|
||||
gpuDetect: () => [...codexLensKeys.gpu(), 'detect'] as const,
|
||||
ignorePatterns: () => [...codexLensKeys.all, 'ignorePatterns'] as const,
|
||||
};
|
||||
|
||||
// Default stale times
|
||||
const STALE_TIME_SHORT = 30 * 1000; // 30 seconds for frequently changing data
|
||||
const STALE_TIME_MEDIUM = 2 * 60 * 1000; // 2 minutes for moderately changing data
|
||||
const STALE_TIME_LONG = 10 * 60 * 1000; // 10 minutes for rarely changing data
|
||||
|
||||
// ========== Query Hooks ==========
|
||||
|
||||
export interface UseCodexLensDashboardOptions {
|
||||
enabled?: boolean;
|
||||
staleTime?: number;
|
||||
}
|
||||
|
||||
export interface UseCodexLensDashboardReturn {
|
||||
data: CodexLensDashboardInitResponse | undefined;
|
||||
installed: boolean;
|
||||
status: CodexLensVenvStatus | undefined;
|
||||
config: CodexLensConfig | undefined;
|
||||
semantic: { available: boolean } | undefined;
|
||||
isLoading: boolean;
|
||||
isFetching: boolean;
|
||||
error: Error | null;
|
||||
refetch: () => Promise<void>;
|
||||
}
|
||||
|
||||
/**
|
||||
* Hook for fetching CodexLens dashboard initialization data
|
||||
*/
|
||||
export function useCodexLensDashboard(options: UseCodexLensDashboardOptions = {}): UseCodexLensDashboardReturn {
|
||||
const { enabled = true, staleTime = STALE_TIME_SHORT } = options;
|
||||
|
||||
const query = useQuery({
|
||||
queryKey: codexLensKeys.dashboard(),
|
||||
queryFn: fetchCodexLensDashboardInit,
|
||||
staleTime,
|
||||
enabled,
|
||||
retry: 2,
|
||||
});
|
||||
|
||||
const refetch = async () => {
|
||||
await query.refetch();
|
||||
};
|
||||
|
||||
return {
|
||||
data: query.data,
|
||||
installed: query.data?.installed ?? false,
|
||||
status: query.data?.status,
|
||||
config: query.data?.config,
|
||||
semantic: query.data?.semantic,
|
||||
isLoading: query.isLoading,
|
||||
isFetching: query.isFetching,
|
||||
error: query.error,
|
||||
refetch,
|
||||
};
|
||||
}
|
||||
|
||||
export interface UseCodexLensStatusOptions {
|
||||
enabled?: boolean;
|
||||
staleTime?: number;
|
||||
}
|
||||
|
||||
export interface UseCodexLensStatusReturn {
|
||||
status: CodexLensVenvStatus | undefined;
|
||||
ready: boolean;
|
||||
installed: boolean;
|
||||
isLoading: boolean;
|
||||
error: Error | null;
|
||||
refetch: () => Promise<void>;
|
||||
}
|
||||
|
||||
/**
|
||||
* Hook for fetching CodexLens venv status
|
||||
*/
|
||||
export function useCodexLensStatus(options: UseCodexLensStatusOptions = {}): UseCodexLensStatusReturn {
|
||||
const { enabled = true, staleTime = STALE_TIME_SHORT } = options;
|
||||
|
||||
const query = useQuery({
|
||||
queryKey: codexLensKeys.status(),
|
||||
queryFn: fetchCodexLensStatus,
|
||||
staleTime,
|
||||
enabled,
|
||||
retry: 2,
|
||||
});
|
||||
|
||||
const refetch = async () => {
|
||||
await query.refetch();
|
||||
};
|
||||
|
||||
return {
|
||||
status: query.data,
|
||||
ready: query.data?.ready ?? false,
|
||||
installed: query.data?.installed ?? false,
|
||||
isLoading: query.isLoading,
|
||||
error: query.error,
|
||||
refetch,
|
||||
};
|
||||
}
|
||||
|
||||
export interface UseCodexLensWorkspaceStatusOptions {
|
||||
projectPath?: string;
|
||||
enabled?: boolean;
|
||||
staleTime?: number;
|
||||
}
|
||||
|
||||
export interface UseCodexLensWorkspaceStatusReturn {
|
||||
data: CodexLensWorkspaceStatus | undefined;
|
||||
hasIndex: boolean;
|
||||
ftsPercent: number;
|
||||
vectorPercent: number;
|
||||
isLoading: boolean;
|
||||
error: Error | null;
|
||||
refetch: () => Promise<void>;
|
||||
}
|
||||
|
||||
/**
|
||||
* Hook for fetching CodexLens workspace index status
|
||||
*/
|
||||
export function useCodexLensWorkspaceStatus(options: UseCodexLensWorkspaceStatusOptions = {}): UseCodexLensWorkspaceStatusReturn {
|
||||
const { projectPath, enabled = true, staleTime = STALE_TIME_SHORT } = options;
|
||||
const projectPathFromStore = useWorkflowStore(selectProjectPath);
|
||||
const actualProjectPath = projectPath ?? projectPathFromStore;
|
||||
const queryEnabled = enabled && !!actualProjectPath;
|
||||
|
||||
const query = useQuery({
|
||||
queryKey: codexLensKeys.workspace(actualProjectPath),
|
||||
queryFn: () => fetchCodexLensWorkspaceStatus(actualProjectPath),
|
||||
staleTime,
|
||||
enabled: queryEnabled,
|
||||
retry: 2,
|
||||
});
|
||||
|
||||
const refetch = async () => {
|
||||
await query.refetch();
|
||||
};
|
||||
|
||||
return {
|
||||
data: query.data,
|
||||
hasIndex: query.data?.hasIndex ?? false,
|
||||
ftsPercent: query.data?.fts.percent ?? 0,
|
||||
vectorPercent: query.data?.vector.percent ?? 0,
|
||||
isLoading: query.isLoading,
|
||||
error: query.error,
|
||||
refetch,
|
||||
};
|
||||
}
|
||||
|
||||
export interface UseCodexLensConfigOptions {
|
||||
enabled?: boolean;
|
||||
staleTime?: number;
|
||||
}
|
||||
|
||||
export interface UseCodexLensConfigReturn {
|
||||
config: CodexLensConfig | undefined;
|
||||
indexDir: string;
|
||||
indexCount: number;
|
||||
apiMaxWorkers: number;
|
||||
apiBatchSize: number;
|
||||
isLoading: boolean;
|
||||
error: Error | null;
|
||||
refetch: () => Promise<void>;
|
||||
}
|
||||
|
||||
/**
|
||||
* Hook for fetching CodexLens configuration
|
||||
*/
|
||||
export function useCodexLensConfig(options: UseCodexLensConfigOptions = {}): UseCodexLensConfigReturn {
|
||||
const { enabled = true, staleTime = STALE_TIME_MEDIUM } = options;
|
||||
|
||||
const query = useQuery({
|
||||
queryKey: codexLensKeys.config(),
|
||||
queryFn: fetchCodexLensConfig,
|
||||
staleTime,
|
||||
enabled,
|
||||
retry: 2,
|
||||
});
|
||||
|
||||
const refetch = async () => {
|
||||
await query.refetch();
|
||||
};
|
||||
|
||||
return {
|
||||
config: query.data,
|
||||
indexDir: query.data?.index_dir ?? '~/.codexlens/indexes',
|
||||
indexCount: query.data?.index_count ?? 0,
|
||||
apiMaxWorkers: query.data?.api_max_workers ?? 4,
|
||||
apiBatchSize: query.data?.api_batch_size ?? 8,
|
||||
isLoading: query.isLoading,
|
||||
error: query.error,
|
||||
refetch,
|
||||
};
|
||||
}
|
||||
|
||||
export interface UseCodexLensModelsOptions {
|
||||
enabled?: boolean;
|
||||
staleTime?: number;
|
||||
}
|
||||
|
||||
export interface UseCodexLensModelsReturn {
|
||||
models: CodexLensModelsResponse['models'] | undefined;
|
||||
embeddingModels: CodexLensModelsResponse['models'] | undefined;
|
||||
rerankerModels: CodexLensModelsResponse['models'] | undefined;
|
||||
isLoading: boolean;
|
||||
error: Error | null;
|
||||
refetch: () => Promise<void>;
|
||||
}
|
||||
|
||||
/**
|
||||
* Hook for fetching CodexLens models list
|
||||
*/
|
||||
export function useCodexLensModels(options: UseCodexLensModelsOptions = {}): UseCodexLensModelsReturn {
|
||||
const { enabled = true, staleTime = STALE_TIME_MEDIUM } = options;
|
||||
|
||||
const query = useQuery({
|
||||
queryKey: codexLensKeys.models(),
|
||||
queryFn: fetchCodexLensModels,
|
||||
staleTime,
|
||||
enabled,
|
||||
retry: 2,
|
||||
});
|
||||
|
||||
const refetch = async () => {
|
||||
await query.refetch();
|
||||
};
|
||||
|
||||
const models = query.data?.models ?? [];
|
||||
const embeddingModels = models?.filter(m => m.type === 'embedding');
|
||||
const rerankerModels = models?.filter(m => m.type === 'reranker');
|
||||
|
||||
return {
|
||||
models,
|
||||
embeddingModels,
|
||||
rerankerModels,
|
||||
isLoading: query.isLoading,
|
||||
error: query.error,
|
||||
refetch,
|
||||
};
|
||||
}
|
||||
|
||||
export interface UseCodexLensModelInfoOptions {
|
||||
profile: string;
|
||||
enabled?: boolean;
|
||||
staleTime?: number;
|
||||
}
|
||||
|
||||
export interface UseCodexLensModelInfoReturn {
|
||||
info: CodexLensModelInfoResponse['info'] | undefined;
|
||||
isLoading: boolean;
|
||||
error: Error | null;
|
||||
refetch: () => Promise<void>;
|
||||
}
|
||||
|
||||
/**
|
||||
* Hook for fetching CodexLens model info by profile
|
||||
*/
|
||||
export function useCodexLensModelInfo(options: UseCodexLensModelInfoOptions): UseCodexLensModelInfoReturn {
|
||||
const { profile, enabled = true, staleTime = STALE_TIME_LONG } = options;
|
||||
const queryEnabled = enabled && !!profile;
|
||||
|
||||
const query = useQuery({
|
||||
queryKey: codexLensKeys.modelInfo(profile),
|
||||
queryFn: () => fetchCodexLensModelInfo(profile),
|
||||
staleTime,
|
||||
enabled: queryEnabled,
|
||||
retry: 2,
|
||||
});
|
||||
|
||||
const refetch = async () => {
|
||||
await query.refetch();
|
||||
};
|
||||
|
||||
return {
|
||||
info: query.data?.info,
|
||||
isLoading: query.isLoading,
|
||||
error: query.error,
|
||||
refetch,
|
||||
};
|
||||
}
|
||||
|
||||
export interface UseCodexLensEnvOptions {
|
||||
enabled?: boolean;
|
||||
staleTime?: number;
|
||||
}
|
||||
|
||||
export interface UseCodexLensEnvReturn {
|
||||
data: CodexLensEnvResponse | undefined;
|
||||
env: Record<string, string> | undefined;
|
||||
settings: Record<string, string> | undefined;
|
||||
raw: string | undefined;
|
||||
isLoading: boolean;
|
||||
error: Error | null;
|
||||
refetch: () => Promise<void>;
|
||||
}
|
||||
|
||||
/**
|
||||
* Hook for fetching CodexLens environment variables
|
||||
*/
|
||||
export function useCodexLensEnv(options: UseCodexLensEnvOptions = {}): UseCodexLensEnvReturn {
|
||||
const { enabled = true, staleTime = STALE_TIME_MEDIUM } = options;
|
||||
|
||||
const query = useQuery({
|
||||
queryKey: codexLensKeys.env(),
|
||||
queryFn: fetchCodexLensEnv,
|
||||
staleTime,
|
||||
enabled,
|
||||
retry: 2,
|
||||
});
|
||||
|
||||
const refetch = async () => {
|
||||
await query.refetch();
|
||||
};
|
||||
|
||||
return {
|
||||
data: query.data,
|
||||
env: query.data?.env,
|
||||
settings: query.data?.settings,
|
||||
raw: query.data?.raw,
|
||||
isLoading: query.isLoading,
|
||||
error: query.error,
|
||||
refetch,
|
||||
};
|
||||
}
|
||||
|
||||
export interface UseCodexLensGpuOptions {
|
||||
enabled?: boolean;
|
||||
staleTime?: number;
|
||||
}
|
||||
|
||||
export interface UseCodexLensGpuReturn {
|
||||
detectData: CodexLensGpuDetectResponse | undefined;
|
||||
listData: CodexLensGpuListResponse | undefined;
|
||||
supported: boolean;
|
||||
devices: CodexLensGpuListResponse['devices'] | undefined;
|
||||
selectedDeviceId: string | number | undefined;
|
||||
isLoadingDetect: boolean;
|
||||
isLoadingList: boolean;
|
||||
error: Error | null;
|
||||
refetch: () => Promise<void>;
|
||||
}
|
||||
|
||||
/**
|
||||
* Hook for fetching CodexLens GPU information
|
||||
* Combines both detect and list queries
|
||||
*/
|
||||
export function useCodexLensGpu(options: UseCodexLensGpuOptions = {}): UseCodexLensGpuReturn {
|
||||
const { enabled = true, staleTime = STALE_TIME_LONG } = options;
|
||||
|
||||
const detectQuery = useQuery({
|
||||
queryKey: codexLensKeys.gpuDetect(),
|
||||
queryFn: fetchCodexLensGpuDetect,
|
||||
staleTime,
|
||||
enabled,
|
||||
retry: 2,
|
||||
});
|
||||
|
||||
const listQuery = useQuery({
|
||||
queryKey: codexLensKeys.gpuList(),
|
||||
queryFn: fetchCodexLensGpuList,
|
||||
staleTime,
|
||||
enabled,
|
||||
retry: 2,
|
||||
});
|
||||
|
||||
const refetch = async () => {
|
||||
await Promise.all([detectQuery.refetch(), listQuery.refetch()]);
|
||||
};
|
||||
|
||||
return {
|
||||
detectData: detectQuery.data,
|
||||
listData: listQuery.data,
|
||||
supported: detectQuery.data?.supported ?? false,
|
||||
devices: listQuery.data?.devices,
|
||||
selectedDeviceId: listQuery.data?.selected_device_id,
|
||||
isLoadingDetect: detectQuery.isLoading,
|
||||
isLoadingList: listQuery.isLoading,
|
||||
error: detectQuery.error || listQuery.error,
|
||||
refetch,
|
||||
};
|
||||
}
|
||||
|
||||
export interface UseCodexLensIgnorePatternsOptions {
|
||||
enabled?: boolean;
|
||||
staleTime?: number;
|
||||
}
|
||||
|
||||
export interface UseCodexLensIgnorePatternsReturn {
|
||||
data: CodexLensIgnorePatternsResponse | undefined;
|
||||
patterns: string[] | undefined;
|
||||
extensionFilters: string[] | undefined;
|
||||
defaults: CodexLensIgnorePatternsResponse['defaults'] | undefined;
|
||||
isLoading: boolean;
|
||||
error: Error | null;
|
||||
refetch: () => Promise<void>;
|
||||
}
|
||||
|
||||
/**
|
||||
* Hook for fetching CodexLens ignore patterns
|
||||
*/
|
||||
export function useCodexLensIgnorePatterns(options: UseCodexLensIgnorePatternsOptions = {}): UseCodexLensIgnorePatternsReturn {
|
||||
const { enabled = true, staleTime = STALE_TIME_LONG } = options;
|
||||
|
||||
const query = useQuery({
|
||||
queryKey: codexLensKeys.ignorePatterns(),
|
||||
queryFn: fetchCodexLensIgnorePatterns,
|
||||
staleTime,
|
||||
enabled,
|
||||
retry: 2,
|
||||
});
|
||||
|
||||
const refetch = async () => {
|
||||
await query.refetch();
|
||||
};
|
||||
|
||||
return {
|
||||
data: query.data,
|
||||
patterns: query.data?.patterns,
|
||||
extensionFilters: query.data?.extensionFilters,
|
||||
defaults: query.data?.defaults,
|
||||
isLoading: query.isLoading,
|
||||
error: query.error,
|
||||
refetch,
|
||||
};
|
||||
}
|
||||
|
||||
// ========== Mutation Hooks ==========
|
||||
|
||||
export interface UseUpdateCodexLensConfigReturn {
|
||||
updateConfig: (config: { index_dir: string; api_max_workers?: number; api_batch_size?: number }) => Promise<{ success: boolean; message?: string }>;
|
||||
isUpdating: boolean;
|
||||
error: Error | null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Hook for updating CodexLens configuration
|
||||
*/
|
||||
export function useUpdateCodexLensConfig(): UseUpdateCodexLensConfigReturn {
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
const mutation = useMutation({
|
||||
mutationFn: updateCodexLensConfig,
|
||||
onSuccess: () => {
|
||||
queryClient.invalidateQueries({ queryKey: codexLensKeys.config() });
|
||||
queryClient.invalidateQueries({ queryKey: codexLensKeys.dashboard() });
|
||||
},
|
||||
});
|
||||
|
||||
return {
|
||||
updateConfig: mutation.mutateAsync,
|
||||
isUpdating: mutation.isPending,
|
||||
error: mutation.error,
|
||||
};
|
||||
}
|
||||
|
||||
export interface UseBootstrapCodexLensReturn {
|
||||
bootstrap: () => Promise<{ success: boolean; message?: string; version?: string }>;
|
||||
isBootstrapping: boolean;
|
||||
error: Error | null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Hook for bootstrapping/installing CodexLens
|
||||
*/
|
||||
export function useBootstrapCodexLens(): UseBootstrapCodexLensReturn {
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
const mutation = useMutation({
|
||||
mutationFn: bootstrapCodexLens,
|
||||
onSuccess: () => {
|
||||
queryClient.invalidateQueries({ queryKey: codexLensKeys.all });
|
||||
},
|
||||
});
|
||||
|
||||
return {
|
||||
bootstrap: mutation.mutateAsync,
|
||||
isBootstrapping: mutation.isPending,
|
||||
error: mutation.error,
|
||||
};
|
||||
}
|
||||
|
||||
export interface UseUninstallCodexLensReturn {
|
||||
uninstall: () => Promise<{ success: boolean; message?: string }>;
|
||||
isUninstalling: boolean;
|
||||
error: Error | null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Hook for uninstalling CodexLens
|
||||
*/
|
||||
export function useUninstallCodexLens(): UseUninstallCodexLensReturn {
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
const mutation = useMutation({
|
||||
mutationFn: uninstallCodexLens,
|
||||
onSuccess: () => {
|
||||
queryClient.invalidateQueries({ queryKey: codexLensKeys.all });
|
||||
},
|
||||
});
|
||||
|
||||
return {
|
||||
uninstall: mutation.mutateAsync,
|
||||
isUninstalling: mutation.isPending,
|
||||
error: mutation.error,
|
||||
};
|
||||
}
|
||||
|
||||
export interface UseDownloadModelReturn {
|
||||
downloadModel: (profile: string) => Promise<{ success: boolean; message?: string }>;
|
||||
downloadCustomModel: (modelName: string, modelType?: string) => Promise<{ success: boolean; message?: string }>;
|
||||
isDownloading: boolean;
|
||||
error: Error | null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Hook for downloading CodexLens models
|
||||
*/
|
||||
export function useDownloadModel(): UseDownloadModelReturn {
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
const mutation = useMutation({
|
||||
mutationFn: async ({ profile, modelName, modelType }: { profile?: string; modelName?: string; modelType?: string }) => {
|
||||
if (profile) return downloadCodexLensModel(profile);
|
||||
if (modelName) return downloadCodexLensCustomModel(modelName, modelType);
|
||||
throw new Error('Either profile or modelName must be provided');
|
||||
},
|
||||
onSuccess: () => {
|
||||
queryClient.invalidateQueries({ queryKey: codexLensKeys.models() });
|
||||
},
|
||||
});
|
||||
|
||||
return {
|
||||
downloadModel: (profile) => mutation.mutateAsync({ profile }),
|
||||
downloadCustomModel: (modelName, modelType) => mutation.mutateAsync({ modelName, modelType }),
|
||||
isDownloading: mutation.isPending,
|
||||
error: mutation.error,
|
||||
};
|
||||
}
|
||||
|
||||
export interface UseDeleteModelReturn {
|
||||
deleteModel: (profile: string) => Promise<{ success: boolean; message?: string }>;
|
||||
deleteModelByPath: (cachePath: string) => Promise<{ success: boolean; message?: string }>;
|
||||
isDeleting: boolean;
|
||||
error: Error | null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Hook for deleting CodexLens models
|
||||
*/
|
||||
export function useDeleteModel(): UseDeleteModelReturn {
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
const mutation = useMutation({
|
||||
mutationFn: async ({ profile, cachePath }: { profile?: string; cachePath?: string }) => {
|
||||
if (profile) return deleteCodexLensModel(profile);
|
||||
if (cachePath) return deleteCodexLensModelByPath(cachePath);
|
||||
throw new Error('Either profile or cachePath must be provided');
|
||||
},
|
||||
onSuccess: () => {
|
||||
queryClient.invalidateQueries({ queryKey: codexLensKeys.models() });
|
||||
},
|
||||
});
|
||||
|
||||
return {
|
||||
deleteModel: (profile) => mutation.mutateAsync({ profile }),
|
||||
deleteModelByPath: (cachePath) => mutation.mutateAsync({ cachePath }),
|
||||
isDeleting: mutation.isPending,
|
||||
error: mutation.error,
|
||||
};
|
||||
}
|
||||
|
||||
export interface UseUpdateCodexLensEnvReturn {
|
||||
updateEnv: (request: CodexLensUpdateEnvRequest) => Promise<CodexLensUpdateEnvResponse>;
|
||||
isUpdating: boolean;
|
||||
error: Error | null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Hook for updating CodexLens environment variables
|
||||
*/
|
||||
export function useUpdateCodexLensEnv(): UseUpdateCodexLensEnvReturn {
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
const mutation = useMutation({
|
||||
mutationFn: (request: CodexLensUpdateEnvRequest) => updateCodexLensEnv(request),
|
||||
onSuccess: () => {
|
||||
queryClient.invalidateQueries({ queryKey: codexLensKeys.env() });
|
||||
queryClient.invalidateQueries({ queryKey: codexLensKeys.dashboard() });
|
||||
},
|
||||
});
|
||||
|
||||
return {
|
||||
updateEnv: mutation.mutateAsync,
|
||||
isUpdating: mutation.isPending,
|
||||
error: mutation.error,
|
||||
};
|
||||
}
|
||||
|
||||
export interface UseSelectGpuReturn {
|
||||
selectGpu: (deviceId: string | number) => Promise<{ success: boolean; message?: string }>;
|
||||
resetGpu: () => Promise<{ success: boolean; message?: string }>;
|
||||
isSelecting: boolean;
|
||||
isResetting: boolean;
|
||||
error: Error | null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Hook for selecting/resetting GPU for CodexLens
|
||||
*/
|
||||
export function useSelectGpu(): UseSelectGpuReturn {
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
const selectMutation = useMutation({
|
||||
mutationFn: (deviceId: string | number) => selectCodexLensGpu(deviceId),
|
||||
onSuccess: () => {
|
||||
queryClient.invalidateQueries({ queryKey: codexLensKeys.gpu() });
|
||||
},
|
||||
});
|
||||
|
||||
const resetMutation = useMutation({
|
||||
mutationFn: () => resetCodexLensGpu(),
|
||||
onSuccess: () => {
|
||||
queryClient.invalidateQueries({ queryKey: codexLensKeys.gpu() });
|
||||
},
|
||||
});
|
||||
|
||||
return {
|
||||
selectGpu: selectMutation.mutateAsync,
|
||||
resetGpu: resetMutation.mutateAsync,
|
||||
isSelecting: selectMutation.isPending,
|
||||
isResetting: resetMutation.isPending,
|
||||
error: selectMutation.error || resetMutation.error,
|
||||
};
|
||||
}
|
||||
|
||||
export interface UseUpdateIgnorePatternsReturn {
|
||||
updatePatterns: (request: CodexLensUpdateIgnorePatternsRequest) => Promise<CodexLensIgnorePatternsResponse>;
|
||||
isUpdating: boolean;
|
||||
error: Error | null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Hook for updating CodexLens ignore patterns
|
||||
*/
|
||||
export function useUpdateIgnorePatterns(): UseUpdateIgnorePatternsReturn {
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
const mutation = useMutation({
|
||||
mutationFn: updateCodexLensIgnorePatterns,
|
||||
onSuccess: () => {
|
||||
queryClient.invalidateQueries({ queryKey: codexLensKeys.ignorePatterns() });
|
||||
},
|
||||
});
|
||||
|
||||
return {
|
||||
updatePatterns: mutation.mutateAsync,
|
||||
isUpdating: mutation.isPending,
|
||||
error: mutation.error,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Combined hook for all CodexLens mutations
|
||||
*/
|
||||
export function useCodexLensMutations() {
|
||||
const updateConfig = useUpdateCodexLensConfig();
|
||||
const bootstrap = useBootstrapCodexLens();
|
||||
const uninstall = useUninstallCodexLens();
|
||||
const download = useDownloadModel();
|
||||
const deleteModel = useDeleteModel();
|
||||
const updateEnv = useUpdateCodexLensEnv();
|
||||
const gpu = useSelectGpu();
|
||||
const updatePatterns = useUpdateIgnorePatterns();
|
||||
|
||||
return {
|
||||
updateConfig: updateConfig.updateConfig,
|
||||
isUpdatingConfig: updateConfig.isUpdating,
|
||||
bootstrap: bootstrap.bootstrap,
|
||||
isBootstrapping: bootstrap.isBootstrapping,
|
||||
uninstall: uninstall.uninstall,
|
||||
isUninstalling: uninstall.isUninstalling,
|
||||
downloadModel: download.downloadModel,
|
||||
downloadCustomModel: download.downloadCustomModel,
|
||||
isDownloading: download.isDownloading,
|
||||
deleteModel: deleteModel.deleteModel,
|
||||
deleteModelByPath: deleteModel.deleteModelByPath,
|
||||
isDeleting: deleteModel.isDeleting,
|
||||
updateEnv: updateEnv.updateEnv,
|
||||
isUpdatingEnv: updateEnv.isUpdating,
|
||||
selectGpu: gpu.selectGpu,
|
||||
resetGpu: gpu.resetGpu,
|
||||
isSelectingGpu: gpu.isSelecting || gpu.isResetting,
|
||||
updatePatterns: updatePatterns.updatePatterns,
|
||||
isUpdatingPatterns: updatePatterns.isUpdating,
|
||||
isMutating:
|
||||
updateConfig.isUpdating ||
|
||||
bootstrap.isBootstrapping ||
|
||||
uninstall.isUninstalling ||
|
||||
download.isDownloading ||
|
||||
deleteModel.isDeleting ||
|
||||
updateEnv.isUpdating ||
|
||||
gpu.isSelecting ||
|
||||
gpu.isResetting ||
|
||||
updatePatterns.isUpdating,
|
||||
};
|
||||
}
|
||||
@@ -52,13 +52,12 @@ export function useCommands(options: UseCommandsOptions = {}): UseCommandsReturn
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
const projectPath = useWorkflowStore(selectProjectPath);
|
||||
const queryEnabled = enabled && !!projectPath;
|
||||
|
||||
const query = useQuery({
|
||||
queryKey: commandsKeys.list(filter),
|
||||
queryFn: () => fetchCommands(projectPath),
|
||||
staleTime,
|
||||
enabled: queryEnabled,
|
||||
enabled: enabled, // Remove projectPath requirement
|
||||
retry: 2,
|
||||
});
|
||||
|
||||
|
||||
@@ -15,8 +15,10 @@ import {
|
||||
deactivateQueue,
|
||||
deleteQueue as deleteQueueApi,
|
||||
mergeQueues as mergeQueuesApi,
|
||||
splitQueue as splitQueueApi,
|
||||
fetchDiscoveries,
|
||||
fetchDiscoveryFindings,
|
||||
exportDiscoveryFindingsAsIssues,
|
||||
type Issue,
|
||||
type IssueQueue,
|
||||
type IssuesResponse,
|
||||
@@ -306,10 +308,12 @@ export interface UseQueueMutationsReturn {
|
||||
deactivateQueue: () => Promise<void>;
|
||||
deleteQueue: (queueId: string) => Promise<void>;
|
||||
mergeQueues: (sourceId: string, targetId: string) => Promise<void>;
|
||||
splitQueue: (sourceQueueId: string, itemIds: string[]) => Promise<void>;
|
||||
isActivating: boolean;
|
||||
isDeactivating: boolean;
|
||||
isDeleting: boolean;
|
||||
isMerging: boolean;
|
||||
isSplitting: boolean;
|
||||
isMutating: boolean;
|
||||
}
|
||||
|
||||
@@ -346,16 +350,26 @@ export function useQueueMutations(): UseQueueMutationsReturn {
|
||||
},
|
||||
});
|
||||
|
||||
const splitMutation = useMutation({
|
||||
mutationFn: ({ sourceQueueId, itemIds }: { sourceQueueId: string; itemIds: string[] }) =>
|
||||
splitQueueApi(sourceQueueId, itemIds, projectPath),
|
||||
onSuccess: () => {
|
||||
queryClient.invalidateQueries({ queryKey: workspaceQueryKeys.issueQueue(projectPath) });
|
||||
},
|
||||
});
|
||||
|
||||
return {
|
||||
activateQueue: activateMutation.mutateAsync,
|
||||
deactivateQueue: deactivateMutation.mutateAsync,
|
||||
deleteQueue: deleteMutation.mutateAsync,
|
||||
mergeQueues: (sourceId, targetId) => mergeMutation.mutateAsync({ sourceId, targetId }),
|
||||
splitQueue: (sourceQueueId, itemIds) => splitMutation.mutateAsync({ sourceQueueId, itemIds }),
|
||||
isActivating: activateMutation.isPending,
|
||||
isDeactivating: deactivateMutation.isPending,
|
||||
isDeleting: deleteMutation.isPending,
|
||||
isMerging: mergeMutation.isPending,
|
||||
isMutating: activateMutation.isPending || deactivateMutation.isPending || deleteMutation.isPending || mergeMutation.isPending,
|
||||
isSplitting: splitMutation.isPending,
|
||||
isMutating: activateMutation.isPending || deactivateMutation.isPending || deleteMutation.isPending || mergeMutation.isPending || splitMutation.isPending,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -365,6 +379,8 @@ export interface FindingFilters {
|
||||
severity?: 'critical' | 'high' | 'medium' | 'low';
|
||||
type?: string;
|
||||
search?: string;
|
||||
exported?: boolean;
|
||||
hasIssue?: boolean;
|
||||
}
|
||||
|
||||
export interface UseIssueDiscoveryReturn {
|
||||
@@ -380,6 +396,8 @@ export interface UseIssueDiscoveryReturn {
|
||||
selectSession: (sessionId: string) => void;
|
||||
refetchSessions: () => void;
|
||||
exportFindings: () => void;
|
||||
exportSelectedFindings: (findingIds: string[]) => Promise<{ success: boolean; message?: string; exported?: number }>;
|
||||
isExporting: boolean;
|
||||
}
|
||||
|
||||
export function useIssueDiscovery(options?: { refetchInterval?: number }): UseIssueDiscoveryReturn {
|
||||
@@ -388,6 +406,7 @@ export function useIssueDiscovery(options?: { refetchInterval?: number }): UseIs
|
||||
const projectPath = useWorkflowStore(selectProjectPath);
|
||||
const [activeSessionId, setActiveSessionId] = useState<string | null>(null);
|
||||
const [filters, setFilters] = useState<FindingFilters>({});
|
||||
const [isExporting, setIsExporting] = useState(false);
|
||||
|
||||
const sessionsQuery = useQuery({
|
||||
queryKey: workspaceQueryKeys.discoveries(projectPath),
|
||||
@@ -426,6 +445,14 @@ export function useIssueDiscovery(options?: { refetchInterval?: number }): UseIs
|
||||
f.description.toLowerCase().includes(searchLower)
|
||||
);
|
||||
}
|
||||
// Filter by exported status
|
||||
if (filters.exported !== undefined) {
|
||||
findings = findings.filter(f => f.exported === filters.exported);
|
||||
}
|
||||
// Filter by hasIssue (has associated issue_id)
|
||||
if (filters.hasIssue !== undefined) {
|
||||
findings = findings.filter(f => !!f.issue_id === filters.hasIssue);
|
||||
}
|
||||
return findings;
|
||||
}, [findingsQuery.data, filters]);
|
||||
|
||||
@@ -449,6 +476,26 @@ export function useIssueDiscovery(options?: { refetchInterval?: number }): UseIs
|
||||
URL.revokeObjectURL(url);
|
||||
};
|
||||
|
||||
const exportSelectedFindings = async (findingIds: string[]) => {
|
||||
if (!activeSessionId) return { success: false, message: 'No active session' };
|
||||
setIsExporting(true);
|
||||
try {
|
||||
const result = await exportDiscoveryFindingsAsIssues(
|
||||
activeSessionId,
|
||||
{ findingIds },
|
||||
projectPath
|
||||
);
|
||||
// Invalidate queries to refresh findings with updated exported status
|
||||
await queryClient.invalidateQueries({ queryKey: ['discoveryFindings', activeSessionId, projectPath] });
|
||||
await queryClient.invalidateQueries({ queryKey: workspaceQueryKeys.issues(projectPath) });
|
||||
return result;
|
||||
} catch (error) {
|
||||
return { success: false, message: error instanceof Error ? error.message : 'Export failed' };
|
||||
} finally {
|
||||
setIsExporting(false);
|
||||
}
|
||||
};
|
||||
|
||||
return {
|
||||
sessions: sessionsQuery.data ?? [],
|
||||
activeSession,
|
||||
@@ -464,5 +511,7 @@ export function useIssueDiscovery(options?: { refetchInterval?: number }): UseIs
|
||||
sessionsQuery.refetch();
|
||||
},
|
||||
exportFindings,
|
||||
exportSelectedFindings,
|
||||
isExporting,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -58,14 +58,11 @@ export function useSkills(options: UseSkillsOptions = {}): UseSkillsReturn {
|
||||
const queryClient = useQueryClient();
|
||||
const projectPath = useWorkflowStore(selectProjectPath);
|
||||
|
||||
// Only enable query when projectPath is available
|
||||
const queryEnabled = enabled && !!projectPath;
|
||||
|
||||
const query = useQuery({
|
||||
queryKey: workspaceQueryKeys.skillsList(projectPath),
|
||||
queryFn: () => fetchSkills(projectPath),
|
||||
staleTime,
|
||||
enabled: queryEnabled,
|
||||
enabled: enabled, // Remove projectPath requirement - API works without it
|
||||
retry: 2,
|
||||
});
|
||||
|
||||
|
||||
Reference in New Issue
Block a user