From 7694428ca9ca03a1b3b6929f3b994433a6b29869 Mon Sep 17 00:00:00 2001 From: Danny Avila Date: Thu, 13 Aug 2026 23:37:18 -0400 Subject: [PATCH] =?UTF-8?q?=F0=9F=92=AC=20style:=20Right-Align=20In-Flight?= =?UTF-8?q?=20Steer=20Bubbles=20to=20the=20Message=20UI=20(#14814)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * ๐Ÿ’ฌ style: Right-Align In-Flight Steer Bubbles to the Message UI The chat surface reads as message bubbles now โ€” user turns on the right, assistant turns on the left โ€” but the in-flight steer bubbles anchored above the composer were still left-aligned, so a steer sat on the opposite side from the words the user had just sent, then jumped across on `on_steer_applied` when the persisted `SteerPart` landed in-thread on the right. Align the overlay with the user turn it belongs to: - The bubble stack right-aligns and is constrained to the message column (`max-w-3xl`), so the in-flight bubble sits where its applied twin lands instead of ~52px further right (the composer runs wider than the message column at `xl`). - The bubble adopts the same theme-token geometry as `SteerPart` and every user turn (`rounded-theme-surface rounded-br-theme-control`, `px-theme-normal`), replacing the raw `rounded-3xl`/`pl-3 pr-4`. It keeps its outline: an in-flight steer is still provisional. - The controls flank the bubble โ€” overflow menu outboard-left, send-now arrow outboard-right โ€” so neither reads as belonging to the other. DOM order matches visual order, so focus order stays coherent. Also drops the thin `bg-border-medium` divider that bound the arrow to its message: with the arrow now outboard on the far side of the bubble it has nothing to separate, and `EscalateNowButton` no longer needs its fragment. * ๐Ÿ’ฌ style: Center the Steer Controls on the Bubble's First Line The flanking controls read as neither top-aligned nor centered, because their resting position was an accident of `sticky top-2`: the topmost rail trips the sticky inset at rest and is shoved 8px below the row top, while every rail below it clears the inset and stays at the top. So the controls sat 3.8px above the bubble's centre โ€” and stacked steers did not even agree with each other. Give each rail a `py-3` band that reproduces the bubble's own first line (its `py-2.5`, its 1px border, and half the gap between the 24px control and the taller text line box), and pad the overlay evenly so the topmost rail already clears the sticky inset instead of being displaced by it. A 24px control now centres on the first line: measured at 722.0 against the text's 721.8, versus 718.4 before. Beside a one-line steer that reads as centred; on a tall one it aligns to the opening line rather than drifting to the middle, and sticky still carries it while the stack scrolls. --- .../components/Chat/Input/InFlightSteers.tsx | 82 +++++++++++-------- .../src/components/Chat/Input/SteerMenu.tsx | 71 +++++++--------- 2 files changed, 81 insertions(+), 72 deletions(-) diff --git a/client/src/components/Chat/Input/InFlightSteers.tsx b/client/src/components/Chat/Input/InFlightSteers.tsx index 456c7ea0b4..c5e374b9d6 100644 --- a/client/src/components/Chat/Input/InFlightSteers.tsx +++ b/client/src/components/Chat/Input/InFlightSteers.tsx @@ -52,6 +52,17 @@ const STEER_OVERFLOW_TOLERANCE = 8; * honest unknown outcome; the idempotent arm may still complete server-side. */ const ARM_CONFIRM_TIMEOUT_MS = 10_000; +/** The control rail flanking a bubble. `py-3` reproduces the bubble's own + * first-line band โ€” its `py-2.5` padding, its 1px border, and half the gap + * between the 24px control and the taller text line box โ€” so a 24px control + * centers on the first line: visually centered beside a one-line steer, and + * aligned to the opening line of a tall one rather than adrift in its middle. + * `sticky` then keeps it in view while a tall steer scrolls past (the stack + * scrolls once it passes 35vh); the matching `pt-2` on the overlay means the + * topmost rail already clears the sticky inset, so it is not shoved down at + * rest while the rails below it โ€” which never trip the inset โ€” stay put. */ +const STEER_CONTROL_RAIL = 'sticky top-2 flex shrink-0 items-center py-3'; + type ArmFailure = { name?: string; response?: { data?: { code?: string } }; @@ -403,10 +414,10 @@ const InFlightSteer = memo(function InFlightSteer({ /* pointer-events-auto: the overlay container disables events so wheeling * over the gaps reaches the messages behind; each bubble re-enables them * for its own controls and internal scroll. */ - className="group pointer-events-auto flex flex-col items-start gap-1.5" + className="group pointer-events-auto flex flex-col items-end gap-1.5" > {(images.length > 0 || others.length > 0) && ( -
+
{others.map((file) => ( )} - {/* items-start so the sticky controls have room to travel โ€” see below. */} + {/* Mirrors the user turn: the whole group hugs the right edge like every + * other message the user wrote. The two controls flank the bubble rather + * than stacking beside each other โ€” the overflow menu outboard-left, the + * send-now arrow outboard-right โ€” so neither can read as belonging to + * the other, and the pairing repeats cleanly when steers stack. + * items-start so the sticky controls have room to travel โ€” see below. */}
+ {!sending && ( + /* One always-visible affordance: a label-less menu hidden until hover + * is undiscoverable, and edit/queue/cancel all live inside it now, so + * the menu shows at rest on every pointer (matching the always-on + * controls on the queued rows). `sticky` keeps it in view while the + * user scrolls through a tall, expanded steer (the stack scrolls once + * it passes 35vh). */ +
+ +
+ )}
@@ -493,32 +528,15 @@ const InFlightSteer = memo(function InFlightSteer({ )}
- {!sending && ( - /* One always-visible affordance: a label-less menu hidden until hover - * is undiscoverable, and edit/queue/cancel all live inside it now, so - * the menu shows at rest on every pointer (matching the always-on - * controls on the queued rows). `sticky` keeps it in view while the - * user scrolls through a tall, expanded steer (the stack scrolls once - * it passes 35vh). */ -
- {!preempting && ( - - )} - +
)} @@ -622,7 +640,7 @@ const InFlightSteers = memo(function InFlightSteers({ * steer runs to 16k chars and a run takes up to 10 of them; unbounded it * would cover the whole thread. pointer-events-none lets wheeling over * the gaps reach those messages (each bubble opts back in). */ - className="pointer-events-none absolute inset-x-0 bottom-full flex max-h-[35vh] flex-col items-start gap-2 overflow-y-auto px-2 pb-2" + className="pointer-events-none absolute inset-x-0 bottom-full mx-auto flex max-h-[35vh] max-w-3xl flex-col items-end gap-2 overflow-y-auto p-2" > {inFlight.map((steer) => ( - {/* Thin divider binds the arrow to the message on its left, so it can - * never read as part of the bare-glyph menu control beside it โ€” the - * send-now belongs to THIS chip, and the pairing repeats cleanly when - * several messages stack. */} -