diff --git a/apps/code/src/renderer/features/git-interaction/components/BranchSelector.tsx b/apps/code/src/renderer/features/git-interaction/components/BranchSelector.tsx index 02f95f7e2..05d179859 100644 --- a/apps/code/src/renderer/features/git-interaction/components/BranchSelector.tsx +++ b/apps/code/src/renderer/features/git-interaction/components/BranchSelector.tsx @@ -1,5 +1,6 @@ import { Combobox } from "@components/ui/combobox/Combobox"; import { useGitInteractionStore } from "@features/git-interaction/state/gitInteractionStore"; +import { getSuggestedBranchName } from "@features/git-interaction/utils/deriveBranchName"; import { invalidateGitBranchQueries } from "@features/git-interaction/utils/gitCacheKeys"; import { GitBranch, Plus } from "@phosphor-icons/react"; import { Flex, Spinner, Tooltip } from "@radix-ui/themes"; @@ -20,6 +21,7 @@ interface BranchSelectorProps { onBranchSelect?: (branch: string | null) => void; cloudBranches?: string[]; cloudBranchesLoading?: boolean; + taskId?: string; } export function BranchSelector({ @@ -34,6 +36,7 @@ export function BranchSelector({ onBranchSelect, cloudBranches, cloudBranchesLoading, + taskId, }: BranchSelectorProps) { const [open, setOpen] = useState(false); const trpc = useTRPC(); @@ -140,7 +143,9 @@ export function BranchSelector({ className="combobox-footer-button" onClick={() => { setOpen(false); - actions.openBranch(); + actions.openBranch( + taskId ? getSuggestedBranchName(taskId) : undefined, + ); }} > modal.openPush("publish"), "view-pr": () => viewPr(), "create-pr": () => openCreatePr(), - "branch-here": () => modal.openBranch(), + "branch-here": () => modal.openBranch(getSuggestedBranchName(taskId)), }; actionMap[id](); }; diff --git a/apps/code/src/renderer/features/git-interaction/state/gitInteractionStore.ts b/apps/code/src/renderer/features/git-interaction/state/gitInteractionStore.ts index 2396f3b01..8839fd1f7 100644 --- a/apps/code/src/renderer/features/git-interaction/state/gitInteractionStore.ts +++ b/apps/code/src/renderer/features/git-interaction/state/gitInteractionStore.ts @@ -54,7 +54,7 @@ interface GitInteractionActions { openCommit: (nextStep: CommitNextStep) => void; openPush: (mode: PushMode) => void; openPr: (defaultTitle?: string, defaultBody?: string) => void; - openBranch: () => void; + openBranch: (suggestedName?: string) => void; closeCommit: () => void; closePush: () => void; closePr: () => void; @@ -127,8 +127,12 @@ export const useGitInteractionStore = create((set) => ({ prError: null, prOpen: true, }), - openBranch: () => - set({ branchName: "", branchError: null, branchOpen: true }), + openBranch: (suggestedName) => + set({ + branchName: suggestedName ?? "", + branchError: null, + branchOpen: true, + }), closeCommit: () => set({ commitOpen: false, commitError: null }), closePush: () => set({ diff --git a/apps/code/src/renderer/features/git-interaction/utils/deriveBranchName.test.ts b/apps/code/src/renderer/features/git-interaction/utils/deriveBranchName.test.ts new file mode 100644 index 000000000..7a76b9a82 --- /dev/null +++ b/apps/code/src/renderer/features/git-interaction/utils/deriveBranchName.test.ts @@ -0,0 +1,47 @@ +import { describe, expect, it } from "vitest"; +import { deriveBranchName } from "./deriveBranchName"; + +describe("deriveBranchName", () => { + it("converts a simple title to a branch name", () => { + expect(deriveBranchName("Fix authentication login bug", "abc123")).toBe( + "posthog/fix-authentication-login-bug", + ); + }); + + it("handles special characters", () => { + expect(deriveBranchName("PostHog issue #1234", "abc123")).toBe( + "posthog/posthog-issue-1234", + ); + }); + + it("collapses consecutive dashes", () => { + expect(deriveBranchName("Fix the bug", "abc123")).toBe( + "posthog/fix-the-bug", + ); + }); + + it("strips leading and trailing dashes", () => { + expect(deriveBranchName(" Fix bug ", "abc123")).toBe("posthog/fix-bug"); + }); + + it("truncates long titles", () => { + const longTitle = + "This is a very long task title that should be truncated to a reasonable length for git"; + const result = deriveBranchName(longTitle, "abc123"); + expect(result.length).toBeLessThanOrEqual(68); // 60 slug + "posthog/" prefix + expect(result.startsWith("posthog/")).toBe(true); + expect(result.endsWith("-")).toBe(false); + }); + + it("falls back to task ID when title is empty", () => { + expect(deriveBranchName("", "abc123")).toBe("posthog/task-abc123"); + }); + + it("falls back to task ID when title is only whitespace", () => { + expect(deriveBranchName(" ", "abc123")).toBe("posthog/task-abc123"); + }); + + it("falls back to task ID when title is only special characters", () => { + expect(deriveBranchName("!@#$%", "abc123")).toBe("posthog/task-abc123"); + }); +}); diff --git a/apps/code/src/renderer/features/git-interaction/utils/deriveBranchName.ts b/apps/code/src/renderer/features/git-interaction/utils/deriveBranchName.ts new file mode 100644 index 000000000..9014b4832 --- /dev/null +++ b/apps/code/src/renderer/features/git-interaction/utils/deriveBranchName.ts @@ -0,0 +1,31 @@ +import type { Task } from "@shared/types"; +import { queryClient } from "@utils/queryClient"; + +export function deriveBranchName(title: string, fallbackId: string): string { + const slug = title + .toLowerCase() + .trim() + .replace(/[^a-z0-9-]+/g, "-") + .replace(/-{2,}/g, "-") + .replace(/^-|-$/g, "") + .slice(0, 60) + .replace(/-$/, ""); + + if (!slug) return `posthog/task-${fallbackId}`; + return `posthog/${slug}`; +} + +export function getSuggestedBranchName(taskId: string): string { + const queries = queryClient.getQueriesData({ + queryKey: ["tasks", "list"], + }); + let task: Task | undefined; + for (const [, tasks] of queries) { + task = tasks?.find((t) => t.id === taskId); + if (task) break; + } + const fallbackId = task?.task_number + ? String(task.task_number) + : (task?.slug ?? taskId); + return deriveBranchName(task?.title ?? "", fallbackId); +} diff --git a/apps/code/src/renderer/features/message-editor/components/MessageEditor.tsx b/apps/code/src/renderer/features/message-editor/components/MessageEditor.tsx index 78a47f9a3..735d773c5 100644 --- a/apps/code/src/renderer/features/message-editor/components/MessageEditor.tsx +++ b/apps/code/src/renderer/features/message-editor/components/MessageEditor.tsx @@ -32,6 +32,7 @@ interface ModeAndBranchRowProps { } | null; disabled?: boolean; isBashMode?: boolean; + taskId?: string; } function ModeAndBranchRow({ @@ -42,6 +43,7 @@ function ModeAndBranchRow({ cloudDiffStats, disabled, isBashMode, + taskId, }: ModeAndBranchRowProps) { const { currentBranch: gitBranch, diffStats } = useGitQueries( repoPath ?? undefined, @@ -113,6 +115,7 @@ function ModeAndBranchRow({ currentBranch={currentBranch} disabled={disabled} variant="ghost" + taskId={taskId} /> )} @@ -350,6 +353,7 @@ export const MessageEditor = forwardRef( cloudDiffStats={cloudDiffStats} disabled={disabled} isBashMode={isBashMode} + taskId={taskId} /> );