5889 lines · 231.7 KB · TypeScript
RawDownload
import { RefreshIcon } from "~/components/ui/refresh-icon";import {  questionAttachmentDraftId,  useQuestionAttachmentPreparation,  changeQuestionAttachmentPreparation,} from "../../questionAttachments";import type {  ApprovalRequestId,  AssistantCitation,  ChatFileAttachment,  EnvironmentId,  ModelSelection,  PreviewAnnotationPayload,  ProviderApprovalDecision,  ProviderInteractionMode,  ResolvedKeybindingsConfig,  RuntimeMode,  ScopedThreadRef,  ServerProvider,  ThreadId,  SnapShotSource,} from "@t3tools/contracts";import {  ProviderDriverKind,  ProviderInstanceId,  PROVIDER_SEND_TURN_MAX_ATTACHMENTS,  PROVIDER_SEND_TURN_MAX_IMAGE_BYTES,} from "@t3tools/contracts";import type { EnvironmentConnectionPresentation } from "@t3tools/client-runtime/connection";import { serializeComposerFileLink } from "@t3tools/shared/composerTrigger";import { createModelSelection, normalizeModelSlug } from "@t3tools/shared/model";import { USAGE_LIMITS_COMMAND } from "@t3tools/shared/usageLimits";import {  Fragment,  memo,  type ComponentProps,  type ReactNode,  useCallback,  useEffect,  useImperativeHandle,  useLayoutEffect,  useMemo,  useRef,  useState,  useSyncExternalStore,} from "react";import { createPortal } from "react-dom";import {  clampCollapsedComposerCursor,  type ComposerSubmissionIntent,  type ComposerTrigger,  collapseExpandedComposerCursor,  composerSubmissionIntentForEnter,  detectComposerTrigger,  expandCollapsedComposerCursor,  formatAssistantCitationForComposer,  replaceTextRange,} from "../../composer-logic";import { DISCONNECTED_COMPOSER_PLACEHOLDER } from "../../composerPlaceholder";import {  deriveComposerSendState,  getAntigravitySendBlockReason,  readFileAsDataUrl,  resolveComposerInteractionMode,  resolveComposerProviderSelection,  threadShellHasStarted,} from "../ChatView.logic";import {  dataTransferHasComposerMention,  makeComposerMentionDragHandlers,} from "./composerMentionDrag";import {  composerFloatingLayerProps,  isInsideCollapsedComposerControls,  isInsideComposerFloatingLayer,  isInsideRestingComposerControlScope,} from "./composerEventScope";import {  type ComposerFileAttachment,  type ComposerImageAttachment,  type DraftId,  type PersistedComposerFileAttachment,  type PersistedComposerImageAttachment,  composerFileDedupKey,  composerFileMatchesReattachMarker,  composerFileNeedsReattach,  composerTargetKey,  hydrateImagesFromPersisted,  useComposerDraftStore,  useComposerThreadDraft,  useEffectiveComposerModelState,} from "../../composerDraftStore";import {  MAX_STASH_ENTRIES,  partitionStashAttachments,  usePromptStashStore,  type PromptStashEntry,} from "../../promptStashStore";import { ComposerStashBadge } from "./ComposerStashBadge";import { ComposerStashMenu } from "./ComposerStashMenu";import { useComposerMenuState } from "./useComposerMenuState";import { useComposerFocusState } from "./useComposerFocusState";import { useComposerMultilinePrompt } from "./useComposerMultilinePrompt";import {  ComposerTasksBadge,  ComposerTasksContent,  ComposerTasksDrawer,  type ComposerTaskStep,  type ComposerTasksProgress,} from "./ComposerTasksBadge";import { ComposerActivityRow } from "./ComposerActivityStatus";import type { ThreadSyncPhase } from "../../threadSync";import { ComposerBanner } from "./ComposerBanner";import { ComposerSurface } from "./ComposerSurface";import {  ComposerBannerStack,  type ComposerBannerStackContent,  type ComposerBannerStackItem,} from "./ComposerBannerStack";import { compressImageForStash, prepareImageForAttachment } from "../../lib/imageCompression";import {  fileAttachmentTooLargeMessage,  formatAttachmentSize,} from "@t3tools/client-runtime/state/attachments";import {  attachmentsToReleaseOnUploadCapabilityLoss,  classifyComposerAttachmentFile,  fileAttachmentCapabilityBlockReason,  fileAttachmentStagingLimit,  isPreviewableComposerVideo,  normalizeComposerImageFileMimeType,  shouldHandleComposerAttachmentPaste,} from "./composerAttachmentFiles";import {  readAttachmentUpload,  releaseAttachmentUpload,  releaseDraftAttachment,  releasePersistedAttachmentUpload,  retryAttachmentUpload,  startAttachmentUpload,  useAttachmentUploadStore,  verifyStashedAttachmentUpload,} from "../../lib/attachmentUploadQueue";import {  attachmentUploadBlockReason,  formatAttachmentUploadProgress,} from "../../lib/attachmentUploadState";import { isCommandPaletteOpen } from "../../commandPaletteBus";import { getTerminalFocusOwner } from "../../lib/terminalFocus";import type { AssistantCitationSourceAnchor } from "~/lib/assistantTextSelection";import { resolveShortcutCommand, shortcutLabelForCommand } from "../../keybindings";import {  type TerminalContextDraft,  type TerminalContextSelection,  INLINE_TERMINAL_CONTEXT_PLACEHOLDER,  insertInlineTerminalContextPlaceholder,  removeInlineTerminalContextPlaceholder,} from "../../lib/terminalContext";import { useComposerPathSearch } from "../../lib/composerPathSearchState";import { type ElementContextDraft } from "../../lib/elementContext";import { ComposerPendingElementContexts } from "./ComposerPendingElementContexts";import { ComposerPendingReviewComments } from "./ComposerPendingReviewComments";import { ComposerPreviewAnnotationCards } from "./ComposerPreviewAnnotationCards";import {  COMPOSER_FOOTER_COMPACT_BREAKPOINT_PX,  COMPOSER_FOOTER_WIDE_ACTIONS_COMPACT_BREAKPOINT_PX,  getRestingComposerImagePreviewCounts,  resolveRestingComposerControlsLayout,  shouldAnimateComposerRestingTransition,  shouldUseCompactComposerPrimaryActions,  shouldUseCompactComposerFooter,  shouldUseRestingComposerLayout,} from "../composerFooterLayout";import { measureRestingComposerControls } from "./restingComposerControlsMeasurement";import { observeResponsiveBreakpointFade, usePanelAnimationSettings } from "../../panelAnimations";import { type ComposerPromptEditorHandle, ComposerPromptEditor } from "../ComposerPromptEditor";import { ProviderModelPicker } from "./ProviderModelPicker";import { type ComposerCommandItem, ComposerCommandMenu } from "./ComposerCommandMenu";import { ComposerPendingApprovalActions } from "./ComposerPendingApprovalActions";import { CompactComposerControlsMenu } from "./CompactComposerControlsMenu";import { ComposerPrimaryActions } from "./ComposerPrimaryActions";import { ComposerPendingApprovalPanel } from "./ComposerPendingApprovalPanel";import { ComposerPendingUserInputPanel } from "./ComposerPendingUserInputPanel";import { ComposerPlanFollowUpBanner } from "./ComposerPlanFollowUpBanner";import {  ComposerControl,  ComposerControlIcon,  ComposerControlSeparator,  ComposerSelectControl,} from "./ComposerControl";import { resolveComposerMenuActiveItemId } from "./composerMenuHighlight";import {  searchSlashCommandItems,  slashCommandItemsForPromptPosition,} from "./composerSlashCommandSearch";import {  getComposerPromptInjectionState,  getComposerProviderState,  renderProviderTraitsMenuContent,  renderProviderTraitsPicker,} from "./composerProviderState";import { ContextWindowMeter, ContextWindowMeterPlaceholder } from "./ContextWindowMeter";import {  providerSupportsManualCompaction,  resolveContextWindowModelDisplayName,  shouldReserveContextWindowMeter,} from "./ContextWindowMeter.logic";import {  attachVideoThumbnail,  buildExpandedImagePreview,  type ExpandedImagePreview,} from "./ExpandedImagePreview";import {  SNAP_SHOT_ATTACHMENT_FRAME_CLASS,  SnapShotAttachmentDetails,} from "./SnapShotAttachmentDetails";import {  getPendingSnapShotAnimations,  pendingSnapShotAnimationIdsForTarget,  scheduleSnapShotAnimationDestination,  setSnapShotAnimationDestination,  shouldAnimateSnapShotArrival,  subscribeToPendingSnapShotAnimations,} from "../../lib/snapShotAnimation";import { resizeSnapShotSource } from "../../lib/snapShotSource";import { basenameOfPath } from "../../pierre-icons";import { cn, randomUUID } from "~/lib/utils";import {  getComposerPromptLengthValidationMessage,  getComposerSubmissionValidationMessage,  submitComposerDraft,} from "./composerSubmission";import { ComposerPromptLengthValidation } from "./ComposerPromptLengthValidation";import { PierreEntryIcon } from "./PierreEntryIcon";import {  createComposerScrollGestureState,  recordComposerScrollGestureEvent,  shouldCollapseComposerForScrollKey,  resetComposerScrollGesture,  suppressActiveComposerScrollGesture,} from "./composerScrollGesture";import { prepareVideoFirstFrame } from "../../lib/videoFirstFrame"; function ComposerVideoThumbnail({ file }: { file: File }) {  const setVideo = useCallback(    (video: HTMLVideoElement | null) => {      if (!video) return;      return attachVideoThumbnail(video, file);    },    [file],  );   return (    <video      ref={setVideo}      muted      playsInline      preload="metadata"      aria-hidden="true"      onLoadedMetadata={(event) => prepareVideoFirstFrame(event.currentTarget)}      className="pointer-events-none absolute inset-0 size-full object-cover"    />  );} type ComposerCommandMenuPosition = {  bottom: number;  left: number;  maxHeight: number;  width: number;}; function SnapShotAttachmentFrame({  animationId,  animationSource,  arrival,  animateArrival,  className,  ...props}: ComponentProps<"div"> & {  readonly animationId?: string | undefined;  readonly animationSource?: SnapShotSource | undefined;  readonly arrival?: boolean | undefined;  readonly animateArrival?: boolean | undefined;}) {  const frameRef = useRef<HTMLDivElement>(null);   useLayoutEffect(() => {    const frame = frameRef.current;    if (!frame || (!animationId && !arrival)) return;    frame.scrollIntoView({ block: "nearest", inline: "nearest" });    if (!animationId) return;     return scheduleSnapShotAnimationDestination(animationId, () =>      setSnapShotAnimationDestination(animationId, frame, animationSource),    );  }, [animationId, animationSource, arrival]);   return (    <div      ref={frameRef}      className={cn(        animateArrival &&          !animationId &&          "origin-center transition-[opacity,scale] duration-300 ease-[cubic-bezier(.2,.8,.2,1)] starting:scale-95 starting:opacity-0 motion-reduce:transition-none motion-reduce:starting:scale-100 motion-reduce:starting:opacity-100",        className,      )}      {...props}    />  );} const COMPOSER_SCROLL_COLLAPSE_THRESHOLD_PX = 24;const COMPOSER_SCROLL_GESTURE_RESET_MS = 120;const COMPOSER_RESTING_TRANSITION_DURATION_MS = 280;const COMPOSER_RESTING_TRANSITION_CLEANUP_BUFFER_MS = 50;const COMPOSER_RESTING_TRANSITION_EASING = "cubic-bezier(0.32, 0.72, 0, 1)";const COMPOSER_RESTING_CONTROLS_ARRIVAL_DRIFT_PX = 4; function useComposerRestingTransition(  isCollapsed: boolean,  isResting: boolean,  restingControlsRef: React.RefObject<HTMLDivElement | null>,  onOverlayHeightChange: (height: number) => void,) {  const elementRef = useRef<HTMLDivElement>(null);  const isCollapsedRef = useRef(isCollapsed);  const previousCollapsedRef = useRef(isCollapsed);  const previousRestingRef = useRef(isResting);  const previousHeightRef = useRef<number | null>(null);  const previousContentOffsetsRef = useRef<{    promptFromTop: number | null;    promptHeight: number | null;    actionFromBottom: number | null;  }>({ promptFromTop: null, promptHeight: null, actionFromBottom: null });  const animationRef = useRef<Animation | null>(null);  const animationTargetHeightRef = useRef<number | null>(null);  const contentAnimationsRef = useRef<Animation[]>([]);  const stateChangeAnimationsRef = useRef<Animation[]>([]);  const pinnedOverlayRef = useRef<HTMLElement | null>(null);  const transitionCleanupTimeoutRef = useRef<number | null>(null);  const transitionLayoutRequestRef = useRef(0);  const hasCompletedInitialLayoutRef = useRef(false);   const clearOverlayPin = useCallback(() => {    // The overlay belongs to the chat view and outlives this composer, so it    // is remembered from pin time rather than re-resolved through a ref that    // React may already have detached during unmount.    const overlay = pinnedOverlayRef.current;    pinnedOverlayRef.current = null;    overlay?.style.removeProperty("height");    overlay?.style.removeProperty("display");    overlay?.style.removeProperty("flex-direction");    overlay?.style.removeProperty("justify-content");  }, []);   const clearTransitionStyles = useCallback(() => {    const element = elementRef.current;    const footer = element?.querySelector<HTMLElement>('[data-chat-composer-footer="true"]');    element?.style.removeProperty("overflow");    element      ?.querySelector<HTMLElement>('[data-chat-composer-surface="true"]')      ?.style.removeProperty("height");    footer?.style.removeProperty("position");    footer?.style.removeProperty("top");    footer?.style.removeProperty("bottom");    footer?.style.removeProperty("left");    footer?.style.removeProperty("right");    footer?.style.removeProperty("height");    clearOverlayPin();  }, [clearOverlayPin]);   isCollapsedRef.current = isCollapsed;   const transitionToCurrentGeometry = useCallback(    (stateChanged: boolean) => {      const element = elementRef.current;      const surface = element?.querySelector<HTMLElement>('[data-chat-composer-surface="true"]');      if (!element || !surface) return;       const nextIsCollapsed = isCollapsedRef.current;       const visibleTransitionElement = (selector: string) =>        Array.from(element.querySelectorAll<HTMLElement>(selector)).find(          (candidate) => candidate.getClientRects().length > 0,        ) ?? null;      const prompt = visibleTransitionElement(        '[data-testid="composer-editor"], [data-chat-composer-transition-prompt="true"]',      );      const action = visibleTransitionElement('[data-chat-composer-transition-actions="true"]');      const footer = element.querySelector<HTMLElement>('[data-chat-composer-footer="true"]');      const interruptedAnimation = animationRef.current;      const interruptedPromptTop = interruptedAnimation        ? (prompt?.getBoundingClientRect().top ?? null)        : null;      const interruptedActionTop = interruptedAnimation        ? (action?.getBoundingClientRect().top ?? null)        : null;      const interruptedHeight = interruptedAnimation        ? element.getBoundingClientRect().height        : null;      const interruptedTargetHeight = animationTargetHeightRef.current;      const interruptedCurrentTime =        typeof interruptedAnimation?.currentTime === "number"          ? interruptedAnimation.currentTime          : null;      const interruptedDuration = interruptedAnimation?.effect?.getComputedTiming().duration;      if (transitionCleanupTimeoutRef.current !== null) {        window.clearTimeout(transitionCleanupTimeoutRef.current);        transitionCleanupTimeoutRef.current = null;      }      interruptedAnimation?.cancel();      animationRef.current = null;      for (const animation of contentAnimationsRef.current) animation.cancel();      contentAnimationsRef.current = [];      // The reveal and fade animations keep their own schedule across the      // body-resize re-entries that retarget the geometry mid-flight (every      // transition with a draft triggers one); cancelling them there would      // pop their subjects to full visibility at the start of the tween.      if (stateChanged) {        for (const animation of stateChangeAnimationsRef.current) animation.cancel();        stateChangeAnimationsRef.current = [];      }      clearTransitionStyles();       const nextRect = element.getBoundingClientRect();      const nextHeight = nextRect.height;      // The chat view resize-observes the overlay to place the timeline      // inset, the scroll-to-end pill, and the mini player. Publishing the      // destination height here turns that feedback into one update instead      // of a ChatView re-render on every animation frame.      const overlay = element.closest<HTMLElement>('[data-chat-composer-overlay="true"]');      const overlayHeight = overlay?.getBoundingClientRect().height ?? null;      if (overlayHeight !== null) {        onOverlayHeightChange(overlayHeight);      }      const nextPromptRect = prompt?.getBoundingClientRect() ?? null;      const nextPromptTop = nextPromptRect?.top ?? null;      const nextActionTop = action?.getBoundingClientRect().top ?? null;      const previousHeight = interruptedHeight ?? previousHeightRef.current;      const targetChanged =        interruptedTargetHeight === null || Math.abs(interruptedTargetHeight - nextHeight) >= 0.5;      const prefersReducedMotion = window.matchMedia?.("(prefers-reduced-motion: reduce)").matches;      const shouldAnimate = shouldAnimateComposerRestingTransition({        hasCompletedInitialLayout: hasCompletedInitialLayoutRef.current,        stateChanged,        hasInterruptedAnimation: interruptedHeight !== null,      });       if (        shouldAnimate &&        !prefersReducedMotion &&        previousHeight !== null &&        Math.abs(previousHeight - nextHeight) >= 0.5      ) {        const remainingDuration =          typeof interruptedDuration === "number" && interruptedCurrentTime !== null            ? Math.max(1, interruptedDuration - interruptedCurrentTime)            : COMPOSER_RESTING_TRANSITION_DURATION_MS;        const duration =          interruptedHeight !== null && !targetChanged            ? remainingDuration            : COMPOSER_RESTING_TRANSITION_DURATION_MS;        element.style.overflow = "clip";        surface.style.height = "100%";         // Pinning the overlay at the destination height keeps the resize        // observer quiet for the tween; bottom alignment keeps the animating        // surface glued to the overlay's stable bottom edge. The pin lasts        // only for the tween so later attachment, thread, font, and viewport        // changes remain natural.        if (overlay && overlayHeight !== null) {          overlay.style.height = `${String(overlayHeight)}px`;          overlay.style.display = "flex";          overlay.style.flexDirection = "column";          overlay.style.justifyContent = "flex-end";          pinnedOverlayRef.current = overlay;        }         // Keep the footer attached to the stable bottom edge while the outer        // height changes. Its resting absolute layout otherwise spans the old        // height on collapse, while its expanded flow layout falls below the        // clipped surface on expansion.        if (footer) {          footer.style.position = "absolute";          footer.style.top = "auto";          footer.style.bottom = "1px";          footer.style.height = "3rem";          if (nextIsCollapsed) {            footer.style.left = "auto";            footer.style.right = "1px";          } else {            footer.style.left = "1px";            footer.style.right = "1px";          }        }         const animation = element.animate(          [{ height: `${previousHeight}px` }, { height: `${nextHeight}px` }],          {

Lines 1–500 of 5889.