feat: Add conflict source branch tracking and fix auto-mode subscription cascade

This commit is contained in:
gsxdsm
2026-03-02 07:43:00 -08:00
parent c11f390764
commit 59b100b5cc
11 changed files with 35 additions and 24 deletions

View File

@@ -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

View File

@@ -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) => {

View File

@@ -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);
logger.info('Stopped running feature via drag to backlog:', 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', {

View File

@@ -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,

View File

@@ -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 {

View File

@@ -475,6 +475,7 @@ export function GraphViewPage() {
setCurrentWorktree(currentProject.path, newWorktree.path, newWorktree.branch);
},
currentWorktreeBranch,
stopFeature: autoMode.stopFeature,
});
// Handle add and start feature

View File

@@ -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 {

View File

@@ -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');

View File

@@ -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})`
);