fix(web): make uploaded videos play in the chat#1343
Conversation
- copy TUI video placeholders into the shared cache instead of inlining the original source path - emit <video path="..."> tags so ReadMediaFile / the provider's VideoUploader owns upload behavior - apply the same cache materialization to server prompt video submissions, matching the TUI flow - update TUI unit tests and server e2e test to assert cache-path behavior
Render the server's <video path> tag as a real video and reconcile the echoed user message so the bubble no longer shows raw markup or a duplicate. Serve file downloads with byte-range support and fetch video bytes with the bearer credential into a blob URL, since browsers cannot authorize a <video> src on their own. Also let users click an uploaded image to open it in the preview panel.
🦋 Changeset detectedLatest commit: 03a0590 The changes in this PR will be included in the next version bump. This PR includes changesets to release 1 package
Not sure what this means? Click here to learn what changesets are. Click here if you're a maintainer who wants to add another changeset to this PR |
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 0cbd45e2d7
ℹ️ About Codex in GitHub
Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".
# Conflicts: # apps/kimi-code/src/tui/utils/image-placeholder.ts # packages/server/src/routes/prompts.ts # packages/server/test/prompt.e2e.test.ts
commit: |
openMediaPreview stored the raw getFileUrl as sourceUrl, and FilePreview renders it with a native <img> that sends no Authorization header, so the enlarge action 401'd for uploaded images. When the media carries a fileId, fetch the bytes through the authenticated API client and preview a blob URL instead, revoking it when the preview is replaced or closed.
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: f1f15b7535
ℹ️ About Codex in GitHub
Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".
AuthMedia fetches the file bytes asynchronously; when the component is reused with a new fileId before a prior fetch resolves (e.g. queued thumbnails keyed by index), the older response could still create a blob URL and show the previous file. Add a per-request sequence guard (and an unmount guard) so a stale response is discarded and its blob URL revoked instead of being applied.
|
@codex review |
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 4371b747b7
ℹ️ About Codex in GitHub
Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".
Treating any standalone <video path="..."> text as an uploaded daemon file and stripping the basename into getFileUrl is only valid for server cache files named after the file-store id (f_…). TUI/ReadMediaFile tags use arbitrary cache names like <uuid>-<label>, and older transcripts may point at paths like /tmp/foo.mp4; those produced a broken /files/<basename> request. Only extract a fileId when the basename matches the file-store id shape, otherwise leave the raw tag as text.
Closing an uploaded-image preview before getFileBlob() resolved left previewRequestSeq untouched, so the fetch callback still passed its seq check, created a blob URL, then skipped attaching it because previewFile was already null — leaking up to the file size until another preview opened. Bump previewRequestSeq on close so the in-flight callback bails before creating the blob URL.
|
@codex review |
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 03a0590493
ℹ️ About Codex in GitHub
Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".
| return; | ||
| } | ||
| try { | ||
| const blob = await getKimiWebApi().getFileBlob(props.fileId); |
There was a problem hiding this comment.
Avoid eager full downloads for mounted media
When a rendered chat turn or queued item has a fileId, this call runs immediately on component mount and downloads the entire image/video into a Blob before assigning src. That bypasses the native loading="lazy" and preload="metadata" behavior below, so opening a session with several historical large video uploads can start many full downloads and hold all of those blobs in memory even if the user never scrolls to or plays them. Defer the authenticated fetch until the media is visible/played, or use an authorized streaming URL.
Useful? React with 👍 / 👎.
| previewFile.value = base; | ||
| void getKimiWebApi().getFileBlob(media.fileId).then((blob) => { | ||
| if (seq !== previewRequestSeq) return; | ||
| mediaObjectUrl = URL.createObjectURL(blob); |
There was a problem hiding this comment.
Revoke preview blobs when leaving the file panel
If an uploaded image preview is opened and the user switches to another detail panel, useDetailPanel only changes detailTarget; it does not call closeFilePreview. This promise can still create mediaObjectUrl after the file panel is hidden, and an already-shown blob URL is likewise kept until another file preview or a session switch, leaking potentially large uploaded-image blobs. Check that the file panel is still active before creating the object URL and revoke/close when detailTarget leaves file.
Useful? React with 👍 / 👎.
Related Issue
No linked issue. This continues the video-upload work on
feat/video-in(the server-side materialize of video uploads already landed earlier on this branch).Problem
After the server stopped inlining uploaded videos as base64, the web UI could not actually show a sent video:
<video path="…"></video>tag as text, and the message showed up twice.<video>element that did render was stuck at 0:00 —GET /api/v1/files/{id}did not support byte-range requests, which browsers require to play and seek video.<video src>still failed with 401: the daemon requires aBearertoken, but browsers load<video>natively and never attach it.What changed
<video path="…"></video>text tag in a user message as a real attachment, and reconcile the daemon echo into the optimistic message so the raw tag and duplicate bubble are gone.GET /api/v1/files/{id}so video can stream and seek.<video>src on its own.Tests: added coverage for the video-tag rendering, the echo reconciliation, and the file-download range responses.
Checklist
gen-changesetsskill, or this PR needs no changeset.gen-docsskill, or this PR needs no doc update.