feat: FLIP-style lozenge fly animation when agents are assigned to stories

Implements Story 74: agent lozenges now animate as fixed-position overlays
that fly from the roster badge in AgentPanel to the story slot in StagePanel
(and back when the agent is removed), satisfying all acceptance criteria.

Key changes:
- LozengeFlyContext.tsx (new): coordinates FLIP animations via React context.
  LozengeFlyProvider tracks pipeline changes, hides slot lozenges during
  fly-in (useLayoutEffect before paint), then creates a portal-rendered
  fixed-position clone that transitions from roster → slot (or reverse).
  z-index 9999 ensures the clone travels above all other UI elements.
- AgentPanel.tsx: RosterBadge registers its DOM element with the context
  so fly animations know the correct start/end coordinates.
- StagePanel.tsx: AgentLozenge registers its DOMRect on every render via
  useLayoutEffect (for fly-out) and reads pendingFlyIns to stay hidden
  while a fly-in clone is in flight. Added align-self: flex-start so the
  lozenge maintains its intrinsic width and never stretches in the panel.
- Chat.tsx: right-column panels wrapped in LozengeFlyProvider.
- LozengeFlyContext.test.tsx (new): 10 tests covering fixed width,
  fly-in/fly-out clone creation, portal placement, opacity lifecycle,
  and idle vs active visual distinction.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Dave
2026-02-23 15:04:10 +00:00
parent d5efbc0a53
commit ef728331cf
5 changed files with 915 additions and 11 deletions

View File

@@ -7,6 +7,7 @@ import { api, ChatWebSocket } from "../api/client";
import type { Message, ProviderConfig, ToolCall } from "../types";
import { AgentPanel } from "./AgentPanel";
import { ChatHeader } from "./ChatHeader";
import { LozengeFlyProvider } from "./LozengeFlyContext";
import { StagePanel } from "./StagePanel";
const { useCallback, useEffect, useRef, useState } = React;
@@ -713,12 +714,14 @@ export function Chat({ projectPath, onCloseProject }: ChatProps) {
gap: "12px",
}}
>
<AgentPanel />
<LozengeFlyProvider pipeline={pipeline}>
<AgentPanel />
<StagePanel title="To Merge" items={pipeline.merge} />
<StagePanel title="QA" items={pipeline.qa} />
<StagePanel title="Current" items={pipeline.current} />
<StagePanel title="Upcoming" items={pipeline.upcoming} />
<StagePanel title="To Merge" items={pipeline.merge} />
<StagePanel title="QA" items={pipeline.qa} />
<StagePanel title="Current" items={pipeline.current} />
<StagePanel title="Upcoming" items={pipeline.upcoming} />
</LozengeFlyProvider>
</div>
</div>