mirror of
https://github.com/AutoMaker-Org/automaker.git
synced 2026-02-04 21:23:07 +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