fix: match text and image attachment heights in conversation timeline (#23593)

## Problem

Text attachments (`InlineTextAttachmentButton`) and image thumbnails
(`ImageThumbnail`) rendered at different heights when displayed side by
side in user messages. Text cards had no explicit height
(content-driven), while images used `h-16` (64px).

## Changes

**`ConversationTimeline.tsx`**
- Added `h-16` to `InlineTextAttachmentButton` to match `ImageThumbnail`
- Added `isPlaceholder` prop: when the content hasn't been fetched yet
(file_id path), renders "Pasted text" in sans-serif `text-sm` with
`items-center` alignment instead of monospace `text-xs`
- Once real content loads, it still renders in `font-mono text-xs` with
`formatTextAttachmentPreview()`

**`ConversationTimeline.stories.tsx`**
- Added `UserMessageWithMixedAttachments` story showing a text
attachment and image side by side as a visual regression guard
This commit is contained in:
Kyle Carberry
2026-03-25 14:37:55 +00:00
committed by GitHub
parent 7eca33c69b
commit fdc9b3a7e4
2 changed files with 48 additions and 5 deletions
@@ -299,6 +299,42 @@ export const UserMessageWithTextAttachmentOnly: Story = {
},
};
/** Visual regression: text and image attachments render at the same height. */
export const UserMessageWithMixedAttachments: Story = {
args: {
...defaultArgs,
parsedMessages: parseMessagesWithMergedTools([
{
...baseMessage,
id: 1,
role: "user",
content: [
{ type: "text", text: "Here is a screenshot and some context" },
{
type: "file",
media_type: "image/png",
data: TEST_PNG_B64,
},
{
type: "file",
file_id: "storybook-test-text",
media_type: "text/plain",
},
],
},
]),
},
play: async ({ canvasElement }) => {
const canvas = within(canvasElement);
const images = canvas.getAllByRole("img", { name: "Attached image" });
expect(images).toHaveLength(1);
const textButtons = await canvas.findAllByRole("button", {
name: "View text attachment",
});
expect(textButtons).toHaveLength(1);
},
};
/** Text-only messages must not produce spurious image thumbnails. */
export const UserMessageTextOnly: Story = {
args: {
@@ -123,20 +123,26 @@ const SmoothedResponse: FC<{
const InlineTextAttachmentButton: FC<{
content: string;
onPreview?: (content: string) => void;
}> = ({ content, onPreview }) => {
isPlaceholder?: boolean;
}> = ({ content, onPreview, isPlaceholder }) => {
return (
<button
type="button"
aria-label="View text attachment"
className="inline-flex max-w-sm items-start gap-2 rounded-md border-0 bg-surface-tertiary px-3 py-2 text-left transition-colors hover:bg-surface-quaternary focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-content-link"
className="inline-flex h-16 max-w-sm items-center gap-2 rounded-md border-0 bg-surface-tertiary px-3 py-2 text-left transition-colors hover:bg-surface-quaternary focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-content-link"
onClick={(e) => {
e.stopPropagation();
onPreview?.(content);
}}
>
<FileTextIcon className="mt-0.5 size-icon-sm shrink-0 text-content-secondary" />
<span className="line-clamp-2 min-w-0 font-mono text-xs text-content-secondary">
{formatTextAttachmentPreview(content)}
<FileTextIcon className="size-icon-sm shrink-0 text-content-secondary" />
<span
className={cn(
"line-clamp-2 min-w-0 text-content-secondary",
isPlaceholder ? "text-sm" : "font-mono text-xs",
)}
>
{isPlaceholder ? content : formatTextAttachmentPreview(content)}
</span>
</button>
);
@@ -156,6 +162,7 @@ const TextAttachmentButton: FC<{
return (
<InlineTextAttachmentButton
content={content ?? "Pasted text"}
isPlaceholder={content === null}
onPreview={async () => {
if (content !== null) {
onPreview?.(content);