From 82fb547cb6da4438694405dc7a7d4131407de636 Mon Sep 17 00:00:00 2001 From: Logan Rupe Date: Thu, 1 Oct 2026 10:32:12 +1000 Subject: [PATCH] fix(web): one-repo workspaces get their git controls back A .code-workspace project's workspaceRoot is only the folder holding the file, so the chat view probed a non-repo for git status and hid the checkout strip, New worktree and the header's git actions. With one repo listed, status, the header's git actions, checkout switching and PR checkout now run in that repo. --- apps/web/src/components/ChatView.logic.test.ts | 18 ++++++++++++++++++ apps/web/src/components/ChatView.logic.ts | 17 +++++++++++++++++ apps/web/src/components/ChatView.tsx | 14 ++++++++------ 3 files changed, 43 insertions(+), 6 deletions(-) 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) {