import { memo, type MouseEventHandler, type PointerEventHandler } from "react"; import { CheckIcon, ChevronDownIcon, ChevronLeftIcon, Minimize2Icon, PlayIcon } from "lucide-react"; import { CornerUpRight, ListPlus } from "lucide"; import { MorphIcon } from "~/components/MorphIcon"; import { useEnvironmentIdentificationMode } from "~/hooks/useSettings"; import { formatContextWindowTokens } from "~/lib/contextWindow"; import { cn } from "~/lib/utils"; import { useShortcutModifierState } from "../../shortcutModifierState"; import { StageBackdropButtonArt, useSidebarStageBackdropVariant } from "../SidebarStageBackdrop"; import { Button } from "../ui/button"; import { Menu, MenuItem, MenuPopup, MenuTrigger } from "../ui/menu"; import { Spinner } from "../ui/spinner"; import { Tooltip, TooltipPopup, TooltipTrigger } from "../ui/tooltip"; import { composerFloatingLayerProps } from "./composerEventScope"; import { alternateComposerDispatchAction, resolveComposerDispatchMode, } from "@t3tools/client-runtime/state/composer-dispatch"; interface PendingActionState { questionIndex: number; isLastQuestion: boolean; canAdvance: boolean; isResponding: boolean; isComplete: boolean; } interface ComposerPrimaryActionsProps { compact: boolean; canOperateThread: boolean; pendingAction: PendingActionState | null; isRunning: boolean; canInterrupt: boolean; followUpBehavior?: "queue" | "steer"; canSteer?: boolean; alternateShortcutLabel?: string | null; showPlanFollowUpPrompt: boolean; promptHasText: boolean; isSendBusy: boolean; sendDisabledReason: string | null; isConnecting: boolean; isEnvironmentUnavailable: boolean; isPreparingWorktree: boolean; hasSendableContent: boolean; canResume?: boolean; preserveComposerFocusOnPointerDown?: boolean; isEditingQueuedMessage?: boolean; onSubmitMessage?: MouseEventHandler; onResume?: () => void; onPreviousPendingQuestion: () => void; onInterrupt: () => void; onImplementPlanInNewThread: () => void; compactBeforeSendTokens?: number | null; keepFullHistory?: boolean; onToggleKeepFullHistory?: () => void; } const formatPendingPrimaryActionLabel = (input: { compact: boolean; isLastQuestion: boolean; isResponding: boolean; questionIndex: number; }) => { if (input.isResponding) { return "Submitting..."; } if (input.compact) { return input.isLastQuestion ? "Submit" : "Next"; } if (!input.isLastQuestion) { return "Next question"; } return input.questionIndex > 0 ? "Submit answers" : "Submit answer"; }; const messageActionPillClassName = "inline-flex shrink-0 cursor-pointer items-center justify-center gap-1.5 whitespace-nowrap rounded-full bg-message-action font-medium text-base text-message-action-foreground shadow-xs shadow-message-action/24 outline-none hover:bg-message-action-hover focus-visible:ring-2 focus-visible:ring-inset focus-visible:ring-ring focus-visible:ring-offset-1 focus-visible:ring-offset-background disabled:pointer-events-none disabled:opacity-64 disabled:shadow-none sm:text-sm"; const preventPointerFocus: PointerEventHandler = (event) => { event.preventDefault(); }; export const ComposerPrimaryActions = memo(function ComposerPrimaryActions({ compact, canOperateThread, pendingAction, isRunning, canInterrupt, followUpBehavior = "steer", canSteer = true, alternateShortcutLabel = null, showPlanFollowUpPrompt, promptHasText, isSendBusy, sendDisabledReason, isConnecting, isEnvironmentUnavailable, isPreparingWorktree, hasSendableContent, canResume = false, preserveComposerFocusOnPointerDown = false, isEditingQueuedMessage = false, onSubmitMessage, onResume, onPreviousPendingQuestion, onInterrupt, onImplementPlanInNewThread, compactBeforeSendTokens = null, keepFullHistory = false, onToggleKeepFullHistory, }: ComposerPrimaryActionsProps) { const pointerFocusProps = preserveComposerFocusOnPointerDown ? { onPointerDown: preventPointerFocus } : undefined; const environmentIdentificationMode = useEnvironmentIdentificationMode(); const shortcutModifiers = useShortcutModifierState(); const isQueuing = !isEditingQueuedMessage && resolveComposerDispatchMode({ running: isRunning, activeTurnDefault: followUpBehavior, canSteer, alternateModifier: shortcutModifiers.metaKey || shortcutModifiers.ctrlKey, }) === "queue"; const alternateAction = alternateComposerDispatchAction(followUpBehavior); const isSendDisabled = !canOperateThread || sendDisabledReason !== null; const stageBackdropVariant = useSidebarStageBackdropVariant( environmentIdentificationMode === "artwork", ); const renderStopGenerationButton = (insidePendingAction: boolean) => ( { if (canOperateThread) onInterrupt(); }} aria-label="Stop generation" /> } > Interrupt ); if (pendingAction) { return (
{canInterrupt ? renderStopGenerationButton(true) : null} {pendingAction.questionIndex > 0 ? ( compact ? ( ) : ( ) ) : null}
); } if (showPlanFollowUpPrompt && (promptHasText || !canResume)) { if (promptHasText) { return ( ); } return (
event.preventDefault() : undefined } disabled={isSendBusy || isSendDisabled || isConnecting || isEnvironmentUnavailable} /> } > { if (canOperateThread) onImplementPlanInNewThread(); }} > Implement in a new thread
); } if (canInterrupt && !hasSendableContent && !isEditingQueuedMessage) { return renderStopGenerationButton(false); } const showResume = canResume && !hasSendableContent && !isEditingQueuedMessage; const compactTokens = compactBeforeSendTokens !== null && !showResume && !isEditingQueuedMessage ? formatContextWindowTokens(compactBeforeSendTokens) : null; const compactsBeforeSend = compactTokens !== null && !keepFullHistory; const submitLabel = showResume ? "Resume thread" : isEditingQueuedMessage ? "Update queued message" : compactsBeforeSend ? "Compact and send" : isQueuing ? "Queue message" : isRunning ? "Steer message" : "Submit message"; const submitStatus = isEnvironmentUnavailable ? "Environment disconnected" : (sendDisabledReason ?? (isConnecting ? "Connecting" : isPreparingWorktree ? "Preparing worktree" : isSendBusy ? isEditingQueuedMessage ? "Updating queued message" : "Submitting message" : null)); const submitTooltip = submitStatus ?? (compactsBeforeSend ? `Summarize ${compactTokens} tokens of history, then send` : isRunning && !isEditingQueuedMessage && canSteer ? `Click to ${followUpBehavior}, Ctrl/⌘-click${alternateShortcutLabel ? ` or ${alternateShortcutLabel}` : ""} to ${alternateAction}` : submitLabel); const sendButton = ( ); const submit = ( }>{sendButton} {submitTooltip} ); if (compactTokens === null) return submit; return (
} >
{keepFullHistory ? `Next send keeps all ${compactTokens} tokens.` : `Next send compacts ${compactTokens} tokens first.`}
{keepFullHistory ? "Click to compact first" : "Click to keep full history"}
{submit}
); });