Skip to content

feat(projects): relocate project when directory moved#1941

Open
webdo wants to merge 4 commits intogeneralaction:mainfrom
webdo:feat/relocate-project
Open

feat(projects): relocate project when directory moved#1941
webdo wants to merge 4 commits intogeneralaction:mainfrom
webdo:feat/relocate-project

Conversation

@webdo
Copy link
Copy Markdown

@webdo webdo commented May 9, 2026

Summary

  • Adds Relocate Project so a project whose directory was moved on disk can be re-pointed to a new path without losing tasks, settings, or worktrees.
  • Surfaces the action in two places:
    • Project not found panel — primary Relocate Project button (with folder-input icon) + secondary Remove Project link.
    • Sidebar context menu — only when the local project is currently in path_not_found state (hidden for healthy projects, hidden for SSH).
  • Backend: new relocateLocalProject(projectId, newPath) RPC. Validates the new path is a directory + git repo, guards against the unique-path constraint colliding with another project, closes the existing provider (and surfaces teardown failures), recomputes baseRef, updates the DB row (with !row guard against concurrent deletion), returns the updated LocalProject. Renderer remounts the project after the swap.
  • Errors from the sidebar flow surface via useToast (success + destructive variants) instead of being swallowed by console.error.
  • Local projects only for now; SSH path-not-found state isn't currently surfaced anywhere in the UI and can be added in a follow-up.

Screenshots

image image

Commits

  • feat(projects): relocate project when directory moved
  • fix(projects): address greptile review on relocate!row guard, inspect closeProject Result, toast-based error surface in sidebar
  • fix(projects): only show sidebar Relocate when path-not-found

Test plan

  • Local project, move its directory on disk, reopen emdash → "Project not found" panel renders.
  • Click Relocate Project, pick the new directory → project remounts at the new path; tasks and worktrees preserved.
  • Right-click a path-not-found local project in the sidebar → Relocate Project appears; relocate flow works the same.
  • Right-click a healthy local project in the sidebar → Relocate Project is hidden.
  • Pick a directory that isn't a git repo → user-visible error, no DB mutation.
  • Pick a path already used by another registered project → path-conflict error, no DB mutation.
  • Untested: SSH project context — Relocate is not exposed for SSH; SSH path-not-found is not currently surfaced in the UI.

Local gate

format, lint, typecheck, test all pass on the changed files. Pre-existing upstream failures in app-menu-events.tsx, featurebase-issue-provider.test.ts, and legacy-port/importers/relational/relational.test.ts are unrelated and present on upstream/main without this change.

Adds Relocate action in path-not-found panel and sidebar context menu.
Validates new path is a git repo, recomputes baseRef, preserves project
id and tasks.
@greptile-apps
Copy link
Copy Markdown
Contributor

greptile-apps Bot commented May 9, 2026

Greptile Summary

This PR adds a Relocate Project feature that lets users re-point a local project to a new directory after it has been moved on disk, preserving tasks, settings, and worktrees. The backend validates the new path as a git repo, guards against path conflicts, closes the existing provider, and updates the DB row; the renderer transitions project state and remounts.

  • relocateProject.ts: New RPC operation that sequences git validation → collision check → closeProject → DB update → return updated LocalProject.
  • main-panel.tsx: Replaces the lone "Remove Project" link on the path-not-found panel with a primary Relocate button plus inline error display and a secondary Remove link.
  • project-item.tsx / project-manager.ts: Wires the same relocation flow into the sidebar context menu for healthy local projects and adds the corresponding store method.

Confidence Score: 3/5

Needs fixes in the backend operation and the sidebar error path before merging.

The backend closes the provider then updates the DB without guarding the .returning() result — a race with a concurrent deletion causes an untyped TypeError that escapes the Result boundary, leaving the project closed with stale DB state. The sidebar context menu also swallows relocation errors silently, giving users no feedback on failure.

src/main/core/projects/operations/relocateProject.ts and src/renderer/features/sidebar/project-item.tsx

Important Files Changed

Filename Overview
src/main/core/projects/operations/relocateProject.ts New backend RPC: validates path, checks for git repo, guards path conflicts, closes provider, then updates DB — but row from .returning() is unguarded (undefined if deleted concurrently) and closeProject's error result is silently discarded.
src/renderer/features/projects/stores/project-manager.ts Adds relocateLocalProject store method that calls the RPC, transitions project state, awaits any in-flight mount, then remounts — state management mirrors the existing updateProjectConnection pattern and looks correct.
src/renderer/features/projects/components/main-panel/main-panel.tsx Adds Relocate button to the path-not-found panel with loading state, inline error display, and a now-secondary Remove link — solid UX upgrade for the primary recovery flow.
src/renderer/features/sidebar/project-item.tsx Adds Relocate context menu item for local projects, but errors are only logged to console — users get no visual feedback when relocation fails from the sidebar.
src/main/core/projects/controller.ts Registers relocateLocalProject in the RPC controller — trivial wiring change.

Sequence Diagram

sequenceDiagram
    participant U as User
    participant UI as Renderer (Panel / Sidebar)
    participant Store as ProjectManagerStore
    participant RPC as IPC/RPC
    participant BE as relocateLocalProject (main)
    participant PM as ProjectManager (main)
    participant DB as Database

    U->>UI: Click Relocate Project
    UI->>UI: openSelectDirectoryDialog to newPath
    UI->>Store: relocateLocalProject(projectId, newPath)
    Store->>RPC: rpc.projects.relocateLocalProject(projectId, newPath)
    RPC->>BE: relocateLocalProject(projectId, newPath)
    BE->>DB: getProjectById(projectId)
    BE->>BE: checkIsValidDirectory(newPath)
    BE->>BE: git.detectInfo() to rootPath, baseRef, isGitRepo
    BE->>DB: "SELECT id WHERE path = resolvedPath (collision check)"
    BE->>BE: resolveBaseRef(git, gitInfo.baseRef)
    BE->>PM: closeProject(projectId)
    PM-->>BE: ok / err (result discarded)
    BE->>DB: "UPDATE projects SET path, baseRef WHERE id = projectId RETURNING"
    DB-->>BE: row (may be undefined in race)
    BE-->>RPC: ok(LocalProject)
    RPC-->>Store: result
    Store->>Store: runInAction to transition state to opening
    Store->>Store: await inFlight mount (if any)
    Store->>Store: mountProject(projectId)
    Store-->>UI: done
    UI-->>U: Project remounted at new path
Loading
Prompt To Fix All With AI
Fix the following 3 code review issues. Work through them one at a time, proposing concise fixes.

---

### Issue 1 of 3
src/main/core/projects/operations/relocateProject.ts:88-106
**Unguarded `row` after UPDATE `.returning()`**

The `db.update(...).returning()` call can return an empty array if the project row was deleted in a race between the `getProjectById` check at line 42 and this update (e.g., a concurrent `deleteProject` call). Destructuring into `const [row]` would leave `row` as `undefined`, and the immediately following `row.id` access would throw a `TypeError` that bypasses the typed `Result` return, leaving the project in a closed-but-not-relocated limbo on the backend. Add an explicit guard: if `!row`, return `err({ type: 'not-found', message: '...' })` before the `ok(...)` call.

### Issue 2 of 3
src/renderer/features/sidebar/project-item.tsx:107-108
**Relocation errors silently swallowed in sidebar**

When relocation fails from the context menu (e.g., not a git repo, path conflict, network error), the error is only sent to `console.error` and the user receives no visual feedback. In contrast, `ProjectPathNotFoundPanel` renders the error message inline. Users who trigger **Relocate Project** from the sidebar and hit a validation error will see the spinner disappear with no indication of what went wrong. The component should surface the error (e.g., a toast notification, an inline label, or a tooltip on the menu item).

### Issue 3 of 3
src/main/core/projects/operations/relocateProject.ts:86
**`closeProject` result discarded**

`projectManager.closeProject` returns a `Result<void, ProviderLifecycleError>` and can time out (60 s teardown budget) or fail. The result is awaited but never inspected, so a teardown failure is silently ignored and the code proceeds to mutate the DB with the old provider potentially still running. A check like `const closeResult = await projectManager.closeProject(projectId); if (!closeResult.ok) { /* log or surface the error */ }` would at minimum make teardown failures observable.

Reviews (1): Last reviewed commit: "feat(projects): relocate project when di..." | Re-trigger Greptile

Comment thread src/main/core/projects/operations/relocateProject.ts
Comment thread src/renderer/features/sidebar/project-item.tsx Outdated
Comment thread src/main/core/projects/operations/relocateProject.ts Outdated
Webdo added 3 commits May 9, 2026 00:34
Guard !row after db.update().returning(), inspect closeProject Result
and return error on teardown failure, surface relocate errors via toast
in sidebar instead of console.error.
Hides the Relocate context-menu item for healthy local projects.
Run `git worktree repair` from the new project root after a successful
DB update. Fixes the bidirectional .git pointers when the main repo
moves but the worktrees stay at their original location. Failure is
non-fatal — logged and ignored.
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