diff --git a/apps/web/src/components/ChatView.logic.test.ts b/apps/web/src/components/ChatView.logic.test.ts index 5b08eb92b267..8c407c2b91b8 100644 --- a/apps/web/src/components/ChatView.logic.test.ts +++ b/apps/web/src/components/ChatView.logic.test.ts @@ -88,6 +88,7 @@ import { shouldWriteThreadErrorToCurrentServerThread, waitForRevertedMessage, prepareRevertedMessageAttachments, + resolveProjectGitCwd, } from "./ChatView.logic"; describe("agent browser close confirmation", () => { @@ -2432,3 +2433,20 @@ describe("worktree setup visibility", () => { }); }); }); + +describe("resolveProjectGitCwd", () => { + it("uses the repo of a workspace file that lists one repo", () => { + expect(resolveProjectGitCwd({ workspaceRoot: "/ws", repoRoots: ["/ws/api"] })).toBe("/ws/api"); + }); + + it("keeps the workspace root of an ordinary project", () => { + expect(resolveProjectGitCwd({ workspaceRoot: "/repo", repoRoots: ["/repo"] })).toBe("/repo"); + expect(resolveProjectGitCwd({ workspaceRoot: "/repo" })).toBe("/repo"); + }); + + it("leaves multi-repo projects on the workspace root, since their git surfaces fan out", () => { + expect(resolveProjectGitCwd({ workspaceRoot: "/ws", repoRoots: ["/ws/api", "/ws/web"] })).toBe( + "/ws", + ); + }); +}); diff --git a/apps/web/src/components/ChatView.logic.ts b/apps/web/src/components/ChatView.logic.ts index b99175d1f14e..7d0416547207 100644 --- a/apps/web/src/components/ChatView.logic.ts +++ b/apps/web/src/components/ChatView.logic.ts @@ -25,6 +25,7 @@ import { squashAtomCommandFailure, type AtomCommandResult, } from "@t3tools/client-runtime/state/runtime"; +import { resolveAnchorRepoRoot } from "@t3tools/shared/git"; import { videoMimeType } from "@t3tools/shared/video"; import { appendCodexArtifactTemplateUsePrompt, @@ -1385,3 +1386,19 @@ export function restorePlanFollowUpComposer(input: { detectTrigger: true, }); } + +/** + * The checkout the chat view's single-repo git surfaces run in: status, the + * header's git actions, checkout switching and PR checkout. A `.code-workspace` + * project's `workspaceRoot` is only the folder holding the file, so a + * workspace with one repo uses that repo. Multi-repo projects keep + * `workspaceRoot` here because their git surfaces fan out per repo instead. + */ +export function resolveProjectGitCwd(project: { + readonly workspaceRoot: string; + readonly repoRoots?: ReadonlyArray | undefined; +}): string { + return (project.repoRoots?.length ?? 0) > 1 + ? project.workspaceRoot + : resolveAnchorRepoRoot(project); +} diff --git a/apps/web/src/components/ChatView.tsx b/apps/web/src/components/ChatView.tsx index 8c10ab23d43e..7ce6e7461861 100644 --- a/apps/web/src/components/ChatView.tsx +++ b/apps/web/src/components/ChatView.tsx @@ -485,6 +485,7 @@ import { codexArtifactTemplatePromptToAppend, waitForStartedServerThread, shouldRefocusComposerOnWindowFocus, + resolveProjectGitCwd, } from "./ChatView.logic"; import type { ThreadSyncPhase } from "../threadSync"; import { useLocalStorage } from "~/hooks/useLocalStorage"; @@ -3653,7 +3654,8 @@ export default function ChatView(props: ChatViewProps) { // repo root, swapping in the thread's worktree for roots an isolated run // covers. A single-repo project uses the worktree-aware status cwd. const isMultiRepo = (activeProject?.repoRoots?.length ?? 0) > 1; - const gitStatusCwd = activeThread?.worktreePath ?? gitCwd; + const projectGitCwd = activeProject ? resolveProjectGitCwd(activeProject) : null; + const gitStatusCwd = activeThread?.worktreePath ?? projectGitCwd; const threadWorktrees = activeThread?.worktrees; const multiRepoStatusRoots = useMemo(() => { if (!isMultiRepo || !activeProject?.repoRoots) return null; @@ -6300,7 +6302,7 @@ export default function ChatView(props: ChatViewProps) { }, [activeBranchMismatchKey, showBranchMismatchBanner]); const handleSwitchCheckoutToThread = useCallback(async () => { if ( - !activeProjectCwd || + !projectGitCwd || !activeThread || !localCheckoutBranchMismatch || isRestoringThreadBranch @@ -6311,7 +6313,7 @@ export default function ChatView(props: ChatViewProps) { const checkoutResult = await switchGitRef({ environmentId, input: { - cwd: activeProjectCwd, + cwd: projectGitCwd, refName: localCheckoutBranchMismatch.threadBranch, }, }); @@ -6354,7 +6356,7 @@ export default function ChatView(props: ChatViewProps) { setIsRestoringThreadBranch(false); scheduleComposerFocus(); }, [ - activeProjectCwd, + projectGitCwd, activeThread, environmentId, gitStatusQuery, @@ -9906,7 +9908,7 @@ export default function ChatView(props: ChatViewProps) { keybindings={keybindings} availableEditors={availableEditors} rightPanelOpen={rightPanelOpen} - gitCwd={gitCwd} + gitCwd={gitStatusCwd} onNewThreadInProject={handleNewThreadInActiveProject} {...(activeDraftLogicalProjectKey ? { onOpenProjectSettings: handleOpenDraftProjectSettings } @@ -10364,7 +10366,7 @@ export default function ChatView(props: ChatViewProps) { open environmentId={activeThread.environmentId} threadId={activeThread.id} - cwd={activeProject?.workspaceRoot ?? null} + cwd={projectGitCwd} initialReference={pullRequestDialogState.initialReference} onOpenChange={(open) => { if (!open) {