import { scopeProjectRef, scopeThreadRef } from "@t3tools/client-runtime/environment"; import type { EnvironmentId, ThreadId } from "@t3tools/contracts"; import { ChevronDownIcon, CloudIcon, FolderGit2Icon, FolderGitIcon, FolderIcon, MonitorIcon, } from "lucide-react"; import { memo, useMemo } from "react"; import { useComposerDraftStore, type DraftId } from "../composerDraftStore"; import { useProject, useThread } from "../state/entities"; import { useIsMobile } from "../hooks/useMediaQuery"; import { type EnvMode, type EnvironmentOption, resolveCurrentWorkspaceLabel, resolveEnvModeLabel, resolveEffectiveEnvMode, resolveLockedWorkspaceLabel, } from "./BranchToolbar.logic"; import { BranchToolbarBranchSelector } from "./BranchToolbarBranchSelector"; import { BranchToolbarEnvironmentSelector } from "./BranchToolbarEnvironmentSelector"; import { BranchToolbarEnvModeSelector } from "./BranchToolbarEnvModeSelector"; import { Button } from "./ui/button"; import { Menu, MenuGroup, MenuGroupLabel, MenuPopup, MenuRadioGroup, MenuRadioItem, MenuSeparator, MenuTrigger, } from "./ui/menu"; import { Separator } from "./ui/separator"; interface BranchToolbarProps { environmentId: EnvironmentId; threadId: ThreadId; draftId?: DraftId; onEnvModeChange: (mode: EnvMode) => void; effectiveEnvModeOverride?: EnvMode; activeThreadBranchOverride?: string | null; onActiveThreadBranchOverrideChange?: (branch: string | null) => void; startFromOrigin: boolean; onStartFromOriginChange: (startFromOrigin: boolean) => void; envLocked: boolean; onCheckoutPullRequestRequest?: (reference: string) => void; onComposerFocusRequest?: () => void; availableEnvironments?: readonly EnvironmentOption[]; onEnvironmentChange?: (environmentId: EnvironmentId) => void; } interface MobileRunContextSelectorProps { envLocked: boolean; envModeLocked: boolean; environmentId: EnvironmentId; availableEnvironments: readonly EnvironmentOption[] | undefined; showEnvironmentPicker: boolean; onEnvironmentChange: ((environmentId: EnvironmentId) => void) | undefined; effectiveEnvMode: EnvMode; activeWorktreePath: string | null; onEnvModeChange: (mode: EnvMode) => void; } const MobileRunContextSelector = memo(function MobileRunContextSelector({ envLocked, envModeLocked, environmentId, availableEnvironments, showEnvironmentPicker, onEnvironmentChange, effectiveEnvMode, activeWorktreePath, onEnvModeChange, }: MobileRunContextSelectorProps) { const activeEnvironment = useMemo( () => availableEnvironments?.find((env) => env.environmentId === environmentId) ?? null, [availableEnvironments, environmentId], ); const WorkspaceIcon = effectiveEnvMode === "worktree" ? FolderGit2Icon : activeWorktreePath ? FolderGitIcon : FolderIcon; const workspaceLabel = envModeLocked ? resolveLockedWorkspaceLabel(activeWorktreePath) : effectiveEnvMode === "worktree" ? resolveEnvModeLabel("worktree") : resolveCurrentWorkspaceLabel(activeWorktreePath); const isLocked = envLocked || envModeLocked; const EnvironmentIcon = activeEnvironment?.isPrimary ? MonitorIcon : CloudIcon; const icon = showEnvironmentPicker ? ( // Button's base styles apply `-mx-0.5` to descendant SVGs, which eats 4px // out of whatever gap we set. mx-0! cancels that so gap-0.5 reads as 2px. ) : ( ); const triggerContent = ( <> {icon} {showEnvironmentPicker ? (activeEnvironment?.label ?? "Run on") : workspaceLabel} ); if (isLocked) { return ( {triggerContent} ); } return ( } className="min-w-0 max-w-[48%] flex-1 justify-start text-muted-foreground/70 hover:text-foreground/80 md:hidden" > {triggerContent} {showEnvironmentPicker && availableEnvironments && onEnvironmentChange ? ( <> Run on onEnvironmentChange(value as EnvironmentId)} > {availableEnvironments.map((env) => { const Icon = env.isPrimary ? MonitorIcon : CloudIcon; return ( {env.label} ); })} ) : null} Workspace onEnvModeChange(value as EnvMode)} > {activeWorktreePath ? ( ) : ( )} {resolveCurrentWorkspaceLabel(activeWorktreePath)} {resolveEnvModeLabel("worktree")} ); }); export const BranchToolbar = memo(function BranchToolbar({ environmentId, threadId, draftId, onEnvModeChange, effectiveEnvModeOverride, activeThreadBranchOverride, onActiveThreadBranchOverrideChange, startFromOrigin, onStartFromOriginChange, envLocked, onCheckoutPullRequestRequest, onComposerFocusRequest, availableEnvironments, onEnvironmentChange, }: BranchToolbarProps) { const threadRef = useMemo( () => scopeThreadRef(environmentId, threadId), [environmentId, threadId], ); const serverThread = useThread(threadRef); const draftThread = useComposerDraftStore((store) => draftId ? store.getDraftSession(draftId) : store.getDraftThreadByRef(threadRef), ); const activeProjectRef = serverThread ? scopeProjectRef(serverThread.environmentId, serverThread.projectId) : draftThread ? scopeProjectRef(draftThread.environmentId, draftThread.projectId) : null; const activeProject = useProject(activeProjectRef); const hasActiveThread = serverThread !== null || draftThread !== null; const activeWorktreePath = serverThread?.worktreePath ?? draftThread?.worktreePath ?? null; const effectiveEnvMode = effectiveEnvModeOverride ?? resolveEffectiveEnvMode({ activeWorktreePath, hasServerThread: serverThread !== null, draftThreadEnvMode: draftThread?.envMode, }); const envModeLocked = envLocked || (serverThread !== null && activeWorktreePath !== null); const showEnvironmentPicker = Boolean( availableEnvironments && availableEnvironments.length > 1 && onEnvironmentChange, ); const isMobile = useIsMobile(); if (!hasActiveThread || !activeProject) return null; return (
{isMobile ? ( ) : (
{showEnvironmentPicker && availableEnvironments && onEnvironmentChange && ( <> )}
)}
); });