Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
50 changes: 37 additions & 13 deletions apps/web/src/components/onboarding/WelcomeWizard.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@ import {
resolveOnboardingLandingProject,
resolveOnboardingProjectId,
type OnboardingProjectGroup,
splitOnboardingProjectPath,
} from "../../onboarding/projectImport.logic";
import {
getOnboardingProviderState,
Expand Down Expand Up @@ -1352,7 +1353,7 @@ function ImportCandidateList({
<ImportCandidateRow
key={candidate.key}
candidate={candidate}
label={candidate.path}
path={candidate.path}
nested
checked={selectedKeys.has(candidate.key)}
onCheckedChange={(checked) => setKeys([candidate.key], checked)}
Expand Down Expand Up @@ -1383,7 +1384,7 @@ function ImportRepositoryGroup({
<ImportCandidateRow
candidate={only}
label={group.label}
{...(group.repository === null ? {} : { secondary: only.path })}
path={only.path}
checked={selectedKeys.has(only.key)}
onCheckedChange={(checked) => onToggle([only.key], checked)}
/>
Expand Down Expand Up @@ -1412,7 +1413,7 @@ function ImportRepositoryGroup({
<ImportCandidateRow
key={candidate.key}
candidate={candidate}
label={candidate.path}
path={candidate.path}
nested
checked={selectedKeys.has(candidate.key)}
onCheckedChange={(checked) => onToggle([candidate.key], checked)}
Expand All @@ -1423,17 +1424,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 (
<span className={cn("flex min-w-0 items-baseline font-mono", className)}>
{parent.length > 0 ? <span className="truncate">{parent}</span> : null}
<span className="max-w-full shrink-0 truncate">{name}</span>
</span>
);
}

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;
Expand All @@ -1450,13 +1473,14 @@ function ImportCandidateRow({
<TooltipTrigger
render={<span className="flex min-w-0 flex-1 items-baseline gap-2 truncate" />}
>
<span className={cn("truncate", nested ? "font-mono text-xs" : "text-sm font-medium")}>
{label}
</span>
{secondary !== undefined ? (
<span className="truncate font-mono text-[11px] text-muted-foreground">
{secondary}
</span>
{label !== undefined ? (
<span className="truncate text-sm font-medium">{label}</span>
) : null}
{path !== undefined ? (
<ProjectPathText
path={path}
className={label === undefined ? "text-xs" : "text-[11px] text-muted-foreground"}
/>
) : null}
</TooltipTrigger>
<TooltipPopup className="max-w-96 break-all font-mono">{candidate.path}</TooltipPopup>
Expand Down
39 changes: 39 additions & 0 deletions apps/web/src/onboarding/projectImport.logic.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import {
onboardingProjectKey,
resolveOnboardingLandingProject,
resolveOnboardingProjectId,
splitOnboardingProjectPath,
} from "./projectImport.logic";

const now = Date.parse("2026-08-22T12:00:00.000Z");
Expand Down Expand Up @@ -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\\",
});
});
});
20 changes: 20 additions & 0 deletions apps/web/src/onboarding/projectImport.logic.ts
Original file line number Diff line number Diff line change
Expand Up @@ -135,6 +135,26 @@ export function resolveOnboardingLandingProject<T>(
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]);
Expand Down
Loading