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

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

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

Visibility: public

[fix\(web\): size the chat image slot from server-reported dimensions](https://git.cafe/pingdotgg/t3code/pull/10200.md)

Version: 3

Head commit: d18793ccd7ee493069b937119ea852088ba12cf3

Base commit: cced7aadcbc12ff44f5dc2dd5613e72fb4e34b41

Comparison base: cced7aadcbc12ff44f5dc2dd5613e72fb4e34b41

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

## apps/web/src/components/ChatMarkdown.tsx

Status: modified; +22; −2

```diff
@@ -1235,9 +1235,15 @@
   return `![${escapedAlt}](${src}${titleSuffix})`;
 }
 
+/**
+ * `maxHeightRem` folds a height cap into the width bound: `max-height` alone
+ * would not feed back through `aspect-ratio` once `width` is definite, so a
+ * tall image would keep a box wider than the picture it draws.
+ */
 function authoredImageSizeStyle(
   width: string | number | undefined,
   height: string | number | undefined,
+  maxHeightRem = 30,
 ): CSSProperties | undefined {
   const parsedWidth = Number(width);
   const parsedHeight = Number(height);
@@ -1248,7 +1254,7 @@
       width: parsedWidth,
       height: "auto",
       aspectRatio: `${parsedWidth} / ${parsedHeight}`,
-      maxWidth: `min(100%, 30rem, ${(30 * parsedWidth) / parsedHeight}rem)`,
+      maxWidth: `min(100%, 30rem, ${(maxHeightRem * parsedWidth) / parsedHeight}rem)`,
     };
   }
   if (hasWidth) return { maxWidth: `min(100%, 30rem, ${parsedWidth}px)` };
@@ -1522,6 +1528,8 @@
   readonly srcFragment?: string;
   /** Reserve a slot while loading; off for images that share a line with text. */
   readonly standalone?: boolean | undefined;
+  /** Caps the box height in rem while keeping the image's ratio; 30 by default. */
+  readonly maxHeightRem?: number | undefined;
   readonly style?: CSSProperties | undefined;
   readonly workspaceRoot?: string | undefined;
   readonly onImageExpand?: ((preview: ExpandedImagePreview) => void) | undefined;
@@ -1538,6 +1546,18 @@
   const reference = path ? mediaFileReference(path, props.workspaceRoot) : undefined;
   const relativePath = reference?.relativePath;
   const src = assetUrl._tag === "Success" ? assetUrl.url + (props.srcFragment ?? "") : null;
+  // The server reads the pixel size from the file header, so the slot can be
+  // the image's final box instead of a 16:9 guess. An authored size wins; a
+  // caller's height cap shrinks the box while keeping the ratio.
+  const knownSize = assetUrl._tag === "Success" ? assetUrl.imageDimensions : undefined;
+  const maxHeightRem = props.maxHeightRem ?? 30;
+  const style =
+    props.style ??
+    (knownSize
+      ? authoredImageSizeStyle(knownSize.width, knownSize.height, maxHeightRem)
+      : maxHeightRem !== 30
+        ? { maxHeight: `${maxHeightRem}rem` }
+        : undefined);
   const actionsSource: MediaActionSource = {
     kind: props.kind ?? "image",
     name: props.alt || (props.kind ?? "image"),
@@ -1581,7 +1601,7 @@
       copyMarkdown={props.copyMarkdown}
       standalone={props.standalone ?? true}
       className={CHAT_MARKDOWN_WORKSPACE_IMAGE_CLASS_NAME}
+      style={style}
-      style={props.style}
       actionsSource={actionsSource}
       onImageExpand={props.onImageExpand}
     />
```

## apps/web/src/components/ChatMarkdown.workspace-images.test.tsx

Status: modified; +44; −2

```diff
@@ -5,6 +5,7 @@
 const testState = vi.hoisted(() => ({
   resources: [] as Array<unknown>,
   assetState: "success" as "success" | "loading" | "failure",
+  imageDimensions: undefined as { width: number; height: number } | undefined,
 }));
 
 vi.mock("@effect/atom-react", () => ({ useAtomValue: () => null }));
@@ -14,7 +15,11 @@
     testState.resources.push(resource);
     if (testState.assetState === "loading") return { _tag: "Loading" };
     if (testState.assetState === "failure") return { _tag: "Failure" };
+    return {
-    return { _tag: "Success", url: "https://signed.test/workspace-image.svg" };
+      _tag: "Success",
+      url: "https://signed.test/workspace-image.svg",
+      ...(testState.imageDimensions ? { imageDimensions: testState.imageDimensions } : {}),
+    };
   },
 }));
 vi.mock("../hooks/useTheme", () => ({ useTheme: () => ({ resolvedTheme: "dark" }) }));
@@ -42,7 +47,7 @@
   useOpenChangeRequestLink: () => vi.fn(),
 }));
 
+import ChatMarkdown, { ChatMarkdownAssetImage } from "./ChatMarkdown";
-import ChatMarkdown from "./ChatMarkdown";
 import { FileMarkdownPreview } from "./files/FileMarkdownPreview";
 
 const threadRef = {
@@ -92,6 +97,7 @@
   beforeEach(() => {
     testState.resources = [];
     testState.assetState = "success";
+    testState.imageDimensions = undefined;
   });
 
   it.each([
@@ -245,6 +251,42 @@
     expect(html).toContain('<span id="user-content-diagram"');
   });
 
+  it("sizes the slot from server-reported dimensions so a portrait image never grows", () => {
+    testState.imageDimensions = { width: 720, height: 1400 };
+
+    const style = firstInlineStyle(render("![shot](.t3/workspace-image.svg)"));
+
+    expect(style).toMatchObject({ width: "720px", "aspect-ratio": "720 / 1400" });
+  });
+
+  it("folds a caller's height cap into the width bound so the ratio holds", () => {
+    testState.imageDimensions = { width: 720, height: 1400 };
+
+    const html = renderToStaticMarkup(
+      <ChatMarkdownAssetImage
+        environmentId={threadRef.environmentId}
+        resource={{ _tag: "media-file", threadId: threadRef.threadId, path: "/shot.png" }}
+        alt="shot"
+        maxHeightRem={16}
+      />,
+    );
+
+    expect(firstInlineStyle(html)).toMatchObject({
+      "aspect-ratio": "720 / 1400",
+      "max-width": `min(100%, 30rem, ${(16 * 720) / 1400}rem)`,
+    });
+  });
+
+  it("lets an authored size override server-reported dimensions", () => {
+    testState.imageDimensions = { width: 720, height: 1400 };
+
+    const style = firstInlineStyle(
+      render('<img src=".t3/workspace-image.svg" alt="sized" width="96" height="128">'),
+    );
+
+    expect(style).toMatchObject({ width: "96px", "aspect-ratio": "96 / 128" });
+  });
+
   it("reserves a slot for an image that is alone in a list item", () => {
     expect(render("- ![shot](.t3/workspace-image.svg)")).toContain("aspect-video");
   });
```

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

Status: modified; +1; −1

```diff
@@ -3342,7 +3342,7 @@
             alt={viewedImage.alt}
             srcFragment={viewedImage.srcFragment}
             workspaceRoot={workspaceRoot}
-            style={{ maxHeight: "16rem" }}
+            maxHeightRem={16}
             onImageExpand={onImageExpand}
           />
         </div>
```
