| author | |
| committer | |
| log | 13d6b30515e9d44bb4cc2d223b8b3179c2ba4c79 |
| tree | 91a36941c75dd71d98cb7915c680cb4a25b3f6e8 |
| parent | b3de243d5ed44783c4e7990d8e63cb519d390a03 |
| signature | Signed by PGP key B5690EEEBB952194 |
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 | }); |
| 493 | 511 | ||
| 494 | describe("resolveEnvModeLabel", () => { | 512 | describe("resolveEnvModeLabel", () => { |
| ... | @@ -510,11 +528,17 @@ describe("resolveCurrentWorkspaceLabel", () => { | ... | @@ -510,11 +528,17 @@ describe("resolveCurrentWorkspaceLabel", () => { |
| 510 | 528 | ||
| 511 | describe("resolveLockedWorkspaceLabel", () => { | 529 | describe("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 | }); |
| 515 | 533 | ||
| 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 | }); |
| 520 | 544 |
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 | } |
| 106 | 106 | ||
| 107 | export 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. |
| 109 | export 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 | } |
| 110 | 116 | ||
| 111 | export interface PreviousWorktreeSeed { | 117 | export 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 | } |
| 172 | 183 | ||
| 173 | export function resolveDraftEnvModeAfterBranchChange(input: { | 184 | export 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 = forceNewWorktree | 141 | const workspaceLabel = forceNewWorktree |
| 142 | ? resolveEnvModeLabel("worktree") | 142 | ? resolveEnvModeLabel("worktree") |
| 143 | : envModeLocked | 143 | : 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 = forceNewWorktree | 546 | const activeWorktreePath = forceNewWorktree |
| 547 | ? null | 547 | ? 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); |
| 557 | 551 | ||
| 558 | // "Previous worktree" hops a draft into the most recently active worktree | 552 | // "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 | {...(forceNewWorktree | 717 | 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-control | 65 | 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-motion | 79 | 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 | {forceNewWorktree | 82 | {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 | {forceNewWorktree | 87 | {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 | {...(canOverrideServerThreadEnvMode | 10242 | envMode={envMode} |
| 10242 | ? { effectiveEnvModeOverride: envMode } | ||
| 10243 | : {})} | ||
| 10244 | {...(canOverrideServerThreadEnvMode | 10243 | {...(canOverrideServerThreadEnvMode |
| 10245 | ? { | 10244 | ? { |
| 10246 | activeThreadBranchOverride: activeThreadBranch, | 10245 | activeThreadBranchOverride: activeThreadBranch, |