# Files changed · PR \#23042 · pingdotgg/t3code

[View on GitCafe](https://git.cafe/pingdotgg/t3code/pull/23042/diffs)

Repository: [pingdotgg/t3code](https://git.cafe/pingdotgg/t3code)

Visibility: public

[refactor\(web\): the composer's layout and provider hooks live in their own files](https://git.cafe/pingdotgg/t3code/pull/23042.md)

Version: 6

Head commit: 1807a1e9c9d9c7e2dd80b05c6c43041dc5172a8e

Base commit: ec8a0878b4c9eb31ff47432a143893d0fedba6d9

Comparison base: ec8a0878b4c9eb31ff47432a143893d0fedba6d9

Showing up to 20 entries from this revision. If the pull request changes, reopen the first page before continuing.

## apps/web/src/components/chat/ChatComposer.tsx

Status: modified; +6; −944

```diff
@@ -41,7 +41,6 @@
   PROVIDER_SEND_TURN_MAX_ATTACHMENTS,
   PROVIDER_SEND_TURN_MAX_IMAGE_BYTES,
   PROVIDER_SEND_TURN_MAX_INPUT_CHARS,
-  PROVIDER_WORKSPACE_SNAPSHOT_TTL_MS,
 } from "@t3tools/contracts";
 import type { EnvironmentConnectionPresentation } from "@t3tools/client-runtime/connection";
 import { settlePromise, squashAtomCommandFailure } from "@t3tools/client-runtime/state/runtime";
@@ -67,7 +66,6 @@
   useLayoutEffect,
   useRef,
   useState,
-  useSyncExternalStore,
 } from "react";
 import { createPortal, flushSync } from "react-dom";
 import {
@@ -91,7 +89,6 @@
   getAntigravitySendBlockReason,
   readFileAsDataUrl,
   resolveComposerInteractionMode,
-  resolveComposerProviderSelection,
   threadShellHasStarted,
 } from "../ChatView.logic";
 import {
@@ -116,8 +113,6 @@
   hydrateImagesFromPersisted,
   useComposerDraftStore,
   useComposerThreadDraft,
-  deriveEffectiveComposerModelState,
-  useComposerDraftModelState,
 } from "../../composerDraftStore";
 import {
   MAX_STASH_ENTRIES,
@@ -189,13 +184,10 @@
   toKindScopedComposerContextId,
 } from "@t3tools/shared/composerContextReferences";
 import {
-  resolveRestingComposerControlsLayout,
-  shouldAnimateComposerRestingTransition,
   shouldUseCompactComposerPrimaryActions,
   shouldUseCompactComposerFooter,
   shouldUseRestingComposerLayout,
 } from "../composerFooterLayout";
-import { measureRestingComposerControls } from "./restingComposerControlsMeasurement";
 import {
   type ComposerCitationCommentRequest,
   type ComposerPromptEditorHandle,
@@ -285,7 +277,6 @@
 import { buildPullRequestReferenceContext } from "../pullRequest/pullRequestDetail.logic";
 import {
   getComposerPromptInjectionState,
-  getComposerProviderState,
   getComposerReasoningStep,
   renderProviderTraitsMenuContent,
   renderProviderTraitsPicker,
@@ -301,11 +292,6 @@
   buildExpandedImagePreview,
   type ExpandedImagePreview,
 } from "./ExpandedImagePreview";
-import {
-  getPendingSnapShotAnimations,
-  pendingSnapShotAnimationIdsForTarget,
-  subscribeToPendingSnapShotAnimations,
-} from "../../lib/snapShotAnimation";
 import { resizeSnapShotSource } from "../../lib/snapShotSource";
 import { cn, isMacPlatform, randomUUID } from "~/lib/utils";
 import {
@@ -329,631 +315,14 @@
 } from "./composerScrollGesture";
 import { observeResize } from "~/lib/observeResize";
 
-type ComposerCommandMenuPosition = {
-  bottom: number;
-  left: number;
-  maxHeight: number;
-  width: number;
-};
-
 const isEnvironmentRequestInvalidError = Schema.is(EnvironmentRequestInvalidError);
 
 const COMPOSER_SCROLL_COLLAPSE_THRESHOLD_PX = 24;
 const COMPOSER_SCROLL_GESTURE_RESET_MS = 120;
-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,
-  animationsActive: boolean,
-  animationDurationMs: number,
-) {
-  const elementRef = useRef<HTMLDivElement>(null);
-  const isCollapsedRef = useRef(isCollapsed);
-  const previousCollapsedRef = useRef(isCollapsed);
-  const previousRestingRef = useRef(isResting);
-  const previousHeightRef = useRef<number | null>(null);
-  const previousModelStripHeightRef = useRef<number | null>(null);
-  const previousContentOffsetsRef = useRef<{
-    promptFromTop: number | null;
-    promptHeight: number | null;
-    actionFromBottom: number | null;
-    controlsFromBottom: number | null;
-    controlsFromLeft: number | null;
-    surfaceBottomInset: number | null;
-  }>({
-    promptFromTop: null,
-    promptHeight: null,
-    actionFromBottom: null,
-    controlsFromBottom: null,
-    controlsFromLeft: null,
-    surfaceBottomInset: 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 = () => {
-    // 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 = () => {
-    const element = elementRef.current;
-    const footer = element?.querySelector<HTMLElement>('[data-chat-composer-footer="true"]');
-    element?.style.removeProperty("overflow");
-    element?.style.removeProperty("clip-path");
-    element?.style.removeProperty("overflow-clip-margin");
-    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");
-    const shell = element?.closest<HTMLElement>('[data-slot="composer-shell"]');
-    shell?.removeAttribute("data-model-strip-transition");
-    const modelStrip = shell?.querySelector<HTMLElement>('[data-composer-model-strip="true"]');
-    for (const property of [
-      "position",
-      "top",
-      "visibility",
-      "height",
-      "min-height",
-      "padding-top",
-      "align-items",
-      "z-index",
-      "pointer-events",
-    ]) {
-      modelStrip?.style.removeProperty(property);
-    }
-    clearOverlayPin();
-  };
-
-  useLayoutEffect(() => {
-    isCollapsedRef.current = isCollapsed;
-  });
-
-  const transitionToCurrentGeometry = (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-composer-prompt-surface="true"], [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 continuousControls =
-      element.dataset.inlineRestingControls === "true" || element.dataset.modelOnlyStrip === "true";
-    const controls = nextIsCollapsed
-      ? restingControlsRef.current
-      : element.querySelector<HTMLElement>('[data-chat-composer-controls="left"]');
-
-    const shell = element.closest<HTMLElement>('[data-slot="composer-shell"]');
-    const modelStrip = shell?.querySelector<HTMLElement>('[data-composer-model-strip="true"]');
-    const interruptedStripHeight =
-      animationRef.current && modelStrip ? modelStrip.getBoundingClientRect().height : null;
-    const interruptedAnimation = animationRef.current;
-    const interruptedPromptTop = interruptedAnimation
-      ? (prompt?.getBoundingClientRect().top ?? null)
-      : null;
-    const interruptedActionTop = interruptedAnimation
-      ? (action?.getBoundingClientRect().top ?? null)
-      : null;
-    const interruptedControlsLeft = interruptedAnimation
-      ? (controls?.getBoundingClientRect().left ?? null)
-      : null;
-    const interruptedControlsTop = interruptedAnimation
-      ? (controls?.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 = [];
-    if (stateChanged) {
-      for (const animation of stateChangeAnimationsRef.current) animation.cancel();
-      stateChangeAnimationsRef.current = [];
-    }
-    clearTransitionStyles();
-
-    const nextRect = element.getBoundingClientRect();
-    const nextHeight = nextRect.height;
-    const nextModelStripHeight = modelStrip?.getBoundingClientRect().height ?? null;
-    // 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 overlayRect = overlay?.getBoundingClientRect();
-    const overlayHeight = overlayRect?.height ?? null;
-    if (overlayHeight !== null) {
-      onOverlayHeightChange(overlayHeight);
-    }
-    const nextPromptRect = prompt?.getBoundingClientRect() ?? null;
-    const nextPromptTop = nextPromptRect?.top ?? null;
-    const nextActionTop = action?.getBoundingClientRect().top ?? null;
-    const nextControlsRect = controls?.getBoundingClientRect() ?? null;
-    const nextControlsTop = nextControlsRect?.top ?? null;
-    const footerBottom = footer ? nextRect.bottom - footer.getBoundingClientRect().bottom : 1;
-    const previousHeight = interruptedHeight ?? previousHeightRef.current;
-    const targetChanged =
-      interruptedTargetHeight === null || Math.abs(interruptedTargetHeight - nextHeight) >= 0.5;
-    const shouldAnimate = shouldAnimateComposerRestingTransition({
-      hasCompletedInitialLayout: hasCompletedInitialLayoutRef.current,
-      stateChanged,
-      hasInterruptedAnimation: interruptedHeight !== null,
-    });
-
-    if (
-      shouldAnimate &&
-      animationsActive &&
-      previousHeight !== null &&
-      Math.abs(previousHeight - nextHeight) >= 0.5
-    ) {
-      const remainingDuration =
-        typeof interruptedDuration === "number" && interruptedCurrentTime !== null
-          ? Math.max(1, interruptedDuration - interruptedCurrentTime)
-          : animationDurationMs;
-      const duration =
-        interruptedHeight !== null && !targetChanged ? remainingDuration : animationDurationMs;
-      element.style.overflow = "clip";
-      if (modelStrip) {
-        const controlsClearance = Math.max(
-          nextModelStripHeight ?? 0,
-          previousModelStripHeightRef.current ?? 0,
-        );
-        element.style.overflowClipMargin = `${controlsClearance}px`;
-        element.style.clipPath = `inset(0 0 -${controlsClearance}px 0)`;
-      }
-      surface.style.height = "100%";
-
-      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;
-      }
-
-      if (footer) {
-        footer.style.position = "absolute";
-        footer.style.top = "auto";
-        footer.style.bottom = `${String(footerBottom)}px`;
-        footer.style.height = "3rem";
-        if (nextIsCollapsed) {
-          footer.style.left = "auto";
-          footer.style.right = "1px";
-        } else {
-          footer.style.left = "1px";
-          footer.style.right = "1px";
-        }
-      }
-
-      let stripAnimation: Animation | null = null;
-      if (modelStrip) {
-        const stripHeight = modelStrip.getBoundingClientRect().height;
-        const stripOverlap = -Number.parseFloat(getComputedStyle(modelStrip).marginTop);
-        const fromHeight =
-          interruptedStripHeight ??
-          (previousCollapsedRef.current
-            ? (previousModelStripHeightRef.current ?? stripHeight)
-            : stripOverlap);
-        const toHeight = nextIsCollapsed ? stripHeight : stripOverlap;
-        modelStrip.style.position = "relative";
-        modelStrip.style.top = "auto";
-        modelStrip.style.visibility = "visible";
-        modelStrip.style.minHeight = "0";
-        modelStrip.style.paddingTop = "0";
-        modelStrip.style.alignItems = "flex-end";
-        modelStrip.style.pointerEvents = "none";
-        modelStrip.style.zIndex = "20";
-        shell?.setAttribute("data-model-strip-transition", "true");
-        stripAnimation = modelStrip.animate(
-          [{ height: `${fromHeight}px` }, { height: `${toHeight}px` }],
-          { duration, easing: COMPOSER_RESTING_TRANSITION_EASING, fill: "both" },
-        );
-      }
-
-      const animation = element.animate(
-        [{ height: `${previousHeight}px` }, { height: `${nextHeight}px` }],
-        {
-          duration,
-          easing: COMPOSER_RESTING_TRANSITION_EASING,
-          fill: "both",
-        },
-      );
-      animationRef.current = animation;
-      animationTargetHeightRef.current = nextHeight;
-
-      const animatedRect = element.getBoundingClientRect();
-      const previousBottom =
-        overlayRect && previousContentOffsetsRef.current.surfaceBottomInset !== null
-          ? overlayRect.bottom - previousContentOffsetsRef.current.surfaceBottomInset
-          : animatedRect.bottom;
-      const bottomShift = previousBottom - animatedRect.bottom;
-      const previousPromptTop =
-        interruptedPromptTop ??
-        (previousContentOffsetsRef.current.promptFromTop === null
-          ? null
-          : animatedRect.top + bottomShift + previousContentOffsetsRef.current.promptFromTop);
-      const previousActionTop =
-        interruptedActionTop ??
-        (previousContentOffsetsRef.current.actionFromBottom === null
-          ? null
-          : previousBottom - previousContentOffsetsRef.current.actionFromBottom);
-      const contentAnimations: Animation[] = stripAnimation ? [stripAnimation] : [];
-      const animateContentPosition = (
-        content: HTMLElement | null,
-        previousTop: number | null,
-        previousLeft: number | null = null,
-      ) => {
-        if (!content || previousTop === null) return;
-        const rect = content.getBoundingClientRect();
-        const offset = previousTop - rect.top;
-        const offsetX = previousLeft === null ? 0 : previousLeft - rect.left;
-        if (Math.abs(offset) < 0.5 && Math.abs(offsetX) < 0.5) return;
-        contentAnimations.push(
-          content.animate(
-            [{ transform: `translate(${offsetX}px, ${offset}px)` }, { transform: "none" }],
-            {
-              duration,
-              easing: COMPOSER_RESTING_TRANSITION_EASING,
-            },
-          ),
-        );
-      };
-      animateContentPosition(prompt, previousPromptTop);
-      animateContentPosition(action, previousActionTop);
-      if (continuousControls) {
-        const previousControlsTop =
-          interruptedControlsTop ??
-          (previousContentOffsetsRef.current.controlsFromBottom === null
-            ? null
-            : previousBottom - previousContentOffsetsRef.current.controlsFromBottom);
-        const previousControlsLeft =
-          interruptedControlsLeft ??
-          (previousContentOffsetsRef.current.controlsFromLeft === null
-            ? null
-            : animatedRect.left + previousContentOffsetsRef.current.controlsFromLeft);
-        animateContentPosition(controls, previousControlsTop, previousControlsLeft);
-      }
-      contentAnimationsRef.current = contentAnimations;
-
-      if (stateChanged) {
-        const stateChangeAnimations: Animation[] = [];
-
-        const previousPromptHeight = previousContentOffsetsRef.current.promptHeight;
-        if (
-          !nextIsCollapsed &&
-          prompt &&
-          nextPromptRect &&
-          previousPromptHeight !== null &&
-          nextPromptRect.height - previousPromptHeight >= 0.5
-        ) {
-          const hiddenHeight = nextPromptRect.height - previousPromptHeight;
-          stateChangeAnimations.push(
-            prompt.animate(
-              [
-                { clipPath: `inset(0 0 ${String(hiddenHeight)}px 0)` },
-                { clipPath: "inset(0 0 0 0)" },
-              ],
-              {
-                duration,
-                easing: COMPOSER_RESTING_TRANSITION_EASING,
-              },
-            ),
-          );
-        }
-
-        const arrivingControls = nextIsCollapsed
-          ? restingControlsRef.current
-          : element.querySelector<HTMLElement>('[data-chat-composer-controls="left"]');
-        if (arrivingControls && !continuousControls) {
-          const drift = nextIsCollapsed
-            ? -COMPOSER_RESTING_CONTROLS_ARRIVAL_DRIFT_PX
-            : COMPOSER_RESTING_CONTROLS_ARRIVAL_DRIFT_PX;
-          stateChangeAnimations.push(
-            arrivingControls.animate(
-              [
-                { opacity: 0, transform: `translateY(${String(drift)}px)` },
-                { opacity: 1, transform: "none" },
-              ],
-              {
-                duration: nextIsCollapsed ? duration : duration / 2,
-                delay: nextIsCollapsed ? 0 : duration / 2,
-                fill: "backwards",
-                easing: COMPOSER_RESTING_TRANSITION_EASING,
-              },
-            ),
-          );
-        }
-
-        const arrivingImagePreviews = nextIsCollapsed
-          ? Array.from(
-              element.querySelectorAll<HTMLElement>('[data-chat-composer-resting-images="true"]'),
-            )
-          : Array.from(
-              element.querySelectorAll<HTMLElement>('[data-chat-composer-expanded-image="true"]'),
-            );
-        for (const imagePreview of arrivingImagePreviews) {
-          stateChangeAnimations.push(
-            imagePreview.animate([{ opacity: 0 }, { opacity: 1 }], {
-              duration: nextIsCollapsed ? duration : duration / 2,
-              delay: nextIsCollapsed ? 0 : duration / 2,
-              fill: "backwards",
-              easing: COMPOSER_RESTING_TRANSITION_EASING,
-            }),
-          );
-        }
-        stateChangeAnimationsRef.current = stateChangeAnimations;
-      }
-
-      const finishTransition = (cancelAnimations: boolean) => {
-        if (animationRef.current !== animation) return;
-        if (transitionCleanupTimeoutRef.current !== null) {
-          window.clearTimeout(transitionCleanupTimeoutRef.current);
-          transitionCleanupTimeoutRef.current = null;
-        }
-        if (cancelAnimations) {
-          animation.cancel();
-          for (const contentAnimation of contentAnimationsRef.current) {
-            contentAnimation.cancel();
-          }
-          for (const stateChangeAnimation of stateChangeAnimationsRef.current) {
-            stateChangeAnimation.cancel();
-          }
-        }
-        animationRef.current = null;
-        animationTargetHeightRef.current = null;
-        contentAnimationsRef.current = [];
-        stateChangeAnimationsRef.current = [];
-        animation.cancel();
-        stripAnimation?.cancel();
-        clearTransitionStyles();
-      };
-      void animation.finished.catch(() => undefined).then(() => finishTransition(false));
-      // A suspended document timeline can leave `finished` pending while
-      // these measurement styles remain active. Wall-clock cleanup makes
-      // the natural layout the eventual source of truth in that case.
-      transitionCleanupTimeoutRef.current = window.setTimeout(
-        () => finishTransition(true),
-        duration + COMPOSER_RESTING_TRANSITION_CLEANUP_BUFFER_MS,
-      );
-    } else {
-      animationTargetHeightRef.current = null;
-    }
-
-    previousCollapsedRef.current = nextIsCollapsed;
-    previousHeightRef.current = nextHeight;
-    previousModelStripHeightRef.current = nextModelStripHeight;
-    previousContentOffsetsRef.current = {
-      promptFromTop: nextPromptTop === null ? null : nextPromptTop - nextRect.top,
-      promptHeight: nextPromptRect?.height ?? null,
-      actionFromBottom: nextActionTop === null ? null : nextRect.bottom - nextActionTop,
-      controlsFromBottom: nextControlsTop === null ? null : nextRect.bottom - nextControlsTop,
-      controlsFromLeft: nextControlsRect === null ? null : nextControlsRect.left - nextRect.left,
-      surfaceBottomInset: overlayRect ? overlayRect.bottom - nextRect.bottom : null,
-    };
-  };
-
-  useLayoutEffect(() => {
-    const requestId = transitionLayoutRequestRef.current + 1;
-    transitionLayoutRequestRef.current = requestId;
-    const stateChanged = previousCollapsedRef.current !== isCollapsed;
-    // A non-Git context strip enters or leaves flow through ChatView state in
-    // an earlier layout effect. Let React flush that parent update before the
-    // FLIP reads its destination geometry, while still running before paint.
-    queueMicrotask(() => {
-      if (transitionLayoutRequestRef.current !== requestId) return;
-      transitionToCurrentGeometry(stateChanged);
-    });
-    return () => {
-      if (transitionLayoutRequestRef.current === requestId) {
-        transitionLayoutRequestRef.current += 1;
-      }
-    };
-  }, [isCollapsed, transitionToCurrentGeometry]);
-
-  useLayoutEffect(() => {
-    if (previousRestingRef.current === isResting) return;
-    previousRestingRef.current = isResting;
-    if (animationRef.current) return;
-    const overlay = elementRef.current?.closest<HTMLElement>('[data-chat-composer-overlay="true"]');
-    if (overlay) onOverlayHeightChange(overlay.getBoundingClientRect().height);
-  }, [isResting, onOverlayHeightChange]);
-
-  useLayoutEffect(() => {
-    const element = elementRef.current;
-    if (!element) return;
-
-    const body = element.querySelector<HTMLElement>('[data-chat-composer-body="true"]');
-    return observeResize(body ? [element, body] : element, (entries) => {
-      if (animationRef.current) {
-        if (body && entries.some((entry) => entry.target === body)) {
-          transitionToCurrentGeometry(false);
-        }
-        return;
-      }
-      const elementRect = element.getBoundingClientRect();
-      const visibleTransitionElement = (selector: string) =>
-        Array.from(element.querySelectorAll<HTMLElement>(selector)).find(
-          (candidate) => candidate.getClientRects().length > 0,
-        ) ?? null;
-      const promptRect = visibleTransitionElement(
-        '[data-composer-prompt-surface="true"], [data-chat-composer-transition-prompt="true"]',
-      )?.getBoundingClientRect();
-      const actionTop = visibleTransitionElement(
-        '[data-chat-composer-transition-actions="true"]',
-      )?.getBoundingClientRect().top;
-      const controlsRect = (
-        isCollapsedRef.current
-          ? restingControlsRef.current
-          : visibleTransitionElement('[data-chat-composer-controls="left"]')
-      )?.getBoundingClientRect();
-      previousHeightRef.current = elementRect.height;
-      previousContentOffsetsRef.current = {
-        promptFromTop: promptRect === undefined ? null : promptRect.top - elementRect.top,
-        promptHeight: promptRect?.height ?? null,
-        actionFromBottom: actionTop === undefined ? null : elementRect.bottom - actionTop,
-        controlsFromBottom:
-          controlsRect === undefined ? null : elementRect.bottom - controlsRect.top,
-        controlsFromLeft: controlsRect === undefined ? null : controlsRect.left - elementRect.left,
-        surfaceBottomInset:
-          (element
-            .closest<HTMLElement>('[data-chat-composer-overlay="true"]')
-            ?.getBoundingClientRect().bottom ?? elementRect.bottom) - elementRect.bottom,
-      };
-    });
-  }, [restingControlsRef, transitionToCurrentGeometry]);
-
-  useEffect(() => {
-    hasCompletedInitialLayoutRef.current = true;
-    return () => {
-      if (transitionCleanupTimeoutRef.current !== null) {
-        window.clearTimeout(transitionCleanupTimeoutRef.current);
-        transitionCleanupTimeoutRef.current = null;
-      }
-      animationRef.current?.cancel();
-      animationRef.current = null;
-      animationTargetHeightRef.current = null;
-      for (const animation of contentAnimationsRef.current) animation.cancel();
-      contentAnimationsRef.current = [];
-      for (const animation of stateChangeAnimationsRef.current) animation.cancel();
-      stateChangeAnimationsRef.current = [];
-      clearTransitionStyles();
-    };
-  }, [clearTransitionStyles]);
-
-  return elementRef;
-}
-
-function composerCommandMenuPositionsEqual(
-  a: ComposerCommandMenuPosition,
-  b: ComposerCommandMenuPosition,
-): boolean {
-  return (
-    a.bottom === b.bottom && a.left === b.left && a.maxHeight === b.maxHeight && a.width === b.width
-  );
-}
-
-function ComposerCommandMenuLayer(props: { anchor: HTMLElement | null; children: ReactNode }) {
-  const [position, setPosition] = useState<ComposerCommandMenuPosition | null>(null);
-
-  useLayoutEffect(() => {
-    const anchor = props.anchor;
-    if (!anchor) return;
-
-    const updatePosition = () => {
-      const form = anchor.closest<HTMLElement>('[data-chat-composer-form="true"]');
-      const mainSurface = form?.querySelector<HTMLElement>(
-        '[data-chat-composer-main-surface="true"]',
-      );
-      const rect = (mainSurface ?? form ?? anchor).getBoundingClientRect();
-      const rootFontSizePx =
-        Number.parseFloat(window.getComputedStyle(document.documentElement).fontSize) || 16;
-      const drawerInsetRem = Number.parseFloat(
-        window.getComputedStyle(form ?? anchor).getPropertyValue("--chat-composer-drawer-inset"),
-      );
-      const drawerInset = drawerInsetRem * rootFontSizePx;
-      const width = rect.width - drawerInset * 2;
-      if (width <= 0) {
-        setPosition(null);
-        return;
-      }
-      const composerOverlap = rootFontSizePx + 1;
-      const next = {
-        bottom: window.innerHeight - rect.top - composerOverlap,
-        left: rect.left + drawerInset,
-        maxHeight: Math.max(96, rect.top - 24 + composerOverlap),
-        width,
-      };
-      setPosition((current) =>
-        current && composerCommandMenuPositionsEqual(current, next) ? current : next,
-      );
-    };
-
-    updatePosition();
-    window.addEventListener("resize", updatePosition);
-    window.addEventListener("scroll", updatePosition, true);
-
-    const observed: Element[] = [anchor];
-    for (let element = anchor.parentElement; element; element = element.parentElement) {
-      observed.push(element);
-    }
-    const stopObserving = observeResize(observed, updatePosition);
-
-    return () => {
-      stopObserving();
-      window.removeEventListener("resize", updatePosition);
-      window.removeEventListener("scroll", updatePosition, true);
-    };
-  }, [props.anchor]);
-
-  if (!props.anchor || !position) return null;
-
-  return createPortal(
-    <div
-      className="pointer-events-auto fixed z-40 flex flex-col"
-      data-composer-drawer-layer="true"
-      style={{
-        bottom: position.bottom,
-        left: position.left,
-        maxHeight: position.maxHeight,
-        width: position.width,
-      }}
-    >
-      {props.children}
-    </div>,
-    document.body,
-  );
-}
 import { toastManager } from "../ui/toast";
 import { CircleAlertIcon, PlayIcon, ShieldIcon } from "lucide-react";
 import { proposedPlanTitle } from "@t3tools/shared/proposedPlanText";
 import { hasProviderSetup } from "./ProviderStatusBanner";
-import {
-  applyProviderInstanceSettings,
-  deriveProviderInstanceEntries,
-  NO_PROVIDER_MODEL_SELECTION,
-  sortProviderInstanceEntries,
-  type ProviderInstanceEntry,
-} from "../../providerInstances";
 import { type AppModelOption, getAppModelOptionsForInstance } from "../../modelSelection";
 import type { UnifiedSettings } from "@t3tools/contracts/settings";
 import {
@@ -981,8 +350,6 @@
 } from "@t3tools/client-runtime/state/composer-dispatch";
 import type { ContextWindowSnapshot } from "../../lib/contextWindow";
 import {
-  hasCompleteProviderWorkspaceSnapshot,
-  hasCurrentProviderWorkspaceSnapshot,
   resolveProviderSkillsForCwd,
   resolveProviderSlashCommandsForCwd,
 } from "@t3tools/client-runtime/providerSkills";
@@ -993,10 +360,13 @@
 import { usePanelAnimationSettings } from "../../panelAnimations";
 import { useAtomCommand } from "../../state/use-atom-command";
 import { readEnvironmentScope } from "../../state/session";
-import { serverEnvironment } from "../../state/server";
 import type { ReviewCommentContext } from "../../reviewCommentContext";
+import { useComposerRestingTransition } from "./useComposerRestingTransition";
+import { ComposerCommandMenuLayer } from "./ComposerCommandMenuLayer";
-
+import { useRestingComposerControlsLayout } from "./useRestingComposerControlsLayout";
-const WORKSPACE_SNAPSHOT_RETRY_COOLDOWN_MS = 10_000;
+import { useProviderWorkspaceSnapshotRefresh } from "./useProviderWorkspaceSnapshotRefresh";
+import { useComposerProviderModel } from "./useComposerProviderModel";
+import { useComposerImageGroups } from "./useComposerImageGroups";
 
 /** Reads and disarms a paste-as-text deadline: the shortcut applies to one paste only. */
 function consumePasteAsTextShortcut(deadline: { current: number }): boolean {
@@ -1025,83 +395,6 @@
   return text[rangeEnd] === " " ? rangeEnd + 1 : rangeEnd;
 };
 
-function useRestingComposerControlsLayout(host: HTMLDivElement | null, useControlsAsHost = false) {
-  const [controls, setControls] = useState<HTMLDivElement | null>(null);
-  const controlsRef = useRef<HTMLDivElement>(null);
-  const attachControls = (element: HTMLDivElement | null) => {
-    controlsRef.current = element;
-    setControls(element);
-  };
-  const [layout, setLayout] = useState<{
-    hiddenCount: number;
-    iconOnlyCount?: number;
-    visible: boolean;
-  }>({ hiddenCount: 0, visible: true });
-
-  const measure = () => {
-    const currentHost = useControlsAsHost ? controls : host;
-    if (currentHost === null || !controls) return;
-
-    const measurement = measureRestingComposerControls(controls);
-    if (!measurement) return;
-    const style = getComputedStyle(currentHost);
-    const hostWidth =
-      currentHost.clientWidth -
-      (Number.parseFloat(style.paddingInlineStart) || 0) -
-      (Number.parseFloat(style.paddingInlineEnd) || 0);
-
-    setLayout((current) => {
-      const next = resolveRestingComposerControlsLayout({
-        ...measurement,
-        hostWidth,
-        previous: current,
-      });
-      return next.hiddenCount === current.hiddenCount &&
-        next.iconOnlyCount === current.iconOnlyCount &&
-        next.visible === current.visible
-        ? current
-        : next;
-    });
-  };
-
-  useLayoutEffect(() => {
-    const currentHost = useControlsAsHost ? controls : host;
-    if (!currentHost || !controls) return;
-    let stopObserving = () => {};
-    const observeControls = () => {
-      stopObserving();
-      stopObserving = observeResize(
-        [
-          currentHost,
-          controls,
-          ...controls.querySelectorAll<HTMLElement>(
-            "[data-resting-block], [data-composer-control-label], [data-chat-provider-model-picker-label]",
-          ),
-        ],
-        measure,
-      );
-      measure();
-    };
-    observeControls();
-    const mutations = new MutationObserver(observeControls);
-    mutations.observe(controls, { childList: true, subtree: true, characterData: true });
-    document.fonts.addEventListener("loadingdone", measure);
-    return () => {
-      stopObserving();
-      mutations.disconnect();
-      document.fonts.removeEventListener("loadingdone", measure);
-    };
-  }, [host, controls, useControlsAsHost, measure]);
-
-  return {
-    controlsRef,
-    attachControls,
-    hiddenBlockCount: layout.hiddenCount,
-    iconOnlyBlockCount: layout.iconOnlyCount ?? 0,
-    controlsVisible: layout.visible,
-  };
-}
-
 const runtimeModeOptions = runtimeModes.map((mode) => ({ mode, ...runtimeModeConfig[mode] }));
 
 export interface ChatComposerHandle {
@@ -1330,237 +623,6 @@
   onFileOpen: (attachment: ChatFileAttachment) => void;
 }
 
-/**
- * Keeps the selected provider's workspace snapshot fresh for the composer's
- * cwd. Returns a check to run when the user types.
- */
-function useProviderWorkspaceSnapshotRefresh(input: {
-  readonly environmentId: EnvironmentId;
-  readonly gitCwd: string | null;
-  readonly selectedProviderEntry: ProviderInstanceEntry | undefined;
-  readonly selectedProviderStatus: ServerProvider | null;
-}) {
-  const { environmentId, gitCwd, selectedProviderEntry, selectedProviderStatus } = input;
-  const refreshProviders = useAtomCommand(serverEnvironment.refreshProviders, {
-    reportFailure: false,
-  });
-  // The last scan this composer asked for. A request inside the TTL is not
-  // repeated, so a client clock ahead of the server's cannot loop rescans.
-  const workspaceRefreshKeyRef = useRef<{ key: string; requestedAt: number } | null>(null);
-  const [workspaceRefreshRetry, setWorkspaceRefreshRetry] = useState<{
-    key: string;
-    notBefore: number;
-  } | null>(null);
-  const workspaceRefreshScopeKey =
-    gitCwd && selectedProviderEntry
-      ? `${environmentId}:${selectedProviderEntry.instanceId}:${gitCwd}`
-      : null;
-  const workspaceSlashCommandsPending =
-    selectedProviderStatus?.workspaceSnapshots?.some(
-      (snapshot) => snapshot.cwd === gitCwd && snapshot.slashCommandsPending === true,
-    ) ?? false;
-  useEffect(() => {
-    if (
-      !workspaceSlashCommandsPending ||
-      !workspaceRefreshRetry ||
-      workspaceRefreshRetry.key !== workspaceRefreshScopeKey
-    )
-      return;
-    const timeout = setTimeout(
-      () => {
-        setWorkspaceRefreshRetry((current) => (current === workspaceRefreshRetry ? null : current));
-      },
-      Math.max(0, workspaceRefreshRetry.notBefore - Date.now()),
-    );
-    return () => clearTimeout(timeout);
-  }, [workspaceRefreshRetry, workspaceRefreshScopeKey, workspaceSlashCommandsPending]);
-  // Losing a snapshot (the server dropped it) lifts both the TTL and the retry
-  // cooldown so the next check rescans at once.
-  const hasWorkspaceSnapshot = hasCompleteProviderWorkspaceSnapshot(selectedProviderStatus, gitCwd);
-  const [hadWorkspaceSnapshot, setHadWorkspaceSnapshot] = useState(hasWorkspaceSnapshot);
-  const [workspaceSnapshotLossCount, setWorkspaceSnapshotLossCount] = useState(0);
-  if (hadWorkspaceSnapshot !== hasWorkspaceSnapshot) {
-    setHadWorkspaceSnapshot(hasWorkspaceSnapshot);
-    if (!hasWorkspaceSnapshot) {
-      setWorkspaceSnapshotLossCount((count) => count + 1);
-      setWorkspaceRefreshRetry(null);
-    }
-  }
-  const lastWorkspaceSnapshotLossRef = useRef(workspaceSnapshotLossCount);
-  const refreshWorkspaceSnapshotIfStale = (
-    now: number,
-    key: string | null,
-    retry: { key: string; notBefore: number } | null,
-    snapshotLossCount: number,
-  ) => {
-    if (lastWorkspaceSnapshotLossRef.current !== snapshotLossCount) {
-      lastWorkspaceSnapshotLossRef.current = snapshotLossCount;
-      workspaceRefreshKeyRef.current = null;
-    }
-    if (key === null || !gitCwd || !selectedProviderEntry) return;
-    const lastRequest = workspaceRefreshKeyRef.current;
-    if (
-      lastRequest?.key === key &&
-      now - lastRequest.requestedAt < PROVIDER_WORKSPACE_SNAPSHOT_TTL_MS
-    )
-      return;
-    // A pending retry needs no clearing here: its cooldown lapses long before the TTL.
-    if (hasCurrentProviderWorkspaceSnapshot(selectedProviderStatus, gitCwd, now)) return;
-    if (retry?.key === key && now < retry.notBefore) return;
-    const request = { key, requestedAt: now };
-    workspaceRefreshKeyRef.current = request;
-    const retryLater = () => {
-      if (workspaceRefreshKeyRef.current !== request) return;
-      workspaceRefreshKeyRef.current = null;
-      setWorkspaceRefreshRetry({
-        key,
-        notBefore: Date.now() + WORKSPACE_SNAPSHOT_RETRY_COOLDOWN_MS,
-      });
-    };
-    void refreshProviders({
-      environmentId,
-      input: { instanceId: selectedProviderEntry.instanceId, cwd: gitCwd },
-    }).then((result) => {
-      const refreshedSnapshot =
-        result._tag === "Success" &&
-        hasCompleteProviderWorkspaceSnapshot(
-          result.value.providers.find(
-            (provider) => provider.instanceId === selectedProviderEntry.instanceId,
-          ),
-          gitCwd,
-        );
-      if (!refreshedSnapshot) retryLater();
-    }, retryLater);
-  };
-  const refreshWorkspaceSnapshotOnChange = useEffectEvent(refreshWorkspaceSnapshotIfStale);
-  useEffect(() => {
-    refreshWorkspaceSnapshotOnChange(
-      Date.now(),
-      workspaceRefreshScopeKey,
-      workspaceRefreshRetry,
-      workspaceSnapshotLossCount,
-    );
-  }, [workspaceRefreshScopeKey, workspaceRefreshRetry, workspaceSnapshotLossCount]);
-  return useStableCallback(() =>
-    refreshWorkspaceSnapshotIfStale(
-      Date.now(),
-      workspaceRefreshScopeKey,
-      workspaceRefreshRetry,
-      workspaceSnapshotLossCount,
-    ),
-  );
-}
-
-const NO_PROVIDER_MODELS: ServerProvider["models"] = [];
-
-/**
- * The provider instances the composer offers, which one is selected, and the model and options
- * the draft resolves to on it. The draft store is read first so every derivation follows the only
- * hook here; in the composer body they would straddle its later hooks and lose their cache.
- */
-function useComposerProviderModel(
-  input: Omit<Parameters<typeof resolveComposerProviderSelection>[0], "entries"> & {
-    readonly providerStatuses: ReadonlyArray<ServerProvider>;
-    readonly settings: UnifiedSettings;
-    readonly activeProjectId: ProjectId | null;
-    readonly threadRef: ScopedThreadRef | DraftId;
-    readonly threadModelSelection: ModelSelection | null | undefined;
-    readonly projectModelSelection: ModelSelection | null | undefined;
-    readonly promptInjectionState: ReturnType<typeof getComposerPromptInjectionState>;
-  },
-) {
-  const {
-    providerStatuses,
-    settings,
-    activeProjectId,
-    threadRef,
-    threadModelSelection,
-    projectModelSelection,
-    promptInjectionState,
-    candidateInstanceIds,
-    lockedProvider,
-    lockedInstanceId,
-  } = input;
-  const draftModelState = useComposerDraftModelState(threadRef);
-  const providerInstanceEntries: ReadonlyArray<ProviderInstanceEntry> = sortProviderInstanceEntries(
-    applyProviderInstanceSettings(
-      deriveProviderInstanceEntries(providerStatuses),
-      settings,
-      activeProjectId,
-    ),
-  );
-  const selection = resolveComposerProviderSelection({
-    entries: providerInstanceEntries,
-    candidateInstanceIds,
-    lockedProvider,
-    lockedInstanceId,
-  });
-  const selectedInstanceId =
-    selection.selectedProviderEntry?.instanceId ?? NO_PROVIDER_MODEL_SELECTION.instanceId;
-  const selectedProvider: ProviderDriverKind =
-    selection.selectedProviderEntry?.driverKind ?? selection.requestedDriverKind;
-  const modelState = deriveEffectiveComposerModelState({
-    draft: draftModelState,
-    providers: providerStatuses,
-    selectedProvider,
-    selectedInstanceId,
-    threadModelSelection,
-    projectModelSelection,
-    settings,
-  });
-  const selectedProviderModels = selection.selectedProviderEntry?.models ?? NO_PROVIDER_MODELS;
-  const composerProviderState = getComposerProviderState({
-    provider: selectedProvider,
-    model: modelState.selectedModel,
-    models: selectedProviderModels,
-    promptInjectionState,
-    modelOptions: modelState.modelOptions?.[selectedInstanceId],
-    planModeEnabled: settings.planModeEnabled,
-  });
-  return {
-    providerInstanceEntries,
-    ...selection,
-    selectedInstanceId,
-    selectedProvider,
-    selectedProviderModels,
-    composerModelOptions: modelState.modelOptions,
-    selectedModel: modelState.selectedModel,
-    composerProviderState,
-  };
-}
-
-/**
- * Follows the SnapShots still animating into this draft and splits the draft's images into
- * pending SnapShots and standalone images.
- */
-function useComposerImageGroups(
-  composerDraftTarget: ScopedThreadRef | DraftId,
-  composerImages: ReadonlyArray<ComposerImageAttachment>,
-  composerPreviewAnnotations: ReadonlyArray<{ readonly id: string }>,
-) {
-  const pendingSnapShotAnimations = useSyncExternalStore(
-    subscribeToPendingSnapShotAnimations,
-    getPendingSnapShotAnimations,
-    getPendingSnapShotAnimations,
-  );
-  const pendingSnapShotIds = pendingSnapShotAnimationIdsForTarget(
-    pendingSnapShotAnimations,
-    composerDraftTarget,
-  );
-  const previewAnnotationIds = new Set(
-    composerPreviewAnnotations.map((annotation) => annotation.id),
-  );
-  return {
-    pendingSnapShotAnimations,
-    pendingSnapShotIds,
-    pendingSnapShotIdSet: new Set(pendingSnapShotIds),
-    uncommittedSnapShotIds: pendingSnapShotIds.filter(
-      (id) => !composerImages.some((image) => image.id === id),
-    ),
-    standaloneComposerImages: composerImages.filter((image) => !previewAnnotationIds.has(image.id)),
-  };
-}
-
 const NO_PROVIDER_SKILLS: ServerProvider["skills"] = [];
 const NO_PROVIDER_SLASH_COMMANDS: ServerProvider["slashCommands"] = [];
 
```

## apps/web/src/components/chat/ComposerCommandMenuLayer.tsx

Status: added; +94; −0

```diff
@@ -1,0 +1,94 @@
+import { type ReactNode, useLayoutEffect, useState } from "react";
+import { createPortal } from "react-dom";
+import { observeResize } from "~/lib/observeResize";
+
+type ComposerCommandMenuPosition = {
+  bottom: number;
+  left: number;
+  maxHeight: number;
+  width: number;
+};
+
+function composerCommandMenuPositionsEqual(
+  a: ComposerCommandMenuPosition,
+  b: ComposerCommandMenuPosition,
+): boolean {
+  return (
+    a.bottom === b.bottom && a.left === b.left && a.maxHeight === b.maxHeight && a.width === b.width
+  );
+}
+
+export function ComposerCommandMenuLayer(props: {
+  anchor: HTMLElement | null;
+  children: ReactNode;
+}) {
+  const [position, setPosition] = useState<ComposerCommandMenuPosition | null>(null);
+
+  useLayoutEffect(() => {
+    const anchor = props.anchor;
+    if (!anchor) return;
+
+    const updatePosition = () => {
+      const form = anchor.closest<HTMLElement>('[data-chat-composer-form="true"]');
+      const mainSurface = form?.querySelector<HTMLElement>(
+        '[data-chat-composer-main-surface="true"]',
+      );
+      const rect = (mainSurface ?? form ?? anchor).getBoundingClientRect();
+      const rootFontSizePx =
+        Number.parseFloat(window.getComputedStyle(document.documentElement).fontSize) || 16;
+      const drawerInsetRem = Number.parseFloat(
+        window.getComputedStyle(form ?? anchor).getPropertyValue("--chat-composer-drawer-inset"),
+      );
+      const drawerInset = drawerInsetRem * rootFontSizePx;
+      const width = rect.width - drawerInset * 2;
+      if (width <= 0) {
+        setPosition(null);
+        return;
+      }
+      const composerOverlap = rootFontSizePx + 1;
+      const next = {
+        bottom: window.innerHeight - rect.top - composerOverlap,
+        left: rect.left + drawerInset,
+        maxHeight: Math.max(96, rect.top - 24 + composerOverlap),
+        width,
+      };
+      setPosition((current) =>
+        current && composerCommandMenuPositionsEqual(current, next) ? current : next,
+      );
+    };
+
+    updatePosition();
+    window.addEventListener("resize", updatePosition);
+    window.addEventListener("scroll", updatePosition, true);
+
+    const observed: Element[] = [anchor];
+    for (let element = anchor.parentElement; element; element = element.parentElement) {
+      observed.push(element);
+    }
+    const stopObserving = observeResize(observed, updatePosition);
+
+    return () => {
+      stopObserving();
+      window.removeEventListener("resize", updatePosition);
+      window.removeEventListener("scroll", updatePosition, true);
+    };
+  }, [props.anchor]);
+
+  if (!props.anchor || !position) return null;
+
+  return createPortal(
+    <div
+      className="pointer-events-auto fixed z-40 flex flex-col"
+      data-composer-drawer-layer="true"
+      style={{
+        bottom: position.bottom,
+        left: position.left,
+        maxHeight: position.maxHeight,
+        width: position.width,
+      }}
+    >
+      {props.children}
+    </div>,
+    document.body,
+  );
+}
```

## apps/web/src/components/chat/useComposerImageGroups.ts

Status: added; +40; −0

```diff
@@ -1,0 +1,40 @@
+import { type ComposerImageAttachment, type DraftId } from "../../composerDraftStore";
+import { type ScopedThreadRef } from "@t3tools/contracts";
+import {
+  getPendingSnapShotAnimations,
+  pendingSnapShotAnimationIdsForTarget,
+  subscribeToPendingSnapShotAnimations,
+} from "../../lib/snapShotAnimation";
+import { useSyncExternalStore } from "react";
+
+/**
+ * Follows the SnapShots still animating into this draft and splits the draft's images into
+ * pending SnapShots and standalone images.
+ */
+export function useComposerImageGroups(
+  composerDraftTarget: ScopedThreadRef | DraftId,
+  composerImages: ReadonlyArray<ComposerImageAttachment>,
+  composerPreviewAnnotations: ReadonlyArray<{ readonly id: string }>,
+) {
+  const pendingSnapShotAnimations = useSyncExternalStore(
+    subscribeToPendingSnapShotAnimations,
+    getPendingSnapShotAnimations,
+    getPendingSnapShotAnimations,
+  );
+  const pendingSnapShotIds = pendingSnapShotAnimationIdsForTarget(
+    pendingSnapShotAnimations,
+    composerDraftTarget,
+  );
+  const previewAnnotationIds = new Set(
+    composerPreviewAnnotations.map((annotation) => annotation.id),
+  );
+  return {
+    pendingSnapShotAnimations,
+    pendingSnapShotIds,
+    pendingSnapShotIdSet: new Set(pendingSnapShotIds),
+    uncommittedSnapShotIds: pendingSnapShotIds.filter(
+      (id) => !composerImages.some((image) => image.id === id),
+    ),
+    standaloneComposerImages: composerImages.filter((image) => !previewAnnotationIds.has(image.id)),
+  };
+}
```

## apps/web/src/components/chat/useComposerProviderModel.ts

Status: added; +100; −0

```diff
@@ -1,0 +1,100 @@
+import {
+  type DraftId,
+  deriveEffectiveComposerModelState,
+  useComposerDraftModelState,
+} from "../../composerDraftStore";
+import {
+  type ModelSelection,
+  type ProjectId,
+  ProviderDriverKind,
+  type ScopedThreadRef,
+  type ServerProvider,
+} from "@t3tools/contracts";
+import {
+  NO_PROVIDER_MODEL_SELECTION,
+  type ProviderInstanceEntry,
+  applyProviderInstanceSettings,
+  deriveProviderInstanceEntries,
+  sortProviderInstanceEntries,
+} from "../../providerInstances";
+import { type UnifiedSettings } from "@t3tools/contracts/settings";
+import { getComposerPromptInjectionState, getComposerProviderState } from "./composerProviderState";
+import { resolveComposerProviderSelection } from "../ChatView.logic";
+
+const NO_PROVIDER_MODELS: ServerProvider["models"] = [];
+
+/**
+ * The provider instances the composer offers, which one is selected, and the model and options
+ * the draft resolves to on it. The draft store is read first so every derivation follows the only
+ * hook here; in the composer body they would straddle its later hooks and lose their cache.
+ */
+export function useComposerProviderModel(
+  input: Omit<Parameters<typeof resolveComposerProviderSelection>[0], "entries"> & {
+    readonly providerStatuses: ReadonlyArray<ServerProvider>;
+    readonly settings: UnifiedSettings;
+    readonly activeProjectId: ProjectId | null;
+    readonly threadRef: ScopedThreadRef | DraftId;
+    readonly threadModelSelection: ModelSelection | null | undefined;
+    readonly projectModelSelection: ModelSelection | null | undefined;
+    readonly promptInjectionState: ReturnType<typeof getComposerPromptInjectionState>;
+  },
+) {
+  const {
+    providerStatuses,
+    settings,
+    activeProjectId,
+    threadRef,
+    threadModelSelection,
+    projectModelSelection,
+    promptInjectionState,
+    candidateInstanceIds,
+    lockedProvider,
+    lockedInstanceId,
+  } = input;
+  const draftModelState = useComposerDraftModelState(threadRef);
+  const providerInstanceEntries: ReadonlyArray<ProviderInstanceEntry> = sortProviderInstanceEntries(
+    applyProviderInstanceSettings(
+      deriveProviderInstanceEntries(providerStatuses),
+      settings,
+      activeProjectId,
+    ),
+  );
+  const selection = resolveComposerProviderSelection({
+    entries: providerInstanceEntries,
+    candidateInstanceIds,
+    lockedProvider,
+    lockedInstanceId,
+  });
+  const selectedInstanceId =
+    selection.selectedProviderEntry?.instanceId ?? NO_PROVIDER_MODEL_SELECTION.instanceId;
+  const selectedProvider: ProviderDriverKind =
+    selection.selectedProviderEntry?.driverKind ?? selection.requestedDriverKind;
+  const modelState = deriveEffectiveComposerModelState({
+    draft: draftModelState,
+    providers: providerStatuses,
+    selectedProvider,
+    selectedInstanceId,
+    threadModelSelection,
+    projectModelSelection,
+    settings,
+  });
+  const selectedProviderModels = selection.selectedProviderEntry?.models ?? NO_PROVIDER_MODELS;
+  const composerProviderState = getComposerProviderState({
+    provider: selectedProvider,
+    model: modelState.selectedModel,
+    models: selectedProviderModels,
+    promptInjectionState,
+    modelOptions: modelState.modelOptions?.[selectedInstanceId],
+    planModeEnabled: settings.planModeEnabled,
+  });
+  return {
+    providerInstanceEntries,
+    ...selection,
+    selectedInstanceId,
+    selectedProvider,
+    selectedProviderModels,
+    composerModelOptions: modelState.modelOptions,
+    selectedModel: modelState.selectedModel,
+    composerProviderState,
+  };
+}
```

## apps/web/src/components/chat/useComposerRestingTransition.ts

Status: added; +526; −0

```diff
@@ -1,0 +1,526 @@
+import { observeResize } from "~/lib/observeResize";
+import { shouldAnimateComposerRestingTransition } from "../composerFooterLayout";
+import { useEffect, useLayoutEffect, useRef } from "react";
+
+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;
+
+export function useComposerRestingTransition(
+  isCollapsed: boolean,
+  isResting: boolean,
+  restingControlsRef: React.RefObject<HTMLDivElement | null>,
+  onOverlayHeightChange: (height: number) => void,
+  animationsActive: boolean,
+  animationDurationMs: number,
+) {
+  const elementRef = useRef<HTMLDivElement>(null);
+  const isCollapsedRef = useRef(isCollapsed);
+  const previousCollapsedRef = useRef(isCollapsed);
+  const previousRestingRef = useRef(isResting);
+  const previousHeightRef = useRef<number | null>(null);
+  const previousModelStripHeightRef = useRef<number | null>(null);
+  const previousContentOffsetsRef = useRef<{
+    promptFromTop: number | null;
+    promptHeight: number | null;
+    actionFromBottom: number | null;
+    controlsFromBottom: number | null;
+    controlsFromLeft: number | null;
+    surfaceBottomInset: number | null;
+  }>({
+    promptFromTop: null,
+    promptHeight: null,
+    actionFromBottom: null,
+    controlsFromBottom: null,
+    controlsFromLeft: null,
+    surfaceBottomInset: 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 = () => {
+    // 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 = () => {
+    const element = elementRef.current;
+    const footer = element?.querySelector<HTMLElement>('[data-chat-composer-footer="true"]');
+    element?.style.removeProperty("overflow");
+    element?.style.removeProperty("clip-path");
+    element?.style.removeProperty("overflow-clip-margin");
+    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");
+    const shell = element?.closest<HTMLElement>('[data-slot="composer-shell"]');
+    shell?.removeAttribute("data-model-strip-transition");
+    const modelStrip = shell?.querySelector<HTMLElement>('[data-composer-model-strip="true"]');
+    for (const property of [
+      "position",
+      "top",
+      "visibility",
+      "height",
+      "min-height",
+      "padding-top",
+      "align-items",
+      "z-index",
+      "pointer-events",
+    ]) {
+      modelStrip?.style.removeProperty(property);
+    }
+    clearOverlayPin();
+  };
+
+  useLayoutEffect(() => {
+    isCollapsedRef.current = isCollapsed;
+  });
+
+  const transitionToCurrentGeometry = (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-composer-prompt-surface="true"], [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 continuousControls =
+      element.dataset.inlineRestingControls === "true" || element.dataset.modelOnlyStrip === "true";
+    const controls = nextIsCollapsed
+      ? restingControlsRef.current
+      : element.querySelector<HTMLElement>('[data-chat-composer-controls="left"]');
+
+    const shell = element.closest<HTMLElement>('[data-slot="composer-shell"]');
+    const modelStrip = shell?.querySelector<HTMLElement>('[data-composer-model-strip="true"]');
+    const interruptedStripHeight =
+      animationRef.current && modelStrip ? modelStrip.getBoundingClientRect().height : null;
+    const interruptedAnimation = animationRef.current;
+    const interruptedPromptTop = interruptedAnimation
+      ? (prompt?.getBoundingClientRect().top ?? null)
+      : null;
+    const interruptedActionTop = interruptedAnimation
+      ? (action?.getBoundingClientRect().top ?? null)
+      : null;
+    const interruptedControlsLeft = interruptedAnimation
+      ? (controls?.getBoundingClientRect().left ?? null)
+      : null;
+    const interruptedControlsTop = interruptedAnimation
+      ? (controls?.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 = [];
+    if (stateChanged) {
+      for (const animation of stateChangeAnimationsRef.current) animation.cancel();
+      stateChangeAnimationsRef.current = [];
+    }
+    clearTransitionStyles();
+
+    const nextRect = element.getBoundingClientRect();
+    const nextHeight = nextRect.height;
+    const nextModelStripHeight = modelStrip?.getBoundingClientRect().height ?? null;
+    // 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 overlayRect = overlay?.getBoundingClientRect();
+    const overlayHeight = overlayRect?.height ?? null;
+    if (overlayHeight !== null) {
+      onOverlayHeightChange(overlayHeight);
+    }
+    const nextPromptRect = prompt?.getBoundingClientRect() ?? null;
+    const nextPromptTop = nextPromptRect?.top ?? null;
+    const nextActionTop = action?.getBoundingClientRect().top ?? null;
+    const nextControlsRect = controls?.getBoundingClientRect() ?? null;
+    const nextControlsTop = nextControlsRect?.top ?? null;
+    const footerBottom = footer ? nextRect.bottom - footer.getBoundingClientRect().bottom : 1;
+    const previousHeight = interruptedHeight ?? previousHeightRef.current;
+    const targetChanged =
+      interruptedTargetHeight === null || Math.abs(interruptedTargetHeight - nextHeight) >= 0.5;
+    const shouldAnimate = shouldAnimateComposerRestingTransition({
+      hasCompletedInitialLayout: hasCompletedInitialLayoutRef.current,
+      stateChanged,
+      hasInterruptedAnimation: interruptedHeight !== null,
+    });
+
+    if (
+      shouldAnimate &&
+      animationsActive &&
+      previousHeight !== null &&
+      Math.abs(previousHeight - nextHeight) >= 0.5
+    ) {
+      const remainingDuration =
+        typeof interruptedDuration === "number" && interruptedCurrentTime !== null
+          ? Math.max(1, interruptedDuration - interruptedCurrentTime)
+          : animationDurationMs;
+      const duration =
+        interruptedHeight !== null && !targetChanged ? remainingDuration : animationDurationMs;
+      element.style.overflow = "clip";
+      if (modelStrip) {
+        const controlsClearance = Math.max(
+          nextModelStripHeight ?? 0,
+          previousModelStripHeightRef.current ?? 0,
+        );
+        element.style.overflowClipMargin = `${controlsClearance}px`;
+        element.style.clipPath = `inset(0 0 -${controlsClearance}px 0)`;
+      }
+      surface.style.height = "100%";
+
+      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;
+      }
+
+      if (footer) {
+        footer.style.position = "absolute";
+        footer.style.top = "auto";
+        footer.style.bottom = `${String(footerBottom)}px`;
+        footer.style.height = "3rem";
+        if (nextIsCollapsed) {
+          footer.style.left = "auto";
+          footer.style.right = "1px";
+        } else {
+          footer.style.left = "1px";
+          footer.style.right = "1px";
+        }
+      }
+
+      let stripAnimation: Animation | null = null;
+      if (modelStrip) {
+        const stripHeight = modelStrip.getBoundingClientRect().height;
+        const stripOverlap = -Number.parseFloat(getComputedStyle(modelStrip).marginTop);
+        const fromHeight =
+          interruptedStripHeight ??
+          (previousCollapsedRef.current
+            ? (previousModelStripHeightRef.current ?? stripHeight)
+            : stripOverlap);
+        const toHeight = nextIsCollapsed ? stripHeight : stripOverlap;
+        modelStrip.style.position = "relative";
+        modelStrip.style.top = "auto";
+        modelStrip.style.visibility = "visible";
+        modelStrip.style.minHeight = "0";
+        modelStrip.style.paddingTop = "0";
+        modelStrip.style.alignItems = "flex-end";
+        modelStrip.style.pointerEvents = "none";
+        modelStrip.style.zIndex = "20";
+        shell?.setAttribute("data-model-strip-transition", "true");
+        stripAnimation = modelStrip.animate(
+          [{ height: `${fromHeight}px` }, { height: `${toHeight}px` }],
+          { duration, easing: COMPOSER_RESTING_TRANSITION_EASING, fill: "both" },
+        );
+      }
+
+      const animation = element.animate(
+        [{ height: `${previousHeight}px` }, { height: `${nextHeight}px` }],
+        {
+          duration,
+          easing: COMPOSER_RESTING_TRANSITION_EASING,
+          fill: "both",
+        },
+      );
+      animationRef.current = animation;
+      animationTargetHeightRef.current = nextHeight;
+
+      const animatedRect = element.getBoundingClientRect();
+      const previousBottom =
+        overlayRect && previousContentOffsetsRef.current.surfaceBottomInset !== null
+          ? overlayRect.bottom - previousContentOffsetsRef.current.surfaceBottomInset
+          : animatedRect.bottom;
+      const bottomShift = previousBottom - animatedRect.bottom;
+      const previousPromptTop =
+        interruptedPromptTop ??
+        (previousContentOffsetsRef.current.promptFromTop === null
+          ? null
+          : animatedRect.top + bottomShift + previousContentOffsetsRef.current.promptFromTop);
+      const previousActionTop =
+        interruptedActionTop ??
+        (previousContentOffsetsRef.current.actionFromBottom === null
+          ? null
+          : previousBottom - previousContentOffsetsRef.current.actionFromBottom);
+      const contentAnimations: Animation[] = stripAnimation ? [stripAnimation] : [];
+      const animateContentPosition = (
+        content: HTMLElement | null,
+        previousTop: number | null,
+        previousLeft: number | null = null,
+      ) => {
+        if (!content || previousTop === null) return;
+        const rect = content.getBoundingClientRect();
+        const offset = previousTop - rect.top;
+        const offsetX = previousLeft === null ? 0 : previousLeft - rect.left;
+        if (Math.abs(offset) < 0.5 && Math.abs(offsetX) < 0.5) return;
+        contentAnimations.push(
+          content.animate(
+            [{ transform: `translate(${offsetX}px, ${offset}px)` }, { transform: "none" }],
+            {
+              duration,
+              easing: COMPOSER_RESTING_TRANSITION_EASING,
+            },
+          ),
+        );
+      };
+      animateContentPosition(prompt, previousPromptTop);
+      animateContentPosition(action, previousActionTop);
+      if (continuousControls) {
+        const previousControlsTop =
+          interruptedControlsTop ??
+          (previousContentOffsetsRef.current.controlsFromBottom === null
+            ? null
+            : previousBottom - previousContentOffsetsRef.current.controlsFromBottom);
+        const previousControlsLeft =
+          interruptedControlsLeft ??
+          (previousContentOffsetsRef.current.controlsFromLeft === null
+            ? null
+            : animatedRect.left + previousContentOffsetsRef.current.controlsFromLeft);
+        animateContentPosition(controls, previousControlsTop, previousControlsLeft);
+      }
+      contentAnimationsRef.current = contentAnimations;
+
+      if (stateChanged) {
+        const stateChangeAnimations: Animation[] = [];
+
+        const previousPromptHeight = previousContentOffsetsRef.current.promptHeight;
+        if (
+          !nextIsCollapsed &&
+          prompt &&
+          nextPromptRect &&
+          previousPromptHeight !== null &&
+          nextPromptRect.height - previousPromptHeight >= 0.5
+        ) {
+          const hiddenHeight = nextPromptRect.height - previousPromptHeight;
+          stateChangeAnimations.push(
+            prompt.animate(
+              [
+                { clipPath: `inset(0 0 ${String(hiddenHeight)}px 0)` },
+                { clipPath: "inset(0 0 0 0)" },
+              ],
+              {
+                duration,
+                easing: COMPOSER_RESTING_TRANSITION_EASING,
+              },
+            ),
+          );
+        }
+
+        const arrivingControls = nextIsCollapsed
+          ? restingControlsRef.current
+          : element.querySelector<HTMLElement>('[data-chat-composer-controls="left"]');
+        if (arrivingControls && !continuousControls) {
+          const drift = nextIsCollapsed
+            ? -COMPOSER_RESTING_CONTROLS_ARRIVAL_DRIFT_PX
+            : COMPOSER_RESTING_CONTROLS_ARRIVAL_DRIFT_PX;
+          stateChangeAnimations.push(
+            arrivingControls.animate(
+              [
+                { opacity: 0, transform: `translateY(${String(drift)}px)` },
+                { opacity: 1, transform: "none" },
+              ],
+              {
+                duration: nextIsCollapsed ? duration : duration / 2,
+                delay: nextIsCollapsed ? 0 : duration / 2,
+                fill: "backwards",
+                easing: COMPOSER_RESTING_TRANSITION_EASING,
+              },
+            ),
+          );
+        }
+
+        const arrivingImagePreviews = nextIsCollapsed
+          ? Array.from(
+              element.querySelectorAll<HTMLElement>('[data-chat-composer-resting-images="true"]'),
+            )
+          : Array.from(
+              element.querySelectorAll<HTMLElement>('[data-chat-composer-expanded-image="true"]'),
+            );
+        for (const imagePreview of arrivingImagePreviews) {
+          stateChangeAnimations.push(
+            imagePreview.animate([{ opacity: 0 }, { opacity: 1 }], {
+              duration: nextIsCollapsed ? duration : duration / 2,
+              delay: nextIsCollapsed ? 0 : duration / 2,
+              fill: "backwards",
+              easing: COMPOSER_RESTING_TRANSITION_EASING,
+            }),
+          );
+        }
+        stateChangeAnimationsRef.current = stateChangeAnimations;
+      }
+
+      const finishTransition = (cancelAnimations: boolean) => {
+        if (animationRef.current !== animation) return;
+        if (transitionCleanupTimeoutRef.current !== null) {
+          window.clearTimeout(transitionCleanupTimeoutRef.current);
+          transitionCleanupTimeoutRef.current = null;
+        }
+        if (cancelAnimations) {
+          animation.cancel();
+          for (const contentAnimation of contentAnimationsRef.current) {
+            contentAnimation.cancel();
+          }
+          for (const stateChangeAnimation of stateChangeAnimationsRef.current) {
+            stateChangeAnimation.cancel();
+          }
+        }
+        animationRef.current = null;
+        animationTargetHeightRef.current = null;
+        contentAnimationsRef.current = [];
+        stateChangeAnimationsRef.current = [];
+        animation.cancel();
+        stripAnimation?.cancel();
+        clearTransitionStyles();
+      };
+      void animation.finished.catch(() => undefined).then(() => finishTransition(false));
+      // A suspended document timeline can leave `finished` pending while
+      // these measurement styles remain active. Wall-clock cleanup makes
+      // the natural layout the eventual source of truth in that case.
+      transitionCleanupTimeoutRef.current = window.setTimeout(
+        () => finishTransition(true),
+        duration + COMPOSER_RESTING_TRANSITION_CLEANUP_BUFFER_MS,
+      );
+    } else {
+      animationTargetHeightRef.current = null;
+    }
+
+    previousCollapsedRef.current = nextIsCollapsed;
+    previousHeightRef.current = nextHeight;
+    previousModelStripHeightRef.current = nextModelStripHeight;
+    previousContentOffsetsRef.current = {
+      promptFromTop: nextPromptTop === null ? null : nextPromptTop - nextRect.top,
+      promptHeight: nextPromptRect?.height ?? null,
+      actionFromBottom: nextActionTop === null ? null : nextRect.bottom - nextActionTop,
+      controlsFromBottom: nextControlsTop === null ? null : nextRect.bottom - nextControlsTop,
+      controlsFromLeft: nextControlsRect === null ? null : nextControlsRect.left - nextRect.left,
+      surfaceBottomInset: overlayRect ? overlayRect.bottom - nextRect.bottom : null,
+    };
+  };
+
+  useLayoutEffect(() => {
+    const requestId = transitionLayoutRequestRef.current + 1;
+    transitionLayoutRequestRef.current = requestId;
+    const stateChanged = previousCollapsedRef.current !== isCollapsed;
+    // A non-Git context strip enters or leaves flow through ChatView state in
+    // an earlier layout effect. Let React flush that parent update before the
+    // FLIP reads its destination geometry, while still running before paint.
+    queueMicrotask(() => {
+      if (transitionLayoutRequestRef.current !== requestId) return;
+      transitionToCurrentGeometry(stateChanged);
+    });
+    return () => {
+      if (transitionLayoutRequestRef.current === requestId) {
+        transitionLayoutRequestRef.current += 1;
+      }
+    };
+  }, [isCollapsed, transitionToCurrentGeometry]);
+
+  useLayoutEffect(() => {
+    if (previousRestingRef.current === isResting) return;
+    previousRestingRef.current = isResting;
+    if (animationRef.current) return;
+    const overlay = elementRef.current?.closest<HTMLElement>('[data-chat-composer-overlay="true"]');
+    if (overlay) onOverlayHeightChange(overlay.getBoundingClientRect().height);
+  }, [isResting, onOverlayHeightChange]);
+
+  useLayoutEffect(() => {
+    const element = elementRef.current;
+    if (!element) return;
+
+    const body = element.querySelector<HTMLElement>('[data-chat-composer-body="true"]');
+    return observeResize(body ? [element, body] : element, (entries) => {
+      if (animationRef.current) {
+        if (body && entries.some((entry) => entry.target === body)) {
+          transitionToCurrentGeometry(false);
+        }
+        return;
+      }
+      const elementRect = element.getBoundingClientRect();
+      const visibleTransitionElement = (selector: string) =>
+        Array.from(element.querySelectorAll<HTMLElement>(selector)).find(
+          (candidate) => candidate.getClientRects().length > 0,
+        ) ?? null;
+      const promptRect = visibleTransitionElement(
+        '[data-composer-prompt-surface="true"], [data-chat-composer-transition-prompt="true"]',
+      )?.getBoundingClientRect();
+      const actionTop = visibleTransitionElement(
+        '[data-chat-composer-transition-actions="true"]',
+      )?.getBoundingClientRect().top;
+      const controlsRect = (
+        isCollapsedRef.current
+          ? restingControlsRef.current
+          : visibleTransitionElement('[data-chat-composer-controls="left"]')
+      )?.getBoundingClientRect();
+      previousHeightRef.current = elementRect.height;
+      previousContentOffsetsRef.current = {
+        promptFromTop: promptRect === undefined ? null : promptRect.top - elementRect.top,
+        promptHeight: promptRect?.height ?? null,
+        actionFromBottom: actionTop === undefined ? null : elementRect.bottom - actionTop,
+        controlsFromBottom:
+          controlsRect === undefined ? null : elementRect.bottom - controlsRect.top,
+        controlsFromLeft: controlsRect === undefined ? null : controlsRect.left - elementRect.left,
+        surfaceBottomInset:
+          (element
+            .closest<HTMLElement>('[data-chat-composer-overlay="true"]')
+            ?.getBoundingClientRect().bottom ?? elementRect.bottom) - elementRect.bottom,
+      };
+    });
+  }, [restingControlsRef, transitionToCurrentGeometry]);
+
+  useEffect(() => {
+    hasCompletedInitialLayoutRef.current = true;
+    return () => {
+      if (transitionCleanupTimeoutRef.current !== null) {
+        window.clearTimeout(transitionCleanupTimeoutRef.current);
+        transitionCleanupTimeoutRef.current = null;
+      }
+      animationRef.current?.cancel();
+      animationRef.current = null;
+      animationTargetHeightRef.current = null;
+      for (const animation of contentAnimationsRef.current) animation.cancel();
+      contentAnimationsRef.current = [];
+      for (const animation of stateChangeAnimationsRef.current) animation.cancel();
+      stateChangeAnimationsRef.current = [];
+      clearTransitionStyles();
+    };
+  }, [clearTransitionStyles]);
+
+  return elementRef;
+}
```

## apps/web/src/components/chat/useProviderWorkspaceSnapshotRefresh.ts

Status: added; +137; −0

```diff
@@ -1,0 +1,137 @@
+import {
+  type EnvironmentId,
+  PROVIDER_WORKSPACE_SNAPSHOT_TTL_MS,
+  type ServerProvider,
+} from "@t3tools/contracts";
+import { type ProviderInstanceEntry } from "../../providerInstances";
+import {
+  hasCompleteProviderWorkspaceSnapshot,
+  hasCurrentProviderWorkspaceSnapshot,
+} from "@t3tools/client-runtime/providerSkills";
+import { serverEnvironment } from "../../state/server";
+import { useAtomCommand } from "../../state/use-atom-command";
+import { useEffect, useEffectEvent, useRef, useState } from "react";
+import { useStableCallback } from "../../hooks/useStableCallback";
+
+const WORKSPACE_SNAPSHOT_RETRY_COOLDOWN_MS = 10_000;
+
+/**
+ * Keeps the selected provider's workspace snapshot fresh for the composer's
+ * cwd. Returns a check to run when the user types.
+ */
+export function useProviderWorkspaceSnapshotRefresh(input: {
+  readonly environmentId: EnvironmentId;
+  readonly gitCwd: string | null;
+  readonly selectedProviderEntry: ProviderInstanceEntry | undefined;
+  readonly selectedProviderStatus: ServerProvider | null;
+}) {
+  const { environmentId, gitCwd, selectedProviderEntry, selectedProviderStatus } = input;
+  const refreshProviders = useAtomCommand(serverEnvironment.refreshProviders, {
+    reportFailure: false,
+  });
+  // The last scan this composer asked for. A request inside the TTL is not
+  // repeated, so a client clock ahead of the server's cannot loop rescans.
+  const workspaceRefreshKeyRef = useRef<{ key: string; requestedAt: number } | null>(null);
+  const [workspaceRefreshRetry, setWorkspaceRefreshRetry] = useState<{
+    key: string;
+    notBefore: number;
+  } | null>(null);
+  const workspaceRefreshScopeKey =
+    gitCwd && selectedProviderEntry
+      ? `${environmentId}:${selectedProviderEntry.instanceId}:${gitCwd}`
+      : null;
+  const workspaceSlashCommandsPending =
+    selectedProviderStatus?.workspaceSnapshots?.some(
+      (snapshot) => snapshot.cwd === gitCwd && snapshot.slashCommandsPending === true,
+    ) ?? false;
+  useEffect(() => {
+    if (
+      !workspaceSlashCommandsPending ||
+      !workspaceRefreshRetry ||
+      workspaceRefreshRetry.key !== workspaceRefreshScopeKey
+    )
+      return;
+    const timeout = setTimeout(
+      () => {
+        setWorkspaceRefreshRetry((current) => (current === workspaceRefreshRetry ? null : current));
+      },
+      Math.max(0, workspaceRefreshRetry.notBefore - Date.now()),
+    );
+    return () => clearTimeout(timeout);
+  }, [workspaceRefreshRetry, workspaceRefreshScopeKey, workspaceSlashCommandsPending]);
+  // Losing a snapshot (the server dropped it) lifts both the TTL and the retry
+  // cooldown so the next check rescans at once.
+  const hasWorkspaceSnapshot = hasCompleteProviderWorkspaceSnapshot(selectedProviderStatus, gitCwd);
+  const [hadWorkspaceSnapshot, setHadWorkspaceSnapshot] = useState(hasWorkspaceSnapshot);
+  const [workspaceSnapshotLossCount, setWorkspaceSnapshotLossCount] = useState(0);
+  if (hadWorkspaceSnapshot !== hasWorkspaceSnapshot) {
+    setHadWorkspaceSnapshot(hasWorkspaceSnapshot);
+    if (!hasWorkspaceSnapshot) {
+      setWorkspaceSnapshotLossCount((count) => count + 1);
+      setWorkspaceRefreshRetry(null);
+    }
+  }
+  const lastWorkspaceSnapshotLossRef = useRef(workspaceSnapshotLossCount);
+  const refreshWorkspaceSnapshotIfStale = (
+    now: number,
+    key: string | null,
+    retry: { key: string; notBefore: number } | null,
+    snapshotLossCount: number,
+  ) => {
+    if (lastWorkspaceSnapshotLossRef.current !== snapshotLossCount) {
+      lastWorkspaceSnapshotLossRef.current = snapshotLossCount;
+      workspaceRefreshKeyRef.current = null;
+    }
+    if (key === null || !gitCwd || !selectedProviderEntry) return;
+    const lastRequest = workspaceRefreshKeyRef.current;
+    if (
+      lastRequest?.key === key &&
+      now - lastRequest.requestedAt < PROVIDER_WORKSPACE_SNAPSHOT_TTL_MS
+    )
+      return;
+    // A pending retry needs no clearing here: its cooldown lapses long before the TTL.
+    if (hasCurrentProviderWorkspaceSnapshot(selectedProviderStatus, gitCwd, now)) return;
+    if (retry?.key === key && now < retry.notBefore) return;
+    const request = { key, requestedAt: now };
+    workspaceRefreshKeyRef.current = request;
+    const retryLater = () => {
+      if (workspaceRefreshKeyRef.current !== request) return;
+      workspaceRefreshKeyRef.current = null;
+      setWorkspaceRefreshRetry({
+        key,
+        notBefore: Date.now() + WORKSPACE_SNAPSHOT_RETRY_COOLDOWN_MS,
+      });
+    };
+    void refreshProviders({
+      environmentId,
+      input: { instanceId: selectedProviderEntry.instanceId, cwd: gitCwd },
+    }).then((result) => {
+      const refreshedSnapshot =
+        result._tag === "Success" &&
+        hasCompleteProviderWorkspaceSnapshot(
+          result.value.providers.find(
+            (provider) => provider.instanceId === selectedProviderEntry.instanceId,
+          ),
+          gitCwd,
+        );
+      if (!refreshedSnapshot) retryLater();
+    }, retryLater);
+  };
+  const refreshWorkspaceSnapshotOnChange = useEffectEvent(refreshWorkspaceSnapshotIfStale);
+  useEffect(() => {
+    refreshWorkspaceSnapshotOnChange(
+      Date.now(),
+      workspaceRefreshScopeKey,
+      workspaceRefreshRetry,
+      workspaceSnapshotLossCount,
+    );
+  }, [workspaceRefreshScopeKey, workspaceRefreshRetry, workspaceSnapshotLossCount]);
+  return useStableCallback(() =>
+    refreshWorkspaceSnapshotIfStale(
+      Date.now(),
+      workspaceRefreshScopeKey,
+      workspaceRefreshRetry,
+      workspaceSnapshotLossCount,
+    ),
+  );
+}
```

## apps/web/src/components/chat/useRestingComposerControlsLayout.ts

Status: added; +84; −0

```diff
@@ -1,0 +1,84 @@
+import { measureRestingComposerControls } from "./restingComposerControlsMeasurement";
+import { observeResize } from "~/lib/observeResize";
+import { resolveRestingComposerControlsLayout } from "../composerFooterLayout";
+import { useLayoutEffect, useRef, useState } from "react";
+
+export function useRestingComposerControlsLayout(
+  host: HTMLDivElement | null,
+  useControlsAsHost = false,
+) {
+  const [controls, setControls] = useState<HTMLDivElement | null>(null);
+  const controlsRef = useRef<HTMLDivElement>(null);
+  const attachControls = (element: HTMLDivElement | null) => {
+    controlsRef.current = element;
+    setControls(element);
+  };
+  const [layout, setLayout] = useState<{
+    hiddenCount: number;
+    iconOnlyCount?: number;
+    visible: boolean;
+  }>({ hiddenCount: 0, visible: true });
+
+  const measure = () => {
+    const currentHost = useControlsAsHost ? controls : host;
+    if (currentHost === null || !controls) return;
+
+    const measurement = measureRestingComposerControls(controls);
+    if (!measurement) return;
+    const style = getComputedStyle(currentHost);
+    const hostWidth =
+      currentHost.clientWidth -
+      (Number.parseFloat(style.paddingInlineStart) || 0) -
+      (Number.parseFloat(style.paddingInlineEnd) || 0);
+
+    setLayout((current) => {
+      const next = resolveRestingComposerControlsLayout({
+        ...measurement,
+        hostWidth,
+        previous: current,
+      });
+      return next.hiddenCount === current.hiddenCount &&
+        next.iconOnlyCount === current.iconOnlyCount &&
+        next.visible === current.visible
+        ? current
+        : next;
+    });
+  };
+
+  useLayoutEffect(() => {
+    const currentHost = useControlsAsHost ? controls : host;
+    if (!currentHost || !controls) return;
+    let stopObserving = () => {};
+    const observeControls = () => {
+      stopObserving();
+      stopObserving = observeResize(
+        [
+          currentHost,
+          controls,
+          ...controls.querySelectorAll<HTMLElement>(
+            "[data-resting-block], [data-composer-control-label], [data-chat-provider-model-picker-label]",
+          ),
+        ],
+        measure,
+      );
+      measure();
+    };
+    observeControls();
+    const mutations = new MutationObserver(observeControls);
+    mutations.observe(controls, { childList: true, subtree: true, characterData: true });
+    document.fonts.addEventListener("loadingdone", measure);
+    return () => {
+      stopObserving();
+      mutations.disconnect();
+      document.fonts.removeEventListener("loadingdone", measure);
+    };
+  }, [host, controls, useControlsAsHost, measure]);
+
+  return {
+    controlsRef,
+    attachControls,
+    hiddenBlockCount: layout.hiddenCount,
+    iconOnlyBlockCount: layout.iconOnlyCount ?? 0,
+    controlsVisible: layout.visible,
+  };
+}
```
