Repository navigation
UI Consistency: 1 issue found
apps/web/src/components/chat/ChatComposer.tsx — the new composer video tile renders no thumbnail and no filename for a restored draft video (file === null with uploadedAttachmentId), so it appears as an unlabeled black square; the sibling image tile falls back to the attachment name when it has no preview, and the timeline video tile captions its name. Since videos are now excluded from composerOtherFiles, the previous file row that showed the name is gone for this state. Suggested fix posted inline: render the name inside the tile only when there is no thumbnail.
No other in-scope findings: the raw <button> media tiles are legitimate image/media targets rather than reconstructions of Button; aria-busy/aria-disabled loading states keep focus intact; environment scope is threaded explicitly through isPreviewableComposerVideo(file, environmentId) and openFileAttachment; blob preview URLs created by buildExpandedImagePreview and loadDesktopVideoPreviewUrl are revoked by the expandedImage effect in ChatView.
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.
Files examined in scope: apps/web/src/components/ChatView.tsx, ChatView.logic.ts, chat/ChatComposer.tsx, chat/MessagesTimeline.tsx, chat/ExpandedImageDialog.tsx, chat/ExpandedImagePreview.tsx, chat/composerAttachmentFiles.ts, types.ts, plus pullRequest/PullRequestMarkdown.tsx. Out-of-scope context read: apps/server/src/http.ts, apps/server/src/assets/AssetAccess.ts, apps/desktop/src/electron/ElectronProtocol.ts, apps/web/src/components/ui/button.tsx, apps/web/src/composerDraftStore.ts.
Checks performed:
- Primitive reuse: new tiles are media/image targets with their own geometry (allowed exception); overlay chrome (remove, retry) still uses
Buttonwithicon-xsandTooltip, matching the image tiles. - Accessibility/interaction: play tiles keep the control focusable during loading (
aria-busy/aria-disabledplus a re-entry guard) rather thandisabled, so focus survives the dialog round trip;<video>in the expanded dialog carriesaria-label, and dialog/close labels are media-aware. - Download recovery:
downloadVideoPreviewfetches and re-wraps as a same-origin blob URL, and the server setsaccess-control-allow-origin: *for asset GETs, so the fallback works in browser builds where the asset response is now inline (noContent-Disposition). - Environment routing: composer video partitioning and the opening-state id both derive from the passed
environmentId/uploadedAttachmentId; no active-environment fallback introduced. - Blob lifetime: object URLs from
buildExpandedImagePreview,attachVideoThumbnail, andloadDesktopVideoPreviewUrlall have a matching revoke path (revokeBlobPreviewUrlguards non-blob URLs).
🔍 Agent activity — 23 tool calls
💭 Since many comments have already been addressed across prior runs, I want to check the current head state of the files to spot any new issues that haven't been flagged yet.
1. browse_code — apps/web/src/components/chat/ExpandedImageDialog.tsx
2. browse_code — apps/web/src/components/chat/ExpandedImagePreview.tsx
💭 I'm tracing who's responsible for revoking the blob URL created for local video previews, checking whether ChatView's effect around
expandedImagehandles cleanup when the item is a video. Looking further, I'm checking whether the download-fallback path for failed videos properly reads the blob URL, whether the dialog's next/prev navigation could interfere with single-item video previews, whether the disabled download button'saria-disabledattribute actually applies styling through the Button primitive, and howComposerVideoThumbnailuses the loaded-metadata event. The setTimeout revoke issue flagged in the prior review at c3ca56f now uses a 30-second delay, so that seems resolved -- I need to check ChatComposer and MessagesTimeline's video tiles along with ChatView at head to see what issues remain.
3. browse_code — apps/web/src/components/chat/MessagesTimeline.tsx
4. browse_code — apps/web/src/components/chat/ChatComposer.tsx
💭 The earlier fix removing the filename caption from the composer video tile seems to have resolved the z-index issue flagged before. Now I'm checking remaining edge cases, like when a composer video tile needs re-attachment because the file is null without a matching uploaded attachment ID for the current environment. Videos requiring reattachment fall into the composerOtherFiles list instead, which properly shows a reattach affordance, so that's fine. I want to compare whether composer image tiles show filenames while video tiles don't, and I'm also checking the MessagesTimeline video button's disabled state against the earlier fix for aria-disabled during loading — that looks consistent with sibling file rows. There's a minor cosmetic nit about the cursor style being zoom-in rather than play-related, and I'm noting that neither the new video tiles nor the existing image tiles have a focus-visible ring, so that's consistent behavior rather than a regression. I'm now digging into a more substantive concern: in ExpandedImageDialog, the download fallback for a failed video calls downloadVideoPreview on the item's src, but in the browser (non-desktop) case that src is a cross-origin environment asset URL. Since the desktop-specific preview URL loader only runs on desktop, fetching that cross-origin URL in the browser could hit CORS restrictions and cause downloadVideoPreview to throw, which would incorrectly surface a "could not download this video" failure state — this seems like a real potential regression worth flagging. But I want to avoid re-flagging something already covered in a prior review comment, so I should check server CORS settings on assets and diff against the last commit that was flagged to see what's genuinely new since then.
5. git_tools — c3ca56f..b08b5f1
💭 This might have been a force-pushed branch, so I should check the commits on it instead. Let me look at the UI Button primitive and composer image tile for comparison, along with the ChatView code.
6. browse_code — apps/web/src/components/chat/ChatComposer.tsx
7. browse_code — apps/web/src/components/ui/button.tsx
💭 I'm noting the download Button uses aria-disabled without a visual disabled style, but it's mitigated by the label switching to "Downloading…" for feedback. I'm also checking that failed/downloading video src states are tracked per-source, and looking at how the onError fallback triggers when a video fails to load. Now I'm tracing the object URL cleanup effect that revokes blob video sources when the expanded image changes or unmounts — there's a subtle edge case where clicking "Download video" then closing the dialog could revoke a blob mid-fetch, causing an abort error after the dialog is already gone, though it's minor. I want to confirm this cleanup also covers composer-created local video previews, so I'm checking whether the composer's onExpandImage prop routes through to the same setExpandedImage state in ChatView.
8. git_tools — onExpandImage|setExpandedImage|expandedImage
9. browse_code — apps/web/src/components/ChatView.tsx
10. browse_code — apps/web/src/components/ChatView.tsx
💭 I'm looking at how the composer handles uploaded versus local-file videos: uploaded ones open by matching their attachment id, while local-file videos build the preview synchronously without needing a loading state, though I need to check what happens if a local video shares an attachment id with one currently being opened. Also I'm noticing the composer video tile is missing the name label and the "draft may not persist" indicator that image tiles show, and I want to check whether videos needing reattachment surface anywhere in the UI given how
isPreviewableComposerVideoandcomposerFileNeedsReattachare defined.
11. browse_code — apps/web/src/components/chat/composerAttachmentFiles.ts
💭 I'm noticing that when a video with a local file moves into the tile grid, it loses information the file list used to show — like size limits, blocked-attachment reasons, and reattach prompts — since the tile display doesn't surface those same capability checks the composer's file row relied on. I need to check whether the block reason logic actually uses composerFiles or composerOtherFiles, so let me search for fileAttachmentCapabilityBlockReason usage in ChatComposer.
12. git_tools — composerFiles|composerOtherFiles|composerVideos|fileAttachmentCapabilityBlockReason|composerFileNeedsReattach
💭 I'm noticing the video tile logic doesn't distinguish oversized unuploadable files from normal ones, so it loses the size/reattach affordances the old file list had, even though the composer banner still shows the warning — a borderline but likely acceptable tradeoff. I'm also checking how reattach detection depends on file identity comparison. Looking at the button/video markup, positioning and interactivity seem fine since the video lacks a controls attribute. Now I'm checking the timeline tile's disabled state tied to downloadability for optimistic just-sent files. I'm also comparing the black backdrop styling on video tiles versus the semantic background tokens used on image placeholders — it's a visual inconsistency between themes but leans aesthetic rather than a real accessibility violation, so probably not worth flagging as a finding.
13. git_tools — bg-black|text-white
💭 The hardcoded black/white for media surfaces isn't a strong finding since it's precedented elsewhere. I'm now checking likelier regressions — the video tile's missing focus ring matches its siblings so that's not an issue, and the zoom cursor is just aesthetic. More interesting is the ChatComposer's comparison between
openingVideoAttachmentIdandfile.uploadedAttachmentId, where a null versus undefined mismatch could cause unexpected equality behavior. The real finding is that a restored video tile in the composer (uploaded, no local file) shows just a bare black square with a play icon — no thumbnail, no filename, no size — whereas the equivalent image tile falls back to showing the name, and the timeline video tile also shows its name. This is a genuine information-loss regression and an inconsistency the prior flags didn't catch, since those addressed the timeline caption and a z-index issue rather than the composer tile's missing caption entirely. Actually, tracing through it, a restored file with an uploadedAttachmentId might still trigger the upload effect depending on whether needsReattach applies, so I can't be fully confident there's no overlap with the progress overlay. Let me look for a cleaner finding instead — like the ExpandedImageDialog navigation buttons still saying "Previous image"/"Next image" even though the rest of the labeling was made generic for video support, which seems like a minor but clear inconsistency. I'm also noticing the download-fallback button uses aria-disabled without the disabled attribute, so it doesn't visually look inactive during download even though the label switches to "Downloading…" — a minor polish issue. There's also the failure card logic checking failedVideoSrc against the current item, and I want to check the blob URL revocation effect in ChatView to see how it tracks the expanded image item. src as video and blob-prefixed before cleaning up, and trace how the composer's locally-created video blob flows through onExpandImage into that same state to make sure it gets revoked properly too.
14. browse_code — apps/web/src/components/ChatView.tsx
15. browse_code — apps/web/src/components/ChatView.tsx
💭 I'm tracing through how the composer handles local video previews—each tap on a local video generates a fresh object URL, and prior ones get properly revoked when the expanded image state updates. Now I'm checking how the "opening" state is tracked in the composer tile. Since the click path for a local video file is synchronous with no network fetch, isOpening never triggers from that tile—that's fine, and the timeline video tile's click handler is consistent too. The raw button tiles used for image/media targets are explicitly excluded from the "shared controls and variants" check since they're not recreating a primitive, and the download fallback Button already defaults to type="button" so no issue there. I'm now looking at the keyboard handler in ExpandedImageDialog that intercepts arrow key navigation when there are multiple images, and considering how that interacts with video content. For single-video previews, arrow keys pass through to seek within the video since navigation is disabled at length 1, and escape still closes properly without needing focus on the video. I'm confirming the dialog remounts correctly when the source changes via its composite key, and that the type field is only set for videos while images remain consistent by omission. I'm also checking whether isVideoAttachment's mimeType field could be undefined when called across the different attachment types.
16. browse_code — apps/web/src/types.ts
💭 I notice there's a type mismatch:
isVideoAttachmentexpectsChatFileAttachmentbut gets called withComposerFileAttachmentinstead, so I need to verify the mimeType field is compatible between the two. I also recall an earlier review flagged that treating all video/* attachments as inline-playable removes the download path, which I addressed with an onError fallback plus a download button. The timeline's otherUserFiles list excludes videos entirely, so unplayable ones fall to the dialog's download button, which handles that gap. I'm now checking edge cases: disabled play tiles for non-downloadable videos look consistent with prior behavior, but I need to verify what happens for an optimistic just-sent video with a local blob previewUrl -- specifically whether the timeline video tile respects previewUrl or forces a server round-trip through onFileOpen, and whether optimistic attachments have downloadable set to false in that state.
17. git_tools — downloadable: false
💭 Optimistic file attachments show a disabled black tile with the filename since they lack a previewUrl, which seems fine since it matches the inert file row state. I'm digging for a genuine bug in the newer code, looking at how ExpandedImageDialog tracks failedVideoSrc and how the error message switches from "cannot be played" to "could not download" once a fallback download attempt fails. More importantly, I'm suspicious about downloadVideoPreview's use of fetch on the asset URL — this requires CORS headers from the server, whereas previously downloads worked via anchor navigation relying on Content-Disposition rather than fetch. If the asset route lacks CORS support, this fallback would always fail in the browser, which would be a real regression. I want to check the server's CORS configuration for the asset route to confirm.
18. git_tools — Access-Control-Allow-Origin|cors|Cors|CORS
💭 The CORS setup with global allow-origin looks fine, not a real finding. I'm now checking whether the ChatComposer's rendering of composerVideos within the media grid's visibility condition could reveal a genuine UI consistency issue. Actually, I think I found something real: after a page reload, a restored draft video with no file object but a valid uploadedAttachmentId renders as a tile with no thumbnail, just a plain black square with a play icon and no filename—so multiple such videos become visually indistinguishable, unlike the image tile which explicitly handles the missing-preview case. My suggestion is to mirror the image placeholder pattern and show the truncated filename under the play icon in a flex-column layout, though I need to double check it won't get visually clipped by the absolutely-positioned progress strip at the bottom during active uploads. I'm weighing whether this is really a strong finding rather than just a stylistic nit — since the aria-label already includes the name for accessibility, this is purely a visual identification regression versus the old file row, so it's real but somewhat modest. Let me keep scanning the timeline rendering and the ChatView effect for blob URL cleanup to see if there's a more substantial issue to flag. I trace through the video preview cleanup logic: opening the dialog creates a blob URL for the video, and closing it or switching tiles properly revokes the old one and creates a fresh one, which looks correct on a first pass — so I want to check how the ExpandedImageDialog's key prop interacts with this behavior next. I check the desktop download fallback for blob videos, noting it fetches and duplicates the blob before triggering the download, which is wasteful but not broken. I also want to check whether
openingVideoAttachmentIdbeing required versus optional causes issues at other call sites of MessagesTimeline, so I should search for other places that render it.
19. git_tools — MessagesTimeline|ChatComposer[ ]|<ChatComposer|ExpandedImageDialog|buildExpandedImagePreview
💭 Both call sites look fine with a single usage each. I'm checking the MessagesTimeline call site where it passes a single image with its id, verifying that props.image matches the expected ChatImageAttachment type for the new function signature, and now I'm tracing another usage to confirm the composerImages variant also type-checks correctly.
20. git_tools — interface ComposerImageAttachment|interface ComposerFileAttachment
💭 Now I'm checking that types are compatible, then looking at how buildExpandedImagePreview handles the union of ChatImageAttachment and ComposerFileAttachment — for a video with a local File it should return a video preview correctly. I'm noting a subtlety: the check for file-type video attachments relies on a
fileproperty that ChatImageAttachment doesn't have, so I need to make sure that check is safe across both attachment types. I want to look at the latest commits on the branch to see what the author changed most recently, so I'll list them out.
21. git_tools
💭 The log doesn't show the actual head commit, but I notice a relevant convention: the codebase migrated away from native title tooltips to a styled Tooltip component, and our new tiles don't use title attributes, which is good. Now I want to scan for other UI-consistency issues in the changed files. Checking whether ChatComposer's
openingVideoAttachmentIdprop is correctly wired through to the timeline, and confirming thatisOpeningcomparisons match between the composer tile and the timeline tile — the id mapping looks consistent since the composer passes uploadedAttachmentId as the file's id. I'm also verifying that the loading state, aria-disabled attributes, and thumbnail rendering behind the "Loading…" overlay behave the same way across both components, and so far everything checks out as consistent. Now I'm weighing a few candidate findings: whether the composer's restored video tile lacks a visible filename (moderate concern, possibly overlapping with an already-flagged caption issue), whether the expanded image dialog's nav button labels ever apply to videos (they don't, so not an issue), and whether video playback fallback via blob or server URL works correctly on failure (it does, so no issue there). I'm also checking that unmounting the video element on failure properly discards partial downloads, that the non-video attachment download branch is unaffected, and now I'm looking at whetherrevokeBlobPreviewUrlis called appropriately when a request becomes stale.
22. browse_code — apps/web/src/components/ChatView.logic.ts
💭 I'm checking the video tile's overlay gradient and noticing the thumbnail's absolute positioning relies on the button being positioned, but the button itself is static — so I need to trace up to find the actual positioned ancestor, which looks like the outer relative div. I'm also thinking through stacking order: the remove button sits later in the DOM so it paints above the static button's children by default, but the play icon has its own z-10 stacking context, so it could actually render above the remove button depending on overlap — I'm sizing up whether the centered play icon and the top-right remove button visually collide within the 64px tile. Working out that the retry button and "Loading…" text also carry raised z-index and physically overlap the play icon's footprint, similar to the caption issue already flagged in review comment 2d86bc1, though the bottom upload-progress strip sits low enough to avoid the overlap. Stepping back, I think the strongest, most defensible finding is that restored videos (no local file bytes) lose their filename display entirely, unlike sibling image tiles which explicitly show the name when there's no preview — making multiple restored videos indistinguishable black squares. This ties directly to the changed lines and has a small, clean fix: render the name when the file has no local bytes, since there's no thumbnail to conflict with. A safer, more targeted fix: only render the file name below the play icon when there's no thumbnail, mirroring the sibling image placeholder branch, with truncation applied. Writing inline suggestion code... I need to verify the exact indentation and line numbers match the diff's right-side head file lines (3880-3884), since the review comment must reference valid diff positions within the hunk covering 3845-3931. I'm also weighing whether this single finding is significant enough to flag as a failure in the review. Given the check defaults to failure whenever a finding is posted, and the composer tile does render with no thumbnail and no name when the file object is null—just a black tile with a play icon and an "aria-label" of "Play clip.mp4"—I'm considering whether this qualifies as a real gap or an accepted design tradeoff. Since the PR itself includes a test explicitly asserting that restored videos remain on the preview path in their upload environment, this looks like an intended, supported state rather than an oversight. I've also confirmed the image sibling logic renders the file name only when there's no preview URL, which reinforces the asymmetry between image and video tiles. There's a similar focus-visible ring gap on the video tile, but since sibling elements share that same gap, it's not worth flagging separately. I think one inline comment covering the composer video tile issue is the right call here, though I want to weigh one more potential issue with possibly higher severity around how composerVideos and composerOtherFiles are handled in ChatComposer before finalizing. Tracing through it, the environmentId threading actually works correctly here — a draft video uploaded in a different environment falls back to the generic file list rather than the preview tile, and since those rows aren't clickable anyway, there's no broken behavior to report. I also checked the edge case where file.file is null without an uploadedAttachmentId, and that's properly excluded by the preview check. So I'll write up the one comment now, keeping it concise.
23. modify_pr — One finding on the new composer video tile: a restored (uploaded, no local bytes) video renders with neither a thumbnail nor its name, unlike its sibling tiles.
Agent Credits: 175 credits
Token usage: 38 uncached input · 1,386,194 cache-read input · 76,912 cache-write input · 19,625 output
Agent Credits may also include non-token charges from external tools such as web research.