feat: add tests and implementation for issue discovery and queue pages

- Implemented `DiscoveryPage` with session management and findings display.
- Added tests for `DiscoveryPage` to ensure proper rendering and functionality.
- Created `QueuePage` for managing issue execution queues with stats and actions.
- Added tests for `QueuePage` to verify UI elements and translations.
- Introduced `useIssues` hooks for fetching and managing issue data.
- Added loading skeletons and error handling for better user experience.
- Created `vite-env.d.ts` for TypeScript support in Vite environment.
This commit is contained in:
catlog22
2026-01-31 21:20:10 +08:00
parent 6d225948d1
commit 1bd082a725
79 changed files with 5870 additions and 449 deletions

View File

@@ -70,6 +70,8 @@ export {
useUpdateIssue,
useDeleteIssue,
useIssueMutations,
useQueueMutations,
useIssueDiscovery,
issuesKeys,
} from './useIssues';
export type {
@@ -79,6 +81,9 @@ export type {
UseCreateIssueReturn,
UseUpdateIssueReturn,
UseDeleteIssueReturn,
UseQueueMutationsReturn,
FindingFilters,
UseIssueDiscoveryReturn,
} from './useIssues';
// ========== Skills ==========

View File

@@ -10,6 +10,8 @@ import {
type CliEndpoint,
type CliEndpointsResponse,
} from '../lib/api';
import { useWorkflowStore, selectProjectPath } from '@/stores/workflowStore';
import { workspaceQueryKeys } from '@/lib/queryKeys';
// Query key factory
export const cliEndpointsKeys = {
@@ -273,12 +275,15 @@ export interface UseHooksReturn {
export function useHooks(options: UseHooksOptions = {}): UseHooksReturn {
const { staleTime = STALE_TIME, enabled = true } = options;
const queryClient = useQueryClient();
const projectPath = useWorkflowStore(selectProjectPath);
const queryEnabled = enabled && !!projectPath;
const query = useQuery({
queryKey: hooksKeys.lists(),
queryFn: fetchHooks,
queryKey: workspaceQueryKeys.rulesList(projectPath),
queryFn: () => fetchHooks(projectPath),
staleTime,
enabled,
enabled: queryEnabled,
retry: 2,
});
@@ -381,12 +386,15 @@ export interface UseRulesReturn {
export function useRules(options: UseRulesOptions = {}): UseRulesReturn {
const { staleTime = STALE_TIME, enabled = true } = options;
const queryClient = useQueryClient();
const projectPath = useWorkflowStore(selectProjectPath);
const queryEnabled = enabled && !!projectPath;
const query = useQuery({
queryKey: rulesKeys.lists(),
queryFn: fetchRules,
queryKey: workspaceQueryKeys.rulesList(projectPath),
queryFn: () => fetchRules(projectPath),
staleTime,
enabled,
enabled: queryEnabled,
retry: 2,
});

View File

@@ -8,6 +8,7 @@ import {
fetchCommands,
type Command,
} from '../lib/api';
import { useWorkflowStore, selectProjectPath } from '@/stores/workflowStore';
// Query key factory
export const commandsKeys = {
@@ -50,11 +51,14 @@ export function useCommands(options: UseCommandsOptions = {}): UseCommandsReturn
const { filter, staleTime = STALE_TIME, enabled = true } = options;
const queryClient = useQueryClient();
const projectPath = useWorkflowStore(selectProjectPath);
const queryEnabled = enabled && !!projectPath;
const query = useQuery({
queryKey: commandsKeys.list(filter),
queryFn: fetchCommands,
queryFn: () => fetchCommands(projectPath),
staleTime,
enabled,
enabled: queryEnabled,
retry: 2,
});

View File

@@ -73,7 +73,7 @@ export function useDashboardStats(
const query = useQuery({
queryKey: workspaceQueryKeys.projectOverview(projectPath),
queryFn: fetchDashboardStats,
queryFn: () => fetchDashboardStats(projectPath),
staleTime,
enabled: queryEnabled,
refetchInterval: refetchInterval > 0 ? refetchInterval : false,
@@ -114,7 +114,7 @@ export function usePrefetchDashboardStats() {
if (projectPath) {
queryClient.prefetchQuery({
queryKey: workspaceQueryKeys.projectOverview(projectPath),
queryFn: fetchDashboardStats,
queryFn: () => fetchDashboardStats(projectPath),
staleTime: STALE_TIME,
});
}

View File

@@ -12,6 +12,7 @@ import {
deleteAllHistory,
type HistoryResponse,
} from '../lib/api';
import { useWorkflowStore, selectProjectPath } from '@/stores/workflowStore';
// Query key factory
export const historyKeys = {
@@ -70,11 +71,14 @@ export function useHistory(options: UseHistoryOptions = {}): UseHistoryReturn {
const { filter, staleTime = STALE_TIME, enabled = true } = options;
const queryClient = useQueryClient();
const projectPath = useWorkflowStore(selectProjectPath);
const queryEnabled = enabled && !!projectPath;
const query = useQuery({
queryKey: historyKeys.list(filter),
queryFn: fetchHistory,
queryFn: () => fetchHistory(projectPath),
staleTime,
enabled,
enabled: queryEnabled,
retry: 2,
retryDelay: (attemptIndex) => Math.min(1000 * 2 ** attemptIndex, 10000),
});

View File

@@ -10,6 +10,7 @@ import {
type IndexStatus,
type IndexRebuildRequest,
} from '../lib/api';
import { useWorkflowStore, selectProjectPath } from '@/stores/workflowStore';
// ========== Query Keys ==========
@@ -52,11 +53,14 @@ export function useIndexStatus(options: UseIndexStatusOptions = {}): UseIndexSta
const { staleTime = STALE_TIME, enabled = true, refetchInterval = 0 } = options;
const queryClient = useQueryClient();
const projectPath = useWorkflowStore(selectProjectPath);
const queryEnabled = enabled && !!projectPath;
const query = useQuery({
queryKey: indexKeys.status(),
queryFn: fetchIndexStatus,
queryFn: () => fetchIndexStatus(projectPath),
staleTime,
enabled,
enabled: queryEnabled,
refetchInterval: refetchInterval > 0 ? refetchInterval : false,
retry: 2,
});

View File

@@ -0,0 +1,323 @@
// ========================================
// useIssues Hook Tests
// ========================================
// Tests for issue-related hooks with queue and discovery
import { describe, it, expect, beforeEach, vi } from 'vitest';
import { renderHook, waitFor } from '@testing-library/react';
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
import {
useIssueQueue,
useIssueMutations,
useQueueMutations,
useIssueDiscovery,
} from './useIssues';
import * as api from '@/lib/api';
// Create a proper query client wrapper
const createTestQueryClient = () => {
return new QueryClient({
defaultOptions: {
queries: {
retry: false,
},
mutations: {
retry: false,
},
},
});
};
const createWrapper = () => {
const queryClient = createTestQueryClient();
return ({ children }: { children: React.ReactNode }) => (
<QueryClientProvider client={queryClient}>{children}</QueryClientProvider>
);
};
// Mock store
vi.mock('@/stores/workflowStore', () => ({
useWorkflowStore: () => '/test/path',
selectProjectPath: () => '/test/path',
}));
// Mock API - use vi.mocked for type safety
vi.mock('@/lib/api', () => ({
fetchIssueQueue: vi.fn(),
activateQueue: vi.fn(),
deactivateQueue: vi.fn(),
deleteQueue: vi.fn(),
mergeQueues: vi.fn(),
fetchDiscoveries: vi.fn(),
fetchDiscoveryFindings: vi.fn(),
}));
describe('useIssueQueue', () => {
beforeEach(() => {
vi.clearAllMocks();
});
it('should fetch queue data successfully', async () => {
const mockQueue = {
tasks: ['task1', 'task2'],
solutions: ['solution1'],
conflicts: [],
execution_groups: { 'group-1': ['task1'] },
grouped_items: { 'parallel-group': ['task1', 'task2'] },
};
vi.mocked(api.fetchIssueQueue).mockResolvedValue(mockQueue);
const { result } = renderHook(() => useIssueQueue(), {
wrapper: createWrapper(),
});
await waitFor(() => {
expect(result.current.data).toEqual(mockQueue);
});
});
it('should handle API errors', async () => {
vi.mocked(api.fetchIssueQueue).mockRejectedValue(new Error('API Error'));
const { result } = renderHook(() => useIssueQueue(), {
wrapper: createWrapper(),
});
// Verify the hook returns expected structure even with error
expect(result.current).toHaveProperty('isLoading');
expect(result.current).toHaveProperty('data');
expect(result.current).toHaveProperty('error');
});
});
describe('useQueueMutations', () => {
beforeEach(() => {
vi.clearAllMocks();
});
it('should activate queue successfully', async () => {
vi.mocked(api.activateQueue).mockResolvedValue(undefined);
const { result } = renderHook(() => useQueueMutations(), {
wrapper: createWrapper(),
});
await result.current.activateQueue('queue-1');
expect(api.activateQueue).toHaveBeenCalledWith('queue-1', '/test/path');
});
it('should deactivate queue successfully', async () => {
vi.mocked(api.deactivateQueue).mockResolvedValue(undefined);
const { result } = renderHook(() => useQueueMutations(), {
wrapper: createWrapper(),
});
await result.current.deactivateQueue();
expect(api.deactivateQueue).toHaveBeenCalledWith('/test/path');
});
it('should delete queue successfully', async () => {
vi.mocked(api.deleteQueue).mockResolvedValue(undefined);
const { result } = renderHook(() => useQueueMutations(), {
wrapper: createWrapper(),
});
await result.current.deleteQueue('queue-1');
expect(api.deleteQueue).toHaveBeenCalledWith('queue-1', '/test/path');
});
it('should merge queues successfully', async () => {
vi.mocked(api.mergeQueues).mockResolvedValue(undefined);
const { result } = renderHook(() => useQueueMutations(), {
wrapper: createWrapper(),
});
await result.current.mergeQueues('source-1', 'target-1');
expect(api.mergeQueues).toHaveBeenCalledWith('source-1', 'target-1', '/test/path');
});
it('should track overall mutation state', () => {
const { result } = renderHook(() => useQueueMutations(), {
wrapper: createWrapper(),
});
expect(result.current.isMutating).toBe(false);
});
});
describe('useIssueDiscovery', () => {
beforeEach(() => {
vi.clearAllMocks();
});
it('should fetch discovery sessions successfully', async () => {
const mockSessions = [
{
id: '1',
name: 'Session 1',
status: 'running' as const,
progress: 50,
findings_count: 5,
created_at: '2024-01-01T00:00:00Z',
},
];
vi.mocked(api.fetchDiscoveries).mockResolvedValue(mockSessions);
const { result } = renderHook(() => useIssueDiscovery(), {
wrapper: createWrapper(),
});
await waitFor(() => {
expect(result.current.sessions).toHaveLength(1);
expect(result.current.sessions[0].name).toBe('Session 1');
});
});
it('should filter findings by severity', async () => {
const mockFindings = [
{ id: '1', title: 'Critical issue', severity: 'critical' as const, type: 'bug', description: '' },
{ id: '2', title: 'Minor issue', severity: 'low' as const, type: 'enhancement', description: '' },
];
vi.mocked(api.fetchDiscoveries).mockResolvedValue([
{ id: '1', name: 'Session 1', status: 'completed' as const, progress: 100, findings_count: 2, created_at: '2024-01-01T00:00:00Z' },
]);
vi.mocked(api.fetchDiscoveryFindings).mockResolvedValue(mockFindings);
const { result } = renderHook(() => useIssueDiscovery(), {
wrapper: createWrapper(),
});
// Wait for sessions to load
await waitFor(() => {
expect(result.current.sessions).toHaveLength(1);
});
// Select a session to load findings
result.current.selectSession('1');
await waitFor(() => {
expect(result.current.findings).toHaveLength(2);
});
// Apply severity filter
result.current.setFilters({ severity: 'critical' as const });
await waitFor(() => {
expect(result.current.filteredFindings).toHaveLength(1);
expect(result.current.filteredFindings[0].severity).toBe('critical');
});
});
it('should filter findings by type', async () => {
const mockFindings = [
{ id: '1', title: 'Bug 1', severity: 'high' as const, type: 'bug', description: '' },
{ id: '2', title: 'Enhancement 1', severity: 'medium' as const, type: 'enhancement', description: '' },
];
vi.mocked(api.fetchDiscoveries).mockResolvedValue([
{ id: '1', name: 'Session 1', status: 'completed' as const, progress: 100, findings_count: 2, created_at: '2024-01-01T00:00:00Z' },
]);
vi.mocked(api.fetchDiscoveryFindings).mockResolvedValue(mockFindings);
const { result } = renderHook(() => useIssueDiscovery(), {
wrapper: createWrapper(),
});
// Wait for sessions to load
await waitFor(() => {
expect(result.current.sessions).toHaveLength(1);
});
// Select a session to load findings
result.current.selectSession('1');
await waitFor(() => {
expect(result.current.findings).toHaveLength(2);
});
// Apply type filter
result.current.setFilters({ type: 'bug' });
await waitFor(() => {
expect(result.current.filteredFindings).toHaveLength(1);
expect(result.current.filteredFindings[0].type).toBe('bug');
});
});
it('should search findings by text', async () => {
const mockFindings = [
{ id: '1', title: 'Authentication error', severity: 'high' as const, type: 'bug', description: 'Login fails' },
{ id: '2', title: 'UI bug', severity: 'medium' as const, type: 'bug', description: 'Button color' },
];
vi.mocked(api.fetchDiscoveries).mockResolvedValue([
{ id: '1', name: 'Session 1', status: 'completed' as const, progress: 100, findings_count: 2, created_at: '2024-01-01T00:00:00Z' },
]);
vi.mocked(api.fetchDiscoveryFindings).mockResolvedValue(mockFindings);
const { result } = renderHook(() => useIssueDiscovery(), {
wrapper: createWrapper(),
});
// Wait for sessions to load
await waitFor(() => {
expect(result.current.sessions).toHaveLength(1);
});
// Select a session to load findings
result.current.selectSession('1');
await waitFor(() => {
expect(result.current.findings).toHaveLength(2);
});
// Apply search filter
result.current.setFilters({ search: 'authentication' });
await waitFor(() => {
expect(result.current.filteredFindings).toHaveLength(1);
expect(result.current.filteredFindings[0].title).toContain('Authentication');
});
});
it('should export findings as JSON', async () => {
const mockFindings = [
{ id: '1', title: 'Test finding', severity: 'high' as const, type: 'bug', description: 'Test' },
];
vi.mocked(api.fetchDiscoveries).mockResolvedValue([
{ id: '1', name: 'Session 1', status: 'completed' as const, progress: 100, findings_count: 1, created_at: '2024-01-01T00:00:00Z' },
]);
vi.mocked(api.fetchDiscoveryFindings).mockResolvedValue(mockFindings);
const { result } = renderHook(() => useIssueDiscovery(), {
wrapper: createWrapper(),
});
// Wait for sessions to load
await waitFor(() => {
expect(result.current.sessions).toHaveLength(1);
});
// Select a session to load findings
result.current.selectSession('1');
await waitFor(() => {
expect(result.current.findings).toHaveLength(1);
});
// Verify exportFindings is available as a function
expect(typeof result.current.exportFindings).toBe('function');
});
});

View File

@@ -3,7 +3,7 @@
// ========================================
// TanStack Query hooks for issues with queue management
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
import { useQuery, useMutation, useQueryClient, type UseQueryResult } from '@tanstack/react-query';
import {
fetchIssues,
fetchIssueHistory,
@@ -11,11 +11,21 @@ import {
createIssue,
updateIssue,
deleteIssue,
activateQueue,
deactivateQueue,
deleteQueue as deleteQueueApi,
mergeQueues as mergeQueuesApi,
fetchDiscoveries,
fetchDiscoveryFindings,
type Issue,
type IssueQueue,
type IssuesResponse,
type DiscoverySession,
type Finding,
} from '../lib/api';
import { useWorkflowStore, selectProjectPath } from '@/stores/workflowStore';
import { workspaceQueryKeys } from '@/lib/queryKeys';
import { useState, useMemo } from 'react';
// Query key factory
export const issuesKeys = {
@@ -181,9 +191,9 @@ export function useIssues(options: UseIssuesOptions = {}): UseIssuesReturn {
/**
* Hook for fetching issue queue
*/
export function useIssueQueue(): ReturnType<typeof useQuery> {
export function useIssueQueue(): UseQueryResult<IssueQueue> {
const projectPath = useWorkflowStore(selectProjectPath);
return useQuery({
return useQuery<IssueQueue>({
queryKey: projectPath ? workspaceQueryKeys.issueQueue(projectPath) : ['issueQueue', 'no-project'],
queryFn: () => fetchIssueQueue(projectPath),
staleTime: STALE_TIME,
@@ -288,3 +298,171 @@ export function useIssueMutations() {
isMutating: create.isCreating || update.isUpdating || remove.isDeleting,
};
}
// ========== Queue Mutations ==========
export interface UseQueueMutationsReturn {
activateQueue: (queueId: string) => Promise<void>;
deactivateQueue: () => Promise<void>;
deleteQueue: (queueId: string) => Promise<void>;
mergeQueues: (sourceId: string, targetId: string) => Promise<void>;
isActivating: boolean;
isDeactivating: boolean;
isDeleting: boolean;
isMerging: boolean;
isMutating: boolean;
}
export function useQueueMutations(): UseQueueMutationsReturn {
const queryClient = useQueryClient();
const projectPath = useWorkflowStore(selectProjectPath);
const activateMutation = useMutation({
mutationFn: (queueId: string) => activateQueue(queueId, projectPath),
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: workspaceQueryKeys.issueQueue(projectPath) });
},
});
const deactivateMutation = useMutation({
mutationFn: () => deactivateQueue(projectPath),
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: workspaceQueryKeys.issueQueue(projectPath) });
},
});
const deleteMutation = useMutation({
mutationFn: (queueId: string) => deleteQueueApi(queueId, projectPath),
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: workspaceQueryKeys.issueQueue(projectPath) });
},
});
const mergeMutation = useMutation({
mutationFn: ({ sourceId, targetId }: { sourceId: string; targetId: string }) =>
mergeQueuesApi(sourceId, targetId, 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 }),
isActivating: activateMutation.isPending,
isDeactivating: deactivateMutation.isPending,
isDeleting: deleteMutation.isPending,
isMerging: mergeMutation.isPending,
isMutating: activateMutation.isPending || deactivateMutation.isPending || deleteMutation.isPending || mergeMutation.isPending,
};
}
// ========== Discovery Hook ==========
export interface FindingFilters {
severity?: 'critical' | 'high' | 'medium' | 'low';
type?: string;
search?: string;
}
export interface UseIssueDiscoveryReturn {
sessions: DiscoverySession[];
activeSession: DiscoverySession | null;
findings: Finding[];
filteredFindings: Finding[];
isLoadingSessions: boolean;
isLoadingFindings: boolean;
error: Error | null;
filters: FindingFilters;
setFilters: (filters: FindingFilters) => void;
selectSession: (sessionId: string) => void;
refetchSessions: () => void;
exportFindings: () => void;
}
export function useIssueDiscovery(options?: { refetchInterval?: number }): UseIssueDiscoveryReturn {
const { refetchInterval = 0 } = options ?? {};
const queryClient = useQueryClient();
const projectPath = useWorkflowStore(selectProjectPath);
const [activeSessionId, setActiveSessionId] = useState<string | null>(null);
const [filters, setFilters] = useState<FindingFilters>({});
const sessionsQuery = useQuery({
queryKey: workspaceQueryKeys.discoveries(projectPath),
queryFn: () => fetchDiscoveries(projectPath),
staleTime: STALE_TIME,
enabled: !!projectPath,
refetchInterval: refetchInterval > 0 ? refetchInterval : false,
retry: 2,
});
const findingsQuery = useQuery({
queryKey: activeSessionId ? ['discoveryFindings', activeSessionId, projectPath] : ['discoveryFindings', 'no-session'],
queryFn: () => activeSessionId ? fetchDiscoveryFindings(activeSessionId, projectPath) : [],
staleTime: STALE_TIME,
enabled: !!activeSessionId && !!projectPath,
retry: 2,
});
const activeSession = useMemo(
() => sessionsQuery.data?.find(s => s.id === activeSessionId) ?? null,
[sessionsQuery.data, activeSessionId]
);
const filteredFindings = useMemo(() => {
let findings = findingsQuery.data ?? [];
if (filters.severity) {
findings = findings.filter(f => f.severity === filters.severity);
}
if (filters.type) {
findings = findings.filter(f => f.type === filters.type);
}
if (filters.search) {
const searchLower = filters.search.toLowerCase();
findings = findings.filter(f =>
f.title.toLowerCase().includes(searchLower) ||
f.description.toLowerCase().includes(searchLower)
);
}
return findings;
}, [findingsQuery.data, filters]);
const selectSession = (sessionId: string) => {
setActiveSessionId(sessionId);
};
const exportFindings = () => {
if (!activeSessionId || !findingsQuery.data) return;
const data = {
session: activeSession,
findings: findingsQuery.data,
exported_at: new Date().toISOString(),
};
const blob = new Blob([JSON.stringify(data, null, 2)], { type: 'application/json' });
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = `discovery-${activeSessionId}.json`;
a.click();
URL.revokeObjectURL(url);
};
return {
sessions: sessionsQuery.data ?? [],
activeSession,
findings: findingsQuery.data ?? [],
filteredFindings,
isLoadingSessions: sessionsQuery.isLoading,
isLoadingFindings: findingsQuery.isLoading,
error: sessionsQuery.error || findingsQuery.error,
filters,
setFilters,
selectSession,
refetchSessions: () => {
sessionsQuery.refetch();
},
exportFindings,
};
}

View File

@@ -5,6 +5,8 @@
import { useQuery, useQueryClient } from '@tanstack/react-query';
import { fetchLiteTasks, fetchLiteTaskSession, type LiteTaskSession, type LiteTasksResponse } from '@/lib/api';
import { useWorkflowStore, selectProjectPath } from '@/stores/workflowStore';
import { workspaceQueryKeys } from '@/lib/queryKeys';
type LiteTaskType = 'lite-plan' | 'lite-fix' | 'multi-cli-plan';
@@ -18,6 +20,10 @@ interface UseLiteTasksOptions {
*/
export function useLiteTasks(options: UseLiteTasksOptions = {}) {
const queryClient = useQueryClient();
const projectPath = useWorkflowStore(selectProjectPath);
// Only enable query when projectPath is available
const queryEnabled = (options.enabled ?? true) && !!projectPath;
const {
data = { litePlan: [], liteFix: [], multiCliPlan: [] },
@@ -25,11 +31,11 @@ export function useLiteTasks(options: UseLiteTasksOptions = {}) {
error,
refetch,
} = useQuery<LiteTasksResponse>({
queryKey: ['liteTasks'],
queryFn: fetchLiteTasks,
queryKey: workspaceQueryKeys.liteTasks(projectPath),
queryFn: () => fetchLiteTasks(projectPath),
staleTime: 30000,
refetchInterval: options.refetchInterval,
enabled: options.enabled ?? true,
enabled: queryEnabled,
});
// Get all sessions flattened
@@ -55,7 +61,7 @@ export function useLiteTasks(options: UseLiteTasksOptions = {}) {
const prefetchSession = (sessionId: string, type: LiteTaskType) => {
queryClient.prefetchQuery({
queryKey: ['liteTask', sessionId, type],
queryFn: () => fetchLiteTaskSession(sessionId, type),
queryFn: () => fetchLiteTaskSession(sessionId, type, projectPath),
staleTime: 60000,
});
};
@@ -77,15 +83,20 @@ export function useLiteTasks(options: UseLiteTasksOptions = {}) {
* Hook for fetching a single lite task session
*/
export function useLiteTaskSession(sessionId: string | undefined, type: LiteTaskType) {
const projectPath = useWorkflowStore(selectProjectPath);
// Only enable query when sessionId, type, and projectPath are available
const queryEnabled = !!sessionId && !!type && !!projectPath;
const {
data: session,
isLoading,
error,
refetch,
} = useQuery<LiteTaskSession | null>({
queryKey: ['liteTask', sessionId, type],
queryFn: () => (sessionId ? fetchLiteTaskSession(sessionId, type) : Promise.resolve(null)),
enabled: !!sessionId && !!type,
queryKey: ['liteTask', sessionId, type, projectPath],
queryFn: () => (sessionId ? fetchLiteTaskSession(sessionId, type, projectPath) : Promise.resolve(null)),
enabled: queryEnabled,
staleTime: 60000,
});

View File

@@ -13,6 +13,7 @@ import {
type Loop,
type LoopsResponse,
} from '../lib/api';
import { useWorkflowStore, selectProjectPath } from '@/stores/workflowStore';
// Query key factory
export const loopsKeys = {
@@ -58,11 +59,14 @@ export function useLoops(options: UseLoopsOptions = {}): UseLoopsReturn {
const { filter, staleTime = STALE_TIME, enabled = true, refetchInterval = 0 } = options;
const queryClient = useQueryClient();
const projectPath = useWorkflowStore(selectProjectPath);
const queryEnabled = enabled && !!projectPath;
const query = useQuery({
queryKey: loopsKeys.list(filter),
queryFn: fetchLoops,
queryFn: () => fetchLoops(projectPath),
staleTime,
enabled,
enabled: queryEnabled,
refetchInterval: refetchInterval > 0 ? refetchInterval : false,
retry: 2,
});
@@ -129,10 +133,13 @@ export function useLoops(options: UseLoopsOptions = {}): UseLoopsReturn {
* Hook for fetching a single loop
*/
export function useLoop(loopId: string, options: { enabled?: boolean } = {}) {
const projectPath = useWorkflowStore(selectProjectPath);
const queryEnabled = (options.enabled ?? !!loopId) && !!projectPath;
return useQuery({
queryKey: loopsKeys.detail(loopId),
queryFn: () => fetchLoop(loopId),
enabled: options.enabled ?? !!loopId,
queryFn: () => fetchLoop(loopId, projectPath),
enabled: queryEnabled,
staleTime: STALE_TIME,
});
}

View File

@@ -13,6 +13,7 @@ import {
type McpServer,
type McpServersResponse,
} from '../lib/api';
import { useWorkflowStore, selectProjectPath } from '@/stores/workflowStore';
// Query key factory
export const mcpServersKeys = {
@@ -50,11 +51,14 @@ export function useMcpServers(options: UseMcpServersOptions = {}): UseMcpServers
const { scope, staleTime = STALE_TIME, enabled = true } = options;
const queryClient = useQueryClient();
const projectPath = useWorkflowStore(selectProjectPath);
const queryEnabled = enabled && !!projectPath;
const query = useQuery({
queryKey: mcpServersKeys.list(scope),
queryFn: fetchMcpServers,
queryFn: () => fetchMcpServers(projectPath),
staleTime,
enabled,
enabled: queryEnabled,
retry: 2,
});

View File

@@ -63,7 +63,7 @@ export function useMemory(options: UseMemoryOptions = {}): UseMemoryReturn {
const query = useQuery({
queryKey: workspaceQueryKeys.memoryList(projectPath),
queryFn: fetchMemories,
queryFn: () => fetchMemories(projectPath),
staleTime,
enabled: queryEnabled,
retry: 2,
@@ -137,7 +137,7 @@ export function useCreateMemory(): UseCreateMemoryReturn {
const projectPath = useWorkflowStore(selectProjectPath);
const mutation = useMutation({
mutationFn: createMemory,
mutationFn: (input: { content: string; tags?: string[] }) => createMemory(input, projectPath),
onSuccess: () => {
// Invalidate memory cache to trigger refetch
queryClient.invalidateQueries({ queryKey: projectPath ? workspaceQueryKeys.memory(projectPath) : ['memory'] });
@@ -163,7 +163,7 @@ export function useUpdateMemory(): UseUpdateMemoryReturn {
const mutation = useMutation({
mutationFn: ({ memoryId, input }: { memoryId: string; input: Partial<CoreMemory> }) =>
updateMemory(memoryId, input),
updateMemory(memoryId, input, projectPath),
onSuccess: () => {
// Invalidate memory cache to trigger refetch
queryClient.invalidateQueries({ queryKey: projectPath ? workspaceQueryKeys.memory(projectPath) : ['memory'] });
@@ -188,7 +188,7 @@ export function useDeleteMemory(): UseDeleteMemoryReturn {
const projectPath = useWorkflowStore(selectProjectPath);
const mutation = useMutation({
mutationFn: deleteMemory,
mutationFn: (memoryId: string) => deleteMemory(memoryId, projectPath),
onSuccess: () => {
// Invalidate to ensure sync with server
queryClient.invalidateQueries({ queryKey: projectPath ? workspaceQueryKeys.memory(projectPath) : ['memory'] });

View File

@@ -5,6 +5,7 @@
import { useQuery } from '@tanstack/react-query';
import { fetchProjectOverview } from '../lib/api';
import { useWorkflowStore, selectProjectPath } from '@/stores/workflowStore';
// Query key factory
export const projectOverviewKeys = {
@@ -33,11 +34,14 @@ export interface UseProjectOverviewOptions {
export function useProjectOverview(options: UseProjectOverviewOptions = {}) {
const { staleTime = STALE_TIME, enabled = true } = options;
const projectPath = useWorkflowStore(selectProjectPath);
const queryEnabled = enabled && !!projectPath;
const query = useQuery({
queryKey: projectOverviewKeys.detail(),
queryFn: fetchProjectOverview,
queryFn: () => fetchProjectOverview(projectPath),
staleTime,
enabled,
enabled: queryEnabled,
retry: 2,
retryDelay: (attemptIndex) => Math.min(1000 * 2 ** attemptIndex, 10000),
});

View File

@@ -16,6 +16,7 @@ import {
type PromptsResponse,
type PromptInsightsResponse,
} from '../lib/api';
import { useWorkflowStore, selectProjectPath } from '@/stores/workflowStore';
// Query key factory
export const promptHistoryKeys = {
@@ -63,11 +64,14 @@ export function usePromptHistory(options: UsePromptHistoryOptions = {}): UseProm
const { filter, staleTime = STALE_TIME, enabled = true } = options;
const queryClient = useQueryClient();
const projectPath = useWorkflowStore(selectProjectPath);
const queryEnabled = enabled && !!projectPath;
const query = useQuery({
queryKey: promptHistoryKeys.list(filter),
queryFn: fetchPrompts,
queryFn: () => fetchPrompts(projectPath),
staleTime,
enabled,
enabled: queryEnabled,
retry: 2,
});
@@ -159,11 +163,14 @@ export function usePromptHistory(options: UsePromptHistoryOptions = {}): UseProm
export function usePromptInsights(options: { enabled?: boolean; staleTime?: number } = {}) {
const { enabled = true, staleTime = STALE_TIME } = options;
const projectPath = useWorkflowStore(selectProjectPath);
const queryEnabled = enabled && !!projectPath;
return useQuery({
queryKey: promptHistoryKeys.insights(),
queryFn: fetchPromptInsights,
queryFn: () => fetchPromptInsights(projectPath),
staleTime,
enabled,
enabled: queryEnabled,
retry: 2,
});
}

View File

@@ -5,6 +5,7 @@
import { useQuery } from '@tanstack/react-query';
import { fetchSessionDetail } from '../lib/api';
import { useWorkflowStore, selectProjectPath } from '@/stores/workflowStore';
// Query key factory
export const sessionDetailKeys = {
@@ -33,11 +34,14 @@ export interface UseSessionDetailOptions {
export function useSessionDetail(sessionId: string, options: UseSessionDetailOptions = {}) {
const { staleTime = STALE_TIME, enabled = true } = options;
const projectPath = useWorkflowStore(selectProjectPath);
const queryEnabled = enabled && !!sessionId && !!projectPath;
const query = useQuery({
queryKey: sessionDetailKeys.detail(sessionId),
queryFn: () => fetchSessionDetail(sessionId),
queryFn: () => fetchSessionDetail(sessionId, projectPath),
staleTime,
enabled: enabled && !!sessionId,
enabled: queryEnabled,
retry: 2,
retryDelay: (attemptIndex) => Math.min(1000 * 2 ** attemptIndex, 10000),
});

View File

@@ -89,7 +89,7 @@ export function useSessions(options: UseSessionsOptions = {}): UseSessionsReturn
const query = useQuery({
queryKey: workspaceQueryKeys.sessionsList(projectPath),
queryFn: fetchSessions,
queryFn: () => fetchSessions(projectPath),
staleTime,
enabled: queryEnabled,
refetchInterval: refetchInterval > 0 ? refetchInterval : false,

View File

@@ -63,7 +63,7 @@ export function useSkills(options: UseSkillsOptions = {}): UseSkillsReturn {
const query = useQuery({
queryKey: workspaceQueryKeys.skillsList(projectPath),
queryFn: fetchSkills,
queryFn: () => fetchSkills(projectPath),
staleTime,
enabled: queryEnabled,
retry: 2,