fix(web): return focus to the composer after closing a media preview#9513

merged1
fix/composer-hold-for-selection
fix/image-preview-return-focus
openedSep 3, 2026byJJulius Marminge
View on GitHubTwo-way sync enabledPR last imported 4d ago

Reported: with the composer expanded and an attachment on it, opening the attachment keeps the composer up (correct), but closing the preview collapses it instead of leaving focus on the composer.

Opening the preview leaves focus on the thumbnail button, which is inside the composer, so the composer stays expanded. But interacting with the dialog — the close button, the image, a media control — moves focus into the portaled dialog, which is outside the composer's focus scope. When the dialog unmounts on close, focus falls to <body>; the composer's blur check sees nothing of its own focused and rests it. Reliable repro: attach an image with an empty prompt, open it, close it with the ✕.

Two changes to ExpandedImageDialog:

  • It marks itself as a composer-owned floating layer (composerFloatingLayerProps), so focus landing on the close button or a media control while the preview is open counts as still inside the composer scope — the same mechanism already used for composer menus and popovers.
  • On unmount it returns focus to whatever element opened it, so closing lands the caret back where the user was instead of on <body>.

Applies to image and video previews and every close path (✕, backdrop, Escape, or after interacting with the media). Stacked on #9499; the diff is independent.

Before

Empty prompt + attachment, open the preview, close with ✕ → composer collapses to its resting line.

before

https://gh-file-drop-api-prod-mi5fy3sowv63ufte.pinglabs.workers.dev/f/2ac3f3888c0ee100/before-preview.mp4

After

Same steps; the composer stays expanded with focus back on it.

after

https://gh-file-drop-api-prod-mi5fy3sowv63ufte.pinglabs.workers.dev/f/dfb194be5223fd9f/after-preview.mp4

Surfaces

  • Web and desktop. The composerEventScope test already covers that a composer-owned floating layer counts as inside the composer scope.
  • Manual pass against a copy of real data: on main the composer collapsed 254px → 84px on every close; with the fix it stays 254px across ✕, backdrop, Escape, and after clicking the media, for both image and video.

Claude Fable 5 via Claude Code.

🤖 Generated with Claude Code

Note

Restore focus to previously active element when ExpandedImageDialog closes

Records the document's active element on mount and refocuses it on unmount using preventScroll, so closing a media preview returns focus to the composer. The portal root also now receives composerFloatingLayerProps.

Macroscope summarized f842ddf.

Added 1 commit

  • fix(web): return focus to the composer after closing a media previewf842ddf
Synced from GitHub

Thread transfer impact

✅ Thread transfer remains within every enforced ceiling.

ℹ️ No successful main baseline artifact is available yet. This run establishes the initial measurement.

ProviderMetricMain baselineThis PRImpactPR ceiling
CodexTotal thread wire—13.5 KiB—15.1 KiB✅
CodexThread snapshot wire—7.0 KiB—7.3 KiB✅
CodexLive turn WebSocket wire—6.5 KiB—7.8 KiB✅
CodexLive turn WebSocket decoded—57.0 KiB—66.4 KiB✅
CodexLive turn messages—8—21✅
ClaudeTotal thread wire—13.6 KiB—15.1 KiB✅
ClaudeThread snapshot wire—7.0 KiB—7.3 KiB✅
ClaudeLive turn WebSocket wire—6.5 KiB—7.8 KiB✅
ClaudeLive turn WebSocket decoded—57.8 KiB—66.4 KiB✅
ClaudeLive turn messages—9—21✅

Baseline: unavailable · PR result: f842ddf · Source CI: success

Scenario and decoded snapshot size

10 historical turns, 5 command tools per turn, 878.9 KiB retained MCP result per historical turn, and a 1.05 MiB retained result in the measured turn.

  • Codex decoded thread snapshot: 113.8 KiB
  • Claude decoded thread snapshot: 114.5 KiB

Updated in place by a trusted workflow. PR artifacts are strictly validated and never executed.

Synced from GitHub

Approvability

Verdict: Approved at f842ddf

Macroscope's review found this PR approvable — This is a focused one-file UI bug fix that restores focus after media-preview closure and reuses the existing composer focus-scope mechanism. Its runtime impact is limited to preview focus and composer expansion behavior, with no schema, security, billing, infrastructure, or default changes.

You can add or adjust custom eligibility rules. Learn more.