diff --git a/apps/web/src/components/ChatView.tsx b/apps/web/src/components/ChatView.tsx
index d66d2487ce..d5bece232a 100644
--- a/apps/web/src/components/ChatView.tsx
+++ b/apps/web/src/components/ChatView.tsx
@@ -147,7 +147,10 @@ import { ExpandedImageDialog } from "./chat/ExpandedImageDialog";
import { PullRequestThreadDialog } from "./PullRequestThreadDialog";
import { MessagesTimeline } from "./chat/MessagesTimeline";
import { ChatHeader } from "./chat/ChatHeader";
-import { type ExpandedImagePreview } from "./chat/ExpandedImagePreview";
+import {
+ getExpandedImagePreviewIdentityKey,
+ type ExpandedImagePreview,
+} from "./chat/ExpandedImagePreview";
import { NoActiveThreadState } from "./NoActiveThreadState";
import { resolveEffectiveEnvMode, resolveEnvironmentOptionLabel } from "./BranchToolbar.logic";
import { ProviderStatusBanner } from "./chat/ProviderStatusBanner";
@@ -3773,7 +3776,11 @@ export default function ChatView(props: ChatViewProps) {
) : null}
{expandedImage && (
-
+
)}
);
diff --git a/apps/web/src/components/chat/ExpandedImageDialog.tsx b/apps/web/src/components/chat/ExpandedImageDialog.tsx
index 10031b48cd..8437f3c9c5 100644
--- a/apps/web/src/components/chat/ExpandedImageDialog.tsx
+++ b/apps/web/src/components/chat/ExpandedImageDialog.tsx
@@ -1,4 +1,4 @@
-import { memo, useCallback, useEffect, useState } from "react";
+import { memo, useCallback, useEffect, useEffectEvent, useState } from "react";
import { ChevronLeftIcon, ChevronRightIcon, XIcon } from "lucide-react";
import { Button } from "../ui/button";
import type { ExpandedImagePreview } from "./ExpandedImagePreview";
@@ -8,52 +8,65 @@ interface ExpandedImageDialogProps {
onClose: () => void;
}
-export const ExpandedImageDialog = memo(function ExpandedImageDialog({
- preview: initialPreview,
- onClose,
-}: ExpandedImageDialogProps) {
- const [preview, setPreview] = useState(initialPreview);
-
- // Sync when the parent hands us a new preview reference.
- useEffect(() => {
- setPreview(initialPreview);
- }, [initialPreview]);
-
- const navigateImage = useCallback((direction: -1 | 1) => {
- setPreview((existing) => {
- if (existing.images.length <= 1) return existing;
- const nextIndex =
- (existing.index + direction + existing.images.length) % existing.images.length;
- if (nextIndex === existing.index) return existing;
- return { ...existing, index: nextIndex };
- });
- }, []);
+function useExpandedImageDialogKeyboardShortcuts(input: {
+ readonly imageCount: number;
+ readonly onClose: () => void;
+ readonly onNavigate: (direction: -1 | 1) => void;
+}) {
+ const close = useEffectEvent(input.onClose);
+ const navigate = useEffectEvent(input.onNavigate);
useEffect(() => {
const onKeyDown = (event: globalThis.KeyboardEvent) => {
if (event.key === "Escape") {
event.preventDefault();
event.stopPropagation();
- onClose();
+ close();
return;
}
- if (preview.images.length <= 1) return;
+ if (input.imageCount <= 1) return;
if (event.key === "ArrowLeft") {
event.preventDefault();
event.stopPropagation();
- navigateImage(-1);
+ navigate(-1);
return;
}
if (event.key !== "ArrowRight") return;
event.preventDefault();
event.stopPropagation();
- navigateImage(1);
+ navigate(1);
};
window.addEventListener("keydown", onKeyDown);
return () => window.removeEventListener("keydown", onKeyDown);
- }, [navigateImage, onClose, preview.images.length]);
+ }, [close, input.imageCount, navigate]);
+}
+
+export const ExpandedImageDialog = memo(function ExpandedImageDialog({
+ preview,
+ onClose,
+}: ExpandedImageDialogProps) {
+ const [navigationOffset, setNavigationOffset] = useState(0);
+ const imageCount = preview.images.length;
+ const activeIndex =
+ imageCount > 0 ? (preview.index + navigationOffset + imageCount) % imageCount : preview.index;
+
+ const navigateImage = useCallback(
+ (direction: -1 | 1) => {
+ setNavigationOffset((offset) => {
+ if (imageCount <= 1) return offset;
+ return offset + direction;
+ });
+ },
+ [imageCount],
+ );
+
+ useExpandedImageDialogKeyboardShortcuts({
+ imageCount,
+ onClose,
+ onNavigate: navigateImage,
+ });
- const item = preview.images[preview.index];
+ const item = preview.images[activeIndex];
if (!item) return null;
return (
@@ -69,7 +82,7 @@ export const ExpandedImageDialog = memo(function ExpandedImageDialog({
aria-label="Close image preview"
onClick={onClose}
/>
- {preview.images.length > 1 && (
+ {imageCount > 1 && (
{item.name}
- {preview.images.length > 1 ? ` (${preview.index + 1}/${preview.images.length})` : ""}
+ {imageCount > 1 ? ` (${activeIndex + 1}/${imageCount})` : ""}
- {preview.images.length > 1 && (
+ {imageCount > 1 && (