Skip to content

Fixes critical React crash on the Kanban board view#830

Merged
gsxdsm merged 2 commits intoAutoMaker-Org:v1.0.0rcfrom
gsxdsm:fix/board-react-crash
Mar 4, 2026
Merged

Fixes critical React crash on the Kanban board view#830
gsxdsm merged 2 commits intoAutoMaker-Org:v1.0.0rcfrom
gsxdsm:fix/board-react-crash

Conversation

@gsxdsm
Copy link
Collaborator

@gsxdsm gsxdsm commented Mar 4, 2026

Summary

This PR fixes a critical React crash (Error #185: Maximum update depth exceeded) on the Kanban board view by eliminating redundant dual state mutations. It also includes substantial improvements to board action logic, worktree handling, and feature lifecycle management.

Root Cause

The crash was caused by redundant state mutations in board action and drag-drop handlers:

  • Handlers were calling both Zustand moveFeature/updateFeature and persistFeatureUpdate, which internally handles its own optimistic React Query cache update
  • useBoardActions was subscribing to the entire Zustand store via bare useAppStore(), causing BoardView to re-render on every store change and cascade into infinite effect loops

Changes

Bug Fixes

  • React Error fixing worktree style #185 fix: Replaced bare useAppStore() destructuring with individual selectors (useAppStore((s) => s.field)) to prevent subscribing to the entire store
  • Eliminated redundant state mutations: Removed moveFeature calls before persistFeatureUpdate in handleManualVerify, handleMoveBackToInProgress, handleCompleteFeature, handleUnarchiveFeature, handleCommitFeature, and drag-drop handlers — persistFeatureUpdate already handles the optimistic cache update
  • WS invalidation race condition fix: Added feature-transition-state.ts to suppress WebSocket-driven query invalidations during feature cancel/stop transitions, preventing cache flip-flops that caused re-render cascades
  • Stop feature scope fix: stopFeature previously only removed running tasks from the current worktree scope; now uses removeRunningTaskFromAllWorktrees to clear the feature from all worktree contexts

Files Changed

File Description
use-board-actions.ts Core fixes + new duplicate/bulk-archive/child-deps features
use-board-column-features.ts Aligned with RQ-first state model
use-board-drag-drop.ts Removed redundant dual mutations
use-board-features.ts Simplified, removed Zustand over-subscription
kanban-board.tsx Passes stopFeature prop down to useBoardActions
use-auto-mode.ts Exposes stopFeature for prop-drilling to avoid duplicate subscriptions
use-query-invalidation.ts Respects feature-transition-state guard
feature-transition-state.ts New: transition guard module
app-store.ts Added removeRunningTask cross-worktree helper

Summary by CodeRabbit

  • Bug Fixes

    • Eliminated race conditions between optimistic updates and real-time sync to prevent inconsistent feature states.
    • Fixed duplicate state mutations that could cause unexpected UI behavior during auto-mode events.
  • Performance Improvements

    • Reduced unnecessary re-renders and stabilized worktree-specific updates.
    • Batched auto-mode updates and improved memoization for smoother drag-and-drop and feature transitions.
  • Stability

    • Improved WebSocket-driven invalidation to avoid disrupting in-flight updates.

@coderabbitai
Copy link
Contributor

coderabbitai bot commented Mar 4, 2026

📝 Walkthrough

Walkthrough

Refactors board-view optimistic updates to use persistent updates and a module-level feature transition guard; stabilizes auto-mode/worktree selectors and batching; adds idempotent store guards and minor component memoization. Introduces logic to avoid WebSocket-triggered refetches during in-flight feature transitions.

Changes

Cohort / File(s) Summary
Feature Transition State
apps/ui/src/lib/feature-transition-state.ts
New module: markFeatureTransitioning(), unmarkFeatureTransitioning(), isAnyFeatureTransitioning() to track in-flight feature updates.
Board Actions & Cache Sync
apps/ui/src/components/views/board-view/hooks/use-board-actions.ts
Removed direct React Query/local moveFeature mutations; use persistFeatureUpdate for optimistic state sync and added transition guards/delays to avoid WS race conditions.
Drag & Drop
apps/ui/src/components/views/board-view/hooks/use-board-drag-drop.ts
Dropped moveFeature calls; rely on persistFeatureUpdate/updateFeature for status/branch updates and adjusted exported deps.
Column Features
apps/ui/src/components/views/board-view/hooks/use-board-column-features.ts
Eliminated prevFeatureIdsRef; read recentlyCompletedFeatures directly from store and simplified effect dependencies.
Board Features / Auto Mode Events
apps/ui/src/components/views/board-view/hooks/use-board-features.ts
Removed local running-task removals in auto-mode event handlers; delegated state changes to useAutoMode, leaving UI/toast handling.
Auto Mode Hook
apps/ui/src/hooks/use-auto-mode.ts
Per-worktree selector, stabilized empty tasks constant, runningTasks memoization, and batchedAddAutoModeActivity to batch updates and reduce re-renders.
Query Invalidation / WS Guards
apps/ui/src/hooks/use-query-invalidation.ts
Excluded certain auto_mode events from feature-list invalidations and added isAnyFeatureTransitioning() guard to skip invalidation during in-flight updates.
Store Idempotency
apps/ui/src/store/app-store.ts
Added early-return guards to removeRunningTask(), addRecentlyCompletedFeature(), and clearRecentlyCompletedFeatures() to avoid unnecessary state writes.
Component Memoization
apps/ui/src/components/views/board-view/kanban-board.tsx
Introduced stable EMPTY_FEATURE_IDS constant to avoid reconstructing Set() on each render.
Misc / deps
package.json
Small manifest changes (dependency list adjustments).

Sequence Diagram(s)

sequenceDiagram
    participant User as User/UI
    participant Board as Board Hook
    participant Store as App Store
    participant WS as WebSocket
    participant Server as API/Server

    User->>Board: Trigger feature action (e.g., stop/verify/move)
    Board->>Board: markFeatureTransitioning(featureId)
    Board->>Server: persistFeatureUpdate(featureId, updatedFields)
    Server-->>Board: ACK / updated feature
    Board->>Store: update local state via persistFeatureUpdate result
    Board->>Board: unmarkFeatureTransitioning(featureId)
    WS-->>Board: emits feature-updated event
    alt feature was transitioning
        Board->>Board: isAnyFeatureTransitioning() returns false? (guard prevents refetch)
    else not transitioning
        Board->>Store: invalidate/refetch features list
    end
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Possibly related PRs

Suggested labels

Refactor, cleanup

Poem

🐰
Hop-hop, I mark the feature true,
I pause the buzz so updates queue,
Persisted hops make statuses sing,
Guards hold fast while websockets ring,
A carrot snack — the board's in tune! 🥕

🚥 Pre-merge checks | ✅ 2 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 27.27% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately describes the main fix: resolving a critical React crash (maximum update depth exceeded) on the Kanban board view through state management improvements.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@gemini-code-assist
Copy link
Contributor

Summary of Changes

Hello, I'm Gemini Code Assist1! I'm currently reviewing this pull request and will post my feedback shortly. In the meantime, here's a summary to help you and other reviewers quickly get up to speed!

This pull request significantly enhances the performance and stability of the development server and UI by addressing issues related to excessive logging, redundant state updates, and cascading React re-renders. The changes focus on optimizing event handling, centralizing state management, and introducing mechanisms to prevent race conditions, ultimately leading to a more responsive and resource-efficient application experience.

Highlights

  • Centralized Feature State Updates: Refactored feature status updates to exclusively use persistFeatureUpdate, eliminating redundant moveFeature and updateFeature calls from Zustand, thereby reducing unnecessary re-renders and simplifying state management logic.
  • Optimized React Component Re-renders: Improved UI performance by refining Zustand selectors in useAutoMode and useBoardColumnFeatures to prevent cascading re-renders, ensuring components only re-render when their specific slice of state changes.
  • Introduced Feature Transition State: Implemented a new module (feature-transition-state.ts) to track features undergoing transitions, allowing WebSocket-driven query invalidations to be temporarily skipped to prevent race conditions with optimistic UI updates and avoid React error fixing worktree style #185.
  • Refined Query Invalidation Logic: Adjusted useAutoModeQueryInvalidation to exclude auto_mode_started and auto_mode_stopped events from triggering feature list invalidations, reducing unnecessary data refetches and improving responsiveness.
  • Enhanced Zustand Store Idempotency: Added idempotency checks to removeRunningTask, addRecentlyCompletedFeature, and clearRecentlyCompletedFeatures actions in the app-store, preventing redundant state updates and object reference changes that could trigger unnecessary re-renders.
  • Dev Server Log and Event Throttling: Increased dev server output throttle from 4ms to 100ms and implemented request animation frame (RAF) batching for log streaming, significantly reducing log spam, memory growth, and React re-renders in the UI.

🧠 New Feature in Public Preview: You can now enable Memory to help Gemini Code Assist learn from your team's feedback. This makes future code reviews more consistent and personalized to your project's style. Click here to enable Memory in your admin console.

Changelog
  • apps/ui/src/components/views/board-view/hooks/use-board-actions.ts
    • Removed useQueryClient import and queryClient instance.
    • Replaced queryKeys import with markFeatureTransitioning and unmarkFeatureTransitioning.
    • Removed moveFeature from Zustand store selector.
    • Eliminated direct calls to moveFeature and updateFeature, relying on persistFeatureUpdate.
    • Added markFeatureTransitioning before stopFeature and unmarkFeatureTransitioning in a finally block to manage feature transition state.
  • apps/ui/src/components/views/board-view/hooks/use-board-column-features.ts
    • Removed useRef import and prevFeatureIdsRef.
    • Updated useEffect dependency array for clearing recently completed features to avoid re-trigger loops.
    • Modified logic to directly access recentlyCompletedFeatures from useAppStore.getState() instead of relying on it as a dependency.
  • apps/ui/src/components/views/board-view/hooks/use-board-drag-drop.ts
    • Removed moveFeature from Zustand store selector.
    • Updated drag-and-drop handlers to remove direct moveFeature calls, with persistFeatureUpdate now handling optimistic updates.
  • apps/ui/src/components/views/board-view/hooks/use-board-features.ts
    • Removed removeRunningTask call from auto_mode_error event handler.
    • Removed projectId and eventProjectId calculations.
    • Removed debug logging for ignored auto mode events.
  • apps/ui/src/components/views/board-view/kanban-board.tsx
    • Added EMPTY_FEATURE_IDS constant to provide a stable default prop value for selectedFeatureIds.
  • apps/ui/src/hooks/use-auto-mode.ts
    • Removed autoModeByWorktree from the useShallow selector.
    • Refactored worktreeAutoModeState to use a targeted useAppStore selector, preventing re-renders when other worktrees' states change.
  • apps/ui/src/hooks/use-query-invalidation.ts
    • Imported isAnyFeatureTransitioning.
    • Removed auto_mode_started and auto_mode_stopped from FEATURE_LIST_INVALIDATION_EVENTS.
    • Added a conditional check !isAnyFeatureTransitioning() to prevent feature list invalidation during feature transitions.
  • apps/ui/src/lib/feature-transition-state.ts
    • Added new file to define and export markFeatureTransitioning, unmarkFeatureTransitioning, and isAnyFeatureTransitioning functions for tracking feature transition states.
  • apps/ui/src/store/app-store.ts
    • Added idempotency checks to removeRunningTask to prevent unnecessary state updates.
    • Added idempotency checks to addRecentlyCompletedFeature to avoid creating new Set references if the feature is already tracked.
    • Added idempotency checks to clearRecentlyCompletedFeatures to avoid creating new Set references if the set is already empty.
Activity
  • Optimized server-side event logging by changing high-frequency events to debug level and removing verbose log payloads.
  • Implemented dev server output throttling by increasing OUTPUT_THROTTLE_MS from 4ms to 100ms and OUTPUT_BATCH_SIZE from 4096 to 8192 bytes.
  • Introduced request animation frame (RAF) batching for log streaming in the board view to reduce React re-renders.
  • Refactored board view auto mode event handling to remove duplicate Zustand mutations and unnecessary calculations, preventing re-render cascades.
  • Increased state reconciliation polling interval from 5 seconds to 30 seconds, leveraging WebSocket for real-time updates.
  • Updated test cases to accommodate the new throttle constant.
Using Gemini Code Assist

The full guide for Gemini Code Assist can be found on our documentation page, here are some quick tips.

Invoking Gemini

You can request assistance from Gemini at any point by creating a comment using either /gemini <command> or @gemini-code-assist <command>. Below is a summary of the supported commands on the current page.

Feature Command Description
Code Review /gemini review Performs a code review for the current pull request in its current state.
Pull Request Summary /gemini summary Provides a summary of the current pull request in its current state.
Comment @gemini-code-assist Responds in comments when explicitly tagged, both in pull request comments and review comments.
Help /gemini help Displays a list of available commands.

Customization

To customize Gemini Code Assist for GitHub experience, repository maintainers can create a configuration file and/or provide a custom code review style guide (such as PEP-8 for Python) by creating and adding files to a .gemini/ folder in the base of the repository. Detailed instructions can be found here.

Limitations & Feedback

Gemini Code Assist may make mistakes. Please leave feedback on any instances where its feedback is incorrect or counter productive. You can react with 👍 and 👎 on @gemini-code-assist comments. If you're interested in giving your feedback about your experience with Gemini Code Assist for Github and other Google products, sign up here.

You can also get AI-powered code generation, chat, as well as code reviews directly in the IDE at no cost with the Gemini Code Assist IDE Extension.

Footnotes

  1. Review the Privacy Notices, Generative AI Prohibited Use Policy, Terms of Service, and learn how to configure Gemini Code Assist in GitHub here. Gemini can make mistakes, so double check it and use code with caution.

Copy link
Contributor

@gemini-code-assist gemini-code-assist bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

This pull request introduces a series of significant performance optimizations and stability improvements across the application. The changes effectively reduce log spam, decrease event frequency, and eliminate sources of re-render cascades in the UI, which directly addresses the reported server and browser slowdowns. The refactoring to centralize state updates and the introduction of a state transition guard to prevent race conditions are particularly well-executed. I've included a couple of minor suggestions for further improvement, but overall this is a very high-quality contribution.

Comment on lines 198 to +210
useEffect(() => {
const currentIds = new Set(features.map((f) => f.id));
const currentRecentlyCompleted = useAppStore.getState().recentlyCompletedFeatures;
if (currentRecentlyCompleted.size === 0) return;

// Check if any recently completed features now have terminal statuses in the new data
// If so, we can clear the tracking since the cache is now fresh
const hasUpdatedStatus = Array.from(recentlyCompletedFeatures).some((featureId) => {
const hasUpdatedStatus = Array.from(currentRecentlyCompleted).some((featureId) => {
const feature = features.find((f) => f.id === featureId);
return feature && (feature.status === 'verified' || feature.status === 'completed');
});

if (hasUpdatedStatus) {
clearRecentlyCompletedFeatures();
}

prevFeatureIdsRef.current = currentIds;
}, [features, recentlyCompletedFeatures, clearRecentlyCompletedFeatures]);
}, [features, clearRecentlyCompletedFeatures]);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

This useEffect is well-refactored to avoid dependency loops. For a minor performance improvement, you can optimize the lookup inside the .some() call. Currently, it uses features.find(), which has O(N) complexity, making the total complexity O(M * N) where M is the size of recentlyCompletedFeatures and N is the number of features. By building a Map of features first, you can achieve an O(1) lookup, improving the overall performance of this effect, which is in line with the goals of this PR.

Suggested change
useEffect(() => {
const currentIds = new Set(features.map((f) => f.id));
const currentRecentlyCompleted = useAppStore.getState().recentlyCompletedFeatures;
if (currentRecentlyCompleted.size === 0) return;
// Check if any recently completed features now have terminal statuses in the new data
// If so, we can clear the tracking since the cache is now fresh
const hasUpdatedStatus = Array.from(recentlyCompletedFeatures).some((featureId) => {
const hasUpdatedStatus = Array.from(currentRecentlyCompleted).some((featureId) => {
const feature = features.find((f) => f.id === featureId);
return feature && (feature.status === 'verified' || feature.status === 'completed');
});
if (hasUpdatedStatus) {
clearRecentlyCompletedFeatures();
}
prevFeatureIdsRef.current = currentIds;
}, [features, recentlyCompletedFeatures, clearRecentlyCompletedFeatures]);
}, [features, clearRecentlyCompletedFeatures]);
useEffect(() => {
const currentRecentlyCompleted = useAppStore.getState().recentlyCompletedFeatures;
if (currentRecentlyCompleted.size === 0 || features.length === 0) return;
const featureMap = new Map(features.map((f) => [f.id, f]));
const hasUpdatedStatus = Array.from(currentRecentlyCompleted).some((featureId) => {
const feature = featureMap.get(featureId);
return feature && (feature.status === 'verified' || feature.status === 'completed');
});
if (hasUpdatedStatus) {
clearRecentlyCompletedFeatures();
}
}, [features, clearRecentlyCompletedFeatures]);

// invalidateQueries() has time to settle before WS-driven invalidations
// are allowed through again. Without this, a WS event arriving during
// the refetch window would trigger a conflicting invalidation.
setTimeout(() => unmarkFeatureTransitioning(feature.id), 500);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

While using a setTimeout here is a pragmatic solution for the race condition, the magic number 500 can be hard to maintain. Consider defining it as a named constant at the top of the file (e.g., UNMARK_TRANSITION_DELAY_MS) to clarify its purpose and make it easier to adjust globally if needed.

@gsxdsm gsxdsm changed the title Fix dev server hang by reducing log spam and event frequency Fixes critical React crash on the Kanban board view Mar 4, 2026
Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
apps/ui/src/components/views/board-view/hooks/use-board-actions.ts (1)

741-751: ⚠️ Potential issue | 🟡 Minor

Guard project-null paths before showing success toasts.

These handlers can show success even when persistFeatureUpdate no-ops (when currentProject is unset). Please gate on project presence (and preferably await persistence) before success feedback.

Suggested pattern (apply to all affected handlers)
-  const handleManualVerify = useCallback(
-    (feature: Feature) => {
-      persistFeatureUpdate(feature.id, {
-        status: 'verified',
-        justFinishedAt: undefined,
-      });
-      toast.success('Feature verified', {
-        description: `Marked as verified: ${truncateDescription(feature.description)}`,
-      });
-    },
-    [persistFeatureUpdate]
-  );
+  const handleManualVerify = useCallback(
+    async (feature: Feature) => {
+      if (!currentProject) return;
+      await persistFeatureUpdate(feature.id, {
+        status: 'verified',
+        justFinishedAt: undefined,
+      });
+      toast.success('Feature verified', {
+        description: `Marked as verified: ${truncateDescription(feature.description)}`,
+      });
+    },
+    [currentProject, persistFeatureUpdate]
+  );

Also applies to: 754-766, 947-955, 957-984

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/ui/src/components/views/board-view/hooks/use-board-actions.ts` around
lines 741 - 751, The success toast in handleManualVerify is shown even when
persistFeatureUpdate is a no-op because currentProject may be unset; update
handleManualVerify (and the other affected handlers) to first check that
currentProject is present and only proceed if so, then await
persistFeatureUpdate(feature.id, {...}) before calling toast.success, and handle
errors (eg. show an error toast) if persistence fails; reference the
persistFeatureUpdate function and currentProject variable and apply the same
pattern to the other handlers listed (lines ~754-766, ~947-955, ~957-984).
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@apps/ui/src/components/views/board-view/hooks/use-board-actions.ts`:
- Around line 1014-1020: The transition guard is not re-entrant: replace the
boolean markFeatureTransitioning/unmarkFeatureTransitioning behavior with a
ref-counted guard so repeated stop requests for the same feature are safe;
update the implementations used in use-board-actions (markFeatureTransitioning
and unmarkFeatureTransitioning) to increment a counter for feature.id on
markFeatureTransitioning and decrement on unmarkFeatureTransitioning (only
removing the transitioning state when the counter drops to zero), and ensure any
delayed/unmount cleanup paths call the decrementing unmark so overlapping
delayed unmarks don’t prematurely clear the guard.

In `@apps/ui/src/hooks/use-query-invalidation.ts`:
- Around line 182-187: The guard uses isAnyFeatureTransitioning() to skip
invalidation for all features, which suppresses unrelated updates; change the
logic to scope the transition check to the specific feature referenced by the
event: when FEATURE_LIST_INVALIDATION_EVENTS.includes(event.type) only skip
invalidation if the feature for this event is transitioning (e.g., call
isFeatureTransitioning(event.featureId) or modify isAnyFeatureTransitioning to
accept an id), then invalidate the cache key "features.all" for other
events/features so unrelated feature updates are not dropped.

---

Outside diff comments:
In `@apps/ui/src/components/views/board-view/hooks/use-board-actions.ts`:
- Around line 741-751: The success toast in handleManualVerify is shown even
when persistFeatureUpdate is a no-op because currentProject may be unset; update
handleManualVerify (and the other affected handlers) to first check that
currentProject is present and only proceed if so, then await
persistFeatureUpdate(feature.id, {...}) before calling toast.success, and handle
errors (eg. show an error toast) if persistence fails; reference the
persistFeatureUpdate function and currentProject variable and apply the same
pattern to the other handlers listed (lines ~754-766, ~947-955, ~957-984).

ℹ️ Review info

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between ae48065 and 387fe98.

📒 Files selected for processing (9)
  • apps/ui/src/components/views/board-view/hooks/use-board-actions.ts
  • apps/ui/src/components/views/board-view/hooks/use-board-column-features.ts
  • apps/ui/src/components/views/board-view/hooks/use-board-drag-drop.ts
  • apps/ui/src/components/views/board-view/hooks/use-board-features.ts
  • apps/ui/src/components/views/board-view/kanban-board.tsx
  • apps/ui/src/hooks/use-auto-mode.ts
  • apps/ui/src/hooks/use-query-invalidation.ts
  • apps/ui/src/lib/feature-transition-state.ts
  • apps/ui/src/store/app-store.ts

Comment on lines +1014 to +1020
// Mark this feature as transitioning so WebSocket-driven query invalidation
// (useAutoModeQueryInvalidation) skips redundant cache invalidations while
// persistFeatureUpdate is handling the optimistic update. Without this guard,
// auto_mode_error / auto_mode_stopped WS events race with the optimistic
// update and cause cache flip-flops that cascade through useBoardColumnFeatures,
// triggering React error #185 on mobile.
markFeatureTransitioning(feature.id);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Make transition guarding re-entrant for repeated stop requests.

markFeatureTransitioning(feature.id) + delayed unmark is overlap-unsafe: repeated stop actions for the same feature can clear the flag while another transition is still active, re-enabling WS invalidations too early.

Ref-counted guard approach
 const MAX_DUPLICATES = 50;
+const stopTransitionRefCounts = new Map<string, number>();

 ...
-      markFeatureTransitioning(feature.id);
+      const count = stopTransitionRefCounts.get(feature.id) ?? 0;
+      stopTransitionRefCounts.set(feature.id, count + 1);
+      if (count === 0) {
+        markFeatureTransitioning(feature.id);
+      }

 ...
-        setTimeout(() => unmarkFeatureTransitioning(feature.id), 500);
+        setTimeout(() => {
+          const next = (stopTransitionRefCounts.get(feature.id) ?? 1) - 1;
+          if (next <= 0) {
+            stopTransitionRefCounts.delete(feature.id);
+            unmarkFeatureTransitioning(feature.id);
+          } else {
+            stopTransitionRefCounts.set(feature.id, next);
+          }
+        }, 500);

Also applies to: 1059-1065

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/ui/src/components/views/board-view/hooks/use-board-actions.ts` around
lines 1014 - 1020, The transition guard is not re-entrant: replace the boolean
markFeatureTransitioning/unmarkFeatureTransitioning behavior with a ref-counted
guard so repeated stop requests for the same feature are safe; update the
implementations used in use-board-actions (markFeatureTransitioning and
unmarkFeatureTransitioning) to increment a counter for feature.id on
markFeatureTransitioning and decrement on unmarkFeatureTransitioning (only
removing the transitioning state when the counter drops to zero), and ensure any
delayed/unmount cleanup paths call the decrementing unmark so overlapping
delayed unmarks don’t prematurely clear the guard.

Comment on lines +182 to +187
// Invalidate feature list for lifecycle events.
// Skip invalidation when a feature is mid-transition (e.g., being cancelled)
// because persistFeatureUpdate already handles the optimistic cache update.
// Without this guard, auto_mode_error / auto_mode_stopped WS events race
// with the optimistic update and cause re-render cascades on mobile (React #185).
if (FEATURE_LIST_INVALIDATION_EVENTS.includes(event.type) && !isAnyFeatureTransitioning()) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Global transition guard can suppress unrelated feature invalidations.

At Line 187, using isAnyFeatureTransitioning() blocks features.all invalidation for all events while any feature is transitioning. That can drop updates for other features and leave stale board state.

🛠️ Suggested fix (scope the guard to the event’s feature only)
--- a/apps/ui/src/hooks/use-query-invalidation.ts
+++ b/apps/ui/src/hooks/use-query-invalidation.ts
@@
-import { isAnyFeatureTransitioning } from '@/lib/feature-transition-state';
+import { isFeatureTransitioning } from '@/lib/feature-transition-state';
@@
-      if (FEATURE_LIST_INVALIDATION_EVENTS.includes(event.type) && !isAnyFeatureTransitioning()) {
+      const shouldSkipFeatureListInvalidation =
+        hasFeatureId(event) && isFeatureTransitioning(event.featureId);
+      if (
+        FEATURE_LIST_INVALIDATION_EVENTS.includes(event.type) &&
+        !shouldSkipFeatureListInvalidation
+      ) {
         queryClient.invalidateQueries({
           queryKey: queryKeys.features.all(currentProjectPath),
         });
       }
--- a/apps/ui/src/lib/feature-transition-state.ts
+++ b/apps/ui/src/lib/feature-transition-state.ts
@@
 export function isAnyFeatureTransitioning(): boolean {
   return transitioningFeatures.size > 0;
 }
+
+export function isFeatureTransitioning(featureId: string): boolean {
+  return transitioningFeatures.has(featureId);
+}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/ui/src/hooks/use-query-invalidation.ts` around lines 182 - 187, The
guard uses isAnyFeatureTransitioning() to skip invalidation for all features,
which suppresses unrelated updates; change the logic to scope the transition
check to the specific feature referenced by the event: when
FEATURE_LIST_INVALIDATION_EVENTS.includes(event.type) only skip invalidation if
the feature for this event is transitioning (e.g., call
isFeatureTransitioning(event.featureId) or modify isAnyFeatureTransitioning to
accept an id), then invalidate the cache key "features.all" for other
events/features so unrelated feature updates are not dropped.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
apps/ui/src/hooks/use-auto-mode.ts (1)

517-525: ⚠️ Potential issue | 🟠 Major

Transition guard is scoped by branch only; include project scope too.

Line 517 and Line 558 gate transition behavior using only eventBranchName === branchName. Since this listener is global, events from another project on the same branch (especially null main worktree) can incorrectly clear/suppress this hook’s transition state.

Proposed fix: gate by both project and branch
@@
+      const isCurrentHookWorktreeEvent =
+        eventProjectId === projectId && eventBranchName === branchName;
@@
-            if (isRestartTransitionRef.current && eventBranchName === branchName) {
+            if (isRestartTransitionRef.current && isCurrentHookWorktreeEvent) {
@@
-            if (eventBranchName === branchName && isTransitioningRef.current) {
+            if (isCurrentHookWorktreeEvent && isTransitioningRef.current) {

Also applies to: 558-566

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/ui/src/hooks/use-auto-mode.ts` around lines 517 - 525, The transition
guard currently only checks eventBranchName === branchName (in the block around
isRestartTransitionRef.current and the other block at lines ~558-566); update
both guards to also validate the project scope by checking eventProjectName ===
projectName (i.e., require eventProjectName === projectName && eventBranchName
=== branchName) so the listener ignores events from other projects (including
null/main worktree collisions); modify the conditions in the blocks that
reference isRestartTransitionRef.current and isTransitioningRef.current
accordingly.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@apps/ui/src/hooks/use-auto-mode.ts`:
- Around line 221-239: The current batchedAddAutoModeActivity collects
activities but still calls addAutoModeActivity for each item, so it doesn't
batch Zustand set() calls; change batchedAddAutoModeActivity to, when the flush
timer fires, call a new store action addAutoModeActivities(batch) once (passing
the entire batch) instead of looping and invoking addAutoModeActivity for each
item, and implement addAutoModeActivities(activities: AutoModeActivity[]) inside
the app-store (apps/ui/src/store/app-store.ts) to append all activities in a
single set(...) call; keep pendingActivitiesRef and flushTimerRef logic the same
but replace the per-item flush loop with a single call to addAutoModeActivities.
- Around line 246-253: The cleanup currently clears flushTimerRef but drops any
buffered entries in pendingActivitiesRef; update the useEffect cleanup to drain
the pending queue before unmount by invoking the same flush routine used
elsewhere (e.g., call flushPendingActivities or the function that
processes/sends pendingActivitiesRef.current) or, if no helper exists, iterate
pendingActivitiesRef.current, process/send each entry, then clear the array and
clearTimeout(flushTimerRef.current); reference useEffect, flushTimerRef, and
pendingActivitiesRef to locate and modify the cleanup block.

---

Outside diff comments:
In `@apps/ui/src/hooks/use-auto-mode.ts`:
- Around line 517-525: The transition guard currently only checks
eventBranchName === branchName (in the block around
isRestartTransitionRef.current and the other block at lines ~558-566); update
both guards to also validate the project scope by checking eventProjectName ===
projectName (i.e., require eventProjectName === projectName && eventBranchName
=== branchName) so the listener ignores events from other projects (including
null/main worktree collisions); modify the conditions in the blocks that
reference isRestartTransitionRef.current and isTransitioningRef.current
accordingly.

ℹ️ Review info

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 30a42440-e831-4d22-afdb-292b9b22fced

📥 Commits

Reviewing files that changed from the base of the PR and between 387fe98 and 2e0423b.

📒 Files selected for processing (1)
  • apps/ui/src/hooks/use-auto-mode.ts

Comment on lines +221 to +239
// Batch addAutoModeActivity calls to reduce Zustand set() frequency.
// Without batching, each WS event (especially auto_mode_progress which fires
// rapidly during streaming) triggers a separate set() → all subscriber selectors
// re-evaluate → on mobile this overwhelms React's batching → crash.
// This batches activities in a ref and flushes them in a single set() call.
const pendingActivitiesRef = useRef<Parameters<typeof addAutoModeActivity>[0][]>([]);
const flushTimerRef = useRef<ReturnType<typeof setTimeout> | null>(null);
const batchedAddAutoModeActivity = useCallback(
(activity: Parameters<typeof addAutoModeActivity>[0]) => {
pendingActivitiesRef.current.push(activity);
if (!flushTimerRef.current) {
flushTimerRef.current = setTimeout(() => {
const batch = pendingActivitiesRef.current;
pendingActivitiesRef.current = [];
flushTimerRef.current = null;
// Flush all pending activities in a single store update
for (const act of batch) {
addAutoModeActivity(act);
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

batchedAddAutoModeActivity is not actually batching store writes.

Line 237-239 still calls addAutoModeActivity once per item, and each call performs its own Zustand set(). This keeps update count proportional to event count and undermines the stated crash mitigation.

Proposed fix: flush with a single store action call
@@
-    addAutoModeActivity,
+    addAutoModeActivities,
@@
-      addAutoModeActivity: state.addAutoModeActivity,
+      addAutoModeActivities: state.addAutoModeActivities,
@@
-  const pendingActivitiesRef = useRef<Parameters<typeof addAutoModeActivity>[0][]>([]);
+  const pendingActivitiesRef = useRef<Parameters<typeof addAutoModeActivities>[0]>([]);
@@
-    (activity: Parameters<typeof addAutoModeActivity>[0]) => {
+    (activity: Parameters<typeof addAutoModeActivities>[0][number]) => {
@@
-          // Flush all pending activities in a single store update
-          for (const act of batch) {
-            addAutoModeActivity(act);
-          }
+          // Flush all pending activities in a single store update
+          addAutoModeActivities(batch);
@@
-    [addAutoModeActivity]
+    [addAutoModeActivities]

You’ll also need a matching addAutoModeActivities(activities[]) action in apps/ui/src/store/app-store.ts that appends the whole batch inside one set(...).

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/ui/src/hooks/use-auto-mode.ts` around lines 221 - 239, The current
batchedAddAutoModeActivity collects activities but still calls
addAutoModeActivity for each item, so it doesn't batch Zustand set() calls;
change batchedAddAutoModeActivity to, when the flush timer fires, call a new
store action addAutoModeActivities(batch) once (passing the entire batch)
instead of looping and invoking addAutoModeActivity for each item, and implement
addAutoModeActivities(activities: AutoModeActivity[]) inside the app-store
(apps/ui/src/store/app-store.ts) to append all activities in a single set(...)
call; keep pendingActivitiesRef and flushTimerRef logic the same but replace the
per-item flush loop with a single call to addAutoModeActivities.

Comment on lines +246 to +253
// Cleanup flush timer on unmount
useEffect(() => {
return () => {
if (flushTimerRef.current) {
clearTimeout(flushTimerRef.current);
}
};
}, []);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Queued activities are dropped on unmount.

Line 249-251 clears the flush timer but never drains pendingActivitiesRef. Any buffered activity entries are silently lost during quick unmounts/worktree switches.

Proposed fix: flush pending queue during cleanup
-  useEffect(() => {
+  useEffect(() => {
     return () => {
       if (flushTimerRef.current) {
         clearTimeout(flushTimerRef.current);
+        flushTimerRef.current = null;
       }
+      if (pendingActivitiesRef.current.length > 0) {
+        const batch = pendingActivitiesRef.current;
+        pendingActivitiesRef.current = [];
+        // Prefer single-call batch action if available
+        for (const act of batch) addAutoModeActivity(act);
+      }
     };
-  }, []);
+  }, [addAutoModeActivity]);
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
// Cleanup flush timer on unmount
useEffect(() => {
return () => {
if (flushTimerRef.current) {
clearTimeout(flushTimerRef.current);
}
};
}, []);
// Cleanup flush timer on unmount
useEffect(() => {
return () => {
if (flushTimerRef.current) {
clearTimeout(flushTimerRef.current);
flushTimerRef.current = null;
}
if (pendingActivitiesRef.current.length > 0) {
const batch = pendingActivitiesRef.current;
pendingActivitiesRef.current = [];
// Prefer single-call batch action if available
for (const act of batch) addAutoModeActivity(act);
}
};
}, [addAutoModeActivity]);
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/ui/src/hooks/use-auto-mode.ts` around lines 246 - 253, The cleanup
currently clears flushTimerRef but drops any buffered entries in
pendingActivitiesRef; update the useEffect cleanup to drain the pending queue
before unmount by invoking the same flush routine used elsewhere (e.g., call
flushPendingActivities or the function that processes/sends
pendingActivitiesRef.current) or, if no helper exists, iterate
pendingActivitiesRef.current, process/send each entry, then clear the array and
clearTimeout(flushTimerRef.current); reference useEffect, flushTimerRef, and
pendingActivitiesRef to locate and modify the cleanup block.

@gsxdsm gsxdsm merged commit dd7108a into AutoMaker-Org:v1.0.0rc Mar 4, 2026
9 checks passed
gsxdsm added a commit to gsxdsm/automaker that referenced this pull request Mar 4, 2026
* Changes from fix/board-react-crash

* fix: Prevent cascading re-renders and crashes from high-frequency WS events
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant