Skip to content

Commit f9eb423

Browse files
committed
fix(web): stop treating TypeScript sources as video attachments
Review follow-ups on the #8688 port. videoMimeType accepted any video/* MIME, and hosts map a .ts file to video/mp2t. Attaching a TypeScript source turned it into an unnamed black play tile, dropped its download row in the transcript, and ended at "This video format cannot be played here". The extension now decides first, and a bare MIME is only trusted when it names a container Pylon actually plays. Also: re-wrapping a staged File copied the whole blob whenever the browser reported no type at all, purely to stamp application/octet-stream that mimeType already carries; that copy is now limited to real corrections. And composer video tiles show the filename, which the transcript tile already did — without it two videos are indistinguishable black squares.
1 parent 9964755 commit f9eb423

3 files changed

Lines changed: 53 additions & 7 deletions

File tree

apps/web/src/components/chat/ChatComposer.tsx

Lines changed: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3722,8 +3722,12 @@ export const ChatComposer = memo(function ChatComposer(props: ChatComposerProps)
37223722
error = fileAttachmentTooLargeMessage(file.name, fileStagingLimit);
37233723
continue;
37243724
}
3725+
// Re-wrapping copies the whole blob, so only do it when the resolved
3726+
// type is a real correction. A browser that reported no type at all
3727+
// gains nothing from being stamped octet-stream: `mimeType` below
3728+
// already carries the resolved value.
37253729
const attachmentFile =
3726-
file.type === fileMimeType
3730+
file.type === fileMimeType || fileMimeType === "application/octet-stream"
37273731
? file
37283732
: new File([file], file.name, { type: fileMimeType, lastModified: file.lastModified });
37293733
acceptedFiles.push({
@@ -4602,7 +4606,14 @@ export const ChatComposer = memo(function ChatComposer(props: ChatComposerProps)
46024606
{isOpening ? (
46034607
<span className="relative z-10 text-[10px]">Loading…</span>
46044608
) : (
4605-
<PlayIcon className="relative z-10 size-4 fill-current drop-shadow-md" />
4609+
<>
4610+
<PlayIcon className="relative z-10 size-4 fill-current drop-shadow-md" />
4611+
{/* Two videos are otherwise indistinguishable
4612+
black tiles when no thumbnail decodes. */}
4613+
<span className="pointer-events-none relative z-10 w-full truncate text-center text-[9px] leading-tight drop-shadow-md">
4614+
{file.name}
4615+
</span>
4616+
</>
46064617
)}
46074618
</button>
46084619
{upload?.status === "uploading" && (

apps/web/src/components/chat/composerAttachmentFiles.test.ts

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -322,3 +322,24 @@ describe("composer attachment files", () => {
322322
).toBe(true);
323323
});
324324
});
325+
326+
describe("videoMimeType", () => {
327+
it("resolves known video containers by extension", () => {
328+
expect(videoMimeType({ name: "clip.mkv", mimeType: "" })).toBe("video/x-matroska");
329+
expect(videoMimeType({ name: "clip.MP4", mimeType: "application/octet-stream" })).toBe(
330+
"video/mp4",
331+
);
332+
});
333+
334+
it("does not treat a TypeScript source as a video", () => {
335+
// Hosts map `.ts` to the MPEG transport stream type, so a bare `video/*`
336+
// check would render source files as blank play tiles.
337+
expect(videoMimeType({ name: "session-logic.ts", mimeType: "video/mp2t" })).toBeNull();
338+
expect(videoMimeType({ name: "clip.ts", mimeType: "video/mp2t" })).toBeNull();
339+
});
340+
341+
it("accepts a playable container when the name carries no extension", () => {
342+
expect(videoMimeType({ name: "recording", mimeType: "video/webm" })).toBe("video/webm");
343+
expect(videoMimeType({ name: "recording", mimeType: "video/mp2t" })).toBeNull();
344+
});
345+
});

apps/web/src/types.ts

Lines changed: 19 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -69,15 +69,29 @@ const VIDEO_MIME_TYPE_BY_EXTENSION: Readonly<Record<string, string>> = {
6969
webm: "video/webm",
7070
};
7171

72+
const PLAYABLE_VIDEO_MIME_TYPES: ReadonlySet<string> = new Set(
73+
Object.values(VIDEO_MIME_TYPE_BY_EXTENSION),
74+
);
75+
76+
/**
77+
* The container this attachment should be presented as, or null when it is not
78+
* a video Pylon offers to play.
79+
*
80+
* The extension decides first. Trusting a bare `video/*` prefix misreads files
81+
* the host maps to a transport stream — a TypeScript `.ts` source is reported as
82+
* `video/mp2t` — which would turn source files into blank play tiles.
83+
*/
7284
export function videoMimeType(
7385
attachment: Pick<ChatFileAttachment, "name" | "mimeType">,
7486
): string | null {
75-
const mimeType = attachment.mimeType.split(";", 1)[0]?.trim().toLowerCase() ?? "";
76-
if (mimeType.startsWith("video/")) return mimeType;
7787
const dotIndex = attachment.name.lastIndexOf(".");
78-
return dotIndex < 0
79-
? null
80-
: (VIDEO_MIME_TYPE_BY_EXTENSION[attachment.name.slice(dotIndex + 1).toLowerCase()] ?? null);
88+
const byExtension =
89+
dotIndex < 0
90+
? null
91+
: (VIDEO_MIME_TYPE_BY_EXTENSION[attachment.name.slice(dotIndex + 1).toLowerCase()] ?? null);
92+
if (byExtension !== null) return byExtension;
93+
const mimeType = attachment.mimeType.split(";", 1)[0]?.trim().toLowerCase() ?? "";
94+
return PLAYABLE_VIDEO_MIME_TYPES.has(mimeType) ? mimeType : null;
8195
}
8296

8397
export function isVideoAttachment(attachment: ChatFileAttachment): boolean {

0 commit comments

Comments
 (0)