diff --git a/src/components/chat/ChatHeader.tsx b/src/components/chat/ChatHeader.tsx
index c2dc6cb..7a75080 100644
--- a/src/components/chat/ChatHeader.tsx
+++ b/src/components/chat/ChatHeader.tsx
@@ -20,11 +20,12 @@ import { IpcClient } from "@/ipc/ipc_client";
import { useRouter } from "@tanstack/react-router";
import { selectedChatIdAtom } from "@/atoms/chatAtoms";
import { useChats } from "@/hooks/useChats";
-import { showError } from "@/lib/toast";
+import { showError, showSuccess } from "@/lib/toast";
import { useEffect } from "react";
import { useStreamChat } from "@/hooks/useStreamChat";
import { useCurrentBranch } from "@/hooks/useCurrentBranch";
import { useCheckoutVersion } from "@/hooks/useCheckoutVersion";
+import { useRenameBranch } from "@/hooks/useRenameBranch";
interface ChatHeaderProps {
isVersionPaneOpen: boolean;
@@ -53,6 +54,7 @@ export function ChatHeader({
} = useCurrentBranch(appId);
const { checkoutVersion, isCheckingOutVersion } = useCheckoutVersion();
+ const { renameBranch, isRenamingBranch } = useRenameBranch();
useEffect(() => {
if (appId) {
@@ -65,6 +67,14 @@ export function ChatHeader({
await checkoutVersion({ appId, versionId: "main" });
};
+ const handleRenameMasterToMain = async () => {
+ if (!appId) return;
+ // If this throws, it will automatically show an error toast
+ await renameBranch({ oldBranchName: "master", newBranchName: "main" });
+
+ showSuccess("Master branch renamed to main");
+ };
+
const handleNewChat = async () => {
if (appId) {
try {
@@ -127,14 +137,27 @@ export function ChatHeader({
{branchInfoLoading && Checking branch...}
-
+ {currentBranchName === "master" ? (
+
+ ) : (
+
+ )}
)}
diff --git a/src/hooks/useRenameBranch.ts b/src/hooks/useRenameBranch.ts
new file mode 100644
index 0000000..e700a5a
--- /dev/null
+++ b/src/hooks/useRenameBranch.ts
@@ -0,0 +1,58 @@
+import { useMutation, useQueryClient } from "@tanstack/react-query";
+import { IpcClient } from "@/ipc/ipc_client";
+import { showError } from "@/lib/toast";
+import { selectedAppIdAtom } from "@/atoms/appAtoms";
+import { useAtomValue } from "jotai";
+
+interface RenameBranchParams {
+ appId: number;
+ oldBranchName: string;
+ newBranchName: string;
+}
+
+export function useRenameBranch() {
+ const queryClient = useQueryClient();
+ const currentAppId = useAtomValue(selectedAppIdAtom);
+
+ const mutation = useMutation({
+ mutationFn: async (params: RenameBranchParams) => {
+ if (params.appId === null || params.appId === undefined) {
+ throw new Error("App ID is required to rename a branch.");
+ }
+ if (!params.oldBranchName) {
+ throw new Error("Old branch name is required.");
+ }
+ if (!params.newBranchName) {
+ throw new Error("New branch name is required.");
+ }
+ await IpcClient.getInstance().renameBranch(params);
+ },
+ onSuccess: (_, variables) => {
+ // Invalidate queries that depend on branch information
+ queryClient.invalidateQueries({
+ queryKey: ["currentBranch", variables.appId],
+ });
+ queryClient.invalidateQueries({
+ queryKey: ["versions", variables.appId],
+ });
+ // Potentially show a success message or trigger other actions
+ },
+ meta: {
+ showErrorToast: true,
+ },
+ });
+
+ const renameBranch = async (params: Omit) => {
+ if (!currentAppId) {
+ showError("No application selected.");
+ return;
+ }
+ return mutation.mutateAsync({ ...params, appId: currentAppId });
+ };
+
+ return {
+ renameBranch,
+ isRenamingBranch: mutation.isPending,
+ renameBranchError: mutation.error,
+ };
+}
diff --git a/src/ipc/handlers/app_handlers.ts b/src/ipc/handlers/app_handlers.ts
index 71332ba..5344437 100644
--- a/src/ipc/handlers/app_handlers.ts
+++ b/src/ipc/handlers/app_handlers.ts
@@ -2,7 +2,7 @@ import { ipcMain } from "electron";
import { db, getDatabasePath } from "../../db";
import { apps, chats } from "../../db/schema";
import { desc, eq } from "drizzle-orm";
-import type { App, CreateAppParams } from "../ipc_types";
+import type { App, CreateAppParams, RenameBranchParams } from "../ipc_types";
import fs from "node:fs";
import path from "node:path";
import { getDyadAppPath, getUserDataPath } from "../../paths/paths";
@@ -767,4 +767,55 @@ export function registerAppHandlers() {
const packageJson = JSON.parse(fs.readFileSync(packageJsonPath, "utf-8"));
return { version: packageJson.version };
});
+
+ handle("rename-branch", async (_, params: RenameBranchParams) => {
+ const { appId, oldBranchName, newBranchName } = params;
+ const app = await db.query.apps.findFirst({
+ where: eq(apps.id, appId),
+ });
+
+ if (!app) {
+ throw new Error("App not found");
+ }
+
+ const appPath = getDyadAppPath(app.path);
+
+ return withLock(appId, async () => {
+ try {
+ // Check if the old branch exists
+ const branches = await git.listBranches({ fs, dir: appPath });
+ if (!branches.includes(oldBranchName)) {
+ throw new Error(`Branch '${oldBranchName}' not found.`);
+ }
+
+ // Check if the new branch name already exists
+ if (branches.includes(newBranchName)) {
+ // If newBranchName is 'main' and oldBranchName is 'master',
+ // and 'main' already exists, we might want to allow this if 'main' is the current branch
+ // and just switch to it, or delete 'master'.
+ // For now, let's keep it simple and throw an error.
+ throw new Error(
+ `Branch '${newBranchName}' already exists. Cannot rename.`,
+ );
+ }
+
+ await git.renameBranch({
+ fs: fs,
+ dir: appPath,
+ oldref: oldBranchName,
+ ref: newBranchName,
+ });
+ logger.info(
+ `Branch renamed from '${oldBranchName}' to '${newBranchName}' for app ${appId}`,
+ );
+ } catch (error: any) {
+ logger.error(
+ `Failed to rename branch for app ${appId}: ${error.message}`,
+ );
+ throw new Error(
+ `Failed to rename branch '${oldBranchName}' to '${newBranchName}': ${error.message}`,
+ );
+ }
+ });
+ });
}
diff --git a/src/ipc/ipc_client.ts b/src/ipc/ipc_client.ts
index 9da44de..db98a68 100644
--- a/src/ipc/ipc_client.ts
+++ b/src/ipc/ipc_client.ts
@@ -29,6 +29,7 @@ import type {
ApproveProposalResult,
ImportAppResult,
ImportAppParams,
+ RenameBranchParams,
} from "./ipc_types";
import type { ProposalResult } from "@/lib/schemas";
import { showError } from "@/lib/toast";
@@ -816,4 +817,8 @@ export class IpcClient {
}): Promise<{ exists: boolean }> {
return this.ipcRenderer.invoke("check-app-name", params);
}
+
+ public async renameBranch(params: RenameBranchParams): Promise {
+ await this.ipcRenderer.invoke("rename-branch", params);
+ }
}
diff --git a/src/ipc/ipc_types.ts b/src/ipc/ipc_types.ts
index 6ad3f08..744cefc 100644
--- a/src/ipc/ipc_types.ts
+++ b/src/ipc/ipc_types.ts
@@ -201,3 +201,9 @@ export interface ImportAppResult {
appId: number;
chatId: number;
}
+
+export interface RenameBranchParams {
+ appId: number;
+ oldBranchName: string;
+ newBranchName: string;
+}
diff --git a/src/preload.ts b/src/preload.ts
index 4e53390..3e6110f 100644
--- a/src/preload.ts
+++ b/src/preload.ts
@@ -73,6 +73,7 @@ const validInvokeChannels = [
"check-ai-rules",
"select-app-folder",
"check-app-name",
+ "rename-branch",
] as const;
// Add valid receive channels