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

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

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

Visibility: public

[refactor\(web\): composer attachment tiles live in their own component](https://git.cafe/pingdotgg/t3code/pull/22689.md)

Version: 8

Head commit: 1122a852bc1639e9a64142e58de97f38125c1336

Base commit: 163bdeaa119f32bf92f25d2841048b808df542a5

Comparison base: 163bdeaa119f32bf92f25d2841048b808df542a5

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; +54; −441

```diff
@@ -6,15 +6,12 @@
 import { isLocalEnvironmentDisabled } from "../../localEnvironment";
 import { usePrimaryEnvironmentId } from "../../state/environments";
 import { useRightPanelStore } from "~/rightPanelStore";
-import { AttachmentFilePreview } from "../files/AttachmentFilePreview";
-import { Dialog, DialogPopup, DialogTitle } from "../ui/dialog";
 import {
   composerPullRequestsFromLinks,
   filterComposerPullRequestMatches,
 } from "@t3tools/shared/composerPullRequestMatches";
 import { importPastedComposerText, readPastedComposerContext } from "../composerInlineTokenPaste";
 import { elementContextToPreviewAnnotation } from "../../lib/elementContext";
-import { RefreshIcon } from "~/components/ui/refresh-icon";
 import {
   questionAttachmentDraftId,
   countQuestionAttachments,
@@ -41,7 +38,6 @@
   ScopedThreadRef,
   ServerProvider,
   ThreadId,
-  SnapShotSource,
 } from "@t3tools/contracts";
 import {
   EnvironmentRequestInvalidError,
@@ -69,7 +65,6 @@
 import { USAGE_LIMITS_COMMAND } from "@t3tools/shared/usageLimits";
 import {
   memo,
-  type ComponentProps,
   type ReactNode,
   useEffect,
   useEffectEvent,
@@ -110,7 +105,6 @@
   makeComposerMentionDragHandlers,
 } from "./composerMentionDrag";
 import {
-  composerFloatingLayerProps,
   useComposerMenuProps,
   isInsideCollapsedComposerControls,
   isInsideRestingComposerControlScope,
@@ -189,15 +183,11 @@
   releaseAttachmentUpload,
   releaseDraftAttachment,
   releasePersistedAttachmentUpload,
-  retryAttachmentUpload,
   startAttachmentUpload,
   useAttachmentUploadStore,
   verifyStashedAttachmentUpload,
 } from "../../lib/attachmentUploadQueue";
-import {
-  attachmentUploadBlockReason,
-  formatAttachmentUploadProgress,
-} from "../../lib/attachmentUploadState";
+import { attachmentUploadBlockReason } from "../../lib/attachmentUploadState";
 import { isCommandPaletteOpen } from "../../commandPaletteBus";
 import { getTerminalFocusOwner } from "../../lib/terminalFocus";
 import type { AssistantCitationSourceAnchor } from "~/lib/assistantTextSelection";
@@ -297,6 +287,11 @@
 import { ComposerPendingApprovalActions } from "./ComposerPendingApprovalActions";
 import { CompactComposerControlsMenu } from "./CompactComposerControlsMenu";
 import { ComposerImageThumbnail } from "./ComposerImageThumbnail";
+import {
+  ComposerAttachmentFileList,
+  ComposerAttachmentMediaRow,
+  ComposerFilePreviewDialog,
+} from "./ComposerAttachmentStrip";
 import { ComposerPrimaryActions } from "./ComposerPrimaryActions";
 import { ComposerPendingApprovalPanel } from "./ComposerPendingApprovalPanel";
 import { ComposerPendingUserInputPanel } from "./ComposerPendingUserInputPanel";
@@ -331,21 +326,13 @@
   shouldReserveContextWindowMeter,
 } from "./ContextWindowMeter.logic";
 import {
-  attachVideoThumbnail,
   buildAttachmentVideoPreview,
   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";
@@ -371,28 +358,8 @@
   resetComposerScrollGesture,
   suppressActiveComposerScrollGesture,
 } from "./composerScrollGesture";
-import { prepareVideoFirstFrame } from "../../lib/videoFirstFrame";
 import { observeResize } from "~/lib/observeResize";
 
-function ComposerVideoThumbnail({ file }: { file: File }) {
-  const setVideo = (video: HTMLVideoElement | null) => {
-    if (!video) return;
-    return attachVideoThumbnail(video, 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;
@@ -400,46 +367,6 @@
   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-drawer starting:scale-95 starting:opacity-0 motion-reduce:transition-none motion-reduce:starting:scale-100 motion-reduce:starting:opacity-100",
-        className,
-      )}
-      {...props}
-    />
-  );
-}
-
 const COMPOSER_PULL_REQUEST_LIST_LIMIT = 99;
 const isEnvironmentRequestInvalidError = Schema.is(EnvironmentRequestInvalidError);
 const COMPOSER_PULL_REQUEST_RESULT_LIMIT = 12;
@@ -6404,6 +6331,47 @@
     ],
   );
 
+  // Built outside the form's JSX so typing, which rebuilds that JSX, leaves them cached.
+  const composerAttachmentMediaRow =
+    !isComposerCollapsedMobile &&
+    (uncommittedSnapShotIds.length > 0 ||
+      composerVideos.length > 0 ||
+      expandedComposerImages.length > 0) ? (
+      <ComposerAttachmentMediaRow
+        images={composerImages}
+        expandedImages={expandedComposerImages}
+        videos={composerVideos}
+        pendingSnapShotAnimations={pendingSnapShotAnimations}
+        pendingSnapShotIds={pendingSnapShotIds}
+        uncommittedSnapShotIds={uncommittedSnapShotIds}
+        nonPersistedImageIds={nonPersistedComposerImageIds}
+        snapShotAnimations={settings.snapShotAnimations}
+        supportsAttachmentUploads={supportsAttachmentUploads}
+        maxFileAttachmentBytes={maxFileAttachmentBytes}
+        environmentId={environmentId}
+        attachmentDraftTarget={attachmentDraftTarget}
+        canOperateThread={canOperateThread}
+        onExpandImage={onExpandImage}
+        onFileOpen={onFileOpen}
+        onRemoveImage={removeComposerImage}
+        onRemoveFile={removeComposerFileFromDraft}
+      />
+    ) : null;
+  const composerAttachmentFileList =
+    !isComposerCollapsedMobile && composerOtherFiles.length > 0 ? (
+      <ComposerAttachmentFileList
+        files={composerOtherFiles}
+        resolvedTheme={resolvedTheme}
+        supportsAttachmentUploads={supportsAttachmentUploads}
+        maxFileAttachmentBytes={maxFileAttachmentBytes}
+        fileStagingLimit={fileStagingLimit}
+        environmentId={environmentId}
+        attachmentDraftTarget={attachmentDraftTarget}
+        canOperateThread={canOperateThread}
+        onPreviewFile={setPreviewFileId}
+        onRemoveFile={removeComposerFileFromDraft}
+      />
+    ) : null;
   return (
     <form
       ref={composerFormRef}
@@ -6758,334 +6726,9 @@
                   </div>
                 )}
 
-              {!isComposerCollapsedMobile &&
-                (uncommittedSnapShotIds.length > 0 ||
-                  composerVideos.length > 0 ||
-                  expandedComposerImages.length > 0) && (
-                  <div
-                    className={cn(
-                      "mb-3 flex max-w-full gap-2",
-                      pendingSnapShotIds.length > 0 ||
-                        expandedComposerImages.some((image) => image.source?.kind === "snap-shot")
-                        ? "snap-x snap-proximity overflow-x-auto overscroll-x-contain pb-1 scrollbar-thumb-foreground/18 scrollbar-track-transparent [scrollbar-width:thin] [&::-webkit-scrollbar]:h-2.5 [&::-webkit-scrollbar-thumb]:rounded-full [&::-webkit-scrollbar-thumb]:border-3 [&::-webkit-scrollbar-thumb]:border-transparent [&::-webkit-scrollbar-thumb]:bg-foreground/18 [&::-webkit-scrollbar-thumb]:bg-clip-content [&::-webkit-scrollbar-thumb:hover]:bg-foreground/28 [&::-webkit-scrollbar-track]:mx-1 [&::-webkit-scrollbar-track]:bg-transparent"
-                        : "flex-wrap",
-                    )}
-                  >
-                    {expandedComposerImages
-                      .map((image) => {
-                        const upload = supportsAttachmentUploads
-                          ? uploadsByImageId[image.id]
-                          : undefined;
-                        const snapShotAnimationPending =
-                          image.source?.kind === "snap-shot" && pendingSnapShotIdSet.has(image.id);
-                        const snapShotArrival =
-                          image.source?.kind === "snap-shot" &&
-                          shouldAnimateSnapShotArrival(image.source.capturedAt);
-                        return (
-                          <SnapShotAttachmentFrame
-                            key={image.id}
-                            data-chat-composer-expanded-image="true"
-                            aria-hidden={snapShotAnimationPending || undefined}
-                            inert={snapShotAnimationPending || undefined}
-                            arrival={snapShotArrival}
-                            animateArrival={
-                              settings.snapShotAnimations &&
-                              !snapShotAnimationPending &&
-                              snapShotArrival
-                            }
-                            animationId={snapShotAnimationPending ? image.id : undefined}
-                            animationSource={snapShotAnimationPending ? image.source : undefined}
-                            className={cn(
-                              "group/attachment shrink-0 snap-start bg-background",
-                              image.source?.kind === "snap-shot"
-                                ? SNAP_SHOT_ATTACHMENT_FRAME_CLASS
-                                : "relative h-16 w-16 overflow-hidden rounded-lg border border-border/80",
-                              snapShotAnimationPending && "invisible",
-                            )}
-                          >
-                            {image.previewUrl ? (
-                              <button
-                                type="button"
-                                className="h-full w-full cursor-zoom-in"
-                                aria-label={`Preview ${image.name}`}
-                                onClick={() => {
-                                  const preview = buildExpandedImagePreview(
-                                    composerImages,
-                                    image.id,
-                                  );
-                                  if (!preview) return;
-                                  onExpandImage(preview);
-                                }}
-                              >
-                                <ComposerImageThumbnail
-                                  file={image.file}
-                                  alt={image.name}
-                                  className="h-full w-full object-cover"
-                                  fallback={
-                                    isHeicImageFile({ name: image.name, type: image.mimeType }) ? (
-                                      <AttachmentImage
-                                        name={image.name}
-                                        mimeType={image.mimeType}
-                                        src={image.previewUrl}
-                                        alt={image.name}
-                                        className="h-full w-full object-cover"
-                                      />
-                                    ) : (
-                                      <span className="flex h-full items-center justify-center px-1 text-3xs text-secondary-label">
-                                        {image.name}
-                                      </span>
-                                    )
-                                  }
-                                />
-                              </button>
-                            ) : (
-                              <div className="flex h-full w-full items-center justify-center px-1 text-center text-3xs text-secondary-label">
-                                {image.name}
-                              </div>
-                            )}
-                            {image.source?.kind === "snap-shot" ? (
-                              <SnapShotAttachmentDetails
-                                source={image.source}
-                                className={cn(
-                                  upload?.status === "uploading" && "bottom-4",
-                                  upload?.status === "failed" && "bottom-8",
-                                )}
-                              />
-                            ) : null}
-                            {nonPersistedComposerImageIdSet.has(image.id) && (
-                              <Tooltip>
-                                <TooltipTrigger
-                                  render={
-                                    <span
-                                      role="img"
-                                      aria-label="Draft attachment may not persist"
-                                      className="absolute left-1 top-1 inline-flex items-center justify-center rounded bg-background/85 p-0.5 text-warning-foreground"
-                                    >
-                                      <CircleAlertIcon className="size-3" />
-                                    </span>
-                                  }
+              {composerAttachmentMediaRow}
-                                />
-                                <TooltipPopup side="top">
-                                  Draft attachment could not be saved locally and may be lost on
-                                  navigation.
-                                </TooltipPopup>
-                              </Tooltip>
-                            )}
-                            {upload?.status === "uploading" && (
-                              <span className="pointer-events-none absolute inset-x-0 bottom-0 bg-background/85 px-1 text-center text-3xs text-foreground">
-                                {formatAttachmentUploadProgress(upload.progress)}
-                              </span>
-                            )}
-                            {upload?.status === "failed" && (
-                              <Tooltip>
-                                <TooltipTrigger
-                                  render={
-                                    <Button
-                                      variant="overlay"
-                                      size="icon-xs"
-                                      className="absolute bottom-1 left-1"
-                                      onClick={() =>
-                                        retryAttachmentUpload({
-                                          environmentId,
-                                          image,
-                                          draftTarget: attachmentDraftTarget,
-                                        })
-                                      }
-                                      disabled={!canOperateThread}
-                                      aria-label={`Retry upload for ${image.name}`}
-                                    />
-                                  }
-                                >
-                                  <RefreshIcon />
-                                </TooltipTrigger>
-                                <TooltipPopup side="top">{upload.reason}</TooltipPopup>
-                              </Tooltip>
-                            )}
-                            <span
-                              className={cn(
-                                "absolute right-1 top-1 flex",
-                                image.source?.kind === "snap-shot" &&
-                                  "opacity-0 transition-opacity pointer-coarse:opacity-100 group-hover/attachment:opacity-100 group-focus-within/attachment:opacity-100",
-                              )}
-                            >
-                              <Button
-                                variant="media-close"
-                                size="icon-xs"
-                                onClick={() => removeComposerImage(image.id)}
-                                aria-label={`Remove ${image.name}`}
-                              >
-                                <XIcon />
-                              </Button>
-                            </span>
-                          </SnapShotAttachmentFrame>
-                        );
-                      })
-                      .concat(
-                        uncommittedSnapShotIds.map((captureId) => (
-                          <SnapShotAttachmentFrame
-                            key={captureId}
-                            aria-hidden="true"
-                            animationId={captureId}
-                            animationSource={
-                              pendingSnapShotAnimations.find((capture) => capture.id === captureId)
-                                ?.source
-                            }
-                            className={cn(
-                              SNAP_SHOT_ATTACHMENT_FRAME_CLASS,
-                              "invisible shrink-0 snap-start bg-background",
-                            )}
-                          />
-                        )),
-                      )}
-                    {composerVideos.map((file) => {
-                      const fileCanUpload =
-                        supportsAttachmentUploads &&
-                        maxFileAttachmentBytes !== null &&
-                        file.sizeBytes <= maxFileAttachmentBytes;
-                      const upload = fileCanUpload ? uploadsByImageId[file.id] : undefined;
-                      return (
-                        <div
-                          key={file.id}
-                          className="relative h-16 w-16 shrink-0 snap-start overflow-hidden rounded-lg border border-border/80 bg-black"
-                        >
-                          <button
-                            type="button"
-                            className="flex h-full w-full cursor-zoom-in flex-col items-center justify-center gap-1 px-1 text-white"
-                            aria-label={`Play ${file.name}`}
-                            onClick={() => {
-                              if (file.file !== null) {
-                                const preview = buildExpandedImagePreview([file], file.id);
-                                if (preview) onExpandImage(preview);
-                                return;
-                              }
-                              if (!file.uploadedAttachmentId) return;
-                              onFileOpen({ ...file, id: file.uploadedAttachmentId });
-                            }}
-                          >
-                            {file.file && (
-                              <>
-                                <ComposerVideoThumbnail file={file.file} />
-                                <span className="pointer-events-none absolute inset-0 bg-gradient-to-t from-black/70 via-black/10 to-black/10" />
-                              </>
-                            )}
-                            <PlayIcon className="relative z-10 size-4 fill-current drop-shadow-md" />
-                          </button>
-                          {upload?.status === "uploading" && (
-                            <span className="pointer-events-none absolute inset-x-0 bottom-0 bg-background/85 px-1 text-center text-3xs text-foreground">
-                              {formatAttachmentUploadProgress(upload.progress)}
-                            </span>
-                          )}
-                          {upload?.status === "failed" && (
-                            <Tooltip>
-                              <TooltipTrigger
-                                render={
-                                  <Button
-                                    variant="overlay"
-                                    size="icon-xs"
-                                    className="absolute bottom-1 left-1"
-                                    onClick={() =>
-                                      retryAttachmentUpload({
-                                        environmentId,
-                                        image: file,
-                                        draftTarget: attachmentDraftTarget,
-                                      })
-                                    }
-                                    disabled={!canOperateThread}
-                                    aria-label={`Retry upload for ${file.name}`}
-                                  />
-                                }
-                              >
-                                <RefreshIcon />
-                              </TooltipTrigger>
-                              <TooltipPopup side="top">{upload.reason}</TooltipPopup>
-                            </Tooltip>
-                          )}
-                          <Button
-                            variant="media-close"
-                            size="icon-xs"
-                            className="absolute right-1 top-1"
-                            onClick={() => removeComposerFileFromDraft(file.id)}
-                            aria-label={`Remove ${file.name}`}
-                          >
-                            <XIcon />
-                          </Button>
-                        </div>
-                      );
-                    })}
-                  </div>
-                )}
 
-              {!isComposerCollapsedMobile && composerOtherFiles.length > 0 && (
-                <div className="mb-3 flex flex-col gap-1">
-                  {composerOtherFiles.map((file) => {
-                    const fileCanUpload =
-                      supportsAttachmentUploads &&
-                      maxFileAttachmentBytes !== null &&
-                      file.sizeBytes <= maxFileAttachmentBytes;
-                    const upload = fileCanUpload ? uploadsByImageId[file.id] : undefined;
-                    const needsReattach = composerFileNeedsReattach(file);
-                    const canReattachFile =
-                      fileStagingLimit !== null && file.sizeBytes <= fileStagingLimit;
-                    return (
-                      <div
-                        key={file.id}
-                        className="flex min-w-0 items-center gap-2 py-1 text-sm text-foreground"
-                      >
-                        <PierreEntryIcon pathValue={file.name} kind="file" theme={resolvedTheme} />
-                        <button
-                          type="button"
-                          disabled={needsReattach}
-                          className="min-w-0 flex-1 truncate text-left hover:underline focus-visible:outline-2"
-                          onClick={() => setPreviewFileId(file.id)}
-                        >
-                          {file.name}
-                        </button>
-                        <span className="shrink-0 text-xs text-secondary-label">
-                          {needsReattach
-                            ? canReattachFile
-                              ? "Attach again"
-                              : "Remove to send"
-                            : upload?.status === "uploading"
-                              ? formatAttachmentUploadProgress(upload.progress)
-                              : formatAttachmentSize(file.sizeBytes)}
-                        </span>
-                        {!needsReattach && upload?.status === "failed" ? (
+              {composerAttachmentFileList}
-                          <Tooltip>
-                            <TooltipTrigger
-                              render={
-                                <Button
-                                  variant="ghost"
-                                  size="icon-xs"
-                                  onClick={() =>
-                                    retryAttachmentUpload({
-                                      environmentId,
-                                      image: file,
-                                      draftTarget: attachmentDraftTarget,
-                                    })
-                                  }
-                                  disabled={!canOperateThread}
-                                  aria-label={`Retry upload for ${file.name}`}
-                                />
-                              }
-                            >
-                              <RefreshIcon />
-                            </TooltipTrigger>
-                            <TooltipPopup side="top">{upload.reason}</TooltipPopup>
-                          </Tooltip>
-                        ) : null}
-                        <Button
-                          variant="ghost"
-                          size="icon-xs"
-                          onClick={() => removeComposerFileFromDraft(file.id)}
-                          aria-label={`Remove ${file.name}`}
-                        >
-                          <XIcon />
-                        </Button>
-                      </div>
-                    );
-                  })}
-                </div>
-              )}
 
               <div
                 className={cn("relative", isComposerResting && "flex min-w-0 items-center gap-1")}
@@ -7096,41 +6739,11 @@
                 }
               >
                 {previewFile ? (
+                  <ComposerFilePreviewDialog
+                    file={previewFile}
+                    onRemove={removeComposerFileFromDraft}
-                  <Dialog
-                    open
+                    onPreviewFileIdChange={setPreviewFileId}
+                  />
-                    onOpenChange={(open) => {
-                      if (!open) setPreviewFileId(null);
-                    }}
-                  >
-                    <DialogPopup
-                      {...composerFloatingLayerProps}
-                      className="h-[min(85vh,52rem)] max-w-4xl overflow-hidden"
-                      showCloseButton={false}
-                    >
-                      <DialogTitle className="sr-only">{previewFile.name}</DialogTitle>
-                      <AttachmentFilePreview
-                        key={previewFile.id}
-                        name={previewFile.name}
-                        mimeType={previewFile.mimeType}
-                        sizeBytes={previewFile.sizeBytes}
-                        file={previewFile.file}
-                        origin="Draft"
-                        {...(previewFile.uploadedAttachmentId && previewFile.uploadEnvironmentId
-                          ? {
-                              asset: {
-                                environmentId: previewFile.uploadEnvironmentId,
-                                attachmentId: previewFile.uploadedAttachmentId,
-                              },
-                            }
-                          : {})}
-                        onRemove={() => {
-                          removeComposerFileFromDraft(previewFile.id);
-                          setPreviewFileId(null);
-                        }}
-                        onClose={() => setPreviewFileId(null)}
-                      />
-                    </DialogPopup>
-                  </Dialog>
                 ) : null}
                 <ComposerContextActionsContext value={composerContextActions}>
                   <ComposerPromptEditor
```

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

Status: added; +535; −0

```diff
@@ -1,0 +1,535 @@
+import type {
+  ChatFileAttachment,
+  EnvironmentId,
+  ScopedThreadRef,
+  SnapShotSource,
+} from "@t3tools/contracts";
+import { formatAttachmentSize } from "@t3tools/client-runtime/state/attachments";
+import { CircleAlertIcon, PlayIcon, XIcon } from "lucide-react";
+import { type ComponentProps, useLayoutEffect, useRef } from "react";
+
+import {
+  type ComposerFileAttachment,
+  type ComposerImageAttachment,
+  type DraftId,
+  composerFileNeedsReattach,
+} from "../../composerDraftStore";
+import { retryAttachmentUpload, useAttachmentUploadStore } from "../../lib/attachmentUploadQueue";
+import { formatAttachmentUploadProgress } from "../../lib/attachmentUploadState";
+import { isHeicImageFile } from "../../lib/imageCompression";
+import {
+  type PendingSnapShotAnimation,
+  scheduleSnapShotAnimationDestination,
+  setSnapShotAnimationDestination,
+  shouldAnimateSnapShotArrival,
+} from "../../lib/snapShotAnimation";
+import { cn } from "~/lib/utils";
+import { RefreshIcon } from "~/components/ui/refresh-icon";
+import { AttachmentFilePreview } from "../files/AttachmentFilePreview";
+import { AttachmentImage } from "../media/AttachmentImage";
+import { Button } from "../ui/button";
+import { Dialog, DialogPopup, DialogTitle } from "../ui/dialog";
+import { Tooltip, TooltipPopup, TooltipTrigger } from "../ui/tooltip";
+import { ComposerImageThumbnail } from "./ComposerImageThumbnail";
+import { composerFloatingLayerProps } from "./composerEventScope";
+import {
+  attachVideoThumbnail,
+  buildExpandedImagePreview,
+  type ExpandedImagePreview,
+} from "./ExpandedImagePreview";
+import { PierreEntryIcon } from "./PierreEntryIcon";
+import {
+  SNAP_SHOT_ATTACHMENT_FRAME_CLASS,
+  SnapShotAttachmentDetails,
+} from "./SnapShotAttachmentDetails";
+import { prepareVideoFirstFrame } from "../../lib/videoFirstFrame";
+
+function ComposerVideoThumbnail({ file }: { file: File }) {
+  const setVideo = (video: HTMLVideoElement | null) => {
+    if (!video) return;
+    return attachVideoThumbnail(video, 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"
+    />
+  );
+}
+
+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-drawer starting:scale-95 starting:opacity-0 motion-reduce:transition-none motion-reduce:starting:scale-100 motion-reduce:starting:opacity-100",
+        className,
+      )}
+      {...props}
+    />
+  );
+}
+
+/** Image, SnapShot and video tiles above the composer editor. */
+export function ComposerAttachmentMediaRow({
+  images,
+  expandedImages,
+  videos,
+  pendingSnapShotAnimations,
+  pendingSnapShotIds,
+  uncommittedSnapShotIds,
+  nonPersistedImageIds,
+  snapShotAnimations,
+  supportsAttachmentUploads,
+  maxFileAttachmentBytes,
+  environmentId,
+  attachmentDraftTarget,
+  canOperateThread,
+  onExpandImage,
+  onFileOpen,
+  onRemoveImage,
+  onRemoveFile,
+}: {
+  readonly images: ReadonlyArray<ComposerImageAttachment>;
+  readonly expandedImages: ReadonlyArray<ComposerImageAttachment>;
+  readonly videos: ReadonlyArray<ComposerFileAttachment>;
+  readonly pendingSnapShotAnimations: ReadonlyArray<PendingSnapShotAnimation>;
+  readonly pendingSnapShotIds: ReadonlyArray<string>;
+  readonly uncommittedSnapShotIds: ReadonlyArray<string>;
+  readonly nonPersistedImageIds: ReadonlyArray<string>;
+  readonly snapShotAnimations: boolean;
+  readonly supportsAttachmentUploads: boolean;
+  readonly maxFileAttachmentBytes: number | null;
+  readonly environmentId: EnvironmentId;
+  readonly attachmentDraftTarget: ScopedThreadRef | DraftId;
+  readonly canOperateThread: boolean;
+  readonly onExpandImage: (preview: ExpandedImagePreview) => void;
+  readonly onFileOpen: (attachment: ChatFileAttachment) => void;
+  readonly onRemoveImage: (imageId: string) => void;
+  readonly onRemoveFile: (fileId: string) => void;
+}) {
+  const uploadsByImageId = useAttachmentUploadStore((state) => state.uploadsByImageId);
+  const pendingSnapShotIdSet = new Set(pendingSnapShotIds);
+  const nonPersistedImageIdSet = new Set(nonPersistedImageIds);
+  return (
+    <div
+      className={cn(
+        "mb-3 flex max-w-full gap-2",
+        pendingSnapShotIds.length > 0 ||
+          expandedImages.some((image) => image.source?.kind === "snap-shot")
+          ? "snap-x snap-proximity overflow-x-auto overscroll-x-contain pb-1 scrollbar-thumb-foreground/18 scrollbar-track-transparent [scrollbar-width:thin] [&::-webkit-scrollbar]:h-2.5 [&::-webkit-scrollbar-thumb]:rounded-full [&::-webkit-scrollbar-thumb]:border-3 [&::-webkit-scrollbar-thumb]:border-transparent [&::-webkit-scrollbar-thumb]:bg-foreground/18 [&::-webkit-scrollbar-thumb]:bg-clip-content [&::-webkit-scrollbar-thumb:hover]:bg-foreground/28 [&::-webkit-scrollbar-track]:mx-1 [&::-webkit-scrollbar-track]:bg-transparent"
+          : "flex-wrap",
+      )}
+    >
+      {expandedImages
+        .map((image) => {
+          const upload = supportsAttachmentUploads ? uploadsByImageId[image.id] : undefined;
+          const snapShotAnimationPending =
+            image.source?.kind === "snap-shot" && pendingSnapShotIdSet.has(image.id);
+          const snapShotArrival =
+            image.source?.kind === "snap-shot" &&
+            shouldAnimateSnapShotArrival(image.source.capturedAt);
+          return (
+            <SnapShotAttachmentFrame
+              key={image.id}
+              data-chat-composer-expanded-image="true"
+              aria-hidden={snapShotAnimationPending || undefined}
+              inert={snapShotAnimationPending || undefined}
+              arrival={snapShotArrival}
+              animateArrival={snapShotAnimations && !snapShotAnimationPending && snapShotArrival}
+              animationId={snapShotAnimationPending ? image.id : undefined}
+              animationSource={snapShotAnimationPending ? image.source : undefined}
+              className={cn(
+                "group/attachment shrink-0 snap-start bg-background",
+                image.source?.kind === "snap-shot"
+                  ? SNAP_SHOT_ATTACHMENT_FRAME_CLASS
+                  : "relative h-16 w-16 overflow-hidden rounded-lg border border-border/80",
+                snapShotAnimationPending && "invisible",
+              )}
+            >
+              {image.previewUrl ? (
+                <button
+                  type="button"
+                  className="h-full w-full cursor-zoom-in"
+                  aria-label={`Preview ${image.name}`}
+                  onClick={() => {
+                    const preview = buildExpandedImagePreview(images, image.id);
+                    if (!preview) return;
+                    onExpandImage(preview);
+                  }}
+                >
+                  <ComposerImageThumbnail
+                    file={image.file}
+                    alt={image.name}
+                    className="h-full w-full object-cover"
+                    fallback={
+                      isHeicImageFile({ name: image.name, type: image.mimeType }) ? (
+                        <AttachmentImage
+                          name={image.name}
+                          mimeType={image.mimeType}
+                          src={image.previewUrl}
+                          alt={image.name}
+                          className="h-full w-full object-cover"
+                        />
+                      ) : (
+                        <span className="flex h-full items-center justify-center px-1 text-3xs text-secondary-label">
+                          {image.name}
+                        </span>
+                      )
+                    }
+                  />
+                </button>
+              ) : (
+                <div className="flex h-full w-full items-center justify-center px-1 text-center text-3xs text-secondary-label">
+                  {image.name}
+                </div>
+              )}
+              {image.source?.kind === "snap-shot" ? (
+                <SnapShotAttachmentDetails
+                  source={image.source}
+                  className={cn(
+                    upload?.status === "uploading" && "bottom-4",
+                    upload?.status === "failed" && "bottom-8",
+                  )}
+                />
+              ) : null}
+              {nonPersistedImageIdSet.has(image.id) && (
+                <Tooltip>
+                  <TooltipTrigger
+                    render={
+                      <span
+                        role="img"
+                        aria-label="Draft attachment may not persist"
+                        className="absolute left-1 top-1 inline-flex items-center justify-center rounded bg-background/85 p-0.5 text-warning-foreground"
+                      >
+                        <CircleAlertIcon className="size-3" />
+                      </span>
+                    }
+                  />
+                  <TooltipPopup side="top">
+                    Draft attachment could not be saved locally and may be lost on navigation.
+                  </TooltipPopup>
+                </Tooltip>
+              )}
+              {upload?.status === "uploading" && (
+                <span className="pointer-events-none absolute inset-x-0 bottom-0 bg-background/85 px-1 text-center text-3xs text-foreground">
+                  {formatAttachmentUploadProgress(upload.progress)}
+                </span>
+              )}
+              {upload?.status === "failed" && (
+                <Tooltip>
+                  <TooltipTrigger
+                    render={
+                      <Button
+                        variant="overlay"
+                        size="icon-xs"
+                        className="absolute bottom-1 left-1"
+                        onClick={() =>
+                          retryAttachmentUpload({
+                            environmentId,
+                            image,
+                            draftTarget: attachmentDraftTarget,
+                          })
+                        }
+                        disabled={!canOperateThread}
+                        aria-label={`Retry upload for ${image.name}`}
+                      />
+                    }
+                  >
+                    <RefreshIcon />
+                  </TooltipTrigger>
+                  <TooltipPopup side="top">{upload.reason}</TooltipPopup>
+                </Tooltip>
+              )}
+              <span
+                className={cn(
+                  "absolute right-1 top-1 flex",
+                  image.source?.kind === "snap-shot" &&
+                    "opacity-0 transition-opacity pointer-coarse:opacity-100 group-hover/attachment:opacity-100 group-focus-within/attachment:opacity-100",
+                )}
+              >
+                <Button
+                  variant="media-close"
+                  size="icon-xs"
+                  onClick={() => onRemoveImage(image.id)}
+                  aria-label={`Remove ${image.name}`}
+                >
+                  <XIcon />
+                </Button>
+              </span>
+            </SnapShotAttachmentFrame>
+          );
+        })
+        .concat(
+          uncommittedSnapShotIds.map((captureId) => (
+            <SnapShotAttachmentFrame
+              key={captureId}
+              aria-hidden="true"
+              animationId={captureId}
+              animationSource={
+                pendingSnapShotAnimations.find((capture) => capture.id === captureId)?.source
+              }
+              className={cn(
+                SNAP_SHOT_ATTACHMENT_FRAME_CLASS,
+                "invisible shrink-0 snap-start bg-background",
+              )}
+            />
+          )),
+        )}
+      {videos.map((file) => {
+        const fileCanUpload =
+          supportsAttachmentUploads &&
+          maxFileAttachmentBytes !== null &&
+          file.sizeBytes <= maxFileAttachmentBytes;
+        const upload = fileCanUpload ? uploadsByImageId[file.id] : undefined;
+        return (
+          <div
+            key={file.id}
+            className="relative h-16 w-16 shrink-0 snap-start overflow-hidden rounded-lg border border-border/80 bg-black"
+          >
+            <button
+              type="button"
+              className="flex h-full w-full cursor-zoom-in flex-col items-center justify-center gap-1 px-1 text-white"
+              aria-label={`Play ${file.name}`}
+              onClick={() => {
+                if (file.file !== null) {
+                  const preview = buildExpandedImagePreview([file], file.id);
+                  if (preview) onExpandImage(preview);
+                  return;
+                }
+                if (!file.uploadedAttachmentId) return;
+                onFileOpen({ ...file, id: file.uploadedAttachmentId });
+              }}
+            >
+              {file.file && (
+                <>
+                  <ComposerVideoThumbnail file={file.file} />
+                  <span className="pointer-events-none absolute inset-0 bg-gradient-to-t from-black/70 via-black/10 to-black/10" />
+                </>
+              )}
+              <PlayIcon className="relative z-10 size-4 fill-current drop-shadow-md" />
+            </button>
+            {upload?.status === "uploading" && (
+              <span className="pointer-events-none absolute inset-x-0 bottom-0 bg-background/85 px-1 text-center text-3xs text-foreground">
+                {formatAttachmentUploadProgress(upload.progress)}
+              </span>
+            )}
+            {upload?.status === "failed" && (
+              <Tooltip>
+                <TooltipTrigger
+                  render={
+                    <Button
+                      variant="overlay"
+                      size="icon-xs"
+                      className="absolute bottom-1 left-1"
+                      onClick={() =>
+                        retryAttachmentUpload({
+                          environmentId,
+                          image: file,
+                          draftTarget: attachmentDraftTarget,
+                        })
+                      }
+                      disabled={!canOperateThread}
+                      aria-label={`Retry upload for ${file.name}`}
+                    />
+                  }
+                >
+                  <RefreshIcon />
+                </TooltipTrigger>
+                <TooltipPopup side="top">{upload.reason}</TooltipPopup>
+              </Tooltip>
+            )}
+            <Button
+              variant="media-close"
+              size="icon-xs"
+              className="absolute right-1 top-1"
+              onClick={() => onRemoveFile(file.id)}
+              aria-label={`Remove ${file.name}`}
+            >
+              <XIcon />
+            </Button>
+          </div>
+        );
+      })}
+    </div>
+  );
+}
+
+/** Non-media file attachments, listed under the media row. */
+export function ComposerAttachmentFileList({
+  files,
+  resolvedTheme,
+  supportsAttachmentUploads,
+  maxFileAttachmentBytes,
+  fileStagingLimit,
+  environmentId,
+  attachmentDraftTarget,
+  canOperateThread,
+  onPreviewFile,
+  onRemoveFile,
+}: {
+  readonly files: ReadonlyArray<ComposerFileAttachment>;
+  readonly resolvedTheme: "light" | "dark";
+  readonly supportsAttachmentUploads: boolean;
+  readonly maxFileAttachmentBytes: number | null;
+  readonly fileStagingLimit: number | null;
+  readonly environmentId: EnvironmentId;
+  readonly attachmentDraftTarget: ScopedThreadRef | DraftId;
+  readonly canOperateThread: boolean;
+  readonly onPreviewFile: (fileId: string) => void;
+  readonly onRemoveFile: (fileId: string) => void;
+}) {
+  const uploadsByImageId = useAttachmentUploadStore((state) => state.uploadsByImageId);
+  return (
+    <div className="mb-3 flex flex-col gap-1">
+      {files.map((file) => {
+        const fileCanUpload =
+          supportsAttachmentUploads &&
+          maxFileAttachmentBytes !== null &&
+          file.sizeBytes <= maxFileAttachmentBytes;
+        const upload = fileCanUpload ? uploadsByImageId[file.id] : undefined;
+        const needsReattach = composerFileNeedsReattach(file);
+        const canReattachFile = fileStagingLimit !== null && file.sizeBytes <= fileStagingLimit;
+        return (
+          <div
+            key={file.id}
+            className="flex min-w-0 items-center gap-2 py-1 text-sm text-foreground"
+          >
+            <PierreEntryIcon pathValue={file.name} kind="file" theme={resolvedTheme} />
+            <button
+              type="button"
+              disabled={needsReattach}
+              className="min-w-0 flex-1 truncate text-left hover:underline focus-visible:outline-2"
+              onClick={() => onPreviewFile(file.id)}
+            >
+              {file.name}
+            </button>
+            <span className="shrink-0 text-xs text-secondary-label">
+              {needsReattach
+                ? canReattachFile
+                  ? "Attach again"
+                  : "Remove to send"
+                : upload?.status === "uploading"
+                  ? formatAttachmentUploadProgress(upload.progress)
+                  : formatAttachmentSize(file.sizeBytes)}
+            </span>
+            {!needsReattach && upload?.status === "failed" ? (
+              <Tooltip>
+                <TooltipTrigger
+                  render={
+                    <Button
+                      variant="ghost"
+                      size="icon-xs"
+                      onClick={() =>
+                        retryAttachmentUpload({
+                          environmentId,
+                          image: file,
+                          draftTarget: attachmentDraftTarget,
+                        })
+                      }
+                      disabled={!canOperateThread}
+                      aria-label={`Retry upload for ${file.name}`}
+                    />
+                  }
+                >
+                  <RefreshIcon />
+                </TooltipTrigger>
+                <TooltipPopup side="top">{upload.reason}</TooltipPopup>
+              </Tooltip>
+            ) : null}
+            <Button
+              variant="ghost"
+              size="icon-xs"
+              onClick={() => onRemoveFile(file.id)}
+              aria-label={`Remove ${file.name}`}
+            >
+              <XIcon />
+            </Button>
+          </div>
+        );
+      })}
+    </div>
+  );
+}
+
+export function ComposerFilePreviewDialog({
+  file,
+  onRemove,
+  onPreviewFileIdChange,
+}: {
+  readonly file: ComposerFileAttachment;
+  readonly onRemove: (fileId: string) => void;
+  readonly onPreviewFileIdChange: (fileId: string | null) => void;
+}) {
+  const onClose = () => onPreviewFileIdChange(null);
+  return (
+    <Dialog
+      open
+      onOpenChange={(open) => {
+        if (!open) onClose();
+      }}
+    >
+      <DialogPopup
+        {...composerFloatingLayerProps}
+        className="h-[min(85vh,52rem)] max-w-4xl overflow-hidden"
+        showCloseButton={false}
+      >
+        <DialogTitle className="sr-only">{file.name}</DialogTitle>
+        <AttachmentFilePreview
+          key={file.id}
+          name={file.name}
+          mimeType={file.mimeType}
+          sizeBytes={file.sizeBytes}
+          file={file.file}
+          origin="Draft"
+          {...(file.uploadedAttachmentId && file.uploadEnvironmentId
+            ? {
+                asset: {
+                  environmentId: file.uploadEnvironmentId,
+                  attachmentId: file.uploadedAttachmentId,
+                },
+              }
+            : {})}
+          onRemove={() => {
+            onRemove(file.id);
+            onClose();
+          }}
+          onClose={onClose}
+        />
+      </DialogPopup>
+    </Dialog>
+  );
+}
```
