feat: group linked git worktrees under their main repo in sidebar#650
feat: group linked git worktrees under their main repo in sidebar#650truongsinh wants to merge 6 commits intositeboon:mainfrom
Conversation
When Claude Code runs in a git worktree, the session is stored under a
different project path in ~/.claude/projects/, causing cloudcli to show
it as an unrelated project.
Changes:
- server/projects.js: add resolveWorktreeInfo() which calls
`git worktree list --porcelain` to detect linked worktrees and
returns { isWorktree, mainRepoPath, worktreeBranch }. Applied to all
projects (both directory-scanned and manually added).
- src/types/app.ts: extend Project interface with isWorktree,
mainRepoPath, worktreeBranch optional fields.
- SidebarProjectList: group worktree projects under their main repo
using a useMemo-computed map. Worktrees whose main repo is not in the
project list continue to appear as standalone items.
- SidebarWorktreeItem: new component rendering a linked worktree as an
indented sub-item with a branch icon, connector line, branch name, and
its own session list. Full terminal and git-diff support is unaffected
because both already use the actual worktree path as cwd.
|
Note Reviews pausedIt looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the Use the following commands to manage reviews:
Use the checkboxes below for quick actions:
📝 WalkthroughWalkthroughDetects and classifies Git worktrees on the server, adds an authenticated API to create worktrees, and updates the frontend to group and render linked worktrees under their main repositories with UI to create and manage worktrees. Changes
Sequence DiagramsequenceDiagram
participant User as Browser UI
participant Frontend as React Frontend
participant Server as Server (index.js / projects.js)
participant Git as Git CLI
User->>Frontend: open NewWorktreeDialog & submit branchName
Frontend->>Server: POST /api/projects/:projectName/worktrees { branchName }
Server->>Git: git -C <mainRepo> rev-parse --verify <branch>
alt branch exists
Server->>Git: git worktree add <worktreePath> <branch>
else branch missing
Server->>Git: git worktree add -b <branch> <worktreePath>
end
Git-->>Server: success / failure
Server->>Server: addProjectManually(worktreePath)
Server-->>Frontend: { success, worktreePath, project }
Frontend->>Frontend: refreshProjects() and group worktrees under main repo
Frontend-->>User: render Sidebar with new SidebarWorktreeItem
Possibly related PRs
Suggested reviewers
Poem
🚥 Pre-merge checks | ✅ 2 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (2 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches🧪 Generate unit tests (beta)
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. Comment |
There was a problem hiding this comment.
Actionable comments posted: 3
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@server/projects.js`:
- Around line 424-428: The returned mainRepoPath should be normalized so
frontend lookups match project.fullPath; replace the raw mainWorktree.path with
a resolved path using path.resolve(mainWorktree.path) when constructing the
object in the function that returns { isWorktree: true, mainRepoPath: ...,
worktreeBranch: ... } (use the same module's path import or require if missing),
ensuring mainRepoPath is canonical for SidebarProjectList.tsx's
byPath.get(p.mainRepoPath) lookup.
In `@src/components/sidebar/view/subcomponents/SidebarWorktreeItem.tsx`:
- Around line 154-166: The delete button's group-hover styles never trigger
because the ancestor clickable row element in SidebarWorktreeItem (the row
container element that wraps the project item and handles selection/clicks) is
missing the Tailwind "group" class; add "group" to that container's className so
the nested elements using "group-hover:flex" and "group-hover:opacity-100" (the
delete button wrapper and button) become visible on hover.
- Line 179: The mobile onClick currently calls toggle which only
expands/collapses; change the handler to use the same select-and-toggle behavior
as desktop by replacing the mobile onClick={toggle} with
onClick={selectAndToggle} (use the existing selectAndToggle function referenced
on line ~128 in SidebarWorktreeItem) so tapping a worktree on mobile both
selects the project and toggles its expansion.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Repository UI
Review profile: CHILL
Plan: Pro
Run ID: 0a9de1a6-7702-46f4-b8e3-6c5c67a8d011
📒 Files selected for processing (4)
server/projects.jssrc/components/sidebar/view/subcomponents/SidebarProjectList.tsxsrc/components/sidebar/view/subcomponents/SidebarWorktreeItem.tsxsrc/types/app.ts
src/components/sidebar/view/subcomponents/SidebarWorktreeItem.tsx
Outdated
Show resolved
Hide resolved
- Normalize mainRepoPath with path.resolve() so byPath.get() lookups match project.fullPath which is also resolved - Add 'group' class to desktop row container so group-hover delete button visibility works correctly - Mobile onClick: use selectAndToggle instead of toggle so tapping a worktree both selects it and expands it, matching desktop behavior
- POST /api/projects/:projectName/worktrees endpoint: validates branch name, computes sibling path, runs git worktree add, registers project - createWorktree() API helper in src/utils/api.js - NewWorktreeDialog modal: branch name input, spinner, error display - GitBranch button on non-worktree SidebarProjectItem (desktop + mobile) - Dialog wired in Sidebar.tsx; on success refreshes projects and opens a new session in the new worktree automatically
There was a problem hiding this comment.
Actionable comments posted: 3
🧹 Nitpick comments (2)
src/utils/api.js (1)
107-111: Consider URL-encodingprojectNameto handle special characters.The
projectNameis interpolated directly into the URL path. If the project name contains characters that need URL encoding (e.g., spaces,#,%), the request could fail or be misrouted.♻️ Proposed fix
createWorktree: (projectName, branchName) => - authenticatedFetch(`/api/projects/${projectName}/worktrees`, { + authenticatedFetch(`/api/projects/${encodeURIComponent(projectName)}/worktrees`, { method: 'POST', body: JSON.stringify({ branchName }), }),Note: This is a broader pattern issue - other methods like
sessions(),renameProject(), anddeleteSession()also don't encodeprojectName. Consider addressing this consistently across the API module if special characters are expected in project names.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/utils/api.js` around lines 107 - 111, The createWorktree call interpolates projectName directly into the URL; update calls that build path segments (e.g., createWorktree, sessions, renameProject, deleteSession) to URL-encode projectName using encodeURIComponent before interpolation so special characters (spaces, #, %, etc.) are safe, e.g., ensure the value passed into authenticatedFetch path is encodeURIComponent(projectName) everywhere in this API module to keep behavior consistent; locate these functions and replace direct ${projectName} usage with an encoded variable prior to calling authenticatedFetch.src/components/sidebar/view/subcomponents/NewWorktreeDialog.tsx (1)
68-69: Consider using translation function for hardcoded UI strings.The component receives
t(translation function) as a prop but doesn't use it. Several strings are hardcoded in English:
- "New worktree — ..." (line 69)
- Description paragraph (lines 83-84)
- "Branch name" label (line 89)
- Placeholder "feature/my-feature" (line 98)
- Helper text (line 104)
- "Cancel" and "Create worktree" buttons (lines 118, 134)
If internationalization is important for this codebase, these should use
t().Also applies to: 82-85, 88-89, 103-105, 118-118, 134-134
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/components/sidebar/view/subcomponents/NewWorktreeDialog.tsx` around lines 68 - 69, The NewWorktreeDialog component is using hardcoded English strings despite receiving the translation function prop t; replace all literal UI text in NewWorktreeDialog (the header "New worktree — {project.displayName}", the description paragraph, the "Branch name" label, the placeholder "feature/my-feature", the helper text, and the "Cancel" / "Create worktree" button labels) with calls to t() using appropriate translation keys (e.g. t('newWorktree.title', { name: project.displayName }) for the header) and interpolation for project.displayName, update placeholders and helperText to use t(...) as well, and ensure the same keys are added to the locale files or passed through your i18n namespace so translations are available.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@server/index.js`:
- Around line 651-659: The Promise wrapping the spawn call that runs git with
worktreeArgs (involving spawn, worktreeArgs, mainRepoPath) lacks an 'error'
event handler so the promise can hang if spawning fails; fix it by adding a
proc.on('error', e => reject(e)) (or reject(new Error(...))) handler alongside
the existing stderr/close listeners so any spawn-level errors immediately reject
the promise and include the error details in the rejection.
- Around line 638-645: The promise that wraps the spawn call for git rev-parse
uses the local variable check but doesn't handle the spawn 'error' event, so the
promise can hang if the git process fails to start; update the Promise around
spawn('git', ['rev-parse', '--verify', branch], { cwd: mainRepoPath }) (variable
check) to attach check.on('error', err => reject(err)) in addition to the
'close' handler so OS-level spawn failures reject the Promise and propagate into
the surrounding try/catch.
In `@src/components/sidebar/view/Sidebar.tsx`:
- Around line 129-143: handleWorktreeCreated closes over a stale projects array
so the new worktree can't be found after window.refreshProjects(); fix by
ensuring you read the fresh projects inside the .then() instead of the captured
value — either update the component to maintain a mutable projectsRef (e.g.,
sync projects into projectsRef.current via useEffect and use projectsRef.current
inside handleWorktreeCreated's .then()), or change window.refreshProjects() to
return the updated projects array and use that result to find wt; keep the
existing calls to handleProjectSelect and onNewSession once you locate wt.
---
Nitpick comments:
In `@src/components/sidebar/view/subcomponents/NewWorktreeDialog.tsx`:
- Around line 68-69: The NewWorktreeDialog component is using hardcoded English
strings despite receiving the translation function prop t; replace all literal
UI text in NewWorktreeDialog (the header "New worktree — {project.displayName}",
the description paragraph, the "Branch name" label, the placeholder
"feature/my-feature", the helper text, and the "Cancel" / "Create worktree"
button labels) with calls to t() using appropriate translation keys (e.g.
t('newWorktree.title', { name: project.displayName }) for the header) and
interpolation for project.displayName, update placeholders and helperText to use
t(...) as well, and ensure the same keys are added to the locale files or passed
through your i18n namespace so translations are available.
In `@src/utils/api.js`:
- Around line 107-111: The createWorktree call interpolates projectName directly
into the URL; update calls that build path segments (e.g., createWorktree,
sessions, renameProject, deleteSession) to URL-encode projectName using
encodeURIComponent before interpolation so special characters (spaces, #, %,
etc.) are safe, e.g., ensure the value passed into authenticatedFetch path is
encodeURIComponent(projectName) everywhere in this API module to keep behavior
consistent; locate these functions and replace direct ${projectName} usage with
an encoded variable prior to calling authenticatedFetch.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Repository UI
Review profile: CHILL
Plan: Pro
Run ID: 28c2a9ad-8470-4e17-8e98-3593997e009c
📒 Files selected for processing (6)
server/index.jssrc/components/sidebar/view/Sidebar.tsxsrc/components/sidebar/view/subcomponents/NewWorktreeDialog.tsxsrc/components/sidebar/view/subcomponents/SidebarProjectItem.tsxsrc/components/sidebar/view/subcomponents/SidebarProjectList.tsxsrc/utils/api.js
🚧 Files skipped from review as they are similar to previous changes (1)
- src/components/sidebar/view/subcomponents/SidebarProjectList.tsx
- Add error event handlers to both git spawn calls in create-worktree endpoint so hung promises are rejected if git binary is not found - Fix stale closure in handleWorktreeCreated: use a projectsRef that tracks the latest projects prop so post-refresh lookup finds the new worktree correctly
There was a problem hiding this comment.
🧹 Nitpick comments (2)
server/index.js (2)
667-670: Consider sanitizing error messages in the response.The raw
error.messageis returned to the client, which could potentially leak filesystem paths or system details. While this endpoint is authenticated, consider returning a generic message and logging the detailed error server-side only:} catch (error) { console.error('Error creating worktree:', error); - res.status(500).json({ error: error.message }); + res.status(500).json({ error: 'Failed to create worktree' }); }This is a minor hardening suggestion; the current approach is consistent with other endpoints in this file.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@server/index.js` around lines 667 - 670, In the catch block that currently logs and returns error.message (the "Error creating worktree" catch), stop exposing error.message to the client: keep detailed logging server-side (console.error or processLogger.error with the full error/stack) and change the response to a generic 500 JSON like { error: 'Failed to create worktree' } (or similar). Locate the catch handling around the worktree creation route in server/index.js and replace res.status(500).json({ error: error.message }) with a generic message while preserving the existing detailed server log.
621-625: Consider validating thatworktreePathfalls within an allowed workspace area.The endpoint creates the worktree as a sibling to
mainRepoPath, but there's no validation that this location is within an allowed workspace boundary (likeWORKSPACES_ROOT). If a project was manually added from an arbitrary filesystem location, the worktree could be created in sensitive areas.Other endpoints in this file (e.g.,
browse-filesystem,create-folder) usevalidateWorkspacePathto enforce boundaries. Consider applying similar validation here:const worktreePath = path.join(path.dirname(mainRepoPath), `${repoName}-${safeSuffix}`); + // Validate worktree path is within allowed workspace + const validation = await validateWorkspacePath(worktreePath); + if (!validation.valid) { + return res.status(403).json({ error: validation.error }); + } + // Check if the worktree path already exists🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@server/index.js` around lines 621 - 625, The constructed worktreePath (derived from mainRepoPath, repoName and safeSuffix) is not validated against the allowed workspace boundary; call the existing validateWorkspacePath function (the same one used by browse-filesystem/create-folder) on worktreePath (or otherwise check it is inside WORKSPACES_ROOT) before creating the worktree and return/throw a 4xx error when validation fails so you never create a worktree outside the allowed workspace area.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Nitpick comments:
In `@server/index.js`:
- Around line 667-670: In the catch block that currently logs and returns
error.message (the "Error creating worktree" catch), stop exposing error.message
to the client: keep detailed logging server-side (console.error or
processLogger.error with the full error/stack) and change the response to a
generic 500 JSON like { error: 'Failed to create worktree' } (or similar).
Locate the catch handling around the worktree creation route in server/index.js
and replace res.status(500).json({ error: error.message }) with a generic
message while preserving the existing detailed server log.
- Around line 621-625: The constructed worktreePath (derived from mainRepoPath,
repoName and safeSuffix) is not validated against the allowed workspace
boundary; call the existing validateWorkspacePath function (the same one used by
browse-filesystem/create-folder) on worktreePath (or otherwise check it is
inside WORKSPACES_ROOT) before creating the worktree and return/throw a 4xx
error when validation fails so you never create a worktree outside the allowed
workspace area.
ℹ️ Review info
⚙️ Run configuration
Configuration used: Repository UI
Review profile: CHILL
Plan: Pro
Run ID: ac9ee1e4-526b-4c94-90b6-fc10216189bb
📒 Files selected for processing (2)
server/index.jssrc/components/sidebar/view/Sidebar.tsx
…rror, stale closure - Validate worktreePath against WORKSPACES_ROOT before creating worktree - Return generic 500 message to client instead of raw error.message - Fix stale-closure bug: store pendingWorktreePath in state and navigate via useEffect once projects list updates with the new worktree
There was a problem hiding this comment.
Actionable comments posted: 1
♻️ Duplicate comments (1)
server/index.js (1)
644-647:⚠️ Potential issue | 🟠 MajorAttach
errorhandlers to both Git child processes.If
gitcannot be spawned orcwdis invalid, these promises never settle because they only listen forclose. That leaves the HTTP request hanging instead of failing fast.🛠️ Suggested patch
try { await new Promise((resolve, reject) => { const check = spawn('git', ['rev-parse', '--verify', branch], { cwd: mainRepoPath }); + check.on('error', reject); check.on('close', code => code === 0 ? resolve() : reject()); }); branchExists = true; } catch { branchExists = false; @@ await new Promise((resolve, reject) => { const proc = spawn('git', worktreeArgs, { cwd: mainRepoPath }); let stderr = ''; proc.stderr.on('data', d => { stderr += d.toString(); }); + proc.on('error', reject); proc.on('close', code => { if (code === 0) resolve(); else reject(new Error(stderr.trim() || `git worktree add exited with code ${code}`)); }); });#!/bin/bash rg -n -C2 "spawn\\('git'|\\.on\\('error'" server/index.jsExpected result: both
spawn('git', ...)blocks appear without a nearby.on('error', ...), confirming the request can hang on spawn-level failures.Also applies to: 657-665
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@server/index.js` around lines 644 - 647, The Promise wrapping the Git child process only listens for 'close', so if spawn fails the Promise never settles; update the promise that creates the child assigned to check (spawn('git', ['rev-parse', '--verify', branch], ...)) to also attach an .on('error', err => reject(err)) handler, and do the same for the other git spawn block later (the second spawn('git', ...) around the 657-665 area) so both child processes reject their promises on spawn errors and cannot leave the HTTP request hanging.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@server/index.js`:
- Around line 628-631: The handler incorrectly checks
workspaceValidation.isValid (which is undefined) after calling
validateWorkspacePath(); change the condition to use the correct property
workspaceValidation.valid (i.e., if (!workspaceValidation.valid) return
res.status(400).json(...)) so the endpoint only rejects when the helper
indicates the path is invalid; update any related variable uses of isValid to
valid in this function (e.g., the worktree creation branch) to ensure the
endpoint can proceed when validation passes.
---
Duplicate comments:
In `@server/index.js`:
- Around line 644-647: The Promise wrapping the Git child process only listens
for 'close', so if spawn fails the Promise never settles; update the promise
that creates the child assigned to check (spawn('git', ['rev-parse', '--verify',
branch], ...)) to also attach an .on('error', err => reject(err)) handler, and
do the same for the other git spawn block later (the second spawn('git', ...)
around the 657-665 area) so both child processes reject their promises on spawn
errors and cannot leave the HTTP request hanging.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Repository UI
Review profile: CHILL
Plan: Pro
Run ID: 5b750c22-01f3-404d-b6ae-58d09aab15ea
📒 Files selected for processing (2)
server/index.jssrc/components/sidebar/view/Sidebar.tsx
| const workspaceValidation = await validateWorkspacePath(worktreePath); | ||
| if (!workspaceValidation.isValid) { | ||
| return res.status(400).json({ error: 'Worktree path is outside the allowed workspace area' }); | ||
| } |
There was a problem hiding this comment.
Use valid, not isValid, from validateWorkspacePath().
Line 629 checks a field this helper does not return. As written, workspaceValidation.isValid is undefined, so this branch always returns 400 and the endpoint never creates a worktree.
🐛 Minimal fix
- if (!workspaceValidation.isValid) {
+ if (!workspaceValidation.valid) {
return res.status(400).json({ error: 'Worktree path is outside the allowed workspace area' });
}📝 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.
| const workspaceValidation = await validateWorkspacePath(worktreePath); | |
| if (!workspaceValidation.isValid) { | |
| return res.status(400).json({ error: 'Worktree path is outside the allowed workspace area' }); | |
| } | |
| const workspaceValidation = await validateWorkspacePath(worktreePath); | |
| if (!workspaceValidation.valid) { | |
| return res.status(400).json({ error: 'Worktree path is outside the allowed workspace area' }); | |
| } |
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@server/index.js` around lines 628 - 631, The handler incorrectly checks
workspaceValidation.isValid (which is undefined) after calling
validateWorkspacePath(); change the condition to use the correct property
workspaceValidation.valid (i.e., if (!workspaceValidation.valid) return
res.status(400).json(...)) so the endpoint only rejects when the helper
indicates the path is invalid; update any related variable uses of isValid to
valid in this function (e.g., the worktree creation branch) to ensure the
endpoint can proceed when validation passes.
When a project has linked git worktrees, the "new session" provider selection screen now shows a row of branch pills above the heading. Clicking a pill switches the active project to that worktree so the session starts in the correct directory. Threads projects + onProjectSelect from AppContent → MainContent → ChatInterface → ChatMessagesPane → ProviderSelectionEmptyState.
Problem
When Claude Code (or any other agent) runs inside a git linked worktree, the session is stored under a different path in
~/.claude/projects/, so cloudcli treats it as a completely unrelated project. Users who rely onclaude mcp/EnterWorktreefor parallel workstreams end up with a fragmented sidebar.Solution
Detect worktrees on the server and surface grouping metadata to the frontend, which then renders linked worktrees as indented sub-items beneath their main repo.
Backend (
server/projects.js)resolveWorktreeInfo(projectPath): runsgit -C <path> worktree list --porcelain(fast, no network) and returns{ isWorktree, mainRepoPath, worktreeBranch }for each project path. Falls back silently for non-git directories.Frontend
src/types/app.ts: addisWorktree?,mainRepoPath?,worktreeBranch?toProject.SidebarProjectList:useMemo-groupfilteredProjectsbymainRepoPath. Worktrees whose main repo is present in the list are promoted to sub-items; orphan worktrees (main repo not loaded) continue to appear as standalone entries.SidebarWorktreeItem(new): renders a linked worktree as an indented sub-item with:GitBranchicon + branch name badgeSidebarProjectSessions)Terminal & Git diff — no changes needed
Both features already use the actual worktree
projectPathascwd:server/index.jsline ~1824):pty.spawn(shell, ..., { cwd: resolvedProjectPath })— spawns in the worktree dir, so the shell opens on the right branch.server/routes/git.js): callsgit rev-parse --show-toplevelwith the worktree path, which returns the worktree root. All diff/status/stage/commit operations run scoped to that worktree's branch.Screenshots
Worktrees now appear indented under their main repo with a branch badge:
Testing
git worktree add ../my-repo-feature feature-branchmy-repofeature-branch, notmainSummary by CodeRabbit