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:
catlog22
2026-02-14 12:54:08 +08:00
parent cdb240d2c2
commit 4d22ae4b2f
56 changed files with 4767 additions and 425 deletions

View File

@@ -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,

View File

@@ -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);
}
}

View File

@@ -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;

View File

@@ -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 {

View 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;

View File

@@ -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) });
},
});

View File

@@ -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);
},
});

View File

@@ -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) });
},
});

View File

@@ -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) });
},
});

View File

@@ -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,
};
}

View File

@@ -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) {