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. */} -