import { scopeProjectRef, scopeThreadRef } from "@t3tools/client-runtime/environment";
import type { EnvironmentId, ThreadId } from "@t3tools/contracts";
import {
ChevronDownIcon,
FolderGit2Icon,
FolderGitIcon,
FolderIcon,
HistoryIcon,
ScaleIcon,
} from "lucide-react";
import { memo, useCallback, useEffect, useLayoutEffect, useMemo, useRef, useState } from "react";
import { useComposerDraftStore, type DraftId } from "../composerDraftStore";
import { EnvironmentMachineIcon } from "./EnvironmentMachineIcon";
import { useProject, useThreadShell, useThreadShellsForProjectRefs } from "../state/entities";
import {
type EnvMode,
type EnvironmentOption,
resolveContextStripLabelsCompact,
resolveCurrentWorkspaceLabel,
resolveEnvModeLabel,
resolveEffectiveEnvMode,
resolveLockedWorkspaceLabel,
resolvePreviousWorktreeLabel,
resolvePreviousWorktreeSeed,
shouldShowEnvironmentIndicator,
} 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";
import { ComposerSurface } from "./chat/ComposerSurface";
import { composerFloatingLayerProps } from "./chat/composerEventScope";
import { measureRestingComposerControls } from "./chat/restingComposerControlsMeasurement";
import { resolveRestingComposerControlsNaturalWidth } from "./composerFooterLayout";
import { cn } from "~/lib/utils";
interface BranchToolbarProps {
environmentId: EnvironmentId;
threadId: ThreadId;
showGitControls: boolean;
draftId?: DraftId;
onEnvModeChange: (mode: EnvMode) => void;
effectiveEnvModeOverride?: EnvMode;
activeThreadBranchOverride?: string | null;
onActiveThreadBranchOverrideChange?: (branch: string | null) => void;
startFromOrigin: boolean;
onStartFromOriginChange: (startFromOrigin: boolean) => void;
autoEnvironmentLabel?: string | undefined;
onAutoEnvironment?: (() => void) | undefined;
envLocked: boolean;
onCheckoutPullRequestRequest?: (reference: string) => void;
onComposerFocusRequest?: () => void;
availableEnvironments?: readonly EnvironmentOption[];
onEnvironmentChange?: (environmentId: EnvironmentId) => void;
composerControlsHostRef?: (element: HTMLDivElement | null) => void;
contextStripVisible?: boolean;
}
interface MobileRunContextSelectorProps {
autoEnvironmentLabel?: string | undefined;
onAutoEnvironment?: (() => void) | undefined;
envLocked: boolean;
envModeLocked: boolean;
environmentId: EnvironmentId;
availableEnvironments: readonly EnvironmentOption[] | undefined;
showEnvironmentPicker: boolean;
showEnvironmentIndicator: boolean;
onEnvironmentChange: ((environmentId: EnvironmentId) => void) | undefined;
effectiveEnvMode: EnvMode;
activeWorktreePath: string | null;
onEnvModeChange: (mode: EnvMode) => void;
previousWorktreeLabel: string | null;
onUsePreviousWorktree: () => void;
}
const MobileRunContextSelector = memo(function MobileRunContextSelector({
autoEnvironmentLabel,
onAutoEnvironment,
envLocked,
envModeLocked,
environmentId,
availableEnvironments,
showEnvironmentPicker,
showEnvironmentIndicator,
onEnvironmentChange,
effectiveEnvMode,
activeWorktreePath,
onEnvModeChange,
previousWorktreeLabel,
onUsePreviousWorktree,
}: 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 icon = showEnvironmentIndicator ? (
// 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.
{autoEnvironmentLabel ? (
) : (
)}
) : (
);
const triggerContent = (
<>
{icon}
{autoEnvironmentLabel ??
(showEnvironmentIndicator ? (activeEnvironment?.label ?? "Run on") : workspaceLabel)}
>
);
if (isLocked) {
return (
{triggerContent}
);
}
return (
);
});
/**
* Collapse the strip's labels to icons only when the text no longer fits.
*
* Hidden labels stay measurable because their inner text keeps its natural
* width while the outer layout box collapses. This lets every pass recompute
* the expanded width without remembered values that could go stale or latch
* the strip compact. A small hysteresis keeps the boundary from flapping.
*/
const COMPOSER_CONTEXT_MOTION_DURATION_MS = 180;
const COMPOSER_CONTEXT_MOTION_EASING = "cubic-bezier(0.32, 0.72, 0, 1)";
const COMPOSER_CONTEXT_LABEL_SELECTOR = "[data-composer-label]";
function useLabelsOverflow(element: HTMLDivElement | null): boolean {
const [overflows, setOverflows] = useState(false);
const pendingLabelRectsRef = useRef