authorgravatar for me@t3.ggTheo Browne <me@t3.gg> 2026-09-25 00:03:04-07:00
committergravatar for noreply@github.comGitHub <noreply@github.com> 2026-09-25 00:03:04-07:00
log13d6b30515e9d44bb4cc2d223b8b3179c2ba4c79
tree91a36941c75dd71d98cb7915c680cb4a25b3f6e8
parentb3de243d5ed44783c4e7990d8e63cb519d390a03
signature Signed by PGP key B5690EEEBB952194

fix(web): new worktree threads no longer say "checkout" during setup (#13590)

Co-authored-by: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

5 files changed, 54 insertions(+), 32 deletions(-)

apps/web/src/components/BranchToolbar.logic.test.ts+26-2
...@@ -489,6 +489,24 @@ describe("resolveEffectiveEnvMode", () => {...@@ -489,6 +489,24 @@ describe("resolveEffectiveEnvMode", () => {
489 }),489 }),
490 ).toBe("worktree");490 ).toBe("worktree");
491 });491 });
492
493 it("keeps a server thread in worktree mode while its worktree is still being created", () => {
494 expect(
495 resolveEffectiveEnvMode({
496 activeWorktreePath: null,
497 hasServerThread: true,
498 draftThreadEnvMode: undefined,
499 preparingWorktree: true,
500 }),
501 ).toBe("worktree");
502 expect(
503 resolveEffectiveEnvMode({
504 activeWorktreePath: null,
505 hasServerThread: true,
506 draftThreadEnvMode: undefined,
507 }),
508 ).toBe("local");
509 });
492});510});
493511
494describe("resolveEnvModeLabel", () => {512describe("resolveEnvModeLabel", () => {
...@@ -510,11 +528,17 @@ describe("resolveCurrentWorkspaceLabel", () => {...@@ -510,11 +528,17 @@ describe("resolveCurrentWorkspaceLabel", () => {
510528
511describe("resolveLockedWorkspaceLabel", () => {529describe("resolveLockedWorkspaceLabel", () => {
512 it("uses a shorter label for the main repo checkout", () => {530 it("uses a shorter label for the main repo checkout", () => {
513 expect(resolveLockedWorkspaceLabel(null)).toBe("Local checkout");531 expect(resolveLockedWorkspaceLabel(null, "local")).toBe("Local checkout");
514 });532 });
515533
516 it("uses a shorter label for an attached worktree", () => {534 it("uses a shorter label for an attached worktree", () => {
517 expect(resolveLockedWorkspaceLabel("/repo/.t3/worktrees/feature-a")).toBe("Worktree");535 expect(resolveLockedWorkspaceLabel("/repo/.t3/worktrees/feature-a", "worktree")).toBe(
536 "Worktree",
537 );
538 });
539
540 it("describes a worktree that is still being created as a new worktree", () => {
541 expect(resolveLockedWorkspaceLabel(null, "worktree")).toBe("New worktree");
518 });542 });
519});543});
520544
apps/web/src/components/BranchToolbar.logic.ts+15-4
...@@ -104,8 +104,14 @@ export function resolveCurrentWorkspaceLabel(activeWorktreePath: string | null):...@@ -104,8 +104,14 @@ export function resolveCurrentWorkspaceLabel(activeWorktreePath: string | null):
104 return activeWorktreePath ? "Current worktree" : resolveEnvModeLabel("local");104 return activeWorktreePath ? "Current worktree" : resolveEnvModeLabel("local");
105}105}
106106
107export function resolveLockedWorkspaceLabel(activeWorktreePath: string | null): string {107// A locked thread in worktree mode with no path is still creating its
108 return activeWorktreePath ? "Worktree" : "Local checkout";108// worktree, so it reads as a new worktree rather than the project checkout.
109export function resolveLockedWorkspaceLabel(
110 activeWorktreePath: string | null,
111 effectiveEnvMode: EnvMode,
112): string {
113 if (activeWorktreePath) return "Worktree";
114 return effectiveEnvMode === "worktree" ? resolveEnvModeLabel("worktree") : "Local checkout";
109}115}
110116
111export interface PreviousWorktreeSeed {117export interface PreviousWorktreeSeed {
...@@ -159,15 +165,20 @@ export function resolveEffectiveEnvMode(input: {...@@ -159,15 +165,20 @@ export function resolveEffectiveEnvMode(input: {
159 activeWorktreePath: string | null;165 activeWorktreePath: string | null;
160 hasServerThread: boolean;166 hasServerThread: boolean;
161 draftThreadEnvMode: EnvMode | undefined;167 draftThreadEnvMode: EnvMode | undefined;
168 /**
169 * The server is still creating this thread's worktree. The thread exists
170 * from the start of that setup but gets its worktree path only at the end.
171 */
172 preparingWorktree?: boolean;
162}): EnvMode {173}): EnvMode {
163 const { activeWorktreePath, hasServerThread, draftThreadEnvMode } = input;174 const { activeWorktreePath, hasServerThread, draftThreadEnvMode, preparingWorktree } = input;
164 if (!hasServerThread) {175 if (!hasServerThread) {
165 if (activeWorktreePath) {176 if (activeWorktreePath) {
166 return "local";177 return "local";
167 }178 }
168 return draftThreadEnvMode === "worktree" ? "worktree" : "local";179 return draftThreadEnvMode === "worktree" ? "worktree" : "local";
169 }180 }
170 return activeWorktreePath ? "worktree" : "local";181 return activeWorktreePath || preparingWorktree ? "worktree" : "local";
171}182}
172183
173export function resolveDraftEnvModeAfterBranchChange(input: {184export function resolveDraftEnvModeAfterBranchChange(input: {
apps/web/src/components/BranchToolbar.tsx+6-16
...@@ -28,7 +28,6 @@ import {...@@ -28,7 +28,6 @@ import {
28 resolveContextStripLabelsCompact,28 resolveContextStripLabelsCompact,
29 resolveCurrentWorkspaceLabel,29 resolveCurrentWorkspaceLabel,
30 resolveEnvModeLabel,30 resolveEnvModeLabel,
31 resolveEffectiveEnvMode,
32 resolveLockedWorkspaceLabel,31 resolveLockedWorkspaceLabel,
33 resolvePreviousWorktreeLabel,32 resolvePreviousWorktreeLabel,
34 resolvePreviousWorktreeSeed,33 resolvePreviousWorktreeSeed,
...@@ -74,7 +73,8 @@ interface BranchToolbarProps {...@@ -74,7 +73,8 @@ interface BranchToolbarProps {
74 showGitControls: boolean;73 showGitControls: boolean;
75 draftId?: DraftId;74 draftId?: DraftId;
76 onEnvModeChange: (mode: EnvMode) => void;75 onEnvModeChange: (mode: EnvMode) => void;
77 effectiveEnvModeOverride?: EnvMode;76 /** The thread's env mode as ChatView resolves it. */
77 envMode: EnvMode;
78 activeThreadBranchOverride?: string | null;78 activeThreadBranchOverride?: string | null;
79 onActiveThreadBranchOverrideChange?: (branch: string | null) => void;79 onActiveThreadBranchOverrideChange?: (branch: string | null) => void;
80 startFromOrigin: boolean;80 startFromOrigin: boolean;
...@@ -141,7 +141,7 @@ const MobileRunContextSelector = memo(function MobileRunContextSelector({...@@ -141,7 +141,7 @@ const MobileRunContextSelector = memo(function MobileRunContextSelector({
141 const workspaceLabel = forceNewWorktree141 const workspaceLabel = forceNewWorktree
142 ? resolveEnvModeLabel("worktree")142 ? resolveEnvModeLabel("worktree")
143 : envModeLocked143 : envModeLocked
144 ? resolveLockedWorkspaceLabel(activeWorktreePath)144 ? resolveLockedWorkspaceLabel(activeWorktreePath, effectiveEnvMode)
145 : effectiveEnvMode === "worktree"145 : effectiveEnvMode === "worktree"
146 ? resolveEnvModeLabel("worktree")146 ? resolveEnvModeLabel("worktree")
147 : resolveCurrentWorkspaceLabel(activeWorktreePath);147 : resolveCurrentWorkspaceLabel(activeWorktreePath);
...@@ -511,7 +511,7 @@ export const BranchToolbar = memo(function BranchToolbar({...@@ -511,7 +511,7 @@ export const BranchToolbar = memo(function BranchToolbar({
511 showGitControls,511 showGitControls,
512 draftId,512 draftId,
513 onEnvModeChange,513 onEnvModeChange,
514 effectiveEnvModeOverride,514 envMode,
515 activeThreadBranchOverride,515 activeThreadBranchOverride,
516 onActiveThreadBranchOverrideChange,516 onActiveThreadBranchOverrideChange,
517 startFromOrigin,517 startFromOrigin,
...@@ -546,13 +546,7 @@ export const BranchToolbar = memo(function BranchToolbar({...@@ -546,13 +546,7 @@ export const BranchToolbar = memo(function BranchToolbar({
546 const activeWorktreePath = forceNewWorktree546 const activeWorktreePath = forceNewWorktree
547 ? null547 ? null
548 : (serverThread?.worktreePath ?? draftThread?.worktreePath ?? null);548 : (serverThread?.worktreePath ?? draftThread?.worktreePath ?? null);
549 const effectiveEnvMode =549 const effectiveEnvMode = forceNewWorktree ? "worktree" : envMode;
550 (forceNewWorktree ? "worktree" : effectiveEnvModeOverride) ??
551 resolveEffectiveEnvMode({
552 activeWorktreePath,
553 hasServerThread: serverThread !== null,
554 draftThreadEnvMode: draftThread?.envMode,
555 });
556 const envModeLocked = envLocked || (serverThread !== null && activeWorktreePath !== null);550 const envModeLocked = envLocked || (serverThread !== null && activeWorktreePath !== null);
557551
558 // "Previous worktree" hops a draft into the most recently active worktree552 // "Previous worktree" hops a draft into the most recently active worktree
...@@ -720,11 +714,7 @@ export const BranchToolbar = memo(function BranchToolbar({...@@ -720,11 +714,7 @@ export const BranchToolbar = memo(function BranchToolbar({
720 threadId={threadId}714 threadId={threadId}
721 {...(draftId ? { draftId } : {})}715 {...(draftId ? { draftId } : {})}
722 envLocked={envLocked}716 envLocked={envLocked}
723 {...(forceNewWorktree717 effectiveEnvModeOverride={effectiveEnvMode}
724 ? { effectiveEnvModeOverride: "worktree" }
725 : effectiveEnvModeOverride
726 ? { effectiveEnvModeOverride }
727 : {})}
728 {...(activeThreadBranchOverride !== undefined ? { activeThreadBranchOverride } : {})}718 {...(activeThreadBranchOverride !== undefined ? { activeThreadBranchOverride } : {})}
729 {...(onActiveThreadBranchOverrideChange ? { onActiveThreadBranchOverrideChange } : {})}719 {...(onActiveThreadBranchOverrideChange ? { onActiveThreadBranchOverrideChange } : {})}
730 startFromOrigin={startFromOrigin}720 startFromOrigin={startFromOrigin}
apps/web/src/components/BranchToolbarEnvModeSelector.tsx+5-7
...@@ -64,10 +64,10 @@ export const BranchToolbarEnvModeSelector = memo(function BranchToolbarEnvModeSe...@@ -64,10 +64,10 @@ export const BranchToolbarEnvModeSelector = memo(function BranchToolbarEnvModeSe
64 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"64 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"
65 data-composer-context-control65 data-composer-context-control
66 >66 >
67 {forceNewWorktree ? (67 {activeWorktreePath ? (
68 <FolderGit2Icon className="size-3 shrink-0" />
69 ) : activeWorktreePath ? (
70 <FolderGitIcon className="size-3 shrink-0" />68 <FolderGitIcon className="size-3 shrink-0" />
69 ) : effectiveEnvMode === "worktree" ? (
70 <FolderGit2Icon className="size-3 shrink-0" />
71 ) : (71 ) : (
72 <FolderIcon className="size-3 shrink-0" />72 <FolderIcon className="size-3 shrink-0" />
73 )}73 )}
...@@ -79,16 +79,14 @@ export const BranchToolbarEnvModeSelector = memo(function BranchToolbarEnvModeSe...@@ -79,16 +79,14 @@ export const BranchToolbarEnvModeSelector = memo(function BranchToolbarEnvModeSe
79 data-composer-label-motion79 data-composer-label-motion
80 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"80 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"
81 >81 >
82 {forceNewWorktree82 {resolveLockedWorkspaceLabel(activeWorktreePath, effectiveEnvMode)}
83 ? resolveEnvModeLabel("worktree")
84 : resolveLockedWorkspaceLabel(activeWorktreePath)}
85 </span>83 </span>
86 </span>84 </span>
87 </TooltipTrigger>85 </TooltipTrigger>
88 <TooltipPopup>86 <TooltipPopup>
89 {forceNewWorktree87 {forceNewWorktree
90 ? "Each model starts in its own worktree."88 ? "Each model starts in its own worktree."
91 : resolveLockedWorkspaceLabel(activeWorktreePath)}89 : resolveLockedWorkspaceLabel(activeWorktreePath, effectiveEnvMode)}
92 </TooltipPopup>90 </TooltipPopup>
93 </Tooltip>91 </Tooltip>
94 );92 );
apps/web/src/components/ChatView.tsx+2-3
...@@ -5845,6 +5845,7 @@ export default function ChatView(props: ChatViewProps) {...@@ -5845,6 +5845,7 @@ export default function ChatView(props: ChatViewProps) {
5845 activeWorktreePath,5845 activeWorktreePath,
5846 hasServerThread: isServerThread,5846 hasServerThread: isServerThread,
5847 draftThreadEnvMode: isLocalDraftThread ? draftThread?.envMode : undefined,5847 draftThreadEnvMode: isLocalDraftThread ? draftThread?.envMode : undefined,
5848 preparingWorktree: isPreparingWorktree,
5848 });5849 });
5849 const canOverrideServerThreadEnvMode = Boolean(5850 const canOverrideServerThreadEnvMode = Boolean(
5850 isServerThread &&5851 isServerThread &&
...@@ -10238,9 +10239,7 @@ export default function ChatView(props: ChatViewProps) {...@@ -10238,9 +10239,7 @@ export default function ChatView(props: ChatViewProps) {
10238 onEnvModeChange={onEnvModeChange}10239 onEnvModeChange={onEnvModeChange}
10239 startFromOrigin={startFromOrigin}10240 startFromOrigin={startFromOrigin}
10240 onStartFromOriginChange={onStartFromOriginChange}10241 onStartFromOriginChange={onStartFromOriginChange}
10241 {...(canOverrideServerThreadEnvMode10242 envMode={envMode}
10242 ? { effectiveEnvModeOverride: envMode }
10243 : {})}
10244 {...(canOverrideServerThreadEnvMode10243 {...(canOverrideServerThreadEnvMode
10245 ? {10244 ? {
10246 activeThreadBranchOverride: activeThreadBranch,10245 activeThreadBranchOverride: activeThreadBranch,