Skip to content

fix(frontend): auto-select newly created project#3

Open
nikzdevz wants to merge 1 commit intodevin/frontend-project-isolationfrom
devin/fix-project-autoselect-race
Open

fix(frontend): auto-select newly created project#3
nikzdevz wants to merge 1 commit intodevin/frontend-project-isolationfrom
devin/fix-project-autoselect-race

Conversation

@nikzdevz
Copy link
Copy Markdown
Owner

@nikzdevz nikzdevz commented Apr 19, 2026

Summary

Smoke-test follow-up to PR #2. Manual end-to-end testing (and Devin Review, independently — see #2 (comment)) surfaced one wiring bug: creating a new project via the sidebar modal did not auto-switch to it. The sidebar kept showing the previously selected project even though the new project was persisted and visible in the dropdown list.

Root cause — race between CreateProjectModal.onSuccess and useProjects fallback effect

  1. User fills the modal for "Bravo" and clicks Create.

  2. POST /api/projects succeeds and onSuccess fires:

    • qc.invalidateQueries({ queryKey: ['projects'] }) — triggers an async refetch of the projects list.
    • setCurrent(project.id) — sets currentProjectId = <Bravo id> synchronously.
  3. React re-renders. useProjects still has the stale cached list [Alpha] in query.data (the refetch hasn't landed yet).

  4. Its effect runs with the new currentId = <Bravo id> but list = [Alpha]:

    if (!currentId || !list.some((p) => p.id === currentId)) {
      setCurrent(list[0].id);   // resets back to Alpha
    }

    The "not-in-list" branch fires and overwrites the Bravo selection with Alpha's id.

  5. The refetch finally lands with [Alpha, Bravo], but currentId is now Alpha again — the effect's guard passes and the bug is locked in.

Fix

Two small defensive changes; either alone would close the race:

  1. useProjects.ts — the fallback effect now only defaults when there is no selection at all (currentId == null). It no longer reacts to a stale list by clobbering a valid selection. If the user's selected project is later deleted, ProjectSelector already falls back to the "Select project" label.
  2. CreateProjectModal.tsx — on successful create, synchronously seed the projects query cache via qc.setQueryData before calling setCurrent, then invalidate. This eliminates the stale-list window entirely.

Evidence from the smoke test

Recorded flow: create Alpha, add task-in-alpha, create Bravo from the modal. Expected: sidebar flips to "Bravo". Actual on devin/frontend-project-isolation: sidebar stays on "Alpha", and Bravo only appears in the dropdown list:

bug screenshot

Full recording with on-screen annotations: https://app.devin.ai/attachments/aa8ea7d1-5481-43e7-a1b5-945815741bb7/rec-89d9e14b-b59c-404f-a19b-306835613a2c-edited.mp4

Everything else PR #2 claimed tested green during the smoke test:

  • Manual project switch via the dropdown works; sidebar label flips correctly.
  • Per-project task lists are scoped; creating a task under Bravo does not appear under Alpha.
  • /api/stats counts flip on project switch.
  • Selection persists in localStorage (obsmcp:project) across reload.
  • /api/knowledge-graph?project_id=… filters correctly (tested by seeding one knowledge node under Alpha via sqlite3; Alpha returns it, Bravo returns empty).
  • /api/stats no longer leaks a _scope debug field.

Base branch

This PR targets devin/frontend-project-isolation (PR #2's branch) so the diff is only the fix. Merge PR #2 first, then this; or rebase this onto main if PR #2 is squashed.

Review & Testing Checklist for Human

  • With an existing project selected, open the modal and create a second project. Sidebar label should switch to the new project immediately, and its Tasks / Dashboard should show empty state (not the previous project's items).
  • Confirm UX for a deleted currently-selected project: it will no longer be silently reassigned; sidebar shows "Select project" until the user picks one. Verify this matches your intent — if you'd rather auto-fall-back to another project on delete, add a separate effect keyed on projects that reassigns only when the current id is genuinely missing after a successful refetch.
  • In DevTools Network tab, confirm POST /api/projects still fires and the response body is used — the new setQueryData optimistic-append relies on project.id from the server.

Notes

Frontend-only changes; no backend touched, no schema changes, no CI (repo has no GitHub Actions by design). PR #2's inline Devin Review comment flagging the same race is at #2 (comment).

…effect)

Co-Authored-By: thesyperdev@gmail.com <thesyperdev@gmail.com>
Copy link
Copy Markdown

@devin-ai-integration devin-ai-integration Bot left a comment

Choose a reason for hiding this comment

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

Devin Review found 1 potential issue.

View 2 additional findings in Devin Review.

Open in Devin Review

Comment on lines +19 to 21
if (currentId == null) {
setCurrent(list[0].id);
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🔴 Removed guard allows stale project ID to persist after project deletion

The old code reset the current project when the stored ID was not found in the fetched project list: if (!currentId || !list.some((p) => p.id === currentId)). The new code only checks if (currentId == null), removing the !list.some(...) guard. If a project is deleted (via API, CLI tool, or database reset) while its ID is stored in localStorage, the currentProjectId remains a non-null stale string, so currentId == null is false and the auto-selection never fires. This causes the ProjectSelector to show "Select project" (frontend/src/components/ProjectSelector.tsx:15 finds no match), all page queries (Dashboard, Tasks, Sessions, etc.) to filter by a non-existent project ID returning empty results, and SSE events to be silently dropped at frontend/src/events/EventBus.ts:153-158 because the event's project_id won't match the stale ID.

Suggested change
if (currentId == null) {
setCurrent(list[0].id);
}
if (currentId == null || !list.some((p) => p.id === currentId)) {
setCurrent(list[0].id);
}
Open in Devin Review

Was this helpful? React with 👍 or 👎 to provide feedback.

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