From 845674128e148531cef1f63d79d7e0783d8db3ba Mon Sep 17 00:00:00 2001 From: Shirone Date: Thu, 15 Jan 2026 16:20:38 +0100 Subject: [PATCH] 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 --- apps/ui/src/hooks/mutations/index.ts | 79 +++ .../mutations/use-auto-mode-mutations.ts | 373 ++++++++++++++ .../use-cursor-permissions-mutations.ts | 96 ++++ .../hooks/mutations/use-feature-mutations.ts | 267 ++++++++++ .../hooks/mutations/use-github-mutations.ts | 159 ++++++ .../hooks/mutations/use-ideation-mutations.ts | 82 +++ .../hooks/mutations/use-settings-mutations.ts | 144 ++++++ .../src/hooks/mutations/use-spec-mutations.ts | 179 +++++++ .../hooks/mutations/use-worktree-mutations.ts | 480 ++++++++++++++++++ 9 files changed, 1859 insertions(+) create mode 100644 apps/ui/src/hooks/mutations/index.ts create mode 100644 apps/ui/src/hooks/mutations/use-auto-mode-mutations.ts create mode 100644 apps/ui/src/hooks/mutations/use-cursor-permissions-mutations.ts create mode 100644 apps/ui/src/hooks/mutations/use-feature-mutations.ts create mode 100644 apps/ui/src/hooks/mutations/use-github-mutations.ts create mode 100644 apps/ui/src/hooks/mutations/use-ideation-mutations.ts create mode 100644 apps/ui/src/hooks/mutations/use-settings-mutations.ts create mode 100644 apps/ui/src/hooks/mutations/use-spec-mutations.ts create mode 100644 apps/ui/src/hooks/mutations/use-worktree-mutations.ts diff --git a/apps/ui/src/hooks/mutations/index.ts b/apps/ui/src/hooks/mutations/index.ts new file mode 100644 index 00000000..9cab4bea --- /dev/null +++ b/apps/ui/src/hooks/mutations/index.ts @@ -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'; diff --git a/apps/ui/src/hooks/mutations/use-auto-mode-mutations.ts b/apps/ui/src/hooks/mutations/use-auto-mode-mutations.ts new file mode 100644 index 00000000..b94d23ad --- /dev/null +++ b/apps/ui/src/hooks/mutations/use-auto-mode-mutations.ts @@ -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, + }); + }, + }); +} diff --git a/apps/ui/src/hooks/mutations/use-cursor-permissions-mutations.ts b/apps/ui/src/hooks/mutations/use-cursor-permissions-mutations.ts new file mode 100644 index 00000000..3b813d2e --- /dev/null +++ b/apps/ui/src/hooks/mutations/use-cursor-permissions-mutations.ts @@ -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', + }); + }, + }); +} diff --git a/apps/ui/src/hooks/mutations/use-feature-mutations.ts b/apps/ui/src/hooks/mutations/use-feature-mutations.ts new file mode 100644 index 00000000..0b8c4e84 --- /dev/null +++ b/apps/ui/src/hooks/mutations/use-feature-mutations.ts @@ -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; + 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( + queryKeys.features.all(projectPath) + ); + + // Optimistically update the cache + if (previousFeatures) { + queryClient.setQueryData( + 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( + queryKeys.features.all(projectPath) + ); + + if (previousFeatures) { + queryClient.setQueryData( + 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 }>) => { + 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( + queryKeys.features.all(projectPath) + ); + + if (previousFeatures) { + const updatesMap = new Map(updates.map((u) => [u.featureId, u.updates])); + queryClient.setQueryData( + 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), + }); + }, + }); +} diff --git a/apps/ui/src/hooks/mutations/use-github-mutations.ts b/apps/ui/src/hooks/mutations/use-github-mutations.ts new file mode 100644 index 00000000..4f4336ba --- /dev/null +++ b/apps/ui/src/hooks/mutations/use-github-mutations.ts @@ -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 ?? []; + }, + }); +} diff --git a/apps/ui/src/hooks/mutations/use-ideation-mutations.ts b/apps/ui/src/hooks/mutations/use-ideation-mutations.ts new file mode 100644 index 00000000..61841d9e --- /dev/null +++ b/apps/ui/src/hooks/mutations/use-ideation-mutations.ts @@ -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 => { + 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 + }); +} diff --git a/apps/ui/src/hooks/mutations/use-settings-mutations.ts b/apps/ui/src/hooks/mutations/use-settings-mutations.ts new file mode 100644 index 00000000..aa1862ed --- /dev/null +++ b/apps/ui/src/hooks/mutations/use-settings-mutations.ts @@ -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) => { + 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 + | { projectPath: string; settings: Record } + ) => { + // Support both call patterns: + // 1. useUpdateProjectSettings(projectPath) then mutate(settings) + // 2. useUpdateProjectSettings() then mutate({ projectPath, settings }) + let path: string; + let settings: Record; + + 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) => { + 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, + }); + }, + }); +} diff --git a/apps/ui/src/hooks/mutations/use-spec-mutations.ts b/apps/ui/src/hooks/mutations/use-spec-mutations.ts new file mode 100644 index 00000000..98279d65 --- /dev/null +++ b/apps/ui/src/hooks/mutations/use-spec-mutations.ts @@ -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', + }); + }, + }); +} diff --git a/apps/ui/src/hooks/mutations/use-worktree-mutations.ts b/apps/ui/src/hooks/mutations/use-worktree-mutations.ts new file mode 100644 index 00000000..ec8dd6e0 --- /dev/null +++ b/apps/ui/src/hooks/mutations/use-worktree-mutations.ts @@ -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, + }); + }, + }); +}