diff --git a/packages/dashboard/app/api/legacy.ts b/packages/dashboard/app/api/legacy.ts index 876bede201..28b33deb29 100644 --- a/packages/dashboard/app/api/legacy.ts +++ b/packages/dashboard/app/api/legacy.ts @@ -2502,8 +2502,8 @@ export interface GitRemote { } /** Fetch GitHub remotes from the current git repository */ -export function fetchGitRemotes(projectId?: string): Promise { - return api(withProjectId("/git/remotes", projectId)); +export function fetchGitRemotes(projectId?: string, repoPath?: string): Promise { + return api(withRepoPath(withProjectId("/git/remotes", projectId), repoPath)); } /** Detailed git remote info with fetch and push URLs */ @@ -2514,36 +2514,36 @@ export interface GitRemoteDetailed { } /** Fetch all git remotes with their fetch and push URLs */ -export function fetchGitRemotesDetailed(projectId?: string): Promise { - return api(withProjectId("/git/remotes/detailed", projectId)); +export function fetchGitRemotesDetailed(projectId?: string, repoPath?: string): Promise { + return api(withRepoPath(withProjectId("/git/remotes/detailed", projectId), repoPath)); } /** Add a new git remote */ -export function addGitRemote(name: string, url: string, projectId?: string): Promise { - return api(withProjectId("/git/remotes", projectId), { +export function addGitRemote(name: string, url: string, projectId?: string, repoPath?: string): Promise { + return api(withRepoPath(withProjectId("/git/remotes", projectId), repoPath), { method: "POST", body: JSON.stringify({ name, url }), }); } /** Remove a git remote */ -export function removeGitRemote(name: string, projectId?: string): Promise { - return api(withProjectId(`/git/remotes/${encodeURIComponent(name)}`, projectId), { +export function removeGitRemote(name: string, projectId?: string, repoPath?: string): Promise { + return api(withRepoPath(withProjectId(`/git/remotes/${encodeURIComponent(name)}`, projectId), repoPath), { method: "DELETE", }); } /** Rename a git remote */ -export function renameGitRemote(name: string, newName: string, projectId?: string): Promise { - return api(withProjectId(`/git/remotes/${encodeURIComponent(name)}`, projectId), { +export function renameGitRemote(name: string, newName: string, projectId?: string, repoPath?: string): Promise { + return api(withRepoPath(withProjectId(`/git/remotes/${encodeURIComponent(name)}`, projectId), repoPath), { method: "PATCH", body: JSON.stringify({ newName }), }); } /** Update the URL for a git remote */ -export function updateGitRemoteUrl(name: string, url: string, projectId?: string): Promise { - return api(withProjectId(`/git/remotes/${encodeURIComponent(name)}/url`, projectId), { +export function updateGitRemoteUrl(name: string, url: string, projectId?: string, repoPath?: string): Promise { + return api(withRepoPath(withProjectId(`/git/remotes/${encodeURIComponent(name)}/url`, projectId), repoPath), { method: "PUT", body: JSON.stringify({ url }), }); @@ -3041,104 +3041,105 @@ export interface GitPushResult { * resolution, ahead/behind vs both local and origin integration tip, dirty * breakdown, stash count, index-stale detection, and recent merge-advance * audit events for the project-root worktree. */ -export function fetchGitStatus(projectId?: string, opts?: { extended?: boolean }): Promise { - const base = withProjectId("/git/status", projectId); +export function fetchGitStatus(projectId?: string, opts?: { extended?: boolean }, repoPath?: string): Promise { + const base = withRepoPath(withProjectId("/git/status", projectId), repoPath); if (!opts?.extended) return api(base); const sep = base.includes("?") ? "&" : "?"; return api(`${base}${sep}extended=1`); } /** Fetch recent commits */ -export function fetchGitCommits(limit?: number, projectId?: string): Promise { +export function fetchGitCommits(limit?: number, projectId?: string, repoPath?: string): Promise { const query = limit ? `?limit=${limit}` : ""; - return api(withProjectId(`/git/commits${query}`, projectId)); + return api(withRepoPath(withProjectId(`/git/commits${query}`, projectId), repoPath)); } /** Fetch diff for a specific commit */ -export function fetchCommitDiff(hash: string, projectId?: string): Promise<{ stat: string; patch: string }> { - return api<{ stat: string; patch: string }>(withProjectId(`/git/commits/${hash}/diff`, projectId)); +export function fetchCommitDiff(hash: string, projectId?: string, repoPath?: string): Promise<{ stat: string; patch: string }> { + return api<{ stat: string; patch: string }>(withRepoPath(withProjectId(`/git/commits/${hash}/diff`, projectId), repoPath)); } /** Fetch local commits ahead of the upstream tracking branch (commits to push) */ -export function fetchAheadCommits(projectId?: string): Promise { - return api(withProjectId("/git/commits/ahead", projectId)); +export function fetchAheadCommits(projectId?: string, repoPath?: string): Promise { + return api(withRepoPath(withProjectId("/git/commits/ahead", projectId), repoPath)); } /** Fetch recent commits for a specific remote */ -export function fetchRemoteCommits(remote: string, ref?: string, limit?: number, projectId?: string): Promise { +export function fetchRemoteCommits(remote: string, ref?: string, limit?: number, projectId?: string, repoPath?: string): Promise { const params = new URLSearchParams(); if (ref) params.set("ref", ref); if (limit) params.set("limit", String(limit)); const query = params.size > 0 ? `?${params.toString()}` : ""; - return api(withProjectId(`/git/remotes/${encodeURIComponent(remote)}/commits${query}`, projectId)); + return api(withRepoPath(withProjectId(`/git/remotes/${encodeURIComponent(remote)}/commits${query}`, projectId), repoPath)); } /** Fetch all local branches */ -export function fetchGitBranches(projectId?: string): Promise { - return api(withProjectId("/git/branches", projectId)); +export function fetchGitBranches(projectId?: string, repoPath?: string): Promise { + return api(withRepoPath(withProjectId("/git/branches", projectId), repoPath)); } /** Fetch recent commits for a specific branch */ -export function fetchBranchCommits(branchName: string, limit?: number, projectId?: string): Promise { +export function fetchBranchCommits(branchName: string, limit?: number, projectId?: string, repoPath?: string): Promise { const query = limit ? `?limit=${limit}` : ""; - return api(withProjectId(`/git/branches/${encodeURIComponent(branchName)}/commits${query}`, projectId)); + return api(withRepoPath(withProjectId(`/git/branches/${encodeURIComponent(branchName)}/commits${query}`, projectId), repoPath)); } /** Fetch all worktrees */ -export function fetchGitWorktrees(projectId?: string): Promise { - return api(withProjectId("/git/worktrees", projectId)); +export function fetchGitWorktrees(projectId?: string, repoPath?: string): Promise { + return api(withRepoPath(withProjectId("/git/worktrees", projectId), repoPath)); } /** Create a new branch */ -export function createBranch(name: string, base?: string, projectId?: string): Promise { - return api(withProjectId("/git/branches", projectId), { +export function createBranch(name: string, base?: string, projectId?: string, repoPath?: string): Promise { + return api(withRepoPath(withProjectId("/git/branches", projectId), repoPath), { method: "POST", body: JSON.stringify({ name, base }), }); } /** Checkout an existing branch */ -export function checkoutBranch(name: string, projectId?: string): Promise { - return api(withProjectId(`/git/branches/${encodeURIComponent(name)}/checkout`, projectId), { +export function checkoutBranch(name: string, projectId?: string, repoPath?: string): Promise { + return api(withRepoPath(withProjectId(`/git/branches/${encodeURIComponent(name)}/checkout`, projectId), repoPath), { method: "POST", }); } /** Delete a branch */ -export function deleteBranch(name: string, force?: boolean, projectId?: string): Promise { +export function deleteBranch(name: string, force?: boolean, projectId?: string, repoPath?: string): Promise { const query = force ? "?force=true" : ""; - return api(withProjectId(`/git/branches/${encodeURIComponent(name)}${query}`, projectId), { + return api(withRepoPath(withProjectId(`/git/branches/${encodeURIComponent(name)}${query}`, projectId), repoPath), { method: "DELETE", }); } /** Fetch from remote */ -export function fetchRemote(remote?: string, projectId?: string): Promise { - return api(withProjectId("/git/fetch", projectId), { +export function fetchRemote(remote?: string, projectId?: string, repoPath?: string): Promise { + return api(withRepoPath(withProjectId("/git/fetch", projectId), repoPath), { method: "POST", body: JSON.stringify({ remote }), }); } /** Pull current branch */ -export function pullBranch(options?: { rebase?: boolean }, projectId?: string): Promise; -export function pullBranch(projectId?: string): Promise; +export function pullBranch(options?: { rebase?: boolean }, projectId?: string, repoPath?: string): Promise; +export function pullBranch(projectId?: string, repoPath?: string): Promise; export function pullBranch( optionsOrProjectId?: { rebase?: boolean } | string, projectId?: string, + repoPath?: string, ): Promise { const options = typeof optionsOrProjectId === "string" ? undefined : optionsOrProjectId; const resolvedProjectId = typeof optionsOrProjectId === "string" ? optionsOrProjectId : projectId; - return api(withProjectId("/git/pull", resolvedProjectId), { + return api(withRepoPath(withProjectId("/git/pull", resolvedProjectId), repoPath), { method: "POST", body: JSON.stringify({ rebase: options?.rebase ?? false }), }); } /** Push current branch */ -export function pushBranch(projectId?: string): Promise { - return api(withProjectId("/git/push", projectId), { +export function pushBranch(projectId?: string, repoPath?: string): Promise { + return api(withRepoPath(withProjectId("/git/push", projectId), repoPath), { method: "POST", }); } @@ -3160,83 +3161,83 @@ export interface GitFileChange { } /** Fetch stash list */ -export function fetchGitStashList(projectId?: string): Promise { - return api(withProjectId("/git/stashes", projectId)); +export function fetchGitStashList(projectId?: string, repoPath?: string): Promise { + return api(withRepoPath(withProjectId("/git/stashes", projectId), repoPath)); } /** Create a new stash */ -export function createStash(message?: string, projectId?: string): Promise<{ message: string }> { - return api<{ message: string }>(withProjectId("/git/stashes", projectId), { +export function createStash(message?: string, projectId?: string, repoPath?: string): Promise<{ message: string }> { + return api<{ message: string }>(withRepoPath(withProjectId("/git/stashes", projectId), repoPath), { method: "POST", body: JSON.stringify({ message }), }); } /** Apply a stash entry */ -export function applyStash(index: number, drop?: boolean, projectId?: string): Promise<{ message: string }> { - return api<{ message: string }>(withProjectId(`/git/stashes/${index}/apply`, projectId), { +export function applyStash(index: number, drop?: boolean, projectId?: string, repoPath?: string): Promise<{ message: string }> { + return api<{ message: string }>(withRepoPath(withProjectId(`/git/stashes/${index}/apply`, projectId), repoPath), { method: "POST", body: JSON.stringify({ drop }), }); } /** Drop a stash entry */ -export function dropStash(index: number, projectId?: string): Promise<{ message: string }> { - return api<{ message: string }>(withProjectId(`/git/stashes/${index}`, projectId), { +export function dropStash(index: number, projectId?: string, repoPath?: string): Promise<{ message: string }> { + return api<{ message: string }>(withRepoPath(withProjectId(`/git/stashes/${index}`, projectId), repoPath), { method: "DELETE", }); } /** Fetch stash diff (stat + patch) */ -export function fetchStashDiff(index: number, projectId?: string): Promise<{ stat: string; patch: string }> { - return api<{ stat: string; patch: string }>(withProjectId(`/git/stashes/${index}/diff`, projectId)); +export function fetchStashDiff(index: number, projectId?: string, repoPath?: string): Promise<{ stat: string; patch: string }> { + return api<{ stat: string; patch: string }>(withRepoPath(withProjectId(`/git/stashes/${index}/diff`, projectId), repoPath)); } /** Fetch unstaged diff (working directory changes) */ -export function fetchUnstagedDiff(projectId?: string): Promise<{ stat: string; patch: string }> { - return api<{ stat: string; patch: string }>(withProjectId("/git/diff", projectId)); +export function fetchUnstagedDiff(projectId?: string, repoPath?: string): Promise<{ stat: string; patch: string }> { + return api<{ stat: string; patch: string }>(withRepoPath(withProjectId("/git/diff", projectId), repoPath)); } /** Fetch diff for a specific file in staged or unstaged mode */ -export function fetchGitFileDiff(path: string, staged: boolean, projectId?: string): Promise<{ stat: string; patch: string }> { +export function fetchGitFileDiff(path: string, staged: boolean, projectId?: string, repoPath?: string): Promise<{ stat: string; patch: string }> { const params = new URLSearchParams(); params.set("path", path); params.set("staged", String(staged)); - return api<{ stat: string; patch: string }>(withProjectId(`/git/diff/file?${params.toString()}`, projectId)); + return api<{ stat: string; patch: string }>(withRepoPath(withProjectId(`/git/diff/file?${params.toString()}`, projectId), repoPath)); } /** Fetch file changes (staged and unstaged) */ -export function fetchFileChanges(projectId?: string): Promise { - return api(withProjectId("/git/changes", projectId)); +export function fetchFileChanges(projectId?: string, repoPath?: string): Promise { + return api(withRepoPath(withProjectId("/git/changes", projectId), repoPath)); } /** Stage specific files */ -export function stageFiles(files: string[], projectId?: string): Promise<{ staged: string[] }> { - return api<{ staged: string[] }>(withProjectId("/git/stage", projectId), { +export function stageFiles(files: string[], projectId?: string, repoPath?: string): Promise<{ staged: string[] }> { + return api<{ staged: string[] }>(withRepoPath(withProjectId("/git/stage", projectId), repoPath), { method: "POST", body: JSON.stringify({ files }), }); } /** Unstage specific files */ -export function unstageFiles(files: string[], projectId?: string): Promise<{ unstaged: string[] }> { - return api<{ unstaged: string[] }>(withProjectId("/git/unstage", projectId), { +export function unstageFiles(files: string[], projectId?: string, repoPath?: string): Promise<{ unstaged: string[] }> { + return api<{ unstaged: string[] }>(withRepoPath(withProjectId("/git/unstage", projectId), repoPath), { method: "POST", body: JSON.stringify({ files }), }); } /** Create a commit */ -export function createCommit(message: string, projectId?: string): Promise<{ hash: string; message: string }> { - return api<{ hash: string; message: string }>(withProjectId("/git/commit", projectId), { +export function createCommit(message: string, projectId?: string, repoPath?: string): Promise<{ hash: string; message: string }> { + return api<{ hash: string; message: string }>(withRepoPath(withProjectId("/git/commit", projectId), repoPath), { method: "POST", body: JSON.stringify({ message }), }); } /** Discard changes in working directory for specific files */ -export function discardChanges(files: string[], projectId?: string): Promise<{ discarded: string[] }> { - return api<{ discarded: string[] }>(withProjectId("/git/discard", projectId), { +export function discardChanges(files: string[], projectId?: string, repoPath?: string): Promise<{ discarded: string[] }> { + return api<{ discarded: string[] }>(withRepoPath(withProjectId("/git/discard", projectId), repoPath), { method: "POST", body: JSON.stringify({ files }), }); @@ -5881,6 +5882,18 @@ function withProjectId(path: string, projectId?: string): string { return `${path}${separator}projectId=${encodeURIComponent(projectId)}`; } +/** Append repoPath query param for workspace-mode sub-repo targeting */ +function withRepoPath(path: string, repoPath?: string): string { + if (!repoPath) return path; + const separator = path.includes("?") ? "&" : "?"; + return `${path}${separator}repoPath=${encodeURIComponent(repoPath)}`; +} + +/** Fetch workspace sub-repos for a project */ +export function fetchWorkspaceRepos(projectId?: string): Promise<{ repos: string[] }> { + return api<{ repos: string[] }>(withProjectId("/git/workspace-repos", projectId)); +} + /** * Rewrite a path to route through the node proxy when viewing a remote node. * When nodeId is provided and differs from localNodeId (i.e., it's a remote node), diff --git a/packages/dashboard/app/components/GitManagerModal.tsx b/packages/dashboard/app/components/GitManagerModal.tsx index eb2e62a5ee..b2e1f04966 100644 --- a/packages/dashboard/app/components/GitManagerModal.tsx +++ b/packages/dashboard/app/components/GitManagerModal.tsx @@ -58,6 +58,7 @@ import { fetchAheadCommits, fetchRemoteCommits, fetchBranchCommits, + fetchWorkspaceRepos, } from "../api"; import { StashRecoveryView } from "./StashRecoveryView"; import { @@ -256,6 +257,17 @@ export function GitManagerModal({ isOpen, onClose, tasks: _tasks, addToast, proj const [rootDir, setRootDir] = useState(null); + // ── Workspace repo selector state + /* + FNXC:Workspace 2026-06-24-21:00: + In workspace mode (multi-repo), the git manager shows a repo selector so the + user can pick which sub-repo to inspect. selectedRepo is the relative path + (e.g. "openvide"); gitRepoPath is passed as repoPath to all git API calls. + */ + const [workspaceRepos, setWorkspaceRepos] = useState([]); + const [selectedRepo, setSelectedRepo] = useState(null); + const gitRepoPath = selectedRepo ?? undefined; + // ── Changes state const [fileChanges, setFileChanges] = useState([]); const [selectedFiles, setSelectedFiles] = useState>(new Set()); @@ -313,12 +325,12 @@ export function GitManagerModal({ isOpen, onClose, tasks: _tasks, addToast, proj try { switch (activeSection) { case "status": { - const statusData = await fetchGitStatus(projectId, { extended: true }); + const statusData = await fetchGitStatus(projectId, { extended: true }, gitRepoPath); setStatus(statusData); break; } case "changes": { - const [statusData, changes] = await Promise.all([fetchGitStatus(projectId, { extended: true }), fetchFileChanges(projectId)]); + const [statusData, changes] = await Promise.all([fetchGitStatus(projectId, { extended: true }, gitRepoPath), fetchFileChanges(projectId, gitRepoPath)]); setStatus(statusData); setFileChanges(changes); setSelectedFiles(new Set()); @@ -328,23 +340,23 @@ export function GitManagerModal({ isOpen, onClose, tasks: _tasks, addToast, proj break; } case "commits": { - const commitsData = await fetchGitCommits(commitsLimit, projectId); + const commitsData = await fetchGitCommits(commitsLimit, projectId, gitRepoPath); setCommits(commitsData); break; } case "branches": { - const [branchesData, statusForBranch] = await Promise.all([fetchGitBranches(projectId), fetchGitStatus(projectId, { extended: true })]); + const [branchesData, statusForBranch] = await Promise.all([fetchGitBranches(projectId, gitRepoPath), fetchGitStatus(projectId, { extended: true }, gitRepoPath)]); setBranches(branchesData); setStatus(statusForBranch); break; } case "worktrees": { - const worktreesData = await fetchGitWorktrees(projectId); + const worktreesData = await fetchGitWorktrees(projectId, gitRepoPath); setWorktrees(worktreesData); break; } case "stashes": { - const stashesData = await fetchGitStashList(projectId); + const stashesData = await fetchGitStashList(projectId, gitRepoPath); setStashes(stashesData); setExpandedStashIndex(null); setStashDiff(null); @@ -357,7 +369,7 @@ export function GitManagerModal({ isOpen, onClose, tasks: _tasks, addToast, proj break; } case "remotes": { - const remoteStatus = await fetchGitStatus(projectId, { extended: true }); + const remoteStatus = await fetchGitStatus(projectId, { extended: true }, gitRepoPath); setStatus(remoteStatus); break; } @@ -368,7 +380,7 @@ export function GitManagerModal({ isOpen, onClose, tasks: _tasks, addToast, proj } finally { setLoading(false); } - }, [activeSection, isOpen, commitsLimit, addToast, projectId]); + }, [activeSection, isOpen, commitsLimit, addToast, projectId, gitRepoPath]); useEffect(() => { if (isOpen) { @@ -405,9 +417,9 @@ export function GitManagerModal({ isOpen, onClose, tasks: _tasks, addToast, proj const handleStageFiles = useCallback(async (files: string[]) => { try { - await stageFiles(files, projectId); + await stageFiles(files, projectId, gitRepoPath); addToast(t("git.stagedFiles", "Staged {{count}} file(s)", { count: files.length }), "success"); - const changes = await fetchFileChanges(projectId); + const changes = await fetchFileChanges(projectId, gitRepoPath); setFileChanges(changes); setSelectedFiles(new Set()); setSelectedDiffTarget(null); @@ -420,9 +432,9 @@ export function GitManagerModal({ isOpen, onClose, tasks: _tasks, addToast, proj const handleUnstageFiles = useCallback(async (files: string[]) => { try { - await unstageFiles(files, projectId); + await unstageFiles(files, projectId, gitRepoPath); addToast(t("git.unstagedFiles", "Unstaged {{count}} file(s)", { count: files.length }), "success"); - const changes = await fetchFileChanges(projectId); + const changes = await fetchFileChanges(projectId, gitRepoPath); setFileChanges(changes); setSelectedFiles(new Set()); setSelectedDiffTarget(null); @@ -441,9 +453,9 @@ export function GitManagerModal({ isOpen, onClose, tasks: _tasks, addToast, proj }); if (!shouldDiscard) return; try { - await discardChanges(files, projectId); + await discardChanges(files, projectId, gitRepoPath); addToast(t("git.discardedFiles", "Discarded changes to {{count}} file(s)", { count: files.length }), "success"); - const [changes, statusData] = await Promise.all([fetchFileChanges(projectId), fetchGitStatus(projectId, { extended: true })]); + const [changes, statusData] = await Promise.all([fetchFileChanges(projectId, gitRepoPath), fetchGitStatus(projectId, { extended: true }, gitRepoPath)]); setFileChanges(changes); setStatus(statusData); setSelectedFiles(new Set()); @@ -460,11 +472,11 @@ export function GitManagerModal({ isOpen, onClose, tasks: _tasks, addToast, proj if (!commitMessage.trim()) return; setCommitting(true); try { - const result = await createCommit(commitMessage.trim(), projectId); + const result = await createCommit(commitMessage.trim(), projectId, gitRepoPath); addToast(t("git.committedHash", "Committed: {{hash}}", { hash: result.hash }), "success"); setCommitMessage(""); // Refresh changes and status - const [changes, statusData] = await Promise.all([fetchFileChanges(projectId), fetchGitStatus(projectId, { extended: true })]); + const [changes, statusData] = await Promise.all([fetchFileChanges(projectId, gitRepoPath), fetchGitStatus(projectId, { extended: true }, gitRepoPath)]); setFileChanges(changes); setStatus(statusData); setSelectedDiffTarget(null); @@ -483,12 +495,12 @@ export function GitManagerModal({ isOpen, onClose, tasks: _tasks, addToast, proj try { const unstaged = fileChanges.filter((f) => !f.staged).map((f) => f.file); if (unstaged.length > 0) { - await stageFiles(unstaged, projectId); + await stageFiles(unstaged, projectId, gitRepoPath); } - const result = await createCommit(commitMessage.trim(), projectId); + const result = await createCommit(commitMessage.trim(), projectId, gitRepoPath); addToast(t("git.committedHash", "Committed: {{hash}}", { hash: result.hash }), "success"); setCommitMessage(""); - const [changes, statusData] = await Promise.all([fetchFileChanges(projectId), fetchGitStatus(projectId, { extended: true })]); + const [changes, statusData] = await Promise.all([fetchFileChanges(projectId, gitRepoPath), fetchGitStatus(projectId, { extended: true }, gitRepoPath)]); setFileChanges(changes); setStatus(statusData); setSelectedDiffTarget(null); @@ -509,7 +521,7 @@ export function GitManagerModal({ isOpen, onClose, tasks: _tasks, addToast, proj changeDiffRequestIdRef.current = requestId; try { - const diff = await fetchGitFileDiff(file, staged, projectId); + const diff = await fetchGitFileDiff(file, staged, projectId, gitRepoPath); if (changeDiffRequestIdRef.current !== requestId) { return; } @@ -552,7 +564,7 @@ export function GitManagerModal({ isOpen, onClose, tasks: _tasks, addToast, proj setSelectedCommit(hash); setLoadingDiff(true); try { - const diff = await fetchCommitDiff(hash, projectId); + const diff = await fetchCommitDiff(hash, projectId, gitRepoPath); setCommitDiff(diff); } catch (err) { addToast(getErrorMessage(err) || t("git.failedToLoadDiff", "Failed to load diff"), "error"); @@ -584,11 +596,11 @@ export function GitManagerModal({ isOpen, onClose, tasks: _tasks, addToast, proj if (!newBranchName.trim()) return; setLoading(true); try { - await createBranch(newBranchName.trim(), branchBase.trim() || undefined, projectId); + await createBranch(newBranchName.trim(), branchBase.trim() || undefined, projectId, gitRepoPath); addToast(t("git.createdBranch", "Created branch {{name}}", { name: newBranchName }), "success"); setNewBranchName(""); setBranchBase(""); - const branchesData = await fetchGitBranches(projectId); + const branchesData = await fetchGitBranches(projectId, gitRepoPath); setBranches(branchesData); } catch (err) { addToast(getErrorMessage(err) || t("git.failedToCreateBranch", "Failed to create branch"), "error"); @@ -600,9 +612,9 @@ export function GitManagerModal({ isOpen, onClose, tasks: _tasks, addToast, proj const handleCheckoutBranch = useCallback(async (name: string) => { setLoading(true); try { - await checkoutBranch(name, projectId); + await checkoutBranch(name, projectId, gitRepoPath); addToast(t("git.switchedToBranch", "Switched to {{name}}", { name }), "success"); - const [statusData, branchesData] = await Promise.all([fetchGitStatus(projectId, { extended: true }), fetchGitBranches(projectId)]); + const [statusData, branchesData] = await Promise.all([fetchGitStatus(projectId, { extended: true }, gitRepoPath), fetchGitBranches(projectId, gitRepoPath)]); setStatus(statusData); setBranches(branchesData); } catch (err) { @@ -621,9 +633,9 @@ export function GitManagerModal({ isOpen, onClose, tasks: _tasks, addToast, proj if (!shouldDelete) return; setLoading(true); try { - await deleteBranch(name, undefined, projectId); + await deleteBranch(name, undefined, projectId, gitRepoPath); addToast(t("git.deletedBranch", "Deleted branch {{name}}", { name }), "success"); - const branchesData = await fetchGitBranches(projectId); + const branchesData = await fetchGitBranches(projectId, gitRepoPath); setBranches(branchesData); } catch (err) { if (getErrorMessage(err).includes("not fully merged")) { @@ -634,9 +646,9 @@ export function GitManagerModal({ isOpen, onClose, tasks: _tasks, addToast, proj }); if (shouldForceDelete) { try { - await deleteBranch(name, true, projectId); + await deleteBranch(name, true, projectId, gitRepoPath); addToast(t("git.forceDeletedBranch", "Force deleted branch {{name}}", { name }), "success"); - const branchesData = await fetchGitBranches(projectId); + const branchesData = await fetchGitBranches(projectId, gitRepoPath); setBranches(branchesData); } catch (forceErr) { addToast(getErrorMessage(forceErr) || t("git.failedToDeleteBranch", "Failed to delete branch"), "error"); @@ -674,7 +686,7 @@ export function GitManagerModal({ isOpen, onClose, tasks: _tasks, addToast, proj setBranchCommitDiff(null); setLoadingBranchCommits(true); try { - const data = await fetchBranchCommits(name, 10, projectId); + const data = await fetchBranchCommits(name, 10, projectId, gitRepoPath); setBranchCommits(data); } catch { setBranchCommits([]); @@ -694,7 +706,7 @@ export function GitManagerModal({ isOpen, onClose, tasks: _tasks, addToast, proj setBranchCommitDiff(null); setLoadingBranchCommitDiff(true); try { - const diff = await fetchCommitDiff(hash, projectId); + const diff = await fetchCommitDiff(hash, projectId, gitRepoPath); setBranchCommitDiff(diff); } catch { setBranchCommitDiff(null); @@ -726,10 +738,10 @@ export function GitManagerModal({ isOpen, onClose, tasks: _tasks, addToast, proj setStashLoading("create"); resetStashDiffState(); try { - await createStash(stashMessage.trim() || undefined, projectId); + await createStash(stashMessage.trim() || undefined, projectId, gitRepoPath); addToast(t("git.changesStashed", "Changes stashed"), "success"); setStashMessage(""); - const stashesData = await fetchGitStashList(projectId); + const stashesData = await fetchGitStashList(projectId, gitRepoPath); setStashes(stashesData); } catch (err) { addToast(getErrorMessage(err) || t("git.failedToStashChanges", "Failed to stash changes"), "error"); @@ -742,9 +754,9 @@ export function GitManagerModal({ isOpen, onClose, tasks: _tasks, addToast, proj setStashLoading(`apply-${index}`); resetStashDiffState(); try { - await applyStash(index, drop, projectId); + await applyStash(index, drop, projectId, gitRepoPath); addToast(drop ? t("git.stashPopped", "Stash popped") : t("git.stashApplied", "Stash applied"), "success"); - const stashesData = await fetchGitStashList(projectId); + const stashesData = await fetchGitStashList(projectId, gitRepoPath); setStashes(stashesData); } catch (err) { addToast(getErrorMessage(err) || t("git.failedToApplyStash", "Failed to apply stash"), "error"); @@ -763,9 +775,9 @@ export function GitManagerModal({ isOpen, onClose, tasks: _tasks, addToast, proj setStashLoading(`drop-${index}`); resetStashDiffState(); try { - await dropStash(index, projectId); + await dropStash(index, projectId, gitRepoPath); addToast(t("git.stashDropped", "Stash dropped"), "success"); - const stashesData = await fetchGitStashList(projectId); + const stashesData = await fetchGitStashList(projectId, gitRepoPath); setStashes(stashesData); } catch (err) { addToast(getErrorMessage(err) || t("git.failedToDropStash", "Failed to drop stash"), "error"); @@ -787,7 +799,7 @@ export function GitManagerModal({ isOpen, onClose, tasks: _tasks, addToast, proj setStashDiffError(null); setLoadingStashDiff(true); try { - const diff = await fetchStashDiff(index, projectId); + const diff = await fetchStashDiff(index, projectId, gitRepoPath); if (stashDiffRequestIdRef.current !== requestId) { return; } @@ -810,10 +822,10 @@ export function GitManagerModal({ isOpen, onClose, tasks: _tasks, addToast, proj const handleFetch = useCallback(async () => { setRemoteLoading("fetch"); try { - const result = await fetchRemote(undefined, projectId); + const result = await fetchRemote(undefined, projectId, gitRepoPath); setLastRemoteResult(result); addToast(result.message || t("git.fetchCompleted", "Fetch completed"), result.fetched ? "success" : "info"); - const statusData = await fetchGitStatus(projectId, { extended: true }); + const statusData = await fetchGitStatus(projectId, { extended: true }, gitRepoPath); setStatus(statusData); } catch (err) { addToast(getErrorMessage(err) || t("git.fetchFailed", "Fetch failed"), "error"); @@ -825,7 +837,7 @@ export function GitManagerModal({ isOpen, onClose, tasks: _tasks, addToast, proj const handlePull = useCallback(async (options?: { rebase?: boolean }) => { setRemoteLoading("pull"); try { - const result = await pullBranch(options, projectId); + const result = await pullBranch(options, projectId, gitRepoPath); setLastRemoteResult(result); if (result.conflict) { addToast(t("git.mergeConflictDetected", "Merge conflict detected. Resolve manually."), "error"); @@ -833,7 +845,7 @@ export function GitManagerModal({ isOpen, onClose, tasks: _tasks, addToast, proj const fallbackMessage = options?.rebase ? t("git.pullRebaseCompleted", "Pull --rebase completed") : t("git.pullCompleted", "Pull completed"); addToast(result.message || fallbackMessage, "success"); } - const statusData = await fetchGitStatus(projectId, { extended: true }); + const statusData = await fetchGitStatus(projectId, { extended: true }, gitRepoPath); setStatus(statusData); } catch (err) { addToast(getErrorMessage(err) || t("git.pullFailed", "Pull failed"), "error"); @@ -845,10 +857,10 @@ export function GitManagerModal({ isOpen, onClose, tasks: _tasks, addToast, proj const handlePush = useCallback(async () => { setRemoteLoading("push"); try { - const result = await pushBranch(projectId); + const result = await pushBranch(projectId, gitRepoPath); setLastRemoteResult(result); addToast(result.message || t("git.pushCompleted", "Push completed"), "success"); - const statusData = await fetchGitStatus(projectId, { extended: true }); + const statusData = await fetchGitStatus(projectId, { extended: true }, gitRepoPath); setStatus(statusData); } catch (err) { addToast(getErrorMessage(err) || t("git.pushFailed", "Push failed"), "error"); @@ -860,17 +872,17 @@ export function GitManagerModal({ isOpen, onClose, tasks: _tasks, addToast, proj const handleSyncWithOrigin = useCallback(async () => { setRemoteLoading("sync"); try { - const pullResult = await pullBranch({ rebase: true }, projectId); + const pullResult = await pullBranch({ rebase: true }, projectId, gitRepoPath); setLastRemoteResult(pullResult); if (pullResult.conflict) { addToast(t("git.mergeConflictDetected", "Merge conflict detected. Resolve manually."), "error"); return; } - const pushResult = await pushBranch(projectId); + const pushResult = await pushBranch(projectId, gitRepoPath); setLastRemoteResult(pushResult); addToast(t("git.syncedWithOrigin", "Synced with origin (pull --rebase + push)"), "success"); - const statusData = await fetchGitStatus(projectId, { extended: true }); + const statusData = await fetchGitStatus(projectId, { extended: true }, gitRepoPath); setStatus(statusData); } catch (err) { addToast(getErrorMessage(err) || t("git.syncWithOriginFailed", "Sync with origin failed"), "error"); @@ -885,6 +897,18 @@ export function GitManagerModal({ isOpen, onClose, tasks: _tasks, addToast, proj fetchConfig(projectId).then((cfg) => setRootDir(cfg.rootDir)).catch(() => setRootDir(null)); }, [projectId]); + // Fetch workspace repos on mount to determine if this is a multi-repo project. + useEffect(() => { + fetchWorkspaceRepos(projectId) + .then((result) => { + setWorkspaceRepos(result.repos); + if (result.repos.length > 0 && !selectedRepo) { + setSelectedRepo(result.repos[0]!); + } + }) + .catch(() => setWorkspaceRepos([])); + }, [projectId]); // intentionally omit selectedRepo to avoid resetting on repo switch + const handleSyncIntegrationTip = useCallback(async () => { if (!status?.integrationBranch || status.isOnIntegrationBranch === false) return; const worktreePath = rootDir; @@ -909,7 +933,7 @@ export function GitManagerModal({ isOpen, onClose, tasks: _tasks, addToast, proj }), }); addToast(t("git.syncedWorktreeToIntegrationTip", "Synced worktree to local integration tip"), "success"); - const statusData = await fetchGitStatus(projectId, { extended: true }); + const statusData = await fetchGitStatus(projectId, { extended: true }, gitRepoPath); setStatus(statusData); } catch (err) { addToast(getErrorMessage(err) || t("git.syncFailed", "Sync failed"), "error"); @@ -932,6 +956,29 @@ export function GitManagerModal({ isOpen, onClose, tasks: _tasks, addToast, proj <> {/* Sidebar Navigation */}