feat(story-85): agent lozenges move between roster and work items

- Add `hiddenRosterAgents: ReadonlySet<string>` to LozengeFlyContext:
  - Derived from pipeline: any agent currently assigned to a work item
  - `flyingOutAgents` state keeps badge hidden for 500 ms during the
    fly-out animation so the returning clone lands before the badge reappears
  - Union of both sets exposed as `hiddenRosterAgents` in context
- Update AgentPanel: wrap each RosterBadge in a collapsing div
  controlled by `hiddenRosterAgents`. The div transitions max-width
  0→300px / opacity 0→1 so the roster gap closes/opens smoothly.
- Add tests covering:
  - `hiddenRosterAgents` is empty when no agents are assigned
  - Badge hidden immediately when agent appears in pipeline
  - Badge hidden during fly-out (0–499 ms) and visible after (≥500 ms)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Dave
2026-02-23 19:52:23 +00:00
parent a91d663894
commit 6c1f8555e8
3 changed files with 301 additions and 7 deletions

View File

@@ -121,6 +121,7 @@ function agentKey(storyId: string, agentName: string): string {
}
export function AgentPanel() {
const { hiddenRosterAgents } = useLozengeFly();
const [agents, setAgents] = useState<Record<string, AgentState>>({});
const [roster, setRoster] = useState<AgentConfigInfo[]>([]);
const [actionError, setActionError] = useState<string | null>(null);
@@ -412,12 +413,22 @@ export function AgentPanel() {
const activeStoryId = activeEntry
? activeEntry[0].split(":")[0]
: null;
const isHidden = hiddenRosterAgents.has(a.name);
return (
<RosterBadge
key={`roster-${a.name}`}
agent={a}
activeStoryId={activeStoryId}
/>
// Collapsing wrapper: smoothly shrinks when agent departs
// to a work item and expands when it returns.
<div
key={`roster-wrapper-${a.name}`}
data-testid={`roster-badge-wrapper-${a.name}`}
style={{
overflow: "hidden",
maxWidth: isHidden ? "0" : "300px",
opacity: isHidden ? 0 : 1,
transition: "max-width 0.35s ease, opacity 0.2s ease",
}}
>
<RosterBadge agent={a} activeStoryId={activeStoryId} />
</div>
);
})}
</div>