refactor: Improve all git operations, add stash support, add improved pull request flow, add worktree file copy options, address code review comments, add cherry pick options

This commit is contained in:
gsxdsm
2026-02-17 22:02:58 -08:00
parent f4e87d4c25
commit 9af63bc1ef
89 changed files with 6811 additions and 351 deletions

View File

@@ -9,6 +9,7 @@ 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';
/**
* Start running a feature in auto mode
@@ -159,9 +160,26 @@ export function useVerifyFeature(projectPath: string) {
if (!result.success) {
throw new Error(result.error || 'Failed to verify feature');
}
return result;
return { ...result, featureId };
},
onSuccess: () => {
onSuccess: (data) => {
// If verification passed, optimistically update React Query cache
// to move the feature to 'verified' status immediately
if (data.passes) {
const previousFeatures = queryClient.getQueryData<Feature[]>(
queryKeys.features.all(projectPath)
);
if (previousFeatures) {
queryClient.setQueryData<Feature[]>(
queryKeys.features.all(projectPath),
previousFeatures.map((f) =>
f.id === data.featureId
? { ...f, status: 'verified' as const, justFinishedAt: undefined }
: f
)
);
}
}
queryClient.invalidateQueries({ queryKey: queryKeys.features.all(projectPath) });
},
onError: (error: Error) => {

View File

@@ -126,10 +126,18 @@ export function usePushWorktree() {
const queryClient = useQueryClient();
return useMutation({
mutationFn: async ({ worktreePath, force }: { worktreePath: string; force?: boolean }) => {
mutationFn: async ({
worktreePath,
force,
remote,
}: {
worktreePath: string;
force?: boolean;
remote?: string;
}) => {
const api = getElectronAPI();
if (!api.worktree) throw new Error('Worktree API not available');
const result = await api.worktree.push(worktreePath, force);
const result = await api.worktree.push(worktreePath, force, remote);
if (!result.success) {
throw new Error(result.error || 'Failed to push changes');
}
@@ -156,10 +164,10 @@ export function usePullWorktree() {
const queryClient = useQueryClient();
return useMutation({
mutationFn: async (worktreePath: string) => {
mutationFn: async ({ worktreePath, remote }: { worktreePath: string; remote?: string }) => {
const api = getElectronAPI();
if (!api.worktree) throw new Error('Worktree API not available');
const result = await api.worktree.pull(worktreePath);
const result = await api.worktree.pull(worktreePath, remote);
if (!result.success) {
throw new Error(result.error || 'Failed to pull changes');
}
@@ -283,17 +291,6 @@ export function useMergeWorktree(projectPath: string) {
});
}
/**
* Result from the switch branch API call
*/
interface SwitchBranchResult {
previousBranch: string;
currentBranch: string;
message: string;
hasConflicts?: boolean;
stashedChanges?: boolean;
}
/**
* Switch to a different branch
*
@@ -316,14 +313,17 @@ export function useSwitchBranch(options?: {
}: {
worktreePath: string;
branchName: string;
}): Promise<SwitchBranchResult> => {
}) => {
const api = getElectronAPI();
if (!api.worktree) throw new Error('Worktree API not available');
const result = await api.worktree.switchBranch(worktreePath, branchName);
if (!result.success) {
throw new Error(result.error || 'Failed to switch branch');
}
return result.result as SwitchBranchResult;
if (!result.result) {
throw new Error('Switch branch returned no result');
}
return result.result;
},
onSuccess: (data, variables) => {
queryClient.invalidateQueries({ queryKey: ['worktrees'] });
@@ -388,6 +388,36 @@ export function useCheckoutBranch() {
});
}
/**
* Generate a PR title and description from branch diff
*
* @returns Mutation for generating a PR description
*/
export function useGeneratePRDescription() {
return useMutation({
mutationFn: async ({
worktreePath,
baseBranch,
}: {
worktreePath: string;
baseBranch?: string;
}) => {
const api = getElectronAPI();
if (!api.worktree) throw new Error('Worktree API not available');
const result = await api.worktree.generatePRDescription(worktreePath, baseBranch);
if (!result.success) {
throw new Error(result.error || 'Failed to generate PR description');
}
return { title: result.title ?? '', body: result.body ?? '' };
},
onError: (error: Error) => {
toast.error('Failed to generate PR description', {
description: error.message,
});
},
});
}
/**
* Generate a commit message from git diff
*

View File

@@ -144,8 +144,10 @@ export function useGeminiUsage(enabled = true) {
throw new Error('Gemini API bridge unavailable');
}
const result = await api.gemini.getUsage();
// Server always returns a response with 'authenticated' field, even on error
// So we can safely cast to GeminiUsage
// Check if result is an error-only response (no 'authenticated' field means it's the error variant)
if (!('authenticated' in result) && 'error' in result) {
throw new Error(result.message || result.error);
}
return result as GeminiUsage;
},
enabled,

View File

@@ -86,6 +86,7 @@ export function useAutoMode(worktree?: WorktreeInfo) {
getMaxConcurrencyForWorktree,
setMaxConcurrencyForWorktree,
isPrimaryWorktreeBranch,
globalMaxConcurrency,
} = useAppStore(
useShallow((state) => ({
autoModeByWorktree: state.autoModeByWorktree,
@@ -100,6 +101,7 @@ export function useAutoMode(worktree?: WorktreeInfo) {
getMaxConcurrencyForWorktree: state.getMaxConcurrencyForWorktree,
setMaxConcurrencyForWorktree: state.setMaxConcurrencyForWorktree,
isPrimaryWorktreeBranch: state.isPrimaryWorktreeBranch,
globalMaxConcurrency: state.maxConcurrency,
}))
);
@@ -143,11 +145,13 @@ export function useAutoMode(worktree?: WorktreeInfo) {
const isAutoModeRunning = worktreeAutoModeState.isRunning;
const runningAutoTasks = worktreeAutoModeState.runningTasks;
// Use getMaxConcurrencyForWorktree which properly falls back to the global
// maxConcurrency setting, instead of DEFAULT_MAX_CONCURRENCY (1) which would
// incorrectly block agents when the user has set a higher global limit
// Use the subscribed worktreeAutoModeState.maxConcurrency (from the reactive
// autoModeByWorktree store slice) so canStartNewTask stays reactive when
// refreshStatus updates worktree state or when the global setting changes.
// Falls back to the subscribed globalMaxConcurrency (also reactive) when no
// per-worktree value is set, and to DEFAULT_MAX_CONCURRENCY when no project.
const maxConcurrency = projectId
? getMaxConcurrencyForWorktree(projectId, branchName)
? (worktreeAutoModeState.maxConcurrency ?? globalMaxConcurrency)
: DEFAULT_MAX_CONCURRENCY;
// Check if we can start a new task based on concurrency limit

View File

@@ -25,6 +25,7 @@ export function useProjectSettingsLoader() {
const setAutoDismissInitScriptIndicator = useAppStore(
(state) => state.setAutoDismissInitScriptIndicator
);
const setWorktreeCopyFiles = useAppStore((state) => state.setWorktreeCopyFiles);
const setCurrentProject = useAppStore((state) => state.setCurrentProject);
const appliedProjectRef = useRef<{ path: string; dataUpdatedAt: number } | null>(null);
@@ -95,6 +96,11 @@ export function useProjectSettingsLoader() {
setAutoDismissInitScriptIndicator(projectPath, settings.autoDismissInitScriptIndicator);
}
// Apply worktreeCopyFiles if present
if (settings.worktreeCopyFiles !== undefined) {
setWorktreeCopyFiles(projectPath, settings.worktreeCopyFiles);
}
// Apply activeClaudeApiProfileId and phaseModelOverrides if present
// These are stored directly on the project, so we need to update both
// currentProject AND the projects array to keep them in sync
@@ -152,6 +158,7 @@ export function useProjectSettingsLoader() {
setShowInitScriptIndicator,
setDefaultDeleteBranch,
setAutoDismissInitScriptIndicator,
setWorktreeCopyFiles,
setCurrentProject,
]);
}

View File

@@ -4,8 +4,8 @@ import {
type ClaudeAuthMethod,
type CodexAuthMethod,
type ZaiAuthMethod,
type GeminiAuthMethod,
} from '@/store/setup-store';
import type { GeminiAuthStatus } from '@automaker/types';
import { getHttpApiClient } from '@/lib/http-api-client';
import { createLogger } from '@automaker/utils/logger';
@@ -159,11 +159,16 @@ export function useProviderAuthInit() {
// Set Auth status - always set a status to mark initialization as complete
if (result.auth) {
const auth = result.auth;
const validMethods: GeminiAuthMethod[] = ['cli_login', 'api_key_env', 'api_key', 'none'];
const validMethods: GeminiAuthStatus['method'][] = [
'google_login',
'api_key',
'vertex_ai',
'none',
];
const method = validMethods.includes(auth.method as GeminiAuthMethod)
? (auth.method as GeminiAuthMethod)
: ((auth.authenticated ? 'cli_login' : 'none') as GeminiAuthMethod);
const method = validMethods.includes(auth.method as GeminiAuthStatus['method'])
? (auth.method as GeminiAuthStatus['method'])
: ((auth.authenticated ? 'google_login' : 'none') as GeminiAuthStatus['method']);
setGeminiAuthStatus({
authenticated: auth.authenticated,