From 6947f0d9d85dd92e74b13a70bd3d97a27d899506 Mon Sep 17 00:00:00 2001 From: tachytelicdetonation <47115683+tachytelicdetonation@users.noreply.github.com> Date: Tue, 13 Oct 2026 15:24:04 -0500 Subject: [PATCH] fix(web): keep clone folder names readable in the setup project picker (#12474) Co-authored-by: Claude Fable 5.1 Co-authored-by: Yash Singh --- .../components/onboarding/WelcomeWizard.tsx | 48 ++++++++++++++----- .../onboarding/projectImport.logic.test.ts | 39 +++++++++++++++ .../web/src/onboarding/projectImport.logic.ts | 20 ++++++++ 3 files changed, 96 insertions(+), 11 deletions(-) diff --git a/apps/web/src/components/onboarding/WelcomeWizard.tsx b/apps/web/src/components/onboarding/WelcomeWizard.tsx index 7da09e0b3b4711c50db5f0f31193b89fabebb79c..617962f73321ce59849537db937e40c946ffb245 100644 --- a/apps/web/src/components/onboarding/WelcomeWizard.tsx +++ b/apps/web/src/components/onboarding/WelcomeWizard.tsx @@ -47,6 +47,7 @@ import { resolveOnboardingLandingProject, resolveOnboardingProjectId, type OnboardingProjectGroup, + splitOnboardingProjectPath, } from "../../onboarding/projectImport.logic"; import { getOnboardingProviderState, @@ -1622,7 +1623,7 @@ function ImportCandidateList({ setKeys([candidate.key], checked)} @@ -1653,7 +1654,7 @@ function ImportRepositoryGroup({ onToggle([only.key], checked)} /> @@ -1682,7 +1683,7 @@ function ImportRepositoryGroup({ onToggle([candidate.key], checked)} @@ -1693,17 +1694,39 @@ function ImportRepositoryGroup({ ); } +/** + * Clones of one repository and plain folders differ only by folder name, so + * the name stays visible and the shared parent path truncates first. + */ +function ProjectPathText({ + path, + className, +}: { + readonly path: string; + readonly className?: string; +}) { + const { name, parent } = splitOnboardingProjectPath(path); + return ( + + {parent.length > 0 ? {parent} : null} + {name} + + ); +} + function ImportCandidateRow({ candidate, label, - secondary, + path, nested = false, checked, onCheckedChange, }: { readonly candidate: ImportCandidate; - readonly label: string; - readonly secondary?: string; + /** Text primary label. Nested rows omit it and are labelled by their path. */ + readonly label?: string; + /** Shown with the folder name kept visible while the parent path truncates. */ + readonly path?: string; readonly nested?: boolean; readonly checked: boolean; readonly onCheckedChange: (checked: boolean) => void; @@ -1720,11 +1743,14 @@ function ImportCandidateRow({ } > - - {label} - - {secondary !== undefined ? ( - {secondary} + {label !== undefined ? ( + {label} + ) : null} + {path !== undefined ? ( + ) : null} {candidate.path} diff --git a/apps/web/src/onboarding/projectImport.logic.test.ts b/apps/web/src/onboarding/projectImport.logic.test.ts index a675cae4a53875e6359495064c8a417c89b808da..4847b191071a48a309abce2986f91b103e314442 100644 --- a/apps/web/src/onboarding/projectImport.logic.test.ts +++ b/apps/web/src/onboarding/projectImport.logic.test.ts @@ -7,6 +7,7 @@ import { onboardingProjectKey, resolveOnboardingLandingProject, resolveOnboardingProjectId, + splitOnboardingProjectPath, } from "./projectImport.logic"; const now = Date.parse("2026-08-22T12:00:00.000Z"); @@ -349,3 +350,41 @@ describe("projects on multiple computers", () => { expect(partitionOnboardingProjects([first, second], now).recent).toEqual([first, second]); }); }); + +describe("splitOnboardingProjectPath", () => { + it("keeps sibling checkouts that share a prefix distinguishable by name", () => { + const left = splitOnboardingProjectPath("/Users/me/Projects/clients/acme/t3code"); + const right = splitOnboardingProjectPath("/Users/me/Projects/clients/acme/t3code-worktree"); + expect(left).toEqual({ name: "t3code", parent: "/Users/me/Projects/clients/acme/" }); + expect(right).toEqual({ name: "t3code-worktree", parent: "/Users/me/Projects/clients/acme/" }); + expect(left.name).not.toBe(right.name); + }); + + it("splits Windows paths and ignores a trailing separator", () => { + expect(splitOnboardingProjectPath("C:\\Users\\me\\t3code\\")).toEqual({ + name: "t3code", + parent: "C:\\Users\\me\\", + }); + }); + + it("keeps a root or bare name whole", () => { + expect(splitOnboardingProjectPath("/")).toEqual({ name: "/", parent: "" }); + expect(splitOnboardingProjectPath("/t3code")).toEqual({ name: "t3code", parent: "/" }); + expect(splitOnboardingProjectPath("t3code")).toEqual({ name: "t3code", parent: "" }); + expect(splitOnboardingProjectPath("t3code/")).toEqual({ name: "t3code", parent: "" }); + }); + + it.each(["C:\\", "C:/", "\\\\server\\share", "\\\\server\\share\\", "//server/share/"])( + "keeps the filesystem root %s whole", + (path) => { + expect(splitOnboardingProjectPath(path)).toEqual({ name: path, parent: "" }); + }, + ); + + it("splits a folder below a network share", () => { + expect(splitOnboardingProjectPath("\\\\server\\share\\project\\")).toEqual({ + name: "project", + parent: "\\\\server\\share\\", + }); + }); +}); diff --git a/apps/web/src/onboarding/projectImport.logic.ts b/apps/web/src/onboarding/projectImport.logic.ts index d7044b98b6affd5ed7d33a5ee7210cde3edc8fbe..86f34754c32efecee8fd26fa087d45e01c9cec43 100644 --- a/apps/web/src/onboarding/projectImport.logic.ts +++ b/apps/web/src/onboarding/projectImport.logic.ts @@ -135,6 +135,26 @@ export function resolveOnboardingLandingProject( return undefined; } +/** + * Splits a scan path into the folder name and the directory above it (with + * its trailing separator), so a picker row can keep the name visible and let + * the shared prefix truncate. Handles both separators because candidates + * come from any environment. + */ +export function splitOnboardingProjectPath(path: string): { + readonly name: string; + readonly parent: string; +} { + if (/^[A-Za-z]:[\\/]+$/.test(path) || /^[\\/]{2}[^\\/]+[\\/][^\\/]+[\\/]*$/.test(path)) { + return { name: path, parent: "" }; + } + const trimmed = path.replace(/[\\/]+$/, ""); + const separatorIndex = Math.max(trimmed.lastIndexOf("/"), trimmed.lastIndexOf("\\")); + if (trimmed.length === 0) return { name: path, parent: "" }; + if (separatorIndex < 0) return { name: trimmed, parent: "" }; + return { name: trimmed.slice(separatorIndex + 1), parent: trimmed.slice(0, separatorIndex + 1) }; +} + /** Paths identify projects only within the computer that owns them. */ export function onboardingProjectKey(environmentId: EnvironmentId, path: string): string { return JSON.stringify([environmentId, path]); -- 2.54.0