mirror of
https://github.com/AutoMaker-Org/automaker.git
synced 2026-03-24 00:13:07 +00:00
* feat: Add PR review comments and resolution endpoints, improve prompt handling * Feature: File Editor (#789) * feat: Add file management feature * feat: Add auto-save functionality to file editor * fix: Replace HardDriveDownload icon with Save icon for consistency * fix: Prevent recursive copy/move and improve shell injection prevention * refactor: Extract editor settings form into separate component * ``` fix: Improve error handling and stabilize async operations - Add error event handlers to GraphQL process spawns to prevent unhandled rejections - Replace execAsync with execFile for safer command execution and better control - Fix timeout cleanup in withTimeout generator to prevent memory leaks - Improve outdated comment detection logic by removing redundant condition - Use resolveModelString for consistent model string handling - Replace || with ?? for proper falsy value handling in dialog initialization - Add comments clarifying branch name resolution logic for local branches with slashes - Add catch handler for project selection to handle async errors gracefully ``` * refactor: Extract PR review comments logic to dedicated service * fix: Improve robustness and UX for PR review and file operations * fix: Consolidate exec utilities and improve type safety * refactor: Replace ScrollArea with div and improve file tree layout
211 lines
6.2 KiB
TypeScript
211 lines
6.2 KiB
TypeScript
/**
|
|
* 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, ThinkingLevel, ReasoningEffort } from '@automaker/types';
|
|
import { resolveModelString } from '@automaker/model-resolver';
|
|
|
|
/**
|
|
* Input for validating a GitHub issue
|
|
*/
|
|
interface ValidateIssueInput {
|
|
issue: GitHubIssue;
|
|
model?: ModelId;
|
|
thinkingLevel?: ThinkingLevel;
|
|
reasoningEffort?: ReasoningEffort;
|
|
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) {
|
|
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,
|
|
};
|
|
|
|
// Resolve model alias to canonical model identifier
|
|
const resolvedModel = model ? resolveModelString(model) : undefined;
|
|
|
|
const result = await api.github.validateIssue(
|
|
projectPath,
|
|
validationInput,
|
|
resolvedModel,
|
|
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
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Resolve or unresolve a PR review thread
|
|
*
|
|
* @param projectPath - Path to the project
|
|
* @param prNumber - PR number (for cache invalidation)
|
|
* @returns Mutation for resolving/unresolving a review thread
|
|
*
|
|
* @example
|
|
* ```tsx
|
|
* const resolveThread = useResolveReviewThread(projectPath, prNumber);
|
|
* resolveThread.mutate({ threadId: comment.threadId, resolve: true });
|
|
* ```
|
|
*/
|
|
export function useResolveReviewThread(projectPath: string, prNumber: number) {
|
|
const queryClient = useQueryClient();
|
|
|
|
return useMutation({
|
|
mutationFn: async ({ threadId, resolve }: { threadId: string; resolve: boolean }) => {
|
|
const api = getElectronAPI();
|
|
if (!api.github?.resolveReviewThread) {
|
|
throw new Error('Resolve review thread API not available');
|
|
}
|
|
|
|
const result = await api.github.resolveReviewThread(projectPath, threadId, resolve);
|
|
|
|
if (!result.success) {
|
|
throw new Error(result.error || 'Failed to resolve review thread');
|
|
}
|
|
|
|
return { isResolved: result.isResolved ?? resolve };
|
|
},
|
|
onSuccess: (_, variables) => {
|
|
const action = variables.resolve ? 'resolved' : 'unresolved';
|
|
toast.success(`Comment ${action}`, {
|
|
description: `The review thread has been ${action} on GitHub`,
|
|
});
|
|
// Invalidate the PR review comments cache to reflect updated resolved status
|
|
queryClient.invalidateQueries({
|
|
queryKey: queryKeys.github.prReviewComments(projectPath, prNumber),
|
|
});
|
|
},
|
|
onError: (error) => {
|
|
toast.error('Failed to update comment', {
|
|
description: error instanceof Error ? error.message : 'Unknown error',
|
|
});
|
|
},
|
|
});
|
|
}
|
|
|
|
/**
|
|
* 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 ?? [];
|
|
},
|
|
});
|
|
}
|