Skip to content

fix(web): keep video previews current

MacroscopeApp / Macroscope - UI Consistency failed Aug 30, 2026 in 5m 6s

UI Consistency: 1 new issue, 1 unresolved

New finding (comment posted)

  • apps/web/src/components/ChatView.tsx (~L2566-2568): the video branch of openFileAttachment awaits loadDesktopVideoPreviewUrl, which buffers the whole file on desktop, but no pending state is surfaced to the play tile. The click has no visible effect until the download completes, and repeated clicks start additional full downloads (the new request-id guard only discards the superseded blob).

Unresolved from a previous run (not re-commented)

  • apps/web/src/components/chat/MessagesTimeline.tsx (~L1067-1082): video attachments moved out of the file list into the media grid still render without a visible filename, unlike sibling image tiles (which fall back to image.name) and the file rows below. It is most visible right after send, where downloadable: false renders a dimmed, unlabeled black square.

Resolved since the last run

  • ExpandedImageDialog video now carries aria-label={item.name}.
  • PullRequestMarkdown doc comment restores the media-src CSP rationale alongside the codec one.

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: apps/web/src/**/*.{ts,tsx,css}. Files examined at head 7f072fb: ChatView.tsx, ChatView.logic.ts, chat/ChatComposer.tsx, chat/MessagesTimeline.tsx, chat/ExpandedImageDialog.tsx, chat/ExpandedImagePreview.tsx, types.ts, plus composerDraftStore.ts and the server/electron changes as context.

Checks performed and found clean:

  • Blob lifecycle: composer previews created in buildExpandedImagePreview and desktop previews created in openFileAttachment both terminate in ChatView's expandedImage effect, which revokes blob: video sources on close/unmount; revokeBlobPreviewUrl no-ops on non-blob URLs, so the non-desktop signed-URL path is safe. StrictMode double-invoke does not apply because the effect short-circuits while expandedImage is null.
  • Prop rename onFileDownload -> onFileOpen: only one call site (ChatView.tsx:7166); no stale consumers remain.
  • buildExpandedImagePreview signature change to ChatImageAttachment | ComposerFileAttachment: all three call sites pass typed attachments carrying a type discriminator, so the new image.type === "image" filter cannot silently empty an existing image preview.
  • Composer split (composerVideos / composerOtherFiles): a hydrated video with file === null falls into the file-row branch, so needs-reattach and size labels are preserved; upload progress, retry and remove controls still use the Button primitive with the same variants as the image tiles.
  • New raw <button> elements are media/zoom targets that mirror the adjacent image tiles (same cursor-zoom-in, same absence of a ring), so they are the documented exception rather than a reconstructed Button.
  • Environment routing: openFileAttachment resolves the connection from the surface's environmentId; no active-environment fallback introduced.

πŸ” Agent activity β€” 24 tool calls

πŸ’­ I'm checking the current PR head to confirm what's changed since prior review passes. The aria-label and CSP rationale fixes seem applied, but I need to verify whether the video tile in MessagesTimeline still lacks a filename caption.

1. browse_code β€” apps/web/src/components/chat/MessagesTimeline.tsx

2. browse_code β€” apps/web/src/components/chat/ExpandedImagePreview.tsx

πŸ’­ The missing filename caption issue on the video tile was already flagged before and remains unaddressed, so I shouldn't repeat that. Instead, I should look for new issues in ChatComposer's video tile, ExpandedImageDialog, and ChatView. I want to check whether the raw button used for composer video tiles is consistent with how composer image tiles are rendered, verify that blob URLs created for expanded video previews get properly revoked, confirm that ChatComposer's expand-image callback wires into the same state as ChatView's dialog, and compare focus styling between the video and image composer buttons.

3. browse_code β€” apps/web/src/components/chat/ChatComposer.tsx

4. browse_code β€” apps/web/src/components/chat/ExpandedImageDialog.tsx

πŸ’­ Checking the ExpandedImageDialog's keyboard handling for videos β€” arrow keys pass through since the length check returns early for single videos, and space isn't intercepted either, so native video controls work fine. Now I'm moving on to look at the composer component. For composer video tiles, I notice the filename isn't shown, unlike other file types, echoing the same gap I flagged in the timeline. More importantly, videos needing reattach (where file is null) wouldn't render in composerVideos since that list filters for videos with an actual File object β€” those would instead fall into composerOtherFiles, so I need to verify that reattach flow works correctly for videos. Now I'm tracing the object URL lifecycle for expanded video previews β€” buildExpandedImagePreview creates a fresh blob URL for videos each time they're opened, and I need to confirm ChatView's effect properly revokes it on close, unlike persistent image preview URLs which are managed elsewhere. Let me check the relevant sections of ChatView around the expandedImage state and its cleanup effect.

5. browse_code β€” apps/web/src/components/ChatView.tsx

6. browse_code β€” apps/web/src/components/ChatView.tsx

πŸ’­ I'm checking the blob revocation effect that cleans up video preview URLs when the expanded image changes, tracing through cases where the same blob transitions between states to make sure nothing gets revoked prematurely. I also wonder whether React StrictMode's double-invocation of effects in dev could cause the object URL to be revoked before the video actually loads, though that's speculative unless the app enables StrictMode. Beyond that, I'm scanning for other UI consistency issues across the composer, timeline, and download-disabled video tiles, but nothing new stands out yet. One real concern: the video uses autoPlay without muted, and browsers typically block autoplaying unmuted video, so it would likely just show a paused first frame instead of actually playing. I'm also noting a subtler issue: pressing Escape while a video is in native fullscreen triggers both the browser's fullscreen exit and the dialog's own Escape handler, closing the dialog and unmounting/revoking the video simultaneously β€” a real behavioral quirk introduced by adding video support. I also spot a minor style regression where a string concatenation replaced what would've been a template literal, but that's cosmetic and outside the scope of a UI consistency review. The more interesting question is whether object URLs created by the composer's expanded-image preview are reliably revoked -- ChatView revokes them in its effect, but only if onExpandImage is actually wired to that state setter. I want to check if ChatComposer is reused elsewhere without that cleanup, which could cause a real leak. Actually, most callers probably already pass ChatImageAttachment objects. I should grep for buildExpandedImagePreview and onExpandImage call sites to verify.

7. git_tools β€” buildExpandedImagePreview|onExpandImage|setExpandedImage|ExpandedImagePreview

πŸ’­ I want to check the type of props.image used in that expanded image preview call, and also look at how setExpandedImage(null) is handled elsewhere in ChatView. Let me look at the surrounding lines in MessagesTimeline for more context.

8. browse_code β€” apps/web/src/components/chat/MessagesTimeline.tsx

9. browse_code β€” apps/web/src/components/ChatView.tsx

πŸ’­ I'm trying to trace whether ChatComposer's onExpandImage prop connects to the same handler as ChatView's onExpandTimelineImage, and checking how MessagesTimeline passes image attachment data through to it. Now I want to see if ChatComposer is rendered anywhere else with a different onExpandImage handler that skips the blob URL revocation logic, so I'm searching for other usages of the component.

10. git_tools β€” onExpandImage=|<ChatComposer

πŸ’­ With one ChatComposer routing both timeline and composer to setExpandedImage, and the blob revoke effect covering composer video blobs, I'm now tracing a possible leak: buildExpandedImagePreview creates an object URL before onExpandImage fires, and I need to check whether that URL always gets revoked when the preview is non-null. I want to check for StrictMode in main.tsx, and also verify whether isVideoAttachment is being called with types where mimeType might be undefined β€” if so, calling toLowerCase() on it could throw. I need to look at the ComposerFileAttachment and ChatFileAttachment type definitions to confirm mimeType is always required.

11. browse_code β€” apps/web/src/types.ts

12. git_tools β€” interface ComposerFileAttachment|type ComposerFileAttachment

πŸ’­ I'm checking whether mimeType is required on ChatFileAttachment in the contract, since some server code treats it as possibly undefined while web code assumes it's always present. I also want to look at the composer video tile's handling of unpersisted images and the warning badge for files needing reattachment. The composer video tile drops the filename and size that the file row normally displays, similar to an issue I flagged earlier in the timeline componentβ€”but since that's a different file location, I'm weighing whether raising it again would be a useful distinct catch or just noisy duplication of a nit already noted. Actually, checking the composer image tiles, they also skip showing filenames when a preview URL exists, so the video tile behavior is consistent with existing patterns thereβ€”no finding needed. I also check whether the new video buttons in the timeline and composer lack focus-visible rings compared to sibling file buttons, but since neighboring image buttons show the same lack of focus styling, this is consistent with existing behavior rather than a new issue. I note the timeline's disabled video button for non-downloadable files renders differently than the file row's non-interactive div for the same caseβ€”a minor inconsistency, but acceptable overall. With the expanded image dialog for video also looking fine, I want to check whether anything new was introduced between the last reviewed commit and the current one, so I'm looking at what changed in that range.

13. git_tools β€” f988a0f..7f072fb

πŸ’­ The MessagesTimeline caption issue is still unresolved, so I shouldn't bring it up again. Looking deeper at other scope files, the environment routing in openFileAttachment seems fine since it uses ChatView's environmentId, and the blob URL revocation logic in ChatView only handles the single expanded image case, which should be okay for composer video previews too. Checking further, the composer's blob URL creation for local files looks properly paired with revocation on dialog close, and the effect's dependency on the expandedImage object identity means it won't rerun unnecessarily. I want to verify StrictMode's double-invoke behavior in main.tsx, and confirm that revokeBlobPreviewUrl only targets actual blob URLs β€” which it does. The video URL handling for non-desktop browsers using signed asset URLs also checks out fine. I should also check where ExpandedImageDialog renders relative to closeExpandedImage, verify the timeline's video tile grid layout doesn't need a fixed aspect ratio, and confirm the download button's aria-label and onFileOpen usage stay consistent for non-video files. Most importantly, I want to grep for any remaining onFileDownload references across the codebase to catch stale call sites that weren't updated during the rename.

14. git_tools β€” onFileDownload|onFileOpen

15. git_tools β€” StrictMode

πŸ’­ There's a discrepancyβ€”the grep only shows one instance of onFileOpen at line 1107, but my earlier view of lines 1067-1122 seemed to show it appearing twice, at 1077 and 1122. I need to reconcile this mismatch. I'm wondering if the view was pulled at a different commit reference than what grep defaults to, or if there's some quirk with how matches are being deduplicated or truncated. To resolve this, I should re-run the grep with surrounding context and an explicit revision to see exactly what's around those lines.

16. git_tools β€” ctx.onFileOpen|isVideoAttachment|PlayIcon

πŸ’­ StrictMode is used in main.tsx for dev, so I'm checking whether the double-invoke behavior affects the revoke effect: since the initial mount happens when expandedImage is null, the effect returns early without a cleanup, and later state updates don't trigger the double-mounting behavior again. For the desktop video preview, it fetches the whole video into memory as a blob because the CSP media-src only allows self, t3code:, and blob:, and since the desktop app origin is t3code://, a remote http://127.0.0.1 media source would get blocked β€” so converting to a blob URL is deliberate, not a bug. The web path uses a signed asset URL directly since it isn't subject to that same Electron CSP restriction, and I've confirmed the ExpandedImageDialog behaves correctly for single-image video previews with no arrows shown. I'm checking whether the video preview request guard correctly compares captured and current thread keys, and it does. I'm also looking at whether the new composer video tile's black background and white text violate the semantic color token rule β€” but since it mirrors the video dialog's intentional black backdrop for media, this hardcoded styling seems justified rather than a genuine inconsistency. I don't see anything concrete enough to flag yet, so I'm going back through the diff history to see what's changed since the last review pass beyond the two fixes already applied.

17. git_tools β€” 2daff8c..7f072fb

πŸ’­ I see the new commit 7f072fb addresses keeping video previews current via a stale-request guard in ChatView. I want to diff against f988a0f but that commit wasn't found by the shallow diff earlier, so I'll try the full SHA instead.

18. git_tools β€” 7f072fb

πŸ’­ The new commit addresses both prior comments and adds a request-currency guard, which looks reasonable, but I should check whether the shared request ID ref could cause a race if a user opens video A then quickly switches to video B. Checking the effect that resets expanded image on threadId change and revokes the blob afterward β€” that seems handled correctly. Now I want to verify the composer's video filtering logic and whether needsReattach is properly excluded when a file reference exists, so I need to look at the definition in the composer draft store around lines 108-125.

19. browse_code β€” apps/web/src/composerDraftStore.ts

πŸ’­ I'm noticing an inconsistency: after a reload, a previously uploaded video loses its local file bytes but keeps its attachment ID, so it falls into the plain file list instead of rendering as a video tile with playback. Before reload it shows as a video preview, after reload it shows as a generic file row β€” this asymmetry might be worth flagging as a gap. I'm also verifying the object URL revocation logic for the composer's video preview: the cleanup effect checks the item type and blob prefix before revoking, and that path looks safe for composer videos and on unmount. I'm now shifting to check whether the ExpandedImageDialog's video element itself has any similar issue. I don't see one β€” arrow navigation between mixed image/video lists is moot since videos never appear alongside images in that array, and the escape/controls/playsInline attributes all look fine. So I'm circling back to whether assistant-authored messages render file attachments through the same onFileOpen path as user messages, since the timeline seems to wire this in two separate spots.

20. git_tools β€” Download ${

πŸ’­ I'm checking whether video attachment tiles respect previewUrl for optimistic messages, and noticing they instead always call onFileOpen, which gets disabled when downloadable is falseβ€”so the local preview link never gets used for videos. For file rows generally, the previewUrl check happens before the downloadable check, meaning a just-sent file could still download from its local blob even if not confirmed. Videos skip this ordering, so I want to verify whether previewUrl is actually populated on optimistic attachments in ChatView.

21. git_tools β€” downloadable: false

πŸ’­ Optimistic attachments only get previewUrl for images, not videos, so that's not the source of the regression I suspected. I need to trace where file previewUrl actually gets set server-side, maybe through the handoff preview mapping, since the timeline clearly reads file.previewUrl from somewhere.

22. git_tools β€” previewUrl:

πŸ’­ File attachments don't get previewUrl in ChatView, only images do, so that branch is for other cases. The remaining open issue is still the timeline caption one already flagged. Checking for new issues in the latest commit, I notice the desktop path revokes previewUrl on superseded requests, but I should verify whether previewUrl there could be a non-blob URL that shouldn't be revoked.

23. browse_code β€” apps/web/src/components/ChatView.logic.ts

πŸ’­ I notice clicking play on a timeline video triggers an async asset URL fetch plus a full desktop download before the dialog appears, with no loading indicator at all β€” the button stays clickable and each click can kick off another full download, which is a real usability gap for large files. Previously the click triggered a native browser download UI, so users had feedback; now there's a silent dead-click period, and the guard commit's mention of superseded requests confirms repeated clicks are expected. This feels like a legitimate but moderate finding β€” concrete and user-visible, though I should think about the smallest reasonable fix, like disabling the button or showing a spinner while the download resolves. Adding pending state would require plumbing state from ChatView through to MessagesTimeline, which is cross-file and non-trivial, so that's likely a category 3 finding without an obvious inline diff. I'm weighing whether this should push the check to failure β€” the instructions say the check defaults to failure when findings exist, and even though I shouldn't re-post the earlier caption issue, its being unresolved still counts as an active finding, so the overall check state should reflect failure while avoiding a duplicate comment. I still need to decide whether to file a new comment specifically about the missing pending state. I'll target the ChatView.tsx line around loadDesktopVideoPreviewUrl since that's the smallest relevant range in scope, and it's not a duplicate of what the prior review flagged. Checking the composer path for the same issue β€” that one loads instantly from a local blob, so it's fine. I also wonder whether the browser video path supports range requests for seeking, but that's server-side and out of scope for my comments, so I'll skip it. I'm fairly confident this is a real finding: the old attachment-click flow was fast (RPC plus anchor click), but the new desktop path does a full-file fetch with no loading feedback, so double-clicking would trigger two full downloads β€” wasted bandwidth even though the blob-revoke guard keeps it correct. That's a solid supporting argument for flagging the missing pending-state indicator, so I'll post a single comment on that line with no diff. I'm locating the exact added line in ChatView.tsx where loadDesktopVideoPreviewUrl is invoked, since the comment needs to target that specific line in the PR diff. I'm phrasing the comment to note that the fetch buffers the whole file with no pending affordance on the tile, so repeated clicks silently kick off duplicate downloads, and suggesting a pending attachment id be threaded from openFileAttachment into MessagesTimeline to show a spinner or disabled state. No further comments are needed on MessagesTimeline itself since that would be redundant β€” I'll finalize with a state failure result and a "UI Consistency: 1 issue found" title, noting the prior unresolved finding too. Now I'm ready to submit the review.

24. modify_pr β€” One new finding on the video playback path; the unresolved caption gap from the previous run is noted in the check summary rather than re-commented.


Agent Credits: 169 credits

Token usage: 38 uncached input Β· 1,188,671 cache-read input Β· 89,452 cache-write input Β· 18,002 output
Agent Credits may also include non-token charges from external tools such as web research.