Feature: Add PR review comments and resolution, improve AI prompt handling (#790)

* 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
This commit is contained in:
gsxdsm
2026-02-20 21:34:40 -08:00
committed by GitHub
parent 0e020f7e4a
commit c81ea768a7
60 changed files with 4568 additions and 681 deletions

View File

@@ -62,6 +62,7 @@ export {
useValidateIssue,
useMarkValidationViewed,
useGetValidationStatus,
useResolveReviewThread,
} from './use-github-mutations';
// Ideation mutations

View File

@@ -135,6 +135,55 @@ export function useMarkValidationViewed(projectPath: string) {
});
}
/**
* 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
*

View File

@@ -20,6 +20,7 @@ export {
useGitHubValidations,
useGitHubRemote,
useGitHubIssueComments,
useGitHubPRReviewComments,
} from './use-github';
// Usage

View File

@@ -6,8 +6,8 @@
* automatic caching, deduplication, and background refetching.
*/
import { useMemo } from 'react';
import { useQuery } from '@tanstack/react-query';
import { useMemo, useEffect, useRef } from 'react';
import { useQuery, useQueryClient } from '@tanstack/react-query';
import { getElectronAPI } from '@/lib/electron';
import { queryKeys } from '@/lib/query-keys';
import { STALE_TIMES } from '@/lib/query-client';
@@ -151,6 +151,34 @@ export function useFeatures(projectPath: string | undefined) {
[projectPath]
);
const queryClient = useQueryClient();
// Subscribe to React Query cache changes for features and sync to localStorage.
// This ensures optimistic updates (e.g., status changes to 'verified') are
// persisted to localStorage immediately, not just when queryFn runs.
// Without this, a page refresh after an optimistic update could show stale
// localStorage data where features appear in the wrong column (e.g., verified
// features showing up in backlog).
const projectPathRef = useRef(projectPath);
projectPathRef.current = projectPath;
useEffect(() => {
if (!projectPath) return;
const targetQueryHash = JSON.stringify(queryKeys.features.all(projectPath));
const unsubscribe = queryClient.getQueryCache().subscribe((event) => {
if (
event.type === 'updated' &&
event.action.type === 'success' &&
event.query.queryHash === targetQueryHash
) {
const features = event.query.state.data as Feature[] | undefined;
if (features && projectPathRef.current) {
writePersistedFeatures(projectPathRef.current, features);
}
}
});
return unsubscribe;
}, [projectPath, queryClient]);
return useQuery({
queryKey: queryKeys.features.all(projectPath ?? ''),
queryFn: async (): Promise<Feature[]> => {
@@ -166,7 +194,11 @@ export function useFeatures(projectPath: string | undefined) {
},
enabled: !!projectPath,
initialData: () => persisted?.features,
initialDataUpdatedAt: () => persisted?.timestamp,
// Always treat localStorage cache as stale so React Query immediately
// fetches fresh data from the server on page load. This prevents stale
// feature statuses (e.g., 'verified' features appearing in backlog)
// while still showing cached data instantly for a fast initial render.
initialDataUpdatedAt: 0,
staleTime: STALE_TIMES.FEATURES,
refetchInterval: createSmartPollingInterval(FEATURES_POLLING_INTERVAL),
refetchOnWindowFocus: FEATURES_REFETCH_ON_FOCUS,

View File

@@ -8,7 +8,13 @@ import { useQuery, useInfiniteQuery } from '@tanstack/react-query';
import { getElectronAPI } from '@/lib/electron';
import { queryKeys } from '@/lib/query-keys';
import { STALE_TIMES } from '@/lib/query-client';
import type { GitHubIssue, GitHubPR, GitHubComment, StoredValidation } from '@/lib/electron';
import type {
GitHubIssue,
GitHubPR,
GitHubComment,
PRReviewComment,
StoredValidation,
} from '@/lib/electron';
interface GitHubIssuesResult {
openIssues: GitHubIssue[];
@@ -197,3 +203,45 @@ export function useGitHubIssueComments(
staleTime: STALE_TIMES.GITHUB,
});
}
/**
* Fetch review comments for a GitHub PR
*
* Fetches both regular PR comments and inline code review comments
* with file path and line context for each.
*
* @param projectPath - Path to the project
* @param prNumber - PR number
* @returns Query result with review comments
*
* @example
* ```tsx
* const { data, isLoading } = useGitHubPRReviewComments(projectPath, prNumber);
* const comments = data?.comments ?? [];
* ```
*/
export function useGitHubPRReviewComments(
projectPath: string | undefined,
prNumber: number | undefined
) {
return useQuery({
queryKey: queryKeys.github.prReviewComments(projectPath ?? '', prNumber ?? 0),
queryFn: async (): Promise<{ comments: PRReviewComment[]; totalCount: number }> => {
if (!projectPath || !prNumber) throw new Error('Missing project path or PR number');
const api = getElectronAPI();
if (!api.github) {
throw new Error('GitHub API not available');
}
const result = await api.github.getPRReviewComments(projectPath, prNumber);
if (!result.success) {
throw new Error(result.error || 'Failed to fetch PR review comments');
}
return {
comments: (result.comments ?? []) as PRReviewComment[],
totalCount: result.totalCount ?? 0,
};
},
enabled: !!projectPath && !!prNumber,
staleTime: STALE_TIMES.GITHUB,
});
}

View File

@@ -100,6 +100,7 @@ const SETTINGS_FIELDS_TO_SYNC = [
'projectHistory',
'projectHistoryIndex',
'lastSelectedSessionByProject',
'currentWorktreeByProject',
// Codex CLI Settings
'codexAutoLoadAgents',
'codexSandboxMode',
@@ -768,6 +769,8 @@ export async function refreshSettingsFromServer(): Promise<boolean> {
projectHistory: serverSettings.projectHistory,
projectHistoryIndex: serverSettings.projectHistoryIndex,
lastSelectedSessionByProject: serverSettings.lastSelectedSessionByProject,
currentWorktreeByProject:
serverSettings.currentWorktreeByProject ?? currentAppState.currentWorktreeByProject,
// UI State (previously in localStorage)
worktreePanelCollapsed: serverSettings.worktreePanelCollapsed ?? false,
lastProjectDir: serverSettings.lastProjectDir ?? '',