Skip to content

fix(desktop): keep floating composer on-screen, scoped to the thread area#50977

Merged
OutThisLife merged 4 commits into
mainfrom
bb/composer-fixed-portal
Jun 22, 2026
Merged

fix(desktop): keep floating composer on-screen, scoped to the thread area#50977
OutThisLife merged 4 commits into
mainfrom
bb/composer-fixed-portal

Conversation

@OutThisLife

@OutThisLife OutThisLife commented Jun 22, 2026

Copy link
Copy Markdown
Collaborator

Summary

Second fast-follow to the composer pop-out (#49488, bounds #50466). Users on the latest build still reported losing the floating composer off-screen even after #50466 made the bounds clamp account for the box size.

Root cause

The popped-out composer is position: fixed, but its ancestor — the chat content wrapper — sets contain: layout paint:

relative min-h-0 max-w-full flex-1 overflow-hidden ... contain-[layout_paint]

contain: layout/paint makes that element a containing block for fixed descendants and paint clips them to its box. So the floating composer was positioned/clipped relative to the chat column, not the viewport. That column resizes/shifts with the sidebars, so the viewport-based clamp measured a different rectangle than the box lived in, and anything outside the column got clipped → invisible. Matches the field report + the earlier "sidebars hidden" symptom.

Fix

  1. Escape the containing block. Render ChatBar as a sibling of the contained wrapper, inside the same ChatRuntimeBoundary (pure context, no DOM). Its nearest positioned ancestor becomes the outer relative isolate container (no transform/contain/filter), so docked stays absolute (identical placement) and floating fixed resolves against the viewport. The composer stays mounted across dock⇄float — no remount, no portal. (Verified PaneMain + the shell chain above ChatView use no transform/contain/filter.)
  2. Clamp to the thread area, not the window. The thread wrapper is tagged data-slot="composer-bounds"; the clamp confines the box to that rect (which already excludes a pinned sidebar and the header), falling back to the full window before it's measured. Threaded through every entry point: mount, resize, per-frame drag, peel-off, release-persist. This subsumes the old titlebar top-margin.
  3. Self-heal on load. On pop-out we re-clamp immediately and again on the next frame (after sidebar/font layout settles), persisting the corrected position — so anyone who loads in with a stranded position (saved on another monitor/layout, or from before this fix) is pulled back on-screen automatically. A degenerate pre-layout bounds rect is treated as unknown so the box is never clamped into a collapsed area.

Test plan

  • Pop out, open/close both sidebars → composer stays put and fully visible; never slides under a pinned sidebar.
  • Drag to each edge with sidebars open and hidden → stops at the thread edges in both.
  • Seed a far-off hermes.desktop.composerPopout.position in localStorage, reload → snaps back on-screen and the value is rewritten.
  • Toggle dock ⇄ float repeatedly with text in the editor → no remount; draft + caret preserved.
  • Docked placement/centering unchanged with sidebars open/closed.
  • Secondary window (Ctrl+Shift+N) / subagent view → composer stays docked.
  • macOS + Windows + WSLg.

Related: #50466, #49903.

… off-screen

The popped-out composer is position:fixed, but the chat content wrapper sets
`contain: layout paint`, which makes it a containing block for — and clips —
fixed descendants. Inline, the floating composer was positioned/clipped relative
to the chat column (which shifts with the sidebars), not the viewport, so the
viewport-based bounds clamp from #50466 couldn't keep it reachable: users still
lost it off-screen. Portal it to <body> when popped out so fixed positioning and
the clamp finally share the viewport as their reference. Docked stays inline
(it's absolute within the chat column by design).
@github-actions

github-actions Bot commented Jun 22, 2026

Copy link
Copy Markdown
Contributor

🔎 Lint report: bb/composer-fixed-portal vs origin/main

ruff

Total: 0 on HEAD, 0 on base (➖ 0)

🆕 New issues: none

✅ Fixed issues: none

Unchanged: 0 pre-existing issues carried over.

ty (type checker)

Total: 11435 on HEAD, 11433 on base (🆕 +2)

🆕 New issues (2):

Rule Count
unresolved-attribute 2
First entries
tests/run_agent/test_credits_notices_toggle.py:76: [unresolved-attribute] unresolved-attribute: Unresolved attribute `_credits_session_start_micros` on type `AIAgent`
run_agent.py:2984: [unresolved-attribute] unresolved-attribute: Object of type `Self@get_credits_spent_micros` has no attribute `_credits_session_start_micros`

✅ Fixed issues (1):

Rule Count
invalid-assignment 1
First entries
tests/run_agent/test_credits_notices_toggle.py:76: [invalid-assignment] invalid-assignment: Object of type `None` is not assignable to attribute `_credits_session_start_micros` of type `int`

Unchanged: 6018 pre-existing issues carried over.

Diagnostics are surfaced as warnings — this check never fails the build.

Replaces the body-portal approach: render ChatBar as a sibling of the
contain:[layout paint] chat wrapper (inside the same runtime boundary) rather
than portaling the floating instance to <body>. The wrapper is a containing
block for — and clips — position:fixed descendants, which is what stranded the
popped-out composer off-screen. As a sibling it anchors to the outer relative
container: docked stays absolute (identical placement), floating resolves
against the viewport. Both states stay mounted, so dock<->float no longer
remounts the editor (the portal toggle did).
@OutThisLife OutThisLife changed the title fix(desktop): portal floating composer to body (can't be clipped off-screen) fix(desktop): keep floating composer out of the contain wrapper (can't be clipped off-screen) Jun 22, 2026
…le window

Now that the popped-out composer is fixed to the viewport, clamping against the
window let it slide under a pinned sidebar. Confine it to the thread region
(data-slot="composer-bounds") instead — its rect already excludes a pinned
sidebar and the header — falling back to the full window before it's measured.
This subsumes the old titlebar top-margin (the thread rect starts below the
header).
Re-clamp once more on the next frame after pop-out so layout (sidebar widths,
fonts) has settled, and treat a degenerate pre-layout bounds rect as "unknown"
(fall back to the window) so we never clamp the box into a collapsed area. Net:
anyone who loads in with a stranded position is pulled back on-screen and the
fix is persisted, even if the first measure was premature.
@OutThisLife OutThisLife changed the title fix(desktop): keep floating composer out of the contain wrapper (can't be clipped off-screen) fix(desktop): keep floating composer on-screen, scoped to the thread area Jun 22, 2026
@OutThisLife OutThisLife merged commit 7dece1d into main Jun 22, 2026
35 checks passed
@OutThisLife OutThisLife deleted the bb/composer-fixed-portal branch June 22, 2026 19:12
@alt-glitch alt-glitch added type/bug Something isn't working comp/tui Terminal UI (ui-tui/ + tui_gateway/) P3 Low — cosmetic, nice to have labels Jun 22, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

comp/tui Terminal UI (ui-tui/ + tui_gateway/) P3 Low — cosmetic, nice to have type/bug Something isn't working

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants