fix(composer): keep strip edge fades live when pill content grows after a resize

The overflow-fade hook resize-observes the scroller and its content
wrapper, but the wrapper was a min-w-0 flex item: under a narrow
container it collapsed to the scroller's content width, so the shrink-0
pill slots overflowed it invisibly. When a pill's deferred label ticker
swapped in a longer label AFTER the resize settled, scrollWidth grew
without resizing any observed element — no ResizeObserver callback, and
the edge fades stayed stale (missing) until a manual scroll. Caught by
the "narrow strip scrolls horizontally with edge fades" e2e spec, which
failed deterministically because label swaps are intentionally deferred
until slot layout settles.

- BotActivityBar.tsx: give the scroller's content wrapper min-w-max on a
  multi-item strip so its border box always tracks content size and every
  late growth fires the ResizeObserver; keep min-w-0 for the lone-item
  case so a single pill/typing group still shrinks to fit (shrinkToFit)
  instead of overflowing into scroll
- Side effect: pills no longer visually compress below their natural
  width inside a constrained strip (the collapsed wrapper was squeezing
  them); they keep readable widths and the strip scrolls as designed
- Document the min-w-max dependency at the ResizeObserver in
  useStripOverflowFades so the wrapper's sizing isn't "simplified" back
  into a stale-fade regression

Verified: all 13 activity/pill/typing specs in channels.spec.ts pass
(including the previously failing narrow-strip fade spec and the new
typing-inside-strip spec), plus both channel-composer-overflow specs.

Co-authored-by: Taylor Ho <taylorkmho@gmail.com>
Signed-off-by: Taylor Ho <taylorkmho@gmail.com>
This commit is contained in:
Taylor Ho
2026-08-05 16:42:00 -07:00
committed by Carl
parent 690cc8f753
commit c320cedfcb
@@ -105,7 +105,11 @@ function useStripOverflowFades(
const observer = new ResizeObserver(updateFades);
observer.observe(node);
// The content wrapper is the scroller's only child; observing it catches
// overflow changes that don't touch the scroller's own box.
// overflow changes that don't touch the scroller's own box. This relies
// on the wrapper being min-w-max (multi-item strip): if it could
// collapse to the scroller's width, content growing INSIDE it (label
// tickers swap after a resize settles) would change scrollWidth without
// resizing any observed element, leaving the fades stale.
const content = node.firstElementChild;
if (content !== null) {
observer.observe(content);
@@ -602,7 +606,19 @@ export function BotActivityComposerAction({
onScroll={updateFades}
ref={scrollerRef}
>
<div className="flex min-w-0 items-center gap-1.5">
<div
// The overflow-fade hook resize-observes this wrapper as its proxy
// for content size. min-w-max keeps it from collapsing to the
// scroller's width (it is a shrinkable flex item), so late content
// growth — label tickers swap AFTER a resize settles — still
// changes an observed box and refreshes the fades. A lone item
// instead gets min-w-0 so it can shrink with the container
// (shrinkToFit) rather than overflow into scroll.
className={cn(
"flex items-center gap-1.5",
itemCount === 1 ? "min-w-0" : "min-w-max",
)}
>
<AnimatePresence initial={false}>
{orderedAgents.map((agent) => (
<AnimatedPillSlot