Repository navigation
UI Consistency: 1 issue found
apps/web/src/components/chat/ExpandedImageDialog.tsx—ExpandedVideoderivessourceFailedonly fromassetUrl._tag === "Failure", so an attachment video opened while the environment is reconnecting sits on the black loading panel indefinitely, with no message or retry. The previous attachment path toasted "The environment is not connected.", and the mobile player added in this PR treats a missing connection as unavailable.
Everything else reviewed in scope looked consistent: the dialog now owns both non-playing surfaces via stateClassName and restores intrinsic video geometry; MediaActions action ids, ordering, and labels line up with the shared MediaActionId contract and the mobile menu; removing the busy state from the timeline and composer play buttons is safe because the dialog opens synchronously and renders its own loading surface.
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: assets/assetUrls.ts, components/ChatMarkdown.tsx, components/ChatView.tsx, components/ChatView.logic.ts, components/chat/ChatComposer.tsx, components/chat/ExpandedImageDialog.tsx, components/chat/ExpandedImagePreview.tsx, components/chat/MessagesTimeline.tsx, components/media/MediaActions.tsx, components/media/MediaVideoPlayer.tsx, filePathDisplay.ts, markdown-links.ts, terminal-links.ts, plus tests.
Checks performed:
- Traced every
MediaVideoPlayercall site (ChatMarkdownVideo,ExpandedImageDialog,FilePreviewPanel.WorkspaceVideoPreview) after theonExpandremoval; no call site still relies on the expand affordance, and the docs change records native fullscreen as the replacement. - Verified
stateClassNamemerges cleanly for both the loading (block aspect-video w-full) and failure (min-h-28 … bg-muted/40) panels, and that the dialog's fixedh-48 w-[min(92vw,32rem)]surface fits the failure content (alert row + Retry / Open in browser buttons). - Verified
videoClassNameoverrides (aspect-auto w-auto max-h-[86vh]) win over the inline base geometry so expanded videos keep intrinsic sizing, withtext-centercentering them. - Confirmed
MediaActionsno longer needshasActions: the item list always contains at leastsave, and the id set matchesMediaActionIdand the mobileuseMediaActionsordering. - Confirmed removal of
openingVideoAttachmentIdis safe:openFileAttachmentsets the expanded preview synchronously for videos, so feedback moves into the dialog's own loading surface. - Traced the disconnected path through
assetUrlStateFromResultandcreateEnvironmentQueryAtomFamily:available/offline/blockedfail (failure panel renders), butconnecting/backoffand session-lessconnectedreturnEffect.never, which is the indefinite loading state reported above.
🔍 Agent activity — 22 tool calls
💭 I'm scanning the diff to identify which web UI files fall within scope, covering assets, chat components, and their tests. I notice the video tile refactor moved to a separate PR, so MessagesTimeline changes are now much smaller — just removing the loading state from the play button — meaning my earlier comments there no longer apply. I'm now checking ExpandedImageDialog.tsx to confirm whether the loading and failure states for expanded video are properly handled with the shared state class name. Now I want to look deeper: MessagesTimeline and ChatComposer both dropped their busy/loading indicators on play buttons, but since the dialog shows its own loading placeholder, that seems acceptable — though I should verify how composer video attachments behave on click since they may use local blob URLs without any minting delay. I also want to check MediaActions.tsx, where the
hasActionscheck was removed so the context menu now always opens regardless of whether there are actual actions to show. The item list always includes a "save" action plus optional copy-path/copy-url/open-file/copy-image entries, so at minimum there's always one item — meaning the menu will never be completely empty, which makes removinghasActionsseem safe. But I'm now wondering whether that "save" item could end up disabled with nothing else available, producing a menu with just a single disabled action, which would be a real regression worth flagging — I need to trace when a video source would lack both a reference and a usable src/asset to confirm this edge case actually occurs. I want to check MediaActions.tsx directly to nail down that logic, then verify whether MediaVideoPlayer's removed expand button still has stray callers like ChatMarkdown or other preview panels passing onExpand/onImageExpand that would now be dead props. I'm also considering whether the fixed height and min-height overrides on the expanded image failure panel could clip its retry/open-in-browser content, though the panel's width suggests it likely still fits.
1. browse_code — apps/web/src/components/media/MediaVideoPlayer.tsx
2. browse_code — apps/web/src/components/media/MediaActions.tsx
💭 Let me check the ExpandedImageDialog's current state fully.
3. browse_code — apps/web/src/components/chat/ExpandedImageDialog.tsx
💭 ExpandedVideo handling looks fine since markdown-direct videos already use a proper URL in item.src. The MediaActions wrapper around the dialog card was already present before, so that's not a new change. Now I'm checking whether removing the expand button from MediaVideoPlayer affects other places in the codebase that relied on it.
4. git_tools — MediaVideoPlayer|onExpand|onImageExpand
💭 With only three call sites—ChatMarkdown, ExpandedImageDialog, and FilePreviewPanel—removing the expand button means inline videos lose their expand-to-fullscreen affordance, though the docs suggest this is intentional since native fullscreen controls now cover that case. Images still keep their expand-to-dialog behavior though, so there's a slight inconsistency between how images and videos are treated inline. Let me check whether FilePreviewPanel actually passes onExpand by looking around line 360.
5. browse_code — apps/web/src/components/files/FilePreviewPanel.tsx
💭 FilePreviewPanel looks unchanged and fine without onExpand. Now I'm checking ChatMarkdown's video rendering and image expand handling, plus tracing how ChatView downloads attachments and opens video dialogs, since one path currently opens with a null src that needs fixing.
6. browse_code — apps/web/src/components/chat/ChatComposer.tsx
💭 I'm tracing how local versus uploaded files get previewed: local files get an instant blob URL preview, while uploaded files trigger onFileOpen leading to a video preview dialog that loads its own placeholder, so removing the busy state should be safe. Now I need to double check how ChatView's onFileOpen handles the attachment id being passed in from the composer. Confirmed the callback routes video attachments to buildAttachmentVideoPreview, which sets src to null and lets the dialog mint the asset URL itself via useAssetUrlState, so the context menu ends up with just a "Save video" action since there's no reference or src yet. Now I'm reviewing the assetUrls.ts diff, noting useAssetUrlState now accepts null and returns a loading state, and a doc comment on useAssetUrlRefresh was removed. Tracing through what happens when the asset is null and environmentId is present versus null — the empty atom yields a "Loading" tag, so ExpandedVideo won't treat it as a failure and will just use the raw src, which seems fine given onRetry is only wired up when an asset exists. I'm also checking whether useAssetUrlRefresh returning undefined for a null environmentId causes any type issues downstream, but it looks safe since onRetry isn't invoked in that path. Moving on to check the ChatMarkdown diff, where onImageExpand was removed from ChatMarkdownVideo. I want to see if
originalUrlis now an unused prop in ChatMarkdownVideo, which could trigger a lint or type error, so let me pull up that component.
7. browse_code — apps/web/src/components/ChatMarkdown.tsx
💭 I'm noting that the direct-video branch in ChatMarkdown around line 2621 dropped the onImageExpand handler, and considering whether removing MediaVideoPlayer's expand button is a UI-consistency issue since inline videos used to have an expand icon (now relying on native fullscreen) while inline images still open into the dialog. The failure panel also lost its expand button but that seems fine, since retry logic still works correctly for direct videos without an asset — calling retry resets playback state and reloads. I'm checking the fixed height and padding on the failure panel to confirm content fits comfortably within it, and the min-height override is redundant but harmless. Now I'm looking at the key prop used on the expanded video element. Since MediaActions already wraps the whole dialog card while MediaVideoPlayer skips passing its own actionsSource, that avoids double-wrapping. But I'm noticing that wrapping the video in MediaActions with a context-menu preventDefault changes right-click behavior — the native browser video menu (save video, picture-in-picture, playback speed) gets replaced by the app's custom menu, and I need to verify whether this was already the existing behavior before this change or something newly introduced by the diff. Now I'm looking at MediaActions.tsx more closely:
onContextMenunow always suppresses the native menu and shows the app one, even when a source has no real actions. That means for an unavailable media item, right-click would surface a menu with only disabled entries like "Save video" or "Copy image" instead of falling back to the browser's native menu — I want to check whether that case actually occurs in the modified code, like in ExpandedImageDialog's fallback source handling. I don't think this is a high-probability scenario worth flagging, since most image/video sources in the affected paths still have a src or actionsSource present. ThecanCopyImagegating addition looks like a reasonable improvement, and the "Save video" logic for inline markdown videos with an asset still routes correctly through downloadMedia. I'm also noticing the menu item ordering changed to align desktop and mobile — both now list copy paths, copy-url, open-file, save, copy-image in the same sequence, which looks like an intentional consistency fix. Separately, the removal of theopeningVideoAttachmentIdbusy state for the user video play button makes sense since clicking now synchronously opens the expanded image dialog with a loading placeholder, so no busy indicator is needed. Checking thatctx.onFileOpen(file)in MessagesTimeline correctly maps to ChatView'sonFileOpencallback, which handles the video mime type check — that looks fine. I'm also confirming the blob preview URL cleanup effect only fires for video items with blob-based sources, revoking the URL properly on cleanup. Now I'm tracingdownloadFileAttachment, which no longer needs video-specific handling since its error title is hardcoded. Looking atresolveMarkdownMediaPreview, I'm following how it resolves the media source, branching based on whether access is direct (resolving a protocol-relative URL) versus unavailable, where it needs the thread reference and HTTP base URL to construct a proper path. Checking the name fallback logic, it seemsresolveMediaSourcealready guarantees a non-empty name by falling back through the reference's filename, then basename, then kind—so that behavior hasn't changed. Let me pull up the full file to verify nothing else shifted.
8. browse_code — apps/web/src/components/chat/ExpandedImagePreview.tsx
💭 I'm checking that autoPlay behavior stays consistent between the composer's local video preview and the expanded video dialog, and similarly for markdown media previews using MediaVideoPlayer with autoPlay false and metadata preload—both match prior behavior. Now I'm considering whether there's a difference specifically when a markdown video is expanded through the dialog. Right, markdown video no longer expands directly since onImageExpand was removed from ChatMarkdownVideo, though resolveMarkdownMediaPreview still supports the video kind for other paths like clicking a file link. I'm verifying that the merged CSS classes in ExpandedVideo properly override MediaVideoPlayer's base display styling via tailwind-merge. I'm also checking that no style prop leaks into the dialog, that the loading/failed states and retry button behave correctly when asset minting fails, and confirming the removed openingVideoAttachmentId prop and its related test assertions are cleanly gone from ChatComposer and MessagesTimeline. I'm now checking whether removing aria-disabled styling from the composer while keeping disabled classes in the timeline causes any inconsistency, and confirming that saving a locally-blobbed video and rendering image failures with a null src both behave correctly in the dialog.
9. browse_code — apps/web/src/assets/assetUrls.ts
💭 I need to verify that usePreparedConnection accepts a null environmentId, since ExpandedVideo calls hooks unconditionally with nulls when there's no asset. If httpBaseUrl ends up null, useAssetUrlState would likely just return a Loading state.
10. git_tools — export function usePreparedConnection
💭 Now let me check MessagesTimeline's current video attachment rendering after the move, confirming the busy state removal didn't break anything and that the disabled play button still shows correctly when
file.downloadable === false.
11. browse_code — apps/web/src/components/chat/MessagesTimeline.tsx
💭 Tracing through the timeline video click handler, the disabled state matches expected behavior for in-flight attachments, and the video preview builder correctly receives the environmentId it needs. Now I'm checking whether the dialog properly displays the title or filename. There's no MediaActions reference for attachments, consistent with mobile's save-only behavior. I'm now scanning for any remaining UI consistency issues — checking whether right-click still works correctly on disabled save states, whether removing the expand button breaks inline markdown video playback, whether the dialog's keying logic handles attachment videos without a src, and whether removing hasActions changes keyboard behavior somewhere. I'm confirming the context menu/Shift+F10 handler still opens correctly since the app menu always has at least one item now, so no regression there. I'm also checking the composer video button for uploaded files — the click-guard logic and removed aria-disabled/aria-busy classes look consistent with prior behavior since they were only used for the isOpening state. Now I'm looking at the expanded video component's sourceFailed logic to see if there's an edge case when the asset is undefined and the source itself is null. Checking whether web image previews could ever have a pending/null src outside the dialog paths — mobile uses resource-based resolution while web always has a src, so that seems fine. Now I'm looking at the file-link refactor across
filePathDisplay.ts,markdown-links.ts, andterminal-links.ts, specifically noting thatresolveInlineCodeFileLinkMetalost its fallback path that used to resolve viaresolvePathLinkTargetwhenbaseDirand conventional file position checks passed — now it just relies onresolveMarkdownFileLinkMetadirectly, which is a real behavior change worth flagging. I'm checking whether the newlooksLikeFilePathcovers the same ground as the oldisConventionalFilePosition, since extensionless names like "Makefile" or "Dockerfile" combined with a line number are handled through the new extensionless-file-name set, but I want to verify the pattern still matches nested paths like "src/Makefile:12" the same way the old regex-based check did.
12. browse_code — packages/client-runtime/src/markdownLinks.ts
💭 I'm noticing that resolveInlineCodeFileLinkMeta used to fall back to a path-based resolution when a conventional file position was detected, but the new version appears to simplify it down to just calling resolveMarkdownFileLinkMeta directly, dropping that fallback behavior. I'm tracing whether the removed
isConventionalFilePositioncheck is still effectively covered by the newer file-detection logic, since extensionless filenames like "Makefile" now get recognized as files even without a line position -- which is a broader match than before, and I want to confirm this widening applies consistently to other conventional filenames too. Now I want to check whether this widening affects the Windows drive path detection logic in the web markdown-links file, since that's a related regex pattern I should verify hasn't shifted alongside these changes. It formats the position, then checks if the path is relative — if not, returns it directly, otherwise resolves against baseDir. I want to check lines 60-110 of the web file to confirm the rest of the logic.
13. browse_code — apps/web/src/markdown-links.ts
💭 I'm comparing the new
fileBasenameimplementation against the oldbasenameOfPathlogic, which stripped trailing slashes and then extracted the segment after the last path separator. Tracing through the edge case of a root path "/", I find the old function returned an empty string while the new one returns "/" itself, which is actually a minor improvement since the test expects a non-empty label. Now I'm shifting to comparesplitFilePathPositionagainst the oldsplitPathAndPositionin terminal-links, noting the new version returns numeric line/column values and omits zero or negative ones entirely, which changes behavior for something like "file.ts:0". I trace through resolvePathLinkTarget: the old code's truthy check on the string "0" meant it kept the ":0" suffix and produced "resolvedPath:0", whereas the new formatFilePathPosition drops the line entirely and just returns the plain path. This means a terminal link like "file.ts:0" now opens the file without any line reference, which seems like reasonable behavior but I'm flagging the edge case of a file whose name might legitimately end in ":0". I then compare the old and new regex-based line/column parsing (splitPathAndPositionvs.POSITION_SUFFIX_CAPTURE_PATTERN) against several test cases like "file.ts:12:5" and "a:12:5:7", confirming both versions extract the same line, column, and path values. Now I'm checkingformatWorkspaceRelativePath, which switched from the old string-based split/position handling to the newsplitFilePathPosition/formatFilePathPositionfunctions, and I want to see how it behaves when the line number is "0". I'm noting the old behavior returned${displayPath}:0for a falsy-but-truthy "0" line value, while the new version drops it entirely—a minor discrepancy, but these are really logic refactors rather than true UI consistency issues, and the author has already pushed back on similar refactor-parity concerns before. I want to refocus on whether there's an actual UI-level finding in the current codebase, specifically looking at the styling changes in MediaVideoPlayer (state class name, autoPlay behavior, expand removal), ExpandedImageDialog, MediaActions, and the removals across ChatComposer, MessagesTimeline, and ChatMarkdown. I'm particularly interested in examining howautoPlayis handled in MediaVideoPlayer next. Looking at how autoPlay interacts with the video element's preload behavior, the cleanup effects on unmount and visibility change, and the playbackSource state tracking, everything seems consistent with prior behavior—the expanded dialog still autoplays by default just as before. I'm now considering whether autoplay with sound could be blocked by browsers, though this matches the prior implementation's behavior too. I'm also checking accessibility: the loading state has a proper status role and label, which looks fine. Then I'm tracing through what happens when an attachment video fails—looking at how originalUrl and the fallback src get passed to the OpenMediaLink component in that failure panel. I want to check whether OpenMediaLink gracefully handles the case where both originalUrl and src end up null, since that could leave the link without a valid href.
14. browse_code — apps/web/src/components/media/OpenMediaLink.tsx
💭 I confirm the failed attachment video panel shows the unavailable message with a working retry button, and check that the secondary button's contrast holds against the black background using semantic tokens. Now I'm scanning ChatView.tsx for any unused imports or leftover code that might need cleanup. Tracing through the video flow further: the blob cleanup effect now only applies to composer local previews since videos stream via minted asset URLs, and the dialog's src construction and save action for attachment videos both check out fine. I'm re-scanning the diff for remaining style-level inconsistencies, starting with the removal of
hasActionsin MediaActions.tsx which changes right-click menu behavior. Only videos previously lacking a reference and onOpenFile — like a composer's local fallback video preview — hadhasActionsfalse, meaning right-clicking hit the native browser video menu (save, loop, PiP, speed controls). Now that case routes through the app's own menu with a single "Save video" option instead, suppressing the native menu — this looks like an intentional consistency change matching the stated behavior that right-clicking media should surface the app's own save/copy options rather than the browser's built-in ones. I also want to double check that opening an uploaded composer video without a reference (via onFileOpen) still shows immediate loading feedback in the dialog — it does, with a black placeholder panel, so no regression there. I should trace where ChatView passes onFileOpen into ChatComposer to confirm it's the same handler used for videos elsewhere.
15. git_tools — onFileOpen=
16. browse_code — apps/web/src/components/ChatView.tsx
💭 Now I'm considering what happens when opening a video attachment in a disconnected environment—previously it would download and show an error toast, but now the dialog opens and the asset URL state stays stuck in Loading forever since there's no connection, resulting in an infinite loading panel with no error feedback to the user. That seems like a real UX regression worth checking further. I'm weighing whether this is worth flagging as a real bug — mobile explicitly treats a disconnected environment as unavailable while web doesn't, and the existing
ExpandedMediaFailurecomponent in ExpandedImageDialog shows there's already a pattern for surfacing failure states that video isn't using. The open question is whether a disconnected environment eventually surfaces as an actual Failure state via a timed-out RPC, or whether it just hangs indefinitely in Loading with no distinguishing signal. Let me check how assetEnvironment.createUrl behaves when disconnected, over in client-runtime/state/assets.ts.
17. browse_code — packages/client-runtime/src/state/assets.ts
💭 The RPC query probably stays in a waiting state while disconnected rather than failing outright, since mobile explicitly checks for the disconnected connection tag to show an unavailable state — implying it needs that check because the query alone would just hang loading. That's decent evidence the web ExpandedVideo, lacking this same check, is left showing an indefinite loading panel when disconnected, and this looks like something introduced by the changed lines. Previously attachment videos went through a path that explicitly toasted "not connected" via a connection guard, and markdown video dialogs got their src pre-minted with a throw for missing httpBaseUrl — so the dialog never even opened in a loading state before. Now the dialog itself owns minting for attachments and has no disconnected end state, which is a genuine, concrete gap matching the mobile pattern this same PR introduced. My suggested fix is to have ExpandedVideo use the same prepared-connection check that's already available on web. Working through how to guard against the case where the asset itself is undefined so the connection option doesn't get misread as disconnected. Since the fix needs both a logic change and a new import, it can't be expressed as a single clean inline suggestion or unified diff, so I'll just describe the fix in prose rather than provide a diff. I'm also weighing an alternative: instead of changing ExpandedVideo, ChatView could pre-check the connection before opening the dialog and show a toast if it's missing, similar to its existing guard pattern. That's a smaller, more localized fix, though the dialog-level check would stay more consistent with how mobile handles reconnection while the dialog is open, so I'll mention both approaches but lean toward the dialog-level one. Before settling, I want to confirm there isn't already some failure state the RPC query atom can surface on its own.
18. git_tools — export function createEnvironmentRpcQueryAtomFamily
💭 I want to check how createEnvironmentQueryAtomFamily and runInEnvironment behave when there's no active connection — specifically whether they block or wait on the environment supervisor before proceeding.
19. git_tools — function createEnvironmentQueryAtomFamily
20. browse_code — packages/client-runtime/src/state/runtime.ts
💭 I'm confirming that when disconnected and offline/available/blocked, the atom fails and shows the failure panel with retry, which works fine. But during connecting/backoff states, the atom stays in indefinite Loading with no feedback to the user, and preparedConnection is None the whole time — that's the endless-loading bug I need to fix. Previously, the attachment path immediately toasted "The environment is not connected" during this same connecting phase, since readPreparedConnection returns null when not prepared. So this is a real regression: users went from getting an explicit error message to silently seeing a black box while reconnecting. I think this is worth flagging as a moderate finding, since it touches on preserving consistent loading/error feedback across the migration. I want to check whether there's anything stronger before finalizing — let me look at the ChatMarkdown diff for the removal of expand from videos, and check whether the direct-video call site around line 2620 still passes originalUrl correctly.
21. browse_code — apps/web/src/components/ChatMarkdown.tsx
💭 I notice the markdown image renderer still relies on the older classification helpers instead of the new shared resolver, though that's not something requiring a fix. I'm also checking that expandMedia falls back to the local preview setter, and confirming videos don't use it anymore, which seems fine. One real finding stands out: ExpandedVideo lacks a disconnected/unavailable end state when the environment isn't connected, unlike the mobile version in this same PR which checks the connection tag, and unlike the old attachment path which surfaced a "not connected" toast. I want to verify this is actually a regression by checking whether main's onFileOpen routed timeline videos through downloadFileAttachment, which handled the video case with a connection check before setting the expanded image. So the behavior change is real: previously a disconnected environment gave an immediate toast, now it opens a dialog that hangs indefinitely. The fix would be to derive sourceFailed from the prepared connection state, mirroring mobile, or restore the pre-check in openFileAttachment. I'm trying to figure out where to place this as an inline comment on ExpandedImageDialog.tsx near the sourceFailed line, and whether I can propose a single-hunk fix without needing a new import, since adding usePreparedConnection would require touching multiple hunks. Finishing sourceFailed...", or alternatively keep the connection check in openFileAttachment before the dialog opens. Though I realize there's a subtlety: if the session hasn't fully established yet, prepared connection could briefly read as None even while actually connecting, causing a flash of "unavailable" right after opening — mobile has this same tradeoff, so it's probably acceptable. Confirming
usePreparedConnectionreturns an Option matching assetUrls.ts usage. Scanning for anything else in scope: the ChatView.logic.ts removal and MessagesTimeline prop removal both look fine, and MediaActions' save/copy labeling for images versus videos seems consistent too. I'll target the review comment at line 42, wheresourceFailed={assetUrl._tag === "Failure"}sits inside the new ExpandedVideo block (lines 29-53), confirming that's part of the diff hunk. Given this is a single moderate finding rather than a clear regression risk, I'll frame the final verdict accordingly. Since there's an actual finding, the check should default to failure rather than staying neutral, so I'll post the review with that conclusion and a concise summary via the complete_check call.
22. modify_pr — Submitted review with 1 inline comment
Agent Credits: 360 credits
Token usage: 44 uncached input · 3,263,165 cache-read input · 187,012 cache-write input · 24,780 output
Agent Credits may also include non-token charges from external tools such as web research.