Skip to content

feat(projects): stop collapsing a nested workspace into its parent repo - #8490

Closed
mfattakhov wants to merge 1 commit into
pingdotgg:mainfrom
mfattakhov:mfattakhov/nested-projects
Closed

mfattakhov wants to merge 1 commit into
pingdotgg:mainfrom
mfattakhov:mfattakhov/nested-projects

Conversation

@mfattakhov

@mfattakhov mfattakhov commented Aug 28, 2026 •

Copy link
Copy Markdown

A project is an environment-local workspace record keyed by workspaceRoot, and git identity is correlation and labelling — not routing. Default sidebar grouping does not respect that: it collapses every project sharing a git remote into one row named after the repo. Adding ~/delta/commerce-pricing next to ~/delta produces a single owner/delta row, ⌘N / "New thread in…" offers one entry, and typing commerce-pricing finds nothing. The server will happily create both projects — uniqueness is the exact path — but you can never aim an agent at the subdirectory on purpose.

Default repository grouping now keys a nested workspace separately from its parent, while sibling clones of one remote still share a row — that is the whole change on the grouping seam, and the local+remote checkout feature is untouched. Rows that would collide on the git name gain the repo-relative path as a disambiguator. Project search terms grow to cover the git display name, owner/name, the full workspace root, and every path segment, so commerce-pricing matches even when the row reads owner/delta. A new default-off setting flips the primary label from the git name to the path. Wired through the web sidebar, both command-palette project sections, Settings → Projects, and the mobile home list, thread sidebar, and grouping screen.

How I use it: I work in a GitButler workspace at ~/delta and I also need a project whose cwd is ~/delta/commerce-pricing, so file search, scripts, t3.json, and the agent session all start in the package instead of the repo root. Before this, the two collapsed into one row and one picker entry, so every thread I started for the package silently started at the repo root — and I had no way to find the package by the name I actually call it. Git operations still resolve the real toplevel, so status, PRs, and commits work unchanged from the nested cwd.

UI change note: this alters sidebar rows and adds a setting; I can attach before/after screenshots on request.

Model: Claude Opus 5 (1M context), harness: Claude Code.


Note

Medium Risk
Changes default sidebar/project picker grouping and labels across web, mobile, and shared runtime; behavior is well-tested but affects how users see and find projects daily.

Overview
Nested workspaces no longer collapse into one sidebar row. Default repository grouping now keys projects by git remote plus repo-relative path, so ~/delta and ~/delta/commerce-pricing stay distinct while sibling clones at different roots still merge. Shared client-runtime logic adds path disambiguators (owner/repo · commerce-pricing), formatProjectGroupLabel, and deriveProjectSearchTerms so web sidebar, command palette, Settings → Projects, and mobile home/thread lists stay aligned.

Adds sidebarProjectNamesUsePath (default off) on desktop/web client settings and mobile projectNamesUsePath preference, with UI toggles in General settings and mobile Project Grouping. Mobile list code now passes full ProjectGroupingSettings instead of a single grouping mode.

Documents nested projects in user docs and internals; extends tests across client-runtime, web, mobile, and contracts. Server tests assert requireActiveProjectWorkspaceRootAbsent allows creating a project nested under an existing root while still rejecting duplicate exact paths.

Reviewed by Cursor Bugbot for commit 9dc6632. Bugbot is set up for automated code reviews on this repo. Configure here.

Note

Stop collapsing nested workspaces into their parent repo across grouping modes

  • Nested workspaces inside a repository now get separate group keys in all repository grouping modes. deriveRepositoryScopedKey always appends the repo-relative path instead of short-circuiting for 'repository' mode.
  • Adds a sidebarProjectNamesUsePath setting (default false) to web and mobile settings, letting users label projects by workspace path rather than git repo name.
  • Introduces formatProjectGroupLabel and deriveProjectSearchTerms to disambiguate colliding labels with repo-relative paths and to make nested workspaces searchable by path segments.
  • Updates sidebar, command palette, settings, and mobile project grouping screens to consume a full ProjectGroupingSettings object and render formatted labels.
  • Behavioral Change: buildProjectGroups now yields separate groups for nested workspaces in repository modes; deriveRepositoryScopedKey and deriveLogicalProjectKey signatures changed (removed groupingMode param). Consumers still passing the old projectGroupingMode enum or expecting parent/nested collapse will break.
📊 Macroscope summarized 9dc6632. 25 files reviewed, 4 issues evaluated, 0 issues filtered, 4 comments posted

🗂️ Filtered Issues

A project is identified by its workspace root, but default sidebar grouping
collapsed every project sharing a git remote into one row. Adding
~/delta/commerce-pricing next to ~/delta produced a single "owner/delta" row,
the new-thread picker offered one entry, and typing the package name found
nothing — so an agent could not be aimed at the subdirectory on purpose.

Default `repository` grouping now keys a nested workspace separately from its
parent while sibling clones of one remote still share a row, which is the whole
change on the grouping seam. Rows that would collide on the git name gain the
repo-relative path as a disambiguator, project search terms grow to cover the
git owner/name, the full workspace root, and every path segment, and a new
default-off setting flips the primary label from the git name to the path.
Wired through the web sidebar, both command-palette project sections, Settings,
and the mobile home list, thread sidebar, and grouping screen.
@coderabbitai

coderabbitai Bot commented Aug 28, 2026

Copy link
Copy Markdown

Important

Review skipped

Auto reviews are disabled on this repository. Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro Plus

Run ID: adeb5cc6-bfbd-42c8-bd07-d8b3eef89237

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review

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

@github-actions github-actions Bot added vouch:unvouched PR author is not yet trusted in the VOUCHED list. size:L 100-499 changed lines (additions + deletions). labels Aug 28, 2026

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🟠 High

const rootPrefix = `${normalizedRootPath}${separator}`;

For repositories rooted at / or C:\, deriveRepositoryRelativeProjectPath returns null for nested workspaces, so they receive the unscoped canonicalKey and collapse into the repository-root group. This happens because line 66 appends another separator, testing // or c:\\; preserve an existing trailing separator when building rootPrefix.

Suggested change
const rootPrefix = normalizedRootPath.endsWith(separator)
? normalizedRootPath
: `${normalizedRootPath}${separator}`;
🤖 Copy this AI Prompt to have your agent fix this:
In file @packages/client-runtime/src/state/projectGrouping.ts around line 66:

For repositories rooted at `/` or `C:\`, `deriveRepositoryRelativeProjectPath` returns `null` for nested workspaces, so they receive the unscoped `canonicalKey` and collapse into the repository-root group. This happens because line 66 appends another separator, testing `//` or `c:\\`; preserve an existing trailing separator when building `rootPrefix`.

projectSortOrder: "updated_at",
}),
[groupingSettings.sidebarProjectGroupingMode, projects, threads],
[groupingSettings, projects, threads],

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🟡 Medium threads/new-task-flow-provider.tsx:210

projectScopes is recomputed and activity-sorted on every draft update, including each keystroke, which can make composing a new task janky for larger project/thread sets. useMobileProjectGroupingSettings() returns a new groupingSettings object on each successful render, so this dependency invalidates the useMemo even when the grouping settings have not changed; memoize the resolved settings or depend on stable setting values.

🤖 Copy this AI Prompt to have your agent fix this:
In file @apps/mobile/src/features/threads/new-task-flow-provider.tsx around line 210:

`projectScopes` is recomputed and activity-sorted on every draft update, including each keystroke, which can make composing a new task janky for larger project/thread sets. `useMobileProjectGroupingSettings()` returns a new `groupingSettings` object on each successful render, so this dependency invalidates the `useMemo` even when the grouping settings have not changed; memoize the resolved settings or depend on stable setting values.

{project.displayName}
</span>
{project.pathLabel ? (
<span className="shrink-0 truncate text-secondary-label text-[11px]">

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🟡 Medium components/LegacySidebar.tsx:2307

Long pathLabel values consume their full intrinsic width, causing the project title to disappear and the disambiguator to be clipped at the sidebar edge instead of ellipsized. Replace shrink-0 with a shrinkable constrained flex item so truncate can render an ellipsis.

🤖 Copy this AI Prompt to have your agent fix this:
In file @apps/web/src/components/LegacySidebar.tsx around line 2307:

Long `pathLabel` values consume their full intrinsic width, causing the project title to disappear and the disambiguator to be clipped at the sidebar edge instead of ellipsized. Replace `shrink-0` with a shrinkable constrained flex item so `truncate` can render an ellipsis.

if (relativePath === null) {
return lastPathSegment(project.workspaceRoot);
}
return relativePath.length === 0 ? "." : relativePath;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🟡 Medium state/projectGrouping.ts:222

Repository-root projects receive the identical disambiguator `

-  return relativePath.length === 0 ? "." : relativePath;
+  return relativePath.length === 0 ? project.workspaceRoot : relativePath;
🤖 Copy this AI Prompt to have your agent fix this:
In file @packages/client-runtime/src/state/projectGrouping.ts around line 222:

Repository-root projects receive the identical disambiguator `

@macroscopeapp macroscopeapp Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Reviewed the changed web UI surfaces for the nested-project labelling work. The primitive usage is consistent (the new General setting reuses SettingsRow + Switch + searchableSetting, and every group label now routes through formatSidebarProjectLabel). Two follow-ups below: one layout ownership issue on the new sidebar path label, and one accessible-name gap where the disambiguated label is not applied to adjacent controls.

Posted via Macroscope — UI Consistency

Comment on lines +2306 to +2310
{project.pathLabel ? (
<span className="shrink-0 truncate text-secondary-label text-[11px]">
{project.pathLabel}
</span>
) : null}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

The new path label is shrink-0, so it never yields width inside the flex row while the name span (truncate, shrink allowed) absorbs all the shrinkage. Unlike the neighbouring {count} projects badge, this text is unbounded (packages/integrations/pricing), so on a narrow sidebar the project name can truncate to nothing while the path renders in full — and truncate on a shrink-0 item never actually triggers. Making the label shrinkable keeps both parts readable and lets the path be the thing that ellipsises.

Suggested change
{project.pathLabel ? (
<span className="shrink-0 truncate text-secondary-label text-[11px]">
{project.pathLabel}
</span>
) : null}
{project.pathLabel ? (
<span className="min-w-0 shrink truncate text-secondary-label text-[11px]">
{project.pathLabel}
</span>
) : null}

Posted via Macroscope — UI Consistency

Comment on lines +3551 to +3553
<span className="min-w-0 truncate text-sm">
{formatSidebarProjectLabel(project)}
</span>

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

The visible row label is now disambiguated, but the settings button immediately below still derives its accessible name and tooltip from project.displayName. For a parent workspace and a workspace nested inside it, both menu items expose the identical name (Project settings for kosyanmedia/delta), so keyboard and screen-reader users lose the distinction the new label restores. Same pattern applies to the Create new thread in ${project.displayName} label in LegacySidebar.tsx (~line 2354).

                             <Button
-                              aria-label={`Project settings for ${project.displayName}`}
-                              title={`Project settings for ${project.displayName}`}
+                              aria-label={`Project settings for ${formatSidebarProjectLabel(project)}`}
+                              title={`Project settings for ${formatSidebarProjectLabel(project)}`}

Posted via Macroscope — UI Consistency

@macroscopeapp

macroscopeapp Bot commented Aug 28, 2026

Copy link
Copy Markdown
Contributor

Approvability

Verdict: Not approved

Macroscope's review found this PR not approvable — This 32-file change alters default project grouping and labels across shared runtime, web, and mobile navigation, while adding persisted settings and new search behavior; it is not a small mechanical or off-by-default change. An unresolved root-path grouping edge case and concrete accessibility/UI concerns further warrant human review.

Not approved because:

  • 4 blocking correctness issues found at or above your repo's Minimum Blocking Severity

Adjust the Minimum Blocking Severity for this repo — including turning it Off — in Settings. You can add or adjust custom eligibility rules. Learn more.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

size:L 100-499 changed lines (additions + deletions). vouch:unvouched PR author is not yet trusted in the VOUCHED list.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant