11// @ts -nocheck - feature update logic with partial updates and image/file handling
22import { useCallback } from 'react' ;
3- import { useQueryClient } from '@tanstack/react-query' ;
43import {
54 Feature ,
65 FeatureImage ,
@@ -18,7 +17,10 @@ import { useVerifyFeature, useResumeFeature } from '@/hooks/mutations';
1817import { truncateDescription } from '@/lib/utils' ;
1918import { getBlockingDependencies } from '@automaker/dependency-resolver' ;
2019import { createLogger } from '@automaker/utils/logger' ;
21- import { queryKeys } from '@/lib/query-keys' ;
20+ import {
21+ markFeatureTransitioning ,
22+ unmarkFeatureTransitioning ,
23+ } from '@/lib/feature-transition-state' ;
2224
2325const logger = createLogger ( 'BoardActions' ) ;
2426
@@ -116,16 +118,13 @@ export function useBoardActions({
116118 currentWorktreeBranch,
117119 stopFeature,
118120} : UseBoardActionsProps ) {
119- const queryClient = useQueryClient ( ) ;
120-
121121 // IMPORTANT: Use individual selectors instead of bare useAppStore() to prevent
122122 // subscribing to the entire store. Bare useAppStore() causes the host component
123123 // (BoardView) to re-render on EVERY store change, which cascades through effects
124124 // and triggers React error #185 (maximum update depth exceeded).
125125 const addFeature = useAppStore ( ( s ) => s . addFeature ) ;
126126 const updateFeature = useAppStore ( ( s ) => s . updateFeature ) ;
127127 const removeFeature = useAppStore ( ( s ) => s . removeFeature ) ;
128- const moveFeature = useAppStore ( ( s ) => s . moveFeature ) ;
129128 const worktreesEnabled = useAppStore ( ( s ) => s . useWorktrees ) ;
130129 const enableDependencyBlocking = useAppStore ( ( s ) => s . enableDependencyBlocking ) ;
131130 const skipVerificationInAutoMode = useAppStore ( ( s ) => s . skipVerificationInAutoMode ) ;
@@ -707,8 +706,7 @@ export function useBoardActions({
707706 try {
708707 const result = await verifyFeatureMutation . mutateAsync ( feature . id ) ;
709708 if ( result . passes ) {
710- // Immediately move card to verified column (optimistic update)
711- moveFeature ( feature . id , 'verified' ) ;
709+ // persistFeatureUpdate handles the optimistic RQ cache update internally
712710 persistFeatureUpdate ( feature . id , {
713711 status : 'verified' ,
714712 justFinishedAt : undefined ,
@@ -725,7 +723,7 @@ export function useBoardActions({
725723 // Error toast is already shown by the mutation's onError handler
726724 }
727725 } ,
728- [ currentProject , verifyFeatureMutation , moveFeature , persistFeatureUpdate ]
726+ [ currentProject , verifyFeatureMutation , persistFeatureUpdate ]
729727 ) ;
730728
731729 const handleResumeFeature = useCallback (
@@ -742,7 +740,6 @@ export function useBoardActions({
742740
743741 const handleManualVerify = useCallback (
744742 ( feature : Feature ) => {
745- moveFeature ( feature . id , 'verified' ) ;
746743 persistFeatureUpdate ( feature . id , {
747744 status : 'verified' ,
748745 justFinishedAt : undefined ,
@@ -751,7 +748,7 @@ export function useBoardActions({
751748 description : `Marked as verified: ${ truncateDescription ( feature . description ) } ` ,
752749 } ) ;
753750 } ,
754- [ moveFeature , persistFeatureUpdate ]
751+ [ persistFeatureUpdate ]
755752 ) ;
756753
757754 const handleMoveBackToInProgress = useCallback (
@@ -760,13 +757,12 @@ export function useBoardActions({
760757 status : 'in_progress' as const ,
761758 startedAt : new Date ( ) . toISOString ( ) ,
762759 } ;
763- updateFeature ( feature . id , updates ) ;
764760 persistFeatureUpdate ( feature . id , updates ) ;
765761 toast . info ( 'Feature moved back' , {
766762 description : `Moved back to In Progress: ${ truncateDescription ( feature . description ) } ` ,
767763 } ) ;
768764 } ,
769- [ updateFeature , persistFeatureUpdate ]
765+ [ persistFeatureUpdate ]
770766 ) ;
771767
772768 const handleOpenFollowUp = useCallback (
@@ -885,7 +881,6 @@ export function useBoardActions({
885881 ) ;
886882
887883 if ( result . success ) {
888- moveFeature ( feature . id , 'verified' ) ;
889884 persistFeatureUpdate ( feature . id , { status : 'verified' } ) ;
890885 toast . success ( 'Feature committed' , {
891886 description : `Committed and verified: ${ truncateDescription ( feature . description ) } ` ,
@@ -907,7 +902,7 @@ export function useBoardActions({
907902 await loadFeatures ( ) ;
908903 }
909904 } ,
910- [ currentProject , moveFeature , persistFeatureUpdate , loadFeatures , onWorktreeCreated ]
905+ [ currentProject , persistFeatureUpdate , loadFeatures , onWorktreeCreated ]
911906 ) ;
912907
913908 const handleMergeFeature = useCallback (
@@ -951,17 +946,12 @@ export function useBoardActions({
951946
952947 const handleCompleteFeature = useCallback (
953948 ( feature : Feature ) => {
954- const updates = {
955- status : 'completed' as const ,
956- } ;
957- updateFeature ( feature . id , updates ) ;
958- persistFeatureUpdate ( feature . id , updates ) ;
959-
949+ persistFeatureUpdate ( feature . id , { status : 'completed' as const } ) ;
960950 toast . success ( 'Feature completed' , {
961951 description : `Archived: ${ truncateDescription ( feature . description ) } ` ,
962952 } ) ;
963953 } ,
964- [ updateFeature , persistFeatureUpdate ]
954+ [ persistFeatureUpdate ]
965955 ) ;
966956
967957 const handleUnarchiveFeature = useCallback (
@@ -978,11 +968,7 @@ export function useBoardActions({
978968 ( projectPath ? isPrimaryWorktreeBranch ( projectPath , currentWorktreeBranch ) : true )
979969 : featureBranch === currentWorktreeBranch ;
980970
981- const updates : Partial < Feature > = {
982- status : 'verified' as const ,
983- } ;
984- updateFeature ( feature . id , updates ) ;
985- persistFeatureUpdate ( feature . id , updates ) ;
971+ persistFeatureUpdate ( feature . id , { status : 'verified' as const } ) ;
986972
987973 if ( willBeVisibleOnCurrentView ) {
988974 toast . success ( 'Feature restored' , {
@@ -994,13 +980,7 @@ export function useBoardActions({
994980 } ) ;
995981 }
996982 } ,
997- [
998- updateFeature ,
999- persistFeatureUpdate ,
1000- currentWorktreeBranch ,
1001- projectPath ,
1002- isPrimaryWorktreeBranch ,
1003- ]
983+ [ persistFeatureUpdate , currentWorktreeBranch , projectPath , isPrimaryWorktreeBranch ]
1004984 ) ;
1005985
1006986 const handleViewOutput = useCallback (
@@ -1031,6 +1011,13 @@ export function useBoardActions({
10311011
10321012 const handleForceStopFeature = useCallback (
10331013 async ( feature : Feature ) => {
1014+ // Mark this feature as transitioning so WebSocket-driven query invalidation
1015+ // (useAutoModeQueryInvalidation) skips redundant cache invalidations while
1016+ // persistFeatureUpdate is handling the optimistic update. Without this guard,
1017+ // auto_mode_error / auto_mode_stopped WS events race with the optimistic
1018+ // update and cause cache flip-flops that cascade through useBoardColumnFeatures,
1019+ // triggering React error #185 on mobile.
1020+ markFeatureTransitioning ( feature . id ) ;
10341021 try {
10351022 await stopFeature ( feature . id ) ;
10361023
@@ -1048,25 +1035,11 @@ export function useBoardActions({
10481035 removeRunningTaskFromAllWorktrees ( currentProject . id , feature . id ) ;
10491036 }
10501037
1051- // Optimistically update the React Query features cache so the board
1052- // moves the card immediately. Without this, the card stays in
1053- // "in_progress" until the next poll cycle (30s) because the async
1054- // refetch races with the persistFeatureUpdate write.
1055- if ( currentProject ) {
1056- queryClient . setQueryData (
1057- queryKeys . features . all ( currentProject . path ) ,
1058- ( oldFeatures : Feature [ ] | undefined ) => {
1059- if ( ! oldFeatures ) return oldFeatures ;
1060- return oldFeatures . map ( ( f ) =>
1061- f . id === feature . id ? { ...f , status : targetStatus } : f
1062- ) ;
1063- }
1064- ) ;
1065- }
1066-
10671038 if ( targetStatus !== feature . status ) {
1068- moveFeature ( feature . id , targetStatus ) ;
1069- // Must await to ensure file is written before user can restart
1039+ // persistFeatureUpdate handles the optimistic RQ cache update, the
1040+ // Zustand store update (on server response), and the final cache
1041+ // invalidation internally — no need for separate queryClient.setQueryData
1042+ // or moveFeature calls which would cause redundant re-renders.
10701043 await persistFeatureUpdate ( feature . id , { status : targetStatus } ) ;
10711044 }
10721045
@@ -1083,9 +1056,15 @@ export function useBoardActions({
10831056 toast . error ( 'Failed to stop agent' , {
10841057 description : error instanceof Error ? error . message : 'An error occurred' ,
10851058 } ) ;
1059+ } finally {
1060+ // Delay unmarking so the refetch triggered by persistFeatureUpdate's
1061+ // invalidateQueries() has time to settle before WS-driven invalidations
1062+ // are allowed through again. Without this, a WS event arriving during
1063+ // the refetch window would trigger a conflicting invalidation.
1064+ setTimeout ( ( ) => unmarkFeatureTransitioning ( feature . id ) , 500 ) ;
10861065 }
10871066 } ,
1088- [ stopFeature , moveFeature , persistFeatureUpdate , currentProject , queryClient ]
1067+ [ stopFeature , persistFeatureUpdate , currentProject ]
10891068 ) ;
10901069
10911070 const handleStartNextFeatures = useCallback ( async ( ) => {
0 commit comments