From e8c79bc2cda819fd266f2a8178808b72bf301c8c Mon Sep 17 00:00:00 2001 From: Thomas Nguyen Date: Thu, 12 Nov 2026 16:39:38 -0500 Subject: [PATCH 1/2] fix(client-runtime): grouped projects no longer take one folder's name as their label When a repository group's checkouts have different titles and disagree on the repository name's casing, the label fell back to the representative's title. Every thread in the group was then labelled with one subfolder's name. Fall back to the representative's repository name instead. Co-Authored-By: Claude Opus 5.5 --- .../src/state/projectGrouping.test.ts | 19 +++++++++++++++++++ .../src/state/projectGrouping.ts | 10 +++++++++- 2 files changed, 28 insertions(+), 1 deletion(-) diff --git a/packages/client-runtime/src/state/projectGrouping.test.ts b/packages/client-runtime/src/state/projectGrouping.test.ts index f4edba2b8d456..1fd75b92385a2 100644 --- a/packages/client-runtime/src/state/projectGrouping.test.ts +++ b/packages/client-runtime/src/state/projectGrouping.test.ts @@ -303,6 +303,25 @@ describe("buildProjectGroups", () => { expect(label(renamed)).toBe("t3code"); }); + it("labels a group by its repository when clones disagree on its casing", () => { + // A subfolder project, its repository root, and a clone whose remote URL is + // lowercased: no shared title or display name, but one repository. + const subfolder = makeProject("design-study", "/work/NewCo/reference/design-study"); + const root = makeProject("NewCo", "/work/NewCo"); + const lowercasedClone = makeProject("NewCo", "/remote/NewCo", { + environmentId: EnvironmentId.make("remote"), + repositoryIdentity: { ...repositoryIdentity, name: "T3CODE", displayName: "t3 code" }, + }); + + const groups = buildProjectGroups({ + projects: [subfolder, root, lowercasedClone], + settings: settings("repository"), + preferredEnvironmentId: environmentId, + }); + expect(groups).toHaveLength(1); + expect(groups[0]?.label).toBe("T3 Code"); + }); + it("applies a physical-project override without dropping its siblings", () => { const first = makeProject("t3code", "/work/t3code"); const second = makeProject("t3code-2", "/work/t3code-2"); diff --git a/packages/client-runtime/src/state/projectGrouping.ts b/packages/client-runtime/src/state/projectGrouping.ts index c05a9724ce207..1e4f6643fdf33 100644 --- a/packages/client-runtime/src/state/projectGrouping.ts +++ b/packages/client-runtime/src/state/projectGrouping.ts @@ -230,7 +230,15 @@ export function deriveProjectGroupLabel(input: { return sharedRepositoryNames[0]!; } - return input.representative.title; + // Members share a repository, so its name describes the whole group. Clones + // can disagree on its casing (each keeps its own remote URL's), and one + // member's title would label every thread with that one folder's name. + const representativeIdentity = input.representative.repositoryIdentity; + return ( + (representativeIdentity ? repositoryGroupingDisplayNameOf(representativeIdentity) : null) ?? + representativeIdentity?.name ?? + input.representative.title + ); } export interface ProjectGroupMember { From 6408be298196fb8f4691f3307f6d448f18c42b69 Mon Sep 17 00:00:00 2001 From: Julius Marminge Date: Sat, 14 Nov 2026 08:14:00 +0000 Subject: [PATCH 2/2] fix(client-runtime): subfolder groups keep their own label The repository-name fallback now applies only to whole-repository groups. A repository_path subfolder group falls back to its representative's title, as before. Co-Authored-By: Claude Opus 5.5 --- .../SettingsProjectOverviewRouteScreen.tsx | 6 ++++- .../features/settings/SettingsRouteScreen.tsx | 3 ++- .../src/state/projectGrouping.test.ts | 25 +++++++++++++++++++ .../src/state/projectGrouping.ts | 23 +++++++++++------ 4 files changed, 47 insertions(+), 10 deletions(-) diff --git a/apps/mobile/src/features/settings/SettingsProjectOverviewRouteScreen.tsx b/apps/mobile/src/features/settings/SettingsProjectOverviewRouteScreen.tsx index 89408243ef81a..05ef2af346daa 100644 --- a/apps/mobile/src/features/settings/SettingsProjectOverviewRouteScreen.tsx +++ b/apps/mobile/src/features/settings/SettingsProjectOverviewRouteScreen.tsx @@ -138,7 +138,11 @@ function ProjectOverviewContent(props: { }) { const { isRemoving, setIsRemoving, startRemoval, finishRemoval } = props; const representative = props.members[0]!; - const displayName = deriveProjectGroupLabel({ representative, members: props.members }); + const displayName = deriveProjectGroupLabel({ + groupKey: props.group.key, + representative, + members: props.members, + }); const [draftName, setDraftName] = useState(null); const [isSaving, setIsSaving] = useState(false); const threads = useThreadShells(); diff --git a/apps/mobile/src/features/settings/SettingsRouteScreen.tsx b/apps/mobile/src/features/settings/SettingsRouteScreen.tsx index 50ad81383addc..bde67d793aeb4 100644 --- a/apps/mobile/src/features/settings/SettingsRouteScreen.tsx +++ b/apps/mobile/src/features/settings/SettingsRouteScreen.tsx @@ -252,8 +252,9 @@ function useSettingsIndexSections(): SettingsListSection[] { selectedTargets.some((target) => target.environmentId === project.environmentId), ) ?? []; const projectLabel = - scopedProjectMembers.length > 0 + selectedProject && scopedProjectMembers.length > 0 ? deriveProjectGroupLabel({ + groupKey: selectedProject.key, representative: scopedProjectMembers[0]!, members: scopedProjectMembers, }) diff --git a/packages/client-runtime/src/state/projectGrouping.test.ts b/packages/client-runtime/src/state/projectGrouping.test.ts index 1fd75b92385a2..a890c852bcbf3 100644 --- a/packages/client-runtime/src/state/projectGrouping.test.ts +++ b/packages/client-runtime/src/state/projectGrouping.test.ts @@ -320,6 +320,31 @@ describe("buildProjectGroups", () => { }); expect(groups).toHaveLength(1); expect(groups[0]?.label).toBe("T3 Code"); + + // A subfolder group in repository_path mode keeps its own title. + const localIdentity = { ...repositoryIdentity, rootPath: "/work/t3code" }; + const pathGroups = buildProjectGroups({ + projects: [ + makeProject("design", "/work/t3code/design", { + title: "Design docs", + hasCustomTitle: true, + repositoryIdentity: localIdentity, + }), + makeProject("design", "/remote/t3code/design", { + environmentId: EnvironmentId.make("remote"), + repositoryIdentity: { + ...repositoryIdentity, + rootPath: "/remote/t3code", + name: "T3CODE", + displayName: "t3 code", + }, + }), + makeProject("t3code", "/work/t3code", { repositoryIdentity: localIdentity }), + ], + settings: settings("repository_path"), + preferredEnvironmentId: environmentId, + }); + expect(pathGroups.map((group) => group.label)).toEqual(["Design docs", "t3code"]); }); it("applies a physical-project override without dropping its siblings", () => { diff --git a/packages/client-runtime/src/state/projectGrouping.ts b/packages/client-runtime/src/state/projectGrouping.ts index 1e4f6643fdf33..b54342b5dfcb6 100644 --- a/packages/client-runtime/src/state/projectGrouping.ts +++ b/packages/client-runtime/src/state/projectGrouping.ts @@ -199,6 +199,7 @@ export function resolveScratchProjectScopeKey( } export function deriveProjectGroupLabel(input: { + readonly groupKey: string; readonly representative: Pick; readonly members: ReadonlyArray< Pick @@ -230,15 +231,20 @@ export function deriveProjectGroupLabel(input: { return sharedRepositoryNames[0]!; } - // Members share a repository, so its name describes the whole group. Clones - // can disagree on its casing (each keeps its own remote URL's), and one - // member's title would label every thread with that one folder's name. + // A whole-repository group is named for the repository, not one member's folder: + // clones can disagree on its casing. A subfolder group keeps its own title. const representativeIdentity = input.representative.repositoryIdentity; - return ( - (representativeIdentity ? repositoryGroupingDisplayNameOf(representativeIdentity) : null) ?? - representativeIdentity?.name ?? - input.representative.title - ); + if ( + representativeIdentity && + input.groupKey === repositoryGroupingKeyOf(representativeIdentity) + ) { + return ( + repositoryGroupingDisplayNameOf(representativeIdentity) ?? + representativeIdentity.name ?? + input.representative.title + ); + } + return input.representative.title; } export interface ProjectGroupMember { @@ -377,6 +383,7 @@ export function buildProjectGroups(input: { ? "No project" : members.length > 1 ? deriveProjectGroupLabel({ + groupKey: key, representative, members: members.map((member) => member.project), })