Skip to content

Comments

Fix crash on project selection and worktree switch#795

Merged
gsxdsm merged 1 commit intoAutoMaker-Org:v0.15.0rcfrom
gsxdsm:fix/manual-crash
Feb 22, 2026
Merged

Fix crash on project selection and worktree switch#795
gsxdsm merged 1 commit intoAutoMaker-Org:v0.15.0rcfrom
gsxdsm:fix/manual-crash

Conversation

@gsxdsm
Copy link
Collaborator

@gsxdsm gsxdsm commented Feb 22, 2026

Fix crash on project load and worktree switch

Summary by CodeRabbit

Release Notes

  • Refactor
    • Improved project switching responsiveness and smoothness through optimized state update batching
    • Enhanced rendering efficiency during worktree selection with improved reference management
    • Optimized backend synchronization timing to reduce performance cascades during project transitions
    • Streamlined caching mechanisms for faster and more efficient project state persistence

@gemini-code-assist
Copy link
Contributor

Summary of Changes

Hello @gsxdsm, 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 implements several targeted optimizations and fixes to enhance the stability and performance of the UI, particularly around project and worktree switching. By leveraging React's startTransition for state updates, stabilizing object references, debouncing API calls, and sanitizing cached data, it effectively resolves issues related to excessive re-renders and potential crashes, leading to a smoother user experience.

Highlights

  • Optimized Project Switching with React's startTransition: The project switching logic in ProjectSwitcher now utilizes React's startTransition API. This change batches state updates for setCurrentProject and navigation, preventing cascading re-renders and addressing the 'maximum update depth exceeded' error (React error fixing worktree style #185) that could occur during rapid project changes.
  • Stabilized Worktree Object References: In BoardView, the selectedWorktree object reference is now stabilized using a useRef hook. This ensures that downstream components only re-render when the actual key properties of the worktree change, rather than on every render cycle due to a new object reference, thereby mitigating further instances of React error fixing worktree style #185.
  • Debounced Auto Mode Status Refresh: The useAutoMode hook now debounces the refreshStatus API call on mount with a 150ms delay. This prevents a synchronous cascade of store updates and re-renders that could trigger React error fixing worktree style #185, allowing React to settle initial renders before additional state mutations occur.
  • Sanitized UI Cache for Worktrees: The syncUICache function now sanitizes the currentWorktreeByProject cache. It only persists entries for main branch worktrees (where path is null), preventing potential crash loops on application restore if cached worktree paths point to directories that no longer exist on disk.

🧠 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/layout/project-switcher/project-switcher.tsx
    • Imported startTransition from React.
    • Wrapped setCurrentProject and navigate calls within startTransition to batch updates and prevent cascading re-renders.
  • apps/ui/src/components/views/board-view.tsx
    • Added prevSelectedWorktreeRef using useRef to store the previous WorktreeInfo object.
    • Modified the selectedWorktree useMemo hook to return the prevSelectedWorktreeRef.current if the worktree's key properties (path, branch, isMain, isCurrent, hasWorktree) have not changed, stabilizing the object reference.
  • apps/ui/src/hooks/use-auto-mode.ts
    • Updated the useEffect hook to debounce the refreshStatus call with a 150ms setTimeout, preventing synchronous update cascades on mount or project switch.
  • apps/ui/src/store/ui-cache-store.ts
    • Implemented sanitization logic for currentWorktreeByProject in syncUICache, ensuring only main branch worktree entries (where path is null) are persisted to prevent issues with stale worktree paths.
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.

@coderabbitai
Copy link

coderabbitai bot commented Feb 22, 2026

📝 Walkthrough

Walkthrough

The PR optimizes UI performance during project and worktree switches by: batching state updates in ProjectSwitcher via startTransition, stabilizing worktree object references in BoardView to prevent cascading re-renders, debouncing the initial sync in useAutoMode, and sanitizing cached worktree persistence to main branches only.

Changes

Cohort / File(s) Summary
Component-Level Optimizations
apps/ui/src/components/layout/project-switcher/project-switcher.tsx, apps/ui/src/components/views/board-view.tsx
ProjectSwitcher now wraps state updates and navigation in startTransition for batched low-priority updates. BoardView introduces ref-based stabilization for selected worktree objects, returning previous reference when key fields (path, branch, isMain, isCurrent, hasWorktree) remain unchanged to reduce cascading re-renders.
Hook & Store Optimizations
apps/ui/src/hooks/use-auto-mode.ts, apps/ui/src/store/ui-cache-store.ts
useAutoMode now debounces initial backend sync by 150ms on mount instead of immediate invocation. ui-cache-store conditionally sanitizes currentWorktreeByProject entries, persisting only those with null paths (main-branch entries) to prevent stale per-project state restoration.

Sequence Diagram

sequenceDiagram
    actor User
    participant ProjectSwitcher
    participant startTransition
    participant Navigation
    participant BoardView as BoardView<br/>(Worktree Selector)
    participant useAutoMode
    participant UICache as UICache Store

    User->>ProjectSwitcher: Click project
    ProjectSwitcher->>startTransition: Batch state & nav
    startTransition->>ProjectSwitcher: Low-priority update
    ProjectSwitcher->>Navigation: Navigate to /board
    Navigation->>BoardView: Render with new project
    BoardView->>BoardView: Memoized worktree select<br/>with ref stabilization
    BoardView->>useAutoMode: Mount/trigger
    useAutoMode->>useAutoMode: Debounce 150ms
    useAutoMode->>UICache: refreshStatus()
    UICache->>UICache: Sanitize worktree cache<br/>(null-path only)
    UICache->>BoardView: Return stabilized ref
    BoardView->>User: Render stable worktree UI
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~20 minutes

Possibly related PRs

Suggested labels

Ready-To-Merge, scope: ui

Poem

🐰 With transitions batched, our updates stay fast,
Stabilized refs ensure no cascades blast,
Debounced syncs and sanitized stores,
The UI hops through project doors! ✨🚀

🚥 Pre-merge checks | ✅ 2 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 50.00% 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 'Fix crash on project selection and worktree switch' directly matches the main objectives—fixing crashes during project selection and worktree switching. It accurately summarizes the primary purpose of the changeset across all modified files.

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

✨ Finishing Touches
  • 📝 Generate docstrings (stacked PR)
  • 📝 Generate docstrings (commit on current branch)
🧪 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.

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 effectively addresses a crash related to cascading re-renders during project and worktree switches. The use of startTransition in the project switcher, reference stabilization in the board view, and debouncing in the useAutoMode hook are all appropriate and well-implemented solutions for breaking render loops. Additionally, the change to sanitize cached worktree data on write is a great defensive improvement that enhances application stability. I have one minor suggestion to refactor a loop for conciseness, but overall this is an excellent fix.

Comment on lines +123 to +134
const sanitized: Record<string, { path: string | null; branch: string }> = {};
for (const [projectPath, worktree] of Object.entries(appState.currentWorktreeByProject)) {
if (
typeof worktree === 'object' &&
worktree !== null &&
'path' in worktree &&
worktree.path === null
) {
sanitized[projectPath] = worktree;
}
}
update.cachedCurrentWorktreeByProject = sanitized;
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 sanitization logic is great for ensuring cache integrity. For improved conciseness and a more functional style, you could consider refactoring this loop using Object.fromEntries and Array.prototype.filter.

const sanitized = Object.fromEntries(
  Object.entries(appState.currentWorktreeByProject).filter(
    ([, worktree]) =>
      typeof worktree === 'object' &&
      worktree !== null &&
      'path' in worktree &&
      worktree.path === null
  )
);
update.cachedCurrentWorktreeByProject = sanitized;

Copy link

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

🧹 Nitpick comments (4)
apps/ui/src/components/layout/project-switcher/project-switcher.tsx (1)

152-208: handleOpenFolder navigates outside startTransition — inconsistency worth aligning.

handleOpenFolder calls upsertAndSetCurrentProject(...) (which also sets currentProject in the store) followed by navigate({ to: '/board' }) at line 200, without the transition batching added to handleProjectClick. When a user opens an existing project via the folder picker, the same store-update → navigate cascade that caused the original crash could theoretically recur.

♻️ Proposed fix for handleOpenFolder (line 200)
-        navigate({ to: '/board' });
+        startTransition(() => {
+          navigate({ to: '/board' });
+        });

Note: upsertAndSetCurrentProject fires before this point (line 177) so it remains outside the transition; only the navigation call needs to be deferred here, mirroring the pattern in handleProjectClick.

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

In `@apps/ui/src/components/layout/project-switcher/project-switcher.tsx` around
lines 152 - 208, The navigation call in handleOpenFolder runs immediately after
upsertAndSetCurrentProject, risking the same store-update→navigate race that
startTransition avoided in handleProjectClick; wrap the navigate({ to: '/board'
}) call in React's startTransition (e.g. import { startTransition } from 'react'
and call startTransition(() => navigate({ to: '/board' }))) so navigation is
deferred/batched like in handleProjectClick, keeping upsertAndSetCurrentProject
unchanged.
apps/ui/src/components/views/board-view.tsx (1)

487-487: Document the intentional stale-field invariant in the stabilization check.

The stabilization correctly returns prev when the five key fields are unchanged, preventing reference-equality churn for useAutoMode. However, optional fields spread from found (hasChanges, changedFilesCount, pr, hasConflicts, conflictType, conflictFiles) are silently frozen whenever those five fields are stable — even if the backing store data for those fields has updated.

This is safe today because none of those optional fields are consumed from selectedWorktree anywhere in board-view.tsx (the WorktreePanel sources its own detail data via refreshTrigger). But the invariant is non-obvious and creates a hidden trap for future readers.

📝 Proposed inline documentation
     // Return the previous reference if the key fields haven't changed,
     // preventing downstream hooks from seeing a "new" worktree on every render.
+    //
+    // NOTE: Only the five identity fields (path, branch, isMain, isCurrent,
+    // hasWorktree) are included in this check. Optional fields spread from the
+    // store (hasChanges, pr, hasConflicts, etc.) will NOT update while these
+    // five fields are stable. This is intentional — useAutoMode (the only
+    // consumer of selectedWorktree) does not read those fields. If future code
+    // needs hasChanges/pr from selectedWorktree, either add those fields to this
+    // check or source them directly from the `worktrees` array.
     const prev = prevSelectedWorktreeRef.current;

Also applies to: 521-535

🤖 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.tsx` at line 487, The stabilization
logic around prevSelectedWorktreeRef intentionally returns the previous object
when the five core identity fields are unchanged, which freezes optional spread
fields (hasChanges, changedFilesCount, pr, hasConflicts, conflictType,
conflictFiles) even if their backing store updates; add an explicit comment next
to the stabilization block referencing prevSelectedWorktreeRef and
selectedWorktree explaining this stale-field invariant, why it exists (to
prevent reference-equality churn for useAutoMode), that WorktreePanel consumes
up-to-date detail data via refreshTrigger, and warn future maintainers not to
rely on optional fields from selectedWorktree (also apply the same explanatory
comment to the analogous block around the logic at the other stabilization
site).
apps/ui/src/store/ui-cache-store.ts (1)

116-134: Consider extracting the duplicate sanitization predicate into a shared helper.

The path === null filtering loop added here (lines 123–133) is structurally identical to the one already present in restoreFromUICache (lines 192–207). If the filtering rule ever changes (e.g., a future migration retains a specific set of non-null paths), both branches must be updated in lockstep.

♻️ Proposed extraction
+/**
+ * Strips any entries whose worktree `path` is non-null (i.e. points to a
+ * directory that may no longer exist on disk). Only main-branch entries
+ * (path === null) are safe to persist/restore across app restarts.
+ */
+function filterMainBranchWorktrees(
+  input: Record<string, { path: string | null; branch: string }>
+): Record<string, { path: string | null; branch: string }> {
+  const result: Record<string, { path: string | null; branch: string }> = {};
+  for (const [projectPath, worktree] of Object.entries(input)) {
+    if (
+      typeof worktree === 'object' &&
+      worktree !== null &&
+      'path' in worktree &&
+      worktree.path === null
+    ) {
+      result[projectPath] = worktree;
+    }
+  }
+  return result;
+}

Then in syncUICache:

-    const sanitized: Record<string, { path: string | null; branch: string }> = {};
-    for (const [projectPath, worktree] of Object.entries(appState.currentWorktreeByProject)) {
-      if (
-        typeof worktree === 'object' &&
-        worktree !== null &&
-        'path' in worktree &&
-        worktree.path === null
-      ) {
-        sanitized[projectPath] = worktree;
-      }
-    }
-    update.cachedCurrentWorktreeByProject = sanitized;
+    update.cachedCurrentWorktreeByProject = filterMainBranchWorktrees(appState.currentWorktreeByProject);

And replace the equivalent block in restoreFromUICache (lines 192–207) with a call to the same helper.

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

In `@apps/ui/src/store/ui-cache-store.ts` around lines 116 - 134, The duplicate
sanitization logic that filters currentWorktreeByProject by worktree.path ===
null appears in syncUICache and restoreFromUICache; extract it into a single
helper (e.g., sanitizeCurrentWorktreeByProject or isPersistableWorktree) and
replace the loop in both syncUICache and restoreFromUICache to call that helper
so the predicate and map-building live in one place (ensure the helper accepts
the original Record<string, any> and returns Record<string, { path: string |
null; branch: string }> preserving the exact semantics).
apps/ui/src/hooks/use-auto-mode.ts (1)

265-268: Extract the 150 debounce delay to a named constant.

AUTO_MODE_POLLING_INTERVAL (line 20) sets the pattern for named timing constants. Keeping 150 inline is a minor inconsistency, and co-locating it with the polling interval makes both easier to adjust.

♻️ Proposed change
+const AUTO_MODE_INITIAL_SYNC_DELAY_MS = 150;
 const AUTO_MODE_POLLING_INTERVAL = 30000;
-    const timer = setTimeout(() => void refreshStatus(), 150);
+    const timer = setTimeout(() => void refreshStatus(), AUTO_MODE_INITIAL_SYNC_DELAY_MS);
🤖 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 265 - 268, Extract the
literal 150ms debounce into a named constant (e.g. AUTO_MODE_DEBOUNCE_MS)
located alongside the existing AUTO_MODE_POLLING_INTERVAL, then replace the
inline 150 in the useEffect timeout call with that constant so the useEffect
hook (which calls refreshStatus via setTimeout) uses the new named debounce
value.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Nitpick comments:
In `@apps/ui/src/components/layout/project-switcher/project-switcher.tsx`:
- Around line 152-208: The navigation call in handleOpenFolder runs immediately
after upsertAndSetCurrentProject, risking the same store-update→navigate race
that startTransition avoided in handleProjectClick; wrap the navigate({ to:
'/board' }) call in React's startTransition (e.g. import { startTransition }
from 'react' and call startTransition(() => navigate({ to: '/board' }))) so
navigation is deferred/batched like in handleProjectClick, keeping
upsertAndSetCurrentProject unchanged.

In `@apps/ui/src/components/views/board-view.tsx`:
- Line 487: The stabilization logic around prevSelectedWorktreeRef intentionally
returns the previous object when the five core identity fields are unchanged,
which freezes optional spread fields (hasChanges, changedFilesCount, pr,
hasConflicts, conflictType, conflictFiles) even if their backing store updates;
add an explicit comment next to the stabilization block referencing
prevSelectedWorktreeRef and selectedWorktree explaining this stale-field
invariant, why it exists (to prevent reference-equality churn for useAutoMode),
that WorktreePanel consumes up-to-date detail data via refreshTrigger, and warn
future maintainers not to rely on optional fields from selectedWorktree (also
apply the same explanatory comment to the analogous block around the logic at
the other stabilization site).

In `@apps/ui/src/hooks/use-auto-mode.ts`:
- Around line 265-268: Extract the literal 150ms debounce into a named constant
(e.g. AUTO_MODE_DEBOUNCE_MS) located alongside the existing
AUTO_MODE_POLLING_INTERVAL, then replace the inline 150 in the useEffect timeout
call with that constant so the useEffect hook (which calls refreshStatus via
setTimeout) uses the new named debounce value.

In `@apps/ui/src/store/ui-cache-store.ts`:
- Around line 116-134: The duplicate sanitization logic that filters
currentWorktreeByProject by worktree.path === null appears in syncUICache and
restoreFromUICache; extract it into a single helper (e.g.,
sanitizeCurrentWorktreeByProject or isPersistableWorktree) and replace the loop
in both syncUICache and restoreFromUICache to call that helper so the predicate
and map-building live in one place (ensure the helper accepts the original
Record<string, any> and returns Record<string, { path: string | null; branch:
string }> preserving the exact semantics).

@gsxdsm gsxdsm merged commit dfa7190 into AutoMaker-Org:v0.15.0rc Feb 22, 2026
9 checks passed
@gsxdsm gsxdsm deleted the fix/manual-crash branch February 22, 2026 01:32
gsxdsm added a commit to gsxdsm/automaker that referenced this pull request Feb 23, 2026
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