Skip to content
Merged
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
61 changes: 35 additions & 26 deletions apps/desktop/src/lib/trpc/routers/workspaces/procedures/create.ts
Original file line number Diff line number Diff line change
Expand Up @@ -267,6 +267,7 @@ export const createCreateProcedures = () => {
branchName: z.string().optional(),
baseBranch: z.string().optional(),
useExistingBranch: z.boolean().optional(),
applyPrefix: z.boolean().optional().default(true),
}),
)
.mutation(async ({ input }) => {
Expand Down Expand Up @@ -302,30 +303,36 @@ export const createCreateProcedures = () => {
const { local, remote } = await listBranches(project.mainRepoPath);
const existingBranches = [...local, ...remote];

const globalSettings = localDb.select().from(settings).get();
const projectOverrides = project.branchPrefixMode != null;
const prefixMode = projectOverrides
? project.branchPrefixMode
: (globalSettings?.branchPrefixMode ?? "none");
const customPrefix = projectOverrides
? project.branchPrefixCustom
: globalSettings?.branchPrefixCustom;

const rawPrefix = await getBranchPrefix({
repoPath: project.mainRepoPath,
mode: prefixMode,
customPrefix,
});
const rawAuthorPrefix = rawPrefix
? sanitizeAuthorPrefix(rawPrefix)
: undefined;
let branchPrefix: string | undefined;
if (input.applyPrefix) {
const globalSettings = localDb.select().from(settings).get();
const projectOverrides = project.branchPrefixMode != null;
const prefixMode = projectOverrides
? project.branchPrefixMode
: (globalSettings?.branchPrefixMode ?? "none");
const customPrefix = projectOverrides
? project.branchPrefixCustom
: globalSettings?.branchPrefixCustom;

const rawPrefix = await getBranchPrefix({
repoPath: project.mainRepoPath,
mode: prefixMode,
customPrefix,
});
const sanitizedPrefix = rawPrefix
? sanitizeAuthorPrefix(rawPrefix)
: undefined;

const existingSet = new Set(
existingBranches.map((b) => b.toLowerCase()),
);
const prefixWouldCollide =
rawAuthorPrefix && existingSet.has(rawAuthorPrefix.toLowerCase());
const authorPrefix = prefixWouldCollide ? undefined : rawAuthorPrefix;
const existingSet = new Set(
existingBranches.map((b) => b.toLowerCase()),
);
const prefixWouldCollide =
sanitizedPrefix && existingSet.has(sanitizedPrefix.toLowerCase());
branchPrefix = prefixWouldCollide ? undefined : sanitizedPrefix;
}

const withPrefix = (name: string): string =>
branchPrefix ? `${branchPrefix}/${name}` : name;

let branch: string;
if (existingBranchName) {
Expand All @@ -336,10 +343,12 @@ export const createCreateProcedures = () => {
}
branch = existingBranchName;
} else if (input.branchName?.trim()) {
const sanitized = sanitizeBranchName(input.branchName);
branch = authorPrefix ? `${authorPrefix}/${sanitized}` : sanitized;
branch = withPrefix(sanitizeBranchName(input.branchName));
} else {
branch = generateBranchName({ existingBranches, authorPrefix });
branch = generateBranchName({
existingBranches,
authorPrefix: branchPrefix,
});
}

const worktreePath = join(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -47,17 +47,8 @@ import {
} from "shared/utils/branch";
import { ExistingWorktreesList } from "./components/ExistingWorktreesList";

function generateBranchFromTitle({
title,
prefix,
}: {
title: string;
prefix: string | null;
}): string {
const slug = sanitizeSegment(title);
if (!slug) return "";

return prefix ? `${prefix}/${slug}` : slug;
function generateSlugFromTitle(title: string): string {
return sanitizeSegment(title);
}

type Mode = "existing" | "new" | "cloud";
Expand Down Expand Up @@ -139,13 +130,16 @@ export function NewWorkspaceModal() {
setBaseBranch(null);
}, [selectedProjectId]);

const generatedBranchName = generateBranchFromTitle({
title,
prefix: resolvedPrefix,
});
const branchNameToCreate = branchNameEdited
const branchSlug = branchNameEdited
? sanitizeBranchName(branchName)
: generatedBranchName;
: generateSlugFromTitle(title);

const applyPrefix = !branchNameEdited;

const branchPreview =
branchSlug && applyPrefix && resolvedPrefix
? `${resolvedPrefix}/${branchSlug}`
: branchSlug;
Comment on lines +133 to +142
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.

⚠️ Potential issue | 🟡 Minor

Keep applyPrefix/preview aligned with backend collision logic and empty edits.

Right now applyPrefix depends only on branchNameEdited. If a user clears the branch field (without blur), applyPrefix becomes false and the backend will generate an unprefixed branch. Also, the backend drops the prefix when it collides with an existing branch, but the preview still shows it. Consider factoring in empty slug and prefix collisions to keep UI and backend consistent.

Proposed adjustment (frontend-only)
 	const branchSlug = branchNameEdited
 		? sanitizeBranchName(branchName)
 		: generateSlugFromTitle(title);

-	const applyPrefix = !branchNameEdited;
+	const prefixWouldCollide = useMemo(() => {
+		if (!resolvedPrefix || !branchData?.branches?.length) return false;
+		const existingSet = new Set(
+			branchData.branches.map((b) => b.name.toLowerCase()),
+		);
+		return existingSet.has(resolvedPrefix.toLowerCase());
+	}, [resolvedPrefix, branchData?.branches]);
+
+	const applyPrefix = (!branchNameEdited || !branchSlug) && !prefixWouldCollide;

 	const branchPreview =
 		branchSlug && applyPrefix && resolvedPrefix
 			? `${resolvedPrefix}/${branchSlug}`
 			: branchSlug;
📝 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.

Suggested change
const branchSlug = branchNameEdited
? sanitizeBranchName(branchName)
: generatedBranchName;
: generateSlugFromTitle(title);
const applyPrefix = !branchNameEdited;
const branchPreview =
branchSlug && applyPrefix && resolvedPrefix
? `${resolvedPrefix}/${branchSlug}`
: branchSlug;
const branchSlug = branchNameEdited
? sanitizeBranchName(branchName)
: generateSlugFromTitle(title);
const prefixWouldCollide = useMemo(() => {
if (!resolvedPrefix || !branchData?.branches?.length) return false;
const existingSet = new Set(
branchData.branches.map((b) => b.name.toLowerCase()),
);
return existingSet.has(resolvedPrefix.toLowerCase());
}, [resolvedPrefix, branchData?.branches]);
const applyPrefix = (!branchNameEdited || !branchSlug) && !prefixWouldCollide;
const branchPreview =
branchSlug && applyPrefix && resolvedPrefix
? `${resolvedPrefix}/${branchSlug}`
: branchSlug;
🤖 Prompt for AI Agents
In `@apps/desktop/src/renderer/components/NewWorkspaceModal/NewWorkspaceModal.tsx`
around lines 133 - 142, The preview/prefix logic must match backend collision
and empty-edit behavior: compute branchSlug via sanitizeBranchName(branchName)
or generateSlugFromTitle(title) as you already do, then set applyPrefix to false
not only when branchNameEdited is true, but also when branchSlug is empty or
when the resolvedPrefix would be dropped by backend due to collision; i.e.
change applyPrefix to something like: !branchNameEdited && branchSlug !== '' &&
!isPrefixCollision(resolvedPrefix, branchSlug) (call an existing collision-check
util or small API/utility function that mirrors backend collision logic), and
use that applyPrefix for branchPreview generation so the UI matches backend
behavior (refer to branchNameEdited, sanitizeBranchName, generateSlugFromTitle,
resolvedPrefix, branchPreview).


const resetForm = () => {
setSelectedProjectId(null);
Expand Down Expand Up @@ -229,8 +223,9 @@ export function NewWorkspaceModal() {
const result = await createWorkspace.mutateAsync({
projectId: selectedProjectId,
name: workspaceName,
branchName: branchNameToCreate || undefined,
branchName: branchSlug || undefined,
baseBranch: effectiveBaseBranch || undefined,
applyPrefix,
});

handleClose();
Expand Down Expand Up @@ -356,7 +351,7 @@ export function NewWorkspaceModal() {
<p className="text-xs text-muted-foreground flex items-center gap-1.5">
<GoGitBranch className="size-3" />
<span className="font-mono">
{branchNameToCreate || "branch-name"}
{branchPreview || "branch-name"}
</span>
<span className="text-muted-foreground/60">
from {effectiveBaseBranch}
Expand Down Expand Up @@ -386,9 +381,7 @@ export function NewWorkspaceModal() {
id="branch"
className="h-8 text-sm font-mono"
placeholder="auto-generated"
value={
branchNameEdited ? branchName : generatedBranchName
}
value={branchNameEdited ? branchName : branchPreview}
onChange={(e) =>
handleBranchNameChange(e.target.value)
}
Expand Down
Loading