diff --git a/ui/desktop/src/components/ChatInput.tsx b/ui/desktop/src/components/ChatInput.tsx
index d56078bb3..fc075d943 100644
--- a/ui/desktop/src/components/ChatInput.tsx
+++ b/ui/desktop/src/components/ChatInput.tsx
@@ -10,6 +10,7 @@ import { ChatState } from '../types/chatState';
import debounce from 'lodash/debounce';
import { LocalMessageStorage } from '../utils/localMessageStorage';
import { DirSwitcher } from './bottom_menu/DirSwitcher';
+import { GitBranchIndicator } from './GitBranchIndicator';
import ModelsBottomBar from './settings/models/bottom_bar/ModelsBottomBar';
import { BottomMenuExtensionSelection } from './bottom_menu/BottomMenuExtensionSelection';
import { cn } from '../utils';
@@ -1683,6 +1684,10 @@ export default function ChatInput({
/>
)}
+ {!isBottomBarNarrow && currentWorkingDir && (
+
+ )}
+
{/* Spacer */}
diff --git a/ui/desktop/src/components/GitBranchIndicator.tsx b/ui/desktop/src/components/GitBranchIndicator.tsx
new file mode 100644
index 000000000..62d884bd2
--- /dev/null
+++ b/ui/desktop/src/components/GitBranchIndicator.tsx
@@ -0,0 +1,152 @@
+import React, { useEffect, useRef, useState } from 'react';
+import { GitBranch, Check, Search } from 'lucide-react';
+import { toastError } from '../toasts';
+import { cn } from '../utils';
+import { defineMessages, useIntl } from '../i18n';
+import {
+ DropdownMenu,
+ DropdownMenuContent,
+ DropdownMenuItem,
+ DropdownMenuSeparator,
+ DropdownMenuTrigger,
+} from './ui/dropdown-menu';
+
+const i18n = defineMessages({
+ noBranchesFound: {
+ id: 'gitBranchIndicator.noBranchesFound',
+ defaultMessage: 'No branches found',
+ },
+ searchBranches: {
+ id: 'gitBranchIndicator.searchBranches',
+ defaultMessage: 'Search branches…',
+ },
+ failedToSwitch: {
+ id: 'gitBranchIndicator.failedToSwitch',
+ defaultMessage: 'Failed to switch to {branch}',
+ },
+ uncommittedChanges: {
+ id: 'gitBranchIndicator.uncommittedChanges',
+ defaultMessage: 'You may have uncommitted changes.',
+ },
+});
+
+export const GitBranchIndicator: React.FC<{ dir: string; className?: string }> = ({
+ dir,
+ className,
+}) => {
+ const [branch, setBranch] = useState(null);
+ const [open, setOpen] = useState(false);
+ const [branches, setBranches] = useState([]);
+ const [search, setSearch] = useState('');
+ const [switching, setSwitching] = useState(false);
+ const intl = useIntl();
+ const searchRef = useRef(null);
+
+ useEffect(() => {
+ if (!dir) return;
+ let cancelled = false;
+ const refresh = () => {
+ window.electron
+ .getGitBranchInfo(dir)
+ .then((info) => {
+ if (!cancelled) setBranch(info?.branch ?? null);
+ })
+ .catch(() => {});
+ };
+ refresh();
+ window.addEventListener('focus', refresh);
+ return () => {
+ cancelled = true;
+ window.removeEventListener('focus', refresh);
+ };
+ }, [dir]);
+
+ useEffect(() => {
+ if (!open) return;
+ let cancelled = false;
+ setBranches([]);
+ setSearch('');
+ Promise.all([window.electron.getGitBranchInfo(dir), window.electron.listGitBranches(dir)])
+ .then(([info, list]) => {
+ if (!cancelled) {
+ setBranch(info?.branch ?? null);
+ setBranches(list);
+ }
+ })
+ .catch(() => {});
+ setTimeout(() => searchRef.current?.focus(), 50);
+ return () => {
+ cancelled = true;
+ };
+ }, [open, dir]);
+
+ if (!branch) return null;
+
+ const filtered = branches.filter((b) => b.toLowerCase().includes(search.toLowerCase()));
+
+ const handleSwitch = async (target: string) => {
+ if (target === branch || switching) return;
+ setSwitching(true);
+ setOpen(false);
+ const result = await window.electron
+ .switchGitBranch(dir, target)
+ .catch((err: Error) => ({ success: false, error: err.message }));
+ if (result.success) {
+ setBranch(target);
+ } else {
+ toastError({
+ title: intl.formatMessage(i18n.failedToSwitch, { branch: target }),
+ msg: result.error ?? intl.formatMessage(i18n.uncommittedChanges),
+ });
+ }
+ setSwitching(false);
+ };
+
+ return (
+
+
+
+
+
+
+ {filtered.length === 0 && (
+
+ {intl.formatMessage(i18n.noBranchesFound)}
+
+ )}
+ {filtered.map((b) => (
+
void handleSwitch(b)}>
+ {b}
+ {b === branch && }
+
+ ))}
+
+
+
+
+ setSearch(e.target.value)}
+ onKeyDown={(e) => {
+ if (e.key.length === 1) e.stopPropagation();
+ }}
+ placeholder={intl.formatMessage(i18n.searchBranches)}
+ aria-label={intl.formatMessage(i18n.searchBranches)}
+ className="flex-1 bg-transparent text-sm text-text-primary placeholder:text-text-primary/50 outline-none"
+ />
+
+
+
+ );
+};
diff --git a/ui/desktop/src/i18n/messages/de.json b/ui/desktop/src/i18n/messages/de.json
index f3f031427..b4ed93b84 100644
--- a/ui/desktop/src/i18n/messages/de.json
+++ b/ui/desktop/src/i18n/messages/de.json
@@ -932,6 +932,18 @@
"dirSwitcher.recentDirectories": {
"defaultMessage": "Zuletzt verwendete Verzeichnisse"
},
+ "gitBranchIndicator.failedToSwitch": {
+ "defaultMessage": "Wechsel zu {branch} fehlgeschlagen"
+ },
+ "gitBranchIndicator.noBranchesFound": {
+ "defaultMessage": "Keine Branches gefunden"
+ },
+ "gitBranchIndicator.searchBranches": {
+ "defaultMessage": "Branches durchsuchen…"
+ },
+ "gitBranchIndicator.uncommittedChanges": {
+ "defaultMessage": "Möglicherweise gibt es noch nicht committete Änderungen."
+ },
"elicitationRequest.accept": {
"defaultMessage": "Akzeptieren"
},
diff --git a/ui/desktop/src/i18n/messages/en.json b/ui/desktop/src/i18n/messages/en.json
index 8440e32a9..0c2ddf79a 100644
--- a/ui/desktop/src/i18n/messages/en.json
+++ b/ui/desktop/src/i18n/messages/en.json
@@ -1259,6 +1259,18 @@
"externalBackendSection.workingDirPlaceholder": {
"defaultMessage": "/home/goose/workspace"
},
+ "gitBranchIndicator.failedToSwitch": {
+ "defaultMessage": "Failed to switch to {branch}"
+ },
+ "gitBranchIndicator.noBranchesFound": {
+ "defaultMessage": "No branches found"
+ },
+ "gitBranchIndicator.searchBranches": {
+ "defaultMessage": "Search branches…"
+ },
+ "gitBranchIndicator.uncommittedChanges": {
+ "defaultMessage": "You may have uncommitted changes."
+ },
"goosehintsModal.close": {
"defaultMessage": "Close"
},
diff --git a/ui/desktop/src/i18n/messages/es.json b/ui/desktop/src/i18n/messages/es.json
index 07d3a635a..7b274e551 100644
--- a/ui/desktop/src/i18n/messages/es.json
+++ b/ui/desktop/src/i18n/messages/es.json
@@ -932,6 +932,18 @@
"dirSwitcher.recentDirectories": {
"defaultMessage": "Directorios recientes"
},
+ "gitBranchIndicator.failedToSwitch": {
+ "defaultMessage": "No se pudo cambiar a {branch}"
+ },
+ "gitBranchIndicator.noBranchesFound": {
+ "defaultMessage": "No se encontraron ramas"
+ },
+ "gitBranchIndicator.searchBranches": {
+ "defaultMessage": "Buscar ramas…"
+ },
+ "gitBranchIndicator.uncommittedChanges": {
+ "defaultMessage": "Puede que tengas cambios sin confirmar."
+ },
"elicitationRequest.accept": {
"defaultMessage": "Aceptar"
},
diff --git a/ui/desktop/src/i18n/messages/fr.json b/ui/desktop/src/i18n/messages/fr.json
index 7d72de7f4..e65755c88 100644
--- a/ui/desktop/src/i18n/messages/fr.json
+++ b/ui/desktop/src/i18n/messages/fr.json
@@ -932,6 +932,18 @@
"dirSwitcher.recentDirectories": {
"defaultMessage": "Répertoires récents"
},
+ "gitBranchIndicator.failedToSwitch": {
+ "defaultMessage": "Impossible de basculer vers {branch}"
+ },
+ "gitBranchIndicator.noBranchesFound": {
+ "defaultMessage": "Aucune branche trouvée"
+ },
+ "gitBranchIndicator.searchBranches": {
+ "defaultMessage": "Rechercher des branches…"
+ },
+ "gitBranchIndicator.uncommittedChanges": {
+ "defaultMessage": "Vous avez peut-être des modifications non validées."
+ },
"elicitationRequest.accept": {
"defaultMessage": "Accepter"
},
diff --git a/ui/desktop/src/i18n/messages/hi.json b/ui/desktop/src/i18n/messages/hi.json
index ebc3fa608..674606433 100644
--- a/ui/desktop/src/i18n/messages/hi.json
+++ b/ui/desktop/src/i18n/messages/hi.json
@@ -932,6 +932,18 @@
"dirSwitcher.recentDirectories": {
"defaultMessage": "हाल की निर्देशिकाएँ"
},
+ "gitBranchIndicator.failedToSwitch": {
+ "defaultMessage": "{branch} पर स्विच नहीं किया जा सका"
+ },
+ "gitBranchIndicator.noBranchesFound": {
+ "defaultMessage": "कोई ब्रांच नहीं मिली"
+ },
+ "gitBranchIndicator.searchBranches": {
+ "defaultMessage": "ब्रांच खोजें…"
+ },
+ "gitBranchIndicator.uncommittedChanges": {
+ "defaultMessage": "आपके कुछ बदलाव कमिट नहीं हुए हो सकते हैं।"
+ },
"elicitationRequest.accept": {
"defaultMessage": "स्वीकार करो"
},
diff --git a/ui/desktop/src/i18n/messages/id.json b/ui/desktop/src/i18n/messages/id.json
index 84e84a542..c0dba490b 100644
--- a/ui/desktop/src/i18n/messages/id.json
+++ b/ui/desktop/src/i18n/messages/id.json
@@ -932,6 +932,18 @@
"dirSwitcher.recentDirectories": {
"defaultMessage": "Direktori terbaru"
},
+ "gitBranchIndicator.failedToSwitch": {
+ "defaultMessage": "Gagal beralih ke {branch}"
+ },
+ "gitBranchIndicator.noBranchesFound": {
+ "defaultMessage": "Tidak ada branch yang ditemukan"
+ },
+ "gitBranchIndicator.searchBranches": {
+ "defaultMessage": "Cari branch…"
+ },
+ "gitBranchIndicator.uncommittedChanges": {
+ "defaultMessage": "Anda mungkin memiliki perubahan yang belum di-commit."
+ },
"elicitationRequest.accept": {
"defaultMessage": "Terima"
},
diff --git a/ui/desktop/src/i18n/messages/it.json b/ui/desktop/src/i18n/messages/it.json
index c6a8fa86a..b0d25fef3 100644
--- a/ui/desktop/src/i18n/messages/it.json
+++ b/ui/desktop/src/i18n/messages/it.json
@@ -932,6 +932,18 @@
"dirSwitcher.recentDirectories": {
"defaultMessage": "Cartelle recenti"
},
+ "gitBranchIndicator.failedToSwitch": {
+ "defaultMessage": "Impossibile passare a {branch}"
+ },
+ "gitBranchIndicator.noBranchesFound": {
+ "defaultMessage": "Nessun branch trovato"
+ },
+ "gitBranchIndicator.searchBranches": {
+ "defaultMessage": "Cerca branch…"
+ },
+ "gitBranchIndicator.uncommittedChanges": {
+ "defaultMessage": "Potresti avere modifiche non incluse in un commit."
+ },
"elicitationRequest.accept": {
"defaultMessage": "Accetta"
},
diff --git a/ui/desktop/src/i18n/messages/ja.json b/ui/desktop/src/i18n/messages/ja.json
index aa9a467fd..a0973b0bb 100644
--- a/ui/desktop/src/i18n/messages/ja.json
+++ b/ui/desktop/src/i18n/messages/ja.json
@@ -932,6 +932,18 @@
"dirSwitcher.recentDirectories": {
"defaultMessage": "最近のディレクトリ"
},
+ "gitBranchIndicator.failedToSwitch": {
+ "defaultMessage": "{branch} への切り替えに失敗しました"
+ },
+ "gitBranchIndicator.noBranchesFound": {
+ "defaultMessage": "ブランチが見つかりません"
+ },
+ "gitBranchIndicator.searchBranches": {
+ "defaultMessage": "ブランチを検索…"
+ },
+ "gitBranchIndicator.uncommittedChanges": {
+ "defaultMessage": "コミットされていない変更がある可能性があります。"
+ },
"elicitationRequest.accept": {
"defaultMessage": "承諾"
},
diff --git a/ui/desktop/src/i18n/messages/ko.json b/ui/desktop/src/i18n/messages/ko.json
index 94135c817..53a07e342 100644
--- a/ui/desktop/src/i18n/messages/ko.json
+++ b/ui/desktop/src/i18n/messages/ko.json
@@ -932,6 +932,18 @@
"dirSwitcher.recentDirectories": {
"defaultMessage": "최근 디렉터리"
},
+ "gitBranchIndicator.failedToSwitch": {
+ "defaultMessage": "{branch} 브랜치로 전환하지 못했습니다"
+ },
+ "gitBranchIndicator.noBranchesFound": {
+ "defaultMessage": "브랜치를 찾을 수 없습니다"
+ },
+ "gitBranchIndicator.searchBranches": {
+ "defaultMessage": "브랜치 검색…"
+ },
+ "gitBranchIndicator.uncommittedChanges": {
+ "defaultMessage": "커밋되지 않은 변경 사항이 있을 수 있습니다."
+ },
"elicitationRequest.accept": {
"defaultMessage": "수락"
},
diff --git a/ui/desktop/src/i18n/messages/ms.json b/ui/desktop/src/i18n/messages/ms.json
index 386271cd3..e2ed52864 100644
--- a/ui/desktop/src/i18n/messages/ms.json
+++ b/ui/desktop/src/i18n/messages/ms.json
@@ -932,6 +932,18 @@
"dirSwitcher.recentDirectories": {
"defaultMessage": "Direktori terkini"
},
+ "gitBranchIndicator.failedToSwitch": {
+ "defaultMessage": "Gagal beralih ke {branch}"
+ },
+ "gitBranchIndicator.noBranchesFound": {
+ "defaultMessage": "Tiada cabang ditemui"
+ },
+ "gitBranchIndicator.searchBranches": {
+ "defaultMessage": "Cari cabang…"
+ },
+ "gitBranchIndicator.uncommittedChanges": {
+ "defaultMessage": "Anda mungkin mempunyai perubahan yang belum dikomit."
+ },
"elicitationRequest.accept": {
"defaultMessage": "Terima"
},
diff --git a/ui/desktop/src/i18n/messages/pt.json b/ui/desktop/src/i18n/messages/pt.json
index afb03f900..f772f54a2 100644
--- a/ui/desktop/src/i18n/messages/pt.json
+++ b/ui/desktop/src/i18n/messages/pt.json
@@ -932,6 +932,18 @@
"dirSwitcher.recentDirectories": {
"defaultMessage": "Diretórios recentes"
},
+ "gitBranchIndicator.failedToSwitch": {
+ "defaultMessage": "Falha ao mudar para {branch}"
+ },
+ "gitBranchIndicator.noBranchesFound": {
+ "defaultMessage": "Nenhum ramo encontrado"
+ },
+ "gitBranchIndicator.searchBranches": {
+ "defaultMessage": "Pesquisar ramos…"
+ },
+ "gitBranchIndicator.uncommittedChanges": {
+ "defaultMessage": "Pode haver alterações sem commit."
+ },
"elicitationRequest.accept": {
"defaultMessage": "Aceitar"
},
diff --git a/ui/desktop/src/i18n/messages/ru.json b/ui/desktop/src/i18n/messages/ru.json
index 0960bd1c4..48080862b 100644
--- a/ui/desktop/src/i18n/messages/ru.json
+++ b/ui/desktop/src/i18n/messages/ru.json
@@ -932,6 +932,18 @@
"dirSwitcher.recentDirectories": {
"defaultMessage": "Недавние каталоги"
},
+ "gitBranchIndicator.failedToSwitch": {
+ "defaultMessage": "Не удалось переключиться на {branch}"
+ },
+ "gitBranchIndicator.noBranchesFound": {
+ "defaultMessage": "Ветки не найдены"
+ },
+ "gitBranchIndicator.searchBranches": {
+ "defaultMessage": "Поиск веток…"
+ },
+ "gitBranchIndicator.uncommittedChanges": {
+ "defaultMessage": "Возможно, у вас есть незакоммиченные изменения."
+ },
"elicitationRequest.accept": {
"defaultMessage": "Принять"
},
diff --git a/ui/desktop/src/i18n/messages/tr.json b/ui/desktop/src/i18n/messages/tr.json
index a9c151f05..0aaa9f832 100644
--- a/ui/desktop/src/i18n/messages/tr.json
+++ b/ui/desktop/src/i18n/messages/tr.json
@@ -932,6 +932,18 @@
"dirSwitcher.recentDirectories": {
"defaultMessage": "Son dizinler"
},
+ "gitBranchIndicator.failedToSwitch": {
+ "defaultMessage": "{branch} dalına geçilemedi"
+ },
+ "gitBranchIndicator.noBranchesFound": {
+ "defaultMessage": "Dal bulunamadı"
+ },
+ "gitBranchIndicator.searchBranches": {
+ "defaultMessage": "Dal ara…"
+ },
+ "gitBranchIndicator.uncommittedChanges": {
+ "defaultMessage": "Commit edilmemiş değişiklikleriniz olabilir."
+ },
"elicitationRequest.accept": {
"defaultMessage": "Kabul et"
},
diff --git a/ui/desktop/src/i18n/messages/vi.json b/ui/desktop/src/i18n/messages/vi.json
index 9664bab0f..bc7fb1445 100644
--- a/ui/desktop/src/i18n/messages/vi.json
+++ b/ui/desktop/src/i18n/messages/vi.json
@@ -932,6 +932,18 @@
"dirSwitcher.recentDirectories": {
"defaultMessage": "Thư mục gần đây"
},
+ "gitBranchIndicator.failedToSwitch": {
+ "defaultMessage": "Không thể chuyển sang {branch}"
+ },
+ "gitBranchIndicator.noBranchesFound": {
+ "defaultMessage": "Không tìm thấy nhánh"
+ },
+ "gitBranchIndicator.searchBranches": {
+ "defaultMessage": "Tìm kiếm nhánh…"
+ },
+ "gitBranchIndicator.uncommittedChanges": {
+ "defaultMessage": "Bạn có thể có các thay đổi chưa commit."
+ },
"elicitationRequest.accept": {
"defaultMessage": "Chấp nhận"
},
diff --git a/ui/desktop/src/i18n/messages/zh-CN.json b/ui/desktop/src/i18n/messages/zh-CN.json
index 90c620168..28cceb80c 100644
--- a/ui/desktop/src/i18n/messages/zh-CN.json
+++ b/ui/desktop/src/i18n/messages/zh-CN.json
@@ -932,6 +932,18 @@
"dirSwitcher.recentDirectories": {
"defaultMessage": "最近的目录"
},
+ "gitBranchIndicator.failedToSwitch": {
+ "defaultMessage": "无法切换到 {branch}"
+ },
+ "gitBranchIndicator.noBranchesFound": {
+ "defaultMessage": "未找到分支"
+ },
+ "gitBranchIndicator.searchBranches": {
+ "defaultMessage": "搜索分支…"
+ },
+ "gitBranchIndicator.uncommittedChanges": {
+ "defaultMessage": "你可能有尚未提交的更改。"
+ },
"elicitationRequest.accept": {
"defaultMessage": "接受"
},
diff --git a/ui/desktop/src/i18n/messages/zh-TW.json b/ui/desktop/src/i18n/messages/zh-TW.json
index e1fe91ff3..62b8a9c6f 100644
--- a/ui/desktop/src/i18n/messages/zh-TW.json
+++ b/ui/desktop/src/i18n/messages/zh-TW.json
@@ -932,6 +932,18 @@
"dirSwitcher.recentDirectories": {
"defaultMessage": "最近的目錄"
},
+ "gitBranchIndicator.failedToSwitch": {
+ "defaultMessage": "無法切換至 {branch}"
+ },
+ "gitBranchIndicator.noBranchesFound": {
+ "defaultMessage": "找不到分支"
+ },
+ "gitBranchIndicator.searchBranches": {
+ "defaultMessage": "搜尋分支…"
+ },
+ "gitBranchIndicator.uncommittedChanges": {
+ "defaultMessage": "你可能有尚未提交的變更。"
+ },
"elicitationRequest.accept": {
"defaultMessage": "接受"
},
diff --git a/ui/desktop/src/main.ts b/ui/desktop/src/main.ts
index 60a5fe3a3..ac622eaf6 100644
--- a/ui/desktop/src/main.ts
+++ b/ui/desktop/src/main.ts
@@ -51,6 +51,7 @@ import {
updateTrayMenu,
} from './utils/autoUpdater';
import { UPDATES_ENABLED } from './updates';
+import './utils/gitBranchIpc';
import './utils/recipeHash';
import type { GooseApp } from './types/apps';
import installExtension, { REACT_DEVELOPER_TOOLS } from 'electron-devtools-installer';
diff --git a/ui/desktop/src/preload.ts b/ui/desktop/src/preload.ts
index 0d33f9597..50c442c9a 100644
--- a/ui/desktop/src/preload.ts
+++ b/ui/desktop/src/preload.ts
@@ -180,6 +180,9 @@ type ElectronAPI = {
addRecentDir: (dir: string) => Promise;
listRecentDirs: () => Promise;
listGitWorktreeDirs: (dir: string) => Promise;
+ getGitBranchInfo: (dir: string) => Promise<{ branch: string } | null>;
+ listGitBranches: (dir: string) => Promise;
+ switchGitBranch: (dir: string, branch: string) => Promise<{ success: boolean; error?: string }>;
};
type AppConfigAPI = {
@@ -339,6 +342,10 @@ const electronAPI: ElectronAPI = {
addRecentDir: (dir: string) => ipcRenderer.invoke('add-recent-dir', dir),
listRecentDirs: () => ipcRenderer.invoke('list-recent-dirs'),
listGitWorktreeDirs: (dir: string) => ipcRenderer.invoke('list-git-worktree-dirs', dir),
+ getGitBranchInfo: (dir: string) => ipcRenderer.invoke('get-git-branch-info', dir),
+ listGitBranches: (dir: string) => ipcRenderer.invoke('list-git-branches', dir),
+ switchGitBranch: (dir: string, branch: string) =>
+ ipcRenderer.invoke('switch-git-branch', dir, branch),
};
function getAppLocale(): unknown {
diff --git a/ui/desktop/src/utils/gitBranchIpc.ts b/ui/desktop/src/utils/gitBranchIpc.ts
new file mode 100644
index 000000000..373db426e
--- /dev/null
+++ b/ui/desktop/src/utils/gitBranchIpc.ts
@@ -0,0 +1,72 @@
+import { execFile } from 'child_process';
+import { ipcMain } from 'electron';
+
+const gitArgs = (dir: string, args: string[]) => [
+ '-c',
+ 'safe.bareRepository=explicit',
+ '-c',
+ 'core.fsmonitor=false',
+ '-C',
+ dir,
+ ...args,
+];
+
+const git = (dir: string, args: string[], timeout = 3000) =>
+ new Promise((resolve, reject) => {
+ execFile('git', gitArgs(dir, args), { timeout }, (error, stdout) => {
+ if (error) reject(error);
+ else resolve(stdout.trim());
+ });
+ });
+
+const getCurrentBranch = async (dir: string) => {
+ try {
+ const ref = await git(dir, ['symbolic-ref', 'HEAD']);
+ return ref.startsWith('refs/heads/') ? ref.slice('refs/heads/'.length) : ref;
+ } catch {
+ return git(dir, ['rev-parse', '--short', 'HEAD']).catch(() => null);
+ }
+};
+
+ipcMain.handle(
+ 'get-git-branch-info',
+ async (_event, dir: string): Promise<{ branch: string } | null> => {
+ if (!dir?.trim()) return null;
+
+ const branch = await getCurrentBranch(dir);
+ return branch ? { branch } : null;
+ }
+);
+
+ipcMain.handle('list-git-branches', async (_event, dir: string): Promise => {
+ if (!dir?.trim()) return [];
+
+ try {
+ const branches = await git(dir, [
+ 'for-each-ref',
+ 'refs/heads/',
+ '--format=%(refname:lstrip=2)',
+ ]);
+ return branches.split('\n').filter(Boolean);
+ } catch {
+ return [];
+ }
+});
+
+ipcMain.handle(
+ 'switch-git-branch',
+ async (_event, dir: string, branch: string): Promise<{ success: boolean; error?: string }> => {
+ if (!dir?.trim() || !branch?.trim()) return { success: false };
+
+ try {
+ await git(dir, ['checkout', branch], 30000);
+ return { success: true };
+ } catch (error) {
+ const currentBranch = await getCurrentBranch(dir);
+ if (currentBranch === branch) return { success: true };
+
+ const gitError = error as Error & { stderr?: string };
+ return { success: false, error: gitError.stderr?.toString() || gitError.message };
+ }
+ }
+);