diff --git a/apps/web/src/components/BranchToolbar.logic.test.ts b/apps/web/src/components/BranchToolbar.logic.test.ts index ff4bb76bf12a..bd1b11a6cd28 100644 --- a/apps/web/src/components/BranchToolbar.logic.test.ts +++ b/apps/web/src/components/BranchToolbar.logic.test.ts @@ -489,6 +489,24 @@ describe("resolveEffectiveEnvMode", () => { }), ).toBe("worktree"); }); + + it("keeps a server thread in worktree mode while its worktree is still being created", () => { + expect( + resolveEffectiveEnvMode({ + activeWorktreePath: null, + hasServerThread: true, + draftThreadEnvMode: undefined, + preparingWorktree: true, + }), + ).toBe("worktree"); + expect( + resolveEffectiveEnvMode({ + activeWorktreePath: null, + hasServerThread: true, + draftThreadEnvMode: undefined, + }), + ).toBe("local"); + }); }); describe("resolveEnvModeLabel", () => { @@ -510,11 +528,17 @@ describe("resolveCurrentWorkspaceLabel", () => { describe("resolveLockedWorkspaceLabel", () => { it("uses a shorter label for the main repo checkout", () => { - expect(resolveLockedWorkspaceLabel(null)).toBe("Local checkout"); + expect(resolveLockedWorkspaceLabel(null, "local")).toBe("Local checkout"); }); it("uses a shorter label for an attached worktree", () => { - expect(resolveLockedWorkspaceLabel("/repo/.t3/worktrees/feature-a")).toBe("Worktree"); + expect(resolveLockedWorkspaceLabel("/repo/.t3/worktrees/feature-a", "worktree")).toBe( + "Worktree", + ); + }); + + it("describes a worktree that is still being created as a new worktree", () => { + expect(resolveLockedWorkspaceLabel(null, "worktree")).toBe("New worktree"); }); }); diff --git a/apps/web/src/components/BranchToolbar.logic.ts b/apps/web/src/components/BranchToolbar.logic.ts index 85d306f919ae..cbc887954019 100644 --- a/apps/web/src/components/BranchToolbar.logic.ts +++ b/apps/web/src/components/BranchToolbar.logic.ts @@ -104,8 +104,14 @@ export function resolveCurrentWorkspaceLabel(activeWorktreePath: string | null): return activeWorktreePath ? "Current worktree" : resolveEnvModeLabel("local"); } -export function resolveLockedWorkspaceLabel(activeWorktreePath: string | null): string { - return activeWorktreePath ? "Worktree" : "Local checkout"; +// A locked thread in worktree mode with no path is still creating its +// worktree, so it reads as a new worktree rather than the project checkout. +export function resolveLockedWorkspaceLabel( + activeWorktreePath: string | null, + effectiveEnvMode: EnvMode, +): string { + if (activeWorktreePath) return "Worktree"; + return effectiveEnvMode === "worktree" ? resolveEnvModeLabel("worktree") : "Local checkout"; } export interface PreviousWorktreeSeed { @@ -159,15 +165,20 @@ export function resolveEffectiveEnvMode(input: { activeWorktreePath: string | null; hasServerThread: boolean; draftThreadEnvMode: EnvMode | undefined; + /** + * The server is still creating this thread's worktree. The thread exists + * from the start of that setup but gets its worktree path only at the end. + */ + preparingWorktree?: boolean; }): EnvMode { - const { activeWorktreePath, hasServerThread, draftThreadEnvMode } = input; + const { activeWorktreePath, hasServerThread, draftThreadEnvMode, preparingWorktree } = input; if (!hasServerThread) { if (activeWorktreePath) { return "local"; } return draftThreadEnvMode === "worktree" ? "worktree" : "local"; } - return activeWorktreePath ? "worktree" : "local"; + return activeWorktreePath || preparingWorktree ? "worktree" : "local"; } export function resolveDraftEnvModeAfterBranchChange(input: { diff --git a/apps/web/src/components/BranchToolbar.tsx b/apps/web/src/components/BranchToolbar.tsx index 1e5cb85a9737..eca4fab601d0 100644 --- a/apps/web/src/components/BranchToolbar.tsx +++ b/apps/web/src/components/BranchToolbar.tsx @@ -28,7 +28,6 @@ import { resolveContextStripLabelsCompact, resolveCurrentWorkspaceLabel, resolveEnvModeLabel, - resolveEffectiveEnvMode, resolveLockedWorkspaceLabel, resolvePreviousWorktreeLabel, resolvePreviousWorktreeSeed, @@ -74,7 +73,8 @@ interface BranchToolbarProps { showGitControls: boolean; draftId?: DraftId; onEnvModeChange: (mode: EnvMode) => void; - effectiveEnvModeOverride?: EnvMode; + /** The thread's env mode as ChatView resolves it. */ + envMode: EnvMode; activeThreadBranchOverride?: string | null; onActiveThreadBranchOverrideChange?: (branch: string | null) => void; startFromOrigin: boolean; @@ -141,7 +141,7 @@ const MobileRunContextSelector = memo(function MobileRunContextSelector({ const workspaceLabel = forceNewWorktree ? resolveEnvModeLabel("worktree") : envModeLocked - ? resolveLockedWorkspaceLabel(activeWorktreePath) + ? resolveLockedWorkspaceLabel(activeWorktreePath, effectiveEnvMode) : effectiveEnvMode === "worktree" ? resolveEnvModeLabel("worktree") : resolveCurrentWorkspaceLabel(activeWorktreePath); @@ -511,7 +511,7 @@ export const BranchToolbar = memo(function BranchToolbar({ showGitControls, draftId, onEnvModeChange, - effectiveEnvModeOverride, + envMode, activeThreadBranchOverride, onActiveThreadBranchOverrideChange, startFromOrigin, @@ -546,13 +546,7 @@ export const BranchToolbar = memo(function BranchToolbar({ const activeWorktreePath = forceNewWorktree ? null : (serverThread?.worktreePath ?? draftThread?.worktreePath ?? null); - const effectiveEnvMode = - (forceNewWorktree ? "worktree" : effectiveEnvModeOverride) ?? - resolveEffectiveEnvMode({ - activeWorktreePath, - hasServerThread: serverThread !== null, - draftThreadEnvMode: draftThread?.envMode, - }); + const effectiveEnvMode = forceNewWorktree ? "worktree" : envMode; const envModeLocked = envLocked || (serverThread !== null && activeWorktreePath !== null); // "Previous worktree" hops a draft into the most recently active worktree @@ -720,11 +714,7 @@ export const BranchToolbar = memo(function BranchToolbar({ threadId={threadId} {...(draftId ? { draftId } : {})} envLocked={envLocked} - {...(forceNewWorktree - ? { effectiveEnvModeOverride: "worktree" } - : effectiveEnvModeOverride - ? { effectiveEnvModeOverride } - : {})} + effectiveEnvModeOverride={effectiveEnvMode} {...(activeThreadBranchOverride !== undefined ? { activeThreadBranchOverride } : {})} {...(onActiveThreadBranchOverrideChange ? { onActiveThreadBranchOverrideChange } : {})} startFromOrigin={startFromOrigin} diff --git a/apps/web/src/components/BranchToolbarEnvModeSelector.tsx b/apps/web/src/components/BranchToolbarEnvModeSelector.tsx index 1d72ddf87e18..40692063e979 100644 --- a/apps/web/src/components/BranchToolbarEnvModeSelector.tsx +++ b/apps/web/src/components/BranchToolbarEnvModeSelector.tsx @@ -64,10 +64,10 @@ export const BranchToolbarEnvModeSelector = memo(function BranchToolbarEnvModeSe className="inline-flex h-7 min-w-0 items-center gap-1 border border-transparent px-1.75 font-normal text-muted-foreground/70 text-xs sm:h-6" data-composer-context-control > - {forceNewWorktree ? ( - - ) : activeWorktreePath ? ( + {activeWorktreePath ? ( + ) : effectiveEnvMode === "worktree" ? ( + ) : ( )} @@ -79,16 +79,14 @@ export const BranchToolbarEnvModeSelector = memo(function BranchToolbarEnvModeSe data-composer-label-motion className="block w-full min-w-0 max-w-[240px] truncate transition-opacity duration-180 ease-drawer group-data-[compact]/composer-context:opacity-0 motion-reduce:transition-none" > - {forceNewWorktree - ? resolveEnvModeLabel("worktree") - : resolveLockedWorkspaceLabel(activeWorktreePath)} + {resolveLockedWorkspaceLabel(activeWorktreePath, effectiveEnvMode)} {forceNewWorktree ? "Each model starts in its own worktree." - : resolveLockedWorkspaceLabel(activeWorktreePath)} + : resolveLockedWorkspaceLabel(activeWorktreePath, effectiveEnvMode)} ); diff --git a/apps/web/src/components/ChatView.tsx b/apps/web/src/components/ChatView.tsx index cd36675860df..f5c3825cee33 100644 --- a/apps/web/src/components/ChatView.tsx +++ b/apps/web/src/components/ChatView.tsx @@ -5845,6 +5845,7 @@ export default function ChatView(props: ChatViewProps) { activeWorktreePath, hasServerThread: isServerThread, draftThreadEnvMode: isLocalDraftThread ? draftThread?.envMode : undefined, + preparingWorktree: isPreparingWorktree, }); const canOverrideServerThreadEnvMode = Boolean( isServerThread && @@ -10238,9 +10239,7 @@ export default function ChatView(props: ChatViewProps) { onEnvModeChange={onEnvModeChange} startFromOrigin={startFromOrigin} onStartFromOriginChange={onStartFromOriginChange} - {...(canOverrideServerThreadEnvMode - ? { effectiveEnvModeOverride: envMode } - : {})} + envMode={envMode} {...(canOverrideServerThreadEnvMode ? { activeThreadBranchOverride: activeThreadBranch,