mirror of
https://github.com/AutoMaker-Org/automaker.git
synced 2026-01-30 06:12:03 +00:00
feat(ui): add React Query mutation hooks
- Add feature mutations (create, update, delete with optimistic updates) - Add auto-mode mutations (start, stop, approve plan) - Add worktree mutations (create, delete, checkout, switch branch) - Add settings mutations (update global/project, validate API keys) - Add GitHub mutations (create PR, validate PR) - Add cursor permissions mutations (apply profile, copy config) - Add spec mutations (generate, update, save) - Add pipeline mutations (toggle, update config) - Add session mutations with cache invalidation Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
79
apps/ui/src/hooks/mutations/index.ts
Normal file
79
apps/ui/src/hooks/mutations/index.ts
Normal file
@@ -0,0 +1,79 @@
|
||||
/**
|
||||
* Mutations Barrel Export
|
||||
*
|
||||
* Central export point for all React Query mutations.
|
||||
*
|
||||
* @example
|
||||
* ```tsx
|
||||
* import { useCreateFeature, useStartFeature, useCommitWorktree } from '@/hooks/mutations';
|
||||
* ```
|
||||
*/
|
||||
|
||||
// Feature mutations
|
||||
export {
|
||||
useCreateFeature,
|
||||
useUpdateFeature,
|
||||
useDeleteFeature,
|
||||
useGenerateTitle,
|
||||
useBatchUpdateFeatures,
|
||||
} from './use-feature-mutations';
|
||||
|
||||
// Auto mode mutations
|
||||
export {
|
||||
useStartFeature,
|
||||
useResumeFeature,
|
||||
useStopFeature,
|
||||
useVerifyFeature,
|
||||
useApprovePlan,
|
||||
useFollowUpFeature,
|
||||
useCommitFeature,
|
||||
useAnalyzeProject,
|
||||
useStartAutoMode,
|
||||
useStopAutoMode,
|
||||
} from './use-auto-mode-mutations';
|
||||
|
||||
// Settings mutations
|
||||
export {
|
||||
useUpdateGlobalSettings,
|
||||
useUpdateProjectSettings,
|
||||
useSaveCredentials,
|
||||
} from './use-settings-mutations';
|
||||
|
||||
// Worktree mutations
|
||||
export {
|
||||
useCreateWorktree,
|
||||
useDeleteWorktree,
|
||||
useCommitWorktree,
|
||||
usePushWorktree,
|
||||
usePullWorktree,
|
||||
useCreatePullRequest,
|
||||
useMergeWorktree,
|
||||
useSwitchBranch,
|
||||
useCheckoutBranch,
|
||||
useGenerateCommitMessage,
|
||||
useOpenInEditor,
|
||||
useInitGit,
|
||||
useSetInitScript,
|
||||
useDeleteInitScript,
|
||||
} from './use-worktree-mutations';
|
||||
|
||||
// GitHub mutations
|
||||
export {
|
||||
useValidateIssue,
|
||||
useMarkValidationViewed,
|
||||
useGetValidationStatus,
|
||||
} from './use-github-mutations';
|
||||
|
||||
// Ideation mutations
|
||||
export { useGenerateIdeationSuggestions } from './use-ideation-mutations';
|
||||
|
||||
// Spec mutations
|
||||
export {
|
||||
useCreateSpec,
|
||||
useRegenerateSpec,
|
||||
useGenerateFeatures,
|
||||
useSaveSpec,
|
||||
} from './use-spec-mutations';
|
||||
|
||||
// Cursor Permissions mutations
|
||||
export { useApplyCursorProfile, useCopyCursorConfig } from './use-cursor-permissions-mutations';
|
||||
373
apps/ui/src/hooks/mutations/use-auto-mode-mutations.ts
Normal file
373
apps/ui/src/hooks/mutations/use-auto-mode-mutations.ts
Normal file
@@ -0,0 +1,373 @@
|
||||
/**
|
||||
* Auto Mode Mutations
|
||||
*
|
||||
* React Query mutations for auto mode operations like running features,
|
||||
* stopping features, and plan approval.
|
||||
*/
|
||||
|
||||
import { useMutation, useQueryClient } from '@tanstack/react-query';
|
||||
import { getElectronAPI } from '@/lib/electron';
|
||||
import { queryKeys } from '@/lib/query-keys';
|
||||
import { toast } from 'sonner';
|
||||
|
||||
/**
|
||||
* Start running a feature in auto mode
|
||||
*
|
||||
* @param projectPath - Path to the project
|
||||
* @returns Mutation for starting a feature
|
||||
*
|
||||
* @example
|
||||
* ```tsx
|
||||
* const startFeature = useStartFeature(projectPath);
|
||||
* startFeature.mutate({ featureId: 'abc123', useWorktrees: true });
|
||||
* ```
|
||||
*/
|
||||
export function useStartFeature(projectPath: string) {
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
return useMutation({
|
||||
mutationFn: async ({
|
||||
featureId,
|
||||
useWorktrees,
|
||||
worktreePath,
|
||||
}: {
|
||||
featureId: string;
|
||||
useWorktrees?: boolean;
|
||||
worktreePath?: string;
|
||||
}) => {
|
||||
const api = getElectronAPI();
|
||||
const result = await api.autoMode.runFeature(
|
||||
projectPath,
|
||||
featureId,
|
||||
useWorktrees,
|
||||
worktreePath
|
||||
);
|
||||
if (!result.success) {
|
||||
throw new Error(result.error || 'Failed to start feature');
|
||||
}
|
||||
return result;
|
||||
},
|
||||
onSuccess: () => {
|
||||
queryClient.invalidateQueries({ queryKey: queryKeys.runningAgents.all() });
|
||||
queryClient.invalidateQueries({ queryKey: queryKeys.features.all(projectPath) });
|
||||
},
|
||||
onError: (error: Error) => {
|
||||
toast.error('Failed to start feature', {
|
||||
description: error.message,
|
||||
});
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Resume a paused or interrupted feature
|
||||
*
|
||||
* @param projectPath - Path to the project
|
||||
* @returns Mutation for resuming a feature
|
||||
*/
|
||||
export function useResumeFeature(projectPath: string) {
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
return useMutation({
|
||||
mutationFn: async ({
|
||||
featureId,
|
||||
useWorktrees,
|
||||
}: {
|
||||
featureId: string;
|
||||
useWorktrees?: boolean;
|
||||
}) => {
|
||||
const api = getElectronAPI();
|
||||
const result = await api.autoMode.resumeFeature(projectPath, featureId, useWorktrees);
|
||||
if (!result.success) {
|
||||
throw new Error(result.error || 'Failed to resume feature');
|
||||
}
|
||||
return result;
|
||||
},
|
||||
onSuccess: () => {
|
||||
queryClient.invalidateQueries({ queryKey: queryKeys.runningAgents.all() });
|
||||
queryClient.invalidateQueries({ queryKey: queryKeys.features.all(projectPath) });
|
||||
},
|
||||
onError: (error: Error) => {
|
||||
toast.error('Failed to resume feature', {
|
||||
description: error.message,
|
||||
});
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Stop a running feature
|
||||
*
|
||||
* @returns Mutation for stopping a feature
|
||||
*/
|
||||
export function useStopFeature() {
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
return useMutation({
|
||||
mutationFn: async (featureId: string) => {
|
||||
const api = getElectronAPI();
|
||||
const result = await api.autoMode.stopFeature(featureId);
|
||||
if (!result.success) {
|
||||
throw new Error(result.error || 'Failed to stop feature');
|
||||
}
|
||||
return result;
|
||||
},
|
||||
onSuccess: () => {
|
||||
queryClient.invalidateQueries({ queryKey: queryKeys.runningAgents.all() });
|
||||
toast.success('Feature stopped');
|
||||
},
|
||||
onError: (error: Error) => {
|
||||
toast.error('Failed to stop feature', {
|
||||
description: error.message,
|
||||
});
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Verify a completed feature
|
||||
*
|
||||
* @param projectPath - Path to the project
|
||||
* @returns Mutation for verifying a feature
|
||||
*/
|
||||
export function useVerifyFeature(projectPath: string) {
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
return useMutation({
|
||||
mutationFn: async (featureId: string) => {
|
||||
const api = getElectronAPI();
|
||||
const result = await api.autoMode.verifyFeature(projectPath, featureId);
|
||||
if (!result.success) {
|
||||
throw new Error(result.error || 'Failed to verify feature');
|
||||
}
|
||||
return result;
|
||||
},
|
||||
onSuccess: () => {
|
||||
queryClient.invalidateQueries({ queryKey: queryKeys.features.all(projectPath) });
|
||||
},
|
||||
onError: (error: Error) => {
|
||||
toast.error('Failed to verify feature', {
|
||||
description: error.message,
|
||||
});
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Approve or reject a plan
|
||||
*
|
||||
* @param projectPath - Path to the project
|
||||
* @returns Mutation for plan approval
|
||||
*
|
||||
* @example
|
||||
* ```tsx
|
||||
* const approvePlan = useApprovePlan(projectPath);
|
||||
* approvePlan.mutate({ featureId: 'abc', approved: true });
|
||||
* ```
|
||||
*/
|
||||
export function useApprovePlan(projectPath: string) {
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
return useMutation({
|
||||
mutationFn: async ({
|
||||
featureId,
|
||||
approved,
|
||||
editedPlan,
|
||||
feedback,
|
||||
}: {
|
||||
featureId: string;
|
||||
approved: boolean;
|
||||
editedPlan?: string;
|
||||
feedback?: string;
|
||||
}) => {
|
||||
const api = getElectronAPI();
|
||||
const result = await api.autoMode.approvePlan(
|
||||
projectPath,
|
||||
featureId,
|
||||
approved,
|
||||
editedPlan,
|
||||
feedback
|
||||
);
|
||||
if (!result.success) {
|
||||
throw new Error(result.error || 'Failed to submit plan decision');
|
||||
}
|
||||
return result;
|
||||
},
|
||||
onSuccess: (_, { approved }) => {
|
||||
queryClient.invalidateQueries({ queryKey: queryKeys.features.all(projectPath) });
|
||||
if (approved) {
|
||||
toast.success('Plan approved');
|
||||
} else {
|
||||
toast.info('Plan rejected');
|
||||
}
|
||||
},
|
||||
onError: (error: Error) => {
|
||||
toast.error('Failed to submit plan decision', {
|
||||
description: error.message,
|
||||
});
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Send a follow-up prompt to a feature
|
||||
*
|
||||
* @param projectPath - Path to the project
|
||||
* @returns Mutation for sending follow-up
|
||||
*/
|
||||
export function useFollowUpFeature(projectPath: string) {
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
return useMutation({
|
||||
mutationFn: async ({
|
||||
featureId,
|
||||
prompt,
|
||||
imagePaths,
|
||||
useWorktrees,
|
||||
}: {
|
||||
featureId: string;
|
||||
prompt: string;
|
||||
imagePaths?: string[];
|
||||
useWorktrees?: boolean;
|
||||
}) => {
|
||||
const api = getElectronAPI();
|
||||
const result = await api.autoMode.followUpFeature(
|
||||
projectPath,
|
||||
featureId,
|
||||
prompt,
|
||||
imagePaths,
|
||||
useWorktrees
|
||||
);
|
||||
if (!result.success) {
|
||||
throw new Error(result.error || 'Failed to send follow-up');
|
||||
}
|
||||
return result;
|
||||
},
|
||||
onSuccess: () => {
|
||||
queryClient.invalidateQueries({ queryKey: queryKeys.runningAgents.all() });
|
||||
queryClient.invalidateQueries({ queryKey: queryKeys.features.all(projectPath) });
|
||||
},
|
||||
onError: (error: Error) => {
|
||||
toast.error('Failed to send follow-up', {
|
||||
description: error.message,
|
||||
});
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Commit feature changes
|
||||
*
|
||||
* @param projectPath - Path to the project
|
||||
* @returns Mutation for committing feature
|
||||
*/
|
||||
export function useCommitFeature(projectPath: string) {
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
return useMutation({
|
||||
mutationFn: async (featureId: string) => {
|
||||
const api = getElectronAPI();
|
||||
const result = await api.autoMode.commitFeature(projectPath, featureId);
|
||||
if (!result.success) {
|
||||
throw new Error(result.error || 'Failed to commit changes');
|
||||
}
|
||||
return result;
|
||||
},
|
||||
onSuccess: () => {
|
||||
queryClient.invalidateQueries({ queryKey: queryKeys.features.all(projectPath) });
|
||||
queryClient.invalidateQueries({ queryKey: queryKeys.worktrees.all(projectPath) });
|
||||
toast.success('Changes committed');
|
||||
},
|
||||
onError: (error: Error) => {
|
||||
toast.error('Failed to commit changes', {
|
||||
description: error.message,
|
||||
});
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Analyze project structure
|
||||
*
|
||||
* @returns Mutation for project analysis
|
||||
*/
|
||||
export function useAnalyzeProject() {
|
||||
return useMutation({
|
||||
mutationFn: async (projectPath: string) => {
|
||||
const api = getElectronAPI();
|
||||
const result = await api.autoMode.analyzeProject(projectPath);
|
||||
if (!result.success) {
|
||||
throw new Error(result.error || 'Failed to analyze project');
|
||||
}
|
||||
return result;
|
||||
},
|
||||
onSuccess: () => {
|
||||
toast.success('Project analysis started');
|
||||
},
|
||||
onError: (error: Error) => {
|
||||
toast.error('Failed to analyze project', {
|
||||
description: error.message,
|
||||
});
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Start auto mode for all pending features
|
||||
*
|
||||
* @param projectPath - Path to the project
|
||||
* @returns Mutation for starting auto mode
|
||||
*/
|
||||
export function useStartAutoMode(projectPath: string) {
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
return useMutation({
|
||||
mutationFn: async (maxConcurrency?: number) => {
|
||||
const api = getElectronAPI();
|
||||
const result = await api.autoMode.start(projectPath, maxConcurrency);
|
||||
if (!result.success) {
|
||||
throw new Error(result.error || 'Failed to start auto mode');
|
||||
}
|
||||
return result;
|
||||
},
|
||||
onSuccess: () => {
|
||||
queryClient.invalidateQueries({ queryKey: queryKeys.runningAgents.all() });
|
||||
toast.success('Auto mode started');
|
||||
},
|
||||
onError: (error: Error) => {
|
||||
toast.error('Failed to start auto mode', {
|
||||
description: error.message,
|
||||
});
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Stop auto mode for all features
|
||||
*
|
||||
* @param projectPath - Path to the project
|
||||
* @returns Mutation for stopping auto mode
|
||||
*/
|
||||
export function useStopAutoMode(projectPath: string) {
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
return useMutation({
|
||||
mutationFn: async () => {
|
||||
const api = getElectronAPI();
|
||||
const result = await api.autoMode.stop(projectPath);
|
||||
if (!result.success) {
|
||||
throw new Error(result.error || 'Failed to stop auto mode');
|
||||
}
|
||||
return result;
|
||||
},
|
||||
onSuccess: () => {
|
||||
queryClient.invalidateQueries({ queryKey: queryKeys.runningAgents.all() });
|
||||
toast.success('Auto mode stopped');
|
||||
},
|
||||
onError: (error: Error) => {
|
||||
toast.error('Failed to stop auto mode', {
|
||||
description: error.message,
|
||||
});
|
||||
},
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,96 @@
|
||||
/**
|
||||
* Cursor Permissions Mutation Hooks
|
||||
*
|
||||
* React Query mutations for managing Cursor CLI permissions.
|
||||
*/
|
||||
|
||||
import { useMutation, useQueryClient } from '@tanstack/react-query';
|
||||
import { getHttpApiClient } from '@/lib/http-api-client';
|
||||
import { queryKeys } from '@/lib/query-keys';
|
||||
import { toast } from 'sonner';
|
||||
|
||||
interface ApplyProfileInput {
|
||||
profileId: 'strict' | 'development';
|
||||
scope: 'global' | 'project';
|
||||
}
|
||||
|
||||
/**
|
||||
* Apply a Cursor permission profile
|
||||
*
|
||||
* @param projectPath - Optional path to the project (required for project scope)
|
||||
* @returns Mutation for applying permission profiles
|
||||
*
|
||||
* @example
|
||||
* ```tsx
|
||||
* const applyMutation = useApplyCursorProfile(projectPath);
|
||||
* applyMutation.mutate({ profileId: 'development', scope: 'project' });
|
||||
* ```
|
||||
*/
|
||||
export function useApplyCursorProfile(projectPath?: string) {
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
return useMutation({
|
||||
mutationFn: async (input: ApplyProfileInput) => {
|
||||
const { profileId, scope } = input;
|
||||
const api = getHttpApiClient();
|
||||
const result = await api.setup.applyCursorPermissionProfile(
|
||||
profileId,
|
||||
scope,
|
||||
scope === 'project' ? projectPath : undefined
|
||||
);
|
||||
|
||||
if (!result.success) {
|
||||
throw new Error(result.error || 'Failed to apply profile');
|
||||
}
|
||||
|
||||
return result;
|
||||
},
|
||||
onSuccess: (result) => {
|
||||
// Invalidate permissions cache
|
||||
queryClient.invalidateQueries({
|
||||
queryKey: queryKeys.cursorPermissions.permissions(projectPath),
|
||||
});
|
||||
toast.success(result.message || 'Profile applied');
|
||||
},
|
||||
onError: (error) => {
|
||||
toast.error('Failed to apply profile', {
|
||||
description: error instanceof Error ? error.message : 'Unknown error',
|
||||
});
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Copy Cursor example config to clipboard
|
||||
*
|
||||
* @returns Mutation for copying config
|
||||
*
|
||||
* @example
|
||||
* ```tsx
|
||||
* const copyMutation = useCopyCursorConfig();
|
||||
* copyMutation.mutate('development');
|
||||
* ```
|
||||
*/
|
||||
export function useCopyCursorConfig() {
|
||||
return useMutation({
|
||||
mutationFn: async (profileId: 'strict' | 'development') => {
|
||||
const api = getHttpApiClient();
|
||||
const result = await api.setup.getCursorExampleConfig(profileId);
|
||||
|
||||
if (!result.success || !result.config) {
|
||||
throw new Error(result.error || 'Failed to get config');
|
||||
}
|
||||
|
||||
await navigator.clipboard.writeText(result.config);
|
||||
return result;
|
||||
},
|
||||
onSuccess: () => {
|
||||
toast.success('Config copied to clipboard');
|
||||
},
|
||||
onError: (error) => {
|
||||
toast.error('Failed to copy config', {
|
||||
description: error instanceof Error ? error.message : 'Unknown error',
|
||||
});
|
||||
},
|
||||
});
|
||||
}
|
||||
267
apps/ui/src/hooks/mutations/use-feature-mutations.ts
Normal file
267
apps/ui/src/hooks/mutations/use-feature-mutations.ts
Normal file
@@ -0,0 +1,267 @@
|
||||
/**
|
||||
* Feature Mutations
|
||||
*
|
||||
* React Query mutations for creating, updating, and deleting features.
|
||||
* Includes optimistic updates for better UX.
|
||||
*/
|
||||
|
||||
import { useMutation, useQueryClient } from '@tanstack/react-query';
|
||||
import { getElectronAPI } from '@/lib/electron';
|
||||
import { queryKeys } from '@/lib/query-keys';
|
||||
import { toast } from 'sonner';
|
||||
import type { Feature } from '@/store/app-store';
|
||||
|
||||
/**
|
||||
* Create a new feature
|
||||
*
|
||||
* @param projectPath - Path to the project
|
||||
* @returns Mutation for creating a feature
|
||||
*
|
||||
* @example
|
||||
* ```tsx
|
||||
* const createFeature = useCreateFeature(projectPath);
|
||||
* createFeature.mutate({ id: 'uuid', title: 'New Feature', ... });
|
||||
* ```
|
||||
*/
|
||||
export function useCreateFeature(projectPath: string) {
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
return useMutation({
|
||||
mutationFn: async (feature: Feature) => {
|
||||
const api = getElectronAPI();
|
||||
const result = await api.features?.create(projectPath, feature);
|
||||
if (!result?.success) {
|
||||
throw new Error(result?.error || 'Failed to create feature');
|
||||
}
|
||||
return result.feature;
|
||||
},
|
||||
onSuccess: () => {
|
||||
queryClient.invalidateQueries({
|
||||
queryKey: queryKeys.features.all(projectPath),
|
||||
});
|
||||
toast.success('Feature created');
|
||||
},
|
||||
onError: (error: Error) => {
|
||||
toast.error('Failed to create feature', {
|
||||
description: error.message,
|
||||
});
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Update an existing feature
|
||||
*
|
||||
* @param projectPath - Path to the project
|
||||
* @returns Mutation for updating a feature with optimistic updates
|
||||
*/
|
||||
export function useUpdateFeature(projectPath: string) {
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
return useMutation({
|
||||
mutationFn: async ({
|
||||
featureId,
|
||||
updates,
|
||||
descriptionHistorySource,
|
||||
enhancementMode,
|
||||
preEnhancementDescription,
|
||||
}: {
|
||||
featureId: string;
|
||||
updates: Partial<Feature>;
|
||||
descriptionHistorySource?: 'enhance' | 'edit';
|
||||
enhancementMode?: 'improve' | 'technical' | 'simplify' | 'acceptance' | 'ux-reviewer';
|
||||
preEnhancementDescription?: string;
|
||||
}) => {
|
||||
const api = getElectronAPI();
|
||||
const result = await api.features?.update(
|
||||
projectPath,
|
||||
featureId,
|
||||
updates,
|
||||
descriptionHistorySource,
|
||||
enhancementMode,
|
||||
preEnhancementDescription
|
||||
);
|
||||
if (!result?.success) {
|
||||
throw new Error(result?.error || 'Failed to update feature');
|
||||
}
|
||||
return result.feature;
|
||||
},
|
||||
// Optimistic update
|
||||
onMutate: async ({ featureId, updates }) => {
|
||||
// Cancel any outgoing refetches
|
||||
await queryClient.cancelQueries({
|
||||
queryKey: queryKeys.features.all(projectPath),
|
||||
});
|
||||
|
||||
// Snapshot the previous value
|
||||
const previousFeatures = queryClient.getQueryData<Feature[]>(
|
||||
queryKeys.features.all(projectPath)
|
||||
);
|
||||
|
||||
// Optimistically update the cache
|
||||
if (previousFeatures) {
|
||||
queryClient.setQueryData<Feature[]>(
|
||||
queryKeys.features.all(projectPath),
|
||||
previousFeatures.map((f) => (f.id === featureId ? { ...f, ...updates } : f))
|
||||
);
|
||||
}
|
||||
|
||||
return { previousFeatures };
|
||||
},
|
||||
onError: (error: Error, _, context) => {
|
||||
// Rollback on error
|
||||
if (context?.previousFeatures) {
|
||||
queryClient.setQueryData(queryKeys.features.all(projectPath), context.previousFeatures);
|
||||
}
|
||||
toast.error('Failed to update feature', {
|
||||
description: error.message,
|
||||
});
|
||||
},
|
||||
onSettled: () => {
|
||||
// Always refetch after error or success
|
||||
queryClient.invalidateQueries({
|
||||
queryKey: queryKeys.features.all(projectPath),
|
||||
});
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete a feature
|
||||
*
|
||||
* @param projectPath - Path to the project
|
||||
* @returns Mutation for deleting a feature with optimistic updates
|
||||
*/
|
||||
export function useDeleteFeature(projectPath: string) {
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
return useMutation({
|
||||
mutationFn: async (featureId: string) => {
|
||||
const api = getElectronAPI();
|
||||
const result = await api.features?.delete(projectPath, featureId);
|
||||
if (!result?.success) {
|
||||
throw new Error(result?.error || 'Failed to delete feature');
|
||||
}
|
||||
},
|
||||
// Optimistic delete
|
||||
onMutate: async (featureId) => {
|
||||
await queryClient.cancelQueries({
|
||||
queryKey: queryKeys.features.all(projectPath),
|
||||
});
|
||||
|
||||
const previousFeatures = queryClient.getQueryData<Feature[]>(
|
||||
queryKeys.features.all(projectPath)
|
||||
);
|
||||
|
||||
if (previousFeatures) {
|
||||
queryClient.setQueryData<Feature[]>(
|
||||
queryKeys.features.all(projectPath),
|
||||
previousFeatures.filter((f) => f.id !== featureId)
|
||||
);
|
||||
}
|
||||
|
||||
return { previousFeatures };
|
||||
},
|
||||
onError: (error: Error, _, context) => {
|
||||
if (context?.previousFeatures) {
|
||||
queryClient.setQueryData(queryKeys.features.all(projectPath), context.previousFeatures);
|
||||
}
|
||||
toast.error('Failed to delete feature', {
|
||||
description: error.message,
|
||||
});
|
||||
},
|
||||
onSuccess: () => {
|
||||
toast.success('Feature deleted');
|
||||
},
|
||||
onSettled: () => {
|
||||
queryClient.invalidateQueries({
|
||||
queryKey: queryKeys.features.all(projectPath),
|
||||
});
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate a title for a feature description
|
||||
*
|
||||
* @returns Mutation for generating a title
|
||||
*/
|
||||
export function useGenerateTitle() {
|
||||
return useMutation({
|
||||
mutationFn: async (description: string) => {
|
||||
const api = getElectronAPI();
|
||||
const result = await api.features?.generateTitle(description);
|
||||
if (!result?.success) {
|
||||
throw new Error(result?.error || 'Failed to generate title');
|
||||
}
|
||||
return result.title ?? '';
|
||||
},
|
||||
onError: (error: Error) => {
|
||||
toast.error('Failed to generate title', {
|
||||
description: error.message,
|
||||
});
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Batch update multiple features (for reordering)
|
||||
*
|
||||
* @param projectPath - Path to the project
|
||||
* @returns Mutation for batch updating features
|
||||
*/
|
||||
export function useBatchUpdateFeatures(projectPath: string) {
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
return useMutation({
|
||||
mutationFn: async (updates: Array<{ featureId: string; updates: Partial<Feature> }>) => {
|
||||
const api = getElectronAPI();
|
||||
const results = await Promise.all(
|
||||
updates.map(({ featureId, updates: featureUpdates }) =>
|
||||
api.features?.update(projectPath, featureId, featureUpdates)
|
||||
)
|
||||
);
|
||||
|
||||
const failed = results.filter((r) => !r?.success);
|
||||
if (failed.length > 0) {
|
||||
throw new Error(`Failed to update ${failed.length} features`);
|
||||
}
|
||||
},
|
||||
// Optimistic batch update
|
||||
onMutate: async (updates) => {
|
||||
await queryClient.cancelQueries({
|
||||
queryKey: queryKeys.features.all(projectPath),
|
||||
});
|
||||
|
||||
const previousFeatures = queryClient.getQueryData<Feature[]>(
|
||||
queryKeys.features.all(projectPath)
|
||||
);
|
||||
|
||||
if (previousFeatures) {
|
||||
const updatesMap = new Map(updates.map((u) => [u.featureId, u.updates]));
|
||||
queryClient.setQueryData<Feature[]>(
|
||||
queryKeys.features.all(projectPath),
|
||||
previousFeatures.map((f) => {
|
||||
const featureUpdates = updatesMap.get(f.id);
|
||||
return featureUpdates ? { ...f, ...featureUpdates } : f;
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
return { previousFeatures };
|
||||
},
|
||||
onError: (error: Error, _, context) => {
|
||||
if (context?.previousFeatures) {
|
||||
queryClient.setQueryData(queryKeys.features.all(projectPath), context.previousFeatures);
|
||||
}
|
||||
toast.error('Failed to update features', {
|
||||
description: error.message,
|
||||
});
|
||||
},
|
||||
onSettled: () => {
|
||||
queryClient.invalidateQueries({
|
||||
queryKey: queryKeys.features.all(projectPath),
|
||||
});
|
||||
},
|
||||
});
|
||||
}
|
||||
159
apps/ui/src/hooks/mutations/use-github-mutations.ts
Normal file
159
apps/ui/src/hooks/mutations/use-github-mutations.ts
Normal file
@@ -0,0 +1,159 @@
|
||||
/**
|
||||
* GitHub Mutation Hooks
|
||||
*
|
||||
* React Query mutations for GitHub operations like validating issues.
|
||||
*/
|
||||
|
||||
import { useMutation, useQueryClient } from '@tanstack/react-query';
|
||||
import { getElectronAPI, GitHubIssue, GitHubComment } from '@/lib/electron';
|
||||
import { queryKeys } from '@/lib/query-keys';
|
||||
import { toast } from 'sonner';
|
||||
import type { LinkedPRInfo, ModelId } from '@automaker/types';
|
||||
|
||||
/**
|
||||
* Input for validating a GitHub issue
|
||||
*/
|
||||
interface ValidateIssueInput {
|
||||
issue: GitHubIssue;
|
||||
model?: ModelId;
|
||||
thinkingLevel?: number;
|
||||
reasoningEffort?: string;
|
||||
comments?: GitHubComment[];
|
||||
linkedPRs?: LinkedPRInfo[];
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate a GitHub issue with AI
|
||||
*
|
||||
* This mutation triggers an async validation process. Results are delivered
|
||||
* via WebSocket events (issue_validation_complete, issue_validation_error).
|
||||
*
|
||||
* @param projectPath - Path to the project
|
||||
* @returns Mutation for validating issues
|
||||
*
|
||||
* @example
|
||||
* ```tsx
|
||||
* const validateMutation = useValidateIssue(projectPath);
|
||||
*
|
||||
* validateMutation.mutate({
|
||||
* issue,
|
||||
* model: 'sonnet',
|
||||
* comments,
|
||||
* linkedPRs,
|
||||
* });
|
||||
* ```
|
||||
*/
|
||||
export function useValidateIssue(projectPath: string) {
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
return useMutation({
|
||||
mutationFn: async (input: ValidateIssueInput) => {
|
||||
const { issue, model, thinkingLevel, reasoningEffort, comments, linkedPRs } = input;
|
||||
|
||||
const api = getElectronAPI();
|
||||
if (!api.github?.validateIssue) {
|
||||
throw new Error('Validation API not available');
|
||||
}
|
||||
|
||||
const validationInput = {
|
||||
issueNumber: issue.number,
|
||||
issueTitle: issue.title,
|
||||
issueBody: issue.body || '',
|
||||
issueLabels: issue.labels.map((l) => l.name),
|
||||
comments,
|
||||
linkedPRs,
|
||||
};
|
||||
|
||||
const result = await api.github.validateIssue(
|
||||
projectPath,
|
||||
validationInput,
|
||||
model,
|
||||
thinkingLevel,
|
||||
reasoningEffort
|
||||
);
|
||||
|
||||
if (!result.success) {
|
||||
throw new Error(result.error || 'Failed to start validation');
|
||||
}
|
||||
|
||||
return { issueNumber: issue.number };
|
||||
},
|
||||
onSuccess: (_, variables) => {
|
||||
toast.info(`Starting validation for issue #${variables.issue.number}`, {
|
||||
description: 'You will be notified when the analysis is complete',
|
||||
});
|
||||
},
|
||||
onError: (error) => {
|
||||
toast.error('Failed to validate issue', {
|
||||
description: error instanceof Error ? error.message : 'Unknown error',
|
||||
});
|
||||
},
|
||||
// Note: We don't invalidate queries here because the actual result
|
||||
// comes through WebSocket events which handle cache invalidation
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Mark a validation as viewed
|
||||
*
|
||||
* @param projectPath - Path to the project
|
||||
* @returns Mutation for marking validation as viewed
|
||||
*
|
||||
* @example
|
||||
* ```tsx
|
||||
* const markViewedMutation = useMarkValidationViewed(projectPath);
|
||||
* markViewedMutation.mutate(issueNumber);
|
||||
* ```
|
||||
*/
|
||||
export function useMarkValidationViewed(projectPath: string) {
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
return useMutation({
|
||||
mutationFn: async (issueNumber: number) => {
|
||||
const api = getElectronAPI();
|
||||
if (!api.github?.markValidationViewed) {
|
||||
throw new Error('Mark viewed API not available');
|
||||
}
|
||||
|
||||
const result = await api.github.markValidationViewed(projectPath, issueNumber);
|
||||
|
||||
if (!result.success) {
|
||||
throw new Error(result.error || 'Failed to mark as viewed');
|
||||
}
|
||||
|
||||
return { issueNumber };
|
||||
},
|
||||
onSuccess: () => {
|
||||
// Invalidate validations cache to refresh the viewed state
|
||||
queryClient.invalidateQueries({
|
||||
queryKey: queryKeys.github.validations(projectPath),
|
||||
});
|
||||
},
|
||||
// Silent mutation - no toast needed for marking as viewed
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Get running validation status
|
||||
*
|
||||
* @param projectPath - Path to the project
|
||||
* @returns Mutation for getting validation status (returns running issue numbers)
|
||||
*/
|
||||
export function useGetValidationStatus(projectPath: string) {
|
||||
return useMutation({
|
||||
mutationFn: async () => {
|
||||
const api = getElectronAPI();
|
||||
if (!api.github?.getValidationStatus) {
|
||||
throw new Error('Validation status API not available');
|
||||
}
|
||||
|
||||
const result = await api.github.getValidationStatus(projectPath);
|
||||
|
||||
if (!result.success) {
|
||||
throw new Error(result.error || 'Failed to get validation status');
|
||||
}
|
||||
|
||||
return result.runningIssues ?? [];
|
||||
},
|
||||
});
|
||||
}
|
||||
82
apps/ui/src/hooks/mutations/use-ideation-mutations.ts
Normal file
82
apps/ui/src/hooks/mutations/use-ideation-mutations.ts
Normal file
@@ -0,0 +1,82 @@
|
||||
/**
|
||||
* Ideation Mutation Hooks
|
||||
*
|
||||
* React Query mutations for ideation operations like generating suggestions.
|
||||
*/
|
||||
|
||||
import { useMutation, useQueryClient } from '@tanstack/react-query';
|
||||
import { getElectronAPI } from '@/lib/electron';
|
||||
import { queryKeys } from '@/lib/query-keys';
|
||||
import { toast } from 'sonner';
|
||||
import type { IdeaCategory, IdeaSuggestion } from '@automaker/types';
|
||||
|
||||
/**
|
||||
* Input for generating ideation suggestions
|
||||
*/
|
||||
interface GenerateSuggestionsInput {
|
||||
promptId: string;
|
||||
category: IdeaCategory;
|
||||
}
|
||||
|
||||
/**
|
||||
* Result from generating suggestions
|
||||
*/
|
||||
interface GenerateSuggestionsResult {
|
||||
suggestions: IdeaSuggestion[];
|
||||
promptId: string;
|
||||
category: IdeaCategory;
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate ideation suggestions based on a prompt
|
||||
*
|
||||
* @param projectPath - Path to the project
|
||||
* @returns Mutation for generating suggestions
|
||||
*
|
||||
* @example
|
||||
* ```tsx
|
||||
* const generateMutation = useGenerateIdeationSuggestions(projectPath);
|
||||
*
|
||||
* generateMutation.mutate({
|
||||
* promptId: 'prompt-1',
|
||||
* category: 'ux',
|
||||
* }, {
|
||||
* onSuccess: (data) => {
|
||||
* console.log('Generated', data.suggestions.length, 'suggestions');
|
||||
* },
|
||||
* });
|
||||
* ```
|
||||
*/
|
||||
export function useGenerateIdeationSuggestions(projectPath: string) {
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
return useMutation({
|
||||
mutationFn: async (input: GenerateSuggestionsInput): Promise<GenerateSuggestionsResult> => {
|
||||
const { promptId, category } = input;
|
||||
|
||||
const api = getElectronAPI();
|
||||
if (!api.ideation?.generateSuggestions) {
|
||||
throw new Error('Ideation API not available');
|
||||
}
|
||||
|
||||
const result = await api.ideation.generateSuggestions(projectPath, promptId, category);
|
||||
|
||||
if (!result.success) {
|
||||
throw new Error(result.error || 'Failed to generate suggestions');
|
||||
}
|
||||
|
||||
return {
|
||||
suggestions: result.suggestions ?? [],
|
||||
promptId,
|
||||
category,
|
||||
};
|
||||
},
|
||||
onSuccess: () => {
|
||||
// Invalidate ideation ideas cache
|
||||
queryClient.invalidateQueries({
|
||||
queryKey: queryKeys.ideation.ideas(projectPath),
|
||||
});
|
||||
},
|
||||
// Toast notifications are handled by the component since it has access to prompt title
|
||||
});
|
||||
}
|
||||
144
apps/ui/src/hooks/mutations/use-settings-mutations.ts
Normal file
144
apps/ui/src/hooks/mutations/use-settings-mutations.ts
Normal file
@@ -0,0 +1,144 @@
|
||||
/**
|
||||
* Settings Mutations
|
||||
*
|
||||
* React Query mutations for updating global and project settings.
|
||||
*/
|
||||
|
||||
import { useMutation, useQueryClient } from '@tanstack/react-query';
|
||||
import { getElectronAPI } from '@/lib/electron';
|
||||
import { queryKeys } from '@/lib/query-keys';
|
||||
import { toast } from 'sonner';
|
||||
|
||||
interface UpdateGlobalSettingsOptions {
|
||||
/** Show success toast (default: true) */
|
||||
showSuccessToast?: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
* Update global settings
|
||||
*
|
||||
* @param options - Configuration options
|
||||
* @returns Mutation for updating global settings
|
||||
*
|
||||
* @example
|
||||
* ```tsx
|
||||
* const mutation = useUpdateGlobalSettings();
|
||||
* mutation.mutate({ enableSkills: true });
|
||||
*
|
||||
* // With custom success handling (no default toast)
|
||||
* const mutation = useUpdateGlobalSettings({ showSuccessToast: false });
|
||||
* mutation.mutate({ enableSkills: true }, {
|
||||
* onSuccess: () => toast.success('Skills enabled'),
|
||||
* });
|
||||
* ```
|
||||
*/
|
||||
export function useUpdateGlobalSettings(options: UpdateGlobalSettingsOptions = {}) {
|
||||
const { showSuccessToast = true } = options;
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
return useMutation({
|
||||
mutationFn: async (settings: Record<string, unknown>) => {
|
||||
const api = getElectronAPI();
|
||||
// Use updateGlobal for partial updates
|
||||
const result = await api.settings.updateGlobal(settings);
|
||||
if (!result.success) {
|
||||
throw new Error(result.error || 'Failed to update settings');
|
||||
}
|
||||
return result;
|
||||
},
|
||||
onSuccess: () => {
|
||||
queryClient.invalidateQueries({ queryKey: queryKeys.settings.global() });
|
||||
if (showSuccessToast) {
|
||||
toast.success('Settings saved');
|
||||
}
|
||||
},
|
||||
onError: (error: Error) => {
|
||||
toast.error('Failed to save settings', {
|
||||
description: error.message,
|
||||
});
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Update project settings
|
||||
*
|
||||
* @param projectPath - Optional path to the project (can also pass via mutation variables)
|
||||
* @returns Mutation for updating project settings
|
||||
*/
|
||||
export function useUpdateProjectSettings(projectPath?: string) {
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
return useMutation({
|
||||
mutationFn: async (
|
||||
variables:
|
||||
| Record<string, unknown>
|
||||
| { projectPath: string; settings: Record<string, unknown> }
|
||||
) => {
|
||||
// Support both call patterns:
|
||||
// 1. useUpdateProjectSettings(projectPath) then mutate(settings)
|
||||
// 2. useUpdateProjectSettings() then mutate({ projectPath, settings })
|
||||
let path: string;
|
||||
let settings: Record<string, unknown>;
|
||||
|
||||
if ('projectPath' in variables && 'settings' in variables) {
|
||||
path = variables.projectPath;
|
||||
settings = variables.settings;
|
||||
} else if (projectPath) {
|
||||
path = projectPath;
|
||||
settings = variables;
|
||||
} else {
|
||||
throw new Error('Project path is required');
|
||||
}
|
||||
|
||||
const api = getElectronAPI();
|
||||
const result = await api.settings.setProject(path, settings);
|
||||
if (!result.success) {
|
||||
throw new Error(result.error || 'Failed to update project settings');
|
||||
}
|
||||
return { ...result, projectPath: path };
|
||||
},
|
||||
onSuccess: (data) => {
|
||||
const path = data.projectPath || projectPath;
|
||||
if (path) {
|
||||
queryClient.invalidateQueries({ queryKey: queryKeys.settings.project(path) });
|
||||
}
|
||||
toast.success('Project settings saved');
|
||||
},
|
||||
onError: (error: Error) => {
|
||||
toast.error('Failed to save project settings', {
|
||||
description: error.message,
|
||||
});
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Save credentials (API keys)
|
||||
*
|
||||
* @returns Mutation for saving credentials
|
||||
*/
|
||||
export function useSaveCredentials() {
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
return useMutation({
|
||||
mutationFn: async (credentials: Record<string, string>) => {
|
||||
const api = getElectronAPI();
|
||||
const result = await api.settings.setCredentials(credentials);
|
||||
if (!result.success) {
|
||||
throw new Error(result.error || 'Failed to save credentials');
|
||||
}
|
||||
return result;
|
||||
},
|
||||
onSuccess: () => {
|
||||
queryClient.invalidateQueries({ queryKey: queryKeys.settings.credentials() });
|
||||
queryClient.invalidateQueries({ queryKey: queryKeys.cli.apiKeys() });
|
||||
toast.success('Credentials saved');
|
||||
},
|
||||
onError: (error: Error) => {
|
||||
toast.error('Failed to save credentials', {
|
||||
description: error.message,
|
||||
});
|
||||
},
|
||||
});
|
||||
}
|
||||
179
apps/ui/src/hooks/mutations/use-spec-mutations.ts
Normal file
179
apps/ui/src/hooks/mutations/use-spec-mutations.ts
Normal file
@@ -0,0 +1,179 @@
|
||||
/**
|
||||
* Spec Mutation Hooks
|
||||
*
|
||||
* React Query mutations for spec operations like creating, regenerating, and saving.
|
||||
*/
|
||||
|
||||
import { useMutation, useQueryClient } from '@tanstack/react-query';
|
||||
import { getElectronAPI } from '@/lib/electron';
|
||||
import { queryKeys } from '@/lib/query-keys';
|
||||
import { toast } from 'sonner';
|
||||
import type { FeatureCount } from '@/components/views/spec-view/types';
|
||||
|
||||
/**
|
||||
* Input for creating a spec
|
||||
*/
|
||||
interface CreateSpecInput {
|
||||
projectOverview: string;
|
||||
generateFeatures: boolean;
|
||||
analyzeProject: boolean;
|
||||
featureCount?: FeatureCount;
|
||||
}
|
||||
|
||||
/**
|
||||
* Input for regenerating a spec
|
||||
*/
|
||||
interface RegenerateSpecInput {
|
||||
projectDefinition: string;
|
||||
generateFeatures: boolean;
|
||||
analyzeProject: boolean;
|
||||
featureCount?: FeatureCount;
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a new spec for a project
|
||||
*
|
||||
* This mutation triggers an async spec creation process. Progress and completion
|
||||
* are delivered via WebSocket events (spec_regeneration_progress, spec_regeneration_complete).
|
||||
*
|
||||
* @param projectPath - Path to the project
|
||||
* @returns Mutation for creating specs
|
||||
*
|
||||
* @example
|
||||
* ```tsx
|
||||
* const createMutation = useCreateSpec(projectPath);
|
||||
*
|
||||
* createMutation.mutate({
|
||||
* projectOverview: 'A todo app with...',
|
||||
* generateFeatures: true,
|
||||
* analyzeProject: true,
|
||||
* featureCount: 50,
|
||||
* });
|
||||
* ```
|
||||
*/
|
||||
export function useCreateSpec(projectPath: string) {
|
||||
return useMutation({
|
||||
mutationFn: async (input: CreateSpecInput) => {
|
||||
const { projectOverview, generateFeatures, analyzeProject, featureCount } = input;
|
||||
|
||||
const api = getElectronAPI();
|
||||
if (!api.specRegeneration) {
|
||||
throw new Error('Spec regeneration API not available');
|
||||
}
|
||||
|
||||
const result = await api.specRegeneration.create(
|
||||
projectPath,
|
||||
projectOverview.trim(),
|
||||
generateFeatures,
|
||||
analyzeProject,
|
||||
generateFeatures ? featureCount : undefined
|
||||
);
|
||||
|
||||
if (!result.success) {
|
||||
throw new Error(result.error || 'Failed to start spec creation');
|
||||
}
|
||||
|
||||
return result;
|
||||
},
|
||||
// Toast/state updates are handled by the component since it tracks WebSocket events
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Regenerate an existing spec
|
||||
*
|
||||
* @param projectPath - Path to the project
|
||||
* @returns Mutation for regenerating specs
|
||||
*/
|
||||
export function useRegenerateSpec(projectPath: string) {
|
||||
return useMutation({
|
||||
mutationFn: async (input: RegenerateSpecInput) => {
|
||||
const { projectDefinition, generateFeatures, analyzeProject, featureCount } = input;
|
||||
|
||||
const api = getElectronAPI();
|
||||
if (!api.specRegeneration) {
|
||||
throw new Error('Spec regeneration API not available');
|
||||
}
|
||||
|
||||
const result = await api.specRegeneration.generate(
|
||||
projectPath,
|
||||
projectDefinition.trim(),
|
||||
generateFeatures,
|
||||
analyzeProject,
|
||||
generateFeatures ? featureCount : undefined
|
||||
);
|
||||
|
||||
if (!result.success) {
|
||||
throw new Error(result.error || 'Failed to start spec regeneration');
|
||||
}
|
||||
|
||||
return result;
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate features from existing spec
|
||||
*
|
||||
* @param projectPath - Path to the project
|
||||
* @returns Mutation for generating features
|
||||
*/
|
||||
export function useGenerateFeatures(projectPath: string) {
|
||||
return useMutation({
|
||||
mutationFn: async () => {
|
||||
const api = getElectronAPI();
|
||||
if (!api.specRegeneration) {
|
||||
throw new Error('Spec regeneration API not available');
|
||||
}
|
||||
|
||||
const result = await api.specRegeneration.generateFeatures(projectPath);
|
||||
|
||||
if (!result.success) {
|
||||
throw new Error(result.error || 'Failed to start feature generation');
|
||||
}
|
||||
|
||||
return result;
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Save spec file content
|
||||
*
|
||||
* @param projectPath - Path to the project
|
||||
* @returns Mutation for saving spec
|
||||
*
|
||||
* @example
|
||||
* ```tsx
|
||||
* const saveMutation = useSaveSpec(projectPath);
|
||||
*
|
||||
* saveMutation.mutate(specContent, {
|
||||
* onSuccess: () => setHasChanges(false),
|
||||
* });
|
||||
* ```
|
||||
*/
|
||||
export function useSaveSpec(projectPath: string) {
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
return useMutation({
|
||||
mutationFn: async (content: string) => {
|
||||
const api = getElectronAPI();
|
||||
|
||||
await api.writeFile(`${projectPath}/.automaker/app_spec.txt`, content);
|
||||
|
||||
return { content };
|
||||
},
|
||||
onSuccess: () => {
|
||||
// Invalidate spec file cache
|
||||
queryClient.invalidateQueries({
|
||||
queryKey: queryKeys.spec.file(projectPath),
|
||||
});
|
||||
toast.success('Spec saved');
|
||||
},
|
||||
onError: (error) => {
|
||||
toast.error('Failed to save spec', {
|
||||
description: error instanceof Error ? error.message : 'Unknown error',
|
||||
});
|
||||
},
|
||||
});
|
||||
}
|
||||
480
apps/ui/src/hooks/mutations/use-worktree-mutations.ts
Normal file
480
apps/ui/src/hooks/mutations/use-worktree-mutations.ts
Normal file
@@ -0,0 +1,480 @@
|
||||
/**
|
||||
* Worktree Mutations
|
||||
*
|
||||
* React Query mutations for worktree operations like creating, deleting,
|
||||
* committing, pushing, and creating pull requests.
|
||||
*/
|
||||
|
||||
import { useMutation, useQueryClient } from '@tanstack/react-query';
|
||||
import { getElectronAPI } from '@/lib/electron';
|
||||
import { queryKeys } from '@/lib/query-keys';
|
||||
import { toast } from 'sonner';
|
||||
|
||||
/**
|
||||
* Create a new worktree
|
||||
*
|
||||
* @param projectPath - Path to the project
|
||||
* @returns Mutation for creating a worktree
|
||||
*/
|
||||
export function useCreateWorktree(projectPath: string) {
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
return useMutation({
|
||||
mutationFn: async ({ branchName, baseBranch }: { branchName: string; baseBranch?: string }) => {
|
||||
const api = getElectronAPI();
|
||||
const result = await api.worktree.create(projectPath, branchName, baseBranch);
|
||||
if (!result.success) {
|
||||
throw new Error(result.error || 'Failed to create worktree');
|
||||
}
|
||||
return result.worktree;
|
||||
},
|
||||
onSuccess: () => {
|
||||
queryClient.invalidateQueries({ queryKey: queryKeys.worktrees.all(projectPath) });
|
||||
toast.success('Worktree created');
|
||||
},
|
||||
onError: (error: Error) => {
|
||||
toast.error('Failed to create worktree', {
|
||||
description: error.message,
|
||||
});
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete a worktree
|
||||
*
|
||||
* @param projectPath - Path to the project
|
||||
* @returns Mutation for deleting a worktree
|
||||
*/
|
||||
export function useDeleteWorktree(projectPath: string) {
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
return useMutation({
|
||||
mutationFn: async ({
|
||||
worktreePath,
|
||||
deleteBranch,
|
||||
}: {
|
||||
worktreePath: string;
|
||||
deleteBranch?: boolean;
|
||||
}) => {
|
||||
const api = getElectronAPI();
|
||||
const result = await api.worktree.delete(projectPath, worktreePath, deleteBranch);
|
||||
if (!result.success) {
|
||||
throw new Error(result.error || 'Failed to delete worktree');
|
||||
}
|
||||
return result.deleted;
|
||||
},
|
||||
onSuccess: () => {
|
||||
queryClient.invalidateQueries({ queryKey: queryKeys.worktrees.all(projectPath) });
|
||||
toast.success('Worktree deleted');
|
||||
},
|
||||
onError: (error: Error) => {
|
||||
toast.error('Failed to delete worktree', {
|
||||
description: error.message,
|
||||
});
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Commit changes in a worktree
|
||||
*
|
||||
* @returns Mutation for committing changes
|
||||
*/
|
||||
export function useCommitWorktree() {
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
return useMutation({
|
||||
mutationFn: async ({ worktreePath, message }: { worktreePath: string; message: string }) => {
|
||||
const api = getElectronAPI();
|
||||
const result = await api.worktree.commit(worktreePath, message);
|
||||
if (!result.success) {
|
||||
throw new Error(result.error || 'Failed to commit changes');
|
||||
}
|
||||
return result.result;
|
||||
},
|
||||
onSuccess: (_, { worktreePath }) => {
|
||||
// Invalidate all worktree queries since we don't know the project path
|
||||
queryClient.invalidateQueries({ queryKey: ['worktrees'] });
|
||||
toast.success('Changes committed');
|
||||
},
|
||||
onError: (error: Error) => {
|
||||
toast.error('Failed to commit changes', {
|
||||
description: error.message,
|
||||
});
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Push worktree branch to remote
|
||||
*
|
||||
* @returns Mutation for pushing changes
|
||||
*/
|
||||
export function usePushWorktree() {
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
return useMutation({
|
||||
mutationFn: async ({ worktreePath, force }: { worktreePath: string; force?: boolean }) => {
|
||||
const api = getElectronAPI();
|
||||
const result = await api.worktree.push(worktreePath, force);
|
||||
if (!result.success) {
|
||||
throw new Error(result.error || 'Failed to push changes');
|
||||
}
|
||||
return result.result;
|
||||
},
|
||||
onSuccess: () => {
|
||||
queryClient.invalidateQueries({ queryKey: ['worktrees'] });
|
||||
toast.success('Changes pushed to remote');
|
||||
},
|
||||
onError: (error: Error) => {
|
||||
toast.error('Failed to push changes', {
|
||||
description: error.message,
|
||||
});
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Pull changes from remote
|
||||
*
|
||||
* @returns Mutation for pulling changes
|
||||
*/
|
||||
export function usePullWorktree() {
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
return useMutation({
|
||||
mutationFn: async (worktreePath: string) => {
|
||||
const api = getElectronAPI();
|
||||
const result = await api.worktree.pull(worktreePath);
|
||||
if (!result.success) {
|
||||
throw new Error(result.error || 'Failed to pull changes');
|
||||
}
|
||||
return result.result;
|
||||
},
|
||||
onSuccess: () => {
|
||||
queryClient.invalidateQueries({ queryKey: ['worktrees'] });
|
||||
toast.success('Changes pulled from remote');
|
||||
},
|
||||
onError: (error: Error) => {
|
||||
toast.error('Failed to pull changes', {
|
||||
description: error.message,
|
||||
});
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a pull request from a worktree
|
||||
*
|
||||
* @returns Mutation for creating a PR
|
||||
*/
|
||||
export function useCreatePullRequest() {
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
return useMutation({
|
||||
mutationFn: async ({
|
||||
worktreePath,
|
||||
options,
|
||||
}: {
|
||||
worktreePath: string;
|
||||
options?: {
|
||||
projectPath?: string;
|
||||
commitMessage?: string;
|
||||
prTitle?: string;
|
||||
prBody?: string;
|
||||
baseBranch?: string;
|
||||
draft?: boolean;
|
||||
};
|
||||
}) => {
|
||||
const api = getElectronAPI();
|
||||
const result = await api.worktree.createPR(worktreePath, options);
|
||||
if (!result.success) {
|
||||
throw new Error(result.error || 'Failed to create pull request');
|
||||
}
|
||||
return result.result;
|
||||
},
|
||||
onSuccess: (result) => {
|
||||
queryClient.invalidateQueries({ queryKey: ['worktrees'] });
|
||||
queryClient.invalidateQueries({ queryKey: ['github', 'prs'] });
|
||||
if (result?.prUrl) {
|
||||
toast.success('Pull request created', {
|
||||
description: `PR #${result.prNumber} created`,
|
||||
action: {
|
||||
label: 'Open',
|
||||
onClick: () => {
|
||||
const api = getElectronAPI();
|
||||
api.openExternalLink(result.prUrl!);
|
||||
},
|
||||
},
|
||||
});
|
||||
} else if (result?.prAlreadyExisted) {
|
||||
toast.info('Pull request already exists');
|
||||
}
|
||||
},
|
||||
onError: (error: Error) => {
|
||||
toast.error('Failed to create pull request', {
|
||||
description: error.message,
|
||||
});
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Merge a worktree branch into main
|
||||
*
|
||||
* @param projectPath - Path to the project
|
||||
* @returns Mutation for merging a feature
|
||||
*/
|
||||
export function useMergeWorktree(projectPath: string) {
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
return useMutation({
|
||||
mutationFn: async ({
|
||||
branchName,
|
||||
worktreePath,
|
||||
options,
|
||||
}: {
|
||||
branchName: string;
|
||||
worktreePath: string;
|
||||
options?: {
|
||||
squash?: boolean;
|
||||
message?: string;
|
||||
};
|
||||
}) => {
|
||||
const api = getElectronAPI();
|
||||
const result = await api.worktree.mergeFeature(
|
||||
projectPath,
|
||||
branchName,
|
||||
worktreePath,
|
||||
options
|
||||
);
|
||||
if (!result.success) {
|
||||
throw new Error(result.error || 'Failed to merge feature');
|
||||
}
|
||||
return result;
|
||||
},
|
||||
onSuccess: () => {
|
||||
queryClient.invalidateQueries({ queryKey: queryKeys.worktrees.all(projectPath) });
|
||||
queryClient.invalidateQueries({ queryKey: queryKeys.features.all(projectPath) });
|
||||
toast.success('Feature merged successfully');
|
||||
},
|
||||
onError: (error: Error) => {
|
||||
toast.error('Failed to merge feature', {
|
||||
description: error.message,
|
||||
});
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Switch to a different branch
|
||||
*
|
||||
* @returns Mutation for switching branches
|
||||
*/
|
||||
export function useSwitchBranch() {
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
return useMutation({
|
||||
mutationFn: async ({
|
||||
worktreePath,
|
||||
branchName,
|
||||
}: {
|
||||
worktreePath: string;
|
||||
branchName: string;
|
||||
}) => {
|
||||
const api = getElectronAPI();
|
||||
const result = await api.worktree.switchBranch(worktreePath, branchName);
|
||||
if (!result.success) {
|
||||
throw new Error(result.error || 'Failed to switch branch');
|
||||
}
|
||||
return result.result;
|
||||
},
|
||||
onSuccess: () => {
|
||||
queryClient.invalidateQueries({ queryKey: ['worktrees'] });
|
||||
toast.success('Switched branch');
|
||||
},
|
||||
onError: (error: Error) => {
|
||||
toast.error('Failed to switch branch', {
|
||||
description: error.message,
|
||||
});
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Checkout a new branch
|
||||
*
|
||||
* @returns Mutation for creating and checking out a new branch
|
||||
*/
|
||||
export function useCheckoutBranch() {
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
return useMutation({
|
||||
mutationFn: async ({
|
||||
worktreePath,
|
||||
branchName,
|
||||
}: {
|
||||
worktreePath: string;
|
||||
branchName: string;
|
||||
}) => {
|
||||
const api = getElectronAPI();
|
||||
const result = await api.worktree.checkoutBranch(worktreePath, branchName);
|
||||
if (!result.success) {
|
||||
throw new Error(result.error || 'Failed to checkout branch');
|
||||
}
|
||||
return result.result;
|
||||
},
|
||||
onSuccess: () => {
|
||||
queryClient.invalidateQueries({ queryKey: ['worktrees'] });
|
||||
toast.success('New branch created and checked out');
|
||||
},
|
||||
onError: (error: Error) => {
|
||||
toast.error('Failed to checkout branch', {
|
||||
description: error.message,
|
||||
});
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate a commit message from git diff
|
||||
*
|
||||
* @returns Mutation for generating a commit message
|
||||
*/
|
||||
export function useGenerateCommitMessage() {
|
||||
return useMutation({
|
||||
mutationFn: async (worktreePath: string) => {
|
||||
const api = getElectronAPI();
|
||||
const result = await api.worktree.generateCommitMessage(worktreePath);
|
||||
if (!result.success) {
|
||||
throw new Error(result.error || 'Failed to generate commit message');
|
||||
}
|
||||
return result.message ?? '';
|
||||
},
|
||||
onError: (error: Error) => {
|
||||
toast.error('Failed to generate commit message', {
|
||||
description: error.message,
|
||||
});
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Open worktree in editor
|
||||
*
|
||||
* @returns Mutation for opening in editor
|
||||
*/
|
||||
export function useOpenInEditor() {
|
||||
return useMutation({
|
||||
mutationFn: async ({
|
||||
worktreePath,
|
||||
editorCommand,
|
||||
}: {
|
||||
worktreePath: string;
|
||||
editorCommand?: string;
|
||||
}) => {
|
||||
const api = getElectronAPI();
|
||||
const result = await api.worktree.openInEditor(worktreePath, editorCommand);
|
||||
if (!result.success) {
|
||||
throw new Error(result.error || 'Failed to open in editor');
|
||||
}
|
||||
return result.result;
|
||||
},
|
||||
onError: (error: Error) => {
|
||||
toast.error('Failed to open in editor', {
|
||||
description: error.message,
|
||||
});
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Initialize git in a project
|
||||
*
|
||||
* @returns Mutation for initializing git
|
||||
*/
|
||||
export function useInitGit() {
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
return useMutation({
|
||||
mutationFn: async (projectPath: string) => {
|
||||
const api = getElectronAPI();
|
||||
const result = await api.worktree.initGit(projectPath);
|
||||
if (!result.success) {
|
||||
throw new Error(result.error || 'Failed to initialize git');
|
||||
}
|
||||
return result.result;
|
||||
},
|
||||
onSuccess: () => {
|
||||
queryClient.invalidateQueries({ queryKey: ['worktrees'] });
|
||||
queryClient.invalidateQueries({ queryKey: ['github'] });
|
||||
toast.success('Git repository initialized');
|
||||
},
|
||||
onError: (error: Error) => {
|
||||
toast.error('Failed to initialize git', {
|
||||
description: error.message,
|
||||
});
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Set init script for a project
|
||||
*
|
||||
* @param projectPath - Path to the project
|
||||
* @returns Mutation for setting init script
|
||||
*/
|
||||
export function useSetInitScript(projectPath: string) {
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
return useMutation({
|
||||
mutationFn: async (content: string) => {
|
||||
const api = getElectronAPI();
|
||||
const result = await api.worktree.setInitScript(projectPath, content);
|
||||
if (!result.success) {
|
||||
throw new Error(result.error || 'Failed to save init script');
|
||||
}
|
||||
return result;
|
||||
},
|
||||
onSuccess: () => {
|
||||
queryClient.invalidateQueries({ queryKey: queryKeys.worktrees.initScript(projectPath) });
|
||||
toast.success('Init script saved');
|
||||
},
|
||||
onError: (error: Error) => {
|
||||
toast.error('Failed to save init script', {
|
||||
description: error.message,
|
||||
});
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete init script for a project
|
||||
*
|
||||
* @param projectPath - Path to the project
|
||||
* @returns Mutation for deleting init script
|
||||
*/
|
||||
export function useDeleteInitScript(projectPath: string) {
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
return useMutation({
|
||||
mutationFn: async () => {
|
||||
const api = getElectronAPI();
|
||||
const result = await api.worktree.deleteInitScript(projectPath);
|
||||
if (!result.success) {
|
||||
throw new Error(result.error || 'Failed to delete init script');
|
||||
}
|
||||
return result;
|
||||
},
|
||||
onSuccess: () => {
|
||||
queryClient.invalidateQueries({ queryKey: queryKeys.worktrees.initScript(projectPath) });
|
||||
toast.success('Init script deleted');
|
||||
},
|
||||
onError: (error: Error) => {
|
||||
toast.error('Failed to delete init script', {
|
||||
description: error.message,
|
||||
});
|
||||
},
|
||||
});
|
||||
}
|
||||
Reference in New Issue
Block a user