Skip to content

Commit 847e1b3

Browse files
committed
Keep the tile's icon whole when the truncation warning is on it
The warning arrived as a third line of text in a tile whose height is fixed, so it did not fit: at this repository's 15px root the two in-flow children need 63.75px inside a 58px box, and the icon is the only one that can give. Measured in Chromium, it rendered 18.75x13 on exactly the tiles carrying the warning. Pinning the icon alone only moves the overflow onto the border, so the warning folds onto the size line instead and the tile keeps two lines. `truncate` is what guarantees that: `formatBytes` is unbounded, and a large enough file would otherwise wrap the line and put the third one back. Also drops the `attachment.type` test from the composer's derivation. The filter above it already excludes images using `stagedModality`, which prefers the mime the server sniffed; `attachment.type` is the browser's claim, fixed at pick time and never revised. Testing both could only subtract, and the file it subtracted is the one that matters: a file the browser called a PNG and the server read as text goes down the extraction path and is exactly what gets cut. Covered by a composer-level test, which the warning did not have.
1 parent d28a666 commit 847e1b3

4 files changed

Lines changed: 80 additions & 13 deletions

File tree

app/src/components/channels/composer/attachment-strip.tsx

Lines changed: 29 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -126,26 +126,45 @@ export function AttachmentStrip({
126126
className="flex h-20 w-40 flex-col justify-between rounded-xl border border-border bg-muted/40 p-2"
127127
key={file.id}
128128
>
129-
<IconFile className="size-5 text-muted-foreground" />
129+
{/*
130+
* `shrink-0` because the tile's height is fixed and the text below it is not. A third
131+
* line, which is exactly what the truncation warning adds, takes the extra out of the
132+
* only child that can give: this icon. It renders visibly smaller on precisely the
133+
* tiles that carry the warning, which is the wrong moment to look broken.
134+
*/}
135+
<IconFile className="size-5 shrink-0 text-muted-foreground" />
130136
<div className="min-w-0">
131137
<p className="truncate font-medium text-xs" title={file.name}>
132138
{file.name}
133139
</p>
134140
{file.loading ? (
135141
<p className="text-muted-foreground text-xs">Uploading…</p>
136142
) : file.size === undefined ? null : (
137-
<p className="text-muted-foreground text-xs">
138-
{formatBytes(file.size)}
139-
</p>
140-
)}
141-
{file.mayTruncate === true ? (
143+
/*
144+
* ON THE SIZE LINE, NOT UNDER IT, AND THAT IS A LAYOUT CONSTRAINT RATHER THAN A
145+
* PREFERENCE. The tile's height is fixed so that a file and a thumbnail read as
146+
* one row. A third line does not fit: measured in Chromium at this repository's
147+
* 15px root, the two in-flow children need 63.75px inside a 58px box. Something
148+
* has to give, and the only choices are the icon shrinking to 13px or the text
149+
* spilling past the border. Folding the warning onto this line keeps two.
150+
*
151+
* `truncate` is the guarantee, not the wording: `formatBytes` is unbounded, so a
152+
* large enough file would wrap this line and put the third one back. The short
153+
* phrasing is what keeps it from ellipsizing at any realistic size; `title`
154+
* carries the sentence that actually explains it.
155+
*/
142156
<p
143-
className="text-muted-foreground text-xs"
144-
title="The model reads the first 120,000 characters of this file."
157+
className="truncate text-muted-foreground text-xs"
158+
title={
159+
file.mayTruncate === true
160+
? "The model reads the first 120,000 characters of this file."
161+
: undefined
162+
}
145163
>
146-
May be read truncated
164+
{formatBytes(file.size)}
165+
{file.mayTruncate === true ? " · may be cut" : null}
147166
</p>
148-
) : null}
167+
)}
149168
</div>
150169
<RemoveButton name={file.name} onRemove={() => onRemove(file.id)} />
151170
</Staged>

app/src/components/channels/composer/composer.tsx

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1013,10 +1013,16 @@ export function Composer({
10131013
* the extraction path: images are excluded by the filter above, and an
10141014
* unnamed pick whose type is still unknown gets no warning until the
10151015
* server has sniffed it (then the strip re-renders off the url source).
1016+
*
1017+
* THE FILTER IS THE ONLY IMAGE TEST, DELIBERATELY. `attachment.type` is
1018+
* the browser's claim, fixed at pick time and never revised; the filter
1019+
* runs `stagedModality`, which prefers what the server sniffed. Testing
1020+
* both would not narrow this to text, it would only subtract: a file the
1021+
* browser called a PNG and the server read as text lands in this strip,
1022+
* goes down the extraction path, and is exactly the one that gets cut.
10161023
*/
10171024
mayTruncate:
10181025
attachment.status !== "uploading" &&
1019-
attachment.type !== "image" &&
10201026
attachment.size !== undefined &&
10211027
mayBeTruncatedForModel(attachment.size),
10221028
})),

app/tests/composer-attachment-strip.test.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -130,7 +130,7 @@ test("a text file that fits shows no truncation warning", () => {
130130
<AttachmentStrip files={[notes]} images={[]} onRemove={() => {}} />,
131131
);
132132

133-
expect(queryByText("May be read truncated")).toBeNull();
133+
expect(queryByText(/may be cut/)).toBeNull();
134134
});
135135

136136
test("a text file over the extraction ceiling warns it may be read truncated", () => {
@@ -145,5 +145,5 @@ test("a text file over the extraction ceiling warns it may be read truncated", (
145145
/>,
146146
);
147147

148-
expect(getByText("May be read truncated")).toBeTruthy();
148+
expect(getByText(/· may be cut/)).toBeTruthy();
149149
});

app/tests/composer-attachments-ui.test.tsx

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -404,3 +404,45 @@ test("a file the browser called an image is drawn as a file once the server read
404404
expect(container.querySelector("img")).toBeNull();
405405
expect(container.textContent).toContain("notes.png");
406406
});
407+
408+
/**
409+
* THE WARNING IS DERIVED, AND THIS IS THE CASE THE DERIVATION CAN GET WRONG.
410+
*
411+
* A tile renders the warning it is handed; whether it is handed one is decided in the composer, off
412+
* the same `stagedModality` the file filter uses. Those have to be the same test. The file below is
413+
* the one that proves it: the browser calls it a PNG, the server reads it as text, so it lands in
414+
* the file strip and goes down the extraction path, and at 120,001 bytes the model sees roughly a
415+
* tenth of it. Deciding on the browser's claim instead would drop the warning on exactly this file.
416+
*/
417+
test("warns on a large file the browser called an image and the server read as text", async () => {
418+
serverSniffs("text/plain");
419+
const view = render(
420+
<Composer channelId="channel-1" compact onSubmit={() => {}} />,
421+
);
422+
const { container } = view;
423+
424+
drop(container.querySelector("form") as HTMLFormElement, [
425+
new File(["x".repeat(120_001)], "notes.png", { type: "image/png" }),
426+
]);
427+
428+
await uploaded(view, "notes.png");
429+
430+
expect(container.textContent).toContain("may be cut");
431+
});
432+
433+
/** The same file under the ceiling earns no warning, so the one above is the size and not the path. */
434+
test("does not warn on a small file the server read as text", async () => {
435+
serverSniffs("text/plain");
436+
const view = render(
437+
<Composer channelId="channel-1" compact onSubmit={() => {}} />,
438+
);
439+
const { container } = view;
440+
441+
drop(container.querySelector("form") as HTMLFormElement, [
442+
new File(["hello"], "notes.png", { type: "image/png" }),
443+
]);
444+
445+
await uploaded(view, "notes.png");
446+
447+
expect(container.textContent).not.toContain("may be cut");
448+
});

0 commit comments

Comments
 (0)