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", () => {
489489 }),
490490 ).toBe("worktree");
491491 });
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 });
492510});
493511
494512describe("resolveEnvModeLabel", () => {
......@@ -510,11 +528,17 @@ describe("resolveCurrentWorkspaceLabel", () => {
510528
511529describe("resolveLockedWorkspaceLabel", () => {
512530 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");
514532 });
515533
516534 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");
518542 });
519543});
520544
apps/web/src/components/BranchToolbar.logic.ts+15-4
......@@ -104,8 +104,14 @@ export function resolveCurrentWorkspaceLabel(activeWorktreePath: string | null):
104104 return activeWorktreePath ? "Current worktree" : resolveEnvModeLabel("local");
105105}
106106
107export function resolveLockedWorkspaceLabel(activeWorktreePath: string | null): string {
108 return activeWorktreePath ? "Worktree" : "Local checkout";
107// A locked thread in worktree mode with no path is still creating its
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";
109115}
110116
111117export interface PreviousWorktreeSeed {
......@@ -159,15 +165,20 @@ export function resolveEffectiveEnvMode(input: {
159165 activeWorktreePath: string | null;
160166 hasServerThread: boolean;
161167 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;
162173}): EnvMode {
163 const { activeWorktreePath, hasServerThread, draftThreadEnvMode } = input;
174 const { activeWorktreePath, hasServerThread, draftThreadEnvMode, preparingWorktree } = input;
164175 if (!hasServerThread) {
165176 if (activeWorktreePath) {
166177 return "local";
167178 }
168179 return draftThreadEnvMode === "worktree" ? "worktree" : "local";
169180 }
170 return activeWorktreePath ? "worktree" : "local";
181 return activeWorktreePath || preparingWorktree ? "worktree" : "local";
171182}
172183
173184export function resolveDraftEnvModeAfterBranchChange(input: {
apps/web/src/components/BranchToolbar.tsx+6-16
......@@ -28,7 +28,6 @@ import {
2828 resolveContextStripLabelsCompact,
2929 resolveCurrentWorkspaceLabel,
3030 resolveEnvModeLabel,
31 resolveEffectiveEnvMode,
3231 resolveLockedWorkspaceLabel,
3332 resolvePreviousWorktreeLabel,
3433 resolvePreviousWorktreeSeed,
......@@ -74,7 +73,8 @@ interface BranchToolbarProps {
7473 showGitControls: boolean;
7574 draftId?: DraftId;
7675 onEnvModeChange: (mode: EnvMode) => void;
77 effectiveEnvModeOverride?: EnvMode;
76 /** The thread's env mode as ChatView resolves it. */
77 envMode: EnvMode;
7878 activeThreadBranchOverride?: string | null;
7979 onActiveThreadBranchOverrideChange?: (branch: string | null) => void;
8080 startFromOrigin: boolean;
......@@ -141,7 +141,7 @@ const MobileRunContextSelector = memo(function MobileRunContextSelector({
141141 const workspaceLabel = forceNewWorktree
142142 ? resolveEnvModeLabel("worktree")
143143 : envModeLocked
144 ? resolveLockedWorkspaceLabel(activeWorktreePath)
144 ? resolveLockedWorkspaceLabel(activeWorktreePath, effectiveEnvMode)
145145 : effectiveEnvMode === "worktree"
146146 ? resolveEnvModeLabel("worktree")
147147 : resolveCurrentWorkspaceLabel(activeWorktreePath);
......@@ -511,7 +511,7 @@ export const BranchToolbar = memo(function BranchToolbar({
511511 showGitControls,
512512 draftId,
513513 onEnvModeChange,
514 effectiveEnvModeOverride,
514 envMode,
515515 activeThreadBranchOverride,
516516 onActiveThreadBranchOverrideChange,
517517 startFromOrigin,
......@@ -546,13 +546,7 @@ export const BranchToolbar = memo(function BranchToolbar({
546546 const activeWorktreePath = forceNewWorktree
547547 ? null
548548 : (serverThread?.worktreePath ?? draftThread?.worktreePath ?? null);
549 const effectiveEnvMode =
550 (forceNewWorktree ? "worktree" : effectiveEnvModeOverride) ??
551 resolveEffectiveEnvMode({
552 activeWorktreePath,
553 hasServerThread: serverThread !== null,
554 draftThreadEnvMode: draftThread?.envMode,
555 });
549 const effectiveEnvMode = forceNewWorktree ? "worktree" : envMode;
556550 const envModeLocked = envLocked || (serverThread !== null && activeWorktreePath !== null);
557551
558552 // "Previous worktree" hops a draft into the most recently active worktree
......@@ -720,11 +714,7 @@ export const BranchToolbar = memo(function BranchToolbar({
720714 threadId={threadId}
721715 {...(draftId ? { draftId } : {})}
722716 envLocked={envLocked}
723 {...(forceNewWorktree
724 ? { effectiveEnvModeOverride: "worktree" }
725 : effectiveEnvModeOverride
726 ? { effectiveEnvModeOverride }
727 : {})}
717 effectiveEnvModeOverride={effectiveEnvMode}
728718 {...(activeThreadBranchOverride !== undefined ? { activeThreadBranchOverride } : {})}
729719 {...(onActiveThreadBranchOverrideChange ? { onActiveThreadBranchOverrideChange } : {})}
730720 startFromOrigin={startFromOrigin}
apps/web/src/components/BranchToolbarEnvModeSelector.tsx+5-7
......@@ -64,10 +64,10 @@ export const BranchToolbarEnvModeSelector = memo(function BranchToolbarEnvModeSe
6464 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"
6565 data-composer-context-control
6666 >
67 {forceNewWorktree ? (
68 <FolderGit2Icon className="size-3 shrink-0" />
69 ) : activeWorktreePath ? (
67 {activeWorktreePath ? (
7068 <FolderGitIcon className="size-3 shrink-0" />
69 ) : effectiveEnvMode === "worktree" ? (
70 <FolderGit2Icon className="size-3 shrink-0" />
7171 ) : (
7272 <FolderIcon className="size-3 shrink-0" />
7373 )}
......@@ -79,16 +79,14 @@ export const BranchToolbarEnvModeSelector = memo(function BranchToolbarEnvModeSe
7979 data-composer-label-motion
8080 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"
8181 >
82 {forceNewWorktree
83 ? resolveEnvModeLabel("worktree")
84 : resolveLockedWorkspaceLabel(activeWorktreePath)}
82 {resolveLockedWorkspaceLabel(activeWorktreePath, effectiveEnvMode)}
8583 </span>
8684 </span>
8785 </TooltipTrigger>
8886 <TooltipPopup>
8987 {forceNewWorktree
9088 ? "Each model starts in its own worktree."
91 : resolveLockedWorkspaceLabel(activeWorktreePath)}
89 : resolveLockedWorkspaceLabel(activeWorktreePath, effectiveEnvMode)}
9290 </TooltipPopup>
9391 </Tooltip>
9492 );
apps/web/src/components/ChatView.tsx+2-3
......@@ -5845,6 +5845,7 @@ export default function ChatView(props: ChatViewProps) {
58455845 activeWorktreePath,
58465846 hasServerThread: isServerThread,
58475847 draftThreadEnvMode: isLocalDraftThread ? draftThread?.envMode : undefined,
5848 preparingWorktree: isPreparingWorktree,
58485849 });
58495850 const canOverrideServerThreadEnvMode = Boolean(
58505851 isServerThread &&
......@@ -10238,9 +10239,7 @@ export default function ChatView(props: ChatViewProps) {
1023810239 onEnvModeChange={onEnvModeChange}
1023910240 startFromOrigin={startFromOrigin}
1024010241 onStartFromOriginChange={onStartFromOriginChange}
10241 {...(canOverrideServerThreadEnvMode
10242 ? { effectiveEnvModeOverride: envMode }
10243 : {})}
10242 envMode={envMode}
1024410243 {...(canOverrideServerThreadEnvMode
1024510244 ? {
1024610245 activeThreadBranchOverride: activeThreadBranch,