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}
/>
);