| author | |
| committer | |
| log | 64a991ad455234e6fdd808a5a3caadc51f18a152 |
| tree | 6092e27e03ee32be715d335488f358349e05f934 |
| parent | 0ec4fbc4a376cbf6465e59b05506ce9d89b3d078 |
| signature | Signed by PGP key B5690EEEBB952194 |
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"; |
| 22 | 23 | ||
| ... | @@ -421,6 +422,38 @@ describe("shouldShowEnvironmentIndicator", () => { | ... | @@ -421,6 +422,38 @@ describe("shouldShowEnvironmentIndicator", () => { |
| 421 | }); | 422 | }); |
| 422 | }); | 423 | }); |
| 423 | 424 | ||
| 425 | describe("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 | |||
| 424 | describe("resolveEffectiveEnvMode", () => { | 457 | describe("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 | } |
| 56 | 56 | ||
| 57 | export function shouldShowComposerContextStrip(input: { | ||
| 58 | hasActiveProject: boolean; | ||
| 59 | isGitRepo: boolean; | ||
| 60 | showEnvironmentIndicator: boolean; | ||
| 61 | }): boolean { | ||
| 62 | return input.hasActiveProject && (input.isGitRepo || input.showEnvironmentIndicator); | ||
| 63 | } | ||
| 64 | |||
| 57 | export function resolveEnvModeLabel(mode: EnvMode): string { | 65 | export 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"; |
| 44 | interface BranchToolbarProps { | 44 | interface 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 { |
| 309 | export const BranchToolbar = memo(function BranchToolbar({ | 310 | export 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 | <MobileRunContextSelector | 409 | <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 | <BranchToolbarEnvModeSelector | 438 | {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 | )} |
| 444 | 450 | ||
| 445 | <BranchToolbarBranchSelector | 451 | {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"; |
| 244 | import { PanelLayoutControls, RightPanelMaximizeControl } from "./chat/PanelLayoutControls"; | 244 | import { PanelLayoutControls, RightPanelMaximizeControl } from "./chat/PanelLayoutControls"; |
| 245 | import { type ExpandedImagePreview } from "./chat/ExpandedImagePreview"; | 245 | import { type ExpandedImagePreview } from "./chat/ExpandedImagePreview"; |
| 246 | import { NoActiveThreadState } from "./NoActiveThreadState"; | 246 | import { NoActiveThreadState } from "./NoActiveThreadState"; |
| 247 | import { resolveEffectiveEnvMode, resolveLocalCheckoutBranchMismatch } from "./BranchToolbar.logic"; | 247 | import { |
| 248 | resolveEffectiveEnvMode, | ||
| 249 | resolveLocalCheckoutBranchMismatch, | ||
| 250 | shouldShowComposerContextStrip, | ||
| 251 | shouldShowEnvironmentIndicator, | ||
| 252 | } from "./BranchToolbar.logic"; | ||
| 248 | import { | 253 | import { |
| 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 | }); | ||
| 1748 | 1761 | ||
| 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 | <BranchToolbar | 6197 | <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} |