Skip to content

fix(web): preview videos before sending

MacroscopeApp / Macroscope - UI Consistency failed Aug 30, 2026 in 3m 40s

UI Consistency: 1 new finding, 2 still open

New finding (posted inline)

  • apps/web/src/components/chat/MessagesTimeline.tsx (lines 1067-1082) — video attachments moved from the file list into the media grid lose their visible filename. Sibling tiles keep it (an image without a preview falls back to rendering image.name; the file rows below always show the name). Worst in the optimistic state: optimisticAttachments in ChatView.tsx sets downloadable: false for every file, so a just-sent video renders as a dimmed, unlabeled black square where it previously showed the filename. Suggested fix: caption the tile with file.name.

Still open from the previous run (not re-posted)

  • apps/web/src/components/chat/ExpandedImageDialog.tsx:87-93 — the new <video> branch has no accessible name while the <img> branch uses alt={item.name}.
  • apps/web/src/components/pullRequest/PullRequestMarkdown.tsx:13-15 — the rewritten comment drops the still-valid Electron CSP rationale (media-src 'self' <scheme>: blob: still refuses remote GitHub/S3 media).

Details

Note

Your check run agent prompt is: .macroscope/check-run-agents/ui-consistency.md
More information about how Check Run Agents work can be found in our Help Center.

Scope examined (include patterns apps/web/src/**/*.{ts,tsx,css}): components/ChatView.tsx, components/ChatView.logic.ts, components/chat/ChatComposer.tsx, components/chat/MessagesTimeline.tsx, components/chat/ExpandedImageDialog.tsx, components/chat/ExpandedImagePreview.tsx, components/pullRequest/PullRequestMarkdown.tsx, types.ts, plus the new tests. Server, Electron and docs changes were read as context only.

Checked and found acceptable

  • Raw <button> play targets in the timeline and composer are media targets with tile geometry, not reconstructions of Button; they mirror the existing image-preview buttons (same cursor-zoom-in, same absence of a focus ring), and the overlay controls (remove, retry) still use Button/Tooltip primitives.
  • buildExpandedImagePreview signature change: all call sites (MessagesTimeline regularImages / [props.image], ChatComposer composerImages / [file]) pass tagged attachments, so the new image.type === "image" guard does not silently drop existing image previews.
  • Object-URL ownership: composer video previews are created in buildExpandedImagePreview and revoked by the new expandedImage effect in ChatView (guarded on type === "video" and a blob: prefix), so store-owned image preview URLs are untouched.
  • Composer split (composerVideos requires file !== null) keeps hydrated/needs-reattach video markers in the file-row list where "Attach again" / "Remove to send" is rendered; an oversized video is still named by fileAttachmentCapabilityBlockReason, so the tile omitting size text is not a feedback regression.
  • onFileOpen replaces onFileDownload with a NOOP default and is threaded from the owning surface; no active-environment fallback is introduced (openFileAttachment resolves the connection from the ChatView environmentId).
  • No global CSS, theme variable, or Tailwind-variant changes in this diff.

🔍 Agent activity — 16 tool calls

💭 I already left comments on this PR at an earlier commit, and now the head has moved to a new commit. I need to check what's changed since then to see if my previous feedback was addressed and whether any new issues have popped up, so I'll review the current file states.

1. git_tools — 276494b..f988a0f

2. browse_code — apps/web/src/components/chat/ExpandedImagePreview.tsx

💭 I recall two prior comments—one about the ExpandedImageDialog video aria-label, another about the PullRequestMarkdown comment—so I want to check if those were addressed at head. Looking at the merge-base diff, neither the aria-label nor the CSP rationale comment appears changed, so those issues seem to still be open, but I shouldn't duplicate them. Instead I should focus on finding new issues in the in-scope files, starting with checking the chat component.

3. browse_code — apps/web/src/components/chat/ExpandedImageDialog.tsx

4. browse_code — apps/web/src/components/chat/MessagesTimeline.tsx

💭 Skipping duplicate prior findings since guidance says not to repost unless code changed. Checking the video play button in MessagesTimeline—it's an image target so lacking a focus-visible ring is consistent with the sibling preview button, not a real issue. Now moving on to check the ChatComposer video thumbnail next. Tracing the blob URL lifecycle: buildExpandedImagePreview creates an object URL for composer videos, and ChatView's cleanup effect revokes it when expandedImage changes, but only for item?.type === "video". I need to verify the composer's onExpandImage path actually routes into that same expandedImage state so the revoke logic applies correctly—checking the ChatComposer changes now.

5. browse_code — apps/web/src/components/chat/ChatComposer.tsx

💭 The composer video tile is largely similar to the image tile, missing the persistence indicator since that's image-specific, but it does handle the reattach case by filtering to other-files when file is null. I'm now checking whether videos exceeding the size limit properly show the "Remove to send" warning like other files do. I notice that oversized videos silently fail — since fileCanUpload becomes false, the upload progress just disappears with no failure indicator or size shown, leaving the user without feedback on why the video won't upload. I want to verify composerFileNeedsReattach actually corresponds to file being null, and also check whether isVideoAttachment's type signature is compatible with the ComposerFileAttachment being passed to it — it should be fine since it just needs a mimeType field. I'm also tracing the blob URL lifecycle for expanded video previews: since ChatView revokes blob URLs on expandedImage change via a cleanup effect, and each click generates a fresh URL through buildExpandedImagePreview, this should be safe to revoke — unlike composer image previews, which are persistent and owned by the draft store rather than revoked per-click. I want to confirm setExpandedImage is the actual state setter being called, and double check the cleanup effect's dependency array and closure over item.src are correct. Now I'm looking at other UI consistency issues, like a video tile button using disabled and zoom-in cursor styling that may need to align with how other file tiles handle non-downloadable states. I'm also verifying whether buildExpandedImagePreview's updated signature — now accepting both ChatImageAttachment and ComposerFileAttachment and filtering on image.type === "image" && image.previewUrl — could break existing call sites passing objects that don't have a type field at all, since the old function signature was more permissive.

6. git_tools — buildExpandedImagePreview

7. git_tools — composerFileNeedsReattach|ComposerFileAttachment =|interface ComposerFileAttachment

💭 I'm tracing where images and files get passed through MessagesTimeline and ChatComposer, confirming the type checks line up correctly, and now checking whether the non-persisted attachment indicator set also covers file ids or just images. Looking at the ChatComposer video tile, I notice there's no size feedback shown for oversized videos, unlike file rows which show size and reattach warnings — a minor consistency gap, though likely acceptable since blocking happens globally via the capability reason. I'm also noting that videos with null local file but a persisted upload id render as generic file rows rather than playable previews, which degrades gracefully but isn't perfectly consistent. Now I want to look at the full openFileAttachment function in ChatView.

8. browse_code — apps/web/src/components/ChatView.tsx

9. browse_code — apps/web/src/components/ChatView.tsx

💭 I'm checking whether ExpandedImageDialog's expandedImage state flows from ChatView through the composer's onExpandImage prop, and confirming blob URLs get revoked properly when the dialog closes for video previews. There may be a subtle bug lurking in how the composer handles image previews similarly. I'm satisfied that image previews without a type don't get revoked since they're owned by the store, and video revocation happens correctly on dialog close and re-opens cleanly with a new blob URL each time. Now I'm reviewing the check's specific rules: environment routing looks consistent with existing patterns, and the new raw buttons preserve the same accessibility semantics as their sibling elements, even lacking focus-visible rings like the existing image button. I'm checking whether disabled states and non-persisted indicators are handled consistently for video attachments compared to images, and confirming that the video mimetype detection is scoped correctly. I'm now looking at whether there's a real bug in how the expanded video dialog handles autoplay, controls, and navigation between images. Confirming the video path in MessagesTimeline bypasses buildExpandedImagePreview entirely, so that type mismatch concern doesn't apply there. I'm trying to figure out if there's a genuinely new finding worth reporting, but without the ability to diff against the prior commit, I'm not sure what's changed since the last review.

10. github_api_read_only — repos/{owner}/{repo}/pulls/{pull_number}/commits

💭 The prior review's commit is no longer part of the branch since it was force-pushed, and the new head commit adds video preview functionality to the composer. I should diff against the newest changes to check whether the earlier flagged issues in ExpandedImageDialog.tsx and PullRequestMarkdown.tsx are still present given the rebase. Since those problems still exist but the code wasn't changed in response to my earlier comments, I'll avoid reposting duplicates and instead focus on reviewing the latest commit for new issues.

11. git_tools — b974194..f988a0f

💭 I'm noticing a potential regression: in the latest commit videos moved from file rows into the media grid, but for optimistic messages with a local previewUrl, the video tile still calls onFileOpen which fetches the asset by attachment.id — something that may not exist yet on the server for a pending message. Previously file rows used the local previewUrl directly for downloads; now videos always route through onFileOpen ignoring previewUrl, so I need to check ChatView's attachmentPreviewHandoffByMessageId mechanism to see if this actually breaks. I also notice the video tile no longer shows the filename, which the old downloadable file row did. The disabled state for expired attachments is a bigger concern — the sibling image tile falls back to showing image.name, but the video tile just shows a dimmed black box with no visible label (though the aria-label still exists for screen readers). This is a moderate consistency finding worth flagging. Also worth noting that using disabled on the button removes it from tab order compared to the div pattern elsewhere, though that's minor. I want to check whether ChatFileAttachment.previewUrl is even being populated for optimistic video messages in ChatView.

12. git_tools — previewUrl

💭 I'm checking lines 5900-5960 to see how optimistic messages are constructed for file attachments, specifically whether they receive a previewUrl.

13. browse_code — apps/web/src/components/ChatView.tsx

💭 I'm noticing that optimistic video attachments render as a disabled, dimmed black box with no filename, whereas before this change users could see the actual file name like "demo.mp4". This is a real visual regression, and it also breaks consistency with the sibling image tile's fallback behavior. I'll flag this specific issue in MessagesTimeline.tsx around lines 1067-1082, suggesting the fix add a visible filename caption to match the image fallback pattern. Drafting inline suggestion code... I want to check that adding this span doesn't break the existing tests checking for min-h-[72px] and the aria-label, and I'm now weighing whether this finding is significant enough to justify a hard failure verdict given the review defaults to failing unless there's a clear, actionable issue. Since even the fully-sent, enabled state loses the filename display in this new design, that strengthens the case that this is a genuine regression rather than just an edge case. I'm also starting to check whether the composer's video tile has a similar problem, since the image tile preserves the filename display when there's no preview URL, but the video tile might be dropping both the filename and size/status metadata. For an oversized video, there's no visible indication of why sending is blocked — no size shown, no error reason. Let me check whether there's a separate message near the send button, like a fileCapabilityBlockReason, that explains this to the user.

14. git_tools — export function fileAttachmentCapabilityBlockReason

💭 The oversized video case is already covered by a global block message naming the file, so it's a minor point I'll skip or mention lightly rather than dwell on. Confirming the timeline never shows the file name for video elsewhere while images still fall back to showing the name, I'll frame my comment around the name loss together with the disabled optimistic tile state. I'm also checking whether the grid layout's mixed image/video rows create any stretching issues, but since grid items stretch by default the row height should just match naturally, so that's fine. Since the earlier ExpandedImageDialog aria-label comment was posted on a commit that's no longer an ancestor due to a force-push, I need to verify those prior comments still actually exist on the PR before deciding whether skipping a repost is the right call.

15. github_api_read_only — repos/{owner}/{repo}/pulls/{pull_number}/comments?per_page=100

💭 Since prior comments already cover related points, I'll skip reposting those and focus on the new finding about MessagesTimeline.tsx losing the visible filename on video tiles, particularly in the optimistic downloadable === false state. I'm targeting lines 1072-1080 for an inline suggestion covering that button className range. Writing button replacement code... Confirming the indentation matches, I'm noting this file tile lost its filename display compared to the composer's established pattern, so I'm weighing whether this counts as a genuine regression worth flagging. Given this plus the two unresolved prior findings, I'm leaning toward marking this review as a failure overall.

16. modify_pr — One new finding on the timeline video tile. Two earlier findings (video element accessible name in ExpandedImageDialog.tsx, dropped CSP rationale in PullRequestMarkdown.tsx) are still open on this head and are not re-posted here.


Agent Credits: 148 credits

Token usage: 26 uncached input · 851,646 cache-read input · 106,120 cache-write input · 12,659 output
Agent Credits may also include non-token charges from external tools such as web research.