setExpandedSrc(null)}
+ />
+ ) : null}
>
) : failed ? (
diff --git a/apps/web/src/components/chat/MessagesTimeline.tsx b/apps/web/src/components/chat/MessagesTimeline.tsx
index 8d2b445a22cea..6f5865d06171b 100644
--- a/apps/web/src/components/chat/MessagesTimeline.tsx
+++ b/apps/web/src/components/chat/MessagesTimeline.tsx
@@ -2956,6 +2956,11 @@ function ProposedPlanTimelineRow({
function HtmlRenderTimelineRow({ row }: { row: Extract }) {
const ctx = use(TimelineRowCtx);
+ const { onAppFullscreenChange } = ctx;
+ const onFullscreenChange = useCallback(
+ (fullscreen: boolean) => onAppFullscreenChange(row.id, fullscreen),
+ [onAppFullscreenChange, row.id],
+ );
return (
@@ -2965,6 +2970,7 @@ function HtmlRenderTimelineRow({ row }: { row: Extract
);
diff --git a/apps/web/src/components/ui/dialog.tsx b/apps/web/src/components/ui/dialog.tsx
index 463cedd79fba1..071b17924ee65 100644
--- a/apps/web/src/components/ui/dialog.tsx
+++ b/apps/web/src/components/ui/dialog.tsx
@@ -66,17 +66,21 @@ function DialogPopup({
}: DialogPrimitive.Popup.Props & {
showCloseButton?: boolean;
bottomStickOnMobile?: boolean;
- variant?: "default" | "media";
+ variant?: "default" | "media" | "fullscreen";
}) {
return (
{/* Media opens from inside other overlays (a composer chip, a popover), so it sits above them. */}
-
+
Date: Tue, 13 Oct 2026 18:51:35 +0200
Subject: [PATCH 2/3] fix(web): expand visual replies like image previews
---
.../components/chat/HtmlRenderDialog.test.tsx | 5 ++
.../src/components/chat/HtmlRenderDialog.tsx | 55 +++++++++++++------
apps/web/src/components/ui/dialog.tsx | 10 +---
3 files changed, 45 insertions(+), 25 deletions(-)
diff --git a/apps/web/src/components/chat/HtmlRenderDialog.test.tsx b/apps/web/src/components/chat/HtmlRenderDialog.test.tsx
index a423bb3137d87..59cf57de2de57 100644
--- a/apps/web/src/components/chat/HtmlRenderDialog.test.tsx
+++ b/apps/web/src/components/chat/HtmlRenderDialog.test.tsx
@@ -113,6 +113,10 @@ it("opens with a fresh URL while keeping the inline page loaded", async () => {
await renderInline();
const inlineFrame = document.querySelector("iframe")!;
const originalSrc = inlineFrame.src;
+ const expandButton = document.querySelector(
+ '[aria-label="Open full screen"]',
+ )!;
+ expandButton.focus();
refresh.mockResolvedValue("https://environment.test/renewed.html");
await click("Open full screen");
expect(refresh).toHaveBeenCalledOnce();
@@ -123,6 +127,7 @@ it("opens with a fresh URL while keeping the inline page loaded", async () => {
await click("Close");
expect(document.querySelector('[role="dialog"]')).toBeNull();
expect(document.querySelector("iframe")).toBe(inlineFrame);
+ expect(document.activeElement).toBe(expandButton);
});
it.each([null, new Error("Disconnected")])(
diff --git a/apps/web/src/components/chat/HtmlRenderDialog.tsx b/apps/web/src/components/chat/HtmlRenderDialog.tsx
index 47be3d4bd2f6b..92274667eb268 100644
--- a/apps/web/src/components/chat/HtmlRenderDialog.tsx
+++ b/apps/web/src/components/chat/HtmlRenderDialog.tsx
@@ -1,9 +1,9 @@
-import { MinusIcon, PlusIcon } from "lucide-react";
-import { useState } from "react";
+import { MinusIcon, PlusIcon, XIcon } from "lucide-react";
+import { useRef, useState } from "react";
import { HtmlRenderDocument } from "../files/BrowserDocumentFrame";
import { Button } from "../ui/button";
-import { Dialog, DialogHeader, DialogPopup, DialogTitle } from "../ui/dialog";
+import { Dialog, DialogPopup, DialogTitle } from "../ui/dialog";
export function HtmlRenderDialog(props: {
readonly src: string;
@@ -11,31 +11,57 @@ export function HtmlRenderDialog(props: {
readonly onClose: () => void;
}) {
const [zoom, setZoom] = useState(1);
+ const closeButtonRef = useRef(null);
return (