mirror of
https://github.com/AutoMaker-Org/automaker.git
synced 2026-03-18 22:33:08 +00:00
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:
@@ -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) => {
|
||||
|
||||
@@ -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
|
||||
*
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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,
|
||||
]);
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user