mirror of
https://github.com/block/buzz.git
synced 2026-08-18 06:50:31 +02:00
feat(composer): ship live activity by default and scroll the pill strip when narrow
Remove the composerLiveActivity preview flag so the live-activity hover
preview ships for everyone, and land the pill strip overflow behavior
for narrow containers.
Flag removal (ships by default):
- preview-features.json: drop the composerLiveActivity entry — absence
from the manifest means stable/always-on; E2E preview seeding and the
Settings > Experiments card both derive from the manifest, so they
pick this up automatically
- BotActivityBar.tsx: remove the useFeatureEnabled gate and the legacy
flag-off "View activity" popover item; the pill hover popover now
always renders ComposerLiveActivityFeed
- Remove the now-dead openAgentSessionPubkey plumbing through
BotActivityAgentPill / BotActivityComposerAction /
ChannelComposerActivityRow / ChannelPane — it only backed the legacy
item's active-session highlight
- Update stale flag references in ComposerLiveActivityFeed's doc
comment, the agents debug harness README, and a channels.spec.ts
comment
Pill strip overflow (BotActivityBar.tsx, ChannelComposerActivityRow.tsx):
- With several pills in a narrow container (thread panel toolbars
especially), pills keep their natural label-truncated width and the
strip scrolls horizontally — scrollbar hidden, edge gradient fades
signalling clipped pills — instead of compressing every pill into an
unreadable sliver
- New useStripOverflowFades hook tracks scroll position plus
scroller/content resizes (ResizeObserver) so fades appear and
disappear without a re-render triggering event
- A lone pill shrinks to fit (min-w-0) instead of scrolling — an edge
fade over a single pill reads as a cut-off bug, not an affordance
- layoutScroll on the scroller keeps motion's slot layout animations
measured relative to the scroll offset
- e2e: add "narrow strip scrolls horizontally with edge fades instead
of compressing pills" and "lone pill shrinks to fit a narrow
container without scroll fades"
Verified: tsc, biome check, desktop unit tests (3651 pass), and the
pill/strip/live-activity e2e subset. Two pre-existing channels.spec.ts
failures ("composer does not shift when the activity row mounts and
clears", "hovering the bar itself freezes pill order without opening a
card") reproduce identically on the parent commit with a clean tree —
unrelated to this change.
Co-authored-by: Taylor Ho <taylorkmho@gmail.com>
Signed-off-by: Taylor Ho <taylorkmho@gmail.com>
This commit is contained in:
@@ -1,5 +1,4 @@
|
||||
import * as React from "react";
|
||||
import { Loader2 } from "lucide-react";
|
||||
import { AnimatePresence, motion, useReducedMotion } from "motion/react";
|
||||
|
||||
import {
|
||||
@@ -9,7 +8,6 @@ import {
|
||||
import { useAgentTranscript } from "@/features/agents/ui/useObserverEvents";
|
||||
import type { UserProfileLookup } from "@/features/profile/lib/identity";
|
||||
import type { ManagedAgent } from "@/shared/api/types";
|
||||
import { useFeatureEnabled } from "@/shared/features";
|
||||
import { cn } from "@/shared/lib/cn";
|
||||
import { useNow } from "@/shared/lib/useNow";
|
||||
import { Popover, PopoverContent, PopoverTrigger } from "@/shared/ui/popover";
|
||||
@@ -27,7 +25,6 @@ type BotActivityBarProps = {
|
||||
agents: BotActivityAgent[];
|
||||
channelId?: string | null;
|
||||
onOpenAgentSession: (pubkey: string, channelId?: string | null) => void;
|
||||
openAgentSessionPubkey: string | null;
|
||||
profiles?: UserProfileLookup;
|
||||
workingBotPubkeys: string[];
|
||||
};
|
||||
@@ -78,6 +75,52 @@ type StripHoverPopover = {
|
||||
scheduleClose: () => void;
|
||||
};
|
||||
|
||||
/**
|
||||
* Edge-fade state for the horizontally scrollable pill strip: which sides
|
||||
* currently have pills clipped out of view. Tracks the scroller's scroll
|
||||
* position plus size changes of both the scroller (container narrows —
|
||||
* thread panel resize) and its content wrapper (pills entering, leaving, or
|
||||
* relabeling to a different width), so the fades appear and disappear
|
||||
* without a re-render triggering event.
|
||||
*/
|
||||
function useStripOverflowFades(
|
||||
scrollerRef: React.RefObject<HTMLDivElement | null>,
|
||||
) {
|
||||
const [fades, setFades] = React.useState({ end: false, start: false });
|
||||
|
||||
const updateFades = React.useCallback(() => {
|
||||
const node = scrollerRef.current;
|
||||
if (node === null) {
|
||||
return;
|
||||
}
|
||||
// 1px slack absorbs sub-pixel rounding in scrollWidth/clientWidth.
|
||||
const start = node.scrollLeft > 1;
|
||||
const end = node.scrollLeft + node.clientWidth < node.scrollWidth - 1;
|
||||
setFades((current) =>
|
||||
current.start === start && current.end === end ? current : { end, start },
|
||||
);
|
||||
}, [scrollerRef]);
|
||||
|
||||
React.useEffect(() => {
|
||||
const node = scrollerRef.current;
|
||||
if (node === null) {
|
||||
return;
|
||||
}
|
||||
updateFades();
|
||||
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.
|
||||
const content = node.firstElementChild;
|
||||
if (content !== null) {
|
||||
observer.observe(content);
|
||||
}
|
||||
return () => observer.disconnect();
|
||||
}, [scrollerRef, updateFades]);
|
||||
|
||||
return { fades, updateFades };
|
||||
}
|
||||
|
||||
function useStripHoverPopover(): StripHoverPopover {
|
||||
const [activePubkey, setActivePubkey] = React.useState<string | null>(null);
|
||||
// Ref mirror so pointer handlers can branch on open-vs-closed synchronously.
|
||||
@@ -158,9 +201,7 @@ function useStripHoverPopover(): StripHoverPopover {
|
||||
* out — deferred until the pill's slot settles when a reorder is in flight.
|
||||
* Hovering shows the agent's live activity feed as the popover surface
|
||||
* itself — flat, no inset box, no tab strip — while clicking the pill opens
|
||||
* the agent's full runtime in the auxiliary panel. With the
|
||||
* `composerLiveActivity` preview flag off, the hover popover keeps the
|
||||
* legacy "View activity" item instead.
|
||||
* the agent's full runtime in the auxiliary panel.
|
||||
*
|
||||
* Only observer-backed agents reach this pill: typing-fallback-only agents
|
||||
* are diverted into the combined typing indicator group by
|
||||
@@ -172,9 +213,7 @@ function BotActivityAgentPill({
|
||||
channelId,
|
||||
holdLabelSwap,
|
||||
hover,
|
||||
liveActivityEnabled,
|
||||
onOpenAgentSession,
|
||||
openAgentSessionPubkey,
|
||||
pinWidth,
|
||||
profiles,
|
||||
}: {
|
||||
@@ -185,9 +224,7 @@ function BotActivityAgentPill({
|
||||
holdLabelSwap: boolean;
|
||||
/** Strip-level hover popover state shared by every pill. */
|
||||
hover: StripHoverPopover;
|
||||
liveActivityEnabled: boolean;
|
||||
onOpenAgentSession: (pubkey: string, channelId?: string | null) => void;
|
||||
openAgentSessionPubkey: string | null;
|
||||
/** Freeze the pill's rendered width (a hover card is showing). */
|
||||
pinWidth: boolean;
|
||||
profiles?: UserProfileLookup;
|
||||
@@ -223,8 +260,6 @@ function BotActivityAgentPill({
|
||||
: { id: activeId, label: activeLabel },
|
||||
);
|
||||
}, [holdLabelSwap, activeId, activeLabel]);
|
||||
const isSessionOpen =
|
||||
openAgentSessionPubkey?.toLowerCase() === agent.pubkey.toLowerCase();
|
||||
|
||||
// While a hover card is showing, the pill must not resize: label swaps
|
||||
// keep animating, but a longer/shorter label truncating inside a FROZEN
|
||||
@@ -322,9 +357,7 @@ function BotActivityAgentPill({
|
||||
</PopoverTrigger>
|
||||
<PopoverContent
|
||||
align="start"
|
||||
className={cn(
|
||||
liveActivityEnabled ? "w-80 overflow-hidden p-0" : "w-64 p-1",
|
||||
)}
|
||||
className="w-80 overflow-hidden p-0"
|
||||
onCloseAutoFocus={(event) => {
|
||||
// A hover preview must not yank focus back to the trigger on
|
||||
// close: the trigger's focus handler would re-open the card.
|
||||
@@ -336,39 +369,13 @@ function BotActivityAgentPill({
|
||||
side="top"
|
||||
sideOffset={8}
|
||||
>
|
||||
{liveActivityEnabled ? (
|
||||
<ComposerLiveActivityFeed
|
||||
agent={agent}
|
||||
channelId={channelId}
|
||||
className="h-48 rounded-[inherit]"
|
||||
onOpenAgentSession={() => openSession()}
|
||||
profiles={profiles}
|
||||
/>
|
||||
) : (
|
||||
<button
|
||||
className={cn(
|
||||
"flex w-full items-center gap-2 rounded-lg px-2 py-1.5 text-left text-sm transition-colors",
|
||||
isSessionOpen
|
||||
? "bg-primary/10 text-primary"
|
||||
: "text-foreground hover:bg-accent hover:text-accent-foreground",
|
||||
)}
|
||||
data-testid={`bot-activity-composer-item-${agent.pubkey}`}
|
||||
onClick={openSession}
|
||||
type="button"
|
||||
>
|
||||
<UserAvatar
|
||||
avatarUrl={avatarUrl}
|
||||
className="shrink-0"
|
||||
displayName={agent.name}
|
||||
size="sm"
|
||||
/>
|
||||
<span className="min-w-0 flex-1 truncate">{agent.name}</span>
|
||||
<span className="shrink-0 whitespace-nowrap text-xs font-medium opacity-80">
|
||||
View activity
|
||||
</span>
|
||||
<Loader2 className="h-4 w-4 shrink-0 animate-spin text-muted-foreground/70" />
|
||||
</button>
|
||||
)}
|
||||
<ComposerLiveActivityFeed
|
||||
agent={agent}
|
||||
channelId={channelId}
|
||||
className="h-48 rounded-[inherit]"
|
||||
onOpenAgentSession={() => openSession()}
|
||||
profiles={profiles}
|
||||
/>
|
||||
</PopoverContent>
|
||||
</Popover>
|
||||
);
|
||||
@@ -387,12 +394,19 @@ function AnimatedPillSlot({
|
||||
children,
|
||||
freezeLayout,
|
||||
shouldReduceMotion,
|
||||
shrinkToFit,
|
||||
}: {
|
||||
/** Render prop so the pill can defer label swaps while its slot moves. */
|
||||
children: (isMoving: boolean) => React.ReactNode;
|
||||
/** Disable slot layout animation while a hover card is showing. */
|
||||
freezeLayout: boolean;
|
||||
shouldReduceMotion: boolean;
|
||||
/**
|
||||
* Lone pill: shrink with the container (label ellipsizes) instead of
|
||||
* overflowing into scroll — an edge fade over a single pill reads as a
|
||||
* cut-off bug, not an affordance.
|
||||
*/
|
||||
shrinkToFit: boolean;
|
||||
}) {
|
||||
const [isMoving, setIsMoving] = React.useState(false);
|
||||
|
||||
@@ -408,7 +422,12 @@ function AnimatedPillSlot({
|
||||
return (
|
||||
<motion.div
|
||||
animate={{ opacity: isMoving ? [1, 0.35, 1] : 1, scale: 1 }}
|
||||
className="flex min-w-0"
|
||||
// With several pills, shrink-0 keeps each at its natural
|
||||
// (label-truncated) width so a strip that outgrows a narrow container
|
||||
// scrolls — with the edge fade signalling the pills off view —
|
||||
// instead of compressing every pill into an unreadable sliver. A lone
|
||||
// pill instead shrinks to fit (min-w-0) so it never scrolls.
|
||||
className={cn("flex", shrinkToFit ? "min-w-0" : "shrink-0")}
|
||||
exit={{ opacity: 0, scale: 0.9 }}
|
||||
initial={shouldReduceMotion ? false : { opacity: 0, scale: 0.9 }}
|
||||
layout={!shouldReduceMotion && !freezeLayout}
|
||||
@@ -441,18 +460,21 @@ function AnimatedPillSlot({
|
||||
* slot as the order changes — except while the cursor is over the bar or a
|
||||
* hover card is showing, when order, membership, layout animation, and pill
|
||||
* widths are all frozen so nothing can move out from under (or slide under)
|
||||
* the cursor. Pills shrink and truncate their labels when several agents
|
||||
* work at once so the strip never wraps the fixed-height row.
|
||||
* the cursor. Each pill truncates its label at its own max width. A lone
|
||||
* pill shrinks with a narrow container (its label ellipsizes) so it always
|
||||
* fits; when SEVERAL pills outgrow a narrow container (thread panel
|
||||
* toolbars especially) the strip scrolls horizontally — scrollbar hidden,
|
||||
* with edge gradient fades signalling the pills off view — instead of
|
||||
* compressing every pill into an unreadable sliver or wrapping the
|
||||
* fixed-height row.
|
||||
*/
|
||||
export function BotActivityComposerAction({
|
||||
agents,
|
||||
channelId = null,
|
||||
onOpenAgentSession,
|
||||
openAgentSessionPubkey,
|
||||
profiles,
|
||||
workingBotPubkeys,
|
||||
}: BotActivityBarProps) {
|
||||
const liveActivityEnabled = useFeatureEnabled("composerLiveActivity");
|
||||
const shouldReduceMotion = useReducedMotion();
|
||||
const hover = useStripHoverPopover();
|
||||
// The freeze engages as soon as the cursor is anywhere over the bar — not
|
||||
@@ -463,6 +485,9 @@ export function BotActivityComposerAction({
|
||||
const [barHovered, setBarHovered] = React.useState(false);
|
||||
const holdActive = barHovered || hover.activePubkey !== null;
|
||||
|
||||
const scrollerRef = React.useRef<HTMLDivElement | null>(null);
|
||||
const { fades, updateFades } = useStripOverflowFades(scrollerRef);
|
||||
|
||||
const workingAgents = React.useMemo(() => {
|
||||
const workingSet = new Set(
|
||||
workingBotPubkeys.map((pubkey) => pubkey.toLowerCase()),
|
||||
@@ -530,37 +555,68 @@ export function BotActivityComposerAction({
|
||||
return (
|
||||
// biome-ignore lint/a11y/noStaticElementInteractions: hover-only hold — keyboard focus drives the same hold via the pill triggers.
|
||||
<div
|
||||
className="flex min-w-0 flex-1 items-center gap-1.5 overflow-visible"
|
||||
className="relative min-w-0 flex-1"
|
||||
data-testid="bot-activity-strip"
|
||||
onMouseEnter={() => setBarHovered(true)}
|
||||
onMouseLeave={() => setBarHovered(false)}
|
||||
>
|
||||
<AnimatePresence initial={false}>
|
||||
{orderedAgents.map((agent) => (
|
||||
<AnimatedPillSlot
|
||||
freezeLayout={holdActive}
|
||||
key={agent.pubkey}
|
||||
shouldReduceMotion={Boolean(shouldReduceMotion)}
|
||||
>
|
||||
{(isMoving) => (
|
||||
<BotActivityAgentPill
|
||||
agent={agent}
|
||||
avatarUrl={
|
||||
profiles?.[agent.pubkey.toLowerCase()]?.avatarUrl ?? null
|
||||
}
|
||||
channelId={channelId}
|
||||
holdLabelSwap={isMoving}
|
||||
hover={hover}
|
||||
liveActivityEnabled={liveActivityEnabled}
|
||||
onOpenAgentSession={onOpenAgentSession}
|
||||
openAgentSessionPubkey={openAgentSessionPubkey}
|
||||
pinWidth={holdActive}
|
||||
profiles={profiles}
|
||||
/>
|
||||
)}
|
||||
</AnimatedPillSlot>
|
||||
))}
|
||||
</AnimatePresence>
|
||||
{/* layoutScroll keeps the slot layout animations correct while the
|
||||
strip is scrolled — motion measures positions relative to the
|
||||
scroll offset instead of jumping. The negative-margin/padding pair
|
||||
gives focus rings and pill shadows room inside the clip box
|
||||
without changing the row's height. */}
|
||||
<motion.div
|
||||
className="-my-1 flex items-center overflow-x-auto overscroll-x-contain py-1 scrollbar-none [&::-webkit-scrollbar]:hidden"
|
||||
data-testid="bot-activity-strip-scroller"
|
||||
layoutScroll
|
||||
onScroll={updateFades}
|
||||
ref={scrollerRef}
|
||||
>
|
||||
<div className="flex min-w-0 items-center gap-1.5">
|
||||
<AnimatePresence initial={false}>
|
||||
{orderedAgents.map((agent) => (
|
||||
<AnimatedPillSlot
|
||||
freezeLayout={holdActive}
|
||||
key={agent.pubkey}
|
||||
shouldReduceMotion={Boolean(shouldReduceMotion)}
|
||||
shrinkToFit={orderedAgents.length === 1}
|
||||
>
|
||||
{(isMoving) => (
|
||||
<BotActivityAgentPill
|
||||
agent={agent}
|
||||
avatarUrl={
|
||||
profiles?.[agent.pubkey.toLowerCase()]?.avatarUrl ?? null
|
||||
}
|
||||
channelId={channelId}
|
||||
holdLabelSwap={isMoving}
|
||||
hover={hover}
|
||||
onOpenAgentSession={onOpenAgentSession}
|
||||
pinWidth={holdActive}
|
||||
profiles={profiles}
|
||||
/>
|
||||
)}
|
||||
</AnimatedPillSlot>
|
||||
))}
|
||||
</AnimatePresence>
|
||||
</div>
|
||||
</motion.div>
|
||||
{/* Edge fades: obvious "more pills off view" affordance. Rendered only
|
||||
for a side that actually has clipped content, so they never dim a
|
||||
fully visible strip. */}
|
||||
{fades.start ? (
|
||||
<div
|
||||
aria-hidden
|
||||
className="pointer-events-none absolute inset-y-0 left-0 z-10 w-8 bg-linear-to-r from-background via-background/60 to-transparent"
|
||||
data-testid="bot-activity-strip-fade-start"
|
||||
/>
|
||||
) : null}
|
||||
{fades.end ? (
|
||||
<div
|
||||
aria-hidden
|
||||
className="pointer-events-none absolute inset-y-0 right-0 z-10 w-8 bg-linear-to-l from-background via-background/60 to-transparent"
|
||||
data-testid="bot-activity-strip-fade-end"
|
||||
/>
|
||||
) : null}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -35,7 +35,6 @@ export function ChannelComposerActivityRow({
|
||||
channel,
|
||||
currentPubkey,
|
||||
onOpenAgentSession,
|
||||
openAgentSessionPubkey,
|
||||
profiles,
|
||||
typingPubkeys,
|
||||
}: {
|
||||
@@ -43,7 +42,6 @@ export function ChannelComposerActivityRow({
|
||||
channel: Channel | null;
|
||||
currentPubkey?: string;
|
||||
onOpenAgentSession: (pubkey: string, channelId?: string | null) => void;
|
||||
openAgentSessionPubkey: string | null;
|
||||
profiles?: UserProfileLookup;
|
||||
typingPubkeys: string[];
|
||||
}) {
|
||||
@@ -113,16 +111,17 @@ export function ChannelComposerActivityRow({
|
||||
data-testid="channel-composer-activity-row"
|
||||
>
|
||||
<div className="flex h-full w-full items-center gap-3 overflow-visible">
|
||||
{/* The pill strip sizes to its content (shrinkable when the row gets
|
||||
tight); the typing group takes whatever is left so it sits
|
||||
directly after the pills instead of splitting the row 50/50. */}
|
||||
{/* The pill strip sizes to its content; when the row gets tight it
|
||||
scrolls horizontally (edge fades signal clipped pills) rather
|
||||
than compressing. The typing group takes whatever is left so it
|
||||
sits directly after the pills instead of splitting the row
|
||||
50/50. */}
|
||||
{observerWorkingPubkeys.length > 0 ? (
|
||||
<div className="flex min-w-0 overflow-visible">
|
||||
<BotActivityComposerAction
|
||||
agents={agents}
|
||||
channelId={channelId}
|
||||
onOpenAgentSession={onOpenAgentSession}
|
||||
openAgentSessionPubkey={openAgentSessionPubkey}
|
||||
profiles={profiles}
|
||||
workingBotPubkeys={observerWorkingPubkeys}
|
||||
/>
|
||||
|
||||
@@ -804,7 +804,6 @@ export const ChannelPane = React.memo(function ChannelPane({
|
||||
channel={activeChannel}
|
||||
currentPubkey={currentPubkey}
|
||||
onOpenAgentSession={onOpenAgentSession}
|
||||
openAgentSessionPubkey={openAgentSessionPubkey}
|
||||
profiles={profiles}
|
||||
typingPubkeys={typingPubkeys}
|
||||
/>
|
||||
@@ -902,7 +901,6 @@ export const ChannelPane = React.memo(function ChannelPane({
|
||||
agents={activityAgents}
|
||||
channelId={activeChannel?.id ?? null}
|
||||
onOpenAgentSession={onOpenAgentSession}
|
||||
openAgentSessionPubkey={openAgentSessionPubkey}
|
||||
profiles={profiles}
|
||||
workingBotPubkeys={threadComposerBotTypingPubkeys}
|
||||
/>
|
||||
|
||||
@@ -16,7 +16,7 @@ import { deriveLastLiveAt } from "./composerLiveActivity";
|
||||
|
||||
/**
|
||||
* Single-agent live activity preview for the composer "agents working"
|
||||
* popover. Preview-gated behind the `composerLiveActivity` feature.
|
||||
* popover.
|
||||
*
|
||||
* Renders the selected working agent's channel-scoped transcript with the
|
||||
* same compact primitive as the profile activity embed
|
||||
|
||||
@@ -2014,8 +2014,7 @@ test("shows and clears activity indicators for active channel agents", async ({
|
||||
const activityPill = page.getByTestId("bot-activity-composer-trigger");
|
||||
await expect(activityPill).toBeVisible();
|
||||
await expect(activityPill).not.toContainText("View activity");
|
||||
// E2E seeds every preview feature on, so the composerLiveActivity path is
|
||||
// what renders here: hovering the pill opens the live-activity preview.
|
||||
// Hovering the pill opens the live-activity preview.
|
||||
await activityPill.hover();
|
||||
await expect(page.getByTestId("composer-live-activity-feed")).toBeVisible();
|
||||
// Clicking the pill promotes the agent's runtime into the aux panel.
|
||||
@@ -2364,6 +2363,98 @@ test("hovering across pills switches a single live-activity card", async ({
|
||||
);
|
||||
});
|
||||
|
||||
test("narrow strip scrolls horizontally with edge fades instead of compressing pills", async ({
|
||||
page,
|
||||
}) => {
|
||||
await installMockBridge(page, { managedAgents: PILL_AGENT_SEEDS });
|
||||
await openAgentsChannelWithTwoWorkingPills(page);
|
||||
|
||||
// Constrain the activity row directly (instead of resizing the whole
|
||||
// window) so the overflow condition is deterministic regardless of the
|
||||
// app's responsive layout — this mirrors a narrow thread panel, where the
|
||||
// same strip renders in the thread composer toolbar.
|
||||
await page
|
||||
.getByTestId("channel-composer-activity-row")
|
||||
.evaluate((element) => {
|
||||
element.style.width = "300px";
|
||||
});
|
||||
|
||||
const scroller = page.getByTestId("bot-activity-strip-scroller");
|
||||
await expect
|
||||
.poll(() => scroller.evaluate((el) => el.scrollWidth - el.clientWidth))
|
||||
.toBeGreaterThan(0);
|
||||
|
||||
// Pills keep a readable natural width instead of compressing into
|
||||
// slivers to fit.
|
||||
const triggers = page.getByTestId("bot-activity-composer-trigger");
|
||||
const firstBox = await triggers.first().boundingBox();
|
||||
expect(firstBox?.width ?? 0).toBeGreaterThan(120);
|
||||
|
||||
// At rest the strip is pinned to the start: clipped content (and its
|
||||
// fade affordance) on the trailing side only.
|
||||
await expect(page.getByTestId("bot-activity-strip-fade-end")).toBeVisible();
|
||||
await expect(page.getByTestId("bot-activity-strip-fade-start")).toHaveCount(
|
||||
0,
|
||||
);
|
||||
|
||||
// Scrolling to the far end swaps the fades to the leading side.
|
||||
await scroller.evaluate((el) => {
|
||||
el.scrollLeft = el.scrollWidth;
|
||||
});
|
||||
await expect(page.getByTestId("bot-activity-strip-fade-start")).toBeVisible();
|
||||
await expect(page.getByTestId("bot-activity-strip-fade-end")).toHaveCount(0);
|
||||
});
|
||||
|
||||
test("lone pill shrinks to fit a narrow container without scroll fades", async ({
|
||||
page,
|
||||
}) => {
|
||||
await installMockBridge(page, { managedAgents: PILL_AGENT_SEEDS });
|
||||
await page.goto("/");
|
||||
await page.getByTestId("channel-agents").click();
|
||||
await expect(page.getByTestId("chat-title")).toHaveText("agents");
|
||||
await page.waitForFunction(
|
||||
() => typeof window.__BUZZ_E2E_SEED_ACTIVE_TURNS__ === "function",
|
||||
null,
|
||||
{ timeout: 10_000 },
|
||||
);
|
||||
await page.evaluate(
|
||||
({ agentPubkey, channelId }) => {
|
||||
window.__BUZZ_E2E_SEED_ACTIVE_TURNS__?.({
|
||||
agentPubkey,
|
||||
channelId,
|
||||
turnId: "lone-pill-turn",
|
||||
});
|
||||
},
|
||||
{ agentPubkey: PILL_AGENT_NOVA, channelId: AGENTS_CHANNEL_ID },
|
||||
);
|
||||
|
||||
const trigger = page.getByTestId("bot-activity-composer-trigger");
|
||||
await expect(trigger).toHaveCount(1);
|
||||
|
||||
// Narrower than the pill's natural width: a lone pill must shrink and
|
||||
// ellipsize rather than overflow into scroll — an edge fade over a single
|
||||
// pill reads as a cut-off bug, not a "more pills off view" affordance.
|
||||
const row = page.getByTestId("channel-composer-activity-row");
|
||||
await row.evaluate((element) => {
|
||||
element.style.width = "160px";
|
||||
});
|
||||
|
||||
const scroller = page.getByTestId("bot-activity-strip-scroller");
|
||||
await expect
|
||||
.poll(() => scroller.evaluate((el) => el.scrollWidth - el.clientWidth))
|
||||
.toBe(0);
|
||||
await expect(page.getByTestId("bot-activity-strip-fade-end")).toHaveCount(0);
|
||||
await expect(page.getByTestId("bot-activity-strip-fade-start")).toHaveCount(
|
||||
0,
|
||||
);
|
||||
|
||||
const pillBox = await trigger.boundingBox();
|
||||
const rowBox = await row.boundingBox();
|
||||
expect(pillBox?.width ?? Number.POSITIVE_INFINITY).toBeLessThanOrEqual(
|
||||
rowBox?.width ?? 0,
|
||||
);
|
||||
});
|
||||
|
||||
test("members sidebar exposes view-activity for a viewer-owned relay agent", async ({
|
||||
page,
|
||||
}) => {
|
||||
|
||||
@@ -30,12 +30,6 @@
|
||||
"name": "Agent-managed profiles",
|
||||
"description": "Let agents manage their own relay name and avatar instead of restoring the desktop copy",
|
||||
"platforms": ["desktop"]
|
||||
},
|
||||
{
|
||||
"id": "composerLiveActivity",
|
||||
"name": "Composer live activity",
|
||||
"description": "Live activity preview for the selected working agent in the agents-working popover below the chat composer",
|
||||
"platforms": ["desktop"]
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user