authorgravatar for me@t3.ggTheo Browne <me@t3.gg> 2026-08-06 20:55:00-04:00
committergravatar for noreply@github.comGitHub <noreply@github.com> 2026-08-06 17:55:00-07:00
log64a991ad455234e6fdd808a5a3caadc51f18a152
tree6092e27e03ee32be715d335488f358349e05f934
parent0ec4fbc4a376cbf6465e59b05506ce9d89b3d078
signature Signed by PGP key B5690EEEBB952194

fix(web): show remote environment for non-Git projects (#5555)


4 files changed, 93 insertions(+), 26 deletions(-)

apps/web/src/components/BranchToolbar.logic.test.ts+33
...@@ -17,6 +17,7 @@ import {...@@ -17,6 +17,7 @@ import {
17 resolvePreviousWorktreeLabel,17 resolvePreviousWorktreeLabel,
18 resolvePreviousWorktreeSeed,18 resolvePreviousWorktreeSeed,
19 shouldIncludeBranchPickerItem,19 shouldIncludeBranchPickerItem,
20 shouldShowComposerContextStrip,
20 shouldShowEnvironmentIndicator,21 shouldShowEnvironmentIndicator,
21} from "./BranchToolbar.logic";22} from "./BranchToolbar.logic";
2223
...@@ -421,6 +422,38 @@ describe("shouldShowEnvironmentIndicator", () => {...@@ -421,6 +422,38 @@ describe("shouldShowEnvironmentIndicator", () => {
421 });422 });
422});423});
423424
425describe("shouldShowComposerContextStrip", () => {
426 it("keeps the environment indicator visible for a non-Git project", () => {
427 expect(
428 shouldShowComposerContextStrip({
429 hasActiveProject: true,
430 isGitRepo: false,
431 showEnvironmentIndicator: true,
432 }),
433 ).toBe(true);
434 });
435
436 it("hides the strip when a non-Git project has no environment indicator", () => {
437 expect(
438 shouldShowComposerContextStrip({
439 hasActiveProject: true,
440 isGitRepo: false,
441 showEnvironmentIndicator: false,
442 }),
443 ).toBe(false);
444 });
445
446 it("shows Git controls without requiring an environment indicator", () => {
447 expect(
448 shouldShowComposerContextStrip({
449 hasActiveProject: true,
450 isGitRepo: true,
451 showEnvironmentIndicator: false,
452 }),
453 ).toBe(true);
454 });
455});
456
424describe("resolveEffectiveEnvMode", () => {457describe("resolveEffectiveEnvMode", () => {
425 it("treats draft threads already attached to a worktree as current-checkout mode", () => {458 it("treats draft threads already attached to a worktree as current-checkout mode", () => {
426 expect(459 expect(
apps/web/src/components/BranchToolbar.logic.ts+8
...@@ -54,6 +54,14 @@ export function shouldShowEnvironmentIndicator(input: {...@@ -54,6 +54,14 @@ export function shouldShowEnvironmentIndicator(input: {
54 return input.activeEnvironment !== null && !input.activeEnvironment.isPrimary;54 return input.activeEnvironment !== null && !input.activeEnvironment.isPrimary;
55}55}
5656
57export function shouldShowComposerContextStrip(input: {
58 hasActiveProject: boolean;
59 isGitRepo: boolean;
60 showEnvironmentIndicator: boolean;
61}): boolean {
62 return input.hasActiveProject && (input.isGitRepo || input.showEnvironmentIndicator);
63}
64
57export function resolveEnvModeLabel(mode: EnvMode): string {65export function resolveEnvModeLabel(mode: EnvMode): string {
58 return mode === "worktree" ? "New worktree" : "Current checkout";66 return mode === "worktree" ? "New worktree" : "Current checkout";
59}67}
apps/web/src/components/BranchToolbar.tsx+32-24
...@@ -44,6 +44,7 @@ import { Separator } from "./ui/separator";...@@ -44,6 +44,7 @@ import { Separator } from "./ui/separator";
44interface BranchToolbarProps {44interface BranchToolbarProps {
45 environmentId: EnvironmentId;45 environmentId: EnvironmentId;
46 threadId: ThreadId;46 threadId: ThreadId;
47 showGitControls: boolean;
47 draftId?: DraftId;48 draftId?: DraftId;
48 onEnvModeChange: (mode: EnvMode) => void;49 onEnvModeChange: (mode: EnvMode) => void;
49 effectiveEnvModeOverride?: EnvMode;50 effectiveEnvModeOverride?: EnvMode;
...@@ -309,6 +310,7 @@ function useLabelsOverflow(element: HTMLDivElement | null): boolean {...@@ -309,6 +310,7 @@ function useLabelsOverflow(element: HTMLDivElement | null): boolean {
309export const BranchToolbar = memo(function BranchToolbar({310export const BranchToolbar = memo(function BranchToolbar({
310 environmentId,311 environmentId,
311 threadId,312 threadId,
313 showGitControls,
312 draftId,314 draftId,
313 onEnvModeChange,315 onEnvModeChange,
314 effectiveEnvModeOverride,316 effectiveEnvModeOverride,
...@@ -403,7 +405,7 @@ export const BranchToolbar = memo(function BranchToolbar({...@@ -403,7 +405,7 @@ export const BranchToolbar = memo(function BranchToolbar({
403 data-compact={labelsOverflow ? "" : undefined}405 data-compact={labelsOverflow ? "" : undefined}
404 className="chat-composer-context-strip group/composer-context -mt-4 mx-auto flex w-[calc(100%-2.75rem)] max-w-[calc(48rem-2.75rem)] items-center gap-2 ps-1 pe-2 pt-5 pb-1"406 className="chat-composer-context-strip group/composer-context -mt-4 mx-auto flex w-[calc(100%-2.75rem)] max-w-[calc(48rem-2.75rem)] items-center gap-2 ps-1 pe-2 pt-5 pb-1"
405 >407 >
406 {isMobile ? (408 {isMobile && showGitControls ? (
407 <MobileRunContextSelector409 <MobileRunContextSelector
408 envLocked={envLocked}410 envLocked={envLocked}
409 envModeLocked={envModeLocked}411 envModeLocked={envModeLocked}
...@@ -428,34 +430,40 @@ export const BranchToolbar = memo(function BranchToolbar({...@@ -428,34 +430,40 @@ export const BranchToolbar = memo(function BranchToolbar({
428 availableEnvironments={availableEnvironments}430 availableEnvironments={availableEnvironments}
429 {...(showEnvironmentPicker && onEnvironmentChange ? { onEnvironmentChange } : {})}431 {...(showEnvironmentPicker && onEnvironmentChange ? { onEnvironmentChange } : {})}
430 />432 />
431 <Separator orientation="vertical" className="mx-0.5 h-3.5!" />433 {showGitControls ? (
434 <Separator orientation="vertical" className="mx-0.5 h-3.5!" />
435 ) : null}
432 </>436 </>
433 )}437 )}
434 <BranchToolbarEnvModeSelector438 {showGitControls ? (
435 envLocked={envModeLocked}439 <BranchToolbarEnvModeSelector
436 effectiveEnvMode={effectiveEnvMode}440 envLocked={envModeLocked}
437 activeWorktreePath={activeWorktreePath}441 effectiveEnvMode={effectiveEnvMode}
438 onEnvModeChange={onEnvModeChange}442 activeWorktreePath={activeWorktreePath}
439 previousWorktreeLabel={previousWorktreeLabel}443 onEnvModeChange={onEnvModeChange}
440 onUsePreviousWorktree={onUsePreviousWorktree}444 previousWorktreeLabel={previousWorktreeLabel}
441 />445 onUsePreviousWorktree={onUsePreviousWorktree}
446 />
447 ) : null}
442 </div>448 </div>
443 )}449 )}
444450
445 <BranchToolbarBranchSelector451 {showGitControls ? (
446 className="min-w-0 flex-1 justify-end md:ml-auto md:flex-none"452 <BranchToolbarBranchSelector
447 environmentId={environmentId}453 className="min-w-0 flex-1 justify-end md:ml-auto md:flex-none"
448 threadId={threadId}454 environmentId={environmentId}
449 {...(draftId ? { draftId } : {})}455 threadId={threadId}
450 envLocked={envLocked}456 {...(draftId ? { draftId } : {})}
451 {...(effectiveEnvModeOverride ? { effectiveEnvModeOverride } : {})}457 envLocked={envLocked}
452 {...(activeThreadBranchOverride !== undefined ? { activeThreadBranchOverride } : {})}458 {...(effectiveEnvModeOverride ? { effectiveEnvModeOverride } : {})}
453 {...(onActiveThreadBranchOverrideChange ? { onActiveThreadBranchOverrideChange } : {})}459 {...(activeThreadBranchOverride !== undefined ? { activeThreadBranchOverride } : {})}
454 startFromOrigin={startFromOrigin}460 {...(onActiveThreadBranchOverrideChange ? { onActiveThreadBranchOverrideChange } : {})}
455 onStartFromOriginChange={onStartFromOriginChange}461 startFromOrigin={startFromOrigin}
456 {...(onCheckoutPullRequestRequest ? { onCheckoutPullRequestRequest } : {})}462 onStartFromOriginChange={onStartFromOriginChange}
457 {...(onComposerFocusRequest ? { onComposerFocusRequest } : {})}463 {...(onCheckoutPullRequestRequest ? { onCheckoutPullRequestRequest } : {})}
458 />464 {...(onComposerFocusRequest ? { onComposerFocusRequest } : {})}
465 />
466 ) : null}
459 </div>467 </div>
460 );468 );
461});469});
apps/web/src/components/ChatView.tsx+20-2
...@@ -244,7 +244,12 @@ import { ChatHeader } from "./chat/ChatHeader";...@@ -244,7 +244,12 @@ import { ChatHeader } from "./chat/ChatHeader";
244import { PanelLayoutControls, RightPanelMaximizeControl } from "./chat/PanelLayoutControls";244import { PanelLayoutControls, RightPanelMaximizeControl } from "./chat/PanelLayoutControls";
245import { type ExpandedImagePreview } from "./chat/ExpandedImagePreview";245import { type ExpandedImagePreview } from "./chat/ExpandedImagePreview";
246import { NoActiveThreadState } from "./NoActiveThreadState";246import { NoActiveThreadState } from "./NoActiveThreadState";
247import { resolveEffectiveEnvMode, resolveLocalCheckoutBranchMismatch } from "./BranchToolbar.logic";247import {
248 resolveEffectiveEnvMode,
249 resolveLocalCheckoutBranchMismatch,
250 shouldShowComposerContextStrip,
251 shouldShowEnvironmentIndicator,
252} from "./BranchToolbar.logic";
248import {253import {
249 getProviderStatusBannerKey,254 getProviderStatusBannerKey,
250 ProviderStatusBanner,255 ProviderStatusBanner,
...@@ -1745,6 +1750,14 @@ function ChatViewContent(props: ChatViewProps) {...@@ -1745,6 +1750,14 @@ function ChatViewContent(props: ChatViewProps) {
1745 return envs;1750 return envs;
1746 }, [activeProject, allProjects, projectGroupingSettings, primaryEnvironmentId, environmentById]);1751 }, [activeProject, allProjects, projectGroupingSettings, primaryEnvironmentId, environmentById]);
1747 const hasMultipleEnvironments = logicalProjectEnvironments.length > 1;1752 const hasMultipleEnvironments = logicalProjectEnvironments.length > 1;
1753 const activeEnvironmentOption =
1754 logicalProjectEnvironments.find(
1755 (environment) => environment.environmentId === activeThread?.environmentId,
1756 ) ?? null;
1757 const showComposerEnvironmentIndicator = shouldShowEnvironmentIndicator({
1758 activeEnvironment: activeEnvironmentOption,
1759 canPickEnvironment: hasMultipleEnvironments,
1760 });
17481761
1749 const openPullRequestDialog = useCallback(1762 const openPullRequestDialog = useCallback(
1750 (reference?: string) => {1763 (reference?: string) => {
...@@ -2514,7 +2527,11 @@ function ChatViewContent(props: ChatViewProps) {...@@ -2514,7 +2527,11 @@ function ChatViewContent(props: ChatViewProps) {
2514 terminalUiLaunchContext?.threadId === activeThreadId ? terminalUiLaunchContext : null;2527 terminalUiLaunchContext?.threadId === activeThreadId ? terminalUiLaunchContext : null;
2515 // Default true while loading to avoid toolbar flicker.2528 // Default true while loading to avoid toolbar flicker.
2516 const isGitRepo = gitStatusQuery.data?.isRepo ?? true;2529 const isGitRepo = gitStatusQuery.data?.isRepo ?? true;
2517 const showComposerContextStrip = isGitRepo && activeProject !== null;2530 const showComposerContextStrip = shouldShowComposerContextStrip({
2531 hasActiveProject: activeProject !== null,
2532 isGitRepo,
2533 showEnvironmentIndicator: showComposerEnvironmentIndicator,
2534 });
2518 const initialDiffPanelGitScope =2535 const initialDiffPanelGitScope =
2519 gitStatusQuery.data?.hasWorkingTreeChanges === true ? "unstaged" : "branch";2536 gitStatusQuery.data?.hasWorkingTreeChanges === true ? "unstaged" : "branch";
2520 const diffPanelGitStatusResolutionKey = gitStatusQuery.data ? "resolved" : "pending";2537 const diffPanelGitStatusResolutionKey = gitStatusQuery.data ? "resolved" : "pending";
...@@ -6180,6 +6197,7 @@ function ChatViewContent(props: ChatViewProps) {...@@ -6180,6 +6197,7 @@ function ChatViewContent(props: ChatViewProps) {
6180 <BranchToolbar6197 <BranchToolbar
6181 environmentId={activeThread.environmentId}6198 environmentId={activeThread.environmentId}
6182 threadId={activeThread.id}6199 threadId={activeThread.id}
6200 showGitControls={isGitRepo}
6183 {...(routeKind === "draft" && draftId ? { draftId } : {})}6201 {...(routeKind === "draft" && draftId ? { draftId } : {})}
6184 onEnvModeChange={onEnvModeChange}6202 onEnvModeChange={onEnvModeChange}
6185 startFromOrigin={startFromOrigin}6203 startFromOrigin={startFromOrigin}