mirror of
https://github.com/catlog22/Claude-Code-Workflow.git
synced 2026-03-06 16:31:12 +08:00
Add orchestrator types and error handling configurations
- Introduced new TypeScript types for orchestrator functionality, including `SessionStrategy`, `ErrorHandlingStrategy`, and `OrchestrationStep`. - Defined interfaces for `OrchestrationPlan` and `ManualOrchestrationParams` to facilitate orchestration management. - Added a new PNG image file for visual representation. - Created a placeholder file named 'nul' for future use.
This commit is contained in:
@@ -20,6 +20,8 @@ export type { UseWebSocketOptions, UseWebSocketReturn } from './useWebSocket';
|
||||
|
||||
export { useWebSocketNotifications } from './useWebSocketNotifications';
|
||||
|
||||
export { useCompletionCallbackChain } from './useCompletionCallbackChain';
|
||||
|
||||
export { useSystemNotifications } from './useSystemNotifications';
|
||||
export type { UseSystemNotificationsReturn, SystemNotificationOptions } from './useSystemNotifications';
|
||||
|
||||
@@ -54,7 +56,6 @@ export {
|
||||
useUpdateLoopStatus,
|
||||
useDeleteLoop,
|
||||
useLoopMutations,
|
||||
loopsKeys,
|
||||
} from './useLoops';
|
||||
export type {
|
||||
LoopsFilter,
|
||||
@@ -118,7 +119,6 @@ export {
|
||||
useCommands,
|
||||
useCommandSearch,
|
||||
useCommandMutations,
|
||||
commandsKeys,
|
||||
} from './useCommands';
|
||||
export type {
|
||||
CommandsFilter,
|
||||
|
||||
@@ -104,16 +104,13 @@ export function useActiveCliExecutions(
|
||||
enabled: boolean,
|
||||
refetchInterval: number = 5000
|
||||
) {
|
||||
const upsertExecution = useCliStreamStore(state => state.upsertExecution);
|
||||
const removeExecution = useCliStreamStore(state => state.removeExecution);
|
||||
const executions = useCliStreamStore(state => state.executions);
|
||||
const setCurrentExecution = useCliStreamStore(state => state.setCurrentExecution);
|
||||
const isExecutionClosedByUser = useCliStreamStore(state => state.isExecutionClosedByUser);
|
||||
const cleanupUserClosedExecutions = useCliStreamStore(state => state.cleanupUserClosedExecutions);
|
||||
|
||||
return useQuery({
|
||||
queryKey: ACTIVE_CLI_EXECUTIONS_QUERY_KEY,
|
||||
queryFn: async () => {
|
||||
// Access store state at execution time to avoid stale closures
|
||||
const store = useCliStreamStore.getState();
|
||||
const currentExecutions = store.executions;
|
||||
|
||||
const response = await fetch('/api/cli/active');
|
||||
if (!response.ok) {
|
||||
throw new Error(`Failed to fetch active executions: ${response.statusText}`);
|
||||
@@ -124,16 +121,16 @@ export function useActiveCliExecutions(
|
||||
const serverIds = new Set(data.executions.map(e => e.id));
|
||||
|
||||
// Clean up userClosedExecutions - remove those no longer on server
|
||||
cleanupUserClosedExecutions(serverIds);
|
||||
store.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)) {
|
||||
for (const [id, exec] of Object.entries(currentExecutions)) {
|
||||
if (store.isExecutionClosedByUser(id)) {
|
||||
// User closed this execution, remove from local state
|
||||
removeExecution(id);
|
||||
store.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);
|
||||
store.removeExecution(id);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -143,11 +140,11 @@ export function useActiveCliExecutions(
|
||||
|
||||
for (const exec of data.executions) {
|
||||
// Skip if user closed this execution
|
||||
if (isExecutionClosedByUser(exec.id)) {
|
||||
if (store.isExecutionClosedByUser(exec.id)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const existing = executions[exec.id];
|
||||
const existing = currentExecutions[exec.id];
|
||||
const historicalOutput = parseHistoricalOutput(exec.output || '', exec.startTime);
|
||||
|
||||
if (!existing) {
|
||||
@@ -187,7 +184,7 @@ export function useActiveCliExecutions(
|
||||
];
|
||||
}
|
||||
|
||||
upsertExecution(exec.id, {
|
||||
store.upsertExecution(exec.id, {
|
||||
tool: exec.tool || 'cli',
|
||||
mode: exec.mode || 'analysis',
|
||||
status: exec.status || 'running',
|
||||
@@ -200,9 +197,9 @@ export function useActiveCliExecutions(
|
||||
|
||||
// Set current execution to first running execution if none selected
|
||||
if (hasNewExecution) {
|
||||
const runningExec = data.executions.find(e => e.status === 'running' && !isExecutionClosedByUser(e.id));
|
||||
if (runningExec && !executions[runningExec.id]) {
|
||||
setCurrentExecution(runningExec.id);
|
||||
const runningExec = data.executions.find(e => e.status === 'running' && !store.isExecutionClosedByUser(e.id));
|
||||
if (runningExec && !currentExecutions[runningExec.id]) {
|
||||
store.setCurrentExecution(runningExec.id);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -23,7 +23,7 @@ export interface UseCliSessionCoreOptions {
|
||||
/** Default resumeKey used when creating sessions via ensureSession/handleCreateSession. */
|
||||
resumeKey?: string;
|
||||
/** Shell to use when creating new sessions. Defaults to 'bash'. */
|
||||
preferredShell?: string;
|
||||
preferredShell?: 'bash' | 'pwsh';
|
||||
/** Additional createCliSession fields (cols, rows, tool, model). */
|
||||
createSessionDefaults?: {
|
||||
cols?: number;
|
||||
|
||||
@@ -14,13 +14,7 @@ import { useWorkflowStore, selectProjectPath } from '@/stores/workflowStore';
|
||||
import { useNotifications } from './useNotifications';
|
||||
import { sanitizeErrorMessage } from '@/utils/errorSanitizer';
|
||||
import { formatMessage } from '@/lib/i18n';
|
||||
|
||||
// Query key factory
|
||||
export const commandsKeys = {
|
||||
all: ['commands'] as const,
|
||||
lists: () => [...commandsKeys.all, 'list'] as const,
|
||||
list: (filters?: CommandsFilter) => [...commandsKeys.lists(), filters] as const,
|
||||
};
|
||||
import { workspaceQueryKeys } from '@/lib/queryKeys';
|
||||
|
||||
// Default stale time: 10 minutes (commands are static)
|
||||
const STALE_TIME = 10 * 60 * 1000;
|
||||
@@ -84,7 +78,7 @@ export function useCommandMutations(): UseCommandMutationsReturn {
|
||||
const { loadingId } = context ?? { loadingId: '' };
|
||||
if (loadingId) removeToast(loadingId);
|
||||
success(formatMessage('feedback.commandToggle.success'));
|
||||
queryClient.invalidateQueries({ queryKey: commandsKeys.all });
|
||||
queryClient.invalidateQueries({ queryKey: workspaceQueryKeys.commands(projectPath) });
|
||||
},
|
||||
onError: (err, __, context) => {
|
||||
const { loadingId } = context ?? { loadingId: '' };
|
||||
@@ -105,7 +99,7 @@ export function useCommandMutations(): UseCommandMutationsReturn {
|
||||
const { loadingId } = context ?? { loadingId: '' };
|
||||
if (loadingId) removeToast(loadingId);
|
||||
success(formatMessage('feedback.commandToggle.success'));
|
||||
queryClient.invalidateQueries({ queryKey: commandsKeys.all });
|
||||
queryClient.invalidateQueries({ queryKey: workspaceQueryKeys.commands(projectPath) });
|
||||
},
|
||||
onError: (err, __, context) => {
|
||||
const { loadingId } = context ?? { loadingId: '' };
|
||||
@@ -129,10 +123,10 @@ export function useCommands(options: UseCommandsOptions = {}): UseCommandsReturn
|
||||
const projectPath = useWorkflowStore(selectProjectPath);
|
||||
|
||||
const query = useQuery({
|
||||
queryKey: commandsKeys.list(filter),
|
||||
queryKey: workspaceQueryKeys.commandsList(projectPath),
|
||||
queryFn: () => fetchCommands(projectPath),
|
||||
staleTime,
|
||||
enabled: enabled, // Remove projectPath requirement
|
||||
enabled: enabled,
|
||||
retry: 2,
|
||||
});
|
||||
|
||||
@@ -213,7 +207,7 @@ export function useCommands(options: UseCommandsOptions = {}): UseCommandsReturn
|
||||
};
|
||||
|
||||
const invalidate = async () => {
|
||||
await queryClient.invalidateQueries({ queryKey: commandsKeys.all });
|
||||
await queryClient.invalidateQueries({ queryKey: workspaceQueryKeys.commands(projectPath) });
|
||||
};
|
||||
|
||||
return {
|
||||
|
||||
199
ccw/frontend/src/hooks/useCompletionCallbackChain.ts
Normal file
199
ccw/frontend/src/hooks/useCompletionCallbackChain.ts
Normal file
@@ -0,0 +1,199 @@
|
||||
// ========================================
|
||||
// useCompletionCallbackChain Hook
|
||||
// ========================================
|
||||
// Watches for WebSocket CLI_COMPLETED and CLI_ERROR events and connects
|
||||
// them to the orchestratorStore for automated step advancement.
|
||||
//
|
||||
// This hook bridges the WebSocket event layer with the orchestration
|
||||
// state machine, enabling the feedback loop:
|
||||
// CLI execution completes -> WebSocket event -> store update -> next step
|
||||
//
|
||||
// Usage: Mount this hook once at the App level to enable callback chain processing.
|
||||
|
||||
import { useEffect } from 'react';
|
||||
import { useNotificationStore } from '@/stores';
|
||||
import { useOrchestratorStore } from '@/stores/orchestratorStore';
|
||||
import type { WebSocketMessage } from '@/types/store';
|
||||
import type { ErrorHandlingStrategy } from '@/types/orchestrator';
|
||||
|
||||
// ========== Payload Types ==========
|
||||
|
||||
interface CliCompletedPayload {
|
||||
executionId: string;
|
||||
success: boolean;
|
||||
duration?: number;
|
||||
result?: unknown;
|
||||
}
|
||||
|
||||
interface CliErrorPayload {
|
||||
executionId: string;
|
||||
error: string;
|
||||
exitCode?: number;
|
||||
}
|
||||
|
||||
// ========== Hook ==========
|
||||
|
||||
/**
|
||||
* Hook that subscribes to WebSocket completion events and updates
|
||||
* the orchestratorStore accordingly. Enables automated step advancement
|
||||
* by connecting CLI execution results to the orchestration state machine.
|
||||
*
|
||||
* On CLI_COMPLETED:
|
||||
* 1. Look up executionId in all active plans' executionIdMap
|
||||
* 2. Call updateStepStatus(planId, stepId, 'completed')
|
||||
* 3. Call _advanceToNextStep(planId) to identify the next ready step
|
||||
*
|
||||
* On CLI_ERROR / CLI_COMPLETED with success=false:
|
||||
* 1. Look up executionId in all active plans' executionIdMap
|
||||
* 2. Call updateStepStatus(planId, stepId, 'failed', error)
|
||||
* 3. Apply error handling strategy from the step or plan defaults
|
||||
*/
|
||||
export function useCompletionCallbackChain(): void {
|
||||
const wsLastMessage = useNotificationStore((state) => state.wsLastMessage);
|
||||
|
||||
useEffect(() => {
|
||||
if (!wsLastMessage) return;
|
||||
|
||||
const { type, payload } = wsLastMessage as WebSocketMessage & {
|
||||
payload?: unknown;
|
||||
};
|
||||
|
||||
// Only process CLI completion/error events
|
||||
if (type !== 'CLI_COMPLETED' && type !== 'CLI_ERROR') return;
|
||||
|
||||
// Access store state directly (zustand pattern for non-rendering access)
|
||||
const store = useOrchestratorStore.getState();
|
||||
|
||||
if (type === 'CLI_COMPLETED') {
|
||||
handleCliCompleted(store, payload as CliCompletedPayload | undefined);
|
||||
} else if (type === 'CLI_ERROR') {
|
||||
handleCliError(store, payload as CliErrorPayload | undefined);
|
||||
}
|
||||
}, [wsLastMessage]);
|
||||
}
|
||||
|
||||
// ========== Event Handlers ==========
|
||||
|
||||
function handleCliCompleted(
|
||||
store: ReturnType<typeof useOrchestratorStore.getState>,
|
||||
payload: CliCompletedPayload | undefined
|
||||
): void {
|
||||
if (!payload?.executionId) return;
|
||||
|
||||
const { executionId, success, result } = payload;
|
||||
|
||||
// Find which plan/step this execution belongs to
|
||||
const match = findPlanStepByExecutionId(store, executionId);
|
||||
if (!match) return; // Not an orchestrated execution
|
||||
|
||||
const { planId, stepId } = match;
|
||||
|
||||
if (success) {
|
||||
// Step completed successfully
|
||||
store.updateStepStatus(planId, stepId, 'completed', { data: result });
|
||||
// Advance to the next ready step (does not execute, only identifies)
|
||||
store._advanceToNextStep(planId);
|
||||
} else {
|
||||
// CLI_COMPLETED with success=false is treated as a failure
|
||||
handleStepFailure(store, planId, stepId, 'CLI execution completed with failure status');
|
||||
}
|
||||
}
|
||||
|
||||
function handleCliError(
|
||||
store: ReturnType<typeof useOrchestratorStore.getState>,
|
||||
payload: CliErrorPayload | undefined
|
||||
): void {
|
||||
if (!payload?.executionId) return;
|
||||
|
||||
const { executionId, error } = payload;
|
||||
|
||||
// Find which plan/step this execution belongs to
|
||||
const match = findPlanStepByExecutionId(store, executionId);
|
||||
if (!match) return; // Not an orchestrated execution
|
||||
|
||||
const { planId, stepId } = match;
|
||||
handleStepFailure(store, planId, stepId, error);
|
||||
}
|
||||
|
||||
// ========== Helpers ==========
|
||||
|
||||
/**
|
||||
* Look up which plan and step an executionId belongs to by scanning
|
||||
* all active plans' executionIdMap.
|
||||
*/
|
||||
function findPlanStepByExecutionId(
|
||||
store: ReturnType<typeof useOrchestratorStore.getState>,
|
||||
executionId: string
|
||||
): { planId: string; stepId: string } | undefined {
|
||||
for (const [planId, runState] of Object.entries(store.activePlans)) {
|
||||
const stepId = runState.executionIdMap[executionId];
|
||||
if (stepId) {
|
||||
return { planId, stepId };
|
||||
}
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolve the effective error handling strategy for a step.
|
||||
* Step-level errorHandling overrides plan-level defaults.
|
||||
*/
|
||||
function getEffectiveErrorStrategy(
|
||||
store: ReturnType<typeof useOrchestratorStore.getState>,
|
||||
planId: string,
|
||||
stepId: string
|
||||
): ErrorHandlingStrategy {
|
||||
const runState = store.activePlans[planId];
|
||||
if (!runState) return 'pause_on_error';
|
||||
|
||||
// Find the step definition
|
||||
const stepDef = runState.plan.steps.find((s) => s.id === stepId);
|
||||
|
||||
// Step-level strategy overrides plan-level default
|
||||
return (
|
||||
stepDef?.errorHandling?.strategy ??
|
||||
runState.plan.defaultErrorHandling.strategy ??
|
||||
'pause_on_error'
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle step failure by applying the appropriate error handling strategy.
|
||||
*/
|
||||
function handleStepFailure(
|
||||
store: ReturnType<typeof useOrchestratorStore.getState>,
|
||||
planId: string,
|
||||
stepId: string,
|
||||
errorMessage: string
|
||||
): void {
|
||||
// Mark the step as failed
|
||||
store.updateStepStatus(planId, stepId, 'failed', { error: errorMessage });
|
||||
|
||||
// Determine error handling strategy
|
||||
const strategy = getEffectiveErrorStrategy(store, planId, stepId);
|
||||
|
||||
switch (strategy) {
|
||||
case 'pause_on_error':
|
||||
// Pause the orchestration for user intervention
|
||||
store.pauseOrchestration(planId);
|
||||
break;
|
||||
|
||||
case 'skip':
|
||||
// Skip this step and advance to the next
|
||||
store.skipStep(planId, stepId);
|
||||
store._advanceToNextStep(planId);
|
||||
break;
|
||||
|
||||
case 'stop':
|
||||
// Stop the entire orchestration
|
||||
store.stopOrchestration(planId, `Step "${stepId}" failed: ${errorMessage}`);
|
||||
break;
|
||||
|
||||
default:
|
||||
// Fallback: pause on error
|
||||
store.pauseOrchestration(planId);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
export default useCompletionCallbackChain;
|
||||
@@ -13,13 +13,7 @@ import {
|
||||
type HistoryResponse,
|
||||
} from '../lib/api';
|
||||
import { useWorkflowStore, selectProjectPath } from '@/stores/workflowStore';
|
||||
|
||||
// Query key factory
|
||||
export const historyKeys = {
|
||||
all: ['history'] as const,
|
||||
lists: () => [...historyKeys.all, 'list'] as const,
|
||||
list: (filter?: HistoryFilter) => [...historyKeys.lists(), filter] as const,
|
||||
};
|
||||
import { workspaceQueryKeys } from '@/lib/queryKeys';
|
||||
|
||||
export interface HistoryFilter {
|
||||
search?: string;
|
||||
@@ -75,7 +69,7 @@ export function useHistory(options: UseHistoryOptions = {}): UseHistoryReturn {
|
||||
const queryEnabled = enabled && !!projectPath;
|
||||
|
||||
const query = useQuery({
|
||||
queryKey: historyKeys.list(filter),
|
||||
queryKey: workspaceQueryKeys.cliHistoryList(projectPath),
|
||||
queryFn: () => fetchHistory(projectPath),
|
||||
staleTime,
|
||||
enabled: queryEnabled,
|
||||
@@ -113,7 +107,7 @@ export function useHistory(options: UseHistoryOptions = {}): UseHistoryReturn {
|
||||
const deleteSingleMutation = useMutation({
|
||||
mutationFn: deleteExecution,
|
||||
onSuccess: () => {
|
||||
queryClient.invalidateQueries({ queryKey: historyKeys.all });
|
||||
queryClient.invalidateQueries({ queryKey: workspaceQueryKeys.cliHistory(projectPath) });
|
||||
},
|
||||
});
|
||||
|
||||
@@ -121,7 +115,7 @@ export function useHistory(options: UseHistoryOptions = {}): UseHistoryReturn {
|
||||
const deleteByToolMutation = useMutation({
|
||||
mutationFn: deleteExecutionsByTool,
|
||||
onSuccess: () => {
|
||||
queryClient.invalidateQueries({ queryKey: historyKeys.all });
|
||||
queryClient.invalidateQueries({ queryKey: workspaceQueryKeys.cliHistory(projectPath) });
|
||||
},
|
||||
});
|
||||
|
||||
@@ -129,7 +123,7 @@ export function useHistory(options: UseHistoryOptions = {}): UseHistoryReturn {
|
||||
const deleteAllMutation = useMutation({
|
||||
mutationFn: deleteAllHistory,
|
||||
onSuccess: () => {
|
||||
queryClient.invalidateQueries({ queryKey: historyKeys.all });
|
||||
queryClient.invalidateQueries({ queryKey: workspaceQueryKeys.cliHistory(projectPath) });
|
||||
},
|
||||
});
|
||||
|
||||
|
||||
@@ -11,13 +11,7 @@ import {
|
||||
type IndexRebuildRequest,
|
||||
} from '../lib/api';
|
||||
import { useWorkflowStore, selectProjectPath } from '@/stores/workflowStore';
|
||||
|
||||
// ========== Query Keys ==========
|
||||
|
||||
export const indexKeys = {
|
||||
all: ['index'] as const,
|
||||
status: () => [...indexKeys.all, 'status'] as const,
|
||||
};
|
||||
import { workspaceQueryKeys } from '@/lib/queryKeys';
|
||||
|
||||
// ========== Stale Time ==========
|
||||
|
||||
@@ -57,7 +51,7 @@ export function useIndexStatus(options: UseIndexStatusOptions = {}): UseIndexSta
|
||||
const queryEnabled = enabled && !!projectPath;
|
||||
|
||||
const query = useQuery({
|
||||
queryKey: indexKeys.status(),
|
||||
queryKey: workspaceQueryKeys.indexStatus(projectPath),
|
||||
queryFn: () => fetchIndexStatus(projectPath),
|
||||
staleTime,
|
||||
enabled: queryEnabled,
|
||||
@@ -70,7 +64,7 @@ export function useIndexStatus(options: UseIndexStatusOptions = {}): UseIndexSta
|
||||
};
|
||||
|
||||
const invalidate = async () => {
|
||||
await queryClient.invalidateQueries({ queryKey: indexKeys.all });
|
||||
await queryClient.invalidateQueries({ queryKey: workspaceQueryKeys.index(projectPath) });
|
||||
};
|
||||
|
||||
return {
|
||||
@@ -105,12 +99,13 @@ export interface UseRebuildIndexReturn {
|
||||
*/
|
||||
export function useRebuildIndex(): UseRebuildIndexReturn {
|
||||
const queryClient = useQueryClient();
|
||||
const projectPath = useWorkflowStore(selectProjectPath);
|
||||
|
||||
const mutation = useMutation({
|
||||
mutationFn: rebuildIndex,
|
||||
onSuccess: (updatedStatus) => {
|
||||
// Update the status query cache
|
||||
queryClient.setQueryData(indexKeys.status(), updatedStatus);
|
||||
queryClient.setQueryData(workspaceQueryKeys.indexStatus(projectPath), updatedStatus);
|
||||
},
|
||||
});
|
||||
|
||||
|
||||
@@ -14,15 +14,7 @@ import {
|
||||
type LoopsResponse,
|
||||
} from '../lib/api';
|
||||
import { useWorkflowStore, selectProjectPath } from '@/stores/workflowStore';
|
||||
|
||||
// Query key factory
|
||||
export const loopsKeys = {
|
||||
all: ['loops'] as const,
|
||||
lists: () => [...loopsKeys.all, 'list'] as const,
|
||||
list: (filters?: LoopsFilter) => [...loopsKeys.lists(), filters] as const,
|
||||
details: () => [...loopsKeys.all, 'detail'] as const,
|
||||
detail: (id: string) => [...loopsKeys.details(), id] as const,
|
||||
};
|
||||
import { workspaceQueryKeys } from '@/lib/queryKeys';
|
||||
|
||||
// Default stale time: 10 seconds (loops update frequently)
|
||||
const STALE_TIME = 10 * 1000;
|
||||
@@ -63,7 +55,7 @@ export function useLoops(options: UseLoopsOptions = {}): UseLoopsReturn {
|
||||
const queryEnabled = enabled && !!projectPath;
|
||||
|
||||
const query = useQuery({
|
||||
queryKey: loopsKeys.list(filter),
|
||||
queryKey: workspaceQueryKeys.loopsList(projectPath),
|
||||
queryFn: () => fetchLoops(projectPath),
|
||||
staleTime,
|
||||
enabled: queryEnabled,
|
||||
@@ -112,7 +104,7 @@ export function useLoops(options: UseLoopsOptions = {}): UseLoopsReturn {
|
||||
};
|
||||
|
||||
const invalidate = async () => {
|
||||
await queryClient.invalidateQueries({ queryKey: loopsKeys.all });
|
||||
await queryClient.invalidateQueries({ queryKey: workspaceQueryKeys.loops(projectPath) });
|
||||
};
|
||||
|
||||
return {
|
||||
@@ -137,7 +129,7 @@ export function useLoop(loopId: string, options: { enabled?: boolean } = {}) {
|
||||
const queryEnabled = (options.enabled ?? !!loopId) && !!projectPath;
|
||||
|
||||
return useQuery({
|
||||
queryKey: loopsKeys.detail(loopId),
|
||||
queryKey: workspaceQueryKeys.loopDetail(projectPath, loopId),
|
||||
queryFn: () => fetchLoop(loopId, projectPath),
|
||||
enabled: queryEnabled,
|
||||
staleTime: STALE_TIME,
|
||||
@@ -154,11 +146,12 @@ export interface UseCreateLoopReturn {
|
||||
|
||||
export function useCreateLoop(): UseCreateLoopReturn {
|
||||
const queryClient = useQueryClient();
|
||||
const projectPath = useWorkflowStore(selectProjectPath);
|
||||
|
||||
const mutation = useMutation({
|
||||
mutationFn: createLoop,
|
||||
onSuccess: (newLoop) => {
|
||||
queryClient.setQueryData<LoopsResponse>(loopsKeys.list(), (old) => {
|
||||
queryClient.setQueryData<LoopsResponse>(workspaceQueryKeys.loopsList(projectPath), (old) => {
|
||||
if (!old) return { loops: [newLoop], total: 1 };
|
||||
return {
|
||||
loops: [newLoop, ...old.loops],
|
||||
@@ -183,19 +176,20 @@ export interface UseUpdateLoopStatusReturn {
|
||||
|
||||
export function useUpdateLoopStatus(): UseUpdateLoopStatusReturn {
|
||||
const queryClient = useQueryClient();
|
||||
const projectPath = useWorkflowStore(selectProjectPath);
|
||||
|
||||
const mutation = useMutation({
|
||||
mutationFn: ({ loopId, action }: { loopId: string; action: 'pause' | 'resume' | 'stop' }) =>
|
||||
updateLoopStatus(loopId, action),
|
||||
onSuccess: (updatedLoop) => {
|
||||
queryClient.setQueryData<LoopsResponse>(loopsKeys.list(), (old) => {
|
||||
queryClient.setQueryData<LoopsResponse>(workspaceQueryKeys.loopsList(projectPath), (old) => {
|
||||
if (!old) return old;
|
||||
return {
|
||||
...old,
|
||||
loops: old.loops.map((l) => (l.id === updatedLoop.id ? updatedLoop : l)),
|
||||
};
|
||||
});
|
||||
queryClient.setQueryData(loopsKeys.detail(updatedLoop.id), updatedLoop);
|
||||
queryClient.setQueryData(workspaceQueryKeys.loopDetail(projectPath, updatedLoop.id), updatedLoop);
|
||||
},
|
||||
});
|
||||
|
||||
@@ -214,14 +208,15 @@ export interface UseDeleteLoopReturn {
|
||||
|
||||
export function useDeleteLoop(): UseDeleteLoopReturn {
|
||||
const queryClient = useQueryClient();
|
||||
const projectPath = useWorkflowStore(selectProjectPath);
|
||||
|
||||
const mutation = useMutation({
|
||||
mutationFn: deleteLoop,
|
||||
onMutate: async (loopId) => {
|
||||
await queryClient.cancelQueries({ queryKey: loopsKeys.all });
|
||||
const previousLoops = queryClient.getQueryData<LoopsResponse>(loopsKeys.list());
|
||||
await queryClient.cancelQueries({ queryKey: workspaceQueryKeys.loops(projectPath) });
|
||||
const previousLoops = queryClient.getQueryData<LoopsResponse>(workspaceQueryKeys.loopsList(projectPath));
|
||||
|
||||
queryClient.setQueryData<LoopsResponse>(loopsKeys.list(), (old) => {
|
||||
queryClient.setQueryData<LoopsResponse>(workspaceQueryKeys.loopsList(projectPath), (old) => {
|
||||
if (!old) return old;
|
||||
return {
|
||||
...old,
|
||||
@@ -234,11 +229,11 @@ export function useDeleteLoop(): UseDeleteLoopReturn {
|
||||
},
|
||||
onError: (_error, _loopId, context) => {
|
||||
if (context?.previousLoops) {
|
||||
queryClient.setQueryData(loopsKeys.list(), context.previousLoops);
|
||||
queryClient.setQueryData(workspaceQueryKeys.loopsList(projectPath), context.previousLoops);
|
||||
}
|
||||
},
|
||||
onSettled: () => {
|
||||
queryClient.invalidateQueries({ queryKey: loopsKeys.all });
|
||||
queryClient.invalidateQueries({ queryKey: workspaceQueryKeys.loops(projectPath) });
|
||||
},
|
||||
});
|
||||
|
||||
|
||||
@@ -18,15 +18,7 @@ import {
|
||||
type InsightsHistoryResponse,
|
||||
} from '../lib/api';
|
||||
import { useWorkflowStore, selectProjectPath } from '@/stores/workflowStore';
|
||||
|
||||
// Query key factory
|
||||
export const promptHistoryKeys = {
|
||||
all: ['promptHistory'] as const,
|
||||
lists: () => [...promptHistoryKeys.all, 'list'] as const,
|
||||
list: (filters?: PromptHistoryFilter) => [...promptHistoryKeys.lists(), filters] as const,
|
||||
insights: () => [...promptHistoryKeys.all, 'insights'] as const,
|
||||
insightsHistory: () => [...promptHistoryKeys.all, 'insightsHistory'] as const,
|
||||
};
|
||||
import { workspaceQueryKeys } from '@/lib/queryKeys';
|
||||
|
||||
// Default stale time: 30 seconds (prompts update less frequently)
|
||||
const STALE_TIME = 30 * 1000;
|
||||
@@ -78,7 +70,7 @@ export function usePromptHistory(options: UsePromptHistoryOptions = {}): UseProm
|
||||
const queryEnabled = enabled && !!projectPath;
|
||||
|
||||
const query = useQuery({
|
||||
queryKey: promptHistoryKeys.list(filter),
|
||||
queryKey: workspaceQueryKeys.promptsList(projectPath),
|
||||
queryFn: () => fetchPrompts(projectPath),
|
||||
staleTime,
|
||||
enabled: queryEnabled,
|
||||
@@ -179,7 +171,7 @@ export function usePromptHistory(options: UsePromptHistoryOptions = {}): UseProm
|
||||
};
|
||||
|
||||
const invalidate = async () => {
|
||||
await queryClient.invalidateQueries({ queryKey: promptHistoryKeys.all });
|
||||
await queryClient.invalidateQueries({ queryKey: workspaceQueryKeys.prompts(projectPath) });
|
||||
};
|
||||
|
||||
return {
|
||||
@@ -214,7 +206,7 @@ export function usePromptInsights(options: { enabled?: boolean; staleTime?: numb
|
||||
const queryEnabled = enabled && !!projectPath;
|
||||
|
||||
return useQuery({
|
||||
queryKey: promptHistoryKeys.insights(),
|
||||
queryKey: workspaceQueryKeys.promptsInsights(projectPath),
|
||||
queryFn: () => fetchPromptInsights(projectPath),
|
||||
staleTime,
|
||||
enabled: queryEnabled,
|
||||
@@ -236,7 +228,7 @@ export function useInsightsHistory(options: {
|
||||
const queryEnabled = enabled && !!projectPath;
|
||||
|
||||
return useQuery({
|
||||
queryKey: promptHistoryKeys.insightsHistory(),
|
||||
queryKey: workspaceQueryKeys.promptsInsightsHistory(projectPath),
|
||||
queryFn: () => fetchInsightsHistory(projectPath, limit),
|
||||
staleTime,
|
||||
enabled: queryEnabled,
|
||||
@@ -254,12 +246,12 @@ export interface UseAnalyzePromptsReturn {
|
||||
|
||||
export function useAnalyzePrompts(): UseAnalyzePromptsReturn {
|
||||
const queryClient = useQueryClient();
|
||||
const projectPath = useWorkflowStore(selectProjectPath);
|
||||
|
||||
const mutation = useMutation({
|
||||
mutationFn: analyzePrompts,
|
||||
onSuccess: () => {
|
||||
// Invalidate insights query after analysis
|
||||
queryClient.invalidateQueries({ queryKey: promptHistoryKeys.insights() });
|
||||
queryClient.invalidateQueries({ queryKey: workspaceQueryKeys.promptsInsights(projectPath) });
|
||||
},
|
||||
});
|
||||
|
||||
@@ -278,14 +270,15 @@ export interface UseDeletePromptReturn {
|
||||
|
||||
export function useDeletePrompt(): UseDeletePromptReturn {
|
||||
const queryClient = useQueryClient();
|
||||
const projectPath = useWorkflowStore(selectProjectPath);
|
||||
|
||||
const mutation = useMutation({
|
||||
mutationFn: deletePrompt,
|
||||
onMutate: async (promptId) => {
|
||||
await queryClient.cancelQueries({ queryKey: promptHistoryKeys.all });
|
||||
const previousPrompts = queryClient.getQueryData<PromptsResponse>(promptHistoryKeys.list());
|
||||
await queryClient.cancelQueries({ queryKey: workspaceQueryKeys.prompts(projectPath) });
|
||||
const previousPrompts = queryClient.getQueryData<PromptsResponse>(workspaceQueryKeys.promptsList(projectPath));
|
||||
|
||||
queryClient.setQueryData<PromptsResponse>(promptHistoryKeys.list(), (old) => {
|
||||
queryClient.setQueryData<PromptsResponse>(workspaceQueryKeys.promptsList(projectPath), (old) => {
|
||||
if (!old) return old;
|
||||
return {
|
||||
...old,
|
||||
@@ -298,11 +291,11 @@ export function useDeletePrompt(): UseDeletePromptReturn {
|
||||
},
|
||||
onError: (_error, _promptId, context) => {
|
||||
if (context?.previousPrompts) {
|
||||
queryClient.setQueryData(promptHistoryKeys.list(), context.previousPrompts);
|
||||
queryClient.setQueryData(workspaceQueryKeys.promptsList(projectPath), context.previousPrompts);
|
||||
}
|
||||
},
|
||||
onSettled: () => {
|
||||
queryClient.invalidateQueries({ queryKey: promptHistoryKeys.all });
|
||||
queryClient.invalidateQueries({ queryKey: workspaceQueryKeys.prompts(projectPath) });
|
||||
},
|
||||
});
|
||||
|
||||
@@ -321,14 +314,15 @@ export interface UseBatchDeletePromptsReturn {
|
||||
|
||||
export function useBatchDeletePrompts(): UseBatchDeletePromptsReturn {
|
||||
const queryClient = useQueryClient();
|
||||
const projectPath = useWorkflowStore(selectProjectPath);
|
||||
|
||||
const mutation = useMutation({
|
||||
mutationFn: batchDeletePrompts,
|
||||
onMutate: async (promptIds) => {
|
||||
await queryClient.cancelQueries({ queryKey: promptHistoryKeys.all });
|
||||
const previousPrompts = queryClient.getQueryData<PromptsResponse>(promptHistoryKeys.list());
|
||||
await queryClient.cancelQueries({ queryKey: workspaceQueryKeys.prompts(projectPath) });
|
||||
const previousPrompts = queryClient.getQueryData<PromptsResponse>(workspaceQueryKeys.promptsList(projectPath));
|
||||
|
||||
queryClient.setQueryData<PromptsResponse>(promptHistoryKeys.list(), (old) => {
|
||||
queryClient.setQueryData<PromptsResponse>(workspaceQueryKeys.promptsList(projectPath), (old) => {
|
||||
if (!old) return old;
|
||||
return {
|
||||
...old,
|
||||
@@ -341,11 +335,11 @@ export function useBatchDeletePrompts(): UseBatchDeletePromptsReturn {
|
||||
},
|
||||
onError: (_error, _promptIds, context) => {
|
||||
if (context?.previousPrompts) {
|
||||
queryClient.setQueryData(promptHistoryKeys.list(), context.previousPrompts);
|
||||
queryClient.setQueryData(workspaceQueryKeys.promptsList(projectPath), context.previousPrompts);
|
||||
}
|
||||
},
|
||||
onSettled: () => {
|
||||
queryClient.invalidateQueries({ queryKey: promptHistoryKeys.all });
|
||||
queryClient.invalidateQueries({ queryKey: workspaceQueryKeys.prompts(projectPath) });
|
||||
},
|
||||
});
|
||||
|
||||
@@ -369,10 +363,10 @@ export function useDeleteInsight(): UseDeleteInsightReturn {
|
||||
const mutation = useMutation({
|
||||
mutationFn: (insightId: string) => deleteInsight(insightId, projectPath),
|
||||
onMutate: async (insightId) => {
|
||||
await queryClient.cancelQueries({ queryKey: promptHistoryKeys.insightsHistory() });
|
||||
const previousInsights = queryClient.getQueryData<InsightsHistoryResponse>(promptHistoryKeys.insightsHistory());
|
||||
await queryClient.cancelQueries({ queryKey: workspaceQueryKeys.promptsInsightsHistory(projectPath) });
|
||||
const previousInsights = queryClient.getQueryData<InsightsHistoryResponse>(workspaceQueryKeys.promptsInsightsHistory(projectPath));
|
||||
|
||||
queryClient.setQueryData<InsightsHistoryResponse>(promptHistoryKeys.insightsHistory(), (old) => {
|
||||
queryClient.setQueryData<InsightsHistoryResponse>(workspaceQueryKeys.promptsInsightsHistory(projectPath), (old) => {
|
||||
if (!old) return old;
|
||||
return {
|
||||
...old,
|
||||
@@ -384,11 +378,11 @@ export function useDeleteInsight(): UseDeleteInsightReturn {
|
||||
},
|
||||
onError: (_error, _insightId, context) => {
|
||||
if (context?.previousInsights) {
|
||||
queryClient.setQueryData(promptHistoryKeys.insightsHistory(), context.previousInsights);
|
||||
queryClient.setQueryData(workspaceQueryKeys.promptsInsightsHistory(projectPath), context.previousInsights);
|
||||
}
|
||||
},
|
||||
onSettled: () => {
|
||||
queryClient.invalidateQueries({ queryKey: promptHistoryKeys.insightsHistory() });
|
||||
queryClient.invalidateQueries({ queryKey: workspaceQueryKeys.promptsInsightsHistory(projectPath) });
|
||||
},
|
||||
});
|
||||
|
||||
|
||||
@@ -3,11 +3,11 @@
|
||||
// ========================================
|
||||
// TanStack Query hooks for team execution visualization
|
||||
|
||||
import { useQuery, useQueryClient } from '@tanstack/react-query';
|
||||
import { fetchTeams, fetchTeamMessages, fetchTeamStatus } from '@/lib/api';
|
||||
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
|
||||
import { fetchTeams, fetchTeamMessages, fetchTeamStatus, archiveTeam, unarchiveTeam, deleteTeam } from '@/lib/api';
|
||||
import { useTeamStore } from '@/stores/teamStore';
|
||||
import type {
|
||||
TeamSummary,
|
||||
TeamSummaryExtended,
|
||||
TeamMessage,
|
||||
TeamMember,
|
||||
TeamMessageFilter,
|
||||
@@ -20,30 +20,33 @@ import type {
|
||||
export const teamKeys = {
|
||||
all: ['teams'] as const,
|
||||
lists: () => [...teamKeys.all, 'list'] as const,
|
||||
listByLocation: (location: string) => [...teamKeys.lists(), location] as const,
|
||||
messages: (team: string, filter?: TeamMessageFilter) =>
|
||||
[...teamKeys.all, 'messages', team, filter] as const,
|
||||
status: (team: string) => [...teamKeys.all, 'status', team] as const,
|
||||
};
|
||||
|
||||
/**
|
||||
* Hook: list all teams
|
||||
* Hook: list all teams with location filter
|
||||
*/
|
||||
export function useTeams() {
|
||||
export function useTeams(location?: string) {
|
||||
const autoRefresh = useTeamStore((s) => s.autoRefresh);
|
||||
const effectiveLocation = location || 'active';
|
||||
|
||||
const query = useQuery({
|
||||
queryKey: teamKeys.lists(),
|
||||
queryKey: teamKeys.listByLocation(effectiveLocation),
|
||||
queryFn: async (): Promise<TeamsListResponse> => {
|
||||
const data = await fetchTeams();
|
||||
return { teams: data.teams ?? [] };
|
||||
const data = await fetchTeams(effectiveLocation);
|
||||
return { teams: (data.teams ?? []) as TeamSummaryExtended[] };
|
||||
},
|
||||
staleTime: 10_000,
|
||||
refetchInterval: autoRefresh ? 10_000 : false,
|
||||
});
|
||||
|
||||
return {
|
||||
teams: (query.data?.teams ?? []) as TeamSummary[],
|
||||
teams: (query.data?.teams ?? []) as TeamSummaryExtended[],
|
||||
isLoading: query.isLoading,
|
||||
isFetching: query.isFetching,
|
||||
error: query.error,
|
||||
refetch: query.refetch,
|
||||
};
|
||||
@@ -125,3 +128,65 @@ export function useInvalidateTeamData() {
|
||||
const queryClient = useQueryClient();
|
||||
return () => queryClient.invalidateQueries({ queryKey: teamKeys.all });
|
||||
}
|
||||
|
||||
// ========== Mutation Hooks ==========
|
||||
|
||||
/**
|
||||
* Hook: archive a team
|
||||
*/
|
||||
export function useArchiveTeam() {
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
const mutation = useMutation({
|
||||
mutationFn: archiveTeam,
|
||||
onSuccess: () => {
|
||||
queryClient.invalidateQueries({ queryKey: teamKeys.all });
|
||||
},
|
||||
});
|
||||
|
||||
return {
|
||||
archiveTeam: mutation.mutateAsync,
|
||||
isArchiving: mutation.isPending,
|
||||
error: mutation.error,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Hook: unarchive a team
|
||||
*/
|
||||
export function useUnarchiveTeam() {
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
const mutation = useMutation({
|
||||
mutationFn: unarchiveTeam,
|
||||
onSuccess: () => {
|
||||
queryClient.invalidateQueries({ queryKey: teamKeys.all });
|
||||
},
|
||||
});
|
||||
|
||||
return {
|
||||
unarchiveTeam: mutation.mutateAsync,
|
||||
isUnarchiving: mutation.isPending,
|
||||
error: mutation.error,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Hook: delete a team
|
||||
*/
|
||||
export function useDeleteTeam() {
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
const mutation = useMutation({
|
||||
mutationFn: deleteTeam,
|
||||
onSuccess: () => {
|
||||
queryClient.invalidateQueries({ queryKey: teamKeys.all });
|
||||
},
|
||||
});
|
||||
|
||||
return {
|
||||
deleteTeam: mutation.mutateAsync,
|
||||
isDeleting: mutation.isPending,
|
||||
error: mutation.error,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -434,8 +434,8 @@ export function useWebSocket(options: UseWebSocketOptions = {}): UseWebSocketRet
|
||||
scheduleReconnect();
|
||||
};
|
||||
|
||||
ws.onerror = (error) => {
|
||||
console.error('[WebSocket] Error:', error);
|
||||
ws.onerror = () => {
|
||||
console.warn('[WebSocket] Connection error');
|
||||
getStoreState().setWsStatus('error');
|
||||
};
|
||||
} catch (error) {
|
||||
|
||||
Reference in New Issue
Block a user