mirror of
https://github.com/AutoMaker-Org/automaker.git
synced 2026-03-16 21:53:07 +00:00
feat: Add conflict source branch tracking and fix auto-mode subscription cascade
This commit is contained in:
@@ -87,6 +87,8 @@ interface WorktreeInfo {
|
||||
conflictType?: 'merge' | 'rebase' | 'cherry-pick';
|
||||
/** List of files with conflicts */
|
||||
conflictFiles?: string[];
|
||||
/** Source branch involved in merge/rebase/cherry-pick, when resolvable */
|
||||
conflictSourceBranch?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -160,7 +162,7 @@ async function detectConflictState(worktreePath: string): Promise<{
|
||||
if (conflictType === 'merge' && mergeHeadExists) {
|
||||
// For merges, resolve MERGE_HEAD to a branch name
|
||||
const mergeHead = (
|
||||
await secureFs.readFile(path.join(gitDir, 'MERGE_HEAD'), 'utf-8')
|
||||
(await secureFs.readFile(path.join(gitDir, 'MERGE_HEAD'), 'utf-8')) as string
|
||||
).trim();
|
||||
try {
|
||||
const branchName = await execGitCommand(
|
||||
@@ -180,7 +182,7 @@ async function detectConflictState(worktreePath: string): Promise<{
|
||||
? path.join(gitDir, 'rebase-merge', 'onto-name')
|
||||
: path.join(gitDir, 'rebase-apply', 'onto-name');
|
||||
try {
|
||||
const ontoName = (await secureFs.readFile(headNamePath, 'utf-8')).trim();
|
||||
const ontoName = ((await secureFs.readFile(headNamePath, 'utf-8')) as string).trim();
|
||||
if (ontoName) {
|
||||
conflictSourceBranch = ontoName.replace(/^refs\/heads\//, '');
|
||||
}
|
||||
@@ -190,7 +192,7 @@ async function detectConflictState(worktreePath: string): Promise<{
|
||||
const ontoPath = rebaseMergeExists
|
||||
? path.join(gitDir, 'rebase-merge', 'onto')
|
||||
: path.join(gitDir, 'rebase-apply', 'onto');
|
||||
const ontoCommit = (await secureFs.readFile(ontoPath, 'utf-8')).trim();
|
||||
const ontoCommit = ((await secureFs.readFile(ontoPath, 'utf-8')) as string).trim();
|
||||
if (ontoCommit) {
|
||||
const branchName = await execGitCommand(
|
||||
['name-rev', '--name-only', '--refs=refs/heads/*', ontoCommit],
|
||||
@@ -208,7 +210,7 @@ async function detectConflictState(worktreePath: string): Promise<{
|
||||
} else if (conflictType === 'cherry-pick' && cherryPickHeadExists) {
|
||||
// For cherry-picks, try to resolve CHERRY_PICK_HEAD to a branch name
|
||||
const cherryPickHead = (
|
||||
await secureFs.readFile(path.join(gitDir, 'CHERRY_PICK_HEAD'), 'utf-8')
|
||||
(await secureFs.readFile(path.join(gitDir, 'CHERRY_PICK_HEAD'), 'utf-8')) as string
|
||||
).trim();
|
||||
try {
|
||||
const branchName = await execGitCommand(
|
||||
@@ -669,6 +671,7 @@ export function createListHandler() {
|
||||
// hasConflicts is true only when there are actual unresolved files
|
||||
worktree.hasConflicts = conflictState.hasConflicts;
|
||||
worktree.conflictFiles = conflictState.conflictFiles;
|
||||
worktree.conflictSourceBranch = conflictState.conflictSourceBranch;
|
||||
} catch {
|
||||
// Ignore conflict detection errors
|
||||
}
|
||||
|
||||
@@ -843,6 +843,7 @@ export function BoardView({ initialFeatureId }: BoardViewProps) {
|
||||
onWorktreeCreated: () => setWorktreeRefreshKey((k) => k + 1),
|
||||
onWorktreeAutoSelect: addAndSelectWorktree,
|
||||
currentWorktreeBranch,
|
||||
stopFeature: autoMode.stopFeature,
|
||||
});
|
||||
|
||||
// Handler for bulk updating multiple features
|
||||
|
||||
@@ -14,7 +14,6 @@ import { FeatureImagePath as DescriptionImagePath } from '@/components/ui/descri
|
||||
import { getElectronAPI } from '@/lib/electron';
|
||||
import { isConnectionError, handleServerOffline, getHttpApiClient } from '@/lib/http-api-client';
|
||||
import { toast } from 'sonner';
|
||||
import { useAutoMode } from '@/hooks/use-auto-mode';
|
||||
import { useVerifyFeature, useResumeFeature } from '@/hooks/mutations';
|
||||
import { truncateDescription } from '@/lib/utils';
|
||||
import { getBlockingDependencies } from '@automaker/dependency-resolver';
|
||||
@@ -86,6 +85,7 @@ interface UseBoardActionsProps {
|
||||
onWorktreeCreated?: () => void;
|
||||
onWorktreeAutoSelect?: (worktree: { path: string; branch: string }) => void;
|
||||
currentWorktreeBranch: string | null; // Branch name of the selected worktree for filtering
|
||||
stopFeature: (featureId: string) => Promise<boolean>; // Passed from parent's useAutoMode to avoid duplicate subscription
|
||||
}
|
||||
|
||||
export function useBoardActions({
|
||||
@@ -114,6 +114,7 @@ export function useBoardActions({
|
||||
onWorktreeCreated,
|
||||
onWorktreeAutoSelect,
|
||||
currentWorktreeBranch,
|
||||
stopFeature,
|
||||
}: UseBoardActionsProps) {
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
@@ -130,7 +131,6 @@ export function useBoardActions({
|
||||
const skipVerificationInAutoMode = useAppStore((s) => s.skipVerificationInAutoMode);
|
||||
const isPrimaryWorktreeBranch = useAppStore((s) => s.isPrimaryWorktreeBranch);
|
||||
const getPrimaryWorktreeBranch = useAppStore((s) => s.getPrimaryWorktreeBranch);
|
||||
const autoMode = useAutoMode();
|
||||
|
||||
// React Query mutations for feature operations
|
||||
const verifyFeatureMutation = useVerifyFeature(currentProject?.path ?? '');
|
||||
@@ -538,7 +538,7 @@ export function useBoardActions({
|
||||
|
||||
if (isRunning) {
|
||||
try {
|
||||
await autoMode.stopFeature(featureId);
|
||||
await stopFeature(featureId);
|
||||
// Remove from all worktrees
|
||||
if (currentProject) {
|
||||
removeRunningTaskFromAllWorktrees(currentProject.id, featureId);
|
||||
@@ -573,7 +573,7 @@ export function useBoardActions({
|
||||
removeFeature(featureId);
|
||||
await persistFeatureDelete(featureId);
|
||||
},
|
||||
[features, runningAutoTasks, autoMode, removeFeature, persistFeatureDelete, currentProject]
|
||||
[features, runningAutoTasks, stopFeature, removeFeature, persistFeatureDelete, currentProject]
|
||||
);
|
||||
|
||||
const handleRunFeature = useCallback(
|
||||
@@ -1032,7 +1032,7 @@ export function useBoardActions({
|
||||
const handleForceStopFeature = useCallback(
|
||||
async (feature: Feature) => {
|
||||
try {
|
||||
await autoMode.stopFeature(feature.id);
|
||||
await stopFeature(feature.id);
|
||||
|
||||
const targetStatus =
|
||||
feature.skipTests && feature.status === 'waiting_approval'
|
||||
@@ -1040,7 +1040,7 @@ export function useBoardActions({
|
||||
: 'backlog';
|
||||
|
||||
// Remove the running task from ALL worktrees for this project.
|
||||
// autoMode.stopFeature only removes from its scoped worktree (branchName),
|
||||
// stopFeature only removes from its scoped worktree (branchName),
|
||||
// but the feature may be tracked under a different worktree branch.
|
||||
// Without this, runningAutoTasksAllWorktrees still contains the feature
|
||||
// and the board column logic forces it into in_progress.
|
||||
@@ -1085,7 +1085,7 @@ export function useBoardActions({
|
||||
});
|
||||
}
|
||||
},
|
||||
[autoMode, moveFeature, persistFeatureUpdate, currentProject, queryClient]
|
||||
[stopFeature, moveFeature, persistFeatureUpdate, currentProject, queryClient]
|
||||
);
|
||||
|
||||
const handleStartNextFeatures = useCallback(async () => {
|
||||
@@ -1197,7 +1197,7 @@ export function useBoardActions({
|
||||
if (runningVerified.length > 0) {
|
||||
await Promise.allSettled(
|
||||
runningVerified.map((feature) =>
|
||||
autoMode.stopFeature(feature.id).catch((error) => {
|
||||
stopFeature(feature.id).catch((error) => {
|
||||
logger.error('Error stopping feature before archive:', error);
|
||||
})
|
||||
)
|
||||
@@ -1236,7 +1236,7 @@ export function useBoardActions({
|
||||
// Reload features to sync state with server on error
|
||||
loadFeatures();
|
||||
}
|
||||
}, [features, runningAutoTasks, autoMode, updateFeature, currentProject, loadFeatures]);
|
||||
}, [features, runningAutoTasks, stopFeature, updateFeature, currentProject, loadFeatures]);
|
||||
|
||||
const handleDuplicateFeature = useCallback(
|
||||
async (feature: Feature, asChild: boolean = false) => {
|
||||
|
||||
@@ -19,7 +19,7 @@ interface UseBoardDragDropProps {
|
||||
runningAutoTasks: string[];
|
||||
persistFeatureUpdate: (featureId: string, updates: Partial<Feature>) => Promise<void>;
|
||||
handleStartImplementation: (feature: Feature) => Promise<boolean>;
|
||||
stopFeature: (featureId: string) => Promise<void>;
|
||||
stopFeature: (featureId: string) => Promise<boolean>;
|
||||
}
|
||||
|
||||
export function useBoardDragDrop({
|
||||
@@ -256,8 +256,12 @@ export function useBoardDragDrop({
|
||||
// If the feature is currently running, stop it first
|
||||
if (isRunningTask) {
|
||||
try {
|
||||
await stopFeature(featureId);
|
||||
const stopped = await stopFeature(featureId);
|
||||
if (stopped) {
|
||||
logger.info('Stopped running feature via drag to backlog:', featureId);
|
||||
} else {
|
||||
logger.warn('Feature was not running by the time stop was requested:', featureId);
|
||||
}
|
||||
} catch (error) {
|
||||
logger.error('Error stopping feature during drag to backlog:', error);
|
||||
toast.error('Failed to stop agent', {
|
||||
|
||||
@@ -482,7 +482,7 @@ export function WorktreeActionsDropdown({
|
||||
<DropdownMenuItem
|
||||
onClick={() =>
|
||||
onCreateConflictResolutionFeature({
|
||||
sourceBranch: worktree.branch,
|
||||
sourceBranch: worktree.conflictSourceBranch ?? worktree.branch,
|
||||
targetBranch: worktree.branch,
|
||||
targetWorktreePath: worktree.path,
|
||||
conflictFiles: worktree.conflictFiles,
|
||||
|
||||
@@ -17,6 +17,8 @@ export interface WorktreeInfo {
|
||||
conflictType?: 'merge' | 'rebase' | 'cherry-pick';
|
||||
/** List of files with conflicts */
|
||||
conflictFiles?: string[];
|
||||
/** The branch that is the source of the conflict (e.g. the branch being merged in) */
|
||||
conflictSourceBranch?: string;
|
||||
}
|
||||
|
||||
export interface BranchInfo {
|
||||
|
||||
@@ -475,6 +475,7 @@ export function GraphViewPage() {
|
||||
setCurrentWorktree(currentProject.path, newWorktree.path, newWorktree.branch);
|
||||
},
|
||||
currentWorktreeBranch,
|
||||
stopFeature: autoMode.stopFeature,
|
||||
});
|
||||
|
||||
// Handle add and start feature
|
||||
|
||||
@@ -28,6 +28,8 @@ interface WorktreeInfo {
|
||||
conflictType?: 'merge' | 'rebase' | 'cherry-pick';
|
||||
/** List of files with conflicts */
|
||||
conflictFiles?: string[];
|
||||
/** The branch that is the source of the conflict (e.g. the branch being merged in) */
|
||||
conflictSourceBranch?: string;
|
||||
}
|
||||
|
||||
interface RemovedWorktree {
|
||||
|
||||
@@ -959,10 +959,10 @@ export function useAutoMode(worktree?: WorktreeInfo) {
|
||||
|
||||
// Stop a specific feature
|
||||
const stopFeature = useCallback(
|
||||
async (featureId: string) => {
|
||||
async (featureId: string): Promise<boolean> => {
|
||||
if (!currentProject) {
|
||||
logger.error('No project selected');
|
||||
return;
|
||||
return false;
|
||||
}
|
||||
|
||||
try {
|
||||
@@ -983,6 +983,7 @@ export function useAutoMode(worktree?: WorktreeInfo) {
|
||||
message: 'Feature stopped by user',
|
||||
passes: false,
|
||||
});
|
||||
return true;
|
||||
} else {
|
||||
logger.error('Failed to stop feature:', result.error);
|
||||
throw new Error(result.error || 'Failed to stop feature');
|
||||
|
||||
@@ -613,10 +613,7 @@ function RootLayoutContent() {
|
||||
// Reconcile ntfy endpoints from server (same rationale as eventHooks)
|
||||
const serverEndpoints = (finalSettings as GlobalSettings).ntfyEndpoints ?? [];
|
||||
const currentEndpoints = useAppStore.getState().ntfyEndpoints;
|
||||
if (
|
||||
JSON.stringify(serverEndpoints) !== JSON.stringify(currentEndpoints) &&
|
||||
serverEndpoints.length > 0
|
||||
) {
|
||||
if (JSON.stringify(serverEndpoints) !== JSON.stringify(currentEndpoints)) {
|
||||
logger.info(
|
||||
`[FAST_HYDRATE] Reconciling ntfyEndpoints from server (server=${serverEndpoints.length}, store=${currentEndpoints.length})`
|
||||
);
|
||||
|
||||
@@ -28,7 +28,7 @@ test.describe('Feature Deep Link', () => {
|
||||
let projectPath: string;
|
||||
let projectName: string;
|
||||
|
||||
test.beforeEach(async ({}, testInfo) => {
|
||||
test.beforeEach(async (_, testInfo) => {
|
||||
projectName = `test-project-${testInfo.workerIndex}-${Date.now()}`;
|
||||
projectPath = path.join(TEST_TEMP_DIR, projectName);
|
||||
fs.mkdirSync(projectPath, { recursive: true });
|
||||
|
||||
Reference in New Issue
Block a user